parseISO.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import {
  2. millisecondsInHour,
  3. millisecondsInMinute,
  4. } from "./constants.js";
  5. import { constructFrom } from "./constructFrom.js";
  6. import { toDate } from "./toDate.js";
  7. /**
  8. * The {@link parseISO} function options.
  9. */
  10. /**
  11. * @name parseISO
  12. * @category Common Helpers
  13. * @summary Parse ISO string
  14. *
  15. * @description
  16. * Parse the given string in ISO 8601 format and return an instance of Date.
  17. *
  18. * Function accepts complete ISO 8601 formats as well as partial implementations.
  19. * ISO 8601: http://en.wikipedia.org/wiki/ISO_8601
  20. *
  21. * If the argument isn't a string, the function cannot parse the string or
  22. * the values are invalid, it returns Invalid Date.
  23. *
  24. * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).
  25. * @typeParam ResultDate - The result `Date` type, it is the type returned from the context function if it is passed, or inferred from the arguments.
  26. *
  27. * @param argument - The value to convert
  28. * @param options - An object with options
  29. *
  30. * @returns The parsed date in the local time zone
  31. *
  32. * @example
  33. * // Convert string '2014-02-11T11:30:30' to date:
  34. * const result = parseISO('2014-02-11T11:30:30')
  35. * //=> Tue Feb 11 2014 11:30:30
  36. *
  37. * @example
  38. * // Convert string '+02014101' to date,
  39. * // if the additional number of digits in the extended year format is 1:
  40. * const result = parseISO('+02014101', { additionalDigits: 1 })
  41. * //=> Fri Apr 11 2014 00:00:00
  42. */
  43. export function parseISO(argument, options) {
  44. const invalidDate = () => constructFrom(options?.in, NaN);
  45. const additionalDigits = options?.additionalDigits ?? 2;
  46. const dateStrings = splitDateString(argument);
  47. let date;
  48. if (dateStrings.date) {
  49. const parseYearResult = parseYear(dateStrings.date, additionalDigits);
  50. date = parseDate(parseYearResult.restDateString, parseYearResult.year);
  51. }
  52. if (!date || isNaN(+date)) return invalidDate();
  53. const timestamp = +date;
  54. let time = 0;
  55. let offset;
  56. if (dateStrings.time) {
  57. time = parseTime(dateStrings.time);
  58. if (isNaN(time)) return invalidDate();
  59. }
  60. if (dateStrings.timezone) {
  61. offset = parseTimezone(dateStrings.timezone);
  62. if (isNaN(offset)) return invalidDate();
  63. } else {
  64. const tmpDate = new Date(timestamp + time);
  65. const result = toDate(0, options?.in);
  66. result.setFullYear(
  67. tmpDate.getUTCFullYear(),
  68. tmpDate.getUTCMonth(),
  69. tmpDate.getUTCDate(),
  70. );
  71. result.setHours(
  72. tmpDate.getUTCHours(),
  73. tmpDate.getUTCMinutes(),
  74. tmpDate.getUTCSeconds(),
  75. tmpDate.getUTCMilliseconds(),
  76. );
  77. return result;
  78. }
  79. return toDate(timestamp + time + offset, options?.in);
  80. }
  81. const patterns = {
  82. dateTimeDelimiter: /[T ]/,
  83. timeZoneDelimiter: /[Z ]/i,
  84. timezone: /([Z+-].*)$/,
  85. };
  86. const dateRegex =
  87. /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/;
  88. const timeRegex =
  89. /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/;
  90. const timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/;
  91. function splitDateString(dateString) {
  92. const dateStrings = {};
  93. const array = dateString.split(patterns.dateTimeDelimiter);
  94. let timeString;
  95. // The regex match should only return at maximum two array elements.
  96. // [date], [time], or [date, time].
  97. if (array.length > 2) {
  98. return dateStrings;
  99. }
  100. if (/:/.test(array[0])) {
  101. timeString = array[0];
  102. } else {
  103. dateStrings.date = array[0];
  104. timeString = array[1];
  105. if (patterns.timeZoneDelimiter.test(dateStrings.date)) {
  106. dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0];
  107. timeString = dateString.substr(
  108. dateStrings.date.length,
  109. dateString.length,
  110. );
  111. }
  112. }
  113. if (timeString) {
  114. const token = patterns.timezone.exec(timeString);
  115. if (token) {
  116. dateStrings.time = timeString.replace(token[1], "");
  117. dateStrings.timezone = token[1];
  118. } else {
  119. dateStrings.time = timeString;
  120. }
  121. }
  122. return dateStrings;
  123. }
  124. function parseYear(dateString, additionalDigits) {
  125. const regex = new RegExp(
  126. "^(?:(\\d{4}|[+-]\\d{" +
  127. (4 + additionalDigits) +
  128. "})|(\\d{2}|[+-]\\d{" +
  129. (2 + additionalDigits) +
  130. "})$)",
  131. );
  132. const captures = dateString.match(regex);
  133. // Invalid ISO-formatted year
  134. if (!captures) return { year: NaN, restDateString: "" };
  135. const year = captures[1] ? parseInt(captures[1]) : null;
  136. const century = captures[2] ? parseInt(captures[2]) : null;
  137. // either year or century is null, not both
  138. return {
  139. year: century === null ? year : century * 100,
  140. restDateString: dateString.slice((captures[1] || captures[2]).length),
  141. };
  142. }
  143. function parseDate(dateString, year) {
  144. // Invalid ISO-formatted year
  145. if (year === null) return new Date(NaN);
  146. const captures = dateString.match(dateRegex);
  147. // Invalid ISO-formatted string
  148. if (!captures) return new Date(NaN);
  149. const isWeekDate = !!captures[4];
  150. const dayOfYear = parseDateUnit(captures[1]);
  151. const month = parseDateUnit(captures[2]) - 1;
  152. const day = parseDateUnit(captures[3]);
  153. const week = parseDateUnit(captures[4]);
  154. const dayOfWeek = parseDateUnit(captures[5]) - 1;
  155. if (isWeekDate) {
  156. if (!validateWeekDate(year, week, dayOfWeek)) {
  157. return new Date(NaN);
  158. }
  159. return dayOfISOWeekYear(year, week, dayOfWeek);
  160. } else {
  161. const date = new Date(0);
  162. if (
  163. !validateDate(year, month, day) ||
  164. !validateDayOfYearDate(year, dayOfYear)
  165. ) {
  166. return new Date(NaN);
  167. }
  168. date.setUTCFullYear(year, month, Math.max(dayOfYear, day));
  169. return date;
  170. }
  171. }
  172. function parseDateUnit(value) {
  173. return value ? parseInt(value) : 1;
  174. }
  175. function parseTime(timeString) {
  176. const captures = timeString.match(timeRegex);
  177. if (!captures) return NaN; // Invalid ISO-formatted time
  178. const hours = parseTimeUnit(captures[1]);
  179. const minutes = parseTimeUnit(captures[2]);
  180. const seconds = parseTimeUnit(captures[3]);
  181. if (!validateTime(hours, minutes, seconds)) {
  182. return NaN;
  183. }
  184. return (
  185. hours * millisecondsInHour + minutes * millisecondsInMinute + seconds * 1000
  186. );
  187. }
  188. function parseTimeUnit(value) {
  189. return (value && parseFloat(value.replace(",", "."))) || 0;
  190. }
  191. function parseTimezone(timezoneString) {
  192. if (timezoneString === "Z") return 0;
  193. const captures = timezoneString.match(timezoneRegex);
  194. if (!captures) return 0;
  195. const sign = captures[1] === "+" ? -1 : 1;
  196. const hours = parseInt(captures[2]);
  197. const minutes = (captures[3] && parseInt(captures[3])) || 0;
  198. if (!validateTimezone(hours, minutes)) {
  199. return NaN;
  200. }
  201. return sign * (hours * millisecondsInHour + minutes * millisecondsInMinute);
  202. }
  203. function dayOfISOWeekYear(isoWeekYear, week, day) {
  204. const date = new Date(0);
  205. date.setUTCFullYear(isoWeekYear, 0, 4);
  206. const fourthOfJanuaryDay = date.getUTCDay() || 7;
  207. const diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay;
  208. date.setUTCDate(date.getUTCDate() + diff);
  209. return date;
  210. }
  211. // Validation functions
  212. // February is null to handle the leap year (using ||)
  213. const daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  214. function isLeapYearIndex(year) {
  215. return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
  216. }
  217. function validateDate(year, month, date) {
  218. return (
  219. month >= 0 &&
  220. month <= 11 &&
  221. date >= 1 &&
  222. date <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28))
  223. );
  224. }
  225. function validateDayOfYearDate(year, dayOfYear) {
  226. return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365);
  227. }
  228. function validateWeekDate(_year, week, day) {
  229. return week >= 1 && week <= 53 && day >= 0 && day <= 6;
  230. }
  231. function validateTime(hours, minutes, seconds) {
  232. if (hours === 24) {
  233. return minutes === 0 && seconds === 0;
  234. }
  235. return (
  236. seconds >= 0 &&
  237. seconds < 60 &&
  238. minutes >= 0 &&
  239. minutes < 60 &&
  240. hours >= 0 &&
  241. hours < 25
  242. );
  243. }
  244. function validateTimezone(_hours, minutes) {
  245. return minutes >= 0 && minutes <= 59;
  246. }
  247. // Fallback for modularized imports:
  248. export default parseISO;