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

refactor(parse-iso-date): Convert to TypeScript #2080

Merged
merged 3 commits into from
Aug 17, 2023
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
5 changes: 2 additions & 3 deletions src/date.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @ts-ignore
import isoParse from './util/isodate';
import { parseIsoDate } from './util/parseIsoDate';
import { date as locale } from './locale';
import Ref from './Reference';
import type { AnyObject, DefaultThunk, Message } from './types';
Expand Down Expand Up @@ -51,7 +50,7 @@ export default class DateSchema<
if (!ctx.spec.coerce || ctx.isType(value) || value === null)
return value;

value = isoParse(value);
value = parseIsoDate(value);

// 0 is a valid timestamp equivalent to 1970-01-01T00:00:00Z(unix epoch) or before.
return !isNaN(value) ? new Date(value) : DateSchema.INVALID_DATE;
Expand Down
64 changes: 0 additions & 64 deletions src/util/isodate.js

This file was deleted.

68 changes: 68 additions & 0 deletions src/util/parseIsoDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* This file is a modified version of the file from the following repository:
* Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
* NON-CONFORMANT EDITION.
* © 2011 Colin Snover <http://zetafleet.com>
* Released under MIT license.
*/

// prettier-ignore
// 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm
const isoReg = /^(\d{4}|[+-]\d{6})(?:-?(\d{2})(?:-?(\d{2}))?)?(?:[ T]?(\d{2}):?(\d{2})(?::?(\d{2})(?:[,.](\d{1,}))?)?(?:(Z)|([+-])(\d{2})(?::?(\d{2}))?)?)?$/;

function toNumber(str: string, defaultValue = 0) {
return Number(str) || defaultValue;
}

export function parseIsoDate(date: string): number {
const regexResult = isoReg.exec(date);
if (!regexResult) return Date.parse ? Date.parse(date) : Number.NaN;

// use of toNumber() avoids NaN timestamps caused by “undefined”
// values being passed to Date constructor
const struct = {
year: toNumber(regexResult[1]),
month: toNumber(regexResult[2], 1) - 1,
day: toNumber(regexResult[3], 1),
hour: toNumber(regexResult[4]),
minute: toNumber(regexResult[5]),
second: toNumber(regexResult[6]),
millisecond: regexResult[7]
? // allow arbitrary sub-second precision beyond milliseconds
toNumber(regexResult[7].substring(0, 3))
: 0,
z: regexResult[8] || undefined,
plusMinus: regexResult[9] || undefined,
hourOffset: toNumber(regexResult[10]),
minuteOffset: toNumber(regexResult[11]),
};

// timestamps without timezone identifiers should be considered local time
if (struct.z === undefined && struct.plusMinus === undefined) {
return new Date(
struct.year,
struct.month,
struct.day,
struct.hour,
struct.minute,
struct.second,
struct.millisecond,
).valueOf();
}

let totalMinutesOffset = 0;
if (struct.z !== 'Z' && struct.plusMinus !== undefined) {
totalMinutesOffset = struct.hourOffset * 60 + struct.minuteOffset;
if (struct.plusMinus === '+') totalMinutesOffset = 0 - totalMinutesOffset;
}

return Date.UTC(
struct.year,
struct.month,
struct.day,
struct.hour,
struct.minute + totalMinutesOffset,
struct.second,
struct.millisecond,
);
}
245 changes: 245 additions & 0 deletions test/util/parseIsoDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/**
* This file is a modified version of the test file from the following repository:
* Date.parse with progressive enhancement for ISO 8601 <https://github.com/csnover/js-iso8601>
* NON-CONFORMANT EDITION.
* © 2011 Colin Snover <http://zetafleet.com>
* Released under MIT license.
*/

import { parseIsoDate } from '../../src/util/parseIsoDate';

const sixHours = 6 * 60 * 60 * 1000;
const sixHoursThirty = sixHours + 30 * 60 * 1000;
const epochLocalTime = new Date(1970, 0, 1, 0, 0, 0, 0).valueOf();

