UNPKG

taiwan-id-validator

Version:

中華民國統一編號、外籍人士居留證統一編號、身分證字號驗證規則、電子發票號碼等規則驗證

382 lines (322 loc) 11.8 kB
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