National ID numbers
National identity numbers of 29 countries checked by their own rules (US SSN, UK NINO, CPF, Aadhaar, PH TIN, DNI and NIE, codice fiscale, PESEL...), with the birth date and sex read out where the number holds them.
Version 1.0.0 · 4 functions · 1 crate
Functions
What use national_ids.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use national_id_check, national_id_valid from national_ids (Quillo 0.6).
national_id_check(country: str, number: str) → anyChecks a national ID number of country (ISO 3166 code: "US", "GB", "BR", "IN", "PH", "ES", "IT", "CN"...; see national_id_countries): { valid, country, kind, formatted, birth_date, sex, note, error }. Spaces, dots, dashes and slashes are ignored. birth_date (ISO) and sex ("M" or "F") are filled where the number holds them; note says what else it tells, or what could not be checked.
national_id_valid(country: str, number: str) → boolWhether number is a valid national ID of country.
national_id_countries() → listThe countries with rules: [{ country, name }].
national_id_mask(number: str, visible: int) → strnumber with all but its last visible characters hidden (*****6789), for showing an ID back to its owner.
Get it
$ quillo install national_ids # national_ids.qwer into this folder$ quillo install national_ids --example # and its example, national_ids.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/national_ids.qwer$ curl -O https://quillolang.org/plugins/files/national_ids.qweThen, in your program, all of it or only what you call:
use national_ids.qweruse national_id_check, national_id_valid from national_ids // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }Example
// The identity step of a sign-up form for an international payroll app: each country's number// checked by its own rules, and the birth date it holds compared with the one typed.//// quillo run national_ids.qwe then open http://localhost:8080/samples and http://localhost:8080/check/BR/529.982.247-25use "national_ids.qwer"route GET /check/:country/*rest = national_id_check(country, rest)let SAMPLES = [ ["US", "123-45-6789"], ["GB", "AB 12 34 56 C"], ["BR", "529.982.247-25"], ["IN", "2341 2341 2346"], ["PH", "123-456-789-000"], ["ES", "12345678Z"], ["ES", "X1234567L"], ["IT", "RSSMRA85T10A562S"], ["FR", "2 55 08 14 168 025 38"], ["NL", "111222333"], ["BE", "85.07.30-033.28"], ["SE", "811228-9874"], ["NO", "15076500565"], ["FI", "131052-308T"], ["PL", "44051401359"], ["CN", "11010519491231002X"], ["ZA", "8001015009087"], ["CL", "12.345.678-5"], ["MX", "HEGG560427MVZRRL04"], ["CA", "130 692 544"], ["SG", "S1234567D"], ["TR", "10000000146"], ["RO", "1800101221144"], ["JP", "1234 5678 9018"], ["IE", "1234567T"], ["IL", "000000018"],]route GET /samples = SAMPLES | map((p) => { let r = national_id_check(p[0], p[1]); `${p[0]} ${r.valid ? r.kind : r.error}${r.birth_date != null ? " " + r.birth_date : ""}` })route GET /details = { italy: national_id_check("IT", "RSSMRA85T10A562S"), china: national_id_check("CN", "11010519491231002X"), typo: national_id_check("BR", "529.982.247-26").error, ssn: national_id_check("US", "666-12-3456").error, masked: national_id_mask("529.982.247-25", 4), countries: len(national_id_countries()),}$ quillo run national_ids.qwe$ curl 'localhost:8080/check/BR/529.982.247-25'$ curl 'localhost:8080/details'$ curl 'localhost:8080/samples'Source
//! National identity numbers of 29 countries checked by their own rules (check digits, dates, reserved ranges) and, where the number holds them, the birth date and sex read out: US SSN, UK NINO, Brazil's CPF, India's Aadhaar, the Philippines' TIN, Spain's DNI and NIE, Italy's codice fiscale, China's resident ID, and more.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};struct Info { kind: &'static str, formatted: String, birth: Option<(i32, u32, u32)>, sex: Option<&'static str>, note: Option<String>,}type R = Result<Info, String>;fn info(kind: &'static str, formatted: String) -> Info { Info { kind, formatted, birth: None, sex: None, note: None }}fn digits_only(s: &str) -> String { s.chars().filter(|c| !c.is_whitespace() && !"-./".contains(*c)).collect::<String>().to_uppercase()}fn d(s: &str) -> Vec<u32> { s.chars().filter_map(|c| c.to_digit(10)).collect()}fn all_digits(s: &str, n: usize) -> Result<(), String> { if s.len() == n && s.chars().all(|c| c.is_ascii_digit()) { Ok(()) } else { Err(format!("{} digits are expected", n)) }}fn num(s: &str, a: usize, b: usize) -> u32 { s[a..b].parse().unwrap_or(0)}fn date_ok(y: i32, m: u32, day: u32) -> bool { let dim = match m { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29, 2 => 28, _ => return false, }; day >= 1 && day <= dim}fn birth(y: i32, m: u32, day: u32) -> Result<Option<(i32, u32, u32)>, String> { if date_ok(y, m, day) { Ok(Some((y, m, day))) } else { Err(format!("{:04}-{:02}-{:02} is not a date", y, m, day)) }}fn luhn(s: &str) -> bool { s.chars().rev().enumerate().map(|(i, c)| { let v = c.to_digit(10).unwrap_or(0); if i % 2 == 1 { let x = v * 2; if x > 9 { x - 9 } else { x } } else { v } }).sum::<u32>() % 10 == 0}fn verhoeff(s: &str) -> bool { const D: [[u8; 10]; 10] = [[0,1,2,3,4,5,6,7,8,9],[1,2,3,4,0,6,7,8,9,5],[2,3,4,0,1,7,8,9,5,6],[3,4,0,1,2,8,9,5,6,7],[4,0,1,2,3,9,5,6,7,8],[5,9,8,7,6,0,4,3,2,1],[6,5,9,8,7,1,0,4,3,2],[7,6,5,9,8,2,1,0,4,3],[8,7,6,5,9,3,2,1,0,4],[9,8,7,6,5,4,3,2,1,0]]; const P: [[u8; 10]; 8] = [[0,1,2,3,4,5,6,7,8,9],[1,5,7,6,2,8,3,0,9,4],[5,8,0,3,7,9,6,1,4,2],[8,9,1,6,0,4,3,5,2,7],[9,4,5,3,1,2,6,8,7,0],[4,2,8,6,5,7,3,9,0,1],[2,7,9,3,8,0,6,4,1,5],[7,0,4,6,9,1,3,2,5,8]]; let mut c = 0u8; for (i, ch) in s.chars().rev().enumerate() { c = D[c as usize][P[i % 8][ch.to_digit(10).unwrap_or(0) as usize] as usize]; } c == 0}fn weighted(ds: &[u32], w: &[u32]) -> u32 { ds.iter().zip(w).map(|(a, b)| a * b).sum()}const BAD_CHECK: &str = "the check digit does not match: a typo";fn us(s: &str) -> R { all_digits(s, 9)?; let (a, g, n) = (num(s, 0, 3), num(s, 3, 5), num(s, 5, 9)); if a == 0 || a == 666 || a >= 900 { return Err(format!("no SSN starts with {:03}", a)); } if g == 0 || n == 0 { return Err("no SSN has 00 in the middle or 0000 at the end".into()); } let mut i = info("Social Security number", format!("{}-{}-{}", &s[..3], &s[3..5], &s[5..])); if ["078051120", "219099999", "123456789"].contains(&s) { i.note = Some("a well-known example number, never issued to a person".into()); } Ok(i)}fn gb(s: &str) -> R { let c: Vec<char> = s.chars().collect(); if c.len() != 9 || !c[0].is_ascii_alphabetic() || !c[1].is_ascii_alphabetic() || !c[2..8].iter().all(|x| x.is_ascii_digit()) || !"ABCD".contains(c[8]) { return Err("a NINO is two letters, six digits and A, B, C or D".into()); } if "DFIQUV".contains(c[0]) || "DFIOQUV".contains(c[1]) || ["BG", "GB", "KN", "NK", "NT", "TN", "ZZ"].contains(&&s[..2]) { return Err(format!("no NINO starts with {}", &s[..2])); } Ok(info("National Insurance number", format!("{} {} {} {} {}", &s[..2], &s[2..4], &s[4..6], &s[6..8], &s[8..])))}fn br(s: &str) -> R { all_digits(s, 11)?; let x = d(s); if x.iter().all(|v| *v == x[0]) { return Err("a CPF of one repeated digit is not valid".into()); } let c1 = weighted(&x[..9], &[10, 9, 8, 7, 6, 5, 4, 3, 2]) * 10 % 11 % 10; let c2 = weighted(&x[..10], &[11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) * 10 % 11 % 10; if c1 != x[9] || c2 != x[10] { return Err(BAD_CHECK.into()); } let regions = ["RS", "DF, GO, MS, MT, TO", "AC, AM, AP, PA, RO, RR", "CE, MA, PI", "AL, PB, PE, RN", "BA, SE", "MG", "ES, RJ", "SP", "PR, SC"]; let mut i = info("CPF", format!("{}.{}.{}-{}", &s[..3], &s[3..6], &s[6..9], &s[9..])); i.note = Some(format!("issued in {}", regions[x[8] as usize])); Ok(i)}fn india(s: &str) -> R { all_digits(s, 12)?; if s.starts_with('0') || s.starts_with('1') { return Err("an Aadhaar number does not start with 0 or 1".into()); } if !verhoeff(s) { return Err(BAD_CHECK.into()); } Ok(info("Aadhaar", format!("{} {} {}", &s[..4], &s[4..8], &s[8..])))}fn ph(s: &str) -> R { if !(s.len() == 9 || s.len() == 12) || !s.chars().all(|c| c.is_ascii_digit()) { return Err("a TIN has 9 digits, or 12 with the branch code".into()); } let branch = if s.len() == 12 { &s[9..] } else { "000" }; let mut i = info("Tax Identification Number", format!("{}-{}-{}-{}", &s[..3], &s[3..6], &s[6..9], branch)); i.note = Some("the BIR publishes no check digit: the format is checked".into()); Ok(i)}fn es(s: &str) -> R { const L: &[u8] = b"TRWAGMYFPDXBNJZSQVHLCKE"; let c: Vec<char> = s.chars().collect(); if c.len() != 9 { return Err("a DNI is 8 digits and a letter; an NIE X, Y or Z, 7 digits and a letter".into()); } let kind = if "XYZ".contains(c[0]) { "NIE" } else { "DNI" }; let numtext = if kind == "NIE" { format!("{}{}", c[0] as u8 - b'X', &s[1..8]) } else { s[..8].to_string() }; if !numtext.chars().all(|x| x.is_ascii_digit()) { return Err(format!("a {} has digits before its letter", kind)); } let n: u32 = numtext.parse().unwrap_or(0); if L[(n % 23) as usize] as char != c[8] { return Err("the letter does not match the number: a typo".into()); } Ok(info(if kind == "NIE" { "NIE" } else { "DNI" }, s.to_string()))}fn it(s: &str) -> R { if s.len() != 16 || !s.chars().all(|c| c.is_ascii_alphanumeric()) { return Err("a codice fiscale has 16 letters and digits".into()); } const ODD: [u32; 36] = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23]; let val = |c: char| -> usize { if c.is_ascii_digit() { c as usize - '0' as usize } else { c as usize - 'A' as usize + 10 } }; let sum: u32 = s.chars().take(15).enumerate().map(|(i, c)| if i % 2 == 0 { ODD[val(c)] } else if c.is_ascii_digit() { c as u32 - '0' as u32 } else { c as u32 - 'A' as u32 }).sum(); if (b'A' + (sum % 26) as u8) as char != s.chars().nth(15).unwrap() { return Err(BAD_CHECK.into()); } // Omocodia: letters standing for digits in a code given twice. let digit = |c: char| -> Option<u32> { c.to_digit(10).or_else(|| "LMNPQRSTUV".find(c).map(|i| i as u32)) }; let yy = digit(s.chars().nth(6).unwrap()).zip(digit(s.chars().nth(7).unwrap())).map(|(a, b)| a * 10 + b).ok_or("the year is not digits")?; let month = "ABCDEHLMPRST".find(s.chars().nth(8).unwrap()).map(|m| m as u32 + 1).ok_or("the month letter is not one of ABCDEHLMPRST")?; let dd = digit(s.chars().nth(9).unwrap()).zip(digit(s.chars().nth(10).unwrap())).map(|(a, b)| a * 10 + b).ok_or("the day is not digits")?; let (day, sex) = if dd > 40 { (dd - 40, "F") } else { (dd, "M") }; let mut i = info("Codice fiscale", s.to_string()); // The century is not in the code: take the one that makes the person at most 100. let y = if yy as i32 + 2000 <= 2026 { 2000 + yy as i32 } else { 1900 + yy as i32 }; i.birth = birth(y, month, day)?; i.sex = Some(sex); i.note = Some(format!("born in the place coded {}", &s[11..15])); Ok(i)}fn fr(s: &str) -> R { if s.len() != 15 { return Err("a NIR has 13 characters and a 2-digit key".into()); } let body = s[..13].replace("2A", "19").replace("2B", "18"); if !body.chars().all(|c| c.is_ascii_digit()) || !s[13..].chars().all(|c| c.is_ascii_digit()) { return Err("a NIR is digits (2A and 2B for Corsica)".into()); } let n: u64 = body.parse().unwrap_or(0); if 97 - (n % 97) as u32 != num(s, 13, 15) { return Err("the key does not match: a typo".into()); } let mut i = info("NIR (numéro de sécurité sociale)", format!("{} {} {} {} {} {} {}", &s[..1], &s[1..3], &s[3..5], &s[5..7], &s[7..10], &s[10..13], &s[13..])); i.sex = match &s[..1] { "1" | "3" | "7" => Some("M"), "2" | "4" | "8" => Some("F"), _ => None }; let m = num(s, 3, 5); if (1..=12).contains(&m) { i.note = Some(format!("born in month {} of a year ending {}", m, &s[1..3])); } Ok(i)}fn nl(s: &str) -> R { all_digits(s, 9)?; let x = d(s); let sum = weighted(&x[..8], &[9, 8, 7, 6, 5, 4, 3, 2]) as i32 - x[8] as i32; if sum % 11 != 0 || sum == 0 { return Err("the number fails the eleven test: a typo".into()); } Ok(info("BSN (burgerservicenummer)", s.to_string()))}fn be(s: &str) -> R { all_digits(s, 11)?; let first: u64 = s[..9].parse().unwrap_or(0); let check = num(s, 9, 11) as u64; let after2000 = 97 - (2_000_000_000 + first) % 97 == check; if 97 - first % 97 != check && !after2000 { return Err(BAD_CHECK.into()); } let mut i = info("Rijksregisternummer / numéro de registre national", format!("{}.{}.{}-{}.{}", &s[..2], &s[2..4], &s[4..6], &s[6..9], &s[9..])); let (y, m, dd) = (num(s, 0, 2) as i32 + if after2000 { 2000 } else { 1900 }, num(s, 2, 4), num(s, 4, 6)); if m > 0 && dd > 0 { i.birth = birth(y, m, dd).ok().flatten(); } i.sex = Some(if num(s, 6, 9) % 2 == 1 { "M" } else { "F" }); Ok(i)}fn se(s: &str) -> R { let ten = match s.len() { 10 => s.to_string(), 12 => s[2..].to_string(), _ => return Err("a personnummer has 10 digits (YYMMDDNNNC) or 12".into()), }; all_digits(&ten, 10)?; if !luhn(&ten) { return Err(BAD_CHECK.into()); } let (m, mut dd) = (num(&ten, 2, 4), num(&ten, 4, 6)); let coordination = dd > 60; if coordination { dd -= 60; } let y = if s.len() == 12 { num(s, 0, 4) as i32 } else { let yy = num(&ten, 0, 2) as i32; if 2000 + yy <= 2026 { 2000 + yy } else { 1900 + yy } }; let mut i = info(if coordination { "Samordningsnummer" } else { "Personnummer" }, format!("{}-{}", &ten[..6], &ten[6..])); i.birth = birth(y, m, dd)?; i.sex = Some(if num(&ten, 8, 9) % 2 == 1 { "M" } else { "F" }); Ok(i)}fn no(s: &str) -> R { all_digits(s, 11)?; let x = d(s); let k = |w: &[u32], n: usize| { let r = 11 - weighted(&x[..n], w) % 11; if r == 11 { Some(0) } else if r == 10 { None } else { Some(r) } }; if k(&[3, 7, 6, 1, 8, 9, 4, 5, 2], 9) != Some(x[9]) || k(&[5, 4, 3, 2, 7, 6, 5, 4, 3, 2], 10) != Some(x[10]) { return Err(BAD_CHECK.into()); } let (mut dd, m, yy, ind) = (num(s, 0, 2), num(s, 2, 4), num(s, 4, 6) as i32, num(s, 6, 9)); let dnum = dd > 40; if dnum { dd -= 40; } let y = match ind { 0..=499 => 1900 + yy, 500..=749 if yy >= 54 => 1800 + yy, 900..=999 if yy >= 40 => 1900 + yy, 500..=999 if yy < 40 => 2000 + yy, _ => 1900 + yy, }; let mut i = info(if dnum { "D-nummer" } else { "Fødselsnummer" }, s.to_string()); i.birth = birth(y, if m > 40 { m - 40 } else { m }, dd)?; i.sex = Some(if x[8] % 2 == 1 { "M" } else { "F" }); Ok(i)}fn dk(s: &str) -> R { all_digits(s, 10)?; let (dd, m, yy, c) = (num(s, 0, 2), num(s, 2, 4), num(s, 4, 6) as i32, num(s, 6, 7)); let y = match c { 0..=3 => 1900 + yy, 4 | 9 => if yy <= 36 { 2000 + yy } else { 1900 + yy }, _ => if yy <= 57 { 2000 + yy } else { 1800 + yy }, }; let mut i = info("CPR-nummer", format!("{}-{}", &s[..6], &s[6..])); i.birth = birth(y, m, dd)?; i.sex = Some(if num(s, 9, 10) % 2 == 1 { "M" } else { "F" }); i.note = Some("CPR numbers have had no check digit since 2007: the date is checked".into()); Ok(i)}fn fi(s: &str) -> R { let c: Vec<char> = s.chars().collect(); if c.len() != 11 { return Err("a henkilötunnus is DDMMYY, a century sign, three digits and a check character".into()); } let century = match c[6] { '+' => 1800, '-' | 'Y' | 'X' | 'W' | 'V' | 'U' => 1900, 'A' | 'B' | 'C' | 'D' | 'E' | 'F' => 2000, x => return Err(format!("`{}` is not a century sign", x)), }; let n = format!("{}{}", &s[..6], &s[7..10]); all_digits(&n, 9)?; let check = "0123456789ABCDEFHJKLMNPRSTUVWXY".chars().nth((n.parse::<u64>().unwrap_or(0) % 31) as usize).unwrap(); if check != c[10] { return Err(BAD_CHECK.into()); } let mut i = info("Henkilötunnus", s.to_string()); i.birth = birth(century + num(s, 4, 6) as i32, num(s, 2, 4), num(s, 0, 2))?; i.sex = Some(if num(s, 7, 10) % 2 == 1 { "M" } else { "F" }); Ok(i)}fn pl(s: &str) -> R { all_digits(s, 11)?; let x = d(s); if (10 - weighted(&x[..10], &[1, 3, 7, 9, 1, 3, 7, 9, 1, 3]) % 10) % 10 != x[10] { return Err(BAD_CHECK.into()); } let (yy, mm, dd) = (num(s, 0, 2) as i32, num(s, 2, 4), num(s, 4, 6)); let (century, m) = match mm { 81..=92 => (1800, mm - 80), 1..=12 => (1900, mm), 21..=32 => (2000, mm - 20), 41..=52 => (2100, mm - 40), 61..=72 => (2200, mm - 60), _ => return Err("the month is not a PESEL month".into()) }; let mut i = info("PESEL", s.to_string()); i.birth = birth(century + yy, m, dd)?; i.sex = Some(if x[9] % 2 == 1 { "M" } else { "F" }); Ok(i)}fn cn(s: &str) -> R { if s.len() != 18 || !s[..17].chars().all(|c| c.is_ascii_digit()) { return Err("a resident ID has 17 digits and a check digit or X".into()); } let sum = weighted(&d(&s[..17]), &[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]); if "10X98765432".chars().nth((sum % 11) as usize) != s.chars().nth(17) { return Err(BAD_CHECK.into()); } let mut i = info("Resident identity card number", s.to_string()); i.birth = birth(num(s, 6, 10) as i32, num(s, 10, 12), num(s, 12, 14))?; i.sex = Some(if num(s, 16, 17) % 2 == 1 { "M" } else { "F" }); i.note = Some(format!("registered in area {}", &s[..6])); Ok(i)}fn za(s: &str) -> R { all_digits(s, 13)?; if !luhn(s) { return Err(BAD_CHECK.into()); } let yy = num(s, 0, 2) as i32; let mut i = info("South African ID number", s.to_string()); i.birth = birth(if 2000 + yy <= 2026 { 2000 + yy } else { 1900 + yy }, num(s, 2, 4), num(s, 4, 6))?; i.sex = Some(if num(s, 6, 10) >= 5000 { "M" } else { "F" }); i.note = Some(match &s[10..11] { "0" => "a South African citizen", "1" => "a permanent resident", _ => "a refugee" }.into()); Ok(i)}fn cl(s: &str) -> R { let (body, check) = s.split_at(s.len().saturating_sub(1)); if body.is_empty() || body.len() > 9 || !body.chars().all(|c| c.is_ascii_digit()) { return Err("a RUN is up to 9 digits and a check digit or K".into()); } let mut sum = 0; let mut m = 2; for c in body.chars().rev() { sum += c.to_digit(10).unwrap() * m; m = if m == 7 { 2 } else { m + 1 }; } let expect = match 11 - sum % 11 { 11 => "0".to_string(), 10 => "K".to_string(), r => r.to_string() }; if expect != check { return Err(BAD_CHECK.into()); } let n: u64 = body.parse().unwrap_or(0); let grouped = n.to_string().as_bytes().rchunks(3).rev().map(|c| std::str::from_utf8(c).unwrap_or("")).collect::<Vec<_>>().join("."); Ok(info("RUN (rol único nacional)", format!("{}-{}", grouped, check)))}fn ar(s: &str) -> R { if !(7..=8).contains(&s.len()) || !s.chars().all(|c| c.is_ascii_digit()) { return Err("a DNI has 7 or 8 digits".into()); } let mut i = info("DNI", s.to_string()); i.note = Some("a DNI has no check digit: the format is checked (see the CUIL/CUIT in tax_ids)".into()); Ok(i)}fn mx(s: &str) -> R { const AL: &str = "0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"; let c: Vec<char> = s.chars().collect(); if c.len() != 18 || !c[..4].iter().all(|x| x.is_ascii_alphabetic()) || !c[4..10].iter().all(|x| x.is_ascii_digit()) || !"HMX".contains(c[10]) { return Err("a CURP is 4 letters, the birth date (YYMMDD), H or M, the state and 5 more characters".into()); } let sum: usize = c[..17].iter().enumerate().map(|(i, ch)| AL.chars().position(|a| a == *ch).unwrap_or(0) * (18 - i)).sum(); if ((10 - sum % 10) % 10) as u32 != c[17].to_digit(10).unwrap_or(99) { return Err(BAD_CHECK.into()); } let century = if c[16].is_ascii_digit() { 1900 } else { 2000 }; let mut i = info("CURP", s.to_string()); i.birth = birth(century + num(s, 4, 6) as i32, num(s, 6, 8), num(s, 8, 10))?; i.sex = Some(match c[10] { 'H' => "M", 'M' => "F", _ => "X" }); i.note = Some(format!("registered in state {}", &s[11..13])); Ok(i)}fn ca(s: &str) -> R { all_digits(s, 9)?; if s.starts_with('0') || s.starts_with('8') { return Err("no SIN starts with 0 or 8".into()); } if !luhn(s) { return Err(BAD_CHECK.into()); } let mut i = info("Social Insurance Number", format!("{} {} {}", &s[..3], &s[3..6], &s[6..])); if s.starts_with('9') { i.note = Some("a 9 SIN is a temporary resident's, and expires".into()); } Ok(i)}fn kr(s: &str) -> R { all_digits(s, 13)?; let x = d(s); let (century, sex, foreign) = match x[6] { 1 => (1900, "M", false), 2 => (1900, "F", false), 3 => (2000, "M", false), 4 => (2000, "F", false), 5 => (1900, "M", true), 6 => (1900, "F", true), 7 => (2000, "M", true), 8 => (2000, "F", true), 9 => (1800, "M", false), _ => (1800, "F", false) }; let mut i = info(if foreign { "Foreign registration number" } else { "Resident registration number" }, format!("{}-{}", &s[..6], &s[6..])); i.birth = birth(century + num(s, 0, 2) as i32, num(s, 2, 4), num(s, 4, 6))?; i.sex = Some(sex); let check = (11 - weighted(&x[..12], &[2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5]) % 11) % 10; if check != x[12] { i.note = Some("the old check digit does not match: fine for numbers issued since October 2020, a typo before".into()); } Ok(i)}fn sg(s: &str) -> R { let c: Vec<char> = s.chars().collect(); if c.len() != 9 || !"STFGM".contains(c[0]) || !c[1..8].iter().all(|x| x.is_ascii_digit()) || !c[8].is_ascii_alphabetic() { return Err("an NRIC or FIN is S, T, F, G or M, 7 digits and a letter".into()); } let kind = if "ST".contains(c[0]) { "NRIC" } else { "FIN" }; if c[0] == 'M' { let mut i = info("FIN", s.to_string()); i.note = Some("M-series FINs are checked by their format only".into()); return Ok(i); } let sum = weighted(&d(&s[1..8]), &[2, 7, 6, 5, 4, 3, 2]) + if "TG".contains(c[0]) { 4 } else { 0 }; let table = if "ST".contains(c[0]) { "JZIHGFEDCBA" } else { "XWUTRQPNMLK" }; if table.chars().nth((sum % 11) as usize) != Some(c[8]) { return Err(BAD_CHECK.into()); } Ok(info(kind, s.to_string()))}fn tr(s: &str) -> R { all_digits(s, 11)?; let x = d(s); if x[0] == 0 { return Err("a TC kimlik number does not start with 0".into()); } let odd = x[0] + x[2] + x[4] + x[6] + x[8]; let even = x[1] + x[3] + x[5] + x[7]; if ((odd * 7) as i32 - even as i32).rem_euclid(10) as u32 != x[9] || x[..10].iter().sum::<u32>() % 10 != x[10] { return Err(BAD_CHECK.into()); } Ok(info("TC kimlik numarası", s.to_string()))}fn ro(s: &str) -> R { all_digits(s, 13)?; let x = d(s); let r = weighted(&x[..12], &[2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9]) % 11; if (if r == 10 { 1 } else { r }) != x[12] { return Err(BAD_CHECK.into()); } let (century, sex) = match x[0] { 1 => (1900, "M"), 2 => (1900, "F"), 3 => (1800, "M"), 4 => (1800, "F"), 5 => (2000, "M"), 6 => (2000, "F"), 7 => (1900, "M"), 8 => (1900, "F"), _ => (1900, "X") }; let mut i = info("CNP (cod numeric personal)", s.to_string()); i.birth = birth(century + num(s, 1, 3) as i32, num(s, 3, 5), num(s, 5, 7))?; i.sex = if sex == "X" { None } else { Some(sex) }; Ok(i)}fn id(s: &str) -> R { all_digits(s, 16)?; let (mut dd, m, yy) = (num(s, 6, 8), num(s, 8, 10), num(s, 10, 12) as i32); let female = dd > 40; if female { dd -= 40; } let mut i = info("NIK (nomor induk kependudukan)", s.to_string()); i.birth = birth(if 2000 + yy <= 2026 { 2000 + yy } else { 1900 + yy }, m, dd)?; i.sex = Some(if female { "F" } else { "M" }); i.note = Some(format!("registered in area {}", &s[..6])); Ok(i)}fn jp(s: &str) -> R { all_digits(s, 12)?; let x = d(s); let sum: u32 = x[..11].iter().rev().enumerate().map(|(i, v)| { let n = i as u32 + 1; v * if n <= 6 { n + 1 } else { n - 5 } }).sum(); let r = sum % 11; if (if r <= 1 { 0 } else { 11 - r }) != x[11] { return Err(BAD_CHECK.into()); } Ok(info("My Number (個人番号)", format!("{} {} {}", &s[..4], &s[4..8], &s[8..])))}fn ie(s: &str) -> R { const L: &str = "WABCDEFGHIJKLMNOPQRSTUV"; if !(8..=9).contains(&s.len()) || !s[..7].chars().all(|c| c.is_ascii_digit()) || !s[7..].chars().all(|c| c.is_ascii_alphabetic()) { return Err("a PPS number is 7 digits and one or two letters".into()); } let mut sum = weighted(&d(&s[..7]), &[8, 7, 6, 5, 4, 3, 2]); if s.len() == 9 { sum += 9 * L.find(s.chars().nth(8).unwrap()).unwrap_or(0) as u32; } if L.chars().nth((sum % 23) as usize) != s.chars().nth(7) { return Err(BAD_CHECK.into()); } Ok(info("PPS number", s.to_string()))}fn il(s: &str) -> R { if s.is_empty() || s.len() > 9 || !s.chars().all(|c| c.is_ascii_digit()) { return Err("a Teudat Zehut number has up to 9 digits".into()); } let p = format!("{:0>9}", s); let sum: u32 = p.chars().enumerate().map(|(i, c)| { let v = c.to_digit(10).unwrap() * if i % 2 == 0 { 1 } else { 2 }; if v > 9 { v - 9 } else { v } }).sum(); if sum % 10 != 0 { return Err(BAD_CHECK.into()); } Ok(info("Teudat Zehut", p))}const COUNTRIES: &[(&str, &str, fn(&str) -> R)] = &[ ("US", "United States: Social Security number", us), ("GB", "United Kingdom: National Insurance number", gb), ("BR", "Brazil: CPF", br), ("IN", "India: Aadhaar", india), ("PH", "Philippines: TIN", ph), ("ES", "Spain: DNI and NIE", es), ("IT", "Italy: codice fiscale", it), ("FR", "France: NIR (social security number)", fr), ("NL", "Netherlands: BSN", nl), ("BE", "Belgium: national register number", be), ("SE", "Sweden: personnummer", se), ("NO", "Norway: fødselsnummer and D-nummer", no), ("DK", "Denmark: CPR number", dk), ("FI", "Finland: henkilötunnus", fi), ("PL", "Poland: PESEL", pl), ("CN", "China: resident identity card", cn), ("ZA", "South Africa: ID number", za), ("CL", "Chile: RUN", cl), ("AR", "Argentina: DNI", ar), ("MX", "Mexico: CURP", mx), ("CA", "Canada: Social Insurance Number", ca), ("KR", "South Korea: resident registration number", kr), ("SG", "Singapore: NRIC and FIN", sg), ("TR", "Turkey: TC kimlik numarası", tr), ("RO", "Romania: CNP", ro), ("ID", "Indonesia: NIK", id), ("JP", "Japan: My Number", jp), ("IE", "Ireland: PPS number", ie), ("IL", "Israel: Teudat Zehut", il),];/// Checks a national ID number of `country` (ISO 3166 code: "US", "GB", "BR", "IN", "PH", "ES", "IT", "CN"...; see/// `national_id_countries`): `{ valid, country, kind, formatted, birth_date, sex, note, error }`. Spaces, dots, dashes and/// slashes are ignored. `birth_date` (ISO) and `sex` ("M" or "F") are filled where the number holds them; `note` says what else/// it tells, or what could not be checked.pub fn national_id_check(country: &str, number: &str) -> Value { let c = country.trim().to_uppercase(); // Finland's century sign is a dash or a plus: keep it there. let s = if c == "FI" { number.split_whitespace().collect::<String>().to_uppercase() } else { digits_only(number) }; let Some((_, _, f)) = COUNTRIES.iter().find(|(code, _, _)| *code == c) else { return json!({ "valid": false, "country": c, "error": format!("no rules for `{}`: {}", c, COUNTRIES.iter().map(|x| x.0).collect::<Vec<_>>().join(", ")) }); }; match f(&s) { Ok(i) => { let mut m = Map::new(); m.insert("valid".into(), json!(true)); m.insert("country".into(), json!(c)); m.insert("kind".into(), json!(i.kind)); m.insert("formatted".into(), json!(i.formatted)); m.insert("birth_date".into(), json!(i.birth.map(|(y, mo, d)| format!("{:04}-{:02}-{:02}", y, mo, d)))); m.insert("sex".into(), json!(i.sex)); m.insert("note".into(), json!(i.note)); m.insert("error".into(), Value::Null); Value::Object(m) } Err(e) => json!({ "valid": false, "country": c, "error": e }), }}/// Whether `number` is a valid national ID of `country`.pub fn national_id_valid(country: &str, number: &str) -> bool { national_id_check(country, number)["valid"] == json!(true)}/// The countries with rules: `[{ country, name }]`.pub fn national_id_countries() -> Vec<Value> { COUNTRIES.iter().map(|(c, n, _)| json!({ "country": c, "name": n })).collect()}/// `number` with all but its last `visible` characters hidden (`*****6789`), for showing an ID back to its owner.pub fn national_id_mask(number: &str, visible: i64) -> String { let s = digits_only(number); let n = s.chars().count(); let keep = (visible.max(0) as usize).min(n); format!("{}{}", "*".repeat(n - keep), s.chars().skip(n - keep).collect::<String>())}Related
cards.qwer · 1.0.0Payment card numbers checked before they reach a payment provider: Luhn check digit, brand and length, expiry, and the number formatted or masked for receipts.7 views · 8 downloadsPassport MRZ passport_mrz.qwer · 1.0.0Machine-readable zones of passports, ID cards and visas (ICAO 9303 TD1, TD2, TD3, MRV) read into names, numbers and dates with every check digit verified, MRZ lines found in OCR text, and MRZs made for test documents.6 views · 9 downloadsPostal codes postal_codes.qwer · 1.0.0Postal codes checked for 45 countries and written the post office's way: SW1A 1AA, 1012 JS, K1A 0B1, 100-0001, 20500-0001.6 views · 8 downloadsVAT numbers vat.qwer · 1.0.0EU VAT numbers checked offline by format and check digits for all 27 countries, Northern Ireland, the UK, Switzerland and Norway, to catch typos before an invoice goes out.8 views · 9 downloadsVINs vin.qwer · 1.0.0Vehicle identification numbers checked and decoded: the check digit, the maker's region, country and name, the model year, plant and serial.9 views · 9 downloadsJSON Schema json_schema.qwer · 1.0.0Check values against a JSON Schema (drafts 4 to 2020-12), with every error and the path of the value at fault.8 views · 9 downloadsThread 0 posts
On National ID numbers: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.