describe('plain date (no time)', () => {
describe('valid dates', () => {
test('Unix epoch', () => {
const result = parseIsoDate('1970-01-01');
expect(result).toBe(epochLocalTime);
});
test('2001', () => {
const result = parseIsoDate('2001');
const expected = new Date(2001, 0, 1, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('2001-02', () => {
const result = parseIsoDate('2001-02');
const expected = new Date(2001, 1, 1, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('2001-02-03', () => {
const result = parseIsoDate('2001-02-03');
const expected = new Date(2001, 1, 3, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-002001', () => {
const result = parseIsoDate('-002001');
const expected = new Date(-2001, 0, 1, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-002001-02', () => {
const result = parseIsoDate('-002001-02');
const expected = new Date(-2001, 1, 1, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-002001-02-03', () => {
const result = parseIsoDate('-002001-02-03');
const expected = new Date(-2001, 1, 3, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('+010000-02', () => {
const result = parseIsoDate('+010000-02');
const expected = new Date(10000, 1, 1, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('+010000-02-03', () => {
const result = parseIsoDate('+010000-02-03');
const expected = new Date(10000, 1, 3, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-010000-02', () => {
const result = parseIsoDate('-010000-02');
const expected = new Date(-10000, 1, 1, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-010000-02-03', () => {
const result = parseIsoDate('-010000-02-03');
const expected = new Date(-10000, 1, 3, 0, 0, 0, 0).valueOf();
expect(result).toBe(expected);
});
});

describe('invalid dates', () => {
test('invalid YYYY (non-digits)', () => {
expect(parseIsoDate('asdf')).toBeNaN();
});
test('invalid YYYY-MM-DD (non-digits)', () => {
expect(parseIsoDate('1970-as-df')).toBeNaN();
});
test('invalid YYYY-MM- (extra hyphen)', () => {
expect(parseIsoDate('1970-01-')).toBe(epochLocalTime);
});
test('invalid YYYY-MM-DD (missing hyphens)', () => {
expect(parseIsoDate('19700101')).toBe(epochLocalTime);
});
test('ambiguous YYYY-MM/YYYYYY (missing plus/minus or hyphen)', () => {
expect(parseIsoDate('197001')).toBe(epochLocalTime);
});
});
});

describe('date-time', () => {
describe('no time zone', () => {
test('2001-02-03T04:05', () => {
const result = parseIsoDate('2001-02-03T04:05');
const expected = new Date(2001, 1, 3, 4, 5, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06', () => {
const result = parseIsoDate('2001-02-03T04:05:06');
const expected = new Date(2001, 1, 3, 4, 5, 6, 0).valueOf();
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06.007', () => {
const result = parseIsoDate('2001-02-03T04:05:06.007');
const expected = new Date(2001, 1, 3, 4, 5, 6, 7).valueOf();
expect(result).toBe(expected);
});
});

describe('Z time zone', () => {
test('2001-02-03T04:05Z', () => {
const result = parseIsoDate('2001-02-03T04:05Z');
const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0);
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06Z', () => {
const result = parseIsoDate('2001-02-03T04:05:06Z');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0);
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06.007Z', () => {
const result = parseIsoDate('2001-02-03T04:05:06.007Z');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7);
expect(result).toBe(expected);
});
});

describe('offset time zone', () => {
test('2001-02-03T04:05-00:00', () => {
const result = parseIsoDate('2001-02-03T04:05-00:00');
const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0);
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06-00:00', () => {
const result = parseIsoDate('2001-02-03T04:05:06-00:00');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0);
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06.007-00:00', () => {
const result = parseIsoDate('2001-02-03T04:05:06.007-00:00');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7);
expect(result).toBe(expected);
});

test('2001-02-03T04:05+00:00', () => {
const result = parseIsoDate('2001-02-03T04:05+00:00');
const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0);
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06+00:00', () => {
const result = parseIsoDate('2001-02-03T04:05:06+00:00');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0);
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06.007+00:00', () => {
const result = parseIsoDate('2001-02-03T04:05:06.007+00:00');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7);
expect(result).toBe(expected);
});

test('2001-02-03T04:05-06:30', () => {
const result = parseIsoDate('2001-02-03T04:05-06:30');
const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0) + sixHoursThirty;
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06-06:30', () => {
const result = parseIsoDate('2001-02-03T04:05:06-06:30');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0) + sixHoursThirty;
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06.007-06:30', () => {
const result = parseIsoDate('2001-02-03T04:05:06.007-06:30');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7) + sixHoursThirty;
expect(result).toBe(expected);
});

test('2001-02-03T04:05+06:30', () => {
const result = parseIsoDate('2001-02-03T04:05+06:30');
const expected = Date.UTC(2001, 1, 3, 4, 5, 0, 0) - sixHoursThirty;
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06+06:30', () => {
const result = parseIsoDate('2001-02-03T04:05:06+06:30');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 0) - sixHoursThirty;
expect(result).toBe(expected);
});
test('2001-02-03T04:05:06.007+06:30', () => {
const result = parseIsoDate('2001-02-03T04:05:06.007+06:30');
const expected = Date.UTC(2001, 1, 3, 4, 5, 6, 7) - sixHoursThirty;
expect(result).toBe(expected);
});
});

describe('incomplete dates', () => {
test('2001T04:05:06.007', () => {
const result = parseIsoDate('2001T04:05:06.007');
const expected = new Date(2001, 0, 1, 4, 5, 6, 7).valueOf();
expect(result).toBe(expected);
});
test('2001-02T04:05:06.007', () => {
const result = parseIsoDate('2001-02T04:05:06.007');
const expected = new Date(2001, 1, 1, 4, 5, 6, 7).valueOf();
expect(result).toBe(expected);
});

test('-010000T04:05', () => {
const result = parseIsoDate('-010000T04:05');
const expected = new Date(-10000, 0, 1, 4, 5, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-010000-02T04:05', () => {
const result = parseIsoDate('-010000-02T04:05');
const expected = new Date(-10000, 1, 1, 4, 5, 0, 0).valueOf();
expect(result).toBe(expected);
});
test('-010000-02-03T04:05', () => {
const result = parseIsoDate('-010000-02-03T04:05');
const expected = new Date(-10000, 1, 3, 4, 5, 0, 0).valueOf();
expect(result).toBe(expected);
});
});

describe('invalid date-times', () => {
test('missing T', () => {
expect(parseIsoDate('1970-01-01 00:00:00')).toBe(epochLocalTime);
});
test('too many characters in millisecond part', () => {
expect(parseIsoDate('1970-01-01T00:00:00.000000')).toBe(epochLocalTime);
});
test('comma instead of dot', () => {
expect(parseIsoDate('1970-01-01T00:00:00,000')).toBe(epochLocalTime);
});
test('missing colon in timezone part', () => {
const subject = '1970-01-01T00:00:00+0630';
expect(parseIsoDate(subject)).toBe(Date.parse(subject));
});
test('missing colon in time part', () => {
expect(parseIsoDate('1970-01-01T0000')).toBe(epochLocalTime);
});
test('msec with missing seconds', () => {
expect(parseIsoDate('1970-01-01T00:00.000')).toBeNaN();
});
});
});
Loading