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.
@@ -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
- 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個數字)
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-D][89]\d{8}$/;
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 letter = id[0] as string;
49
- const numbers = id.slice(1);
17
+ const firstLetter = id[0] as string;
18
+ const secondLetter = id[1] as string;
19
+ const numbers = id.slice(2);
50
20
 
51
- const letterValue = LETTER_MAPPING[letter] as number;
21
+ const firstLetterValue = LETTER_MAPPING[firstLetter] as number;
22
+ const secondLetterValue = LETTER_MAPPING[secondLetter] as number;
52
23
 
53
- // 計算檢查碼(與身分證字號相同的演算法)
54
- const d1 = Math.floor(letterValue / 10);
55
- const d2 = letterValue % 10;
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
- * 驗證新式居留證號格式(2個字母 + 8個數字)
70
- * 格式:AA12345678
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]{2}\d{8}$/;
47
+ const pattern = /^[A-Z][89]\d{8}$/;
77
48
  if (!pattern.test(id)) {
78
49
  return false;
79
50
  }
80
51
 
81
- const firstLetter = id[0] as string;
82
- const secondLetter = id[1] as string;
83
- const numbers = id.slice(2);
52
+ const letter = id[0] as string;
53
+ const numbers = id.slice(1);
84
54
 
85
- const firstLetterValue = LETTER_MAPPING[firstLetter] as number;
86
- const secondLetterValue = LETTER_MAPPING[secondLetter] as number;
55
+ const letterValue = LETTER_MAPPING[letter] as number;
87
56
 
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;
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, 1];
95
- const digits = [d1, d2, d3, d4, ...numbers.split("").map(Number)];
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-D][89]\d{8}$/.test(id)) {
75
+ if (/^[A-Z][A-D]\d{8}$/.test(id)) {
110
76
  return "old";
111
77
  }
112
- if (/^[A-Z]{2}\d{8}$/.test(id)) {
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('A800000000'); // 舊式格式
127
- * validateResidentCertificate('AA12345678'); // 新式格式
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
+ }