diff --git a/src/formats.ts b/src/formats.ts index d3cde9b..ec8c9cb 100644 --- a/src/formats.ts +++ b/src/formats.ts @@ -47,9 +47,9 @@ export const fullFormats: DefinedFormats = { date: fmtDef(date, compareDate), // date-time: http://tools.ietf.org/html/rfc3339#section-5.6 time: fmtDef(getTime(true), compareTime), - "date-time": fmtDef(getDateTime(true), compareDateTime), + "date-time": fmtDef(getDateTime(), compareDateTime), "iso-time": fmtDef(getTime(), compareIsoTime), - "iso-date-time": fmtDef(getDateTime(), compareIsoDateTime), + "iso-date-time": fmtDef(getDateTime(true), compareIsoDateTime), // duration: https://tools.ietf.org/html/rfc3339#appendix-A duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/, uri, @@ -102,7 +102,7 @@ export const fastFormats: DefinedFormats = { compareTime ), "date-time": fmtDef( - /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, + /^\d\d\d\d-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2]\d)|(3[01]))[tT](?:(([0-1]\d)|(2[0-3])):[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:[zZ]|[+-](([0-1]\d)|(2[0-3])):[0-5]\d)$/, compareDateTime ), "iso-time": fmtDef( @@ -110,7 +110,7 @@ export const fastFormats: DefinedFormats = { compareIsoTime ), "iso-date-time": fmtDef( - /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, + /^\d\d\d\d-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2]\d)|(3[01]))[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, compareIsoDateTime ), // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js @@ -197,13 +197,14 @@ function compareIsoTime(t1: string, t2: string): number | undefined { return 0 } -const DATE_TIME_SEPARATOR = /t|\s/i -function getDateTime(strictTimeZone?: boolean): (str: string) => boolean { - const time = getTime(strictTimeZone) +const DATE_TIME_SEPARATOR = /t/i +const ISO_DATE_TIME_SEPARATOR = /t|\s/i +function getDateTime(iso?: boolean): (str: string) => boolean { + const time = getTime(!iso) return function date_time(str: string): boolean { // http://tools.ietf.org/html/rfc3339#section-5.6 - const dateTime: string[] = str.split(DATE_TIME_SEPARATOR) + const dateTime: string[] = str.split(iso ? ISO_DATE_TIME_SEPARATOR : DATE_TIME_SEPARATOR) return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1]) } } @@ -218,8 +219,8 @@ function compareDateTime(dt1: string, dt2: string): number | undefined { function compareIsoDateTime(dt1: string, dt2: string): number | undefined { if (!(dt1 && dt2)) return undefined - const [d1, t1] = dt1.split(DATE_TIME_SEPARATOR) - const [d2, t2] = dt2.split(DATE_TIME_SEPARATOR) + const [d1, t1] = dt1.split(ISO_DATE_TIME_SEPARATOR) + const [d2, t2] = dt2.split(ISO_DATE_TIME_SEPARATOR) const res = compareDate(d1, d2) if (res === undefined) return undefined return res || compareTime(t1, t2) diff --git a/tests/extras/format.json b/tests/extras/format.json index 1a72ec4..c54d360 100644 --- a/tests/extras/format.json +++ b/tests/extras/format.json @@ -560,7 +560,7 @@ ] }, { - "description": "validation of date-time strings", + "description": "validation of iso-date-time strings", "schema": {"format": "iso-date-time"}, "tests": [ { @@ -597,6 +597,132 @@ "description": "a valid iso-date-time string (leap second)", "data": "2016-12-31T23:59:60Z", "valid": true + }, + { + "description": "a valid iso-date-time string (space separator)", + "data": "2016-12-31 23:59:60Z", + "valid": true + } + ] + }, + { + "description": "validation of date-time strings", + "schema": {"format": "date-time"}, + "tests": [ + { + "description": "a valid date-time string", + "data": "2016-12-31T23:59:60Z", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2015-12-31t23:59:60Z", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2015-02-11t22:59:22Z", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2020-01-01T20:00:00.000Z", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2020-01-01T20:00:00.000-00:00", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2023-05-04T01:14:00+21:00", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2023-05-04T01:14:10+16:20", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2023-05-04T01:14:21+09:50", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2023-05-04T01:14:21-04:31", + "valid": true + }, + { + "description": "a valid date-time string", + "data": "2023-05-04T01:14:21-23:59", + "valid": true + }, + { + "description": "an invalid date-time string (invalid month)", + "data": "2016-15-31T23:59:60Z", + "valid": false + }, + { + "description": "an invalid date-time string (invalid month)", + "data": "2015-00-11t22:59:22+00:00", + "valid": false + }, + { + "description": "an invalid date-time string (invalid day)", + "data": "2015-01-00T22:59:22+00:00", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator)", + "data": "2016-12-31 23:59:60Z", + "valid": false + }, + { + "description": "an invalid date-time string (invalid time)", + "data": "2015-02-11t24:59:22Z", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator and time-offset)", + "data": "2020-01-01 20:00:00.000", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator)", + "data": "2020-01-01 20:00:00.000Z", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator)", + "data": "2023-05-04\t01:14:00+21:00", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator)", + "data": "2023-05-04\r01:14:10+16:20", + "valid": false + }, + { + "description": "an invalid date-time string (invalid timezone)", + "data": "2015-02-11t22:59:22+24:30", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator)", + "data": "2023-05-04\n01:14:21+09:50", + "valid": false + }, + { + "description": "an invalid date-time string (invalid separator)", + "data": "2023-05-04\n01:14:21-04:31", + "valid": false + }, + { + "description": "an invalid date-time string (invalid time-offset)", + "data": "2023-05-04t01:14:21-04:31:00", + "valid": false } ] }, diff --git a/tests/index.spec.ts b/tests/index.spec.ts index b434921..352531a 100644 --- a/tests/index.spec.ts +++ b/tests/index.spec.ts @@ -25,7 +25,7 @@ describe("addFormats options", () => { }) test("should support validation mode", () => { - addFormats(ajv, {mode: "fast", formats: ["date", "time"]}) + addFormats(ajv, {mode: "fast", formats: ["date", "time", "date-time", "iso-date-time"]}) const validateDate = ajv.compile({format: "date"}) expect(validateDate("2020-09-17")).toEqual(true) expect(validateDate("2020-09-35")).toEqual(true) @@ -35,6 +35,35 @@ describe("addFormats options", () => { expect(validateTime("17:27:38Z")).toEqual(true) expect(validateTime("25:27:38Z")).toEqual(true) expect(validateTime("17:27")).toEqual(false) + + const validateDatetime = ajv.compile({format: "date-time"}) + expect(validateDatetime("2016-12-31T23:59:60Z")).toEqual(true) + expect(validateDatetime("2015-12-31t23:59:60Z")).toEqual(true) + expect(validateDatetime("2015-02-11t22:59:22Z")).toEqual(true) + expect(validateDatetime("2020-01-01T20:00:00.000Z")).toEqual(true) + expect(validateDatetime("2020-01-01T20:00:00.000Z")).toEqual(true) + expect(validateDatetime("2023-05-04T01:14:00+21:00")).toEqual(true) + expect(validateDatetime("2023-05-04T01:14:10+16:20")).toEqual(true) + expect(validateDatetime("2023-05-04T01:14:21+09:50")).toEqual(true) + expect(validateDatetime("2023-05-04T01:14:21-04:31")).toEqual(true) + expect(validateDatetime("2023-05-04T01:14:21-23:59")).toEqual(true) + expect(validateDatetime("2016-15-31T23:59:60Z")).toEqual(false) + expect(validateDatetime("2015-00-11t22:59:22+00:00")).toEqual(false) + expect(validateDatetime("2015-01-00T22:59:22+00:00")).toEqual(false) + expect(validateDatetime("2016-12-31 23:59:60Z")).toEqual(false) + expect(validateDatetime("2015-02-11t24:59:22Z")).toEqual(false) + expect(validateDatetime("2020-01-01 20:00:00.000")).toEqual(false) + expect(validateDatetime("2020-01-01 20:00:00.000Z")).toEqual(false) + expect(validateDatetime("2023-05-04\t01:14:00+21:00")).toEqual(false) + expect(validateDatetime("2023-05-04\r01:14:10+16:20")).toEqual(false) + expect(validateDatetime("2015-02-11t22:59:22+24:30")).toEqual(false) + expect(validateDatetime("2023-05-04\n01:14:21+09:50")).toEqual(false) + expect(validateDatetime("2023-05-04\n01:14:21-04:31")).toEqual(false) + expect(validateDatetime("2023-05-04t01:14:21-04:31:00")).toEqual(false) + + const validateIsoDatetime = ajv.compile({format: "iso-date-time"}) + expect(validateIsoDatetime("2016-12-31 23:59:60Z")).toEqual(true) + expect(validateIsoDatetime("2016-13-31 23:59:60Z")).toEqual(false) }) })