Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parse well known date strings #81

Merged
merged 2 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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