You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

285 lines
8.7 KiB

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