taiwan-validator 1.1.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 +2 -1
- package/README.md +2 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +21 -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/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,61 +1,32 @@
|
|
|
1
|
-
import type { ValidationResult, ResidentCertificateType } from "../types";
|
|
1
|
+
import type { ValidationResult, ResidentCertificateType, ResidentCertificateInfo } from "../types";
|
|
2
|
+
import { LETTER_MAPPING, REGION_MAPPING } from "./shared";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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個數字)
|
|
37
|
-
* 格式:A800000000
|
|
38
|
-
* - 第一個字元:地區代碼(A、B、C 或 D 表示外國人士)
|
|
39
|
-
* - 第二個字元:性別/類型(8 = 男性,9 = 女性)
|
|
5
|
+
* 驗證舊式居留證號格式與檢查碼(2個字母 + 8個數字)
|
|
6
|
+
* 格式:AA12345678
|
|
7
|
+
* - 第一個字元:地區代碼(字母)
|
|
8
|
+
* - 第二個字元:身分與性別碼(A = 男性無戶籍國民/港澳陸居民,B = 女性無戶籍國民/港澳陸居民,C = 男性外國人,D = 女性外國人)
|
|
40
9
|
* - 最後一個字元:檢查碼
|
|
41
10
|
*/
|
|
42
11
|
function validateOldFormat(id: string): boolean {
|
|
43
|
-
const pattern = /^[A-
|
|
12
|
+
const pattern = /^[A-Z][A-D]\d{8}$/;
|
|
44
13
|
if (!pattern.test(id)) {
|
|
45
14
|
return false;
|
|
46
15
|
}
|
|
47
16
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
17
|
+
const firstLetter = id[0] as string;
|
|
18
|
+
const secondLetter = id[1] as string;
|
|
19
|
+
const numbers = id.slice(2);
|
|
50
20
|
|
|
51
|
-
const
|
|
21
|
+
const firstLetterValue = LETTER_MAPPING[firstLetter] as number;
|
|
22
|
+
const secondLetterValue = LETTER_MAPPING[secondLetter] as number;
|
|
52
23
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
24
|
+
const d1 = Math.floor(firstLetterValue / 10);
|
|
25
|
+
const d2 = firstLetterValue % 10;
|
|
26
|
+
const d3 = secondLetterValue % 10; // 取第二個字母對應數字的個位數
|
|
56
27
|
|
|
57
28
|
const weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1];
|
|
58
|
-
const digits = [d1, d2, ...numbers.split("").map(Number)];
|
|
29
|
+
const digits = [d1, d2, d3, ...numbers.split("").map(Number)];
|
|
59
30
|
|
|
60
31
|
const sum = digits.reduce(
|
|
61
32
|
(acc, digit, index) => acc + digit * weights[index]!,
|
|
@@ -66,33 +37,28 @@ function validateOldFormat(id: string): boolean {
|
|
|
66
37
|
}
|
|
67
38
|
|
|
68
39
|
/**
|
|
69
|
-
*
|
|
70
|
-
* 格式:
|
|
40
|
+
* 驗證新式居留證號格式與檢查碼(1個字母 + 9個數字,其中第二碼為8或9)
|
|
41
|
+
* 格式:A800000001
|
|
71
42
|
* - 第一個字元:地區代碼(字母)
|
|
72
|
-
* - 第二個字元:性別(8 = 男性,9 =
|
|
43
|
+
* - 第二個字元:性別(8 = 男性,9 = 女性)
|
|
73
44
|
* - 最後一個字元:檢查碼
|
|
74
45
|
*/
|
|
75
46
|
function validateNewFormat(id: string): boolean {
|
|
76
|
-
const pattern = /^[A-Z]
|
|
47
|
+
const pattern = /^[A-Z][89]\d{8}$/;
|
|
77
48
|
if (!pattern.test(id)) {
|
|
78
49
|
return false;
|
|
79
50
|
}
|
|
80
51
|
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const numbers = id.slice(2);
|
|
52
|
+
const letter = id[0] as string;
|
|
53
|
+
const numbers = id.slice(1);
|
|
84
54
|
|
|
85
|
-
const
|
|
86
|
-
const secondLetterValue = LETTER_MAPPING[secondLetter] as number;
|
|
55
|
+
const letterValue = LETTER_MAPPING[letter] as number;
|
|
87
56
|
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const d2 = firstLetterValue % 10;
|
|
91
|
-
const d3 = Math.floor(secondLetterValue / 10);
|
|
92
|
-
const d4 = secondLetterValue % 10;
|
|
57
|
+
const d1 = Math.floor(letterValue / 10);
|
|
58
|
+
const d2 = letterValue % 10;
|
|
93
59
|
|
|
94
|
-
const weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1
|
|
95
|
-
const digits = [d1, d2,
|
|
60
|
+
const weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1];
|
|
61
|
+
const digits = [d1, d2, ...numbers.split("").map(Number)];
|
|
96
62
|
|
|
97
63
|
const sum = digits.reduce(
|
|
98
64
|
(acc, digit, index) => acc + digit * weights[index]!,
|
|
@@ -106,25 +72,25 @@ function validateNewFormat(id: string): boolean {
|
|
|
106
72
|
* 偵測居留證號格式類型
|
|
107
73
|
*/
|
|
108
74
|
function detectFormat(id: string): ResidentCertificateType | null {
|
|
109
|
-
if (/^[A-
|
|
75
|
+
if (/^[A-Z][A-D]\d{8}$/.test(id)) {
|
|
110
76
|
return "old";
|
|
111
77
|
}
|
|
112
|
-
if (/^[A-Z]
|
|
78
|
+
if (/^[A-Z][89]\d{8}$/.test(id)) {
|
|
113
79
|
return "new";
|
|
114
80
|
}
|
|
115
81
|
return null;
|
|
116
82
|
}
|
|
117
83
|
|
|
118
84
|
/**
|
|
119
|
-
*
|
|
85
|
+
* 驗證台灣居留證號(支援新舊格式,舊版為2字母+8數字,新版為1字母+9數字)
|
|
120
86
|
* @param id - 要驗證的居留證號
|
|
121
87
|
* @param format - 可選:指定格式類型('old' 或 'new')
|
|
122
88
|
* @returns 驗證結果
|
|
123
89
|
*
|
|
124
90
|
* @example
|
|
125
91
|
* ```typescript
|
|
126
|
-
* validateResidentCertificate('
|
|
127
|
-
* validateResidentCertificate('
|
|
92
|
+
* validateResidentCertificate('AD00000001', 'old'); // 舊式格式
|
|
93
|
+
* validateResidentCertificate('A800000001', 'new'); // 新式格式
|
|
128
94
|
* ```
|
|
129
95
|
*/
|
|
130
96
|
export function validateResidentCertificate(
|
|
@@ -140,7 +106,6 @@ export function validateResidentCertificate(
|
|
|
140
106
|
|
|
141
107
|
const normalizedId = id.trim().toUpperCase();
|
|
142
108
|
|
|
143
|
-
// 如果指定了格式,只驗證該格式
|
|
144
109
|
if (format === "old") {
|
|
145
110
|
const isValid = validateOldFormat(normalizedId);
|
|
146
111
|
return {
|
|
@@ -157,7 +122,6 @@ export function validateResidentCertificate(
|
|
|
157
122
|
};
|
|
158
123
|
}
|
|
159
124
|
|
|
160
|
-
// 自動偵測格式
|
|
161
125
|
const detectedFormat = detectFormat(normalizedId);
|
|
162
126
|
|
|
163
127
|
if (!detectedFormat) {
|
|
@@ -179,3 +143,69 @@ export function validateResidentCertificate(
|
|
|
179
143
|
: `無效的${detectedFormat === "old" ? "舊式" : "新式"}居留證號`,
|
|
180
144
|
};
|
|
181
145
|
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 解析台灣居留證號資訊(格式版本、性別、發證地區、身分類型)
|
|
149
|
+
* @param id - 要解析的居留證號
|
|
150
|
+
* @returns 解析結果,包含驗證狀態及相關欄位
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* parseResidentCertificate('A800000001');
|
|
155
|
+
* // 輸出: { isValid: true, format: 'new', gender: 'male', region: '臺北市' }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export function parseResidentCertificate(id: string): ResidentCertificateInfo {
|
|
159
|
+
if (!id || typeof id !== "string") {
|
|
160
|
+
return {
|
|
161
|
+
isValid: false,
|
|
162
|
+
message: "居留證號必須為非空字串",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const normalizedId = id.trim().toUpperCase();
|
|
167
|
+
|
|
168
|
+
const format = detectFormat(normalizedId);
|
|
169
|
+
if (!format) {
|
|
170
|
+
return {
|
|
171
|
+
isValid: false,
|
|
172
|
+
message: "無效的居留證號格式",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const isValid =
|
|
177
|
+
format === "old"
|
|
178
|
+
? validateOldFormat(normalizedId)
|
|
179
|
+
: validateNewFormat(normalizedId);
|
|
180
|
+
|
|
181
|
+
if (!isValid) {
|
|
182
|
+
return {
|
|
183
|
+
isValid: false,
|
|
184
|
+
message: `無效的${format === "old" ? "舊式" : "新式"}居留證號`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const firstLetter = normalizedId[0] as string;
|
|
189
|
+
const secondChar = normalizedId[1] as string;
|
|
190
|
+
|
|
191
|
+
const region = REGION_MAPPING[firstLetter] as string;
|
|
192
|
+
let gender: "male" | "female";
|
|
193
|
+
let identityType: "non-citizen" | "foreigner" | undefined;
|
|
194
|
+
|
|
195
|
+
if (format === "new") {
|
|
196
|
+
gender = secondChar === "8" ? "male" : "female";
|
|
197
|
+
identityType = "foreigner"; // 新式格式下通常統稱為外來人口
|
|
198
|
+
} else {
|
|
199
|
+
// 舊式格式: A, B, C, D
|
|
200
|
+
gender = (secondChar === "A" || secondChar === "C") ? "male" : "female";
|
|
201
|
+
identityType = (secondChar === "A" || secondChar === "B") ? "non-citizen" : "foreigner";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
isValid: true,
|
|
206
|
+
format,
|
|
207
|
+
gender,
|
|
208
|
+
region,
|
|
209
|
+
identityType,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字母對應數字表(用於身分證字號與居留證號驗證)
|
|
3
|
+
*/
|
|
4
|
+
export const LETTER_MAPPING: Record<string, number> = {
|
|
5
|
+
A: 10,
|
|
6
|
+
B: 11,
|
|
7
|
+
C: 12,
|
|
8
|
+
D: 13,
|
|
9
|
+
E: 14,
|
|
10
|
+
F: 15,
|
|
11
|
+
G: 16,
|
|
12
|
+
H: 17,
|
|
13
|
+
I: 34,
|
|
14
|
+
J: 18,
|
|
15
|
+
K: 19,
|
|
16
|
+
L: 20,
|
|
17
|
+
M: 21,
|
|
18
|
+
N: 22,
|
|
19
|
+
O: 35,
|
|
20
|
+
P: 23,
|
|
21
|
+
Q: 24,
|
|
22
|
+
R: 25,
|
|
23
|
+
S: 26,
|
|
24
|
+
T: 27,
|
|
25
|
+
U: 28,
|
|
26
|
+
V: 29,
|
|
27
|
+
W: 32,
|
|
28
|
+
X: 30,
|
|
29
|
+
Y: 31,
|
|
30
|
+
Z: 33,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 字母對應地區名稱表
|
|
35
|
+
*/
|
|
36
|
+
export const REGION_MAPPING: Record<string, string> = {
|
|
37
|
+
A: "臺北市",
|
|
38
|
+
B: "臺中市",
|
|
39
|
+
C: "基隆市",
|
|
40
|
+
D: "臺南市",
|
|
41
|
+
E: "高雄市",
|
|
42
|
+
F: "新北市",
|
|
43
|
+
G: "宜蘭縣",
|
|
44
|
+
H: "桃園市",
|
|
45
|
+
I: "嘉義市",
|
|
46
|
+
J: "新竹縣",
|
|
47
|
+
K: "苗栗縣",
|
|
48
|
+
L: "臺中縣",
|
|
49
|
+
M: "南投縣",
|
|
50
|
+
N: "彰化縣",
|
|
51
|
+
O: "新竹市",
|
|
52
|
+
P: "雲林縣",
|
|
53
|
+
Q: "嘉義縣",
|
|
54
|
+
R: "臺南縣",
|
|
55
|
+
S: "高雄縣",
|
|
56
|
+
T: "屏東縣",
|
|
57
|
+
U: "花蓮縣",
|
|
58
|
+
V: "臺東縣",
|
|
59
|
+
W: "金門縣",
|
|
60
|
+
X: "澎湖縣",
|
|
61
|
+
Y: "陽明山管理局",
|
|
62
|
+
Z: "連江縣",
|
|
63
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validateUniformInvoice } from "./uniform-invoice";
|
|
2
|
+
|
|
3
|
+
describe("validateUniformInvoice", () => {
|
|
4
|
+
test("should validate correct invoice numbers", () => {
|
|
5
|
+
expect(validateUniformInvoice("AB-12345678").isValid).toBe(true);
|
|
6
|
+
expect(validateUniformInvoice("ab-12345678").isValid).toBe(true);
|
|
7
|
+
expect(validateUniformInvoice("AB12345678").isValid).toBe(true);
|
|
8
|
+
expect(validateUniformInvoice("AB 12345678").isValid).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("should reject invalid invoice numbers", () => {
|
|
12
|
+
expect(validateUniformInvoice("A-12345678").isValid).toBe(false); // Only 1 letter
|
|
13
|
+
expect(validateUniformInvoice("ABC-12345678").isValid).toBe(false); // 3 letters
|
|
14
|
+
expect(validateUniformInvoice("AB-1234567").isValid).toBe(false); // 7 digits
|
|
15
|
+
expect(validateUniformInvoice("AB-123456789").isValid).toBe(false); // 9 digits
|
|
16
|
+
expect(validateUniformInvoice("1234567890").isValid).toBe(false); // No letters
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("should reject empty or non-string inputs", () => {
|
|
20
|
+
expect(validateUniformInvoice("").isValid).toBe(false);
|
|
21
|
+
// @ts-expect-error Testing invalid input type
|
|
22
|
+
expect(validateUniformInvoice(null).isValid).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ValidationResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驗證台灣統一發票號碼格式
|
|
5
|
+
* 格式為 2 碼英文開頭 + 8 碼數字(可包含減號或空格,如 AB-12345678 或 AB 12345678)
|
|
6
|
+
*
|
|
7
|
+
* @param invoice - 要驗證的發票號碼
|
|
8
|
+
* @returns 驗證結果
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* validateUniformInvoice('AB-12345678'); // { isValid: true }
|
|
13
|
+
* validateUniformInvoice('AB12345678'); // { isValid: true }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function validateUniformInvoice(invoice: string): ValidationResult {
|
|
17
|
+
if (!invoice || typeof invoice !== "string") {
|
|
18
|
+
return {
|
|
19
|
+
isValid: false,
|
|
20
|
+
message: "發票號碼必須為非空字串",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 去除空格與減號並轉大寫
|
|
25
|
+
const normalized = invoice.replace(/[-\s]/g, "").toUpperCase();
|
|
26
|
+
|
|
27
|
+
const pattern = /^[A-Z]{2}\d{8}$/;
|
|
28
|
+
|
|
29
|
+
if (!pattern.test(normalized)) {
|
|
30
|
+
return {
|
|
31
|
+
isValid: false,
|
|
32
|
+
message: "發票號碼格式必須為2碼英文與8碼數字",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
isValid: true,
|
|
38
|
+
};
|
|
39
|
+
}
|