Skip to content

Commit

Permalink
feat(resident-certificate): support new resident certificate format
Browse files Browse the repository at this point in the history
  • Loading branch information
enylin committed Aug 21, 2021
1 parent c65bd0c commit 245441e
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 42 deletions.
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
## Features

* 台灣身分證字號驗證
* 台灣外僑及大陸人士在臺居留證、旅行證統一證號驗證
* 舊版臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號
* 新版臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號
* 公司統一編號驗證
* 自然人憑證編號驗證
* 電子發票手機條碼驗證
Expand All @@ -12,7 +13,7 @@
## Installation

```bash
npm i -S taiwan-id-validator
npm i taiwan-id-validator
```

## Usage
Expand All @@ -24,7 +25,9 @@ var taiwanIdValidator = require("taiwan-id-validator");

console.log(taiwanIdValidator.isGuiNumberValid('12345675')); // 統一編號
console.log(taiwanIdValidator.isNationalIdentificationNumberValid('A12345678')); // 身分證字號
console.log(taiwanIdValidator.isResidentCertificateNumberValid('AA00000009')); // 居留證編號
console.log(taiwanIdValidator.isResidentCertificateNumberValid('AA00000009')); // 居留證編號 (舊式與新式)
console.log(taiwanIdValidator.isNewResidentCertificateNumberValid('A800000014')); // 新式居留證編號
console.log(taiwanIdValidator.isOriginalResidentCertificateNumberValid('AA00000009')); // 舊式居留證編號
console.log(taiwanIdValidator.isCitizenDigitalCertificateValid('AA12345678901234')); // 自然人憑證
console.log(taiwanIdValidator.isEInvoiceCellPhoneBarcodeValid('/U.5+A33')); // 手機條碼
console.log(taiwanIdValidator.isEInvoiceDonateCodeValid('001')); // 捐贈碼
Expand All @@ -41,20 +44,24 @@ if (taiwanIdValidator.isGuiNumberValid(s)) {
## ES6, Typescript

```js
// index.js
// index.ts

import {
isGuiNumberValid, // 統一編號
isNationalIdentificationNumberValid, // 身分證字號
isResidentCertificateNumberValid, // 居留證編號
isNewResidentCertificateNumberValid, // 新式居留證編號
isOriginalResidentCertificateNumberValid, // 舊式居留證編號
isCitizenDigitalCertificateValid, // 自然人憑證
isEInvoiceCellPhoneBarcodeValid, // 手機條碼
isEInvoiceDonateCodeValid // 捐贈碼
} from 'taiwan-id-validator'

console.log(isGuiNumberValid('12345675'))
console.log(isNationalIdentificationNumberValid('A12345678'))
console.log(isResidentCertificateNumberValid('AA00000009'))
console.log(isResidentCertificateNumberValid('AA00000009')) // 居留證編號 (舊式與新式)
console.log(isNewResidentCertificateNumberValid('A800000014')) // 新式居留證編號
console.log(isOriginalResidentCertificateNumberValid('AA00000009')) // 舊式居留證編號
console.log(isCitizenDigitalCertificateValid('AA12345678901234'))
console.log(isEInvoiceCellPhoneBarcodeValid('AA12345678901234'))
console.log(isEInvoiceDonateCodeValid('AA12345678901234'))
Expand All @@ -66,4 +73,20 @@ if (isGuiNumberValid(s)) {
} else {
console.log(s + ' is not a valid GUI Number.')
}
```
```

## 新式外來人口統一證號檢查

### 內政部移民署新式外來人口統一證號專案說明

(一)為建立友善外來人口環境,本署參考歐洲在臺商務協會建議,將現行「2碼英文+8碼數字」外來人口統一證號,比照國民身分證號「1碼英文+9碼數字」編碼原則改版(以下簡稱新式統號),新式統號格式說明如下(如上圖):\
1、第1碼:區域碼,依申請地區分,比照國人格式。\
2、第2碼:性別碼,8為男性,9為女性。\
3、第10碼:檢查碼。\
(二)本署預計於110年1月2日起核發載有新式統號的證件,另考量部分永久居留外國人未在境內,為避免影響民眾權益,爰規劃換號期間為10年,舊式統號將於120年1月1日起停止使用。若有相關問題,可透過本署署長信箱系統進行反映。

參考資料:
1. [新式外來人口統一證號專案說明](https://www.immigration.gov.tw/5385/7445/238440/238442/240309/)
1. [新式外來人口統一證號懶人包](https://www.immigration.gov.tw/5382/5385/7445/238440/238442/241508/)
1. [資料標準](https://schema.gov.tw/Commonality/Commonality/Common%20Data)
1. [Introduction to the Replacement Issuance of New UI No. for Foreign Nationals](https://www.roc-taiwan.org/uploads/sites/3/2021/01/Introduction-to-the-Replacement-Issuance-of-New-UI-No.-for-Foreign-Nationals.pdf)
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "taiwan-id-validator",
"version": "0.0.9",
"description": "中華民國統一編號、身分證字號驗證規則、電子發票號碼等規則驗證",
"description": "中華民國統一編號、外籍人士居留證統一編號、身分證字號驗證規則、電子發票號碼等規則驗證",
"main": "dist/index.js",
"license": "ISC",
"scripts": {
Expand Down Expand Up @@ -30,6 +30,7 @@
"電子發票",
"統一編號",
"身分證字號",
"居留證統一編號",
"自然人憑證",
"手機條碼",
"捐贈碼",
Expand Down
91 changes: 56 additions & 35 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function isGuiNumberValid(input: string | number): boolean {
}

/**
* Verify the input is a valid National identification number (中華民國身分證字號)
* Verify the input is a valid National identification number (中華民國身分證字號)
*
* @param { string } input National identification number
* @returns { boolean } is `input` a valid national ID number
Expand All @@ -72,28 +72,13 @@ export function isNationalIdentificationNumberValid(input: string): boolean {
throw new Error('Input type should be string.')
}

/**
* 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 相加
* Step 2: 第 1 位數字 (只能為 1 or 2) 至第 8 位數字分別乘上 8, 7, 6, 5, 4, 3, 2, 1 後相加,再加上第 9 位數字
* Step 3: 如果該數字為 10 的倍數,則為正確身分證字號
*/
const regex = /^[A-Z][1,2]\d{8}$/

return regex.test(input) && verifyTaiwanIdIntermediateString(input)
}

/**
* Verify the input is a valid resident certificate number (外僑及大陸人士在台居留證、旅行證統一證號)
* Verify the input is a valid resident certificate number (臺灣地區無戶籍國民、外國人、大陸地區人民及香港或澳門居民之專屬代號)
*
* @param { string } input resident certificate number
* @returns { boolean } is `input` a valid resident certificate number
Expand All @@ -103,21 +88,41 @@ export function isResidentCertificateNumberValid(input: string): boolean {
throw new Error('Input type should be string.')
}

/**
* 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 相加,第二位英文字母按上表轉換為對應數值的個位數
* Step 2: 第 1 位數字 (由第二位英文所轉換) 至第 8 位數字分別乘上 8, 7, 6, 5, 4, 3, 2, 1 後相加,再加上第 9 位數字
* Step 3: 如果該數字為 10 的倍數,則為正確居留證號
*/
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') {
throw new Error('Input type should be string.')
}

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') {
throw new Error('Input type should be string.')
}

const regex = /^[A-Z]{2}\d{8}$/

return regex.test(input) && verifyTaiwanIdIntermediateString(input)
Expand Down Expand Up @@ -193,6 +198,20 @@ export function isEInvoiceDonateCodeValid(input: string): boolean {
function verifyTaiwanIdIntermediateString(input: string): boolean {
const idArray: string[] = input.split('')
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
Expand Down Expand Up @@ -251,17 +270,19 @@ function verifyTaiwanIdIntermediateString(input: string): boolean {
'3' // Z
]

// if is not a number (居留證編號)
// if is not a number (舊版居留證編號)
if (isNaN(parseInt(idArray[1], intRadix))) {
idArray[1] =
RESIDENT_CERTIFICATE_NUMBER_LIST[input.charCodeAt(1) - 'A'.charCodeAt(0)]
}

// Step 2: 第 1 位數字 (只能為 1 or 2) 至第 8 位數字分別乘上 8, 7, 6, 5, 4, 3, 2, 1 後相加,再加上第 9 位數字
const cb = (sum: number, n: string, index: number) =>
sum +
(index === 0
? TAIWAN_ID_LOCALE_CODE_LIST[idArray[0].charCodeAt(0) - 'A'.charCodeAt(0)]
: parseInt(idArray[index], intRadix) * (index === 9 ? 1 : 9 - index))
? TAIWAN_ID_LOCALE_CODE_LIST[n.charCodeAt(0) - 'A'.charCodeAt(0)]
: parseInt(n, intRadix) * (index === 9 ? 1 : 9 - index))

// Step 3: 如果該數字為 10 的倍數,則為正確身分證字號
return idArray.reduce(cb, 0) % 10 === 0
}
69 changes: 69 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
isGuiNumberValid,
isNationalIdentificationNumberValid,
isOriginalResidentCertificateNumberValid,
isNewResidentCertificateNumberValid,
isResidentCertificateNumberValid,
isCitizenDigitalCertificateValid,
isEInvoiceCellPhoneBarcodeValid,
Expand Down Expand Up @@ -58,6 +60,73 @@ describe('isNationalIdentificationNumberValid', () => {
})
})

describe('isOriginalResidentCertificateNumberValid', () => {
it('should only accept strings with length 10', () => {
expect(isOriginalResidentCertificateNumberValid('AA234567899')).toBe(false)
expect(isOriginalResidentCertificateNumberValid('AA2345678')).toBe(false)
})

it('should only accept strings Begin with 2 English letters', () => {
expect(isOriginalResidentCertificateNumberValid('2123456789')).toBe(false)
expect(isOriginalResidentCertificateNumberValid('1A23456789')).toBe(false)
expect(isOriginalResidentCertificateNumberValid('A123456789')).toBe(false)
})

it('should return true if the input is correct', () => {
expect(isOriginalResidentCertificateNumberValid('AA00000009')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('AB00207171')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('AC03095424')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('BD01300667')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('CC00151114')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('HD02717288')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('TD00251124')).toBe(true)
expect(isOriginalResidentCertificateNumberValid('AD30196818')).toBe(true)
})

it('should return false if the input is incorrect', () => {
expect(isOriginalResidentCertificateNumberValid('aa00000009')).toBe(false)
expect(isOriginalResidentCertificateNumberValid('AA00000000')).toBe(false)
expect(isOriginalResidentCertificateNumberValid('FG31104091')).toBe(false)
expect(isOriginalResidentCertificateNumberValid('OY58238842')).toBe(false)
})
})

describe('isNewResidentCertificateNumberValid', () => {
it('should only accept strings with length 10', () => {
expect(isNewResidentCertificateNumberValid('AA234567899')).toBe(false)
expect(isNewResidentCertificateNumberValid('AA2345678')).toBe(false)
})

it('should only accept strings Begin with 1 English letters', () => {
expect(isNewResidentCertificateNumberValid('2123456789')).toBe(false)
expect(isNewResidentCertificateNumberValid('1A23456789')).toBe(false)
expect(isNewResidentCertificateNumberValid('AA23456789')).toBe(false)
})

it('should return false if the first number is not 8 or 9', () => {
expect(isNationalIdentificationNumberValid('A323456789')).toBe(false)
expect(isNationalIdentificationNumberValid('A423456789')).toBe(false)
})

it('should return true if the input is correct', () => {
expect(isNewResidentCertificateNumberValid('A800000014')).toBe(true)
expect(isNewResidentCertificateNumberValid('A900207177')).toBe(true)
expect(isNewResidentCertificateNumberValid('A803095426')).toBe(true)
expect(isNewResidentCertificateNumberValid('B801300667')).toBe(true)
expect(isNewResidentCertificateNumberValid('C800151116')).toBe(true)
expect(isNewResidentCertificateNumberValid('H802717288')).toBe(true)
expect(isNewResidentCertificateNumberValid('T900251126')).toBe(true)
expect(isNewResidentCertificateNumberValid('A930196810')).toBe(true)
})

it('should return false if the input is incorrect', () => {
expect(isNewResidentCertificateNumberValid('a800000009')).toBe(false)
expect(isNewResidentCertificateNumberValid('A800000000')).toBe(false)
expect(isNewResidentCertificateNumberValid('F931104091')).toBe(false)
expect(isNewResidentCertificateNumberValid('O958238842')).toBe(false)
})
})

describe('isResidentCertificateNumberValid', () => {
it('should only accept strings with length 10', () => {
expect(isResidentCertificateNumberValid('AA234567899')).toBe(false)
Expand Down

0 comments on commit 245441e

Please sign in to comment.