Skip to content

Commit

Permalink
refactor(parse-iso-date): Convert to TypeScript (#2080)
Browse files Browse the repository at this point in the history
* test(parse-iso-date): Port over tests

Adapted from: https://github.com/csnover/js-iso8601/blob/master/tests/test.js

NOTE: Many of these tests do not currently pass because of
modifications that have been made to parseIsoDate.

Those changes include (but probably aren't limited to):

  - The regex has been modified to add more "?" conditionals to make the
    parsing more forgiving
  - Times that lack a time zone specifier are treated as local time

* test(parse-iso-date): Adapt the tests for current impl

All tests now pass with the current implementation of
parseIsoDate.

* refactor(parse-iso-date): Convert to TypeScript

It was the only JS file remaining in the repo, it was also written in
an old, difficult to read style of JavaScript

The functionality remains identical.
  • Loading branch information
0livare authored Aug 17, 2023
1 parent b940eef commit 41b9c58
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 67 deletions.
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();
});
});
});

0 comments on commit 41b9c58

Please sign in to comment.