123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- import {
- millisecondsInHour,
- millisecondsInMinute,
- } from "./constants.js";
- import { constructFrom } from "./constructFrom.js";
- import { toDate } from "./toDate.js";
- /**
- * The {@link parseISO} function options.
- */
- /**
- * @name parseISO
- * @category Common Helpers
- * @summary Parse ISO string
- *
- * @description
- * Parse the given string in ISO 8601 format and return an instance of Date.
- *
- * Function accepts complete ISO 8601 formats as well as partial implementations.
- * ISO 8601: http://en.wikipedia.org/wiki/ISO_8601
- *
- * If the argument isn't a string, the function cannot parse the string or
- * the values are invalid, it returns Invalid Date.
- *
- * @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).
- * @typeParam ResultDate - The result `Date` type, it is the type returned from the context function if it is passed, or inferred from the arguments.
- *
- * @param argument - The value to convert
- * @param options - An object with options
- *
- * @returns The parsed date in the local time zone
- *
- * @example
- * // Convert string '2014-02-11T11:30:30' to date:
- * const result = parseISO('2014-02-11T11:30:30')
- * //=> Tue Feb 11 2014 11:30:30
- *
- * @example
- * // Convert string '+02014101' to date,
- * // if the additional number of digits in the extended year format is 1:
- * const result = parseISO('+02014101', { additionalDigits: 1 })
- * //=> Fri Apr 11 2014 00:00:00
- */
- export function parseISO(argument, options) {
- const invalidDate = () => constructFrom(options?.in, NaN);
- const additionalDigits = options?.additionalDigits ?? 2;
- const dateStrings = splitDateString(argument);
- let date;
- if (dateStrings.date) {
- const parseYearResult = parseYear(dateStrings.date, additionalDigits);
- date = parseDate(parseYearResult.restDateString, parseYearResult.year);
- }
- if (!date || isNaN(+date)) return invalidDate();
- const timestamp = +date;
- let time = 0;
- let offset;
- if (dateStrings.time) {
- time = parseTime(dateStrings.time);
- if (isNaN(time)) return invalidDate();
- }
- if (dateStrings.timezone) {
- offset = parseTimezone(dateStrings.timezone);
- if (isNaN(offset)) return invalidDate();
- } else {
- const tmpDate = new Date(timestamp + time);
- const result = toDate(0, options?.in);
- result.setFullYear(
- tmpDate.getUTCFullYear(),
- tmpDate.getUTCMonth(),
- tmpDate.getUTCDate(),
- );
- result.setHours(
- tmpDate.getUTCHours(),
- tmpDate.getUTCMinutes(),
- tmpDate.getUTCSeconds(),
- tmpDate.getUTCMilliseconds(),
- );
- return result;
- }
- return toDate(timestamp + time + offset, options?.in);
- }
- const patterns = {
- dateTimeDelimiter: /[T ]/,
- timeZoneDelimiter: /[Z ]/i,
- timezone: /([Z+-].*)$/,
- };
- const dateRegex =
- /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/;
- const timeRegex =
- /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/;
- const timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/;
- function splitDateString(dateString) {
- const dateStrings = {};
- const array = dateString.split(patterns.dateTimeDelimiter);
- let timeString;
- // The regex match should only return at maximum two array elements.
- // [date], [time], or [date, time].
- if (array.length > 2) {
- return dateStrings;
- }
- if (/:/.test(array[0])) {
- timeString = array[0];
- } else {
- dateStrings.date = array[0];
- timeString = array[1];
- if (patterns.timeZoneDelimiter.test(dateStrings.date)) {
- dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0];
- timeString = dateString.substr(
- dateStrings.date.length,
- dateString.length,
- );
- }
- }
- if (timeString) {
- const token = patterns.timezone.exec(timeString);
- if (token) {
- dateStrings.time = timeString.replace(token[1], "");
- dateStrings.timezone = token[1];
- } else {
- dateStrings.time = timeString;
- }
- }
- return dateStrings;
- }
- function parseYear(dateString, additionalDigits) {
- const regex = new RegExp(
- "^(?:(\\d{4}|[+-]\\d{" +
- (4 + additionalDigits) +
- "})|(\\d{2}|[+-]\\d{" +
- (2 + additionalDigits) +
- "})$)",
- );
- const captures = dateString.match(regex);
- // Invalid ISO-formatted year
- if (!captures) return { year: NaN, restDateString: "" };
- const year = captures[1] ? parseInt(captures[1]) : null;
- const century = captures[2] ? parseInt(captures[2]) : null;
- // either year or century is null, not both
- return {
- year: century === null ? year : century * 100,
- restDateString: dateString.slice((captures[1] || captures[2]).length),
- };
- }
- function parseDate(dateString, year) {
- // Invalid ISO-formatted year
- if (year === null) return new Date(NaN);
- const captures = dateString.match(dateRegex);
- // Invalid ISO-formatted string
- if (!captures) return new Date(NaN);
- const isWeekDate = !!captures[4];
- const dayOfYear = parseDateUnit(captures[1]);
- const month = parseDateUnit(captures[2]) - 1;
- const day = parseDateUnit(captures[3]);
- const week = parseDateUnit(captures[4]);
- const dayOfWeek = parseDateUnit(captures[5]) - 1;
- if (isWeekDate) {
- if (!validateWeekDate(year, week, dayOfWeek)) {
- return new Date(NaN);
- }
- return dayOfISOWeekYear(year, week, dayOfWeek);
- } else {
- const date = new Date(0);
- if (
- !validateDate(year, month, day) ||
- !validateDayOfYearDate(year, dayOfYear)
- ) {
- return new Date(NaN);
- }
- date.setUTCFullYear(year, month, Math.max(dayOfYear, day));
- return date;
- }
- }
- function parseDateUnit(value) {
- return value ? parseInt(value) : 1;
- }
- function parseTime(timeString) {
- const captures = timeString.match(timeRegex);
- if (!captures) return NaN; // Invalid ISO-formatted time
- const hours = parseTimeUnit(captures[1]);
- const minutes = parseTimeUnit(captures[2]);
- const seconds = parseTimeUnit(captures[3]);
- if (!validateTime(hours, minutes, seconds)) {
- return NaN;
- }
- return (
- hours * millisecondsInHour + minutes * millisecondsInMinute + seconds * 1000
- );
- }
- function parseTimeUnit(value) {
- return (value && parseFloat(value.replace(",", "."))) || 0;
- }
- function parseTimezone(timezoneString) {
- if (timezoneString === "Z") return 0;
- const captures = timezoneString.match(timezoneRegex);
- if (!captures) return 0;
- const sign = captures[1] === "+" ? -1 : 1;
- const hours = parseInt(captures[2]);
- const minutes = (captures[3] && parseInt(captures[3])) || 0;
- if (!validateTimezone(hours, minutes)) {
- return NaN;
- }
- return sign * (hours * millisecondsInHour + minutes * millisecondsInMinute);
- }
- function dayOfISOWeekYear(isoWeekYear, week, day) {
- const date = new Date(0);
- date.setUTCFullYear(isoWeekYear, 0, 4);
- const fourthOfJanuaryDay = date.getUTCDay() || 7;
- const diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay;
- date.setUTCDate(date.getUTCDate() + diff);
- return date;
- }
- // Validation functions
- // February is null to handle the leap year (using ||)
- const daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
- function isLeapYearIndex(year) {
- return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
- }
- function validateDate(year, month, date) {
- return (
- month >= 0 &&
- month <= 11 &&
- date >= 1 &&
- date <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28))
- );
- }
- function validateDayOfYearDate(year, dayOfYear) {
- return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365);
- }
- function validateWeekDate(_year, week, day) {
- return week >= 1 && week <= 53 && day >= 0 && day <= 6;
- }
- function validateTime(hours, minutes, seconds) {
- if (hours === 24) {
- return minutes === 0 && seconds === 0;
- }
- return (
- seconds >= 0 &&
- seconds < 60 &&
- minutes >= 0 &&
- minutes < 60 &&
- hours >= 0 &&
- hours < 25
- );
- }
- function validateTimezone(_hours, minutes) {
- return minutes >= 0 && minutes <= 59;
- }
- // Fallback for modularized imports:
- export default parseISO;
|