index.js 8.2 KB

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