taiwan-id-validator
Version:
中華民國統一編號、外籍人士居留證統一編號、身分證字號驗證規則、電子發票號碼等規則驗證
382 lines (322 loc) • 11.8 kB
text/typescript
function zipWith<T, R>(a1: T[], a2: T[], f: (v1: T, v2: T) => R): R[] {
const length = Math.min(a1.length, a2.length)
const result: R[] = []
for (let i = 0; i < length; i++) result[i] = f(a1[i], a2[i])
return result
}
function add(a: number, b: number) {
return a + b
}
function multiply(a: number, b: number) {
return a * b
}
/**
* Verify the input is a valid GUI Number (中華民國統一編號)
*
* @param { string | number } input GUI Number
* @param { boolean } extended check input using extended format: https://www.fia.gov.tw/singlehtml/6?cntId=aaa97a9dcf2649d5bdd317f554e24f75
* @returns { boolean } is `input` a valid GUI number
*/
export function isGuiNumberValid(
input: string | number,
extended = false
): boolean {
if (typeof input !== 'string' && typeof input !== 'number') return false
/**
* Example: 12345675
* Step 1:
* 1 * 1 = 1
* 2 * 2 = 4
* 3 * 1 = 3
* 4 * 2 = 8
* 5 * 1 = 5
* 6 * 2 = 12
* 7 * 4 = 28
* 5 * 1 = 5
*
* Step 2:
* 1 -> 1
* 4 -> 4
* 3 -> 3
* 8 -> 8
* 5 -> 5
* 12 -> 1 + 2 = 3
* 28 -> 2 + 8 = 10
* 5 -> 5
*
* Step 3:
* (1 + 4 + 3 + 8 + 5 + 3 + 10 + 5) % 10 = 9
*/
const GUI_NUMBER_COEFFICIENTS = [1, 2, 1, 2, 1, 2, 4, 1]
const n = input.toString()
const regex = /^\d{8}$/
if (!regex.test(n)) return false
/**
* Step 1: 先把統一編號的每個數字分別乘上對應的係數 (1, 2, 1, 2, 1, 2, 4, 1)
* Step 2: 再把個別乘積的十位數與個位數相加,得出八個小於 10 的數字
*/
const intRadix = 10
const checksum = zipWith(
GUI_NUMBER_COEFFICIENTS,
n.split('').map(c => parseInt(c, intRadix)),
multiply
)
.map(n => (n % 10) + Math.floor(n / 10))
.reduce(add, 0)
/**
* Step 3: 檢查把這 8 個數字相加之後計算此和除以 5 or 10 的餘數
* Step 4:
* 4-1: 若是餘數為 0,則為正確的統一編號
* 4-2: 若是餘數為 9,且原統一編號的第七位是 7,則也為正確的統一編號
*/
const divisor = extended ? 5 : 10
return (
checksum % divisor === 0 ||
(parseInt(n.charAt(6), intRadix) === 7 && (checksum + 1) % divisor === 0)
)
}
/**
* Verify the input is a valid National identification number (中華民國身分證字號)
*
* @param { string } input National identification number
* @returns { boolean } is `input` a valid national ID number
*/
export function isNationalIdentificationNumberValid(input: string): boolean {
if (typeof input !== 'string') return false
const regex = /^[A-Z][1,2]\d{8}$/
return regex.test(input) && verifyTaiwanIdIntermediateString(input)
}
/**
* Verify the input is a valid resident certificate number (臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號)
*
* @param { string } input resident certificate number
* @returns { boolean } is `input` a valid resident certificate number
*/
export function isResidentCertificateNumberValid(input: string): boolean {
if (typeof input !== 'string') return false
return (
isNewResidentCertificateNumberValid(input) ||
isOriginalResidentCertificateNumberValid(input)
)
}
/**
* Verify the input is a valid new resident certificate number (臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號)
*
* @param { string } input resident certificate number
* @returns { boolean } is `input` a valid new resident certificate number
*/
export function isNewResidentCertificateNumberValid(input: string): boolean {
if (typeof input !== 'string') return false
const regex = /^[A-Z][8,9]\d{8}$/
return regex.test(input) && verifyTaiwanIdIntermediateString(input)
}
/**
* Verify the input is a original valid resident certificate number (臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號)
*
* @param { string } input resident certificate number
* @returns { boolean } is `input` a valid original resident certificate number
*/
export function isOriginalResidentCertificateNumberValid(
input: string
): boolean {
if (typeof input !== 'string') return false
const regex = /^[A-Z]{2}\d{8}$/
return regex.test(input) && verifyTaiwanIdIntermediateString(input)
}
/**
* Verify the input is a valid citizen digital certificate number (自然人憑證)
*
* @param { string } input citizen digital certificate number
* @returns { boolean } is `input` a valid citizen digital certificate number
*/
export function isCitizenDigitalCertificateNumberValid(input: string): boolean {
if (typeof input !== 'string') return false
const n = input.toString()
// 驗證規則為兩碼英文 + 14 碼數字
const regex = /^[A-Z]{2}\d{14}$/
return regex.test(n)
}
/**
* Verify the input is a valid E-Invoice cell phone barcode (電子發票手機條碼)
*
* @param { string } input E-Invoice cell phone barcode
* @returns { boolean } is `input` a valid e-invoice cell phone barcode
*/
export function isEInvoiceCellPhoneBarcodeValid(input: string): boolean {
if (typeof input !== 'string') return false
const n = input.toString()
/**
* 總長度為 8 碼
* 第 1 碼為 /
* 第 2-8 碼由 0-9 (數字), A-Z (大寫英文字母), .(period), -(hyphen), +(plus) 組成
*/
const regex = /^\/[\dA-Z.\-+]{7}$/
return regex.test(n)
}
/**
* Verify the input is a valid E-Invoice donate code (電子發票捐贈碼)
*
* @param { string | number } input E-Invoice donate code
* @returns { boolean } is `input` a valid e-invoice donate code
*/
export function isEInvoiceDonateCodeValid(input: string | number): boolean {
if (typeof input !== 'string' && typeof input !== 'number') return false
const n = input.toString()
// 總長度為 3-7 碼 0-9 的數字
const regex = /^[\d]{3,7}$/
return regex.test(n)
}
export type CreditCardValidationOptions = {
/**
* validate `input` with regex
* (Warning! Card issuers might introduce new card number patterns. Use this in production might cause unexpected results.)
*/
checkIssuerRegexes?: boolean
}
/**
* Verify the input is a valid credit card number (信用卡卡號)
*
* @param { string | number } input credit card number
* @param { CreditCardValidationOptions } creditCardValidationOptions credit card validation options
* @returns { boolean } is `input` a valid credit card number
*/
export function isCreditCardNumberValid(input: string, options: CreditCardValidationOptions = {}): boolean {
if (typeof input !== 'string') return false
const regex = /^[0-9]{12,19}$/
if (!regex.test(input)) return false
// ref:
// https://stackoverflow.com/questions/9315647/regex-credit-card-number-tests
// https://en.wikipedia.org/wiki/Payment_card_number
const issuerRegexes = [
/^3[47][0-9]{13}$/, // American Express
/^(6541|6556)[0-9]{12}$/, // BCGlobal
/^389[0-9]{11}$/, // Carte Blanche
/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/, // Diner's Club
/^65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})$/, // Discover
/^63[7-9][0-9]{13}$/, // Insta Payment
/^(?:2131|1800|35\d{3})\d{11}$/, // JCB
/^9[0-9]{15}$/, // KoreanLocalCard
/^(6304|6706|6709|6771)[0-9]{12,15}$/, // Laser
/^(5018|5020|5038|6304|6759|6761|6763)[0-9]{8,15}$/, // Maestro
/^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/, // Mastercard
/^(6334|6767)[0-9]{12}|(6334|6767)[0-9]{14}|(6334|6767)[0-9]{15}$/, // Solo
/^(4903|4905|4911|4936|6333|6759)[0-9]{12}|(4903|4905|4911|4936|6333|6759)[0-9]{14}|(4903|4905|4911|4936|6333|6759)[0-9]{15}|564182[0-9]{10}|564182[0-9]{12}|564182[0-9]{13}|633110[0-9]{10}|633110[0-9]{12}|633110[0-9]{13}$/, // Switch
/^(62[0-9]{14,17})$/, // Union Pay
/^4[0-9]{12}(?:[0-9]{3})?$/, // Visa
/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})$/ // Visa Master
]
const { checkIssuerRegexes = false } = options
if (checkIssuerRegexes && !issuerRegexes.some(regex => regex.test(input)))
return false
// Luhn algorithm
// ref: https://en.wikipedia.org/wiki/Luhn_algorithm
const digits = input.split('').map(d => parseInt(d, 10))
const sum = digits.reverse().reduce((acc, d, i) => {
if (i % 2 === 0) return acc + d
return acc + (d * 2 > 9 ? d * 2 - 9 : d * 2)
}, 0)
return sum % 10 === 0
}
/**
* Verify the intermediate string for isNationalIdentificationNumberValid and isResidentCertificateNumberValid
*
* @param { string } input String to verify
* @returns { boolean } is `input` a valid Taiwan ID intermediate string
*/
function verifyTaiwanIdIntermediateString(input: string): boolean {
const intRadix = 10
/**
* A=10 台北市 J=18 新竹縣 S=26 高雄縣
* B=11 台中市 K=19 苗栗縣 T=27 屏東縣
* C=12 基隆市 L=20 台中縣 U=28 花蓮縣
* D=13 台南市 M=21 南投縣 V=29 台東縣
* E=14 高雄市 N=22 彰化縣 W=32 金門縣*
* F=15 台北縣 O=35 新竹市* X=30 澎湖縣
* G=16 宜蘭縣 P=23 雲林縣 Y=31 陽明山
* H=17 桃園縣 Q=24 嘉義縣 Z=33 連江縣*
* I=34 嘉義市* R=25 台南縣
*
* Step 1: 英文字母按照上表轉換為數字之後,十位數 * 1 + 個位數 * 9 相加
*/
const TAIWAN_ID_LOCALE_CODE_LIST = [
1, // A -> 10 -> 1 * 1 + 9 * 0 = 1
10, // B -> 11 -> 1 * 1 + 9 * 1 = 10
19, // C -> 12 -> 1 * 1 + 9 * 2 = 19
28, // D
37, // E
46, // F
55, // G
64, // H
39, // I -> 34 -> 1 * 3 + 9 * 4 = 39
73, // J
82, // K
2, // L
11, // M
20, // N
48, // O -> 35 -> 1 * 3 + 9 * 5 = 48
29, // P
38, // Q
47, // R
56, // S
65, // T
74, // U
83, // V
21, // W -> 32 -> 1 * 3 + 9 * 2 = 21
3, // X
12, // Y
30 // Z -> 33 -> 1 * 3 + 9 * 3 = 30
]
const RESIDENT_CERTIFICATE_NUMBER_LIST = [
0, // A
1, // B
2, // C
3, // D
4, // E
5, // F
6, // G
7, // H
4, // I
8, // J
9, // K
0, // L
1, // M
2, // N
5, // O
3, // P
4, // Q
5, // R
6, // S
7, // T
8, // U
9, // V
2, // W
0, // X
1, // Y
3 // Z
]
const getCharOrder = (s: string, i: number) =>
s.charCodeAt(i) - 'A'.charCodeAt(0)
const firstDigit = TAIWAN_ID_LOCALE_CODE_LIST[getCharOrder(input, 0)]
const secondDigit = isNaN(parseInt(input[1], intRadix)) // if is not a number (舊版居留證編號)
? RESIDENT_CERTIFICATE_NUMBER_LIST[getCharOrder(input, 1)]
: parseInt(input[1], intRadix)
const rest = input
.substring(2)
.split('')
.map(n => parseInt(n, intRadix))
const idInDigits = [firstDigit, secondDigit, ...rest]
// Step 2: 第 1 位數字 (只能為 1 or 2) 至第 8 位數字分別乘上 8, 7, 6, 5, 4, 3, 2, 1 後相加,再加上第 9 位數字
const ID_COEFFICIENTS = [1, 8, 7, 6, 5, 4, 3, 2, 1, 1]
const sum = zipWith(idInDigits, ID_COEFFICIENTS, multiply).reduce(add, 0)
// Step 3: 如果該數字為 10 的倍數,則為正確身分證字號
return sum % 10 === 0
}
export const isGUI = isGuiNumberValid
export const isNI = isNationalIdentificationNumberValid
export const isRC = isResidentCertificateNumberValid
export const isNewRC = isNewResidentCertificateNumberValid
export const isOriginalRC = isOriginalResidentCertificateNumberValid
export const isCDC = isCitizenDigitalCertificateNumberValid
export const isCellPhoneBarcode = isEInvoiceCellPhoneBarcodeValid
export const isDonateCode = isEInvoiceDonateCodeValid
export const isCreditCard = isCreditCardNumberValid