Skip to content

Commit

Permalink
feat: parse well known date strings (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Nov 26, 2024
1 parent c666d3f commit 3e1b974
Show file tree
Hide file tree
Showing 9 changed files with 911 additions and 49 deletions.
140 changes: 140 additions & 0 deletions src/dateTime/__tests__/regexParse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {dateTime} from '../dateTime';

test('DateTime from ISO parses as local by default', () => {
const dt = dateTime({input: '2016-05-25T09:08:34.123'});
expect([
dt.year(),
dt.month(),
dt.date(),
dt.hour(),
dt.minute(),
dt.second(),
dt.millisecond(),
]).toEqual([2016, 4, 25, 9, 8, 34, 123]);
});

test('DateTime from ISO uses the offset provided, but keeps the dateTime as local', () => {
const dt = dateTime({input: '2016-05-25T09:08:34.123+06:00'}).utc();
expect([
dt.year(),
dt.month(),
dt.date(),
dt.hour(),
dt.minute(),
dt.second(),
dt.millisecond(),
]).toEqual([2016, 4, 25, 3, 8, 34, 123]);
});

test('DateTime from ISO uses the Z if provided, but keeps the dateTime as local', () => {
const dt = dateTime({input: '2016-05-25T09:08:34.123Z'}).utc();
expect([
dt.year(),
dt.month(),
dt.date(),
dt.hour(),
dt.minute(),
dt.second(),
dt.millisecond(),
]).toEqual([2016, 4, 25, 9, 8, 34, 123]);
});

test.each<[string, [number, number, number, number, number, number, number]]>([
['2016', [2016, 0, 1, 0, 0, 0, 0]],
['2016-05', [2016, 4, 1, 0, 0, 0, 0]],
['201605', [2016, 4, 1, 0, 0, 0, 0]],
['2016-05-25', [2016, 4, 25, 0, 0, 0, 0]],
['20160525', [2016, 4, 25, 0, 0, 0, 0]],
['+002016-05-25', [2016, 4, 25, 0, 0, 0, 0]],
['-002016-05-25', [-2016, 4, 25, 0, 0, 0, 0]],
['2016-05-25T09', [2016, 4, 25, 9, 0, 0, 0]],
['2016-05-25T09:08', [2016, 4, 25, 9, 8, 0, 0]],
['2016-05-25T0908', [2016, 4, 25, 9, 8, 0, 0]],
['2016-05-25T09:08:34', [2016, 4, 25, 9, 8, 34, 0]],
['2016-05-25T090834', [2016, 4, 25, 9, 8, 34, 0]],
['2016-05-25T09:08:34.123', [2016, 4, 25, 9, 8, 34, 123]],
['2016-05-25T09:08:34.123999', [2016, 4, 25, 9, 8, 34, 123]],
['2016-05-25T09:08:34,123', [2016, 4, 25, 9, 8, 34, 123]],
['2016-05-25T090834.123', [2016, 4, 25, 9, 8, 34, 123]],
['2016-05-25T09:08:34.023', [2016, 4, 25, 9, 8, 34, 23]],
['2016-05-25T09:08:34.99999', [2016, 4, 25, 9, 8, 34, 999]],
['2016-05-25T09:08:34.1', [2016, 4, 25, 9, 8, 34, 100]],
['2016-W21', [2016, 4, 16, 0, 0, 0, 0]],
['2016-W21-3', [2016, 4, 18, 0, 0, 0, 0]],
['2016W213', [2016, 4, 18, 0, 0, 0, 0]],
['2016-W21-3T09:24:15.123', [2016, 4, 18, 9, 24, 15, 123]],
['2016W213T09:24:15.123', [2016, 4, 18, 9, 24, 15, 123]],
['2016-200', [2016, 6, 18, 0, 0, 0, 0]],
['2016200', [2016, 6, 18, 0, 0, 0, 0]],
['2016-200T09:24:15.123', [2016, 6, 18, 9, 24, 15, 123]],
['2016200T09:24:15.123', [2016, 6, 18, 9, 24, 15, 123]],
['2016-002', [2016, 0, 2, 0, 0, 0, 0]],
['2018-01-04T24:00', [2018, 0, 5, 0, 0, 0, 0]],
])('DateTime from ISO (%p)', (input, expected) => {
const dt = dateTime({input});
expect([
dt.year(),
dt.month(),
dt.date(),
dt.hour(),
dt.minute(),
dt.second(),
dt.millisecond(),
]).toEqual(expected);
});

test("DateTime from ISO doesn't accept 24:23", () => {
expect(dateTime({input: '2018-05-25T24:23'}).isValid()).toBe(false);
});

test.each<[string, [number, number, number, number]]>([
['09:24:15.123', [9, 24, 15, 123]],
['09:24:15,123', [9, 24, 15, 123]],
['09:24:15', [9, 24, 15, 0]],
['09:24', [9, 24, 0, 0]],
])('DateTime from ISO time (%p)', (input, expected) => {
const dt = dateTime({input});
const now = dateTime();
expect([
dt.year(),
dt.month(),
dt.date(),
dt.hour(),
dt.minute(),
dt.second(),
dt.millisecond(),
]).toEqual([now.year(), now.month(), now.date(), ...expected]);
});

test.each<[string, [number, number, number, number, number, number]]>([
['Sun, 12 Apr 2015 05:06:07 GMT', [2015, 3, 12, 5, 6, 7]],
['Tue, 01 Nov 2016 01:23:45 +0000', [2016, 10, 1, 1, 23, 45]],
['Tue, 01 Nov 16 04:23:45 Z', [2016, 10, 1, 4, 23, 45]],
['01 Nov 2016 05:23:45 z', [2016, 10, 1, 5, 23, 45]],
['01 Nov 2016 13:23 +0600', [2016, 10, 1, 7, 23, 0]],
['Mon, 02 Jan 2017 06:00:00 -0800', [2017, 0, 2, 6 + 8, 0, 0]],
['Mon, 02 Jan 2017 06:00:00 +0800', [2017, 0, 1, 22, 0, 0]],
['Mon, 02 Jan 2017 06:00:00 +0330', [2017, 0, 2, 2, 30, 0]],
['Mon, 02 Jan 2017 06:00:00 -0330', [2017, 0, 2, 9, 30, 0]],
['Mon, 02 Jan 2017 06:00:00 PST', [2017, 0, 2, 6 + 8, 0, 0]],
['Mon, 02 Jan 2017 06:00:00 PDT', [2017, 0, 2, 6 + 7, 0, 0]],
])('DateTime from RFC2822 (%p)', (input, expected) => {
const dt = dateTime({input}).utc();
expect([dt.year(), dt.month(), dt.date(), dt.hour(), dt.minute(), dt.second()]).toEqual(
expected,
);
});

test.each<[string, [number, number, number, number, number, number]]>([
['Fri, 19 Nov 82 16:14:55 GMT', [1982, 10, 19, 16, 14, 55]],
['Sun, 06 Nov 1994 08:49:37 GMT', [1994, 10, 6, 8, 49, 37]],
['Sunday, 06-Nov-94 08:49:37 GMT', [1994, 10, 6, 8, 49, 37]],
['Wednesday, 29-Jun-22 08:49:37 GMT', [2022, 5, 29, 8, 49, 37]],
['Sun Nov 6 08:49:37 1994', [1994, 10, 6, 8, 49, 37]],
['Wed Nov 16 08:49:37 1994', [1994, 10, 16, 8, 49, 37]],
])('DateTime from HTTP (%p)', (input, expected) => {
const dt = dateTime({input}).utc();
expect([dt.year(), dt.month(), dt.date(), dt.hour(), dt.minute(), dt.second()]).toEqual(
expected,
);
});
84 changes: 59 additions & 25 deletions src/dateTime/dateTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import {
weeksInWeekYear,
} from '../utils';
import type {DateObject} from '../utils';
import {getLocaleData, getLocaleWeekValues} from '../utils/locale';

import {formatDate} from './format';
import {getTimestampFromArray, getTimestampFromObject} from './parse';
import {parseDateString} from './regexParse';
import {fromTo} from './relative';

const IS_DATE_TIME = Symbol('isDateTime');
Expand Down Expand Up @@ -281,15 +283,15 @@ class DateTimeImpl implements DateTime {
}

isSame(input?: DateTimeInput, granularity?: DurationUnit): boolean {
const [ts] = getTimestamp(input, 'system');
const [ts] = getTimestamp(input, 'system', this._locale);
if (!this.isValid() || isNaN(ts)) {
return false;
}
return !this.isBefore(ts, granularity) && !this.isAfter(ts, granularity);
}

isBefore(input?: DateTimeInput, granularity?: DurationUnit): boolean {
const [ts] = getTimestamp(input, 'system');
const [ts] = getTimestamp(input, 'system', this._locale);
if (!this.isValid() || isNaN(ts)) {
return false;
}
Expand All @@ -299,7 +301,7 @@ class DateTimeImpl implements DateTime {
}

isAfter(input?: DateTimeInput, granularity?: DurationUnit): boolean {
const [ts] = getTimestamp(input, 'system');
const [ts] = getTimestamp(input, 'system', this._locale);
if (!this.isValid() || isNaN(ts)) {
return false;
}
Expand All @@ -323,7 +325,7 @@ class DateTimeImpl implements DateTime {
const value = DateTimeImpl.isDateTime(amount)
? amount.timeZone(this._timeZone)
: createDateTime({
ts: getTimestamp(amount, 'system')[0],
ts: getTimestamp(amount, 'system', this._locale)[0],
timeZone: this._timeZone,
locale: this._locale,
offset: this._offset,
Expand Down Expand Up @@ -772,11 +774,6 @@ class DateTimeImpl implements DateTime {
}
}

function getLocaleWeekValues(localeData: {yearStart?: number; weekStart?: number}) {
const {weekStart, yearStart} = localeData;
return {startOfWeek: weekStart || 7, minDaysInFirstWeek: yearStart || 1};
}

function absRound(v: number) {
const sign = Math.sign(v);
return Math.round(sign * v) * sign;
Expand Down Expand Up @@ -817,20 +814,27 @@ function createDateTime({
locale: string;
}): DateTime {
const loc = locale || 'en';
const localeData = dayjs.Ls[loc] as Locale;
const localeData = getLocaleData(loc);
const isValid = !isNaN(Number(new Date(ts)));
return new DateTimeImpl({ts, timeZone, offset, locale: loc, localeData, isValid});
return new DateTimeImpl({
ts,
timeZone,
offset,
locale: loc,
localeData,
isValid,
});
}

function getTimestamp(
input: DateTimeInput,
timezone: string,
locale: string,
format?: string,
lang?: string,
utc = false,
fixedOffset?: number,
): [ts: number, offset: number] {
let ts: number;
let offset: number | undefined;
let offset = fixedOffset;
if (
isDateTime(input) ||
typeof input === 'number' ||
Expand All @@ -841,18 +845,35 @@ function getTimestamp(
} else if (input === null || input === undefined) {
ts = Date.now();
} else if (Array.isArray(input)) {
[ts, offset] = getTimestampFromArray(input, timezone);
[ts, offset] = getTimestampFromArray(input, timezone, fixedOffset);
} else if (typeof input === 'object') {
[ts, offset] = getTimestampFromObject(input, timezone);
} else if (utc) {
ts = dayjs.utc(input, format, STRICT).valueOf();
} else {
const locale = dayjs.locale(lang || settings.getLocale(), undefined, true);
[ts, offset] = getTimestampFromObject(input, timezone, fixedOffset);
} else if (format === undefined) {
const [dateObject, timezoneOrOffset] = parseDateString(input);
if (Object.keys(dateObject).length === 0) {
return [NaN, NaN];
}
[ts] = getTimestampFromObject(
dateObject,
typeof timezoneOrOffset === 'string' ? timezoneOrOffset : 'system',
typeof timezoneOrOffset === 'number' ? timezoneOrOffset : fixedOffset,
);
if (
fixedOffset !== undefined &&
timezoneOrOffset !== null &&
timezoneOrOffset !== fixedOffset
) {
ts -= fixedOffset * 60 * 1000;
}
} else if (fixedOffset === undefined) {
const localDate = format
? dayjs(input, format, locale, STRICT)
: dayjs(input, undefined, locale);

ts = localDate.valueOf();
} else {
ts = dayjs.utc(input, format, STRICT).valueOf();
ts -= fixedOffset * 60 * 1000;
}

offset = offset ?? timeZoneOffset(timezone, ts);
Expand Down Expand Up @@ -886,7 +907,7 @@ export function dateTime(opt?: {
const timeZoneOrDefault = normalizeTimeZone(timeZone, settings.getDefaultTimeZone());
const locale = dayjs.locale(lang || settings.getLocale(), undefined, true);

const [ts, offset] = getTimestamp(input, timeZoneOrDefault, format, lang);
const [ts, offset] = getTimestamp(input, timeZoneOrDefault, locale, format);

const date = createDateTime({
ts,
Expand All @@ -898,17 +919,30 @@ export function dateTime(opt?: {
return date;
}

export function dateTimeUtc(opt?: {input?: DateTimeInput; format?: FormatInput; lang?: string}) {
const {input, format, lang} = opt || {};
/**
* Creates a DateTime instance with fixed offset.
* @param [opt]
* @param {DateTimeInput=} [opt.input] - input to parse.
* @param {string=} [opt.format] - strict {@link https://dayjs.gitee.io/docs/en/display/format format} for parsing user's input.
* @param {number=} [opt.offset=0] - specified offset.
* @param {string=} [opt.lang] - specified locale.
*/
export function dateTimeUtc(opt?: {
input?: DateTimeInput;
format?: FormatInput;
lang?: string;
offset?: number;
}): DateTime {
const {input, format, lang, offset = 0} = opt || {};

const locale = dayjs.locale(lang || settings.getLocale(), undefined, true);

const [ts] = getTimestamp(input, UtcTimeZone, format, lang, true);
const [ts] = getTimestamp(input, UtcTimeZone, locale, format, offset);

const date = createDateTime({
ts,
timeZone: UtcTimeZone,
offset: 0,
offset,
locale,
});

Expand Down
18 changes: 18 additions & 0 deletions src/dateTime/dateTimeUtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,23 @@ describe('DateTimeUtc', () => {
const date = dateTimeUtc({input, format}).toISOString();
expect(date).toEqual(expected);
});

test.each<[string, string]>([
['2023-12-31', '2023-12-31T00:00:00.000+02:30'],
['2023-12-31T01:00', '2023-12-31T01:00:00.000+02:30'],
['2023-12-31T01:00Z', '2023-12-31T01:00:00.000+02:30'],
['2023-12-31T03:00+02:00', '2023-12-31T01:00:00.000+02:30'],
])('input option (%p) with offset', (input, expected) => {
const date = dateTimeUtc({input, offset: 150}).toISOString(true);
expect(date).toEqual(expected);
});

test.each<[string, string, string]>([
['31.12.2023', 'DD.MM.YYYY', '2023-12-31T00:00:00.000+02:30'],
['31.12.2023 01:00', 'DD.MM.YYYY HH:mm', '2023-12-31T01:00:00.000+02:30'],
])('input (%p) format (%p) with offset', (input, format, expected) => {
const date = dateTimeUtc({input, format, offset: 150}).toISOString(true);
expect(date).toEqual(expected);
});
});
});
Loading

0 comments on commit 3e1b974

Please sign in to comment.