@hy_ong/zod-kit 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,693 @@
1
+ import { describe, it, expect, beforeEach } from "vitest"
2
+ import { datetime, setLocale, validateDateTimeFormat, parseDateTimeValue, normalizeDateTimeValue } from "../../src"
3
+ import dayjs from "dayjs"
4
+
5
+ describe("Taiwan datetime() validator", () => {
6
+ beforeEach(() => setLocale("en"))
7
+
8
+ describe("basic functionality", () => {
9
+ it("should validate correct datetime formats", () => {
10
+ const schemaISO = datetime({ format: "YYYY-MM-DD HH:mm" })
11
+ const schemaUS = datetime({ format: "MM/DD/YYYY HH:mm" })
12
+ const schemaEU = datetime({ format: "DD/MM/YYYY HH:mm" })
13
+ const schemaWithSeconds = datetime({ format: "YYYY-MM-DD HH:mm:ss" })
14
+ const schema12Hour = datetime({ format: "YYYY-MM-DD hh:mm A" })
15
+
16
+ // ISO format
17
+ expect(schemaISO.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
18
+ expect(schemaISO.parse("2024-12-31 23:59")).toBe("2024-12-31 23:59")
19
+ expect(schemaISO.parse("2024-01-01 00:00")).toBe("2024-01-01 00:00")
20
+
21
+ // US format
22
+ expect(schemaUS.parse("03/15/2024 14:30")).toBe("03/15/2024 14:30")
23
+ expect(schemaUS.parse("12/31/2024 23:59")).toBe("12/31/2024 23:59")
24
+
25
+ // EU format
26
+ expect(schemaEU.parse("15/03/2024 14:30")).toBe("15/03/2024 14:30")
27
+ expect(schemaEU.parse("31/12/2024 23:59")).toBe("31/12/2024 23:59")
28
+
29
+ // With seconds
30
+ expect(schemaWithSeconds.parse("2024-03-15 14:30:45")).toBe("2024-03-15 14:30:45")
31
+ expect(schemaWithSeconds.parse("2024-01-01 00:00:00")).toBe("2024-01-01 00:00:00")
32
+
33
+ // 12-hour format
34
+ expect(schema12Hour.parse("2024-03-15 02:30 PM")).toBe("2024-03-15 02:30 PM")
35
+ expect(schema12Hour.parse("2024-03-15 12:00 AM")).toBe("2024-03-15 12:00 AM")
36
+ expect(schema12Hour.parse("2024-03-15 12:00 PM")).toBe("2024-03-15 12:00 PM")
37
+ })
38
+
39
+ it("should validate special formats", () => {
40
+ const isoSchema = datetime({ format: "ISO" })
41
+ const rfcSchema = datetime({ format: "RFC" })
42
+ const unixSchema = datetime({ format: "UNIX" })
43
+
44
+ // ISO 8601
45
+ expect(isoSchema.parse("2024-03-15T14:30:45.000Z")).toBe("2024-03-15T14:30:45.000Z")
46
+ expect(isoSchema.parse("2024-03-15T14:30:45Z")).toBe("2024-03-15T14:30:45Z")
47
+
48
+ // RFC 2822
49
+ expect(rfcSchema.parse("Fri, 15 Mar 2024 14:30:45 GMT")).toBe("Fri, 15 Mar 2024 14:30:45 GMT")
50
+
51
+ // Unix timestamp
52
+ expect(unixSchema.parse("1710508245")).toBe("1710508245")
53
+ })
54
+
55
+ it("should reject invalid datetime formats", () => {
56
+ const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
57
+
58
+ // Invalid date formats
59
+ expect(() => schema.parse("2024-13-15 14:30")).toThrow("Must be in YYYY-MM-DD HH:mm format")
60
+ expect(() => schema.parse("2024-03-32 14:30")).toThrow("Must be in YYYY-MM-DD HH:mm format")
61
+ expect(() => schema.parse("2024-03-15 25:30")).toThrow("Must be in YYYY-MM-DD HH:mm format")
62
+ expect(() => schema.parse("2024-03-15 14:70")).toThrow("Must be in YYYY-MM-DD HH:mm format")
63
+ expect(() => schema.parse("abc")).toThrow("Must be in YYYY-MM-DD HH:mm format")
64
+ expect(() => schema.parse("2024/03/15 14:30")).toThrow("Must be in YYYY-MM-DD HH:mm format") // Wrong format
65
+ })
66
+
67
+ it("should handle whitespace trimming", () => {
68
+ const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
69
+
70
+ expect(schema.parse(" 2024-03-15 14:30 ")).toBe("2024-03-15 14:30")
71
+ expect(schema.parse("\t2024-03-15 09:15\n")).toBe("2024-03-15 09:15")
72
+ })
73
+ })
74
+
75
+ describe("whitelist functionality", () => {
76
+ it("should accept any string in whitelist regardless of format", () => {
77
+ const schema = datetime({
78
+ format: "YYYY-MM-DD HH:mm",
79
+ whitelist: ["now", "tomorrow", "TBD"]
80
+ })
81
+
82
+ expect(schema.parse("now")).toBe("now")
83
+ expect(schema.parse("tomorrow")).toBe("tomorrow")
84
+ expect(schema.parse("TBD")).toBe("TBD")
85
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30") // Valid datetime still works
86
+ })
87
+
88
+ it("should reject values not in whitelist when whitelistOnly is true", () => {
89
+ const schema = datetime({
90
+ format: "YYYY-MM-DD HH:mm",
91
+ whitelist: ["now", "2024-03-15 14:30"],
92
+ whitelistOnly: true
93
+ })
94
+
95
+ expect(schema.parse("now")).toBe("now")
96
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
97
+
98
+ // Invalid datetimes not in whitelist should be rejected
99
+ expect(() => schema.parse("2024-03-15 15:30")).toThrow("DateTime is not in the allowed list")
100
+ expect(() => schema.parse("later")).toThrow("DateTime is not in the allowed list")
101
+ })
102
+
103
+ it("should work with empty whitelist", () => {
104
+ const schema = datetime({
105
+ format: "YYYY-MM-DD HH:mm",
106
+ whitelist: []
107
+ })
108
+
109
+ // With empty whitelist, should still validate datetime format
110
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
111
+ expect(() => schema.parse("invalid")).toThrow("Must be in YYYY-MM-DD HH:mm format")
112
+ })
113
+
114
+ it("should prioritize whitelist over format validation", () => {
115
+ const schema = datetime({
116
+ format: "YYYY-MM-DD HH:mm",
117
+ whitelist: ["anytime", "flexible", "TBD"]
118
+ })
119
+
120
+ expect(schema.parse("anytime")).toBe("anytime")
121
+ expect(schema.parse("flexible")).toBe("flexible")
122
+ expect(schema.parse("TBD")).toBe("TBD")
123
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30") // Valid datetime still works
124
+ })
125
+ })
126
+
127
+ describe("required/optional behavior", () => {
128
+ it("should handle required=true (default)", () => {
129
+ const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
130
+
131
+ expect(() => schema.parse("")).toThrow("Required")
132
+ expect(() => schema.parse(null)).toThrow("Required")
133
+ expect(() => schema.parse(undefined)).toThrow("Required")
134
+ })
135
+
136
+ it("should handle required=false", () => {
137
+ const schema = datetime({ format: "YYYY-MM-DD HH:mm", required: false })
138
+
139
+ expect(schema.parse("")).toBe(null)
140
+ expect(schema.parse(null)).toBe(null)
141
+ expect(schema.parse(undefined)).toBe(null)
142
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
143
+ })
144
+
145
+ it("should use default values", () => {
146
+ const requiredSchema = datetime({
147
+ format: "YYYY-MM-DD HH:mm",
148
+ defaultValue: "2024-01-01 12:00"
149
+ })
150
+ const optionalSchema = datetime({
151
+ format: "YYYY-MM-DD HH:mm",
152
+ required: false,
153
+ defaultValue: "2024-01-01 12:00"
154
+ })
155
+
156
+ expect(requiredSchema.parse("")).toBe("2024-01-01 12:00")
157
+ expect(requiredSchema.parse(null)).toBe("2024-01-01 12:00")
158
+
159
+ expect(optionalSchema.parse("")).toBe("2024-01-01 12:00")
160
+ expect(optionalSchema.parse(null)).toBe("2024-01-01 12:00")
161
+ })
162
+
163
+ it("should handle whitelist with optional fields", () => {
164
+ const schema = datetime({
165
+ format: "YYYY-MM-DD HH:mm",
166
+ required: false,
167
+ whitelist: ["flexible", "2024-03-15 14:30"],
168
+ whitelistOnly: true
169
+ })
170
+
171
+ expect(schema.parse("")).toBe(null)
172
+ expect(schema.parse("flexible")).toBe("flexible")
173
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
174
+ expect(() => schema.parse("2024-03-15 15:30")).toThrow("DateTime is not in the allowed list")
175
+ })
176
+ })
177
+
178
+ describe("datetime range validation", () => {
179
+ it("should validate minimum and maximum datetimes", () => {
180
+ const schema = datetime({
181
+ format: "YYYY-MM-DD HH:mm",
182
+ min: "2024-03-15 09:00",
183
+ max: "2024-03-15 17:00"
184
+ })
185
+
186
+ // Valid datetimes within range
187
+ expect(schema.parse("2024-03-15 09:00")).toBe("2024-03-15 09:00")
188
+ expect(schema.parse("2024-03-15 12:30")).toBe("2024-03-15 12:30")
189
+ expect(schema.parse("2024-03-15 17:00")).toBe("2024-03-15 17:00")
190
+
191
+ // Invalid datetimes outside range
192
+ expect(() => schema.parse("2024-03-15 08:59")).toThrow("DateTime must be after")
193
+ expect(() => schema.parse("2024-03-15 17:01")).toThrow("DateTime must be before")
194
+ })
195
+
196
+ it("should validate hour ranges", () => {
197
+ const schema = datetime({
198
+ format: "YYYY-MM-DD HH:mm",
199
+ minHour: 9,
200
+ maxHour: 17
201
+ })
202
+
203
+ expect(schema.parse("2024-03-15 09:30")).toBe("2024-03-15 09:30")
204
+ expect(schema.parse("2024-03-15 17:30")).toBe("2024-03-15 17:30")
205
+
206
+ expect(() => schema.parse("2024-03-15 08:30")).toThrow("Hour must be between")
207
+ expect(() => schema.parse("2024-03-15 18:30")).toThrow("Hour must be between")
208
+ })
209
+
210
+ it("should validate allowed hours", () => {
211
+ const schema = datetime({
212
+ format: "YYYY-MM-DD HH:mm",
213
+ allowedHours: [9, 10, 14, 15, 16]
214
+ })
215
+
216
+ expect(schema.parse("2024-03-15 09:30")).toBe("2024-03-15 09:30")
217
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
218
+
219
+ expect(() => schema.parse("2024-03-15 08:30")).toThrow("Hour must be between")
220
+ expect(() => schema.parse("2024-03-15 11:30")).toThrow("Hour must be between")
221
+ expect(() => schema.parse("2024-03-15 17:30")).toThrow("Hour must be between")
222
+ })
223
+
224
+ it("should validate minute steps", () => {
225
+ const schema = datetime({
226
+ format: "YYYY-MM-DD HH:mm",
227
+ minuteStep: 15
228
+ })
229
+
230
+ expect(schema.parse("2024-03-15 14:00")).toBe("2024-03-15 14:00")
231
+ expect(schema.parse("2024-03-15 14:15")).toBe("2024-03-15 14:15")
232
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
233
+ expect(schema.parse("2024-03-15 14:45")).toBe("2024-03-15 14:45")
234
+
235
+ expect(() => schema.parse("2024-03-15 14:05")).toThrow("Minutes must be in 15-minute intervals")
236
+ expect(() => schema.parse("2024-03-15 14:37")).toThrow("Minutes must be in 15-minute intervals")
237
+ })
238
+ })
239
+
240
+ describe("temporal validation", () => {
241
+ it("should validate past dates", () => {
242
+ const schema = datetime({
243
+ format: "YYYY-MM-DD HH:mm",
244
+ mustBePast: true
245
+ })
246
+
247
+ const pastDate = dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm")
248
+ const futureDate = dayjs().add(1, "day").format("YYYY-MM-DD HH:mm")
249
+
250
+ expect(schema.parse(pastDate)).toBe(pastDate)
251
+ expect(() => schema.parse(futureDate)).toThrow("DateTime must be in the past")
252
+ })
253
+
254
+ it("should validate future dates", () => {
255
+ const schema = datetime({
256
+ format: "YYYY-MM-DD HH:mm",
257
+ mustBeFuture: true
258
+ })
259
+
260
+ const pastDate = dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm")
261
+ const futureDate = dayjs().add(1, "day").format("YYYY-MM-DD HH:mm")
262
+
263
+ expect(schema.parse(futureDate)).toBe(futureDate)
264
+ expect(() => schema.parse(pastDate)).toThrow("DateTime must be in the future")
265
+ })
266
+
267
+ it("should validate today", () => {
268
+ const schema = datetime({
269
+ format: "YYYY-MM-DD HH:mm",
270
+ mustBeToday: true
271
+ })
272
+
273
+ const todayDate = dayjs().format("YYYY-MM-DD") + " 14:30"
274
+ const yesterdayDate = dayjs().subtract(1, "day").format("YYYY-MM-DD") + " 14:30"
275
+
276
+ expect(schema.parse(todayDate)).toBe(todayDate)
277
+ expect(() => schema.parse(yesterdayDate)).toThrow("DateTime must be today")
278
+ })
279
+
280
+ it("should validate weekdays and weekends", () => {
281
+ const weekdaySchema = datetime({
282
+ format: "YYYY-MM-DD HH:mm",
283
+ weekdaysOnly: true
284
+ })
285
+
286
+ const weekendSchema = datetime({
287
+ format: "YYYY-MM-DD HH:mm",
288
+ weekendsOnly: true
289
+ })
290
+
291
+ // Find a Monday and a Saturday
292
+ const monday = "2024-03-18 14:30" // This is a Monday
293
+ const saturday = "2024-03-16 14:30" // This is a Saturday
294
+
295
+ expect(weekdaySchema.parse(monday)).toBe(monday)
296
+ expect(() => weekdaySchema.parse(saturday)).toThrow("DateTime must be a weekday")
297
+
298
+ expect(weekendSchema.parse(saturday)).toBe(saturday)
299
+ expect(() => weekendSchema.parse(monday)).toThrow("DateTime must be a weekend")
300
+ })
301
+ })
302
+
303
+ describe("timezone support", () => {
304
+ it("should handle timezone parsing", () => {
305
+ const schema = datetime({
306
+ format: "YYYY-MM-DD HH:mm",
307
+ timezone: "Asia/Taipei"
308
+ })
309
+
310
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
311
+ })
312
+
313
+ it("should validate with timezone-aware ranges", () => {
314
+ const schema = datetime({
315
+ format: "YYYY-MM-DD HH:mm",
316
+ timezone: "Asia/Taipei",
317
+ min: "2024-03-15 09:00",
318
+ max: "2024-03-15 17:00"
319
+ })
320
+
321
+ expect(schema.parse("2024-03-15 12:30")).toBe("2024-03-15 12:30")
322
+ expect(() => schema.parse("2024-03-15 08:30")).toThrow("DateTime must be after")
323
+ })
324
+ })
325
+
326
+ describe("transform function", () => {
327
+ it("should apply custom transform", () => {
328
+ const schema = datetime({
329
+ format: "YYYY-MM-DD HH:mm",
330
+ transform: (val) => val.replace(/\//g, "-")
331
+ })
332
+
333
+ expect(schema.parse("2024/03/15 14:30")).toBe("2024-03-15 14:30")
334
+ })
335
+
336
+ it("should apply transform before validation", () => {
337
+ const schema = datetime({
338
+ format: "YYYY-MM-DD HH:mm",
339
+ transform: (val) => val.replace(/T/g, " ").replace(/Z$/, "")
340
+ })
341
+
342
+ expect(schema.parse("2024-03-15T14:30Z")).toBe("2024-03-15 14:30")
343
+ })
344
+
345
+ it("should work with whitelist after transform", () => {
346
+ const schema = datetime({
347
+ format: "YYYY-MM-DD HH:mm",
348
+ whitelist: ["NOW", "2024-03-15 14:30"],
349
+ transform: (val) => val.toUpperCase()
350
+ })
351
+
352
+ expect(schema.parse("now")).toBe("NOW")
353
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
354
+ })
355
+ })
356
+
357
+ describe("input preprocessing", () => {
358
+ it("should handle string conversion", () => {
359
+ const schema = datetime({ format: "UNIX" })
360
+
361
+ // Test string conversion of numbers
362
+ expect(schema.parse(1710508245)).toBe("1710508245")
363
+ })
364
+
365
+ it("should trim whitespace", () => {
366
+ const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
367
+
368
+ expect(schema.parse(" 2024-03-15 14:30 ")).toBe("2024-03-15 14:30")
369
+ expect(schema.parse("\t2024-03-15 09:15\n")).toBe("2024-03-15 09:15")
370
+ })
371
+
372
+ it("should handle different trim modes", () => {
373
+ const trimStartSchema = datetime({
374
+ format: "YYYY-MM-DD HH:mm",
375
+ trimMode: "trimStart"
376
+ })
377
+ const trimEndSchema = datetime({
378
+ format: "YYYY-MM-DD HH:mm",
379
+ trimMode: "trimEnd"
380
+ })
381
+ const noTrimSchema = datetime({
382
+ format: "YYYY-MM-DD HH:mm",
383
+ trimMode: "none"
384
+ })
385
+
386
+ expect(trimStartSchema.parse(" 2024-03-15 14:30 ")).toBe("2024-03-15 14:30 ")
387
+ expect(trimEndSchema.parse(" 2024-03-15 14:30 ")).toBe(" 2024-03-15 14:30")
388
+ expect(noTrimSchema.parse(" 2024-03-15 14:30 ")).toBe(" 2024-03-15 14:30 ")
389
+ })
390
+
391
+ it("should handle case transformations", () => {
392
+ const upperSchema = datetime({
393
+ format: "YYYY-MM-DD HH:mm",
394
+ whitelist: ["NOW", "TOMORROW"],
395
+ casing: "upper"
396
+ })
397
+
398
+ expect(upperSchema.parse("now")).toBe("NOW")
399
+ expect(upperSchema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
400
+ })
401
+ })
402
+
403
+ describe("custom regex validation", () => {
404
+ it("should use custom regex instead of format validation", () => {
405
+ const schema = datetime({
406
+ regex: /^(morning|afternoon|evening|night)$/
407
+ })
408
+
409
+ expect(schema.parse("morning")).toBe("morning")
410
+ expect(schema.parse("evening")).toBe("evening")
411
+ expect(() => schema.parse("invalid")).toThrow("Invalid datetime format")
412
+ })
413
+
414
+ it("should skip datetime parsing with custom regex", () => {
415
+ const schema = datetime({
416
+ regex: /^(now|later|asap)$/,
417
+ mustBePast: true // This should be ignored when using regex
418
+ })
419
+
420
+ expect(schema.parse("now")).toBe("now")
421
+ expect(schema.parse("later")).toBe("later")
422
+ expect(() => schema.parse("invalid")).toThrow("Invalid datetime format")
423
+ })
424
+ })
425
+
426
+ describe("includes/excludes validation", () => {
427
+ it("should validate includes", () => {
428
+ const schema = datetime({
429
+ format: "YYYY-MM-DD HH:mm",
430
+ includes: "2024"
431
+ })
432
+
433
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
434
+ expect(() => schema.parse("2023-03-15 14:30")).toThrow("Must include 2024")
435
+ })
436
+
437
+ it("should validate excludes", () => {
438
+ const schema = datetime({
439
+ regex: /^[\d\-\s:test]+$/, // Custom regex to allow "test" in the string
440
+ excludes: ["test", "invalid"]
441
+ })
442
+
443
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
444
+ expect(() => schema.parse("2024-test-15 14:30")).toThrow("Must not contain test")
445
+ })
446
+ })
447
+
448
+ describe("utility functions", () => {
449
+ describe("validateDateTimeFormat", () => {
450
+ it("should correctly validate datetime formats", () => {
451
+ expect(validateDateTimeFormat("2024-03-15 14:30", "YYYY-MM-DD HH:mm")).toBe(true)
452
+ expect(validateDateTimeFormat("2024-03-15 14:30:45", "YYYY-MM-DD HH:mm:ss")).toBe(true)
453
+ expect(validateDateTimeFormat("15/03/2024 14:30", "DD/MM/YYYY HH:mm")).toBe(true)
454
+ expect(validateDateTimeFormat("2024-03-15T14:30:45.000Z", "ISO")).toBe(true)
455
+ expect(validateDateTimeFormat("1710508245", "UNIX")).toBe(true)
456
+
457
+ // Invalid formats
458
+ expect(validateDateTimeFormat("2024/03/15 14:30", "YYYY-MM-DD HH:mm")).toBe(false)
459
+ expect(validateDateTimeFormat("invalid", "YYYY-MM-DD HH:mm")).toBe(false)
460
+ expect(validateDateTimeFormat("2024-13-15 14:30", "YYYY-MM-DD HH:mm")).toBe(false)
461
+ })
462
+ })
463
+
464
+ describe("parseDateTimeValue", () => {
465
+ it("should parse different datetime formats correctly", () => {
466
+ const result1 = parseDateTimeValue("2024-03-15 14:30", "YYYY-MM-DD HH:mm")
467
+ expect(result1?.isValid()).toBe(true)
468
+ expect(result1?.year()).toBe(2024)
469
+ expect(result1?.month()).toBe(2) // 0-indexed
470
+ expect(result1?.date()).toBe(15)
471
+ expect(result1?.hour()).toBe(14)
472
+ expect(result1?.minute()).toBe(30)
473
+
474
+ const result2 = parseDateTimeValue("1710508245", "UNIX")
475
+ expect(result2?.isValid()).toBe(true)
476
+
477
+ const result3 = parseDateTimeValue("2024-03-15T14:30:45.000Z", "ISO")
478
+ expect(result3?.isValid()).toBe(true)
479
+ })
480
+
481
+ it("should handle invalid datetimes", () => {
482
+ expect(parseDateTimeValue("invalid", "YYYY-MM-DD HH:mm")).toBe(null)
483
+ expect(parseDateTimeValue("2024-13-15 14:30", "YYYY-MM-DD HH:mm")).toBe(null)
484
+ })
485
+
486
+ it("should handle timezone", () => {
487
+ const result = parseDateTimeValue("2024-03-15 14:30", "YYYY-MM-DD HH:mm", "Asia/Taipei")
488
+ expect(result?.isValid()).toBe(true)
489
+ })
490
+ })
491
+
492
+ describe("normalizeDateTimeValue", () => {
493
+ it("should normalize datetime to specified format", () => {
494
+ const result1 = normalizeDateTimeValue("2024-03-15 14:30", "YYYY-MM-DD HH:mm")
495
+ expect(result1).toBe("2024-03-15 14:30")
496
+
497
+ const result2 = normalizeDateTimeValue("15/03/2024 14:30", "DD/MM/YYYY HH:mm")
498
+ expect(result2).toBe("15/03/2024 14:30")
499
+ })
500
+
501
+ it("should handle different format conversions", () => {
502
+ // Note: These would need actual datetime parsing logic to work correctly
503
+ const result = normalizeDateTimeValue("1710508245", "UNIX")
504
+ expect(result).toBe("1710508245")
505
+ })
506
+
507
+ it("should handle invalid dates", () => {
508
+ expect(normalizeDateTimeValue("invalid", "YYYY-MM-DD HH:mm")).toBe(null)
509
+ })
510
+ })
511
+ })
512
+
513
+ describe("i18n support", () => {
514
+ it("should use English messages by default", () => {
515
+ setLocale("en")
516
+ expect(() => datetime().parse("")).toThrow("Required")
517
+ expect(() => datetime().parse("invalid")).toThrow("Must be in YYYY-MM-DD HH:mm format")
518
+ })
519
+
520
+ it("should use Chinese messages when locale is zh-TW", () => {
521
+ setLocale("zh-TW")
522
+ const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
523
+
524
+ expect(() => schema.parse("")).toThrow("必填")
525
+ expect(() => schema.parse("invalid")).toThrow("必須為 YYYY-MM-DD HH:mm 格式")
526
+ })
527
+
528
+ it("should support whitelist error messages", () => {
529
+ setLocale("en")
530
+ const schema = datetime({
531
+ format: "YYYY-MM-DD HH:mm",
532
+ whitelist: ["now"],
533
+ whitelistOnly: true
534
+ })
535
+
536
+ expect(() => schema.parse("2024-03-15 14:30")).toThrow("DateTime is not in the allowed list")
537
+ })
538
+
539
+ it("should support custom i18n messages", () => {
540
+ const schema = datetime({
541
+ format: "YYYY-MM-DD HH:mm",
542
+ i18n: {
543
+ en: {
544
+ required: "DateTime is required",
545
+ invalid: "Invalid datetime value"
546
+ },
547
+ "zh-TW": {
548
+ required: "請輸入日期時間",
549
+ invalid: "無效的日期時間值"
550
+ }
551
+ }
552
+ })
553
+
554
+ setLocale("en")
555
+ expect(() => schema.parse("")).toThrow("DateTime is required")
556
+
557
+ setLocale("zh-TW")
558
+ expect(() => schema.parse("")).toThrow("請輸入日期時間")
559
+ })
560
+
561
+ it("should support custom whitelist messages", () => {
562
+ const schema = datetime({
563
+ format: "YYYY-MM-DD HH:mm",
564
+ whitelist: ["now"],
565
+ whitelistOnly: true,
566
+ i18n: {
567
+ en: {
568
+ notInWhitelist: "This datetime is not allowed"
569
+ },
570
+ "zh-TW": {
571
+ notInWhitelist: "此日期時間不被允許"
572
+ }
573
+ }
574
+ })
575
+
576
+ setLocale("en")
577
+ expect(() => schema.parse("2024-03-15 14:30")).toThrow("This datetime is not allowed")
578
+
579
+ setLocale("zh-TW")
580
+ expect(() => schema.parse("2024-03-15 14:30")).toThrow("此日期時間不被允許")
581
+ })
582
+ })
583
+
584
+ describe("real world datetime scenarios", () => {
585
+ it("should validate business hours", () => {
586
+ const businessHours = datetime({
587
+ format: "YYYY-MM-DD HH:mm",
588
+ minHour: 9,
589
+ maxHour: 17,
590
+ weekdaysOnly: true,
591
+ minuteStep: 30
592
+ })
593
+
594
+ expect(businessHours.parse("2024-03-18 09:00")).toBe("2024-03-18 09:00") // Monday
595
+ expect(businessHours.parse("2024-03-18 12:30")).toBe("2024-03-18 12:30")
596
+ expect(businessHours.parse("2024-03-18 17:00")).toBe("2024-03-18 17:00")
597
+
598
+ expect(() => businessHours.parse("2024-03-16 14:30")).toThrow("DateTime must be a weekday") // Saturday
599
+ })
600
+
601
+ it("should validate appointment slots", () => {
602
+ const appointmentSlots = datetime({
603
+ format: "YYYY-MM-DD HH:mm",
604
+ allowedHours: [9, 10, 11, 14, 15, 16],
605
+ minuteStep: 30,
606
+ weekdaysOnly: true
607
+ })
608
+
609
+ expect(appointmentSlots.parse("2024-03-18 09:00")).toBe("2024-03-18 09:00")
610
+ expect(appointmentSlots.parse("2024-03-18 15:30")).toBe("2024-03-18 15:30")
611
+
612
+ expect(() => appointmentSlots.parse("2024-03-18 12:00")).toThrow("Hour must be between")
613
+ expect(() => appointmentSlots.parse("2024-03-18 09:15")).toThrow("Minutes must be in 30-minute intervals")
614
+ })
615
+
616
+ it("should handle flexible datetime input", () => {
617
+ const flexibleDateTime = datetime({
618
+ format: "YYYY-MM-DD HH:mm",
619
+ whitelist: ["now", "tomorrow", "next week", "asap"],
620
+ required: false
621
+ })
622
+
623
+ expect(flexibleDateTime.parse("now")).toBe("now")
624
+ expect(flexibleDateTime.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
625
+ expect(flexibleDateTime.parse("")).toBe(null)
626
+ expect(() => flexibleDateTime.parse("invalid")).toThrow("Must be in YYYY-MM-DD HH:mm format")
627
+ })
628
+ })
629
+
630
+ describe("edge cases", () => {
631
+ it("should handle various input types", () => {
632
+ const schema = datetime({ format: "UNIX" })
633
+
634
+ expect(schema.parse("1710508245")).toBe("1710508245")
635
+ expect(schema.parse(1710508245)).toBe("1710508245")
636
+ })
637
+
638
+ it("should handle empty and whitespace inputs", () => {
639
+ const requiredSchema = datetime({ format: "YYYY-MM-DD HH:mm", required: true })
640
+ const optionalSchema = datetime({ format: "YYYY-MM-DD HH:mm", required: false })
641
+
642
+ expect(() => requiredSchema.parse("")).toThrow("Required")
643
+ expect(() => requiredSchema.parse(" ")).toThrow("Required")
644
+
645
+ expect(optionalSchema.parse("")).toBe(null)
646
+ expect(optionalSchema.parse(" ")).toBe(null)
647
+ })
648
+
649
+ it("should preserve valid format after transformation", () => {
650
+ const schema = datetime({
651
+ format: "YYYY-MM-DD HH:mm",
652
+ transform: (val) => val.replace(/[^0-9:\-\s]/g, "")
653
+ })
654
+
655
+ expect(schema.parse("2024abc-03def-15 14:30")).toBe("2024-03-15 14:30")
656
+ })
657
+
658
+ it("should work with complex whitelist scenarios", () => {
659
+ const schema = datetime({
660
+ format: "YYYY-MM-DD HH:mm",
661
+ whitelist: ["2024-03-15 14:30", "TBD", "flexible", ""],
662
+ whitelistOnly: true,
663
+ required: false
664
+ })
665
+
666
+ // Whitelist scenarios
667
+ expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
668
+ expect(schema.parse("TBD")).toBe("TBD")
669
+ expect(schema.parse("flexible")).toBe("flexible")
670
+ expect(schema.parse("")).toBe("")
671
+ // Not in the whitelist
672
+ expect(() => schema.parse("2024-03-15 15:30")).toThrow("DateTime is not in the allowed list")
673
+ expect(() => schema.parse("other-value")).toThrow("DateTime is not in the allowed list")
674
+ })
675
+
676
+ it("should handle boundary cases for different formats", () => {
677
+ const schema24 = datetime({ format: "YYYY-MM-DD HH:mm" })
678
+ const schema12 = datetime({ format: "YYYY-MM-DD hh:mm A" })
679
+
680
+ // Valid boundary cases
681
+ expect(schema24.parse("2024-01-01 00:00")).toBe("2024-01-01 00:00")
682
+ expect(schema24.parse("2024-12-31 23:59")).toBe("2024-12-31 23:59")
683
+ expect(schema12.parse("2024-01-01 12:00 AM")).toBe("2024-01-01 12:00 AM")
684
+ expect(schema12.parse("2024-12-31 11:59 PM")).toBe("2024-12-31 11:59 PM")
685
+
686
+ // Invalid boundary cases
687
+ expect(() => schema24.parse("2024-01-01 24:00")).toThrow("Must be in YYYY-MM-DD HH:mm format")
688
+ expect(() => schema24.parse("2024-01-01 14:60")).toThrow("Must be in YYYY-MM-DD HH:mm format")
689
+ expect(() => schema12.parse("2024-01-01 00:00 AM")).toThrow("Must be in YYYY-MM-DD hh:mm A format")
690
+ expect(() => schema12.parse("2024-01-01 13:00 PM")).toThrow("Must be in YYYY-MM-DD hh:mm A format")
691
+ })
692
+ })
693
+ })