Skip to content

Commit 297506a

Browse files
authored
Remove dependency on moment (#30)
* Remove dependency on moment * Remvoe skipped test cases * Hide rfc3339 from docs
1 parent d09ebee commit 297506a

File tree

5 files changed

+113
-25
lines changed

5 files changed

+113
-25
lines changed

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,5 @@
3434
"/lib"
3535
]
3636
},
37-
"dependencies": {
38-
"moment": "^2.24.0"
39-
}
37+
"dependencies": {}
4038
}

src/index.test.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,12 @@ describe("json-typedef-spec", () => {
5757
readFileSync("json-typedef-spec/tests/validation.json", "utf-8")
5858
);
5959

60-
// See comment in index.ts around why we skip this these particular timestamp
61-
// test cases.
62-
const SKIPPED_TESTS = [
63-
"timestamp type schema - 1990-12-31T23:59:60Z",
64-
"timestamp type schema - 1990-12-31T15:59:60-08:00"
65-
];
66-
6760
for (const [name, { schema, instance, errors }] of Object.entries(
6861
testCases
6962
)) {
7063
it(name, () => {
7164
expect(isSchema(schema)).toBe(true);
7265

73-
if (SKIPPED_TESTS.includes(name)) {
74-
return;
75-
}
76-
7766
if (isSchema(schema)) {
7867
expect(validate(schema, instance)).toEqual(errors);
7968
}

src/rfc3339.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
import isRFC3339 from "./rfc3339"
3+
4+
describe("isRFC3339", () => {
5+
const testCases = {
6+
// From the RFC
7+
"1985-04-12T23:20:50.52Z": true,
8+
"1990-12-31T23:59:60Z": true,
9+
"1990-12-31T15:59:60-08:00": true,
10+
"1937-01-01T12:00:27.87+00:20": true,
11+
12+
// T and Z can be t or z
13+
"1985-04-12t23:20:50.52z": true,
14+
15+
// From http://henry.precheur.org/python/rfc3339
16+
"2008-04-02T20:00:00Z": true,
17+
"1970-01-01T00:00:00Z": true,
18+
19+
// https://github.com/chronotope/chrono/blob/main/src/format/parse.rs
20+
"2015-01-20T17:35:20-08:00": true, // normal case
21+
"1944-06-06T04:04:00Z": true, // D-day
22+
"2001-09-11T09:45:00-08:00": true,
23+
"2015-01-20T17:35:20.001-08:00": true,
24+
"2015-01-20T17:35:20.000031-08:00": true,
25+
"2015-01-20T17:35:20.000000004-08:00": true,
26+
"2015-01-20T17:35:20.000000000452-08:00": true, // too small
27+
"2015-02-30T17:35:20-08:00": false, // bad day of month
28+
"2015-01-20T25:35:20-08:00": false, // bad hour
29+
"2015-01-20T17:65:20-08:00": false, // bad minute
30+
"2015-01-20T17:35:90-08:00": false, // bad second
31+
32+
// Ensure the regex is anchored
33+
"x1985-04-12T23:20:50.52Zx": false,
34+
"1985-04-12T23:20:50.52Zx": false,
35+
}
36+
37+
for (const [s, expected] of Object.entries(testCases)) {
38+
it(`correctly validates ${s}`, () => {
39+
expect(isRFC3339(s)).toEqual(expected)
40+
})
41+
}
42+
})

src/rfc3339.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/** @ignore *//** */
2+
3+
const pattern = /^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$/
4+
5+
export default function isRFC3339(s: string): boolean {
6+
const matches = s.match(pattern);
7+
if (matches === null) {
8+
return false;
9+
}
10+
11+
const year = parseInt(matches[1], 10);
12+
const month = parseInt(matches[2], 10);
13+
const day = parseInt(matches[3], 10);
14+
const hour = parseInt(matches[4], 10);
15+
const minute = parseInt(matches[5], 10);
16+
const second = parseInt(matches[6], 10);
17+
18+
if (month > 12) {
19+
return false;
20+
}
21+
22+
if (day > maxDay(year, month)) {
23+
return false;
24+
}
25+
26+
if (hour > 23) {
27+
return false;
28+
}
29+
30+
if (minute > 59) {
31+
return false;
32+
}
33+
34+
// A value of 60 is permissible as a leap second.
35+
if (second > 60) {
36+
return false;
37+
}
38+
39+
return true;
40+
}
41+
42+
function maxDay(year: number, month: number) {
43+
if (month === 2) {
44+
return isLeapYear(year) ? 29 : 28;
45+
}
46+
47+
return MONTH_LENGTHS[month];
48+
}
49+
50+
function isLeapYear(n: number): boolean {
51+
return n % 4 === 0 && (n % 100 !== 0 || n % 400 === 0);
52+
}
53+
54+
const MONTH_LENGTHS = [
55+
0, // months are 1-indexed, this is a dummy element
56+
31,
57+
0, // Feb is handled separately
58+
31,
59+
30,
60+
31,
61+
30,
62+
31,
63+
31,
64+
30,
65+
31,
66+
30,
67+
31,
68+
];

src/validate.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @packageDocumentation
99
*/
1010

11-
import moment from "moment";
11+
import isRFC3339 from "./rfc3339";
1212
import {
1313
Schema,
1414
isRefForm,
@@ -210,16 +210,7 @@ function validateWithState(
210210
if (typeof instance !== "string") {
211211
pushError(state);
212212
} else {
213-
// ISO 8601 is unfortunately not quite the same thing as RFC 3339.
214-
// However, at the time of writing no adequate alternative,
215-
// widely-used library for parsing RFC3339 timestamps exists.
216-
//
217-
// Notably, moment does not support two of the examples given in
218-
// RFC 3339 with "60" in the seconds place. These timestamps arise
219-
// due to leap seconds. See:
220-
//
221-
// https://tools.ietf.org/html/rfc3339#section-5.8
222-
if (!moment(instance, moment.ISO_8601).isValid()) {
213+
if (!isRFC3339(instance)) {
223214
pushError(state);
224215
}
225216
}

0 commit comments

Comments
 (0)