taiwan-validator 1.0.0 → 1.2.0
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/README.en.md +76 -1
- package/README.md +76 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -8
- package/src/index.ts +27 -6
- package/src/types.ts +22 -0
- package/src/validators/landline-phone.test.ts +34 -0
- package/src/validators/landline-phone.ts +64 -0
- package/src/validators/license-plate.test.ts +336 -0
- package/src/validators/license-plate.ts +215 -0
- package/src/validators/national-id.test.ts +34 -61
- package/src/validators/national-id.ts +42 -115
- package/src/validators/nhi-card.test.ts +21 -0
- package/src/validators/nhi-card.ts +39 -0
- package/src/validators/passport.test.ts +20 -0
- package/src/validators/passport.ts +38 -0
- package/src/validators/postal-code.test.ts +31 -0
- package/src/validators/postal-code.ts +41 -0
- package/src/validators/resident-certificate.test.ts +60 -65
- package/src/validators/resident-certificate.ts +98 -68
- package/src/validators/shared.ts +63 -0
- package/src/validators/uniform-invoice.test.ts +24 -0
- package/src/validators/uniform-invoice.ts +39 -0
|
@@ -1,45 +1,14 @@
|
|
|
1
|
-
import type { ValidationResult, NationalIdType } from "../types";
|
|
1
|
+
import type { ValidationResult, NationalIdType, NationalIdInfo } from "../types";
|
|
2
|
+
import { LETTER_MAPPING, REGION_MAPPING } from "./shared";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*/
|
|
6
|
-
const LETTER_MAPPING: Record<string, number> = {
|
|
7
|
-
A: 10,
|
|
8
|
-
B: 11,
|
|
9
|
-
C: 12,
|
|
10
|
-
D: 13,
|
|
11
|
-
E: 14,
|
|
12
|
-
F: 15,
|
|
13
|
-
G: 16,
|
|
14
|
-
H: 17,
|
|
15
|
-
I: 34,
|
|
16
|
-
J: 18,
|
|
17
|
-
K: 19,
|
|
18
|
-
L: 20,
|
|
19
|
-
M: 21,
|
|
20
|
-
N: 22,
|
|
21
|
-
O: 35,
|
|
22
|
-
P: 23,
|
|
23
|
-
Q: 24,
|
|
24
|
-
R: 25,
|
|
25
|
-
S: 26,
|
|
26
|
-
T: 27,
|
|
27
|
-
U: 28,
|
|
28
|
-
V: 29,
|
|
29
|
-
W: 32,
|
|
30
|
-
X: 30,
|
|
31
|
-
Y: 31,
|
|
32
|
-
Z: 33,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 驗證舊式身分證字號格式(1個字母 + 9個數字)
|
|
5
|
+
* 驗證台灣身分證字號格式與檢查碼(1個字母 + 9個數字)
|
|
37
6
|
* 格式:A123456789
|
|
38
7
|
* - 第一個字元:地區代碼(字母)
|
|
39
8
|
* - 第二個字元:性別(1 = 男性,2 = 女性)
|
|
40
9
|
* - 最後一個字元:檢查碼
|
|
41
10
|
*/
|
|
42
|
-
function
|
|
11
|
+
function validateFormatAndChecksum(id: string): boolean {
|
|
43
12
|
const pattern = /^[A-Z][12]\d{8}$/;
|
|
44
13
|
if (!pattern.test(id)) {
|
|
45
14
|
return false;
|
|
@@ -50,7 +19,6 @@ function validateOldFormat(id: string): boolean {
|
|
|
50
19
|
|
|
51
20
|
const letterValue = LETTER_MAPPING[letter] as number;
|
|
52
21
|
|
|
53
|
-
// 計算檢查碼
|
|
54
22
|
const d1 = Math.floor(letterValue / 10);
|
|
55
23
|
const d2 = letterValue % 10;
|
|
56
24
|
|
|
@@ -66,70 +34,19 @@ function validateOldFormat(id: string): boolean {
|
|
|
66
34
|
}
|
|
67
35
|
|
|
68
36
|
/**
|
|
69
|
-
*
|
|
70
|
-
* 格式:AA12345678
|
|
71
|
-
* - 第一個字元:地區代碼(字母)
|
|
72
|
-
* - 第二個字元:性別(8 = 男性,9 = 女性)以字母表示
|
|
73
|
-
* - 最後一個字元:檢查碼
|
|
74
|
-
*/
|
|
75
|
-
function validateNewFormat(id: string): boolean {
|
|
76
|
-
const pattern = /^[A-Z]{2}\d{8}$/;
|
|
77
|
-
if (!pattern.test(id)) {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const firstLetter = id[0] as string;
|
|
82
|
-
const secondLetter = id[1] as string;
|
|
83
|
-
const numbers = id.slice(2);
|
|
84
|
-
|
|
85
|
-
const firstLetterValue = LETTER_MAPPING[firstLetter] as number;
|
|
86
|
-
const secondLetterValue = LETTER_MAPPING[secondLetter] as number;
|
|
87
|
-
|
|
88
|
-
// 計算新式格式的檢查碼
|
|
89
|
-
const d1 = Math.floor(firstLetterValue / 10);
|
|
90
|
-
const d2 = firstLetterValue % 10;
|
|
91
|
-
const d3 = Math.floor(secondLetterValue / 10);
|
|
92
|
-
const d4 = secondLetterValue % 10;
|
|
93
|
-
|
|
94
|
-
const weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1];
|
|
95
|
-
const digits = [d1, d2, d3, d4, ...numbers.split("").map(Number)];
|
|
96
|
-
|
|
97
|
-
const sum = digits.reduce(
|
|
98
|
-
(acc, digit, index) => acc + digit * weights[index]!,
|
|
99
|
-
0,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
return sum % 10 === 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 偵測身分證字號格式類型
|
|
107
|
-
*/
|
|
108
|
-
function detectFormat(id: string): NationalIdType | null {
|
|
109
|
-
if (/^[A-Z][12]\d{8}$/.test(id)) {
|
|
110
|
-
return "old";
|
|
111
|
-
}
|
|
112
|
-
if (/^[A-Z]{2}\d{8}$/.test(id)) {
|
|
113
|
-
return "new";
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 驗證台灣身分證字號(支援新舊格式)
|
|
37
|
+
* 驗證台灣身分證字號(身分證字號皆為「1個字母 + 9個數字」格式)
|
|
120
38
|
* @param id - 要驗證的身分證字號
|
|
121
|
-
* @param format -
|
|
39
|
+
* @param format - 可選(為維持相容性保留):指定格式類型
|
|
122
40
|
* @returns 驗證結果
|
|
123
41
|
*
|
|
124
42
|
* @example
|
|
125
43
|
* ```typescript
|
|
126
|
-
* validateNationalId('A123456789');
|
|
127
|
-
* validateNationalId('AA12345678'); // 新式格式
|
|
44
|
+
* validateNationalId('A123456789');
|
|
128
45
|
* ```
|
|
129
46
|
*/
|
|
130
47
|
export function validateNationalId(
|
|
131
48
|
id: string,
|
|
132
|
-
|
|
49
|
+
_format?: NationalIdType,
|
|
133
50
|
): ValidationResult {
|
|
134
51
|
if (!id || typeof id !== "string") {
|
|
135
52
|
return {
|
|
@@ -140,42 +57,52 @@ export function validateNationalId(
|
|
|
140
57
|
|
|
141
58
|
const normalizedId = id.trim().toUpperCase();
|
|
142
59
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
60
|
+
const isValid = validateFormatAndChecksum(normalizedId);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
isValid,
|
|
64
|
+
message: isValid ? undefined : "無效的身分證字號",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
151
67
|
|
|
152
|
-
|
|
153
|
-
|
|
68
|
+
/**
|
|
69
|
+
* 解析台灣身分證字號資訊(性別、發證地區)
|
|
70
|
+
* @param id - 要解析的身分證字號
|
|
71
|
+
* @returns 解析結果,包含驗證狀態及相關欄位
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* parseNationalId('A123456789');
|
|
76
|
+
* // 輸出: { isValid: true, gender: 'male', region: '臺北市' }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function parseNationalId(id: string): NationalIdInfo {
|
|
80
|
+
if (!id || typeof id !== "string") {
|
|
154
81
|
return {
|
|
155
|
-
isValid,
|
|
156
|
-
message:
|
|
82
|
+
isValid: false,
|
|
83
|
+
message: "身分證字號必須為非空字串",
|
|
157
84
|
};
|
|
158
85
|
}
|
|
159
86
|
|
|
160
|
-
|
|
161
|
-
const detectedFormat = detectFormat(normalizedId);
|
|
87
|
+
const normalizedId = id.trim().toUpperCase();
|
|
162
88
|
|
|
163
|
-
|
|
89
|
+
const isValid = validateFormatAndChecksum(normalizedId);
|
|
90
|
+
if (!isValid) {
|
|
164
91
|
return {
|
|
165
92
|
isValid: false,
|
|
166
|
-
message: "
|
|
93
|
+
message: "無效的身分證字號",
|
|
167
94
|
};
|
|
168
95
|
}
|
|
169
96
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
97
|
+
const firstLetter = normalizedId[0] as string;
|
|
98
|
+
const genderDigit = normalizedId[1] as string;
|
|
99
|
+
|
|
100
|
+
const gender = genderDigit === "1" ? "male" : "female";
|
|
101
|
+
const region = REGION_MAPPING[firstLetter] as string;
|
|
174
102
|
|
|
175
103
|
return {
|
|
176
|
-
isValid,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
: `無效的${detectedFormat === "old" ? "舊式" : "新式"}身分證字號`,
|
|
104
|
+
isValid: true,
|
|
105
|
+
gender,
|
|
106
|
+
region,
|
|
180
107
|
};
|
|
181
108
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { validateNHICard } from "./nhi-card";
|
|
2
|
+
|
|
3
|
+
describe("validateNHICard", () => {
|
|
4
|
+
test("should validate correct NHI card numbers", () => {
|
|
5
|
+
expect(validateNHICard("0000 1234 5678").isValid).toBe(true);
|
|
6
|
+
expect(validateNHICard("000012345678").isValid).toBe(true);
|
|
7
|
+
expect(validateNHICard("0000-1234-5678").isValid).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("should reject invalid NHI card numbers", () => {
|
|
11
|
+
expect(validateNHICard("00001234567").isValid).toBe(false); // Too short (11 digits)
|
|
12
|
+
expect(validateNHICard("0000123456789").isValid).toBe(false); // Too long (13 digits)
|
|
13
|
+
expect(validateNHICard("00001234567a").isValid).toBe(false); // Non-numeric
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("should reject empty or non-string inputs", () => {
|
|
17
|
+
expect(validateNHICard("").isValid).toBe(false);
|
|
18
|
+
// @ts-expect-error Testing invalid input type
|
|
19
|
+
expect(validateNHICard(null).isValid).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣國民健康保險卡(健保卡)卡號格式
|
|
5
|
+
* 格式為 12 碼數字(可包含減號或空格,如 0000 1234 5678)
|
|
6
|
+
*
|
|
7
|
+
* @param cardNumber - 要驗證的健保卡卡號
|
|
8
|
+
* @returns 驗證結果
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* validateNHICard('0000 1234 5678'); // { isValid: true }
|
|
13
|
+
* validateNHICard('000012345678'); // { isValid: true }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function validateNHICard(cardNumber: string): ValidationResult {
|
|
17
|
+
if (!cardNumber || typeof cardNumber !== "string") {
|
|
18
|
+
return {
|
|
19
|
+
isValid: false,
|
|
20
|
+
message: "健保卡號必須為非空字串",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 去除空格與減號
|
|
25
|
+
const normalized = cardNumber.replace(/[-\s]/g, "");
|
|
26
|
+
|
|
27
|
+
const pattern = /^\d{12}$/;
|
|
28
|
+
|
|
29
|
+
if (!pattern.test(normalized)) {
|
|
30
|
+
return {
|
|
31
|
+
isValid: false,
|
|
32
|
+
message: "健保卡號格式必須為12碼數字",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
isValid: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { validatePassport } from "./passport";
|
|
2
|
+
|
|
3
|
+
describe("validatePassport", () => {
|
|
4
|
+
test("should validate correct passport numbers", () => {
|
|
5
|
+
expect(validatePassport("312345678").isValid).toBe(true);
|
|
6
|
+
expect(validatePassport("123456789").isValid).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("should reject invalid passport numbers", () => {
|
|
10
|
+
expect(validatePassport("12345678").isValid).toBe(false); // Too short (8 digits)
|
|
11
|
+
expect(validatePassport("1234567890").isValid).toBe(false); // Too long (10 digits)
|
|
12
|
+
expect(validatePassport("12345678a").isValid).toBe(false); // Non-numeric
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("should reject empty or non-string inputs", () => {
|
|
16
|
+
expect(validatePassport("").isValid).toBe(false);
|
|
17
|
+
// @ts-expect-error Testing invalid input type
|
|
18
|
+
expect(validatePassport(null).isValid).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證中華民國(台灣)護照號碼格式
|
|
5
|
+
* 晶片護照與一般護照為 9 碼數字格式(晶片護照通常以 3 開頭)
|
|
6
|
+
*
|
|
7
|
+
* @param passport - 要驗證的護照號碼
|
|
8
|
+
* @returns 驗證結果
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* validatePassport('312345678'); // { isValid: true }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function validatePassport(passport: string): ValidationResult {
|
|
16
|
+
if (!passport || typeof passport !== "string") {
|
|
17
|
+
return {
|
|
18
|
+
isValid: false,
|
|
19
|
+
message: "護照號碼必須為非空字串",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalized = passport.trim();
|
|
24
|
+
|
|
25
|
+
// 驗證 9 碼數字
|
|
26
|
+
const pattern = /^\d{9}$/;
|
|
27
|
+
|
|
28
|
+
if (!pattern.test(normalized)) {
|
|
29
|
+
return {
|
|
30
|
+
isValid: false,
|
|
31
|
+
message: "護照號碼格式必須為9位數字",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
isValid: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { validatePostalCode } from "./postal-code";
|
|
2
|
+
|
|
3
|
+
describe("validatePostalCode", () => {
|
|
4
|
+
test("should validate correct postal codes", () => {
|
|
5
|
+
// 3 digits
|
|
6
|
+
expect(validatePostalCode("100").isValid).toBe(true);
|
|
7
|
+
expect(validatePostalCode(100).isValid).toBe(true);
|
|
8
|
+
|
|
9
|
+
// 5 digits
|
|
10
|
+
expect(validatePostalCode("100-01").isValid).toBe(true);
|
|
11
|
+
expect(validatePostalCode("10001").isValid).toBe(true);
|
|
12
|
+
|
|
13
|
+
// 6 digits
|
|
14
|
+
expect(validatePostalCode("100001").isValid).toBe(true);
|
|
15
|
+
expect(validatePostalCode("100-001").isValid).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("should reject invalid postal codes", () => {
|
|
19
|
+
expect(validatePostalCode("001").isValid).toBe(false); // First digit cannot be 0
|
|
20
|
+
expect(validatePostalCode("10").isValid).toBe(false); // Too short
|
|
21
|
+
expect(validatePostalCode("1000").isValid).toBe(false); // 4 digits is not valid
|
|
22
|
+
expect(validatePostalCode("1000001").isValid).toBe(false); // Too long (7 digits)
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("should reject empty or null inputs", () => {
|
|
26
|
+
// @ts-expect-error Testing invalid input type
|
|
27
|
+
expect(validatePostalCode(null).isValid).toBe(false);
|
|
28
|
+
// @ts-expect-error Testing invalid input type
|
|
29
|
+
expect(validatePostalCode(undefined).isValid).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣郵遞區號格式
|
|
5
|
+
* 支援 3 碼、5 碼 (3+2) 及新式 6 碼 (3+3) 格式(可包含減號,如 100-001 或 100001)
|
|
6
|
+
* 第一碼必須為 1-9
|
|
7
|
+
*
|
|
8
|
+
* @param code - 要驗證的郵遞區號
|
|
9
|
+
* @returns 驗證結果
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* validatePostalCode('100'); // { isValid: true }
|
|
14
|
+
* validatePostalCode('100-01'); // { isValid: true }
|
|
15
|
+
* validatePostalCode('100001'); // { isValid: true }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function validatePostalCode(code: string | number): ValidationResult {
|
|
19
|
+
if (code === null || code === undefined) {
|
|
20
|
+
return {
|
|
21
|
+
isValid: false,
|
|
22
|
+
message: "郵遞區號必須為非空字串或數字",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const strCode = String(code).trim().replace(/[-\s]/g, "");
|
|
27
|
+
|
|
28
|
+
// 驗證格式:3 碼、5 碼或 6 碼,且首碼為 1-9
|
|
29
|
+
const pattern = /^[1-9]\d{2}$|^[1-9]\d{4}$|^[1-9]\d{5}$/;
|
|
30
|
+
|
|
31
|
+
if (!pattern.test(strCode)) {
|
|
32
|
+
return {
|
|
33
|
+
isValid: false,
|
|
34
|
+
message: "郵遞區號格式必須為首碼非0的3碼、5碼或6碼數字",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isValid: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -1,101 +1,62 @@
|
|
|
1
|
-
import { validateResidentCertificate } from "./resident-certificate";
|
|
1
|
+
import { validateResidentCertificate, parseResidentCertificate } from "./resident-certificate";
|
|
2
2
|
|
|
3
3
|
describe("validateResidentCertificate", () => {
|
|
4
|
-
describe("
|
|
5
|
-
test("should validate correct
|
|
6
|
-
|
|
7
|
-
expect(validateResidentCertificate("
|
|
8
|
-
true,
|
|
9
|
-
);
|
|
10
|
-
expect(validateResidentCertificate("B923456786", "old").isValid).toBe(
|
|
11
|
-
true,
|
|
12
|
-
);
|
|
13
|
-
expect(validateResidentCertificate("C823456785", "old").isValid).toBe(
|
|
14
|
-
true,
|
|
15
|
-
);
|
|
16
|
-
expect(validateResidentCertificate("D923456788", "old").isValid).toBe(
|
|
17
|
-
true,
|
|
18
|
-
);
|
|
4
|
+
describe("New Format (1 letter + 9 digits, e.g. A823456783)", () => {
|
|
5
|
+
test("should validate correct new format Resident Certificates", () => {
|
|
6
|
+
expect(validateResidentCertificate("A823456783", "new").isValid).toBe(true);
|
|
7
|
+
expect(validateResidentCertificate("B923456786", "new").isValid).toBe(true);
|
|
19
8
|
});
|
|
20
9
|
|
|
21
|
-
test("should reject invalid
|
|
22
|
-
expect(validateResidentCertificate("A823456780", "
|
|
23
|
-
|
|
24
|
-
); // Wrong checksum
|
|
25
|
-
expect(validateResidentCertificate("E823456783", "old").isValid).toBe(
|
|
26
|
-
false,
|
|
27
|
-
); // Invalid first letter
|
|
28
|
-
expect(validateResidentCertificate("A723456783", "old").isValid).toBe(
|
|
29
|
-
false,
|
|
30
|
-
); // Invalid second digit (must be 8 or 9)
|
|
10
|
+
test("should reject invalid new format Resident Certificates", () => {
|
|
11
|
+
expect(validateResidentCertificate("A823456780", "new").isValid).toBe(false); // Wrong checksum
|
|
12
|
+
expect(validateResidentCertificate("E723456783", "new").isValid).toBe(false); // Invalid second digit (must be 8 or 9)
|
|
31
13
|
});
|
|
32
14
|
|
|
33
15
|
test("should handle case insensitive input", () => {
|
|
34
|
-
expect(validateResidentCertificate("a823456783", "
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
expect(validateResidentCertificate("b923456786", "old").isValid).toBe(
|
|
38
|
-
true,
|
|
39
|
-
);
|
|
16
|
+
expect(validateResidentCertificate("a823456783", "new").isValid).toBe(true);
|
|
17
|
+
expect(validateResidentCertificate("b923456786", "new").isValid).toBe(true);
|
|
40
18
|
});
|
|
41
19
|
|
|
42
20
|
test("should handle whitespace", () => {
|
|
43
|
-
expect(validateResidentCertificate(" A823456783 ", "
|
|
44
|
-
true,
|
|
45
|
-
);
|
|
21
|
+
expect(validateResidentCertificate(" A823456783 ", "new").isValid).toBe(true);
|
|
46
22
|
});
|
|
47
23
|
});
|
|
48
24
|
|
|
49
|
-
describe("
|
|
50
|
-
test("should validate correct
|
|
51
|
-
|
|
52
|
-
expect(validateResidentCertificate("
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
expect(validateResidentCertificate("AB23456789", "new").isValid).toBe(
|
|
56
|
-
true,
|
|
57
|
-
);
|
|
25
|
+
describe("Old Format (2 letters + 8 digits, e.g. AB12345677)", () => {
|
|
26
|
+
test("should validate correct old format Resident Certificates", () => {
|
|
27
|
+
expect(validateResidentCertificate("AB12345677", "old").isValid).toBe(true); // Female non-citizen
|
|
28
|
+
expect(validateResidentCertificate("AC12345679", "old").isValid).toBe(true); // Male foreigner
|
|
29
|
+
expect(validateResidentCertificate("AD12345671", "old").isValid).toBe(true); // Female foreigner
|
|
30
|
+
expect(validateResidentCertificate("AA12345675", "old").isValid).toBe(true); // Male non-citizen
|
|
58
31
|
});
|
|
59
32
|
|
|
60
|
-
test("should reject invalid
|
|
61
|
-
expect(validateResidentCertificate("
|
|
62
|
-
|
|
63
|
-
); // Wrong checksum
|
|
64
|
-
expect(validateResidentCertificate("AA2345678", "new").isValid).toBe(
|
|
65
|
-
false,
|
|
66
|
-
); // Too short
|
|
33
|
+
test("should reject invalid old format Resident Certificates", () => {
|
|
34
|
+
expect(validateResidentCertificate("AB12345678", "old").isValid).toBe(false); // Wrong checksum
|
|
35
|
+
expect(validateResidentCertificate("AE12345678", "old").isValid).toBe(false); // Invalid second letter (must be A-D)
|
|
67
36
|
});
|
|
68
37
|
|
|
69
38
|
test("should handle case insensitive input", () => {
|
|
70
|
-
expect(validateResidentCertificate("
|
|
71
|
-
true,
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("should handle whitespace", () => {
|
|
76
|
-
expect(validateResidentCertificate(" AB23456789 ", "new").isValid).toBe(
|
|
77
|
-
true,
|
|
78
|
-
);
|
|
39
|
+
expect(validateResidentCertificate("ab12345677", "old").isValid).toBe(true);
|
|
79
40
|
});
|
|
80
41
|
});
|
|
81
42
|
|
|
82
43
|
describe("Auto-detect Format", () => {
|
|
83
44
|
test("should auto-detect and validate old format", () => {
|
|
84
|
-
expect(validateResidentCertificate("
|
|
45
|
+
expect(validateResidentCertificate("AB12345677").isValid).toBe(true);
|
|
85
46
|
});
|
|
86
47
|
|
|
87
48
|
test("should auto-detect and reject invalid old format", () => {
|
|
88
|
-
const result = validateResidentCertificate("
|
|
49
|
+
const result = validateResidentCertificate("AB12345678");
|
|
89
50
|
expect(result.isValid).toBe(false);
|
|
90
51
|
expect(result.message).toBe("無效的舊式居留證號");
|
|
91
52
|
});
|
|
92
53
|
|
|
93
54
|
test("should auto-detect and validate new format", () => {
|
|
94
|
-
expect(validateResidentCertificate("
|
|
55
|
+
expect(validateResidentCertificate("A823456783").isValid).toBe(true);
|
|
95
56
|
});
|
|
96
57
|
|
|
97
58
|
test("should auto-detect and reject invalid new format", () => {
|
|
98
|
-
const result = validateResidentCertificate("
|
|
59
|
+
const result = validateResidentCertificate("A823456780");
|
|
99
60
|
expect(result.isValid).toBe(false);
|
|
100
61
|
expect(result.message).toBe("無效的新式居留證號");
|
|
101
62
|
});
|
|
@@ -106,6 +67,40 @@ describe("validateResidentCertificate", () => {
|
|
|
106
67
|
});
|
|
107
68
|
});
|
|
108
69
|
|
|
70
|
+
describe("Parser (parseResidentCertificate)", () => {
|
|
71
|
+
test("should parse new format Resident Certificates", () => {
|
|
72
|
+
const parsed = parseResidentCertificate("A823456783");
|
|
73
|
+
expect(parsed.isValid).toBe(true);
|
|
74
|
+
expect(parsed.format).toBe("new");
|
|
75
|
+
expect(parsed.gender).toBe("male");
|
|
76
|
+
expect(parsed.region).toBe("臺北市");
|
|
77
|
+
expect(parsed.identityType).toBe("foreigner");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("should parse old format Resident Certificates", () => {
|
|
81
|
+
const parsed1 = parseResidentCertificate("AB12345677");
|
|
82
|
+
expect(parsed1.isValid).toBe(true);
|
|
83
|
+
expect(parsed1.format).toBe("old");
|
|
84
|
+
expect(parsed1.gender).toBe("female");
|
|
85
|
+
expect(parsed1.region).toBe("臺北市");
|
|
86
|
+
expect(parsed1.identityType).toBe("non-citizen");
|
|
87
|
+
|
|
88
|
+
const parsed2 = parseResidentCertificate("AC12345679");
|
|
89
|
+
expect(parsed2.isValid).toBe(true);
|
|
90
|
+
expect(parsed2.format).toBe("old");
|
|
91
|
+
expect(parsed2.gender).toBe("male");
|
|
92
|
+
expect(parsed2.region).toBe("臺北市");
|
|
93
|
+
expect(parsed2.identityType).toBe("foreigner");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("should return isValid: false for invalid resident certificates", () => {
|
|
97
|
+
const parsed = parseResidentCertificate("A823456780");
|
|
98
|
+
expect(parsed.isValid).toBe(false);
|
|
99
|
+
expect(parsed.format).toBeUndefined();
|
|
100
|
+
expect(parsed.message).toBe("無效的新式居留證號");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
109
104
|
describe("Edge Cases", () => {
|
|
110
105
|
test("should reject empty or invalid input", () => {
|
|
111
106
|
expect(validateResidentCertificate("").isValid).toBe(false);
|
|
@@ -117,7 +112,7 @@ describe("validateResidentCertificate", () => {
|
|
|
117
112
|
});
|
|
118
113
|
|
|
119
114
|
test("should provide meaningful error messages", () => {
|
|
120
|
-
const result1 = validateResidentCertificate("A823456780", "
|
|
115
|
+
const result1 = validateResidentCertificate("A823456780", "new");
|
|
121
116
|
expect(result1.isValid).toBe(false);
|
|
122
117
|
expect(result1.message).toBeDefined();
|
|
123
118
|
|