From c7811f2e2e7992d5e9217a12ba5240df8e992da2 Mon Sep 17 00:00:00 2001 From: "Matt R. Wilson" Date: Sat, 24 Feb 2024 18:15:17 -0700 Subject: [PATCH] Update Duration to better support ISO 8601-2 Use a better regex to support negatives and decimal fractions to the smallest value. Add a lot of valid and invalid inputs to the test. --- deno/lib/__tests__/string.test.ts | 49 ++++++++++++++++++++++++++++--- deno/lib/types.ts | 2 +- src/__tests__/string.test.ts | 49 ++++++++++++++++++++++++++++--- src/types.ts | 2 +- 4 files changed, 92 insertions(+), 10 deletions(-) diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 0a8a8d081..fee7da743 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -675,11 +675,52 @@ test("duration", () => { const duration = z.string().duration(); expect(duration.isDuration).toEqual(true); - duration.parse("P3Y6M4DT12H30M5S"); + const validDurations = [ + "P3Y6M4DT12H30M5S", + "P2Y9M3DT12H31M8.001S", + "+P3Y6M4DT12H30M5S", + "-PT0.001S", + "+PT0.001S", + "PT0,001S", + "PT12H30M5S", + "-P2M1D", + "P-2M-1D", + "-P5DT10H", + "P-5DT-10H", + "P1Y", + "P2MT30M", + "PT6H", + "P5W", + "P0.5Y", + "P0,5Y", + "P42YT7.004M", + ]; + + const invalidDurations = [ + "foo bar", + "", + " ", + "P", + "T1H", + "P0.5Y1D", + "P0,5Y6M", + "P1YT", + ]; + + for (const val of validDurations) { + const result = duration.safeParse(val); + if (!result.success) { + throw Error(`Valid duration could not be parsed: ${val}`); + } + } + + for (const val of invalidDurations) { + const result = duration.safeParse(val); + + if (result.success) { + throw Error(`Invalid duration was successful parsed: ${val}`); + } - const result = duration.safeParse("invalidDuration"); - expect(result.success).toEqual(false); - if (!result.success) { expect(result.error.issues[0].message).toEqual("Invalid duration"); } >>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 7b65ff00b..4af0d141c 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -588,7 +588,7 @@ const uuidRegex = const nanoidRegex = /^[a-z0-9_-]{21}$/i; ======= const durationRegex = - /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/; + /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; >>>>>>> 29773e8 (feat: Add support for ISO-8601 Durations) // from https://stackoverflow.com/a/46181/1550155 diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index e3e976b25..db01a64eb 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -674,11 +674,52 @@ test("duration", () => { const duration = z.string().duration(); expect(duration.isDuration).toEqual(true); - duration.parse("P3Y6M4DT12H30M5S"); + const validDurations = [ + "P3Y6M4DT12H30M5S", + "P2Y9M3DT12H31M8.001S", + "+P3Y6M4DT12H30M5S", + "-PT0.001S", + "+PT0.001S", + "PT0,001S", + "PT12H30M5S", + "-P2M1D", + "P-2M-1D", + "-P5DT10H", + "P-5DT-10H", + "P1Y", + "P2MT30M", + "PT6H", + "P5W", + "P0.5Y", + "P0,5Y", + "P42YT7.004M", + ]; + + const invalidDurations = [ + "foo bar", + "", + " ", + "P", + "T1H", + "P0.5Y1D", + "P0,5Y6M", + "P1YT", + ]; + + for (const val of validDurations) { + const result = duration.safeParse(val); + if (!result.success) { + throw Error(`Valid duration could not be parsed: ${val}`); + } + } + + for (const val of invalidDurations) { + const result = duration.safeParse(val); + + if (result.success) { + throw Error(`Invalid duration was successful parsed: ${val}`); + } - const result = duration.safeParse("invalidDuration"); - expect(result.success).toEqual(false); - if (!result.success) { expect(result.error.issues[0].message).toEqual("Invalid duration"); } }); diff --git a/src/types.ts b/src/types.ts index 15aa011f4..1205276b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -582,7 +582,7 @@ const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; const nanoidRegex = /^[a-z0-9_-]{21}$/i; const durationRegex = - /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/; + /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; // from https://stackoverflow.com/a/46181/1550155 // old version: too slow, didn't support unicode