use nhs_number::NHSNumber;
use std::str::FromStr;
pub fn parse_uk_nhs_number(s: &str) -> Option<String> {
let parsed = NHSNumber::from_str(s).ok()?;
let mut canonical = String::with_capacity(10);
for &d in &parsed.digits {
canonical.push(char::from_digit(d as u32, 10)?);
}
Some(canonical)
}
pub fn parse_fr_nir(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace())
.collect::<String>()
.to_uppercase();
if !cleaned.is_ascii() || cleaned.len() != 15 {
return None;
}
let dept = &cleaned[5..7];
let numeric_body = match dept {
"2A" => format!("{}19{}", &cleaned[0..5], &cleaned[7..13]),
"2B" => format!("{}18{}", &cleaned[0..5], &cleaned[7..13]),
_ => cleaned[0..13].to_string(),
};
if !numeric_body.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let key_str = &cleaned[13..15];
if !key_str.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let n: u64 = numeric_body.parse().ok()?;
let key: u64 = key_str.parse().ok()?;
if 97 - (n % 97) == key {
Some(cleaned)
} else {
None
}
}
pub fn parse_es_tsi(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace() && *c != '-')
.collect::<String>()
.to_uppercase();
if !cleaned.is_ascii() {
return None;
}
if !cleaned.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
if !(10..=20).contains(&cleaned.len()) {
return None;
}
Some(cleaned)
}
pub fn parse_ie_ihi(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 7 {
Some(digits)
} else {
None
}
}
pub fn parse_uk_hc_number(s: &str) -> Option<String> {
parse_uk_nhs_number(s)
}
pub fn parse_us_ssn(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 9 {
return None;
}
let area: u32 = digits[0..3].parse().ok()?;
let group: u32 = digits[3..5].parse().ok()?;
let serial: u32 = digits[5..9].parse().ok()?;
if area == 0 || area == 666 || area >= 900 {
return None;
}
if group == 0 {
return None;
}
if serial == 0 {
return None;
}
Some(digits)
}
pub fn parse_de_kvnr(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace())
.collect::<String>()
.to_uppercase();
if !cleaned.is_ascii() || cleaned.len() != 10 {
return None;
}
let mut chars = cleaned.chars();
let first = chars.next()?;
if !first.is_ascii_alphabetic() {
return None;
}
let digit_chars: Vec<char> = chars.collect();
if !digit_chars.iter().all(|c| c.is_ascii_digit()) {
return None;
}
let letter_ord = (first as u32) - ('A' as u32) + 1;
let mut combined: Vec<u32> = vec![letter_ord / 10, letter_ord % 10];
for c in &digit_chars[..8] {
combined.push(c.to_digit(10)?);
}
let mut total: u32 = 0;
for (i, d) in combined.iter().enumerate() {
let weight = if i % 2 == 0 { 1 } else { 2 };
let product = d * weight;
total += if product >= 10 { product - 9 } else { product };
}
let expected = digit_chars[8].to_digit(10)?;
if total % 10 == expected {
Some(cleaned)
} else {
None
}
}
fn cf_odd_value(c: char) -> Option<u32> {
Some(match c {
'0' | 'A' => 1,
'1' | 'B' => 0,
'2' | 'C' => 5,
'3' | 'D' => 7,
'4' | 'E' => 9,
'5' | 'F' => 13,
'6' | 'G' => 15,
'7' | 'H' => 17,
'8' | 'I' => 19,
'9' | 'J' => 21,
'K' => 2,
'L' => 4,
'M' => 18,
'N' => 20,
'O' => 11,
'P' => 3,
'Q' => 6,
'R' => 8,
'S' => 12,
'T' => 14,
'U' => 16,
'V' => 10,
'W' => 22,
'X' => 25,
'Y' => 24,
'Z' => 23,
_ => return None,
})
}
fn cf_even_value(c: char) -> Option<u32> {
Some(match c {
'0' | 'A' => 0,
'1' | 'B' => 1,
'2' | 'C' => 2,
'3' | 'D' => 3,
'4' | 'E' => 4,
'5' | 'F' => 5,
'6' | 'G' => 6,
'7' | 'H' => 7,
'8' | 'I' => 8,
'9' | 'J' => 9,
'K' => 10,
'L' => 11,
'M' => 12,
'N' => 13,
'O' => 14,
'P' => 15,
'Q' => 16,
'R' => 17,
'S' => 18,
'T' => 19,
'U' => 20,
'V' => 21,
'W' => 22,
'X' => 23,
'Y' => 24,
'Z' => 25,
_ => return None,
})
}
pub fn parse_it_cf(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace())
.collect::<String>()
.to_uppercase();
if !cleaned.is_ascii() || cleaned.len() != 16 {
return None;
}
if !cleaned.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
let mut total: u32 = 0;
for (i, c) in chars.iter().take(15).enumerate() {
let value = if i % 2 == 0 {
cf_odd_value(*c)?
} else {
cf_even_value(*c)?
};
total += value;
}
let expected_check = (b'A' + (total % 26) as u8) as char;
if chars[15] == expected_check {
Some(cleaned)
} else {
None
}
}
pub fn parse_nl_bsn(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 9 {
return None;
}
if digits.chars().all(|c| c == '0') {
return None;
}
let weights: [i32; 9] = [9, 8, 7, 6, 5, 4, 3, 2, -1];
let mut sum: i32 = 0;
for (i, c) in digits.chars().enumerate() {
sum += (c.to_digit(10)? as i32) * weights[i];
}
if sum % 11 == 0 { Some(digits) } else { None }
}
pub fn parse_se_workernummer(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
let luhn_digits: &str = match digits.len() {
10 => &digits,
12 => &digits[2..],
_ => return None,
};
let mut sum: u32 = 0;
for (i, c) in luhn_digits.chars().enumerate() {
let d = c.to_digit(10)?;
let weight = if i % 2 == 0 { 2 } else { 1 };
let product = d * weight;
sum += if product >= 10 { product - 9 } else { product };
}
if sum.is_multiple_of(10) {
Some(digits)
} else {
None
}
}
pub fn parse_au_ihi(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 16 {
return None;
}
let mut sum: u32 = 0;
for (i, c) in digits.chars().enumerate() {
let d = c.to_digit(10)?;
let weight = if i % 2 == 0 { 2 } else { 1 };
let product = d * weight;
sum += if product >= 10 { product - 9 } else { product };
}
if sum.is_multiple_of(10) {
Some(digits)
} else {
None
}
}
pub fn parse_uk_chi_number(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 10 {
return None;
}
let chars: Vec<u32> = digits
.chars()
.map(|c| c.to_digit(10).expect("filtered to digits"))
.collect();
let weights = [10u32, 9, 8, 7, 6, 5, 4, 3, 2];
let sum: u32 = chars
.iter()
.take(9)
.zip(weights.iter())
.map(|(d, w)| d * w)
.sum();
let check = (11 - (sum % 11)) % 11;
if check == 10 {
return None;
}
if check == chars[9] {
Some(digits)
} else {
None
}
}
pub fn parse_be_nn(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
let body: u64 = digits[..9].parse().ok()?;
let check: u64 = digits[9..11].parse().ok()?;
let pre2000 = 97 - body % 97;
let post2000_body: u64 = format!("2{}", &digits[..9]).parse().ok()?;
let post2000 = 97 - post2000_body % 97;
if check == pre2000 || check == post2000 {
Some(digits)
} else {
None
}
}
pub fn parse_bg_egn(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 10 {
return None;
}
let weights: [u32; 9] = [2, 4, 8, 5, 10, 9, 7, 3, 6];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(9).enumerate() {
sum += c.to_digit(10)? * weights[i];
}
let expected = if sum % 11 == 10 { 0 } else { sum % 11 };
if digits.chars().nth(9)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_cz_rc(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
match digits.len() {
9 => Some(digits),
10 => {
let n: u64 = digits.parse().ok()?;
let head: u64 = digits[..9].parse().ok()?;
let tail = digits.chars().last()?.to_digit(10)?;
if n.is_multiple_of(11) || (head % 11 == 10 && tail == 0) {
Some(digits)
} else {
None
}
}
_ => None,
}
}
pub fn parse_dk_cpr(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 10 {
Some(digits)
} else {
None
}
}
fn baltic_cascade_check(digits: &str) -> Option<u32> {
const PASS1: [u32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1];
const PASS2: [u32; 10] = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3];
let body: Vec<u32> = digits
.chars()
.take(10)
.filter_map(|c| c.to_digit(10))
.collect();
if body.len() != 10 {
return None;
}
let s1: u32 = body.iter().zip(PASS1.iter()).map(|(d, w)| d * w).sum();
let r1 = s1 % 11;
if r1 < 10 {
return Some(r1);
}
let s2: u32 = body.iter().zip(PASS2.iter()).map(|(d, w)| d * w).sum();
let r2 = s2 % 11;
if r2 < 10 { Some(r2) } else { Some(0) }
}
pub fn parse_ee_ik(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
let expected = baltic_cascade_check(&digits[..10])?;
if digits.chars().nth(10)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_es_dni(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.is_empty() {
return None;
}
let last = cleaned.chars().last()?;
if !last.is_ascii_alphabetic() {
return None;
}
let body = &cleaned[..cleaned.len() - 1];
let n: u64 = match body.chars().next()? {
'X' => format!("0{}", &body[1..]).parse().ok()?,
'Y' => format!("1{}", &body[1..]).parse().ok()?,
'Z' => format!("2{}", &body[1..]).parse().ok()?,
d if d.is_ascii_digit() => body.parse().ok()?,
_ => return None,
};
const LETTERS: &[u8; 23] = b"TRWAGMYFPDXBNJZSQVHLCKE";
let expected = LETTERS[(n % 23) as usize] as char;
if last == expected {
Some(cleaned)
} else {
None
}
}
pub fn parse_fi_hetu(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace())
.collect::<String>()
.to_uppercase();
if !cleaned.is_ascii() || cleaned.len() != 11 {
return None;
}
let date: &str = &cleaned[..6];
let sign = cleaned.chars().nth(6)?;
let serial: &str = &cleaned[7..10];
let check = cleaned.chars().nth(10)?;
if !date.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if !serial.chars().all(|c| c.is_ascii_digit()) {
return None;
}
if !matches!(
sign,
'-' | '+' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'X' | 'Y'
) {
return None;
}
let n: u64 = format!("{}{}", date, serial).parse().ok()?;
const TABLE: &[u8; 31] = b"0123456789ABCDEFHJKLMNPRSTUVWXY";
let expected = TABLE[(n % 31) as usize] as char;
if check == expected {
Some(cleaned)
} else {
None
}
}
pub fn parse_hr_oib(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
let mut acc: u32 = 10;
for c in digits.chars().take(10) {
let d = c.to_digit(10)?;
let mut x = (d + acc) % 10;
if x == 0 {
x = 10;
}
acc = (x * 2) % 11;
}
let expected = (11 - acc) % 10;
if digits.chars().nth(10)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_is_kt(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 10 {
return None;
}
const WEIGHTS: [u32; 8] = [3, 2, 7, 6, 5, 4, 3, 2];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(8).enumerate() {
sum += c.to_digit(10)? * WEIGHTS[i];
}
let r = sum % 11;
if r == 10 {
return None;
}
let expected = (11 - r) % 11;
if digits.chars().nth(8)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_lt_ak(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
let expected = baltic_cascade_check(&digits[..10])?;
if digits.chars().nth(10)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_lv_pk(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
const WEIGHTS: [i32; 10] = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
let mut sum: i32 = 0;
for (i, c) in digits.chars().take(10).enumerate() {
sum += (c.to_digit(10)? as i32) * WEIGHTS[i];
}
let expected = ((1101 - sum).rem_euclid(11)) % 10;
if digits.chars().nth(10)?.to_digit(10)? as i32 == expected {
Some(digits)
} else {
None
}
}
pub fn parse_mt_id(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 8 {
return None;
}
let last = cleaned.chars().last()?;
if !matches!(last, 'M' | 'G' | 'A' | 'P' | 'L' | 'H' | 'B' | 'Z') {
return None;
}
if !cleaned[..7].chars().all(|c| c.is_ascii_digit()) {
return None;
}
Some(cleaned)
}
pub fn parse_no_fnr(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
const W1: [u32; 9] = [3, 7, 6, 1, 8, 9, 4, 5, 2];
const W2: [u32; 10] = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
let body: Vec<u32> = digits.chars().filter_map(|c| c.to_digit(10)).collect();
if body.len() != 11 {
return None;
}
let s1: u32 = body.iter().take(9).zip(W1.iter()).map(|(d, w)| d * w).sum();
let r1 = s1 % 11;
if r1 == 10 {
return None;
}
let c1 = (11 - r1) % 11;
if c1 != body[9] {
return None;
}
let s2: u32 = body
.iter()
.take(10)
.zip(W2.iter())
.map(|(d, w)| d * w)
.sum();
let r2 = s2 % 11;
if r2 == 10 {
return None;
}
let c2 = (11 - r2) % 11;
if c2 != body[10] {
return None;
}
Some(digits)
}
pub fn parse_pl_pesel(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
const WEIGHTS: [u32; 10] = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(10).enumerate() {
sum += c.to_digit(10)? * WEIGHTS[i];
}
let expected = (10 - (sum % 10)) % 10;
if digits.chars().nth(10)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_ro_cnp(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 13 {
return None;
}
const WEIGHTS: [u32; 12] = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(12).enumerate() {
sum += c.to_digit(10)? * WEIGHTS[i];
}
let r = sum % 11;
let expected = if r == 10 { 1 } else { r };
if digits.chars().nth(12)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_si_emso(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 13 {
return None;
}
const WEIGHTS: [u32; 12] = [7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(12).enumerate() {
sum += c.to_digit(10)? * WEIGHTS[i];
}
let r = sum % 11;
let expected = if r == 0 { 0 } else { 11 - r };
if expected == 10 {
return None;
}
if digits.chars().nth(12)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_sk_rc(s: &str) -> Option<String> {
parse_cz_rc(s)
}
pub fn parse_uk_nino(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 9 {
return None;
}
let bytes = cleaned.as_bytes();
let p1 = bytes[0] as char;
let p2 = bytes[1] as char;
if !p1.is_ascii_alphabetic() || !p2.is_ascii_alphabetic() {
return None;
}
if matches!(p1, 'D' | 'F' | 'I' | 'Q' | 'U' | 'V') {
return None;
}
if matches!(p2, 'D' | 'F' | 'I' | 'O' | 'Q' | 'U' | 'V') {
return None;
}
let prefix = &cleaned[..2];
if matches!(
prefix,
"OO" | "CR" | "FY" | "MW" | "NC" | "PP" | "PZ" | "TN"
) {
return None;
}
if !cleaned[2..8].chars().all(|c| c.is_ascii_digit()) {
return None;
}
let suffix = bytes[8] as char;
if !matches!(suffix, 'A' | 'B' | 'C' | 'D') {
return None;
}
Some(cleaned)
}
pub fn parse_gr_dss(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 10 {
Some(digits)
} else {
None
}
}
pub fn parse_li_id(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if !(10..=11).contains(&cleaned.len()) {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
if !chars[0].is_ascii_alphabetic() || !chars[1].is_ascii_alphabetic() {
return None;
}
if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
return None;
}
Some(cleaned)
}
pub fn parse_nl_id(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 9 {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
for c in chars.iter().take(2) {
if !c.is_ascii_alphabetic() || *c == 'O' {
return None;
}
}
for c in chars.iter().take(8).skip(2) {
if !c.is_ascii_alphanumeric() || *c == 'O' {
return None;
}
}
if !chars[8].is_ascii_digit() {
return None;
}
Some(cleaned)
}
pub fn parse_pl_nip(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 10 {
return None;
}
const WEIGHTS: [u32; 9] = [6, 5, 7, 2, 3, 4, 5, 6, 7];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(9).enumerate() {
sum += c.to_digit(10)? * WEIGHTS[i];
}
let r = sum % 11;
if r == 10 {
return None;
}
if digits.chars().nth(9)?.to_digit(10)? == r {
Some(digits)
} else {
None
}
}
pub fn parse_pt_nif(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 9 {
return None;
}
const WEIGHTS: [u32; 8] = [9, 8, 7, 6, 5, 4, 3, 2];
let mut sum: u32 = 0;
for (i, c) in digits.chars().take(8).enumerate() {
sum += c.to_digit(10)? * WEIGHTS[i];
}
let r = sum % 11;
let expected = if r < 2 { 0 } else { 11 - r };
if digits.chars().nth(8)?.to_digit(10)? == expected {
Some(digits)
} else {
None
}
}
pub fn parse_br_cpf(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 11 {
return None;
}
let bytes = digits.as_bytes();
if bytes.iter().all(|&b| b == bytes[0]) {
return None;
}
let d = |i: usize| (bytes[i] - b'0') as u32;
let mut sum1: u32 = 0;
for i in 0..9 {
sum1 += d(i) * (10 - i as u32);
}
let r1 = sum1 % 11;
let exp1 = if r1 < 2 { 0 } else { 11 - r1 };
if d(9) != exp1 {
return None;
}
let mut sum2: u32 = 0;
for i in 0..10 {
sum2 += d(i) * (11 - i as u32);
}
let r2 = sum2 % 11;
let exp2 = if r2 < 2 { 0 } else { 11 - r2 };
if d(10) != exp2 {
return None;
}
Some(digits)
}
pub fn parse_cn_rrn(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.map(|c| c.to_ascii_uppercase())
.collect();
if cleaned.len() != 18 {
return None;
}
let bytes = cleaned.as_bytes();
for &b in &bytes[..17] {
if !b.is_ascii_digit() {
return None;
}
}
if !bytes[17].is_ascii_digit() && bytes[17] != b'X' {
return None;
}
let yyyy: i32 = cleaned[6..10].parse().ok()?;
let mm: u32 = cleaned[10..12].parse().ok()?;
let dd: u32 = cleaned[12..14].parse().ok()?;
chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
const WEIGHTS: [u32; 17] = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const CHECK: [u8; 11] = [
b'1', b'0', b'X', b'9', b'8', b'7', b'6', b'5', b'4', b'3', b'2',
];
let mut sum: u32 = 0;
for i in 0..17 {
sum += u32::from(bytes[i] - b'0') * WEIGHTS[i];
}
if bytes[17] != CHECK[(sum % 11) as usize] {
return None;
}
Some(cleaned)
}
pub fn parse_in_aadhaar(s: &str) -> Option<String> {
const VERHOEFF_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 VERHOEFF_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 digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 12 {
return None;
}
let bytes = digits.as_bytes();
if bytes.iter().all(|&b| b == bytes[0]) {
return None;
}
if bytes[0] == b'0' || bytes[0] == b'1' {
return None;
}
let mut c: u8 = 0;
for i in 0..12 {
let d = bytes[11 - i] - b'0';
c = VERHOEFF_D[c as usize][VERHOEFF_P[i % 8][d as usize] as usize];
}
if c == 0 { Some(digits) } else { None }
}
pub fn parse_jp_my_number(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 12 {
return None;
}
let bytes = digits.as_bytes();
const WEIGHTS: [u32; 11] = [6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
let mut sum: u32 = 0;
for i in 0..11 {
sum += u32::from(bytes[i] - b'0') * WEIGHTS[i];
}
let r = sum % 11;
let expected = if r < 2 { 0 } else { 11 - r };
if u32::from(bytes[11] - b'0') != expected {
return None;
}
Some(digits)
}
pub fn parse_mx_curp(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| !c.is_whitespace())
.map(|c| c.to_uppercase().next().unwrap_or(c))
.collect();
if cleaned.chars().count() != 18 {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
let is_letter_or_n_tilde = |c: char| c.is_ascii_uppercase() || c == 'Ñ';
if !chars[..4].iter().copied().all(is_letter_or_n_tilde) {
return None;
}
if !chars[4..10].iter().all(|c| c.is_ascii_digit()) {
return None;
}
if chars[10] != 'H' && chars[10] != 'M' {
return None;
}
if !chars[11..16].iter().copied().all(is_letter_or_n_tilde) {
return None;
}
if !chars[16].is_ascii_alphanumeric() && chars[16] != 'Ñ' {
return None;
}
if !chars[17].is_ascii_digit() {
return None;
}
let yy: i32 = cleaned[4..6].parse().ok()?;
let mm: u32 = cleaned[6..8].parse().ok()?;
let dd: u32 = cleaned[8..10].parse().ok()?;
let yyyy = if yy <= 29 { 2000 + yy } else { 1900 + yy };
chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
let value = |c: char| -> Option<u32> {
Some(match c {
'0'..='9' => (c as u32) - ('0' as u32),
'A'..='N' => 10 + ((c as u32) - ('A' as u32)),
'Ñ' => 24,
'O'..='Z' => 25 + ((c as u32) - ('O' as u32)),
_ => return None,
})
};
let mut sum: u32 = 0;
for (i, &c) in chars.iter().enumerate().take(17) {
sum += value(c)? * (18 - i as u32);
}
let expected = (10 - (sum % 10)) % 10;
if u32::from(chars[17] as u8 - b'0') != expected {
return None;
}
Some(cleaned)
}
pub fn parse_nz_nhi(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.map(|c| c.to_ascii_uppercase())
.collect();
if cleaned.len() != 7 {
return None;
}
let bytes = cleaned.as_bytes();
for &b in &bytes[..3] {
if !b.is_ascii_uppercase() || b == b'I' || b == b'O' {
return None;
}
}
for &b in &bytes[3..] {
if !b.is_ascii_digit() {
return None;
}
}
let letter_value = |b: u8| -> u32 {
let idx = u32::from(b - b'A') + 1;
if b > b'O' {
idx - 2
} else if b > b'I' {
idx - 1
} else {
idx
}
};
const WEIGHTS: [u32; 6] = [7, 6, 5, 4, 3, 2];
let mut sum: u32 = 0;
for i in 0..3 {
sum += letter_value(bytes[i]) * WEIGHTS[i];
}
for i in 0..3 {
sum += u32::from(bytes[3 + i] - b'0') * WEIGHTS[3 + i];
}
let r = sum % 11;
if r == 1 {
return None;
}
let expected = if r == 0 { 0 } else { 11 - r };
if u32::from(bytes[6] - b'0') != expected {
return None;
}
Some(cleaned)
}
pub fn parse_za_id(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 13 {
return None;
}
let yy: i32 = digits[0..2].parse().ok()?;
let mm: u32 = digits[2..4].parse().ok()?;
let dd: u32 = digits[4..6].parse().ok()?;
let yyyy = if yy <= 29 { 2000 + yy } else { 1900 + yy };
chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
let bytes = digits.as_bytes();
let mut sum: u32 = 0;
for i in 0..13 {
let mut d = u32::from(bytes[12 - i] - b'0');
if i % 2 == 1 {
d *= 2;
if d > 9 {
d -= 9;
}
}
sum += d;
}
if !sum.is_multiple_of(10) {
return None;
}
Some(digits)
}
pub fn parse_cy_passport(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
let chars: Vec<char> = cleaned.chars().collect();
match (chars.first(), chars.len()) {
(Some('E'), 7) if chars[1..].iter().all(|c| c.is_ascii_digit()) => Some(cleaned),
(Some('K'), 9) if chars[1..].iter().all(|c| c.is_ascii_digit()) => Some(cleaned),
_ => None,
}
}
pub fn parse_cz_passport(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if (8..=12).contains(&digits.len()) {
Some(digits)
} else {
None
}
}
pub fn parse_li_passport(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 6 {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
if !chars[0].is_ascii_alphabetic() {
return None;
}
if !chars[1..].iter().all(|c| c.is_ascii_digit()) {
return None;
}
Some(cleaned)
}
pub fn parse_lt_passport(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 8 {
Some(digits)
} else {
None
}
}
pub fn parse_mt_passport(s: &str) -> Option<String> {
let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() == 7 {
Some(digits)
} else {
None
}
}
pub fn parse_nl_passport(s: &str) -> Option<String> {
parse_nl_id(s)
}
pub fn parse_pt_passport(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 7 {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
if !chars[0].is_ascii_alphabetic() {
return None;
}
if !chars[1..].iter().all(|c| c.is_ascii_digit()) {
return None;
}
Some(cleaned)
}
pub fn parse_ro_passport(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 8 {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
if !chars[..2].iter().all(|c| c.is_ascii_alphabetic()) {
return None;
}
if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
return None;
}
Some(cleaned)
}
pub fn parse_sk_passport(s: &str) -> Option<String> {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 9 {
return None;
}
let chars: Vec<char> = cleaned.chars().collect();
if !chars[..2].iter().all(|c| c.is_ascii_alphabetic()) {
return None;
}
if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
return None;
}
Some(cleaned)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uk_nhs_number_compact_form_parses() {
assert_eq!(parse_uk_nhs_number("9434765919"), Some("9434765919".into()));
}
#[test]
fn uk_nhs_number_spaced_form_parses_to_same_canonical() {
assert_eq!(
parse_uk_nhs_number("943 476 5919"),
parse_uk_nhs_number("9434765919"),
);
}
#[test]
fn uk_nhs_number_rejects_letters_and_short_input() {
assert_eq!(parse_uk_nhs_number("ABCDEFGHIJ"), None);
assert_eq!(parse_uk_nhs_number("123"), None);
assert_eq!(parse_uk_nhs_number(""), None);
}
#[test]
fn fr_nir_round_trip_for_a_constructed_valid_value() {
let valid = "180127512345642";
assert_eq!(parse_fr_nir(valid), Some(valid.into()));
}
#[test]
fn fr_nir_whitespace_is_tolerated() {
assert_eq!(
parse_fr_nir("1 80 12 75 123 456 42"),
Some("180127512345642".into()),
);
}
#[test]
fn fr_nir_rejects_wrong_check_key() {
assert_eq!(parse_fr_nir("180127512345699"), None);
}
#[test]
fn fr_nir_rejects_wrong_length() {
assert_eq!(parse_fr_nir("12345"), None);
assert_eq!(parse_fr_nir("1234567890123456"), None); assert_eq!(parse_fr_nir(""), None);
}
#[test]
fn fr_nir_rejects_letters_in_digit_positions() {
assert_eq!(parse_fr_nir("A80127512345642"), None);
}
#[test]
fn fr_nir_handles_corsica_2a() {
let body = "184032A001234";
let numeric: u64 = "1840319001234".parse().unwrap();
let key = 97 - (numeric % 97);
let nir = format!("{body}{key:02}");
assert_eq!(parse_fr_nir(&nir), Some(nir.clone()));
}
#[test]
fn fr_nir_handles_corsica_2b() {
let body = "184032B001234";
let numeric: u64 = "1840318001234".parse().unwrap();
let key = 97 - (numeric % 97);
let nir = format!("{body}{key:02}");
assert_eq!(parse_fr_nir(&nir), Some(nir.clone()));
}
#[test]
fn fr_nir_canonical_form_is_uppercased() {
let body = "184032a001234";
let numeric: u64 = "1840319001234".parse().unwrap();
let key = 97 - (numeric % 97);
let nir = format!("{body}{key:02}");
let canonical = nir.to_uppercase();
assert_eq!(parse_fr_nir(&nir), Some(canonical));
}
#[test]
fn es_tsi_canonical_cip_sns_parses() {
assert_eq!(
parse_es_tsi("ABCD123456XY1234"),
Some("ABCD123456XY1234".into()),
);
}
#[test]
fn es_tsi_whitespace_and_hyphens_stripped() {
assert_eq!(
parse_es_tsi("abcd 123 456-xy1234"),
Some("ABCD123456XY1234".into()),
);
}
#[test]
fn es_tsi_rejects_too_short_or_too_long() {
assert_eq!(parse_es_tsi("ABC123"), None);
assert_eq!(parse_es_tsi("ABCDEF123456XY12345678"), None);
}
#[test]
fn es_tsi_rejects_non_alphanumerics() {
assert_eq!(parse_es_tsi("ABC@123!XYZ"), None);
}
#[test]
fn es_tsi_rejects_non_ascii() {
assert_eq!(parse_es_tsi("ABCDÑ12345XYZ"), None);
}
#[test]
fn ie_ihi_seven_digits_parses() {
assert_eq!(parse_ie_ihi("1234567"), Some("1234567".into()));
}
#[test]
fn ie_ihi_punctuation_and_spaces_stripped() {
assert_eq!(parse_ie_ihi("123 4567"), Some("1234567".into()));
assert_eq!(parse_ie_ihi("123-45-67"), Some("1234567".into()));
}
#[test]
fn ie_ihi_rejects_wrong_digit_count() {
assert_eq!(parse_ie_ihi("123456"), None);
assert_eq!(parse_ie_ihi("12345678"), None);
assert_eq!(parse_ie_ihi(""), None);
}
#[test]
fn ie_ihi_rejects_when_no_digits_present() {
assert_eq!(parse_ie_ihi("ABCDEFG"), None);
}
#[test]
fn uk_hc_number_matches_nhs_number_semantics() {
assert_eq!(
parse_uk_hc_number("9434765919"),
parse_uk_nhs_number("9434765919"),
);
assert_eq!(
parse_uk_hc_number("943 476 5919"),
parse_uk_nhs_number("943 476 5919"),
);
}
#[test]
fn uk_hc_number_rejects_letters() {
assert_eq!(parse_uk_hc_number("ABCDEFGHIJ"), None);
}
#[test]
fn us_ssn_canonical_compact_form_parses() {
assert_eq!(parse_us_ssn("123456789"), Some("123456789".into()));
}
#[test]
fn us_ssn_hyphenated_form_parses_to_same_canonical() {
assert_eq!(parse_us_ssn("123-45-6789"), parse_us_ssn("123456789"),);
}
#[test]
fn us_ssn_whitespace_variants_canonicalise_identically() {
assert_eq!(parse_us_ssn("123 45 6789"), Some("123456789".into()),);
assert_eq!(parse_us_ssn(" 123 45 6789 "), Some("123456789".into()),);
}
#[test]
fn us_ssn_rejects_invalid_area_numbers() {
assert_eq!(parse_us_ssn("000-12-3456"), None);
assert_eq!(parse_us_ssn("666-12-3456"), None);
assert_eq!(parse_us_ssn("900-12-3456"), None);
assert_eq!(parse_us_ssn("987-65-4321"), None); assert_eq!(parse_us_ssn("999-99-9999"), None);
}
#[test]
fn us_ssn_accepts_boundary_areas() {
assert_eq!(parse_us_ssn("001-23-4567"), Some("001234567".into()));
assert_eq!(parse_us_ssn("899-23-4567"), Some("899234567".into()));
assert_eq!(parse_us_ssn("665-23-4567"), Some("665234567".into()));
assert_eq!(parse_us_ssn("667-23-4567"), Some("667234567".into()));
}
#[test]
fn us_ssn_rejects_zero_group() {
assert_eq!(parse_us_ssn("123-00-4567"), None);
}
#[test]
fn us_ssn_rejects_zero_serial() {
assert_eq!(parse_us_ssn("123-45-0000"), None);
}
#[test]
fn us_ssn_rejects_wrong_length() {
assert_eq!(parse_us_ssn("12345"), None);
assert_eq!(parse_us_ssn("1234567890"), None);
assert_eq!(parse_us_ssn(""), None);
}
#[test]
fn us_ssn_rejects_letters() {
assert_eq!(parse_us_ssn("ABC-DE-FGHI"), None);
assert_eq!(parse_us_ssn("ABCDEFGHI"), None);
}
#[test]
fn us_ssn_strips_arbitrary_punctuation() {
assert_eq!(parse_us_ssn("(123).45.6789"), Some("123456789".into()),);
}
#[test]
fn de_kvnr_canonical_form_parses() {
assert_eq!(parse_de_kvnr("A123456780"), Some("A123456780".into()));
}
#[test]
fn de_kvnr_accepts_lowercase_letter_canonicalises_to_upper() {
assert_eq!(parse_de_kvnr("a123456780"), Some("A123456780".into()));
}
#[test]
fn de_kvnr_accepts_internal_whitespace() {
assert_eq!(parse_de_kvnr("A 123 456 780"), Some("A123456780".into()));
}
#[test]
fn de_kvnr_second_valid_vector() {
assert_eq!(parse_de_kvnr("B987654320"), Some("B987654320".into()));
}
#[test]
fn de_kvnr_rejects_wrong_check_digit() {
assert_eq!(parse_de_kvnr("A123456789"), None);
}
#[test]
fn de_kvnr_rejects_missing_letter() {
assert_eq!(parse_de_kvnr("1234567890"), None);
}
#[test]
fn de_kvnr_rejects_wrong_length() {
assert_eq!(parse_de_kvnr("A12345"), None);
assert_eq!(parse_de_kvnr("A1234567890"), None);
assert_eq!(parse_de_kvnr(""), None);
}
#[test]
fn de_kvnr_rejects_non_digit_in_body() {
assert_eq!(parse_de_kvnr("A12345A780"), None);
}
#[test]
fn it_cf_canonical_form_parses() {
assert_eq!(
parse_it_cf("RSSMRA85T10A562S"),
Some("RSSMRA85T10A562S".into()),
);
}
#[test]
fn it_cf_accepts_lowercase_and_whitespace() {
assert_eq!(
parse_it_cf("rss mra 85t 10a 562s"),
Some("RSSMRA85T10A562S".into()),
);
}
#[test]
fn it_cf_second_valid_vector() {
assert_eq!(
parse_it_cf("MNRMRC75H17H501I"),
Some("MNRMRC75H17H501I".into()),
);
}
#[test]
fn it_cf_rejects_wrong_check_character() {
assert_eq!(parse_it_cf("RSSMRA85T10A562X"), None);
}
#[test]
fn it_cf_rejects_wrong_length() {
assert_eq!(parse_it_cf("RSSMRA85T10A562"), None);
assert_eq!(parse_it_cf("RSSMRA85T10A562SS"), None);
assert_eq!(parse_it_cf(""), None);
}
#[test]
fn it_cf_rejects_non_alphanumeric() {
assert_eq!(parse_it_cf("RSSMRA85T10A562!"), None);
assert_eq!(parse_it_cf("RSSMRA-85T-10A562S"), None);
}
#[test]
fn nl_bsn_canonical_form_parses() {
assert_eq!(parse_nl_bsn("111222333"), Some("111222333".into()));
}
#[test]
fn nl_bsn_second_valid_vector() {
assert_eq!(parse_nl_bsn("123456782"), Some("123456782".into()));
}
#[test]
fn nl_bsn_strips_separators() {
assert_eq!(parse_nl_bsn("111 222 333"), Some("111222333".into()));
assert_eq!(parse_nl_bsn("111-222-333"), Some("111222333".into()));
}
#[test]
fn nl_bsn_rejects_wrong_eleven_test() {
assert_eq!(parse_nl_bsn("111222334"), None);
}
#[test]
fn nl_bsn_rejects_all_zeros() {
assert_eq!(parse_nl_bsn("000000000"), None);
}
#[test]
fn nl_bsn_rejects_wrong_length() {
assert_eq!(parse_nl_bsn("12345"), None);
assert_eq!(parse_nl_bsn("1234567890"), None);
assert_eq!(parse_nl_bsn(""), None);
}
#[test]
fn nl_bsn_rejects_letters() {
assert_eq!(parse_nl_bsn("ABCDEFGHI"), None);
}
#[test]
fn se_pnr_ten_digit_form_parses() {
assert_eq!(
parse_se_workernummer("4603243850"),
Some("4603243850".into()),
);
}
#[test]
fn se_pnr_with_separator_canonicalises_to_ten_digit() {
assert_eq!(
parse_se_workernummer("460324-3850"),
Some("4603243850".into()),
);
assert_eq!(
parse_se_workernummer("460324+3850"),
Some("4603243850".into()),
);
}
#[test]
fn se_pnr_twelve_digit_form_preserves_century() {
assert_eq!(
parse_se_workernummer("19460324-3850"),
Some("194603243850".into()),
);
assert_eq!(
parse_se_workernummer("194603243850"),
Some("194603243850".into()),
);
}
#[test]
fn se_pnr_second_valid_vector() {
assert_eq!(
parse_se_workernummer("8112310092"),
Some("8112310092".into()),
);
}
#[test]
fn se_pnr_rejects_wrong_luhn() {
assert_eq!(parse_se_workernummer("4603243851"), None);
}
#[test]
fn se_pnr_rejects_wrong_length() {
assert_eq!(parse_se_workernummer("12345"), None);
assert_eq!(parse_se_workernummer("12345678901"), None);
assert_eq!(parse_se_workernummer(""), None);
}
#[test]
fn se_pnr_rejects_letters() {
assert_eq!(parse_se_workernummer("ABCDEFGHIJ"), None);
}
#[test]
fn au_ihi_canonical_form_parses() {
assert_eq!(
parse_au_ihi("8003601234567894"),
Some("8003601234567894".into()),
);
}
#[test]
fn au_ihi_strips_whitespace() {
assert_eq!(
parse_au_ihi("8003 6012 3456 7894"),
Some("8003601234567894".into()),
);
}
#[test]
fn au_ihi_second_valid_vector() {
assert_eq!(
parse_au_ihi("8003619876543213"),
Some("8003619876543213".into()),
);
}
#[test]
fn au_ihi_rejects_wrong_luhn() {
assert_eq!(parse_au_ihi("8003601234567890"), None);
}
#[test]
fn au_ihi_rejects_wrong_length() {
assert_eq!(parse_au_ihi("12345"), None);
assert_eq!(parse_au_ihi("80036012345678941"), None);
assert_eq!(parse_au_ihi(""), None);
}
#[test]
fn au_ihi_rejects_letters() {
assert_eq!(parse_au_ihi("ABCDEFGHIJKLMNOP"), None);
}
#[test]
fn uk_chi_canonical_form_parses() {
assert_eq!(parse_uk_chi_number("0101701233"), Some("0101701233".into()),);
}
#[test]
fn uk_chi_strips_whitespace() {
assert_eq!(
parse_uk_chi_number("010 170 1233"),
Some("0101701233".into()),
);
}
#[test]
fn uk_chi_second_valid_vector() {
assert_eq!(parse_uk_chi_number("0101701241"), Some("0101701241".into()),);
}
#[test]
fn uk_chi_rejects_wrong_check_digit() {
assert_eq!(parse_uk_chi_number("0101701234"), None);
}
#[test]
fn uk_chi_rejects_wrong_length() {
assert_eq!(parse_uk_chi_number("12345"), None);
assert_eq!(parse_uk_chi_number("01017012339"), None);
assert_eq!(parse_uk_chi_number(""), None);
}
#[test]
fn uk_chi_rejects_letters() {
assert_eq!(parse_uk_chi_number("ABCDEFGHIJ"), None);
}
#[test]
fn be_nn_canonical_form_parses() {
assert_eq!(parse_be_nn("80010100107"), Some("80010100107".into()));
}
#[test]
fn be_nn_strips_punctuation() {
assert_eq!(parse_be_nn("80.01.01-001.07"), Some("80010100107".into()),);
}
#[test]
fn be_nn_rejects_wrong_check() {
assert_eq!(parse_be_nn("80010100100"), None);
}
#[test]
fn be_nn_rejects_wrong_length() {
assert_eq!(parse_be_nn("12345"), None);
assert_eq!(parse_be_nn(""), None);
}
#[test]
fn bg_egn_canonical_form_parses() {
assert_eq!(parse_bg_egn("8001010013"), Some("8001010013".into()));
}
#[test]
fn bg_egn_rejects_wrong_check() {
assert_eq!(parse_bg_egn("8001010014"), None);
}
#[test]
fn bg_egn_rejects_wrong_length() {
assert_eq!(parse_bg_egn("80010100"), None);
assert_eq!(parse_bg_egn(""), None);
}
#[test]
fn cz_rc_ten_digit_divisible_by_eleven() {
assert_eq!(parse_cz_rc("8001150014"), Some("8001150014".into()));
}
#[test]
fn cz_rc_nine_digit_pre_1954_accepted_as_is() {
assert_eq!(parse_cz_rc("800115001"), Some("800115001".into()));
}
#[test]
fn cz_rc_rejects_wrong_check() {
assert_eq!(parse_cz_rc("8001150015"), None);
}
#[test]
fn cz_rc_rejects_bad_length() {
assert_eq!(parse_cz_rc("12345"), None);
assert_eq!(parse_cz_rc("12345678901"), None);
}
#[test]
fn dk_cpr_canonical_parses() {
assert_eq!(parse_dk_cpr("1501801234"), Some("1501801234".into()));
}
#[test]
fn dk_cpr_strips_separator() {
assert_eq!(parse_dk_cpr("150180-1234"), Some("1501801234".into()));
}
#[test]
fn dk_cpr_rejects_bad_length() {
assert_eq!(parse_dk_cpr("12345"), None);
assert_eq!(parse_dk_cpr(""), None);
}
#[test]
fn ee_ik_canonical_form_parses() {
assert_eq!(parse_ee_ik("48001150011"), Some("48001150011".into()));
}
#[test]
fn ee_ik_rejects_wrong_check() {
assert_eq!(parse_ee_ik("48001150012"), None);
}
#[test]
fn ee_ik_rejects_bad_length() {
assert_eq!(parse_ee_ik("4800115001"), None);
}
#[test]
fn es_dni_canonical_form_parses() {
assert_eq!(parse_es_dni("12345678Z"), Some("12345678Z".into()));
}
#[test]
fn es_dni_rejects_wrong_letter() {
assert_eq!(parse_es_dni("12345678A"), None);
}
#[test]
fn es_dni_lowercase_letter_canonicalises_upper() {
assert_eq!(parse_es_dni("12345678z"), Some("12345678Z".into()));
}
#[test]
fn es_dni_handles_nie_prefix_x() {
assert_eq!(parse_es_dni("X1234567L"), Some("X1234567L".into()));
}
#[test]
fn fi_hetu_canonical_form_parses() {
assert_eq!(parse_fi_hetu("150180-999B"), Some("150180-999B".into()));
}
#[test]
fn fi_hetu_rejects_wrong_check() {
assert_eq!(parse_fi_hetu("150180-999C"), None);
}
#[test]
fn fi_hetu_rejects_bad_length() {
assert_eq!(parse_fi_hetu("12345"), None);
}
#[test]
fn hr_oib_canonical_form_parses() {
assert_eq!(parse_hr_oib("12345678903"), Some("12345678903".into()));
}
#[test]
fn hr_oib_rejects_wrong_check() {
assert_eq!(parse_hr_oib("12345678901"), None);
}
#[test]
fn hr_oib_rejects_bad_length() {
assert_eq!(parse_hr_oib("123456789"), None);
}
#[test]
fn is_kt_canonical_form_parses() {
assert_eq!(parse_is_kt("1501802529"), Some("1501802529".into()));
}
#[test]
fn is_kt_rejects_wrong_check() {
assert_eq!(parse_is_kt("1501802539"), None);
}
#[test]
fn is_kt_rejects_bad_length() {
assert_eq!(parse_is_kt("12345"), None);
}
#[test]
fn lt_ak_canonical_form_parses() {
assert_eq!(parse_lt_ak("48001150011"), Some("48001150011".into()));
}
#[test]
fn lt_ak_rejects_wrong_check() {
assert_eq!(parse_lt_ak("48001150012"), None);
}
#[test]
fn lv_pk_canonical_form_parses() {
assert_eq!(parse_lv_pk("15018010007"), Some("15018010007".into()));
}
#[test]
fn lv_pk_rejects_wrong_check() {
assert_eq!(parse_lv_pk("15018010008"), None);
}
#[test]
fn lv_pk_rejects_bad_length() {
assert_eq!(parse_lv_pk("1501801000"), None);
}
#[test]
fn mt_id_canonical_form_parses() {
assert_eq!(parse_mt_id("1234567M"), Some("1234567M".into()));
}
#[test]
fn mt_id_accepts_all_valid_letters() {
for letter in ['M', 'G', 'A', 'P', 'L', 'H', 'B', 'Z'] {
let s = format!("1234567{letter}");
assert!(parse_mt_id(&s).is_some(), "letter {letter} should be valid");
}
}
#[test]
fn mt_id_rejects_invalid_letter() {
assert_eq!(parse_mt_id("1234567X"), None);
assert_eq!(parse_mt_id("1234567K"), None);
}
#[test]
fn mt_id_rejects_bad_length() {
assert_eq!(parse_mt_id("12345M"), None);
}
#[test]
fn no_fnr_canonical_form_parses() {
assert_eq!(parse_no_fnr("15018012399"), Some("15018012399".into()));
}
#[test]
fn no_fnr_rejects_wrong_check() {
assert_eq!(parse_no_fnr("15018012390"), None);
assert_eq!(parse_no_fnr("15018012398"), None);
}
#[test]
fn no_fnr_rejects_bad_length() {
assert_eq!(parse_no_fnr("12345"), None);
}
#[test]
fn pl_pesel_canonical_form_parses() {
assert_eq!(parse_pl_pesel("80011500014"), Some("80011500014".into()));
}
#[test]
fn pl_pesel_rejects_wrong_check() {
assert_eq!(parse_pl_pesel("80011500015"), None);
}
#[test]
fn pl_pesel_rejects_bad_length() {
assert_eq!(parse_pl_pesel("1234"), None);
}
#[test]
fn ro_cnp_canonical_form_parses() {
assert_eq!(parse_ro_cnp("1800115400012"), Some("1800115400012".into()));
}
#[test]
fn ro_cnp_rejects_wrong_check() {
assert_eq!(parse_ro_cnp("1800115400015"), None);
}
#[test]
fn ro_cnp_rejects_bad_length() {
assert_eq!(parse_ro_cnp("180011540001"), None);
}
#[test]
fn si_emso_canonical_form_parses() {
assert_eq!(parse_si_emso("1501980500015"), Some("1501980500015".into()));
}
#[test]
fn si_emso_rejects_wrong_check() {
assert_eq!(parse_si_emso("1501980500014"), None);
}
#[test]
fn sk_rc_canonical_form_parses() {
assert_eq!(parse_sk_rc("8051150019"), Some("8051150019".into()));
}
#[test]
fn sk_rc_rejects_wrong_check() {
assert_eq!(parse_sk_rc("8051150010"), None);
}
#[test]
fn uk_nino_canonical_form_parses() {
assert_eq!(parse_uk_nino("AB123456A"), Some("AB123456A".into()));
}
#[test]
fn uk_nino_accepts_lowercase_and_whitespace() {
assert_eq!(parse_uk_nino("ab 12 34 56 a"), Some("AB123456A".into()),);
}
#[test]
fn uk_nino_rejects_banned_first_letter() {
for ch in ['D', 'F', 'I', 'Q', 'U', 'V'] {
let s = format!("{ch}A123456A");
assert!(parse_uk_nino(&s).is_none(), "letter {ch} should be banned");
}
}
#[test]
fn uk_nino_rejects_banned_admin_prefix() {
for prefix in ["OO", "CR", "FY", "MW", "NC", "PP", "PZ", "TN"] {
let s = format!("{prefix}123456A");
assert!(
parse_uk_nino(&s).is_none(),
"prefix {prefix} should be banned"
);
}
}
#[test]
fn uk_nino_rejects_bad_suffix() {
for ch in ['E', 'F', 'X', 'Z'] {
let s = format!("AB123456{ch}");
assert!(parse_uk_nino(&s).is_none(), "suffix {ch} should be invalid");
}
}
#[test]
fn uk_nino_rejects_bad_length() {
assert_eq!(parse_uk_nino("AB12345A"), None);
}
#[test]
fn gr_dss_canonical_form_parses() {
assert_eq!(parse_gr_dss("1234567890"), Some("1234567890".into()));
}
#[test]
fn gr_dss_strips_punctuation() {
assert_eq!(parse_gr_dss("12 34-56 78 90"), Some("1234567890".into()));
}
#[test]
fn gr_dss_rejects_bad_length() {
assert_eq!(parse_gr_dss("12345"), None);
assert_eq!(parse_gr_dss("12345678901"), None);
assert_eq!(parse_gr_dss(""), None);
}
#[test]
fn gr_dss_rejects_letters() {
assert_eq!(parse_gr_dss("ABCDEFGHIJ"), None);
}
#[test]
fn li_id_eight_digit_form_parses() {
assert_eq!(parse_li_id("ID12345678"), Some("ID12345678".into()));
}
#[test]
fn li_id_nine_digit_example_from_spec_parses() {
assert_eq!(parse_li_id("ID022143586"), Some("ID022143586".into()));
}
#[test]
fn li_id_lowercase_letters_uppercased() {
assert_eq!(parse_li_id("id12345678"), Some("ID12345678".into()));
}
#[test]
fn li_id_rejects_missing_letters() {
assert_eq!(parse_li_id("1234567890"), None);
assert_eq!(parse_li_id("I12345678"), None); }
#[test]
fn li_id_rejects_bad_length() {
assert_eq!(parse_li_id(""), None);
assert_eq!(parse_li_id("ID1234"), None);
assert_eq!(parse_li_id("ID123456789012"), None);
}
#[test]
fn nl_id_canonical_form_parses() {
assert_eq!(parse_nl_id("AB1234567"), Some("AB1234567".into()));
}
#[test]
fn nl_id_lowercase_and_whitespace_canonicalise() {
assert_eq!(parse_nl_id("ab 12 34 567"), Some("AB1234567".into()));
}
#[test]
fn nl_id_rejects_letter_o_in_disallowed_positions() {
assert_eq!(parse_nl_id("AO1234567"), None);
assert_eq!(parse_nl_id("OB1234567"), None);
assert_eq!(parse_nl_id("ABO234567"), None);
}
#[test]
fn nl_id_allows_digit_zero() {
assert_eq!(parse_nl_id("AB0234567"), Some("AB0234567".into()));
}
#[test]
fn nl_id_rejects_bad_shape() {
assert_eq!(parse_nl_id("12345AB67"), None);
assert_eq!(parse_nl_id("AB12345AB"), None);
assert_eq!(parse_nl_id(""), None);
}
#[test]
fn pl_nip_canonical_form_parses() {
assert_eq!(parse_pl_nip("1234567802"), Some("1234567802".into()));
}
#[test]
fn pl_nip_strips_separators() {
assert_eq!(parse_pl_nip("123-456-78-02"), Some("1234567802".into()));
}
#[test]
fn pl_nip_rejects_wrong_check() {
assert_eq!(parse_pl_nip("1234567803"), None);
}
#[test]
fn pl_nip_rejects_check_value_ten_per_spec() {
assert_eq!(parse_pl_nip("1234567890"), None);
}
#[test]
fn pl_nip_rejects_bad_length() {
assert_eq!(parse_pl_nip("12345"), None);
}
#[test]
fn pt_nif_canonical_form_parses() {
assert_eq!(parse_pt_nif("123456789"), Some("123456789".into()));
}
#[test]
fn pt_nif_rejects_wrong_check() {
assert_eq!(parse_pt_nif("123456780"), None);
}
#[test]
fn pt_nif_rejects_bad_length() {
assert_eq!(parse_pt_nif("12345"), None);
assert_eq!(parse_pt_nif("1234567890"), None);
}
#[test]
fn br_cpf_canonical_form_parses() {
assert_eq!(parse_br_cpf("12345678909"), Some("12345678909".into()));
}
#[test]
fn br_cpf_formatted_input_strips_punctuation() {
assert_eq!(parse_br_cpf("123.456.789-09"), Some("12345678909".into()));
}
#[test]
fn br_cpf_rejects_wrong_check() {
assert_eq!(parse_br_cpf("12345678900"), None);
}
#[test]
fn br_cpf_rejects_all_equal_sequences() {
for d in '0'..='9' {
let s: String = std::iter::repeat_n(d, 11).collect();
assert_eq!(parse_br_cpf(&s), None, "{s}");
}
}
#[test]
fn br_cpf_rejects_bad_length() {
assert_eq!(parse_br_cpf("1234567890"), None);
assert_eq!(parse_br_cpf("123456789090"), None);
}
#[test]
fn br_cpf_rejects_non_digit_only_input() {
assert_eq!(parse_br_cpf("abcdefghijk"), None);
}
#[test]
fn cn_rrn_canonical_form_parses() {
assert_eq!(
parse_cn_rrn("11010519491231002X"),
Some("11010519491231002X".into()),
);
}
#[test]
fn cn_rrn_uppercases_lowercase_x() {
assert_eq!(
parse_cn_rrn("11010519491231002x"),
Some("11010519491231002X".into()),
);
}
#[test]
fn cn_rrn_rejects_wrong_check_char() {
assert_eq!(parse_cn_rrn("11010519491231002Y"), None);
assert_eq!(parse_cn_rrn("110105194912310020"), None);
}
#[test]
fn cn_rrn_rejects_invalid_date_substring() {
assert_eq!(parse_cn_rrn("11010513491231002X"), None);
assert_eq!(parse_cn_rrn("110105194913320002X"), None);
}
#[test]
fn cn_rrn_rejects_bad_length() {
assert_eq!(parse_cn_rrn("11010519491231"), None);
assert_eq!(parse_cn_rrn("11010519491231002XY"), None);
}
#[test]
fn cn_rrn_rejects_non_alnum_letters() {
assert_eq!(parse_cn_rrn("11010519491231002A"), None);
}
#[test]
fn in_aadhaar_canonical_form_parses() {
assert_eq!(
parse_in_aadhaar("234123412346"),
Some("234123412346".into())
);
}
#[test]
fn in_aadhaar_strips_whitespace() {
assert_eq!(
parse_in_aadhaar("2341 2341 2346"),
Some("234123412346".into()),
);
}
#[test]
fn in_aadhaar_rejects_wrong_verhoeff_check() {
assert_eq!(parse_in_aadhaar("234123412347"), None);
assert_eq!(parse_in_aadhaar("234123412345"), None);
}
#[test]
fn in_aadhaar_rejects_all_equal_sequences() {
for d in '2'..='9' {
let s: String = std::iter::repeat_n(d, 12).collect();
assert_eq!(parse_in_aadhaar(&s), None, "{s}");
}
}
#[test]
fn in_aadhaar_rejects_reserved_prefixes() {
assert_eq!(parse_in_aadhaar("034123412346"), None);
assert_eq!(parse_in_aadhaar("134123412346"), None);
}
#[test]
fn in_aadhaar_rejects_bad_length() {
assert_eq!(parse_in_aadhaar("234123412"), None);
assert_eq!(parse_in_aadhaar("2341234123466"), None);
}
#[test]
fn jp_my_number_canonical_form_parses() {
assert_eq!(
parse_jp_my_number("123456789018"),
Some("123456789018".into()),
);
}
#[test]
fn jp_my_number_strips_whitespace() {
assert_eq!(
parse_jp_my_number("1234 5678 9018"),
Some("123456789018".into()),
);
}
#[test]
fn jp_my_number_rejects_wrong_check() {
assert_eq!(parse_jp_my_number("123456789010"), None);
assert_eq!(parse_jp_my_number("123456789019"), None);
}
#[test]
fn jp_my_number_rejects_bad_length() {
assert_eq!(parse_jp_my_number("12345678901"), None);
assert_eq!(parse_jp_my_number("1234567890123"), None);
}
#[test]
fn jp_my_number_rejects_non_digit_only_input() {
assert_eq!(parse_jp_my_number("abcdefghijkl"), None);
}
#[test]
fn mx_curp_canonical_form_parses() {
assert_eq!(
parse_mx_curp("HEGG560427MVZRRL04"),
Some("HEGG560427MVZRRL04".into()),
);
}
#[test]
fn mx_curp_uppercases_input() {
assert_eq!(
parse_mx_curp("hegg560427mvzrrl04"),
Some("HEGG560427MVZRRL04".into()),
);
}
#[test]
fn mx_curp_rejects_wrong_check() {
assert_eq!(parse_mx_curp("HEGG560427MVZRRL05"), None);
}
#[test]
fn mx_curp_rejects_invalid_date_substring() {
assert_eq!(parse_mx_curp("HEGG561327MVZRRL04"), None);
assert_eq!(parse_mx_curp("HEGG569927MVZRRL04"), None);
}
#[test]
fn mx_curp_rejects_bad_sex_char() {
assert_eq!(parse_mx_curp("HEGG560427XVZRRL04"), None);
}
#[test]
fn mx_curp_rejects_bad_length() {
assert_eq!(parse_mx_curp("HEGG560427"), None);
assert_eq!(parse_mx_curp("HEGG560427MVZRRL04EXTRA"), None);
}
#[test]
fn nz_nhi_canonical_form_parses() {
assert_eq!(parse_nz_nhi("ZAA0083"), Some("ZAA0083".into()));
}
#[test]
fn nz_nhi_uppercases_input() {
assert_eq!(parse_nz_nhi("zaa0083"), Some("ZAA0083".into()));
}
#[test]
fn nz_nhi_rejects_wrong_check() {
assert_eq!(parse_nz_nhi("ZAA0082"), None);
}
#[test]
fn nz_nhi_rejects_excluded_letters_i_and_o() {
assert_eq!(parse_nz_nhi("ZAI0083"), None);
assert_eq!(parse_nz_nhi("ZAO0083"), None);
assert_eq!(parse_nz_nhi("IZA0083"), None);
}
#[test]
fn nz_nhi_rejects_bad_length() {
assert_eq!(parse_nz_nhi("ZAA008"), None);
assert_eq!(parse_nz_nhi("ZAA00830"), None);
}
#[test]
fn nz_nhi_rejects_non_letter_prefix() {
assert_eq!(parse_nz_nhi("Z1A0083"), None);
}
#[test]
fn za_id_canonical_form_parses() {
assert_eq!(parse_za_id("8001015009087"), Some("8001015009087".into()));
}
#[test]
fn za_id_strips_whitespace() {
assert_eq!(parse_za_id("800101 5009 087"), Some("8001015009087".into()),);
}
#[test]
fn za_id_rejects_wrong_luhn() {
assert_eq!(parse_za_id("8001015009088"), None);
assert_eq!(parse_za_id("8001015009086"), None);
}
#[test]
fn za_id_rejects_invalid_date_substring() {
assert_eq!(parse_za_id("8013015009087"), None);
assert_eq!(parse_za_id("8002305009087"), None);
}
#[test]
fn za_id_rejects_bad_length() {
assert_eq!(parse_za_id("80010150090"), None);
assert_eq!(parse_za_id("80010150090870"), None);
}
#[test]
fn cy_passport_pre_2010_form_parses() {
assert_eq!(parse_cy_passport("E123456"), Some("E123456".into()));
}
#[test]
fn cy_passport_biometric_form_parses() {
assert_eq!(parse_cy_passport("K12345678"), Some("K12345678".into()));
}
#[test]
fn cy_passport_rejects_wrong_prefix() {
assert_eq!(parse_cy_passport("A123456"), None);
assert_eq!(parse_cy_passport("Z12345678"), None);
}
#[test]
fn cy_passport_rejects_bad_length() {
assert_eq!(parse_cy_passport("E12345"), None);
assert_eq!(parse_cy_passport("K1234567"), None);
}
#[test]
fn cz_passport_eight_digit_form_parses() {
assert_eq!(parse_cz_passport("12345678"), Some("12345678".into()));
}
#[test]
fn cz_passport_accepts_longer_forms() {
assert_eq!(
parse_cz_passport("123456789012"),
Some("123456789012".into())
);
}
#[test]
fn cz_passport_rejects_short_forms() {
assert_eq!(parse_cz_passport("1234567"), None);
assert_eq!(parse_cz_passport(""), None);
}
#[test]
fn li_passport_canonical_form_parses() {
assert_eq!(parse_li_passport("R00536"), Some("R00536".into()));
}
#[test]
fn li_passport_lowercases_to_upper() {
assert_eq!(parse_li_passport("r00536"), Some("R00536".into()));
}
#[test]
fn li_passport_rejects_bad_format() {
assert_eq!(parse_li_passport("RR0536"), None);
assert_eq!(parse_li_passport("123456"), None);
}
#[test]
fn lt_passport_eight_digit_parses() {
assert_eq!(parse_lt_passport("12345678"), Some("12345678".into()));
}
#[test]
fn lt_passport_rejects_wrong_length() {
assert_eq!(parse_lt_passport("1234567"), None);
assert_eq!(parse_lt_passport("123456789"), None);
}
#[test]
fn mt_passport_seven_digit_parses() {
assert_eq!(parse_mt_passport("1234567"), Some("1234567".into()));
}
#[test]
fn mt_passport_rejects_letters() {
assert_eq!(parse_mt_passport("123456M"), None);
}
#[test]
fn nl_passport_uses_nl_id_format() {
assert_eq!(parse_nl_passport("AB1234567"), Some("AB1234567".into()));
assert_eq!(parse_nl_passport("AO1234567"), None);
}
#[test]
fn pt_passport_canonical_form_parses() {
assert_eq!(parse_pt_passport("A123456"), Some("A123456".into()));
}
#[test]
fn pt_passport_rejects_bad_shape() {
assert_eq!(parse_pt_passport("AA12345"), None);
assert_eq!(parse_pt_passport("1234567"), None);
}
#[test]
fn ro_passport_canonical_form_parses() {
assert_eq!(parse_ro_passport("AB123456"), Some("AB123456".into()));
}
#[test]
fn ro_passport_rejects_bad_shape() {
assert_eq!(parse_ro_passport("A1234567"), None);
assert_eq!(parse_ro_passport("ABC12345"), None);
}
#[test]
fn sk_passport_canonical_form_parses() {
assert_eq!(parse_sk_passport("AB1234567"), Some("AB1234567".into()));
}
#[test]
fn sk_passport_rejects_bad_shape() {
assert_eq!(parse_sk_passport("ABC123456"), None);
assert_eq!(parse_sk_passport("AB12345"), None);
}
}