diff --git a/README.md b/README.md index b7968e0ca..91e1a6f89 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,9 @@ - [Coercion for primitives](#coercion-for-primitives) - [Literals](#literals) - [Strings](#strings) - - [ISO datetimes](#iso-datetimes) + - [Datetimes](#datetimes) + - [Dates](#dates) + - [Times](#times) - [IP addresses](#ip-addresses) - [Numbers](#numbers) - [BigInts](#bigints) @@ -893,18 +895,13 @@ z.string().time({ message: "Invalid time string!" }); z.string().ip({ message: "Invalid IP address" }); ``` -### ISO Date, Time & Datetime validation +### Datetimes -As you may have noticed, Zod string includes a few date/time related validations. -These validations are regular expression based, so they are not as strict as a full -date/time library. However, they are very convenient for validating user input. +As you may have noticed, Zod string includes a few date/time related validations. These validations are regular expression based, so they are not as strict as a full date/time library. However, they are very convenient for validating user input. The `z.string().date()` method validates strings in the format `YYYY-MM-DD`. -The `z.string().time()` method validates strings in the format `HH:mm:ss[.SSSSSS][Z|(+|-)hh[:]mm]` -(the time portion of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)). It defaults -to `HH:mm:ss[.SSSSSS]` validation: no timezone offsets or `Z`, with arbitrary sub-second -decimal. +The `z.string().time()` method validates strings in the format `HH:mm:ss[.SSSSSS][Z|(+|-)hh[:]mm]` (the time portion of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)). It defaults to `HH:mm:ss[.SSSSSS]` validation: no timezone offsets or `Z`, with arbitrary sub-second decimal. The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision. @@ -915,18 +912,6 @@ datetime.parse("2020-01-01T00:00:00Z"); // pass datetime.parse("2020-01-01T00:00:00.123Z"); // pass datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision) datetime.parse("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed) - -const date = z.string().date(); - -date.parse("2020-01-01"); // pass - -const time = z.string().time(); - -time.parse("00:00:00"); // pass -time.parse("09:52:31"); // pass -time.parse("23:59:59.9999999"); // pass (arbitrary precision) -time.parse("00:00:00.123Z"); // fail (no `Z` allowed) -time.parse("00:00:00.123+02:00"); // fail (no offsets allowed) ``` Timezone offsets can be allowed by setting the `offset` option to `true`. @@ -939,13 +924,6 @@ datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (millis optional) datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (millis optional) datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours) datetime.parse("2020-01-01T00:00:00Z"); // pass (Z still supported) - -const time = z.string().time({ offset: true }); - -time.parse("00:00:00+02:00"); // pass -time.parse("00:00:00.123+02:00"); // pass (millis optional) -time.parse("00:00:00.123+0200"); // pass (millis optional) -time.parse("00:00:00Z"); // pass (`Z` now supported) ``` You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional). @@ -964,6 +942,43 @@ time.parse("00:00:00"); // fail time.parse("00:00:00.123456"); // fail ``` +### Dates + +The `z.string().date()` method validates strings in the format `YYYY-MM-DD`. + +```ts +const date = z.string().date(); + +date.parse("2020-01-01"); // pass +date.parse("2020-1-1"); // fail +date.parse("2020-01-32"); // fail +``` + +### Times + +The `z.string().time()` method validates strings in the format `HH:MM:SS[.s+]`. The second can include arbitrary decimal precision. It does not allow timezone offsets of any kind. + +```ts +const time = z.string().time(); + +time.parse("00:00:00"); // pass +time.parse("09:52:31"); // pass +time.parse("23:59:59.9999999"); // pass (arbitrary precision) + +time.parse("00:00:00.123Z"); // fail (no `Z` allowed) +time.parse("00:00:00.123+02:00"); // fail (no offsets allowed) +``` + +You can set the `precision` option to constrain the allowable decimal precision. + +```ts +const time = z.string().time({ precision: 3 }); + +time.parse("00:00:00.123"); // pass +time.parse("00:00:00.123456"); // fail +time.parse("00:00:00"); // fail +``` + ### IP addresses The `z.string().ip()` method by default validate IPv4 and IPv6. diff --git a/deno/lib/README.md b/deno/lib/README.md index 1060e236d..91e1a6f89 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -69,8 +69,10 @@ - [Coercion for primitives](#coercion-for-primitives) - [Literals](#literals) - [Strings](#strings) - - [Datetime](#iso-datetimes) - - [IP](#ip-addresses) + - [Datetimes](#datetimes) + - [Dates](#dates) + - [Times](#times) + - [IP addresses](#ip-addresses) - [Numbers](#numbers) - [BigInts](#bigints) - [NaNs](#nans) @@ -638,6 +640,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`zod_utilz`](https://github.com/JacobWeisenburger/zod_utilz): Framework agnostic utilities for Zod. - [`zod-sandbox`](https://github.com/nereumelo/zod-sandbox): Controlled environment for testing zod schemas. [Live demo](https://zod-sandbox.vercel.app/). - [`zod-dev`](https://github.com/schalkventer/zod-dev): Conditionally disables Zod runtime parsing in production. +- [`zod-accelerator`](https://github.com/duplojs/duplojs-zod-accelerator): Accelerates Zod's throughput up to ~100x. ## Installation @@ -892,18 +895,13 @@ z.string().time({ message: "Invalid time string!" }); z.string().ip({ message: "Invalid IP address" }); ``` -### ISO Date, Time & Datetime validation +### Datetimes -As you may have noticed, Zod string includes a few date/time related validations. -These validations are regular expression based, so they are not as strict as a full -date/time library. However, they are very convenient for validating user input. +As you may have noticed, Zod string includes a few date/time related validations. These validations are regular expression based, so they are not as strict as a full date/time library. However, they are very convenient for validating user input. The `z.string().date()` method validates strings in the format `YYYY-MM-DD`. -The `z.string().time()` method validates strings in the format `HH:mm:ss[.SSSSSS][Z|(+|-)hh[:]mm]` -(the time portion of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)). It defaults -to `HH:mm:ss[.SSSSSS]` validation: no timezone offsets or `Z`, with arbitrary sub-second -decimal. +The `z.string().time()` method validates strings in the format `HH:mm:ss[.SSSSSS][Z|(+|-)hh[:]mm]` (the time portion of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)). It defaults to `HH:mm:ss[.SSSSSS]` validation: no timezone offsets or `Z`, with arbitrary sub-second decimal. The `z.string().datetime()` method enforces ISO 8601; default is no timezone offsets and arbitrary sub-second decimal precision. @@ -914,18 +912,6 @@ datetime.parse("2020-01-01T00:00:00Z"); // pass datetime.parse("2020-01-01T00:00:00.123Z"); // pass datetime.parse("2020-01-01T00:00:00.123456Z"); // pass (arbitrary precision) datetime.parse("2020-01-01T00:00:00+02:00"); // fail (no offsets allowed) - -const date = z.string().date(); - -date.parse("2020-01-01"); // pass - -const time = z.string().time(); - -time.parse("00:00:00"); // pass -time.parse("09:52:31"); // pass -time.parse("23:59:59.9999999"); // pass (arbitrary precision) -time.parse("00:00:00.123Z"); // fail (no `Z` allowed) -time.parse("00:00:00.123+02:00"); // fail (no offsets allowed) ``` Timezone offsets can be allowed by setting the `offset` option to `true`. @@ -938,13 +924,6 @@ datetime.parse("2020-01-01T00:00:00.123+02:00"); // pass (millis optional) datetime.parse("2020-01-01T00:00:00.123+0200"); // pass (millis optional) datetime.parse("2020-01-01T00:00:00.123+02"); // pass (only offset hours) datetime.parse("2020-01-01T00:00:00Z"); // pass (Z still supported) - -const time = z.string().time({ offset: true }); - -time.parse("00:00:00+02:00"); // pass -time.parse("00:00:00.123+02:00"); // pass (millis optional) -time.parse("00:00:00.123+0200"); // pass (millis optional) -time.parse("00:00:00Z"); // pass (`Z` now supported) ``` You can additionally constrain the allowable `precision`. By default, arbitrary sub-second precision is supported (but optional). @@ -963,6 +942,43 @@ time.parse("00:00:00"); // fail time.parse("00:00:00.123456"); // fail ``` +### Dates + +The `z.string().date()` method validates strings in the format `YYYY-MM-DD`. + +```ts +const date = z.string().date(); + +date.parse("2020-01-01"); // pass +date.parse("2020-1-1"); // fail +date.parse("2020-01-32"); // fail +``` + +### Times + +The `z.string().time()` method validates strings in the format `HH:MM:SS[.s+]`. The second can include arbitrary decimal precision. It does not allow timezone offsets of any kind. + +```ts +const time = z.string().time(); + +time.parse("00:00:00"); // pass +time.parse("09:52:31"); // pass +time.parse("23:59:59.9999999"); // pass (arbitrary precision) + +time.parse("00:00:00.123Z"); // fail (no `Z` allowed) +time.parse("00:00:00.123+02:00"); // fail (no offsets allowed) +``` + +You can set the `precision` option to constrain the allowable decimal precision. + +```ts +const time = z.string().time({ precision: 3 }); + +time.parse("00:00:00.123"); // pass +time.parse("00:00:00.123456"); // fail +time.parse("00:00:00"); // fail +``` + ### IP addresses The `z.string().ip()` method by default validate IPv4 and IPv6. diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index cafe9b76e..5ae802ac0 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -234,8 +234,8 @@ test("nanoid", () => { const nanoid = z.string().nanoid("custom error"); nanoid.parse("lfNZluvAxMkf7Q8C5H-QS"); nanoid.parse("mIU_4PJWikaU8fMbmkouz"); - nanoid.parse("Hb9ZUtUa2JDm_dD-47EGv"); - nanoid.parse("5Noocgv_8vQ9oPijj4ioQ"); + nanoid.parse("Hb9ZUtUa2JDm_dD-47EGv"); + nanoid.parse("5Noocgv_8vQ9oPijj4ioQ"); const result = nanoid.safeParse("Xq90uDyhddC53KsoASYJGX"); expect(result.success).toEqual(false); if (!result.success) { @@ -566,29 +566,29 @@ test("time parsing", () => { expect(() => time2.parse("00:00:00.000")).toThrow(); expect(() => time2.parse("00:00:00.00+00:00")).toThrow(); - const time3 = z.string().time({ offset: true }); - time3.parse("00:00:00Z"); - time3.parse("09:52:31Z"); - time3.parse("00:00:00+00:00"); - time3.parse("00:00:00+0000"); - time3.parse("00:00:00.000Z"); - time3.parse("00:00:00.000+00:00"); - time3.parse("00:00:00.000+0000"); - expect(() => time3.parse("")).toThrow(); - expect(() => time3.parse("foo")).toThrow(); - expect(() => time3.parse("00:00:00")).toThrow(); - expect(() => time3.parse("00:00:00.000")).toThrow(); - - const time4 = z.string().time({ offset: true, precision: 0 }); - time4.parse("00:00:00Z"); - time4.parse("09:52:31Z"); - time4.parse("00:00:00+00:00"); - time4.parse("00:00:00+0000"); - expect(() => time4.parse("")).toThrow(); - expect(() => time4.parse("foo")).toThrow(); - expect(() => time4.parse("00:00:00.0")).toThrow(); - expect(() => time4.parse("00:00:00.000")).toThrow(); - expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); + // const time3 = z.string().time({ offset: true }); + // time3.parse("00:00:00Z"); + // time3.parse("09:52:31Z"); + // time3.parse("00:00:00+00:00"); + // time3.parse("00:00:00+0000"); + // time3.parse("00:00:00.000Z"); + // time3.parse("00:00:00.000+00:00"); + // time3.parse("00:00:00.000+0000"); + // expect(() => time3.parse("")).toThrow(); + // expect(() => time3.parse("foo")).toThrow(); + // expect(() => time3.parse("00:00:00")).toThrow(); + // expect(() => time3.parse("00:00:00.000")).toThrow(); + + // const time4 = z.string().time({ offset: true, precision: 0 }); + // time4.parse("00:00:00Z"); + // time4.parse("09:52:31Z"); + // time4.parse("00:00:00+00:00"); + // time4.parse("00:00:00+0000"); + // expect(() => time4.parse("")).toThrow(); + // expect(() => time4.parse("foo")).toThrow(); + // expect(() => time4.parse("00:00:00.0")).toThrow(); + // expect(() => time4.parse("00:00:00.000")).toThrow(); + // expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); }); test("IP validation", () => { diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 0a96b0962..65ff8130b 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -543,17 +543,18 @@ export type ZodStringCheck = | { kind: "datetime"; offset: boolean; + local: boolean; precision: number | null; - withDate: true; - withTime: true; message?: string; } - | { kind: "date"; withDate: true; message?: string } + | { + kind: "date"; + // withDate: true; + message?: string; + } | { kind: "time"; - offset: boolean; precision: number | null; - withTime: true; message?: string; } | { kind: "ip"; version?: IpVersion; message?: string }; @@ -599,46 +600,40 @@ const ipv4Regex = const ipv6Regex = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; -// Adapted from https://stackoverflow.com/a/3143231 -const datetimeRegex = (args: { - precision?: number | null; - offset?: boolean; - withDate?: boolean; - withTime?: boolean; -}) => { - let regex = "^"; - - if (args.withDate) { - regex = `${regex}\\d{4}-\\d{2}-\\d{2}`; - - if (args.withTime) { - regex = `${regex}T`; - } - } - - if (!args.withTime) { - return new RegExp(`${regex}$`); - } - - regex = `${regex}\\d{2}:\\d{2}:\\d{2}`; +const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args: { precision?: number | null }) { + let regex = `\\d{2}:\\d{2}:\\d{2}`; if (args.precision) { regex = `${regex}\\.\\d{${args.precision}}`; } else if (args.precision == null) { regex = `${regex}(\\.\\d+)?`; } - // in case of `precision = 0`, don't add the decimal part. - - if (args.offset) { - return new RegExp(`${regex}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } - - if (args.withDate) { - return new RegExp(`${regex}Z$`); - } + return regex; +} +function timeRegex(args: { + offset?: boolean; + local?: boolean; + precision?: number | null; +}) { + return new RegExp(`^${timeRegexSource(args)}$`); +} - return new RegExp(`${regex}$`); -}; +// Adapted from https://stackoverflow.com/a/3143231 +function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + + const opts: string[] = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) opts.push(`([+-]\\d{2}(:?\\d{2})?)`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} function isValidIP(ip: string, version?: IpVersion) { if ((version === "v4" || !version) && ipv4Regex.test(ip)) { @@ -870,7 +865,7 @@ export class ZodString extends ZodType { status.dirty(); } } else if (check.kind === "date") { - const regex = datetimeRegex(check); + const regex = dateRegex; if (!regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -882,7 +877,7 @@ export class ZodString extends ZodType { status.dirty(); } } else if (check.kind === "time") { - const regex = datetimeRegex(check); + const regex = timeRegex(check); if (!regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -970,6 +965,7 @@ export class ZodString extends ZodType { message?: string | undefined; precision?: number | null; offset?: boolean; + local?: boolean; } ) { if (typeof options === "string") { @@ -977,24 +973,23 @@ export class ZodString extends ZodType { kind: "datetime", precision: null, offset: false, - withDate: true, - withTime: true, + local: false, message: options, }); } return this._addCheck({ kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, offset: options?.offset ?? false, - withDate: true, - withTime: true, + local: options?.local ?? false, ...errorUtil.errToObj(options?.message), }); } date(message?: string) { - return this._addCheck({ kind: "date", withDate: true, message }); + return this._addCheck({ kind: "date", message }); } time( @@ -1003,15 +998,12 @@ export class ZodString extends ZodType { | { message?: string | undefined; precision?: number | null; - offset?: boolean; } ) { if (typeof options === "string") { return this._addCheck({ kind: "time", precision: null, - offset: false, - withTime: true, message: options, }); } @@ -1019,8 +1011,6 @@ export class ZodString extends ZodType { kind: "time", precision: typeof options?.precision === "undefined" ? null : options?.precision, - offset: options?.offset ?? false, - withTime: true, ...errorUtil.errToObj(options?.message), }); } diff --git a/playground.ts b/playground.ts index 4e01473b6..1f1055416 100644 --- a/playground.ts +++ b/playground.ts @@ -1,3 +1,8 @@ import { z } from "./src"; z; + +const time = z.string().time(); +time.parse("00:00:00"); +time.parse("09:52:31"); +time.parse("23:59:59.9999999"); diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 52d4bb843..7728ed734 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -233,8 +233,8 @@ test("nanoid", () => { const nanoid = z.string().nanoid("custom error"); nanoid.parse("lfNZluvAxMkf7Q8C5H-QS"); nanoid.parse("mIU_4PJWikaU8fMbmkouz"); - nanoid.parse("Hb9ZUtUa2JDm_dD-47EGv"); - nanoid.parse("5Noocgv_8vQ9oPijj4ioQ"); + nanoid.parse("Hb9ZUtUa2JDm_dD-47EGv"); + nanoid.parse("5Noocgv_8vQ9oPijj4ioQ"); const result = nanoid.safeParse("Xq90uDyhddC53KsoASYJGX"); expect(result.success).toEqual(false); if (!result.success) { @@ -565,29 +565,29 @@ test("time parsing", () => { expect(() => time2.parse("00:00:00.000")).toThrow(); expect(() => time2.parse("00:00:00.00+00:00")).toThrow(); - const time3 = z.string().time({ offset: true }); - time3.parse("00:00:00Z"); - time3.parse("09:52:31Z"); - time3.parse("00:00:00+00:00"); - time3.parse("00:00:00+0000"); - time3.parse("00:00:00.000Z"); - time3.parse("00:00:00.000+00:00"); - time3.parse("00:00:00.000+0000"); - expect(() => time3.parse("")).toThrow(); - expect(() => time3.parse("foo")).toThrow(); - expect(() => time3.parse("00:00:00")).toThrow(); - expect(() => time3.parse("00:00:00.000")).toThrow(); - - const time4 = z.string().time({ offset: true, precision: 0 }); - time4.parse("00:00:00Z"); - time4.parse("09:52:31Z"); - time4.parse("00:00:00+00:00"); - time4.parse("00:00:00+0000"); - expect(() => time4.parse("")).toThrow(); - expect(() => time4.parse("foo")).toThrow(); - expect(() => time4.parse("00:00:00.0")).toThrow(); - expect(() => time4.parse("00:00:00.000")).toThrow(); - expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); + // const time3 = z.string().time({ offset: true }); + // time3.parse("00:00:00Z"); + // time3.parse("09:52:31Z"); + // time3.parse("00:00:00+00:00"); + // time3.parse("00:00:00+0000"); + // time3.parse("00:00:00.000Z"); + // time3.parse("00:00:00.000+00:00"); + // time3.parse("00:00:00.000+0000"); + // expect(() => time3.parse("")).toThrow(); + // expect(() => time3.parse("foo")).toThrow(); + // expect(() => time3.parse("00:00:00")).toThrow(); + // expect(() => time3.parse("00:00:00.000")).toThrow(); + + // const time4 = z.string().time({ offset: true, precision: 0 }); + // time4.parse("00:00:00Z"); + // time4.parse("09:52:31Z"); + // time4.parse("00:00:00+00:00"); + // time4.parse("00:00:00+0000"); + // expect(() => time4.parse("")).toThrow(); + // expect(() => time4.parse("foo")).toThrow(); + // expect(() => time4.parse("00:00:00.0")).toThrow(); + // expect(() => time4.parse("00:00:00.000")).toThrow(); + // expect(() => time4.parse("00:00:00.000+00:00")).toThrow(); }); test("IP validation", () => { diff --git a/src/types.ts b/src/types.ts index 75a6b2676..1b5c22cc8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -543,17 +543,18 @@ export type ZodStringCheck = | { kind: "datetime"; offset: boolean; + local: boolean; precision: number | null; - withDate: true; - withTime: true; message?: string; } - | { kind: "date"; withDate: true; message?: string } + | { + kind: "date"; + // withDate: true; + message?: string; + } | { kind: "time"; - offset: boolean; precision: number | null; - withTime: true; message?: string; } | { kind: "ip"; version?: IpVersion; message?: string }; @@ -599,46 +600,40 @@ const ipv4Regex = const ipv6Regex = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/; -// Adapted from https://stackoverflow.com/a/3143231 -const datetimeRegex = (args: { - precision?: number | null; - offset?: boolean; - withDate?: boolean; - withTime?: boolean; -}) => { - let regex = "^"; - - if (args.withDate) { - regex = `${regex}\\d{4}-\\d{2}-\\d{2}`; - - if (args.withTime) { - regex = `${regex}T`; - } - } - - if (!args.withTime) { - return new RegExp(`${regex}$`); - } - - regex = `${regex}\\d{2}:\\d{2}:\\d{2}`; +const dateRegexSource = `\\d{4}-\\d{2}-\\d{2}`; +const dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args: { precision?: number | null }) { + let regex = `\\d{2}:\\d{2}:\\d{2}`; if (args.precision) { regex = `${regex}\\.\\d{${args.precision}}`; } else if (args.precision == null) { regex = `${regex}(\\.\\d+)?`; } - // in case of `precision = 0`, don't add the decimal part. - - if (args.offset) { - return new RegExp(`${regex}(([+-]\\d{2}(:?\\d{2})?)|Z)$`); - } - - if (args.withDate) { - return new RegExp(`${regex}Z$`); - } + return regex; +} +function timeRegex(args: { + offset?: boolean; + local?: boolean; + precision?: number | null; +}) { + return new RegExp(`^${timeRegexSource(args)}$`); +} - return new RegExp(`${regex}$`); -}; +// Adapted from https://stackoverflow.com/a/3143231 +function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + + const opts: string[] = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) opts.push(`([+-]\\d{2}(:?\\d{2})?)`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} function isValidIP(ip: string, version?: IpVersion) { if ((version === "v4" || !version) && ipv4Regex.test(ip)) { @@ -870,7 +865,7 @@ export class ZodString extends ZodType { status.dirty(); } } else if (check.kind === "date") { - const regex = datetimeRegex(check); + const regex = dateRegex; if (!regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -882,7 +877,7 @@ export class ZodString extends ZodType { status.dirty(); } } else if (check.kind === "time") { - const regex = datetimeRegex(check); + const regex = timeRegex(check); if (!regex.test(input.data)) { ctx = this._getOrReturnCtx(input, ctx); @@ -970,6 +965,7 @@ export class ZodString extends ZodType { message?: string | undefined; precision?: number | null; offset?: boolean; + local?: boolean; } ) { if (typeof options === "string") { @@ -977,24 +973,23 @@ export class ZodString extends ZodType { kind: "datetime", precision: null, offset: false, - withDate: true, - withTime: true, + local: false, message: options, }); } return this._addCheck({ kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, offset: options?.offset ?? false, - withDate: true, - withTime: true, + local: options?.local ?? false, ...errorUtil.errToObj(options?.message), }); } date(message?: string) { - return this._addCheck({ kind: "date", withDate: true, message }); + return this._addCheck({ kind: "date", message }); } time( @@ -1003,15 +998,12 @@ export class ZodString extends ZodType { | { message?: string | undefined; precision?: number | null; - offset?: boolean; } ) { if (typeof options === "string") { return this._addCheck({ kind: "time", precision: null, - offset: false, - withTime: true, message: options, }); } @@ -1019,8 +1011,6 @@ export class ZodString extends ZodType { kind: "time", precision: typeof options?.precision === "undefined" ? null : options?.precision, - offset: options?.offset ?? false, - withTime: true, ...errorUtil.errToObj(options?.message), }); }