Skip to content

Commit 37d7538

Browse files
authored
add formats iso-time and iso-date-time, make time and date-time strict (#42)
1 parent 46dbae5 commit 37d7538

9 files changed

+75
-121
lines changed

README.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ addFormats(ajv)
2626
The package defines these formats:
2727

2828
- _date_: full-date according to [RFC3339](http://tools.ietf.org/html/rfc3339#section-5.6).
29-
- _time_: time with optional time-zone.
30-
- _date-time_: date-time from the same source (time-zone is mandatory).
29+
- _time_: time (time-zone is mandatory).
30+
- _date-time_: date-time (time-zone is mandatory).
31+
- _iso-time_: time with optional time-zone.
32+
- _iso-date-time_: date-time with optional time-zone.
3133
- _duration_: duration from [RFC3339](https://tools.ietf.org/html/rfc3339#appendix-A)
3234
- _uri_: full URI.
3335
- _uri-reference_: URI reference, including full and relative URIs.
@@ -105,12 +107,10 @@ addFormats(ajv, {mode: "fast"})
105107
or
106108

107109
```javascript
108-
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true, strictTime: true})
110+
addFormats(ajv, {mode: "fast", formats: ["date", "time"], keywords: true})
109111
```
110112

111-
In `"fast"` mode the following formats are simplified: `"date"`, `"time"`, `"date-time"`, `"uri"`, `"uri-reference"`, `"email"`. For example `"date"`, `"time"` and `"date-time"` do not validate ranges in `"fast"` mode, only string structure, and other formats have simplified regular expressions.
112-
113-
With `strictTime: true` option timezone becomes required in `time` and `date-time` formats, and (it also implies `full` mode for these formats).
113+
In `"fast"` mode the following formats are simplified: `"date"`, `"time"`, `"date-time"`, `"iso-time"`, `"iso-date-time"`, `"uri"`, `"uri-reference"`, `"email"`. For example, `"date"`, `"time"` and `"date-time"` do not validate ranges in `"fast"` mode, only string structure, and other formats have simplified regular expressions.
114114

115115
## Tests
116116

src/formats.ts

+41-37
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export type FormatName =
77
| "date"
88
| "time"
99
| "date-time"
10+
| "iso-time"
11+
| "iso-date-time"
1012
| "duration"
1113
| "uri"
1214
| "uri-reference"
@@ -44,8 +46,10 @@ export const fullFormats: DefinedFormats = {
4446
// date: http://tools.ietf.org/html/rfc3339#section-5.6
4547
date: fmtDef(date, compareDate),
4648
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
47-
time: fmtDef(time, compareTime),
48-
"date-time": fmtDef(date_time, compareDateTime),
49+
time: fmtDef(getTime(true), compareTime),
50+
"date-time": fmtDef(getDateTime(true), compareDateTime),
51+
"iso-time": fmtDef(getTime(), compareTime),
52+
"iso-date-time": fmtDef(getDateTime(), compareDateTime),
4953
// duration: https://tools.ietf.org/html/rfc3339#appendix-A
5054
duration: /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/,
5155
uri,
@@ -94,11 +98,19 @@ export const fastFormats: DefinedFormats = {
9498
...fullFormats,
9599
date: fmtDef(/^\d\d\d\d-[0-1]\d-[0-3]\d$/, compareDate),
96100
time: fmtDef(
97-
/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
101+
/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i,
98102
compareTime
99103
),
100104
"date-time": fmtDef(
101-
/^\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,
105+
/^\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,
106+
compareDateTime
107+
),
108+
"iso-time": fmtDef(
109+
/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i,
110+
compareTime
111+
),
112+
"iso-date-time": fmtDef(
113+
/^\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,
102114
compareDateTime
103115
),
104116
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
@@ -111,12 +123,6 @@ export const fastFormats: DefinedFormats = {
111123
/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,
112124
}
113125

114-
export const strictFormats: Partial<DefinedFormats> = {
115-
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6
116-
time: fmtDef(strict_time, compareTime),
117-
"date-time": fmtDef(strict_date_time, compareDateTime),
118-
}
119-
120126
export const formatNames = Object.keys(fullFormats) as FormatName[]
121127

122128
function isLeapYear(year: number): boolean {
@@ -151,26 +157,24 @@ function compareDate(d1: string, d2: string): number | undefined {
151157

152158
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i
153159

154-
function time(str: string, withTimeZone?: boolean, strictTime?: boolean): boolean {
155-
const matches: string[] | null = TIME.exec(str)
156-
if (!matches) return false
157-
const hr: number = +matches[1]
158-
const min: number = +matches[2]
159-
const sec: number = +matches[3]
160-
const tz: string | undefined = matches[4]
161-
const tzSign: number = matches[5] === "-" ? -1 : 1
162-
const tzH: number = +(matches[6] || 0)
163-
const tzM: number = +(matches[7] || 0)
164-
if (tzH > 23 || tzM > 59 || (withTimeZone && (tz === "" || (strictTime && !tz)))) return false
165-
if (hr <= 23 && min <= 59 && sec < 60) return true
166-
// leap second
167-
const utcMin = min - tzM * tzSign
168-
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
169-
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61
170-
}
171-
172-
function strict_time(str: string): boolean {
173-
return time(str, true, true)
160+
function getTime(strictTimeZone?: boolean): (str: string) => boolean {
161+
return function time(str: string): boolean {
162+
const matches: string[] | null = TIME.exec(str)
163+
if (!matches) return false
164+
const hr: number = +matches[1]
165+
const min: number = +matches[2]
166+
const sec: number = +matches[3]
167+
const tz: string | undefined = matches[4]
168+
const tzSign: number = matches[5] === "-" ? -1 : 1
169+
const tzH: number = +(matches[6] || 0)
170+
const tzM: number = +(matches[7] || 0)
171+
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false
172+
if (hr <= 23 && min <= 59 && sec < 60) return true
173+
// leap second
174+
const utcMin = min - tzM * tzSign
175+
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
176+
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61
177+
}
174178
}
175179

176180
function compareTime(t1: string, t2: string): number | undefined {
@@ -186,14 +190,14 @@ function compareTime(t1: string, t2: string): number | undefined {
186190
}
187191

188192
const DATE_TIME_SEPARATOR = /t|\s/i
189-
function date_time(str: string, strictTime?: boolean): boolean {
190-
// http://tools.ietf.org/html/rfc3339#section-5.6
191-
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
192-
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1], true, strictTime)
193-
}
193+
function getDateTime(strictTimeZone?: boolean): (str: string) => boolean {
194+
const time = getTime(strictTimeZone)
194195

195-
function strict_date_time(str: string): boolean {
196-
return date_time(str, true)
196+
return function date_time(str: string): boolean {
197+
// http://tools.ietf.org/html/rfc3339#section-5.6
198+
const dateTime: string[] = str.split(DATE_TIME_SEPARATOR)
199+
return dateTime.length === 2 && date(dateTime[0]) && time(dateTime[1])
200+
}
197201
}
198202

199203
function compareDateTime(dt1: string, dt2: string): number | undefined {

src/index.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
formatNames,
66
fastFormats,
77
fullFormats,
8-
strictFormats,
98
} from "./formats"
109
import formatLimit from "./limit"
1110
import type Ajv from "ajv"
@@ -18,7 +17,6 @@ export interface FormatOptions {
1817
mode?: FormatMode
1918
formats?: FormatName[]
2019
keywords?: boolean
21-
strictTime?: boolean
2220
}
2321

2422
export type FormatsPluginOptions = FormatName[] | FormatOptions
@@ -32,7 +30,7 @@ const fastName = new Name("fastFormats")
3230

3331
const formatsPlugin: FormatsPlugin = (
3432
ajv: Ajv,
35-
opts: FormatsPluginOptions = {keywords: true, strictTime: false}
33+
opts: FormatsPluginOptions = {keywords: true}
3634
): Ajv => {
3735
if (Array.isArray(opts)) {
3836
addFormats(ajv, opts, fullFormats, fullName)
@@ -41,7 +39,7 @@ const formatsPlugin: FormatsPlugin = (
4139
const [formats, exportName] =
4240
opts.mode === "fast" ? [fastFormats, fastName] : [fullFormats, fullName]
4341
const list = opts.formats || formatNames
44-
addFormats(ajv, list, opts.strictTime ? {...formats, ...strictFormats} : formats, exportName)
42+
addFormats(ajv, list, formats, exportName)
4543
if (opts.keywords) formatLimit(ajv)
4644
return ajv
4745
}

tests/extras/format.json

+19-14
Original file line numberDiff line numberDiff line change
@@ -524,52 +524,57 @@
524524
]
525525
},
526526
{
527-
"description": "validation of time strings",
528-
"schema": {"format": "time"},
527+
"description": "validation of iso-time strings",
528+
"schema": {"format": "iso-time"},
529529
"tests": [
530530
{
531-
"description": "a valid time",
531+
"description": "a valid iso-time",
532532
"data": "12:34:56",
533533
"valid": true
534534
},
535535
{
536-
"description": "a valid time with milliseconds",
536+
"description": "a valid iso-time with milliseconds",
537537
"data": "12:34:56.789",
538538
"valid": true
539539
},
540540
{
541-
"description": "a valid time with timezone",
541+
"description": "a valid iso-time with timezone",
542542
"data": "12:34:56+01:00",
543543
"valid": true
544544
},
545545
{
546-
"description": "an invalid time format",
546+
"description": "an invalid iso-time format",
547547
"data": "12.34.56",
548548
"valid": false
549549
},
550550
{
551-
"description": "an invalid time",
551+
"description": "an invalid iso-time",
552552
"data": "12:34:67",
553553
"valid": false
554554
},
555555
{
556-
"description": "a valid time (leap second)",
556+
"description": "a valid iso-time (leap second)",
557557
"data": "23:59:60",
558558
"valid": true
559559
}
560560
]
561561
},
562562
{
563563
"description": "validation of date-time strings",
564-
"schema": {"format": "date-time"},
564+
"schema": {"format": "iso-date-time"},
565565
"tests": [
566566
{
567-
"description": "a valid date-time string",
567+
"description": "a valid iso-date-time string",
568568
"data": "1963-06-19T12:13:14Z",
569569
"valid": true
570570
},
571571
{
572-
"description": "an invalid date-time string (no time)",
572+
"description": "a valid iso-date-time string without timezone",
573+
"data": "1963-06-19T12:13:14",
574+
"valid": true
575+
},
576+
{
577+
"description": "an invalid iso-date-time string (no time)",
573578
"data": "1963-06-19",
574579
"valid": false
575580
},
@@ -579,17 +584,17 @@
579584
"valid": false
580585
},
581586
{
582-
"description": "an invalid date-time string (invalid date)",
587+
"description": "an invalid iso-date-time string (invalid date)",
583588
"data": "1963-20-19T12:13:14Z",
584589
"valid": false
585590
},
586591
{
587-
"description": "an invalid date-time string (invalid time)",
592+
"description": "an invalid iso-date-time string (invalid time)",
588593
"data": "1963-06-19T12:13:67Z",
589594
"valid": false
590595
},
591596
{
592-
"description": "a valid date-time string (leap second)",
597+
"description": "a valid iso-date-time string (leap second)",
593598
"data": "2016-12-31T23:59:60Z",
594599
"valid": true
595600
}

tests/extras/formatMaximum.json

-5
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,6 @@
7373
"data": "13:15:17.000+01:00",
7474
"valid": true
7575
},
76-
{
77-
"description": "boundary point is valid, no timezone is ok too",
78-
"data": "13:15:17.000",
79-
"valid": true
80-
},
8176
{
8277
"description": "time before the maximum time is valid",
8378
"data": "10:33:55.000Z",

tests/extras/formatMinimum.json

-5
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,6 @@
7373
"data": "13:15:17.000+01:00",
7474
"valid": true
7575
},
76-
{
77-
"description": "boundary point is valid, no timezone is ok too",
78-
"data": "13:15:17.000",
79-
"valid": true
80-
},
8176
{
8277
"description": "time before the minimum time is invalid",
8378
"data": "10:33:55.000Z",

tests/index.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ describe("addFormats options", () => {
1616
expect(validateDate("2020-09-35")).toEqual(false)
1717

1818
const validateTime = ajv.compile({format: "time"})
19-
expect(validateTime("17:27:38")).toEqual(true)
20-
expect(validateDate("25:27:38")).toEqual(false)
19+
expect(validateTime("17:27:38Z")).toEqual(true)
20+
expect(validateDate("25:27:38Z")).toEqual(false)
2121

2222
expect(() => ajv.compile({format: "date-time"})).toThrow()
2323
addFormats(ajv, ["date-time"])
@@ -32,8 +32,8 @@ describe("addFormats options", () => {
3232
expect(validateDate("2020-09")).toEqual(false)
3333

3434
const validateTime = ajv.compile({format: "time"})
35-
expect(validateTime("17:27:38")).toEqual(true)
36-
expect(validateTime("25:27:38")).toEqual(true)
35+
expect(validateTime("17:27:38Z")).toEqual(true)
36+
expect(validateTime("25:27:38Z")).toEqual(true)
3737
expect(validateTime("17:27")).toEqual(false)
3838
})
3939
})

tests/json-schema.spec.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const jsonSchemaTest = require("json-schema-test")
22
const Ajv = require("ajv").default
33
const addFormats = require("../dist")
44

5-
jsonSchemaTest(getAjv(true), {
5+
jsonSchemaTest(getAjv(), {
66
description: `JSON-Schema Test Suite formats`,
77
suites: {
88
"draft-07 formats": "./JSON-Schema-Test-Suite/tests/draft7/optional/format/*.json",
@@ -31,9 +31,9 @@ jsonSchemaTest(getAjv(), {
3131
cwd: __dirname,
3232
})
3333

34-
function getAjv(strictTime) {
34+
function getAjv() {
3535
const ajv = new Ajv({$data: true, strictTypes: false, formats: {allowedUnknown: true}})
36-
addFormats(ajv, {mode: "full", keywords: true, strictTime})
36+
addFormats(ajv, {mode: "full", keywords: true})
3737
return ajv
3838
}
3939

tests/strictTime.spec.ts

-43
This file was deleted.

0 commit comments

Comments
 (0)