parseISO.js 7.9 KB

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