@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.
- package/.claude/settings.local.json +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Taiwan Postal Code validator for Zod Kit
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive validation for Taiwan postal codes with support for
|
|
5
|
+
* 3-digit, 5-digit (legacy), and 6-digit (current) formats based on official
|
|
6
|
+
* Chunghwa Post specifications.
|
|
7
|
+
*
|
|
8
|
+
* @author Ong Hoe Yuan
|
|
9
|
+
* @version 0.0.5
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z, ZodNullable, ZodString } from "zod"
|
|
13
|
+
import { t } from "../../i18n"
|
|
14
|
+
import { getLocale, type Locale } from "../../config"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type definition for postal code validation error messages
|
|
18
|
+
*
|
|
19
|
+
* @interface PostalCodeMessages
|
|
20
|
+
* @property {string} [required] - Message when field is required but empty
|
|
21
|
+
* @property {string} [invalid] - Message when postal code format is invalid
|
|
22
|
+
* @property {string} [invalidFormat] - Message when format doesn't match expected pattern
|
|
23
|
+
* @property {string} [invalidRange] - Message when postal code is outside valid range
|
|
24
|
+
* @property {string} [legacy5DigitWarning] - Warning message for 5-digit legacy format
|
|
25
|
+
* @property {string} [format3Only] - Message when only 3-digit format is allowed
|
|
26
|
+
* @property {string} [format5Only] - Message when only 5-digit format is allowed
|
|
27
|
+
* @property {string} [format6Only] - Message when only 6-digit format is allowed
|
|
28
|
+
*/
|
|
29
|
+
export type PostalCodeMessages = {
|
|
30
|
+
required?: string
|
|
31
|
+
invalid?: string
|
|
32
|
+
invalidFormat?: string
|
|
33
|
+
invalidRange?: string
|
|
34
|
+
legacy5DigitWarning?: string
|
|
35
|
+
format3Only?: string
|
|
36
|
+
format5Only?: string
|
|
37
|
+
format6Only?: string
|
|
38
|
+
invalidSuffix?: string
|
|
39
|
+
deprecated5Digit?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Postal code format types supported in Taiwan
|
|
44
|
+
*
|
|
45
|
+
* @typedef {"3" | "5" | "6" | "3+5" | "3+6" | "5+6" | "all"} PostalCodeFormat
|
|
46
|
+
*
|
|
47
|
+
* Available formats:
|
|
48
|
+
* - "3": 3-digit basic postal codes (100-982)
|
|
49
|
+
* - "5": 5-digit postal codes (3+2 format, legacy)
|
|
50
|
+
* - "6": 6-digit postal codes (3+3 format, current standard)
|
|
51
|
+
* - "3+5": Accept both 3-digit and 5-digit formats
|
|
52
|
+
* - "3+6": Accept both 3-digit and 6-digit formats (recommended)
|
|
53
|
+
* - "5+6": Accept both 5-digit and 6-digit formats
|
|
54
|
+
* - "all": Accept all formats (3, 5, and 6 digits)
|
|
55
|
+
*/
|
|
56
|
+
export type PostalCodeFormat =
|
|
57
|
+
| "3" // 3-digit only
|
|
58
|
+
| "5" // 5-digit only (legacy)
|
|
59
|
+
| "6" // 6-digit only (current)
|
|
60
|
+
| "3+5" // 3-digit or 5-digit
|
|
61
|
+
| "3+6" // 3-digit or 6-digit (recommended)
|
|
62
|
+
| "5+6" // 5-digit or 6-digit
|
|
63
|
+
| "all" // All formats accepted
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Configuration options for Taiwan postal code validation
|
|
67
|
+
*
|
|
68
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
69
|
+
*
|
|
70
|
+
* @interface PostalCodeOptions
|
|
71
|
+
* @property {IsRequired} [required=true] - Whether the field is required
|
|
72
|
+
* @property {PostalCodeFormat} [format="3+6"] - Which postal code formats to accept
|
|
73
|
+
* @property {boolean} [strictValidation=true] - Enable strict validation against known postal code ranges
|
|
74
|
+
* @property {boolean} [allowDashes=true] - Whether to allow dashes in postal codes (e.g., "100-01" or "100-001")
|
|
75
|
+
* @property {boolean} [warn5Digit=true] - Whether to show warning for 5-digit legacy format
|
|
76
|
+
* @property {string[]} [allowedPrefixes] - Specific 3-digit prefixes to allow (if strictValidation is true)
|
|
77
|
+
* @property {string[]} [blockedPrefixes] - Specific 3-digit prefixes to block
|
|
78
|
+
* @property {Function} [transform] - Custom transformation function for postal codes
|
|
79
|
+
* @property {string | null} [defaultValue] - Default value when input is empty
|
|
80
|
+
* @property {Record<Locale, PostalCodeMessages>} [i18n] - Custom error messages for different locales
|
|
81
|
+
*/
|
|
82
|
+
export type PostalCodeOptions<IsRequired extends boolean = true> = {
|
|
83
|
+
required?: IsRequired
|
|
84
|
+
format?: PostalCodeFormat
|
|
85
|
+
strictValidation?: boolean
|
|
86
|
+
allowDashes?: boolean
|
|
87
|
+
warn5Digit?: boolean
|
|
88
|
+
allowedPrefixes?: string[]
|
|
89
|
+
blockedPrefixes?: string[]
|
|
90
|
+
transform?: (value: string) => string
|
|
91
|
+
defaultValue?: IsRequired extends true ? string : string | null
|
|
92
|
+
i18n?: Record<Locale, PostalCodeMessages>
|
|
93
|
+
strictSuffixValidation?: boolean
|
|
94
|
+
deprecate5Digit?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Type alias for postal code validation schema based on required flag
|
|
99
|
+
*
|
|
100
|
+
* @template IsRequired - Whether the field is required
|
|
101
|
+
* @typedef PostalCodeSchema
|
|
102
|
+
* @description Returns ZodString if required, ZodNullable<ZodString> if optional
|
|
103
|
+
*/
|
|
104
|
+
export type PostalCodeSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Valid 3-digit postal code prefixes for Taiwan
|
|
108
|
+
* Based on official Chunghwa Post data (2024)
|
|
109
|
+
*/
|
|
110
|
+
const VALID_3_DIGIT_PREFIXES = [
|
|
111
|
+
// Taipei City (台北市) - 100-116
|
|
112
|
+
"100",
|
|
113
|
+
"103",
|
|
114
|
+
"104",
|
|
115
|
+
"105",
|
|
116
|
+
"106",
|
|
117
|
+
"108",
|
|
118
|
+
"110",
|
|
119
|
+
"111",
|
|
120
|
+
"112",
|
|
121
|
+
"114",
|
|
122
|
+
"115",
|
|
123
|
+
"116",
|
|
124
|
+
|
|
125
|
+
// New Taipei City (新北市) - 200-253
|
|
126
|
+
"200",
|
|
127
|
+
"201",
|
|
128
|
+
"202",
|
|
129
|
+
"203",
|
|
130
|
+
"204",
|
|
131
|
+
"205",
|
|
132
|
+
"206",
|
|
133
|
+
"207",
|
|
134
|
+
"208",
|
|
135
|
+
"220",
|
|
136
|
+
"221",
|
|
137
|
+
"222",
|
|
138
|
+
"223",
|
|
139
|
+
"224",
|
|
140
|
+
"226",
|
|
141
|
+
"227",
|
|
142
|
+
"228",
|
|
143
|
+
"231",
|
|
144
|
+
"232",
|
|
145
|
+
"233",
|
|
146
|
+
"234",
|
|
147
|
+
"235",
|
|
148
|
+
"236",
|
|
149
|
+
"237",
|
|
150
|
+
"238",
|
|
151
|
+
"239",
|
|
152
|
+
"241",
|
|
153
|
+
"242",
|
|
154
|
+
"243",
|
|
155
|
+
"244",
|
|
156
|
+
"247",
|
|
157
|
+
"248",
|
|
158
|
+
"249",
|
|
159
|
+
"251",
|
|
160
|
+
"252",
|
|
161
|
+
"253",
|
|
162
|
+
|
|
163
|
+
// Keelung City (基隆市) - 200-206
|
|
164
|
+
"200",
|
|
165
|
+
"201",
|
|
166
|
+
"202",
|
|
167
|
+
"203",
|
|
168
|
+
"204",
|
|
169
|
+
"205",
|
|
170
|
+
"206",
|
|
171
|
+
|
|
172
|
+
// Taoyuan City (桃園市) - 300-338
|
|
173
|
+
"300",
|
|
174
|
+
"302",
|
|
175
|
+
"303",
|
|
176
|
+
"304",
|
|
177
|
+
"305",
|
|
178
|
+
"306",
|
|
179
|
+
"307",
|
|
180
|
+
"308",
|
|
181
|
+
"310",
|
|
182
|
+
"311",
|
|
183
|
+
"312",
|
|
184
|
+
"313",
|
|
185
|
+
"314",
|
|
186
|
+
"315",
|
|
187
|
+
"316",
|
|
188
|
+
"317",
|
|
189
|
+
"318",
|
|
190
|
+
"320",
|
|
191
|
+
"324",
|
|
192
|
+
"325",
|
|
193
|
+
"326",
|
|
194
|
+
"327",
|
|
195
|
+
"328",
|
|
196
|
+
"330",
|
|
197
|
+
"333",
|
|
198
|
+
"334",
|
|
199
|
+
"335",
|
|
200
|
+
"336",
|
|
201
|
+
"337",
|
|
202
|
+
"338",
|
|
203
|
+
|
|
204
|
+
// Hsinchu County (新竹縣) - 300-315
|
|
205
|
+
"300",
|
|
206
|
+
"302",
|
|
207
|
+
"303",
|
|
208
|
+
"304",
|
|
209
|
+
"305",
|
|
210
|
+
"306",
|
|
211
|
+
"307",
|
|
212
|
+
"308",
|
|
213
|
+
"310",
|
|
214
|
+
"311",
|
|
215
|
+
"312",
|
|
216
|
+
"313",
|
|
217
|
+
"314",
|
|
218
|
+
"315",
|
|
219
|
+
|
|
220
|
+
// Hsinchu City (新竹市) - 300
|
|
221
|
+
"300",
|
|
222
|
+
|
|
223
|
+
// Miaoli County (苗栗縣) - 350-369
|
|
224
|
+
"350",
|
|
225
|
+
"351",
|
|
226
|
+
"352",
|
|
227
|
+
"353",
|
|
228
|
+
"354",
|
|
229
|
+
"356",
|
|
230
|
+
"357",
|
|
231
|
+
"358",
|
|
232
|
+
"360",
|
|
233
|
+
"361",
|
|
234
|
+
"362",
|
|
235
|
+
"363",
|
|
236
|
+
"364",
|
|
237
|
+
"365",
|
|
238
|
+
"366",
|
|
239
|
+
"367",
|
|
240
|
+
"368",
|
|
241
|
+
"369",
|
|
242
|
+
|
|
243
|
+
// Taichung City (台中市) - 400-439
|
|
244
|
+
"400",
|
|
245
|
+
"401",
|
|
246
|
+
"402",
|
|
247
|
+
"403",
|
|
248
|
+
"404",
|
|
249
|
+
"406",
|
|
250
|
+
"407",
|
|
251
|
+
"408",
|
|
252
|
+
"411",
|
|
253
|
+
"412",
|
|
254
|
+
"413",
|
|
255
|
+
"414",
|
|
256
|
+
"420",
|
|
257
|
+
"421",
|
|
258
|
+
"422",
|
|
259
|
+
"423",
|
|
260
|
+
"424",
|
|
261
|
+
"426",
|
|
262
|
+
"427",
|
|
263
|
+
"428",
|
|
264
|
+
"429",
|
|
265
|
+
"432",
|
|
266
|
+
"433",
|
|
267
|
+
"434",
|
|
268
|
+
"435",
|
|
269
|
+
"436",
|
|
270
|
+
"437",
|
|
271
|
+
"438",
|
|
272
|
+
"439",
|
|
273
|
+
|
|
274
|
+
// Changhua County (彰化縣) - 500-530
|
|
275
|
+
"500",
|
|
276
|
+
"502",
|
|
277
|
+
"503",
|
|
278
|
+
"504",
|
|
279
|
+
"505",
|
|
280
|
+
"506",
|
|
281
|
+
"507",
|
|
282
|
+
"508",
|
|
283
|
+
"509",
|
|
284
|
+
"510",
|
|
285
|
+
"511",
|
|
286
|
+
"512",
|
|
287
|
+
"513",
|
|
288
|
+
"514",
|
|
289
|
+
"515",
|
|
290
|
+
"516",
|
|
291
|
+
"520",
|
|
292
|
+
"521",
|
|
293
|
+
"522",
|
|
294
|
+
"523",
|
|
295
|
+
"524",
|
|
296
|
+
"525",
|
|
297
|
+
"526",
|
|
298
|
+
"527",
|
|
299
|
+
"528",
|
|
300
|
+
"530",
|
|
301
|
+
|
|
302
|
+
// Nantou County (南投縣) - 540-558
|
|
303
|
+
"540",
|
|
304
|
+
"541",
|
|
305
|
+
"542",
|
|
306
|
+
"544",
|
|
307
|
+
"545",
|
|
308
|
+
"546",
|
|
309
|
+
"551",
|
|
310
|
+
"552",
|
|
311
|
+
"553",
|
|
312
|
+
"555",
|
|
313
|
+
"556",
|
|
314
|
+
"557",
|
|
315
|
+
"558",
|
|
316
|
+
|
|
317
|
+
// Yunlin County (雲林縣) - 630-655
|
|
318
|
+
"630",
|
|
319
|
+
"631",
|
|
320
|
+
"632",
|
|
321
|
+
"633",
|
|
322
|
+
"634",
|
|
323
|
+
"635",
|
|
324
|
+
"636",
|
|
325
|
+
"637",
|
|
326
|
+
"638",
|
|
327
|
+
"640",
|
|
328
|
+
"643",
|
|
329
|
+
"646",
|
|
330
|
+
"647",
|
|
331
|
+
"648",
|
|
332
|
+
"649",
|
|
333
|
+
"651",
|
|
334
|
+
"652",
|
|
335
|
+
"653",
|
|
336
|
+
"654",
|
|
337
|
+
"655",
|
|
338
|
+
|
|
339
|
+
// Chiayi County (嘉義縣) - 600-625
|
|
340
|
+
"600",
|
|
341
|
+
"602",
|
|
342
|
+
"603",
|
|
343
|
+
"604",
|
|
344
|
+
"605",
|
|
345
|
+
"606",
|
|
346
|
+
"607",
|
|
347
|
+
"608",
|
|
348
|
+
"611",
|
|
349
|
+
"612",
|
|
350
|
+
"613",
|
|
351
|
+
"614",
|
|
352
|
+
"615",
|
|
353
|
+
"616",
|
|
354
|
+
"621",
|
|
355
|
+
"622",
|
|
356
|
+
"623",
|
|
357
|
+
"624",
|
|
358
|
+
"625",
|
|
359
|
+
|
|
360
|
+
// Chiayi City (嘉義市) - 600
|
|
361
|
+
"600",
|
|
362
|
+
|
|
363
|
+
// Tainan City (台南市) - 700-745
|
|
364
|
+
"700",
|
|
365
|
+
"701",
|
|
366
|
+
"702",
|
|
367
|
+
"704",
|
|
368
|
+
"708",
|
|
369
|
+
"709",
|
|
370
|
+
"710",
|
|
371
|
+
"711",
|
|
372
|
+
"712",
|
|
373
|
+
"713",
|
|
374
|
+
"714",
|
|
375
|
+
"715",
|
|
376
|
+
"716",
|
|
377
|
+
"717",
|
|
378
|
+
"718",
|
|
379
|
+
"719",
|
|
380
|
+
"720",
|
|
381
|
+
"721",
|
|
382
|
+
"722",
|
|
383
|
+
"723",
|
|
384
|
+
"724",
|
|
385
|
+
"725",
|
|
386
|
+
"726",
|
|
387
|
+
"727",
|
|
388
|
+
"730",
|
|
389
|
+
"731",
|
|
390
|
+
"732",
|
|
391
|
+
"733",
|
|
392
|
+
"734",
|
|
393
|
+
"735",
|
|
394
|
+
"736",
|
|
395
|
+
"737",
|
|
396
|
+
"741",
|
|
397
|
+
"742",
|
|
398
|
+
"743",
|
|
399
|
+
"744",
|
|
400
|
+
"745",
|
|
401
|
+
|
|
402
|
+
// Kaohsiung City (高雄市) - 800-852
|
|
403
|
+
"800",
|
|
404
|
+
"801",
|
|
405
|
+
"802",
|
|
406
|
+
"803",
|
|
407
|
+
"804",
|
|
408
|
+
"805",
|
|
409
|
+
"806",
|
|
410
|
+
"807",
|
|
411
|
+
"811",
|
|
412
|
+
"812",
|
|
413
|
+
"813",
|
|
414
|
+
"814",
|
|
415
|
+
"815",
|
|
416
|
+
"820",
|
|
417
|
+
"821",
|
|
418
|
+
"822",
|
|
419
|
+
"823",
|
|
420
|
+
"824",
|
|
421
|
+
"825",
|
|
422
|
+
"826",
|
|
423
|
+
"827",
|
|
424
|
+
"828",
|
|
425
|
+
"829",
|
|
426
|
+
"830",
|
|
427
|
+
"831",
|
|
428
|
+
"832",
|
|
429
|
+
"833",
|
|
430
|
+
"840",
|
|
431
|
+
"842",
|
|
432
|
+
"843",
|
|
433
|
+
"844",
|
|
434
|
+
"845",
|
|
435
|
+
"846",
|
|
436
|
+
"847",
|
|
437
|
+
"848",
|
|
438
|
+
"849",
|
|
439
|
+
"851",
|
|
440
|
+
"852",
|
|
441
|
+
|
|
442
|
+
// Pingtung County (屏東縣) - 900-947
|
|
443
|
+
"900",
|
|
444
|
+
"901",
|
|
445
|
+
"902",
|
|
446
|
+
"903",
|
|
447
|
+
"904",
|
|
448
|
+
"905",
|
|
449
|
+
"906",
|
|
450
|
+
"907",
|
|
451
|
+
"908",
|
|
452
|
+
"909",
|
|
453
|
+
"911",
|
|
454
|
+
"912",
|
|
455
|
+
"913",
|
|
456
|
+
"920",
|
|
457
|
+
"921",
|
|
458
|
+
"922",
|
|
459
|
+
"923",
|
|
460
|
+
"924",
|
|
461
|
+
"925",
|
|
462
|
+
"926",
|
|
463
|
+
"927",
|
|
464
|
+
"928",
|
|
465
|
+
"929",
|
|
466
|
+
"931",
|
|
467
|
+
"932",
|
|
468
|
+
"940",
|
|
469
|
+
"941",
|
|
470
|
+
"942",
|
|
471
|
+
"943",
|
|
472
|
+
"944",
|
|
473
|
+
"945",
|
|
474
|
+
"946",
|
|
475
|
+
"947",
|
|
476
|
+
|
|
477
|
+
// Yilan County (宜蘭縣) - 260-269
|
|
478
|
+
"260",
|
|
479
|
+
"261",
|
|
480
|
+
"262",
|
|
481
|
+
"263",
|
|
482
|
+
"264",
|
|
483
|
+
"265",
|
|
484
|
+
"266",
|
|
485
|
+
"267",
|
|
486
|
+
"268",
|
|
487
|
+
"269",
|
|
488
|
+
|
|
489
|
+
// Hualien County (花蓮縣) - 970-983
|
|
490
|
+
"970",
|
|
491
|
+
"971",
|
|
492
|
+
"972",
|
|
493
|
+
"973",
|
|
494
|
+
"974",
|
|
495
|
+
"975",
|
|
496
|
+
"976",
|
|
497
|
+
"977",
|
|
498
|
+
"978",
|
|
499
|
+
"979",
|
|
500
|
+
"981",
|
|
501
|
+
"982",
|
|
502
|
+
"983",
|
|
503
|
+
|
|
504
|
+
// Taitung County (台東縣) - 950-966
|
|
505
|
+
"950",
|
|
506
|
+
"951",
|
|
507
|
+
"952",
|
|
508
|
+
"953",
|
|
509
|
+
"954",
|
|
510
|
+
"955",
|
|
511
|
+
"956",
|
|
512
|
+
"957",
|
|
513
|
+
"958",
|
|
514
|
+
"959",
|
|
515
|
+
"961",
|
|
516
|
+
"962",
|
|
517
|
+
"963",
|
|
518
|
+
"964",
|
|
519
|
+
"965",
|
|
520
|
+
"966",
|
|
521
|
+
|
|
522
|
+
// Penghu County (澎湖縣) - 880-885
|
|
523
|
+
"880",
|
|
524
|
+
"881",
|
|
525
|
+
"882",
|
|
526
|
+
"883",
|
|
527
|
+
"884",
|
|
528
|
+
"885",
|
|
529
|
+
|
|
530
|
+
// Kinmen County (金門縣) - 890-896
|
|
531
|
+
"890",
|
|
532
|
+
"891",
|
|
533
|
+
"892",
|
|
534
|
+
"893",
|
|
535
|
+
"894",
|
|
536
|
+
"895",
|
|
537
|
+
"896",
|
|
538
|
+
|
|
539
|
+
// Lienchiang County (連江縣/馬祖) - 209-212
|
|
540
|
+
"209",
|
|
541
|
+
"210",
|
|
542
|
+
"211",
|
|
543
|
+
"212",
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Detailed postal code range mappings for specific validation
|
|
548
|
+
* Based on Taiwan postal system structure and known patterns
|
|
549
|
+
*
|
|
550
|
+
* Structure:
|
|
551
|
+
* - 5-digit codes: Generally use 01-99 for delivery segments
|
|
552
|
+
* - 6-digit codes: Use 001-999 for more precise delivery segments
|
|
553
|
+
* - Some areas may have restricted ranges based on actual usage
|
|
554
|
+
*/
|
|
555
|
+
const POSTAL_CODE_RANGES: Record<string, { range5: [number, number]; range6: [number, number] }> = {
|
|
556
|
+
// Major cities with extensive postal networks
|
|
557
|
+
"100": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Zhongzheng
|
|
558
|
+
"103": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Datong
|
|
559
|
+
"104": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Zhongshan
|
|
560
|
+
"105": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Songshan
|
|
561
|
+
"106": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Da'an
|
|
562
|
+
"108": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Wanhua
|
|
563
|
+
"110": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Xinyi
|
|
564
|
+
"111": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Shilin
|
|
565
|
+
"112": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Beitou
|
|
566
|
+
"114": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Neihu
|
|
567
|
+
"115": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Nangang
|
|
568
|
+
"116": { range5: [1, 99], range6: [1, 999] }, // Taipei City - Wenshan
|
|
569
|
+
|
|
570
|
+
// New Taipei City major areas
|
|
571
|
+
"220": { range5: [1, 99], range6: [1, 999] }, // Banqiao
|
|
572
|
+
"221": { range5: [1, 99], range6: [1, 999] }, // Xizhi
|
|
573
|
+
"222": { range5: [1, 99], range6: [1, 999] }, // Shenkeng
|
|
574
|
+
"223": { range5: [1, 99], range6: [1, 999] }, // Shiding
|
|
575
|
+
"224": { range5: [1, 99], range6: [1, 999] }, // Ruifang
|
|
576
|
+
|
|
577
|
+
// Taoyuan City
|
|
578
|
+
"320": { range5: [1, 99], range6: [1, 999] }, // Zhongli
|
|
579
|
+
"324": { range5: [1, 99], range6: [1, 999] }, // Pingzhen
|
|
580
|
+
"330": { range5: [1, 99], range6: [1, 999] }, // Taoyuan
|
|
581
|
+
|
|
582
|
+
// Taichung City
|
|
583
|
+
"400": { range5: [1, 99], range6: [1, 999] }, // Central District
|
|
584
|
+
"401": { range5: [1, 99], range6: [1, 999] }, // East District
|
|
585
|
+
"402": { range5: [1, 99], range6: [1, 999] }, // South District
|
|
586
|
+
"403": { range5: [1, 99], range6: [1, 999] }, // West District
|
|
587
|
+
"404": { range5: [1, 99], range6: [1, 999] }, // North District
|
|
588
|
+
|
|
589
|
+
// Tainan City
|
|
590
|
+
"700": { range5: [1, 99], range6: [1, 999] }, // Central District
|
|
591
|
+
"701": { range5: [1, 99], range6: [1, 999] }, // East District
|
|
592
|
+
"702": { range5: [1, 99], range6: [1, 999] }, // South District
|
|
593
|
+
|
|
594
|
+
// Kaohsiung City
|
|
595
|
+
"800": { range5: [1, 99], range6: [1, 999] }, // Xinxing
|
|
596
|
+
"801": { range5: [1, 99], range6: [1, 999] }, // Qianjin
|
|
597
|
+
"802": { range5: [1, 99], range6: [1, 999] }, // Lingya
|
|
598
|
+
"803": { range5: [1, 99], range6: [1, 999] }, // Yancheng
|
|
599
|
+
|
|
600
|
+
// Smaller areas with more limited ranges
|
|
601
|
+
"880": { range5: [1, 50], range6: [1, 500] }, // Penghu (smaller population)
|
|
602
|
+
"890": { range5: [1, 30], range6: [1, 300] }, // Kinmen (smaller population)
|
|
603
|
+
"209": { range5: [1, 20], range6: [1, 200] }, // Lienchiang/Matsu (smallest population)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get valid suffix ranges for a given prefix
|
|
608
|
+
* Returns default ranges if specific mapping not found
|
|
609
|
+
*/
|
|
610
|
+
const getPostalCodeRanges = (prefix: string): { range5: [number, number]; range6: [number, number] } => {
|
|
611
|
+
return (
|
|
612
|
+
POSTAL_CODE_RANGES[prefix] || {
|
|
613
|
+
range5: [1, 99], // Default range for 5-digit
|
|
614
|
+
range6: [1, 999], // Default range for 6-digit
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Validates 3-digit Taiwan postal code
|
|
621
|
+
*
|
|
622
|
+
* @param {string} value - The 3-digit postal code to validate
|
|
623
|
+
* @param {boolean} strictValidation - Whether to validate against known postal code list
|
|
624
|
+
* @param {string[]} allowedPrefixes - Specific prefixes to allow (overrides strictValidation)
|
|
625
|
+
* @param {string[]} blockedPrefixes - Specific prefixes to block
|
|
626
|
+
* @returns {boolean} True if the postal code is valid
|
|
627
|
+
*/
|
|
628
|
+
const validate3DigitPostalCode = (value: string, strictValidation: boolean = true, allowedPrefixes?: string[], blockedPrefixes?: string[]): boolean => {
|
|
629
|
+
// Basic format check
|
|
630
|
+
if (!/^\d{3}$/.test(value)) {
|
|
631
|
+
return false
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Check blocked prefixes first
|
|
635
|
+
if (blockedPrefixes && blockedPrefixes.includes(value)) {
|
|
636
|
+
return false
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check allowed prefixes (overrides strict validation)
|
|
640
|
+
if (allowedPrefixes) {
|
|
641
|
+
return allowedPrefixes.includes(value)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Strict validation against known postal codes
|
|
645
|
+
if (strictValidation) {
|
|
646
|
+
return VALID_3_DIGIT_PREFIXES.includes(value)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Basic range check (100-999)
|
|
650
|
+
const num = parseInt(value, 10)
|
|
651
|
+
return num >= 100 && num <= 999
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Validates 5-digit Taiwan postal code (legacy format)
|
|
656
|
+
*
|
|
657
|
+
* @param {string} value - The 5-digit postal code to validate
|
|
658
|
+
* @param {boolean} strictValidation - Whether to validate the 3-digit prefix
|
|
659
|
+
* @param {boolean} strictSuffixValidation - Whether to validate the 2-digit suffix
|
|
660
|
+
* @param {string[]} allowedPrefixes - Specific prefixes to allow
|
|
661
|
+
* @param {string[]} blockedPrefixes - Specific prefixes to block
|
|
662
|
+
* @returns {boolean} True if the postal code is valid
|
|
663
|
+
*/
|
|
664
|
+
const validate5DigitPostalCode = (value: string, strictValidation: boolean = true, strictSuffixValidation: boolean = false, allowedPrefixes?: string[], blockedPrefixes?: string[]): boolean => {
|
|
665
|
+
// Basic format check
|
|
666
|
+
if (!/^\d{5}$/.test(value)) {
|
|
667
|
+
return false
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const prefix = value.substring(0, 3)
|
|
671
|
+
const suffix = value.substring(3, 5)
|
|
672
|
+
|
|
673
|
+
// Validate the 3-digit prefix using the same logic
|
|
674
|
+
if (!validate3DigitPostalCode(prefix, strictValidation, allowedPrefixes, blockedPrefixes)) {
|
|
675
|
+
return false
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Validate the 2-digit suffix if strict suffix validation is enabled
|
|
679
|
+
if (strictSuffixValidation) {
|
|
680
|
+
const suffixNum = parseInt(suffix, 10)
|
|
681
|
+
const ranges = getPostalCodeRanges(prefix)
|
|
682
|
+
// Use specific range for this prefix, or fall back to general rule
|
|
683
|
+
if (suffixNum < ranges.range5[0] || suffixNum > ranges.range5[1]) {
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return true
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Validates 6-digit Taiwan postal code (current format)
|
|
693
|
+
*
|
|
694
|
+
* @param {string} value - The 6-digit postal code to validate
|
|
695
|
+
* @param {boolean} strictValidation - Whether to validate the 3-digit prefix
|
|
696
|
+
* @param {boolean} strictSuffixValidation - Whether to validate the 3-digit suffix
|
|
697
|
+
* @param {string[]} allowedPrefixes - Specific prefixes to allow
|
|
698
|
+
* @param {string[]} blockedPrefixes - Specific prefixes to block
|
|
699
|
+
* @returns {boolean} True if the postal code is valid
|
|
700
|
+
*/
|
|
701
|
+
const validate6DigitPostalCode = (value: string, strictValidation: boolean = true, strictSuffixValidation: boolean = false, allowedPrefixes?: string[], blockedPrefixes?: string[]): boolean => {
|
|
702
|
+
// Basic format check
|
|
703
|
+
if (!/^\d{6}$/.test(value)) {
|
|
704
|
+
return false
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const prefix = value.substring(0, 3)
|
|
708
|
+
const suffix = value.substring(3, 6)
|
|
709
|
+
|
|
710
|
+
// Validate the 3-digit prefix using the same logic
|
|
711
|
+
if (!validate3DigitPostalCode(prefix, strictValidation, allowedPrefixes, blockedPrefixes)) {
|
|
712
|
+
return false
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Validate the 3-digit suffix if strict suffix validation is enabled
|
|
716
|
+
if (strictSuffixValidation) {
|
|
717
|
+
const suffixNum = parseInt(suffix, 10)
|
|
718
|
+
const ranges = getPostalCodeRanges(prefix)
|
|
719
|
+
// Use specific range for this prefix, or fall back to general rule
|
|
720
|
+
if (suffixNum < ranges.range6[0] || suffixNum > ranges.range6[1]) {
|
|
721
|
+
return false
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return true
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Main validation function for Taiwan postal codes
|
|
730
|
+
*
|
|
731
|
+
* @param {string} value - The postal code to validate
|
|
732
|
+
* @param {PostalCodeFormat} format - Which formats to accept
|
|
733
|
+
* @param {boolean} strictValidation - Whether to validate against known postal codes
|
|
734
|
+
* @param {boolean} strictSuffixValidation - Whether to validate suffix ranges
|
|
735
|
+
* @param {boolean} allowDashes - Whether dashes/spaces are allowed and should be removed
|
|
736
|
+
* @param {string[]} allowedPrefixes - Specific prefixes to allow
|
|
737
|
+
* @param {string[]} blockedPrefixes - Specific prefixes to block
|
|
738
|
+
* @returns {boolean} True if the postal code is valid
|
|
739
|
+
*/
|
|
740
|
+
const validateTaiwanPostalCode = (
|
|
741
|
+
value: string,
|
|
742
|
+
format: PostalCodeFormat = "3+6",
|
|
743
|
+
strictValidation: boolean = true,
|
|
744
|
+
strictSuffixValidation: boolean = false,
|
|
745
|
+
allowDashes: boolean = true,
|
|
746
|
+
allowedPrefixes?: string[],
|
|
747
|
+
blockedPrefixes?: string[]
|
|
748
|
+
): boolean => {
|
|
749
|
+
if (!value || typeof value !== "string") {
|
|
750
|
+
return false
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Only remove dashes and spaces if they are allowed
|
|
754
|
+
const cleanValue = allowDashes ? value.replace(/[-\s]/g, "") : value
|
|
755
|
+
|
|
756
|
+
// If dashes are not allowed and the value contains them, it's invalid
|
|
757
|
+
if (!allowDashes && /[-\s]/.test(value)) {
|
|
758
|
+
return false
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
switch (format) {
|
|
762
|
+
case "3":
|
|
763
|
+
return cleanValue.length === 3 && validate3DigitPostalCode(cleanValue, strictValidation, allowedPrefixes, blockedPrefixes)
|
|
764
|
+
|
|
765
|
+
case "5":
|
|
766
|
+
return cleanValue.length === 5 && validate5DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes)
|
|
767
|
+
|
|
768
|
+
case "6":
|
|
769
|
+
return cleanValue.length === 6 && validate6DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes)
|
|
770
|
+
|
|
771
|
+
case "3+5":
|
|
772
|
+
return (
|
|
773
|
+
(cleanValue.length === 3 && validate3DigitPostalCode(cleanValue, strictValidation, allowedPrefixes, blockedPrefixes)) ||
|
|
774
|
+
(cleanValue.length === 5 && validate5DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes))
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
case "3+6":
|
|
778
|
+
return (
|
|
779
|
+
(cleanValue.length === 3 && validate3DigitPostalCode(cleanValue, strictValidation, allowedPrefixes, blockedPrefixes)) ||
|
|
780
|
+
(cleanValue.length === 6 && validate6DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes))
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
case "5+6":
|
|
784
|
+
return (
|
|
785
|
+
(cleanValue.length === 5 && validate5DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes)) ||
|
|
786
|
+
(cleanValue.length === 6 && validate6DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes))
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
case "all":
|
|
790
|
+
return (
|
|
791
|
+
(cleanValue.length === 3 && validate3DigitPostalCode(cleanValue, strictValidation, allowedPrefixes, blockedPrefixes)) ||
|
|
792
|
+
(cleanValue.length === 5 && validate5DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes)) ||
|
|
793
|
+
(cleanValue.length === 6 && validate6DigitPostalCode(cleanValue, strictValidation, strictSuffixValidation, allowedPrefixes, blockedPrefixes))
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
default:
|
|
797
|
+
return false
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Creates a Zod schema for Taiwan postal code validation
|
|
803
|
+
*
|
|
804
|
+
* @template IsRequired - Whether the field is required (affects return type)
|
|
805
|
+
* @param {PostalCodeOptions<IsRequired>} [options] - Configuration options for postal code validation
|
|
806
|
+
* @returns {PostalCodeSchema<IsRequired>} Zod schema for postal code validation
|
|
807
|
+
*
|
|
808
|
+
* @description
|
|
809
|
+
* Creates a comprehensive Taiwan postal code validator that supports multiple formats
|
|
810
|
+
* and provides extensive configuration options for different validation scenarios.
|
|
811
|
+
*
|
|
812
|
+
* Features:
|
|
813
|
+
* - 3-digit, 5-digit, and 6-digit postal code format support
|
|
814
|
+
* - Strict validation against official postal code ranges
|
|
815
|
+
* - Legacy 5-digit format with optional warnings
|
|
816
|
+
* - Custom prefix allowlist/blocklist support
|
|
817
|
+
* - Dash and space handling in postal codes
|
|
818
|
+
* - Automatic normalization and formatting
|
|
819
|
+
* - Custom transformation functions
|
|
820
|
+
* - Comprehensive internationalization
|
|
821
|
+
* - Optional field support
|
|
822
|
+
*
|
|
823
|
+
* @example
|
|
824
|
+
* ```typescript
|
|
825
|
+
* // Accept 3-digit or 6-digit formats (recommended)
|
|
826
|
+
* const modernSchema = postalCode()
|
|
827
|
+
* modernSchema.parse("100") // ✓ Valid 3-digit
|
|
828
|
+
* modernSchema.parse("100001") // ✓ Valid 6-digit
|
|
829
|
+
* modernSchema.parse("10001") // ✗ Invalid (5-digit not allowed)
|
|
830
|
+
*
|
|
831
|
+
* // Accept all formats
|
|
832
|
+
* const flexibleSchema = postalCode({ format: "all" })
|
|
833
|
+
* flexibleSchema.parse("100") // ✓ Valid
|
|
834
|
+
* flexibleSchema.parse("10001") // ✓ Valid
|
|
835
|
+
* flexibleSchema.parse("100001") // ✓ Valid
|
|
836
|
+
*
|
|
837
|
+
* // Only 6-digit format (current standard)
|
|
838
|
+
* const modernOnlySchema = postalCode({ format: "6" })
|
|
839
|
+
* modernOnlySchema.parse("100001") // ✓ Valid
|
|
840
|
+
* modernOnlySchema.parse("100") // ✗ Invalid
|
|
841
|
+
*
|
|
842
|
+
* // With dashes allowed
|
|
843
|
+
* const dashSchema = postalCode({ allowDashes: true })
|
|
844
|
+
* dashSchema.parse("100-001") // ✓ Valid (normalized to "100001")
|
|
845
|
+
* dashSchema.parse("100-01") // ✓ Valid if 5-digit format allowed
|
|
846
|
+
*
|
|
847
|
+
* // Specific areas only
|
|
848
|
+
* const taipeiSchema = postalCode({
|
|
849
|
+
* allowedPrefixes: ["100", "103", "104", "105", "106"]
|
|
850
|
+
* })
|
|
851
|
+
* taipeiSchema.parse("100001") // ✓ Valid (Taipei area)
|
|
852
|
+
* taipeiSchema.parse("200001") // ✗ Invalid (not in allowlist)
|
|
853
|
+
*
|
|
854
|
+
* // Block specific areas
|
|
855
|
+
* const blockedSchema = postalCode({
|
|
856
|
+
* blockedPrefixes: ["999"] // Block test codes
|
|
857
|
+
* })
|
|
858
|
+
*
|
|
859
|
+
* // With warning for legacy format
|
|
860
|
+
* const warnSchema = postalCode({
|
|
861
|
+
* format: "all",
|
|
862
|
+
* warn5Digit: true
|
|
863
|
+
* })
|
|
864
|
+
* // Will validate but may show warning for 5-digit codes
|
|
865
|
+
*
|
|
866
|
+
* // Optional with custom transformation
|
|
867
|
+
* const optionalSchema = postalCode({
|
|
868
|
+
* required: false,
|
|
869
|
+
* transform: (value) => value.replace(/\D/g, '') // Remove non-digits
|
|
870
|
+
* })
|
|
871
|
+
*
|
|
872
|
+
* // Strict suffix validation for real postal codes
|
|
873
|
+
* const strictSchema = postalCode({
|
|
874
|
+
* format: "6",
|
|
875
|
+
* strictSuffixValidation: true // Validates suffix range 001-999
|
|
876
|
+
* })
|
|
877
|
+
* strictSchema.parse("100001") // ✓ Valid
|
|
878
|
+
* strictSchema.parse("100000") // ✗ Invalid (suffix 000 not allowed)
|
|
879
|
+
*
|
|
880
|
+
* // Deprecate 5-digit codes entirely
|
|
881
|
+
* const modern2024Schema = postalCode({
|
|
882
|
+
* format: "all",
|
|
883
|
+
* deprecate5Digit: true // Throws error for any 5-digit code
|
|
884
|
+
* })
|
|
885
|
+
* modern2024Schema.parse("100001") // ✓ Valid 6-digit
|
|
886
|
+
* modern2024Schema.parse("10001") // ✗ Error: deprecated format
|
|
887
|
+
* ```
|
|
888
|
+
*
|
|
889
|
+
* @throws {z.ZodError} When validation fails with specific error messages
|
|
890
|
+
* @see {@link PostalCodeOptions} for all available configuration options
|
|
891
|
+
* @see {@link PostalCodeFormat} for supported formats
|
|
892
|
+
* @see {@link validateTaiwanPostalCode} for validation logic details
|
|
893
|
+
*/
|
|
894
|
+
export function postalCode<IsRequired extends boolean = true>(options?: PostalCodeOptions<IsRequired>): PostalCodeSchema<IsRequired> {
|
|
895
|
+
const {
|
|
896
|
+
required = true,
|
|
897
|
+
format = "3+6",
|
|
898
|
+
strictValidation = true,
|
|
899
|
+
allowDashes = true,
|
|
900
|
+
warn5Digit = true,
|
|
901
|
+
allowedPrefixes,
|
|
902
|
+
blockedPrefixes,
|
|
903
|
+
transform,
|
|
904
|
+
defaultValue,
|
|
905
|
+
i18n,
|
|
906
|
+
strictSuffixValidation = false,
|
|
907
|
+
deprecate5Digit = false,
|
|
908
|
+
} = options ?? {}
|
|
909
|
+
|
|
910
|
+
// Set appropriate default value based on required flag
|
|
911
|
+
const actualDefaultValue = defaultValue ?? (required ? "" : null)
|
|
912
|
+
|
|
913
|
+
// Helper function to get custom message or fallback to default i18n
|
|
914
|
+
const getMessage = (key: keyof PostalCodeMessages, params?: Record<string, any>) => {
|
|
915
|
+
if (i18n) {
|
|
916
|
+
const currentLocale = getLocale()
|
|
917
|
+
const customMessages = i18n[currentLocale]
|
|
918
|
+
if (customMessages && customMessages[key]) {
|
|
919
|
+
const template = customMessages[key]!
|
|
920
|
+
return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return t(`taiwan.postalCode.${key}`, params)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Preprocessing function
|
|
927
|
+
const preprocessFn = (val: unknown) => {
|
|
928
|
+
if (val === "" || val === null || val === undefined) {
|
|
929
|
+
return actualDefaultValue
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
let processed = String(val).trim()
|
|
933
|
+
|
|
934
|
+
// Remove dashes and spaces if allowDashes is true
|
|
935
|
+
if (allowDashes) {
|
|
936
|
+
processed = processed.replace(/[-\s]/g, "")
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// If after processing we have an empty string and the field is optional, return null
|
|
940
|
+
if (processed === "" && !required) {
|
|
941
|
+
return null
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (transform) {
|
|
945
|
+
processed = transform(processed)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return processed
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
|
|
952
|
+
|
|
953
|
+
const schema = baseSchema.refine((val) => {
|
|
954
|
+
if (val === null) return true
|
|
955
|
+
|
|
956
|
+
// Required check
|
|
957
|
+
if (required && (val === "" || val === "null" || val === "undefined")) {
|
|
958
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (val === null) return true
|
|
962
|
+
if (!required && val === "") return true
|
|
963
|
+
|
|
964
|
+
// Format-specific validation
|
|
965
|
+
const cleanValue = val.replace(/[-\s]/g, "")
|
|
966
|
+
|
|
967
|
+
// Check if format matches expected pattern
|
|
968
|
+
if (format === "3" && cleanValue.length !== 3) {
|
|
969
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("format3Only"), path: [] }])
|
|
970
|
+
}
|
|
971
|
+
if (format === "5" && cleanValue.length !== 5) {
|
|
972
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("format5Only"), path: [] }])
|
|
973
|
+
}
|
|
974
|
+
if (format === "6" && cleanValue.length !== 6) {
|
|
975
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("format6Only"), path: [] }])
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Check for deprecated 5-digit format
|
|
979
|
+
if (deprecate5Digit && cleanValue.length === 5) {
|
|
980
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("deprecated5Digit"), path: [] }])
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Pre-validate suffix for better error messages before main validation
|
|
984
|
+
if (strictSuffixValidation) {
|
|
985
|
+
if (cleanValue.length === 5) {
|
|
986
|
+
const prefix = cleanValue.substring(0, 3)
|
|
987
|
+
const suffix = cleanValue.substring(3, 5)
|
|
988
|
+
const suffixNum = parseInt(suffix, 10)
|
|
989
|
+
const ranges = getPostalCodeRanges(prefix)
|
|
990
|
+
if (suffixNum < ranges.range5[0] || suffixNum > ranges.range5[1]) {
|
|
991
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalidSuffix"), path: [] }])
|
|
992
|
+
}
|
|
993
|
+
} else if (cleanValue.length === 6) {
|
|
994
|
+
const prefix = cleanValue.substring(0, 3)
|
|
995
|
+
const suffix = cleanValue.substring(3, 6)
|
|
996
|
+
const suffixNum = parseInt(suffix, 10)
|
|
997
|
+
const ranges = getPostalCodeRanges(prefix)
|
|
998
|
+
if (suffixNum < ranges.range6[0] || suffixNum > ranges.range6[1]) {
|
|
999
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalidSuffix"), path: [] }])
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Main postal code validation (only validates prefix if strictSuffixValidation already passed)
|
|
1005
|
+
if (!validateTaiwanPostalCode(val, format, strictValidation, strictSuffixValidation, allowDashes, allowedPrefixes, blockedPrefixes)) {
|
|
1006
|
+
throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Warning for 5-digit legacy format (doesn't fail validation)
|
|
1010
|
+
if (warn5Digit && cleanValue.length === 5 && format !== "5" && !deprecate5Digit) {
|
|
1011
|
+
console.warn(getMessage("legacy5DigitWarning"))
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return true
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
return schema as unknown as PostalCodeSchema<IsRequired>
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Utility functions exported for external use
|
|
1022
|
+
*
|
|
1023
|
+
* @description
|
|
1024
|
+
* These validation functions can be used independently for postal code validation
|
|
1025
|
+
* without creating a full Zod schema. Useful for custom validation logic.
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* ```typescript
|
|
1029
|
+
* import {
|
|
1030
|
+
* validateTaiwanPostalCode,
|
|
1031
|
+
* validate3DigitPostalCode,
|
|
1032
|
+
* validate5DigitPostalCode,
|
|
1033
|
+
* validate6DigitPostalCode,
|
|
1034
|
+
* VALID_3_DIGIT_PREFIXES
|
|
1035
|
+
* } from './postal-code'
|
|
1036
|
+
*
|
|
1037
|
+
* // General validation
|
|
1038
|
+
* const isValid = validateTaiwanPostalCode("100001", "6") // boolean
|
|
1039
|
+
*
|
|
1040
|
+
* // Specific format validation
|
|
1041
|
+
* const is3Digit = validate3DigitPostalCode("100") // boolean
|
|
1042
|
+
* const is5Digit = validate5DigitPostalCode("10001") // boolean
|
|
1043
|
+
* const is6Digit = validate6DigitPostalCode("100001") // boolean
|
|
1044
|
+
*
|
|
1045
|
+
* // Check if prefix is valid
|
|
1046
|
+
* const isValidPrefix = VALID_3_DIGIT_PREFIXES.includes("100") // boolean
|
|
1047
|
+
* ```
|
|
1048
|
+
*/
|
|
1049
|
+
export { validateTaiwanPostalCode, validate3DigitPostalCode, validate5DigitPostalCode, validate6DigitPostalCode, VALID_3_DIGIT_PREFIXES }
|