Skip to main content

worker_matcher/
identifiers.rs

1//! National healthcare identifier parsing and validation.
2//!
3//! This module exposes parsers for the national-level healthcare identifiers
4//! that the crate compares deterministically and probabilistically.
5//!
6//! Function names follow the convention `parse_<cc>_<scheme>`, where `<cc>`
7//! is the ISO 3166-1 alpha-2 country code (lower-cased) and `<scheme>` is
8//! the short identifier name. This keeps related schemes alphabetised within
9//! a country, and makes new countries easy to slot in.
10//!
11//! | Jurisdiction | Identifier | Parser |
12//! |---|---|---|
13//! | United Kingdom — England, Wales, Isle of Man | NHS Number | [`parse_uk_nhs_number`] |
14//! | France | NIR (*Numéro d'Inscription au Répertoire*) | [`parse_fr_nir`] |
15//! | España (Spain) | TSI (*Tarjeta Sanitaria Individual*) / CIP-SNS | [`parse_es_tsi`] |
16//! | Éire (Ireland) | IHI (Individual Health Identifier) | [`parse_ie_ihi`] |
17//! | United Kingdom — Northern Ireland | H&C Number (Health and Care Number) | [`parse_uk_hc_number`] |
18//! | United Kingdom — Scotland | CHI (Community Health Index) | [`parse_uk_chi_number`] |
19//! | United Kingdom | NINO (National Insurance Number) | [`parse_uk_nino`] |
20//! | United States | SSN (Social Security Number) | [`parse_us_ssn`] |
21//! | Germany | KVNR (Krankenversichertennummer) | [`parse_de_kvnr`] |
22//! | Italy | *Codice Fiscale* (CF) | [`parse_it_cf`] |
23//! | Netherlands | BSN (*Burgerservicenummer*) | [`parse_nl_bsn`] |
24//! | Sweden | *Workernummer* | [`parse_se_workernummer`] |
25//! | Australia | IHI (Individual Healthcare Identifier) | [`parse_au_ihi`] |
26//! | Belgium | National Number (*Rijksregisternummer*) | [`parse_be_nn`] |
27//! | Bulgaria | EGN (*Edinen grazhdanski nomer*) | [`parse_bg_egn`] |
28//! | Czech Republic | *Rodné číslo* | [`parse_cz_rc`] |
29//! | Denmark | CPR (*Centrale Workerregister*) | [`parse_dk_cpr`] |
30//! | Estonia | *Isikukood* | [`parse_ee_ik`] |
31//! | España (Spain) | DNI/NIE | [`parse_es_dni`] |
32//! | Finland | HETU (*Henkilötunnus*) | [`parse_fi_hetu`] |
33//! | Croatia | OIB (*Osobni identifikacijski broj*) | [`parse_hr_oib`] |
34//! | Iceland | *Kennitala* | [`parse_is_kt`] |
35//! | Lithuania | *Asmens kodas* | [`parse_lt_ak`] |
36//! | Latvia | *Workeras kods* | [`parse_lv_pk`] |
37//! | Malta | National ID | [`parse_mt_id`] |
38//! | Norway | *Fødselsnummer* | [`parse_no_fnr`] |
39//! | Poland | PESEL | [`parse_pl_pesel`] |
40//! | Romania | CNP (*Cod Numeric Workeral*) | [`parse_ro_cnp`] |
41//! | Slovenia | EMŠO (*Enotna Matična Številka Občana*) | [`parse_si_emso`] |
42//! | Slovakia | *Rodné číslo* | [`parse_sk_rc`] |
43//! | Greece | DSS investor share | [`parse_gr_dss`] |
44//! | Liechtenstein | National Identity Card Number | [`parse_li_id`] |
45//! | Netherlands | National Identity Card Number | [`parse_nl_id`] |
46//! | Poland | NIP (*Numer Identyfikacji Podatkowej*) | [`parse_pl_nip`] |
47//! | Portugal | NIF (*Número de Identificação Fiscal*) | [`parse_pt_nif`] |
48//! | Brazil | CPF (*Cadastro de Pessoas Físicas*) | [`parse_br_cpf`] |
49//! | China | RRN (*居民身份证*) 18-digit | [`parse_cn_rrn`] |
50//! | India | Aadhaar | [`parse_in_aadhaar`] |
51//! | Japan | My Number (*個人番号*) | [`parse_jp_my_number`] |
52//! | Mexico | CURP (*Clave Única de Registro de Población*) | [`parse_mx_curp`] |
53//! | New Zealand | NHI (National Health Index) — original 7-char form | [`parse_nz_nhi`] |
54//! | South Africa | ID Number | [`parse_za_id`] |
55//!
56//! ## Passport-number format validators
57//!
58//! Passport book numbers are not stable across renewals, and a worker
59//! may hold passports from several countries simultaneously — see
60//! [`crate::PassportBook`] for the canonical multi-country, multi-book,
61//! time-varying model used by the matcher. The following per-country
62//! parsers are pure **format validators** that consumers can call before
63//! constructing a `PassportBook` (or as a smell test in their own
64//! ingestion code). They do NOT have a corresponding `Worker` field;
65//! they exist so a country-specific passport number can be canonicalised
66//! and rejected at the system boundary.
67//!
68//! | Jurisdiction | Format | Parser |
69//! |---|---|---|
70//! | Cyprus | `E` + 6 digits (pre-2010) or `K` + 8 digits | [`parse_cy_passport`] |
71//! | Czech Republic | 8 to 12 digits | [`parse_cz_passport`] |
72//! | Liechtenstein | 1 letter + 5 digits | [`parse_li_passport`] |
73//! | Lithuania | 8 digits | [`parse_lt_passport`] |
74//! | Malta | 7 digits | [`parse_mt_passport`] |
75//! | Netherlands | same shape as the NL ID card | [`parse_nl_passport`] |
76//! | Portugal | 1 letter + 6 digits | [`parse_pt_passport`] |
77//! | Romania | 2 letters + 6 digits | [`parse_ro_passport`] |
78//! | Slovakia | 2 letters + 7 digits | [`parse_sk_passport`] |
79//!
80//! Each parser takes a `&str` and returns `Option<String>`:
81//!
82//! - `Some(canonical)` — the input parses for the identifier scheme. The
83//!   returned string is a canonical form (whitespace stripped, letters
84//!   uppercased) suitable for byte-equality comparison.
85//! - `None` — the input fails the scheme's structural or check-digit test.
86//!
87//! Two inputs that represent the same identifier in different textual
88//! layouts always canonicalise to the same string. Consumers compare the
89//! canonical forms for equality; the matching engine does exactly this.
90//!
91//! ## Design notes
92//!
93//! - Parsing is **format-only** unless the scheme has an integral check
94//!   digit (NIR has a Modulus-97 key; H&C / NHS structurally accept any
95//!   10-digit number through the `nhs-number` crate's `FromStr`).
96//! - These parsers do not consult external registries; they verify only
97//!   what can be derived from the identifier's own structure.
98//! - Country-specific semantic ranges (e.g. valid French department codes,
99//!   valid Spanish autonomous-community prefixes) are deliberately NOT
100//!   enforced to avoid rejecting edge-case-but-legitimate values.
101//!
102//! ## Example
103//!
104//! ```
105//! use worker_matcher::identifiers;
106//!
107//! // UK NHS Number — accepts the canonical "XXX XXX XXXX" layout.
108//! assert_eq!(
109//!     identifiers::parse_uk_nhs_number("943 476 5919"),
110//!     Some("9434765919".to_string()),
111//! );
112//!
113//! // Anything that does not match the NHS layout returns None.
114//! assert_eq!(identifiers::parse_uk_nhs_number("not-a-number"), None);
115//! ```
116
117use nhs_number::NHSNumber;
118use std::str::FromStr;
119
120/// Parse a United Kingdom NHS Number (England, Wales, Isle of Man).
121///
122/// Wraps [`nhs_number::NHSNumber::from_str`], which accepts the 10-digit
123/// compact layout (`"9434765919"`) and the spaced layout (`"943 476 5919"`).
124/// On success, the canonical 10-digit form is returned.
125///
126/// The NHS Number applies to England, Wales, and the Isle of Man. Northern
127/// Ireland uses a separate H&C Number that follows the same Modulus-11
128/// algorithm — see [`parse_uk_hc_number`].
129///
130/// # Examples
131///
132/// ```
133/// use worker_matcher::identifiers::parse_uk_nhs_number;
134///
135/// assert_eq!(parse_uk_nhs_number("9434765919"),   Some("9434765919".to_string()));
136/// assert_eq!(parse_uk_nhs_number("943 476 5919"), Some("9434765919".to_string()));
137/// assert_eq!(parse_uk_nhs_number("ABCDEFGHIJ"),   None);
138/// assert_eq!(parse_uk_nhs_number("123"),          None);
139/// ```
140pub fn parse_uk_nhs_number(s: &str) -> Option<String> {
141    let parsed = NHSNumber::from_str(s).ok()?;
142    let mut canonical = String::with_capacity(10);
143    for &d in &parsed.digits {
144        canonical.push(char::from_digit(d as u32, 10)?);
145    }
146    Some(canonical)
147}
148
149/// Parse a France NIR (*Numéro d'Inscription au Répertoire*).
150///
151/// The NIR — also known as the INSEE number or *Numéro de Sécurité Sociale*
152/// — is France's national social-security identifier and the de-facto unique
153/// healthcare identifier. Its structure is:
154///
155/// ```text
156/// S YY MM DD CCC NNN KK
157/// │ │  │  │  │   │   └─ 2-digit check key (Mod-97)
158/// │ │  │  │  │   └───── 3-digit municipal birth-order number
159/// │ │  │  │  └───────── 3-digit commune code
160/// │ │  │  └──────────── 2-digit département (or "2A"/"2B" for Corsica)
161/// │ │  └─────────────── 2-digit month of birth
162/// │ └────────────────── 2-digit year of birth
163/// └──────────────────── sex (1=male, 2=female, plus special values)
164/// ```
165///
166/// Total length is exactly 15 characters. The check key K satisfies
167/// `K = 97 - (N mod 97)`, where N is the 13-digit body. For Corsica, the
168/// department letters are remapped before computing N: `"2A" → "19"`,
169/// `"2B" → "18"`.
170///
171/// Whitespace in the input is stripped before parsing, so the formal layout
172/// `"1 80 12 75 123 456 42"` and the compact `"180127512345642"` both
173/// canonicalise to the same 15-character upper-case string.
174///
175/// # Examples
176///
177/// A canonical, syntactically valid NIR round-trips:
178///
179/// ```
180/// use worker_matcher::identifiers::parse_fr_nir;
181///
182/// // 13-digit body with department 75 (Paris), key computed as 97 - (N mod 97).
183/// let valid = "180127512345642";
184/// assert_eq!(parse_fr_nir(valid), Some(valid.to_string()));
185/// ```
186///
187/// Whitespace is tolerated:
188///
189/// ```
190/// # use worker_matcher::identifiers::parse_fr_nir;
191/// assert_eq!(
192///     parse_fr_nir("1 80 12 75 123 456 42"),
193///     Some("180127512345642".to_string()),
194/// );
195/// ```
196///
197/// An invalid check key rejects:
198///
199/// ```
200/// # use worker_matcher::identifiers::parse_fr_nir;
201/// assert_eq!(parse_fr_nir("180127512345699"), None);  // wrong key
202/// assert_eq!(parse_fr_nir("12345"),           None);  // wrong length
203/// assert_eq!(parse_fr_nir(""),                None);
204/// ```
205pub fn parse_fr_nir(s: &str) -> Option<String> {
206    let cleaned: String = s
207        .chars()
208        .filter(|c| !c.is_whitespace())
209        .collect::<String>()
210        .to_uppercase();
211
212    if !cleaned.is_ascii() || cleaned.len() != 15 {
213        return None;
214    }
215
216    let dept = &cleaned[5..7];
217    let numeric_body = match dept {
218        "2A" => format!("{}19{}", &cleaned[0..5], &cleaned[7..13]),
219        "2B" => format!("{}18{}", &cleaned[0..5], &cleaned[7..13]),
220        _ => cleaned[0..13].to_string(),
221    };
222
223    if !numeric_body.chars().all(|c| c.is_ascii_digit()) {
224        return None;
225    }
226    let key_str = &cleaned[13..15];
227    if !key_str.chars().all(|c| c.is_ascii_digit()) {
228        return None;
229    }
230
231    let n: u64 = numeric_body.parse().ok()?;
232    let key: u64 = key_str.parse().ok()?;
233
234    if 97 - (n % 97) == key {
235        Some(cleaned)
236    } else {
237        None
238    }
239}
240
241/// Parse a España (Spain) TSI (*Tarjeta Sanitaria Individual*) / CIP-SNS identifier.
242///
243/// Spain's healthcare identification is fragmented across 17 autonomous
244/// communities, each of which issues its own TSI card with a region-specific
245/// format. The national-level *Código de Identificación Workeral del Sistema
246/// Nacional de Salud* (CIP-SNS) provides a uniform 16-character code with
247/// the canonical structure `LLLLDDDDDDXXXXXX` (4 letters + 6 digits + 6
248/// alphanumerics), but regional formats are also encountered in practice.
249///
250/// To accept the full population of legitimate identifiers without
251/// privileging any region, this parser is **format-only** and lenient:
252///
253/// 1. Whitespace and ASCII hyphens are stripped.
254/// 2. Letters are uppercased.
255/// 3. The remaining string must contain only ASCII alphanumerics.
256/// 4. The length must be in `10..=20`.
257///
258/// No check-digit calculation is performed because the schemes vary by
259/// community. A consumer that needs stronger validation should layer a
260/// community-specific check on top of this canonical form.
261///
262/// # Examples
263///
264/// ```
265/// use worker_matcher::identifiers::parse_es_tsi;
266///
267/// // 16-character CIP-SNS national code:
268/// assert_eq!(
269///     parse_es_tsi("ABCD123456XY1234"),
270///     Some("ABCD123456XY1234".to_string()),
271/// );
272///
273/// // Whitespace and hyphens are stripped, letters uppercased:
274/// assert_eq!(
275///     parse_es_tsi("abcd 123 456-xy1234"),
276///     Some("ABCD123456XY1234".to_string()),
277/// );
278///
279/// // Too short, too long, or containing non-alphanumerics rejects:
280/// assert_eq!(parse_es_tsi("ABC123"),                 None);  // 6 chars
281/// assert_eq!(parse_es_tsi("ABCDEF123456XY12345678"), None);  // 22 chars
282/// assert_eq!(parse_es_tsi("ABC@123!XYZ"),            None);  // bad chars
283/// ```
284pub fn parse_es_tsi(s: &str) -> Option<String> {
285    let cleaned: String = s
286        .chars()
287        .filter(|c| !c.is_whitespace() && *c != '-')
288        .collect::<String>()
289        .to_uppercase();
290
291    if !cleaned.is_ascii() {
292        return None;
293    }
294    if !cleaned.chars().all(|c| c.is_ascii_alphanumeric()) {
295        return None;
296    }
297    if !(10..=20).contains(&cleaned.len()) {
298        return None;
299    }
300    Some(cleaned)
301}
302
303/// Parse an Éire (Ireland) IHI (Individual Health Identifier).
304///
305/// Under the Health Identifiers Act 2014, every individual receiving
306/// healthcare in Ireland is assigned a 7-digit IHI by the Health Identifiers
307/// Service. The IHI is the unique national healthcare identifier in the
308/// Republic of Ireland.
309///
310/// Parsing rules:
311///
312/// 1. All non-digit characters are stripped (spaces, hyphens, etc.).
313/// 2. The remaining string must contain exactly 7 ASCII digits.
314///
315/// No check-digit algorithm is enforced (none is publicly specified). The
316/// canonical form is the 7-digit string.
317///
318/// # Examples
319///
320/// ```
321/// use worker_matcher::identifiers::parse_ie_ihi;
322///
323/// assert_eq!(parse_ie_ihi("1234567"),    Some("1234567".to_string()));
324/// assert_eq!(parse_ie_ihi("123 4567"),   Some("1234567".to_string()));
325/// assert_eq!(parse_ie_ihi("123-45-67"),  Some("1234567".to_string()));
326///
327/// assert_eq!(parse_ie_ihi("12345"),      None);   // too short
328/// assert_eq!(parse_ie_ihi("12345678"),   None);   // too long
329/// assert_eq!(parse_ie_ihi("ABCDEFG"),    None);   // not digits
330/// ```
331pub fn parse_ie_ihi(s: &str) -> Option<String> {
332    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
333    if digits.len() == 7 {
334        Some(digits)
335    } else {
336        None
337    }
338}
339
340/// Parse a United Kingdom Northern Ireland H&C (Health and Care) Number.
341///
342/// The H&C Number is Northern Ireland's national healthcare identifier,
343/// issued by HSC (Health and Social Care). Structurally it is a 10-digit
344/// number with a Modulus-11 check digit — the same algorithm used by the
345/// UK NHS Number.
346///
347/// This parser delegates to the same logic as [`parse_uk_nhs_number`]: it
348/// accepts either the compact 10-digit form or the spaced
349/// `"XXX XXX XXXX"` form and returns the canonical 10-digit string.
350///
351/// The two parsers are intentionally separate so that callers track *which*
352/// scheme an identifier belongs to: a number that parses successfully as
353/// both an NHS Number and an H&C Number still refers to two distinct people
354/// in two distinct registries.
355///
356/// # Examples
357///
358/// ```
359/// use worker_matcher::identifiers::parse_uk_hc_number;
360///
361/// assert_eq!(parse_uk_hc_number("9434765919"),   Some("9434765919".to_string()));
362/// assert_eq!(parse_uk_hc_number("943 476 5919"), Some("9434765919".to_string()));
363/// assert_eq!(parse_uk_hc_number("not-a-number"), None);
364/// ```
365pub fn parse_uk_hc_number(s: &str) -> Option<String> {
366    parse_uk_nhs_number(s)
367}
368
369/// Parse a United States Social Security Number (SSN).
370///
371/// The SSN is the United States' de-facto national identifier — a 9-digit
372/// number assigned by the Social Security Administration, conventionally
373/// formatted as `"AAA-GG-SSSS"`:
374///
375/// ```text
376/// AAA  - GG - SSSS
377/// │      │    └──── Serial Number (4 digits, 0001..=9999)
378/// │      └───────── Group Number  (2 digits, 01..=99)
379/// └──────────────── Area Number   (3 digits, 001..=665, 667..=899)
380/// ```
381///
382/// Parsing rules:
383///
384/// 1. Keep only ASCII digits (strip whitespace, hyphens, periods,
385///    parentheses, …).
386/// 2. Reject unless the result has exactly 9 digits.
387/// 3. Reject structurally-impossible area numbers (`000`, `666`, and
388///    `900..=999`). These have never been assigned by SSA.
389/// 4. Reject group `00`.
390/// 5. Reject serial `0000`.
391///
392/// Since SSA introduced randomised assignment in June 2011, the area
393/// number no longer encodes the state of issuance, so no geographic
394/// validation is attempted. The canonical form is the 9-digit compact
395/// string `"AAAGGSSSS"`.
396///
397/// # Examples
398///
399/// Three textual layouts of the same SSN canonicalise identically:
400///
401/// ```
402/// use worker_matcher::identifiers::parse_us_ssn;
403///
404/// assert_eq!(parse_us_ssn("123-45-6789"), Some("123456789".to_string()));
405/// assert_eq!(parse_us_ssn("123 45 6789"), Some("123456789".to_string()));
406/// assert_eq!(parse_us_ssn("123456789"),   Some("123456789".to_string()));
407/// ```
408///
409/// Structurally-invalid values are rejected:
410///
411/// ```
412/// # use worker_matcher::identifiers::parse_us_ssn;
413/// assert_eq!(parse_us_ssn("000-12-3456"), None); // area 000 never issued
414/// assert_eq!(parse_us_ssn("666-12-3456"), None); // area 666 never issued
415/// assert_eq!(parse_us_ssn("900-12-3456"), None); // area 900..=999 never issued
416/// assert_eq!(parse_us_ssn("123-00-4567"), None); // group 00 invalid
417/// assert_eq!(parse_us_ssn("123-45-0000"), None); // serial 0000 invalid
418/// assert_eq!(parse_us_ssn("12345"),       None); // too short
419/// assert_eq!(parse_us_ssn("ABCDEFGHI"),   None); // not digits
420/// assert_eq!(parse_us_ssn(""),            None);
421/// ```
422pub fn parse_us_ssn(s: &str) -> Option<String> {
423    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
424    if digits.len() != 9 {
425        return None;
426    }
427    let area: u32 = digits[0..3].parse().ok()?;
428    let group: u32 = digits[3..5].parse().ok()?;
429    let serial: u32 = digits[5..9].parse().ok()?;
430    if area == 0 || area == 666 || area >= 900 {
431        return None;
432    }
433    if group == 0 {
434        return None;
435    }
436    if serial == 0 {
437        return None;
438    }
439    Some(digits)
440}
441
442/// Parse a Germany KVNR (*Krankenversichertennummer*).
443///
444/// The KVNR is the lifelong health-insurance number printed on the
445/// German electronic health card (*elektronische Gesundheitskarte*).
446/// Structure: 10 characters total, one uppercase letter followed by 9
447/// digits. The final digit is a Mod-10 check digit.
448///
449/// Check-digit algorithm:
450///
451/// 1. Map the leading letter to a two-digit ordinal (`A=01`, `B=02`,
452///    …, `Z=26`).
453/// 2. Concatenate that 2-digit value with positions 2..=9 of the KVNR
454///    (the 8 digits before the check digit) → a 10-digit string.
455/// 3. Multiply each of those 10 digits by alternating weights
456///    `1, 2, 1, 2, 1, 2, 1, 2, 1, 2`.
457/// 4. For products `≥ 10`, replace with the digit sum (max product is
458///    `9 × 2 = 18`, so subtract 9 to digit-sum).
459/// 5. Sum all results; the check digit is `sum mod 10`.
460///
461/// Whitespace is stripped before parsing. The canonical form is the
462/// 10-character uppercase string.
463///
464/// # Examples
465///
466/// ```
467/// use worker_matcher::identifiers::parse_de_kvnr;
468///
469/// // Constructed valid KVNR (A=01; alternating Mod-10 yields check digit 0).
470/// assert_eq!(parse_de_kvnr("A123456780"), Some("A123456780".to_string()));
471/// assert_eq!(parse_de_kvnr("a123456780"), Some("A123456780".to_string()));  // lowercase letter accepted
472///
473/// // Wrong check digit:
474/// assert_eq!(parse_de_kvnr("A123456789"), None);
475///
476/// // Wrong length / shape:
477/// assert_eq!(parse_de_kvnr("1234567890"), None);    // no letter
478/// assert_eq!(parse_de_kvnr("A12345"),     None);
479/// assert_eq!(parse_de_kvnr(""),           None);
480/// ```
481pub fn parse_de_kvnr(s: &str) -> Option<String> {
482    let cleaned: String = s
483        .chars()
484        .filter(|c| !c.is_whitespace())
485        .collect::<String>()
486        .to_uppercase();
487    if !cleaned.is_ascii() || cleaned.len() != 10 {
488        return None;
489    }
490    let mut chars = cleaned.chars();
491    let first = chars.next()?;
492    if !first.is_ascii_alphabetic() {
493        return None;
494    }
495    let digit_chars: Vec<char> = chars.collect();
496    if !digit_chars.iter().all(|c| c.is_ascii_digit()) {
497        return None;
498    }
499    let letter_ord = (first as u32) - ('A' as u32) + 1;
500    let mut combined: Vec<u32> = vec![letter_ord / 10, letter_ord % 10];
501    for c in &digit_chars[..8] {
502        combined.push(c.to_digit(10)?);
503    }
504    let mut total: u32 = 0;
505    for (i, d) in combined.iter().enumerate() {
506        let weight = if i % 2 == 0 { 1 } else { 2 };
507        let product = d * weight;
508        total += if product >= 10 { product - 9 } else { product };
509    }
510    let expected = digit_chars[8].to_digit(10)?;
511    if total % 10 == expected {
512        Some(cleaned)
513    } else {
514        None
515    }
516}
517
518/// Per-position lookup table for the Italy *Codice Fiscale* check
519/// character.
520///
521/// "Odd" positions are the 1st, 3rd, 5th, …, 15th characters
522/// (1-indexed); they map per a specific table that intentionally
523/// scatters values so single-character typos are likely to shift the
524/// resulting check character.
525fn cf_odd_value(c: char) -> Option<u32> {
526    Some(match c {
527        '0' | 'A' => 1,
528        '1' | 'B' => 0,
529        '2' | 'C' => 5,
530        '3' | 'D' => 7,
531        '4' | 'E' => 9,
532        '5' | 'F' => 13,
533        '6' | 'G' => 15,
534        '7' | 'H' => 17,
535        '8' | 'I' => 19,
536        '9' | 'J' => 21,
537        'K' => 2,
538        'L' => 4,
539        'M' => 18,
540        'N' => 20,
541        'O' => 11,
542        'P' => 3,
543        'Q' => 6,
544        'R' => 8,
545        'S' => 12,
546        'T' => 14,
547        'U' => 16,
548        'V' => 10,
549        'W' => 22,
550        'X' => 25,
551        'Y' => 24,
552        'Z' => 23,
553        _ => return None,
554    })
555}
556
557/// "Even" positions (2nd, 4th, …, 14th, 1-indexed) for the Italy
558/// *Codice Fiscale* check character. Numeric values map to their digit
559/// value; letters map to `A=0`, `B=1`, …, `Z=25`.
560fn cf_even_value(c: char) -> Option<u32> {
561    Some(match c {
562        '0' | 'A' => 0,
563        '1' | 'B' => 1,
564        '2' | 'C' => 2,
565        '3' | 'D' => 3,
566        '4' | 'E' => 4,
567        '5' | 'F' => 5,
568        '6' | 'G' => 6,
569        '7' | 'H' => 7,
570        '8' | 'I' => 8,
571        '9' | 'J' => 9,
572        'K' => 10,
573        'L' => 11,
574        'M' => 12,
575        'N' => 13,
576        'O' => 14,
577        'P' => 15,
578        'Q' => 16,
579        'R' => 17,
580        'S' => 18,
581        'T' => 19,
582        'U' => 20,
583        'V' => 21,
584        'W' => 22,
585        'X' => 23,
586        'Y' => 24,
587        'Z' => 25,
588        _ => return None,
589    })
590}
591
592/// Parse an Italy *Codice Fiscale* (CF).
593///
594/// The CF is a 16-character alphanumeric identifier issued by the
595/// Italian tax authority and used as the de-facto national healthcare
596/// identifier. It encodes a coded form of the holder's name, date of
597/// birth, sex, and commune of birth, followed by a Mod-26 check
598/// character.
599///
600/// Check-character algorithm:
601///
602/// 1. For each of the first 15 characters, compute a numeric value
603///    using two lookup tables — "odd" positions (1, 3, 5, …, 15;
604///    1-indexed) use the scattered table; "even" positions (2, 4, …,
605///    14) map digits and letters to their natural value.
606/// 2. Sum the 15 values, take mod 26.
607/// 3. Map `0..=25` to `A..=Z`. The result MUST equal the 16th
608///    character.
609///
610/// Whitespace is stripped and letters are uppercased before parsing.
611/// The canonical form is the 16-character uppercase string.
612///
613/// # Examples
614///
615/// ```
616/// use worker_matcher::identifiers::parse_it_cf;
617///
618/// // Synthetic CF with verified check character (sum 122, mod 26 = 18, 18→'S').
619/// assert_eq!(
620///     parse_it_cf("RSSMRA85T10A562S"),
621///     Some("RSSMRA85T10A562S".to_string()),
622/// );
623/// // Lowercase and whitespace tolerated:
624/// assert_eq!(
625///     parse_it_cf("rss mra 85t 10a 562s"),
626///     Some("RSSMRA85T10A562S".to_string()),
627/// );
628///
629/// // Wrong check character:
630/// assert_eq!(parse_it_cf("RSSMRA85T10A562X"), None);
631///
632/// // Wrong length:
633/// assert_eq!(parse_it_cf("RSSMRA85T10A562"),  None);
634/// assert_eq!(parse_it_cf("RSSMRA85T10A562SS"), None);
635/// // Non-alphanumeric content:
636/// assert_eq!(parse_it_cf("RSSMRA85T10A562!"), None);
637/// assert_eq!(parse_it_cf(""),                  None);
638/// ```
639pub fn parse_it_cf(s: &str) -> Option<String> {
640    let cleaned: String = s
641        .chars()
642        .filter(|c| !c.is_whitespace())
643        .collect::<String>()
644        .to_uppercase();
645    if !cleaned.is_ascii() || cleaned.len() != 16 {
646        return None;
647    }
648    if !cleaned.chars().all(|c| c.is_ascii_alphanumeric()) {
649        return None;
650    }
651    let chars: Vec<char> = cleaned.chars().collect();
652    let mut total: u32 = 0;
653    for (i, c) in chars.iter().take(15).enumerate() {
654        // 1-indexed position parity: index 0 is position 1 (odd).
655        let value = if i % 2 == 0 {
656            cf_odd_value(*c)?
657        } else {
658            cf_even_value(*c)?
659        };
660        total += value;
661    }
662    let expected_check = (b'A' + (total % 26) as u8) as char;
663    if chars[15] == expected_check {
664        Some(cleaned)
665    } else {
666        None
667    }
668}
669
670/// Parse a Netherlands BSN (*Burgerservicenummer*).
671///
672/// The BSN is a 9-digit citizen-service number used by all Dutch
673/// authorities, including healthcare providers. It carries an
674/// "11-test" check rule originally derived from the bank account
675/// number validation.
676///
677/// Check rule (the "11-test"):
678///
679/// `9·d₁ + 8·d₂ + 7·d₃ + 6·d₄ + 5·d₅ + 4·d₆ + 3·d₇ + 2·d₈ − d₉ ≡ 0 (mod 11)`
680///
681/// Non-digit characters are stripped before validation (so spaces or
682/// hyphens used for readability are tolerated). The all-zero string
683/// `000000000` is rejected even though it satisfies the arithmetic.
684/// The canonical form is the 9-digit compact string.
685///
686/// # Examples
687///
688/// ```
689/// use worker_matcher::identifiers::parse_nl_bsn;
690///
691/// // 111222333: 9·1 + 8·1 + 7·1 + 6·2 + 5·2 + 4·2 + 3·3 + 2·3 − 3 = 66; 66 mod 11 = 0.
692/// assert_eq!(parse_nl_bsn("111222333"), Some("111222333".to_string()));
693/// assert_eq!(parse_nl_bsn("111 222 333"), Some("111222333".to_string()));
694///
695/// // Wrong check (final digit changed):
696/// assert_eq!(parse_nl_bsn("111222334"), None);
697///
698/// // Wrong length, non-digits, all-zeros, empty:
699/// assert_eq!(parse_nl_bsn("12345"),     None);
700/// assert_eq!(parse_nl_bsn("ABCDEFGHI"), None);
701/// assert_eq!(parse_nl_bsn("000000000"), None);
702/// assert_eq!(parse_nl_bsn(""),          None);
703/// ```
704pub fn parse_nl_bsn(s: &str) -> Option<String> {
705    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
706    if digits.len() != 9 {
707        return None;
708    }
709    if digits.chars().all(|c| c == '0') {
710        return None;
711    }
712    let weights: [i32; 9] = [9, 8, 7, 6, 5, 4, 3, 2, -1];
713    let mut sum: i32 = 0;
714    for (i, c) in digits.chars().enumerate() {
715        sum += (c.to_digit(10)? as i32) * weights[i];
716    }
717    if sum % 11 == 0 { Some(digits) } else { None }
718}
719
720/// Parse a Sweden *Workernummer*.
721///
722/// The Swedish workeral identity number is the national identifier
723/// used for taxation, healthcare, banking, and similar purposes. It
724/// comes in two textual layouts:
725///
726/// - 10-digit form: `YYMMDDNNNC` (or with a `-` / `+` separator
727///   between the date and the serial, e.g. `460324-3850`). The `+`
728///   separator indicates the holder is over 100 years old.
729/// - 12-digit form: `YYYYMMDDNNNC` (or `19460324-3850`).
730///
731/// `Y`/`M`/`D` are the birth-date digits, `NNN` is a 3-digit serial
732/// (odd = male, even = female under the historical convention), and
733/// `C` is the Luhn check digit computed over the 10 digits of the
734/// 10-digit form.
735///
736/// Non-digit characters are stripped before validation. The Luhn
737/// check uses left-to-right weights `2, 1, 2, 1, 2, 1, 2, 1, 2, 1`;
738/// products `≥ 10` are reduced by digit-sum; the total mod 10 must be
739/// `0`.
740///
741/// The canonical form preserves the input length: 10-digit input
742/// returns a 10-character string; 12-digit input returns a 12-character
743/// string. Records using mixed layouts will not match deterministically
744/// on this field, but they will still produce the correct Luhn
745/// validation.
746///
747/// # Examples
748///
749/// ```
750/// use worker_matcher::identifiers::parse_se_workernummer;
751///
752/// // Synthetic 10-digit workernummer with verified Luhn (sum 40, mod 10 = 0).
753/// assert_eq!(
754///     parse_se_workernummer("4603243850"),
755///     Some("4603243850".to_string()),
756/// );
757/// assert_eq!(
758///     parse_se_workernummer("460324-3850"),
759///     Some("4603243850".to_string()),
760/// );
761///
762/// // 12-digit form canonicalises with the century preserved.
763/// assert_eq!(
764///     parse_se_workernummer("19460324-3850"),
765///     Some("194603243850".to_string()),
766/// );
767///
768/// // Wrong Luhn:
769/// assert_eq!(parse_se_workernummer("4603243851"), None);
770///
771/// // Wrong length, non-digits, empty:
772/// assert_eq!(parse_se_workernummer("12345"),       None);
773/// assert_eq!(parse_se_workernummer("ABCDEFGHIJ"),  None);
774/// assert_eq!(parse_se_workernummer(""),            None);
775/// ```
776pub fn parse_se_workernummer(s: &str) -> Option<String> {
777    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
778    let luhn_digits: &str = match digits.len() {
779        10 => &digits,
780        12 => &digits[2..],
781        _ => return None,
782    };
783    let mut sum: u32 = 0;
784    for (i, c) in luhn_digits.chars().enumerate() {
785        let d = c.to_digit(10)?;
786        let weight = if i % 2 == 0 { 2 } else { 1 };
787        let product = d * weight;
788        sum += if product >= 10 { product - 9 } else { product };
789    }
790    if sum.is_multiple_of(10) {
791        Some(digits)
792    } else {
793        None
794    }
795}
796
797/// Parse an Australia IHI (Individual Healthcare Identifier).
798///
799/// The IHI is the unique 16-digit identifier issued by the Healthcare
800/// Identifiers Service (HI Service) of the Australian Digital Health
801/// Agency. It conforms to ISO/IEC 7812-1 with a Luhn check digit.
802///
803/// Non-digit characters are stripped before validation. The Luhn
804/// check uses left-to-right weights `2, 1, 2, 1, …` over all 16
805/// digits (the rightmost digit is the check); products `≥ 10` are
806/// reduced by digit-sum; the total mod 10 must be `0`. The structural
807/// convention that real IHIs begin with `800360` is **not** enforced
808/// here so test and migration data with other prefixes parse cleanly.
809///
810/// # Examples
811///
812/// ```
813/// use worker_matcher::identifiers::parse_au_ihi;
814///
815/// // Synthetic 16-digit IHI with verified Luhn.
816/// assert_eq!(
817///     parse_au_ihi("8003601234567894"),
818///     Some("8003601234567894".to_string()),
819/// );
820/// assert_eq!(
821///     parse_au_ihi("8003 6012 3456 7894"),
822///     Some("8003601234567894".to_string()),
823/// );
824///
825/// // Wrong Luhn / wrong length / non-digits:
826/// assert_eq!(parse_au_ihi("8003601234567890"), None);
827/// assert_eq!(parse_au_ihi("12345"),            None);
828/// assert_eq!(parse_au_ihi("ABCDEFGHIJKLMNOP"), None);
829/// assert_eq!(parse_au_ihi(""),                 None);
830/// ```
831pub fn parse_au_ihi(s: &str) -> Option<String> {
832    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
833    if digits.len() != 16 {
834        return None;
835    }
836    let mut sum: u32 = 0;
837    for (i, c) in digits.chars().enumerate() {
838        let d = c.to_digit(10)?;
839        let weight = if i % 2 == 0 { 2 } else { 1 };
840        let product = d * weight;
841        sum += if product >= 10 { product - 9 } else { product };
842    }
843    if sum.is_multiple_of(10) {
844        Some(digits)
845    } else {
846        None
847    }
848}
849
850/// Parse a Scotland CHI (Community Health Index) Number.
851///
852/// The CHI Number is the unique worker identifier used by NHS
853/// Scotland. Structure: 10 digits formatted `DDMMYYSSSC`, where
854/// `DDMMYY` is the holder's date of birth, `SSS` is a 3-digit
855/// sequence with the third digit encoding sex (odd = male, even =
856/// female), and `C` is a Mod-11 check digit computed in the same
857/// fashion as the UK NHS Number.
858///
859/// Check rule (Mod-11):
860///
861/// 1. Multiply each of the first 9 digits by the weights
862///    `10, 9, 8, 7, 6, 5, 4, 3, 2`.
863/// 2. Sum, take mod 11.
864/// 3. The check digit is `(11 − (sum mod 11)) mod 11`. A computed
865///    check of `10` indicates an invalid identifier and is rejected.
866///
867/// Non-digit characters are stripped before validation. The canonical
868/// form is the 10-digit compact string. Although the NHS Number and
869/// the CHI Number share the same Mod-11 algorithm, the two are
870/// **scheme-local** in this crate and never cross-match (per spec
871/// FR-13 / §12.1).
872///
873/// # Examples
874///
875/// ```
876/// use worker_matcher::identifiers::parse_uk_chi_number;
877///
878/// // Synthetic CHI with verified Mod-11 (sum 74, check = 3).
879/// assert_eq!(
880///     parse_uk_chi_number("0101701233"),
881///     Some("0101701233".to_string()),
882/// );
883/// assert_eq!(
884///     parse_uk_chi_number("010 170 1233"),
885///     Some("0101701233".to_string()),
886/// );
887///
888/// // Wrong check / length / non-digits:
889/// assert_eq!(parse_uk_chi_number("0101701234"), None);
890/// assert_eq!(parse_uk_chi_number("12345"),      None);
891/// assert_eq!(parse_uk_chi_number("ABCDEFGHIJ"), None);
892/// assert_eq!(parse_uk_chi_number(""),           None);
893/// ```
894pub fn parse_uk_chi_number(s: &str) -> Option<String> {
895    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
896    if digits.len() != 10 {
897        return None;
898    }
899    let chars: Vec<u32> = digits
900        .chars()
901        .map(|c| c.to_digit(10).expect("filtered to digits"))
902        .collect();
903    let weights = [10u32, 9, 8, 7, 6, 5, 4, 3, 2];
904    let sum: u32 = chars
905        .iter()
906        .take(9)
907        .zip(weights.iter())
908        .map(|(d, w)| d * w)
909        .sum();
910    let check = (11 - (sum % 11)) % 11;
911    if check == 10 {
912        return None;
913    }
914    if check == chars[9] {
915        Some(digits)
916    } else {
917        None
918    }
919}
920
921// ----------------------------------------------------------------------------
922// Additional national workeral identifiers (T-27).
923//
924// Each parser canonicalises whitespace + (where applicable) case, and verifies
925// the scheme's check digit / check character. Parsers return Option<String>;
926// `Some(canonical)` is suitable for byte-equality comparison.
927// ----------------------------------------------------------------------------
928
929/// Parse a Belgium *Rijksregisternummer* (National Number).
930///
931/// 11 digits: `YYMMDD` + 3-digit serial + 2-digit Mod-97 check.
932/// Pre-2000 births: check = `97 − (first-9-digits mod 97)`.
933/// 2000-and-later births: a `"2"` is prepended before the modulo step.
934/// The parser tries both and accepts either.
935///
936/// ```
937/// use worker_matcher::identifiers::parse_be_nn;
938/// assert_eq!(parse_be_nn("80010100107"), Some("80010100107".to_string()));
939/// assert_eq!(parse_be_nn("80.01.01-001.07"), Some("80010100107".to_string()));
940/// assert_eq!(parse_be_nn("80010100100"), None);   // wrong check
941/// assert_eq!(parse_be_nn("12345"), None);         // wrong length
942/// ```
943pub fn parse_be_nn(s: &str) -> Option<String> {
944    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
945    if digits.len() != 11 {
946        return None;
947    }
948    let body: u64 = digits[..9].parse().ok()?;
949    let check: u64 = digits[9..11].parse().ok()?;
950    let pre2000 = 97 - body % 97;
951    let post2000_body: u64 = format!("2{}", &digits[..9]).parse().ok()?;
952    let post2000 = 97 - post2000_body % 97;
953    if check == pre2000 || check == post2000 {
954        Some(digits)
955    } else {
956        None
957    }
958}
959
960/// Parse a Bulgaria EGN (*Edinen grazhdanski nomer*).
961///
962/// 10 digits: `YYMMDD` (with month-offset for century) + 3-digit area/serial
963/// + 1 check digit. Check uses weights `[2,4,8,5,10,9,7,3,6]` mod 11 (10 → 0).
964///
965/// ```
966/// use worker_matcher::identifiers::parse_bg_egn;
967/// assert_eq!(parse_bg_egn("8001010013"), Some("8001010013".to_string()));
968/// assert_eq!(parse_bg_egn("8001010014"), None);
969/// assert_eq!(parse_bg_egn(""), None);
970/// ```
971pub fn parse_bg_egn(s: &str) -> Option<String> {
972    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
973    if digits.len() != 10 {
974        return None;
975    }
976    let weights: [u32; 9] = [2, 4, 8, 5, 10, 9, 7, 3, 6];
977    let mut sum: u32 = 0;
978    for (i, c) in digits.chars().take(9).enumerate() {
979        sum += c.to_digit(10)? * weights[i];
980    }
981    let expected = if sum % 11 == 10 { 0 } else { sum % 11 };
982    if digits.chars().nth(9)?.to_digit(10)? == expected {
983        Some(digits)
984    } else {
985        None
986    }
987}
988
989/// Parse a Czech Republic *Rodné číslo*.
990///
991/// 9 or 10 digits. The 10-digit form (post-1953) is divisible by 11 (with
992/// the edge case that mod-11 = 10 collapses to a trailing 0; the resulting
993/// 10-digit number may NOT be divisible by 11). The 9-digit form (pre-1954)
994/// is accepted as-is.
995///
996/// ```
997/// use worker_matcher::identifiers::parse_cz_rc;
998/// assert_eq!(parse_cz_rc("8001150014"), Some("8001150014".to_string()));
999/// assert_eq!(parse_cz_rc("800115001"), Some("800115001".to_string())); // 9-digit pre-1954
1000/// assert_eq!(parse_cz_rc("8001150015"), None);
1001/// ```
1002pub fn parse_cz_rc(s: &str) -> Option<String> {
1003    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1004    match digits.len() {
1005        9 => Some(digits),
1006        10 => {
1007            let n: u64 = digits.parse().ok()?;
1008            // Standard rule: 10-digit RČ is divisible by 11. Edge case:
1009            // when first-9-digit mod 11 = 10, the trailing 0 is used and
1010            // the full number's mod 11 is 10, not 0.
1011            let head: u64 = digits[..9].parse().ok()?;
1012            let tail = digits.chars().last()?.to_digit(10)?;
1013            if n.is_multiple_of(11) || (head % 11 == 10 && tail == 0) {
1014                Some(digits)
1015            } else {
1016                None
1017            }
1018        }
1019        _ => None,
1020    }
1021}
1022
1023/// Parse a Denmark CPR (*Centrale Workerregister*).
1024///
1025/// 10 digits `DDMMYYNNNN`. Format-only validation; the historical Modulus-11
1026/// check was abandoned in 2007.
1027///
1028/// ```
1029/// use worker_matcher::identifiers::parse_dk_cpr;
1030/// assert_eq!(parse_dk_cpr("1501801234"), Some("1501801234".to_string()));
1031/// assert_eq!(parse_dk_cpr("150180-1234"), Some("1501801234".to_string()));
1032/// assert_eq!(parse_dk_cpr("12345"), None);
1033/// ```
1034pub fn parse_dk_cpr(s: &str) -> Option<String> {
1035    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1036    if digits.len() == 10 {
1037        Some(digits)
1038    } else {
1039        None
1040    }
1041}
1042
1043/// Cascading Mod-11 check used by Estonia and Lithuania.
1044fn baltic_cascade_check(digits: &str) -> Option<u32> {
1045    const PASS1: [u32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1];
1046    const PASS2: [u32; 10] = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3];
1047    let body: Vec<u32> = digits
1048        .chars()
1049        .take(10)
1050        .filter_map(|c| c.to_digit(10))
1051        .collect();
1052    if body.len() != 10 {
1053        return None;
1054    }
1055    let s1: u32 = body.iter().zip(PASS1.iter()).map(|(d, w)| d * w).sum();
1056    let r1 = s1 % 11;
1057    if r1 < 10 {
1058        return Some(r1);
1059    }
1060    let s2: u32 = body.iter().zip(PASS2.iter()).map(|(d, w)| d * w).sum();
1061    let r2 = s2 % 11;
1062    if r2 < 10 { Some(r2) } else { Some(0) }
1063}
1064
1065/// Parse an Estonia *Isikukood* (Workeral Identification Code).
1066///
1067/// 11 digits `GYYMMDDNNNC`. Check digit uses a cascading Mod-11 algorithm.
1068///
1069/// ```
1070/// use worker_matcher::identifiers::parse_ee_ik;
1071/// assert_eq!(parse_ee_ik("48001150011"), Some("48001150011".to_string()));
1072/// assert_eq!(parse_ee_ik("48001150012"), None);
1073/// ```
1074pub fn parse_ee_ik(s: &str) -> Option<String> {
1075    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1076    if digits.len() != 11 {
1077        return None;
1078    }
1079    let expected = baltic_cascade_check(&digits[..10])?;
1080    if digits.chars().nth(10)?.to_digit(10)? == expected {
1081        Some(digits)
1082    } else {
1083        None
1084    }
1085}
1086
1087/// Parse a Spain DNI / NIE.
1088///
1089/// 8 digits (NIE: prefixed `X`/`Y`/`Z`) + 1 control letter. The letter is
1090/// `"TRWAGMYFPDXBNJZSQVHLCKE"` indexed by `number mod 23`. NIE prefixes map
1091/// to leading digits: `X→0`, `Y→1`, `Z→2`.
1092///
1093/// ```
1094/// use worker_matcher::identifiers::parse_es_dni;
1095/// assert_eq!(parse_es_dni("12345678Z"), Some("12345678Z".to_string()));
1096/// assert_eq!(parse_es_dni("12345678-Z"), Some("12345678Z".to_string()));
1097/// assert_eq!(parse_es_dni("12345678A"), None);  // wrong letter
1098/// ```
1099pub fn parse_es_dni(s: &str) -> Option<String> {
1100    let cleaned: String = s
1101        .chars()
1102        .filter(|c| c.is_ascii_alphanumeric())
1103        .collect::<String>()
1104        .to_uppercase();
1105    if cleaned.is_empty() {
1106        return None;
1107    }
1108    let last = cleaned.chars().last()?;
1109    if !last.is_ascii_alphabetic() {
1110        return None;
1111    }
1112    let body = &cleaned[..cleaned.len() - 1];
1113    let n: u64 = match body.chars().next()? {
1114        'X' => format!("0{}", &body[1..]).parse().ok()?,
1115        'Y' => format!("1{}", &body[1..]).parse().ok()?,
1116        'Z' => format!("2{}", &body[1..]).parse().ok()?,
1117        d if d.is_ascii_digit() => body.parse().ok()?,
1118        _ => return None,
1119    };
1120    const LETTERS: &[u8; 23] = b"TRWAGMYFPDXBNJZSQVHLCKE";
1121    let expected = LETTERS[(n % 23) as usize] as char;
1122    if last == expected {
1123        Some(cleaned)
1124    } else {
1125        None
1126    }
1127}
1128
1129/// Parse a Finland HETU (*Henkilötunnus*).
1130///
1131/// 11 characters `DDMMYYCZZZK` where `C` is a century sign (`-`/`+`/`A` and
1132/// later additions) and `K` is a check character from
1133/// `"0123456789ABCDEFHJKLMNPRSTUVWXY"` indexed by `(DDMMYYZZZ as 9-digit
1134/// number) mod 31`.
1135///
1136/// ```
1137/// use worker_matcher::identifiers::parse_fi_hetu;
1138/// assert_eq!(parse_fi_hetu("150180-999B"), Some("150180-999B".to_string()));
1139/// assert_eq!(parse_fi_hetu("150180-999C"), None);
1140/// ```
1141pub fn parse_fi_hetu(s: &str) -> Option<String> {
1142    let cleaned: String = s
1143        .chars()
1144        .filter(|c| !c.is_whitespace())
1145        .collect::<String>()
1146        .to_uppercase();
1147    if !cleaned.is_ascii() || cleaned.len() != 11 {
1148        return None;
1149    }
1150    let date: &str = &cleaned[..6];
1151    let sign = cleaned.chars().nth(6)?;
1152    let serial: &str = &cleaned[7..10];
1153    let check = cleaned.chars().nth(10)?;
1154    if !date.chars().all(|c| c.is_ascii_digit()) {
1155        return None;
1156    }
1157    if !serial.chars().all(|c| c.is_ascii_digit()) {
1158        return None;
1159    }
1160    // Accept the historically-known and FICORA-extended signs.
1161    if !matches!(
1162        sign,
1163        '-' | '+' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'X' | 'Y'
1164    ) {
1165        return None;
1166    }
1167    let n: u64 = format!("{}{}", date, serial).parse().ok()?;
1168    const TABLE: &[u8; 31] = b"0123456789ABCDEFHJKLMNPRSTUVWXY";
1169    let expected = TABLE[(n % 31) as usize] as char;
1170    if check == expected {
1171        Some(cleaned)
1172    } else {
1173        None
1174    }
1175}
1176
1177/// Parse a Croatia OIB (*Osobni identifikacijski broj*).
1178///
1179/// 11 digits. Check digit via ISO 7064 MOD 11,10.
1180///
1181/// ```
1182/// use worker_matcher::identifiers::parse_hr_oib;
1183/// assert_eq!(parse_hr_oib("12345678903"), Some("12345678903".to_string()));
1184/// assert_eq!(parse_hr_oib("12345678901"), None);
1185/// ```
1186pub fn parse_hr_oib(s: &str) -> Option<String> {
1187    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1188    if digits.len() != 11 {
1189        return None;
1190    }
1191    let mut acc: u32 = 10;
1192    for c in digits.chars().take(10) {
1193        let d = c.to_digit(10)?;
1194        let mut x = (d + acc) % 10;
1195        if x == 0 {
1196            x = 10;
1197        }
1198        acc = (x * 2) % 11;
1199    }
1200    let expected = (11 - acc) % 10;
1201    if digits.chars().nth(10)?.to_digit(10)? == expected {
1202        Some(digits)
1203    } else {
1204        None
1205    }
1206}
1207
1208/// Parse an Iceland *Kennitala*.
1209///
1210/// 10 digits `DDMMYYRRCN`. Check digit uses weights `[3,2,7,6,5,4,3,2]`
1211/// over the first 8 digits; mod 11 = 10 is invalid.
1212///
1213/// ```
1214/// use worker_matcher::identifiers::parse_is_kt;
1215/// assert_eq!(parse_is_kt("1501802529"), Some("1501802529".to_string()));
1216/// assert_eq!(parse_is_kt("1501802539"), None);  // wrong check digit
1217/// ```
1218pub fn parse_is_kt(s: &str) -> Option<String> {
1219    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1220    if digits.len() != 10 {
1221        return None;
1222    }
1223    const WEIGHTS: [u32; 8] = [3, 2, 7, 6, 5, 4, 3, 2];
1224    let mut sum: u32 = 0;
1225    for (i, c) in digits.chars().take(8).enumerate() {
1226        sum += c.to_digit(10)? * WEIGHTS[i];
1227    }
1228    let r = sum % 11;
1229    if r == 10 {
1230        return None;
1231    }
1232    let expected = (11 - r) % 11;
1233    if digits.chars().nth(8)?.to_digit(10)? == expected {
1234        Some(digits)
1235    } else {
1236        None
1237    }
1238}
1239
1240/// Parse a Lithuania *Asmens kodas*.
1241///
1242/// 11 digits `GYYMMDDNNNC` with the same cascading Mod-11 check as Estonia.
1243///
1244/// ```
1245/// use worker_matcher::identifiers::parse_lt_ak;
1246/// assert_eq!(parse_lt_ak("48001150011"), Some("48001150011".to_string()));
1247/// assert_eq!(parse_lt_ak("48001150012"), None);
1248/// ```
1249pub fn parse_lt_ak(s: &str) -> Option<String> {
1250    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1251    if digits.len() != 11 {
1252        return None;
1253    }
1254    let expected = baltic_cascade_check(&digits[..10])?;
1255    if digits.chars().nth(10)?.to_digit(10)? == expected {
1256        Some(digits)
1257    } else {
1258        None
1259    }
1260}
1261
1262/// Parse a Latvia *Workeras kods*.
1263///
1264/// 11 digits `DDMMYYCZZZK`. Check uses weights `[1,6,3,7,9,10,5,8,4,2]`
1265/// over the first 10 digits; `check = ((1101 − Σ) mod 11) mod 10`.
1266///
1267/// ```
1268/// use worker_matcher::identifiers::parse_lv_pk;
1269/// assert_eq!(parse_lv_pk("15018010007"), Some("15018010007".to_string()));
1270/// assert_eq!(parse_lv_pk("15018010008"), None);
1271/// ```
1272pub fn parse_lv_pk(s: &str) -> Option<String> {
1273    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1274    if digits.len() != 11 {
1275        return None;
1276    }
1277    const WEIGHTS: [i32; 10] = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
1278    let mut sum: i32 = 0;
1279    for (i, c) in digits.chars().take(10).enumerate() {
1280        sum += (c.to_digit(10)? as i32) * WEIGHTS[i];
1281    }
1282    let expected = ((1101 - sum).rem_euclid(11)) % 10;
1283    if digits.chars().nth(10)?.to_digit(10)? as i32 == expected {
1284        Some(digits)
1285    } else {
1286        None
1287    }
1288}
1289
1290/// Parse a Malta National ID.
1291///
1292/// 7 digits + 1 letter from `{M, G, A, P, L, H, B, Z}`. Format-only — the
1293/// suffix letter encodes geographic / registration provenance and is not a
1294/// check digit.
1295///
1296/// ```
1297/// use worker_matcher::identifiers::parse_mt_id;
1298/// assert_eq!(parse_mt_id("1234567M"), Some("1234567M".to_string()));
1299/// assert_eq!(parse_mt_id("1234567X"), None);  // X not in valid letter set
1300/// ```
1301pub fn parse_mt_id(s: &str) -> Option<String> {
1302    let cleaned: String = s
1303        .chars()
1304        .filter(|c| c.is_ascii_alphanumeric())
1305        .collect::<String>()
1306        .to_uppercase();
1307    if cleaned.len() != 8 {
1308        return None;
1309    }
1310    let last = cleaned.chars().last()?;
1311    if !matches!(last, 'M' | 'G' | 'A' | 'P' | 'L' | 'H' | 'B' | 'Z') {
1312        return None;
1313    }
1314    if !cleaned[..7].chars().all(|c| c.is_ascii_digit()) {
1315        return None;
1316    }
1317    Some(cleaned)
1318}
1319
1320/// Parse a Norway *Fødselsnummer*.
1321///
1322/// 11 digits with two Mod-11 check digits. Check 1 weights:
1323/// `[3,7,6,1,8,9,4,5,2]` over the first 9 digits. Check 2 weights:
1324/// `[5,4,3,2,7,6,5,4,3,2]` over the first 10. mod 11 = 10 is invalid.
1325///
1326/// ```
1327/// use worker_matcher::identifiers::parse_no_fnr;
1328/// assert_eq!(parse_no_fnr("15018012399"), Some("15018012399".to_string()));
1329/// assert_eq!(parse_no_fnr("15018012390"), None);
1330/// ```
1331pub fn parse_no_fnr(s: &str) -> Option<String> {
1332    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1333    if digits.len() != 11 {
1334        return None;
1335    }
1336    const W1: [u32; 9] = [3, 7, 6, 1, 8, 9, 4, 5, 2];
1337    const W2: [u32; 10] = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
1338    let body: Vec<u32> = digits.chars().filter_map(|c| c.to_digit(10)).collect();
1339    if body.len() != 11 {
1340        return None;
1341    }
1342    let s1: u32 = body.iter().take(9).zip(W1.iter()).map(|(d, w)| d * w).sum();
1343    let r1 = s1 % 11;
1344    if r1 == 10 {
1345        return None;
1346    }
1347    let c1 = (11 - r1) % 11;
1348    if c1 != body[9] {
1349        return None;
1350    }
1351    let s2: u32 = body
1352        .iter()
1353        .take(10)
1354        .zip(W2.iter())
1355        .map(|(d, w)| d * w)
1356        .sum();
1357    let r2 = s2 % 11;
1358    if r2 == 10 {
1359        return None;
1360    }
1361    let c2 = (11 - r2) % 11;
1362    if c2 != body[10] {
1363        return None;
1364    }
1365    Some(digits)
1366}
1367
1368/// Parse a Poland PESEL.
1369///
1370/// 11 digits `YYMMDDZZZZK` with century-encoded month. Check uses weights
1371/// `[1,3,7,9,1,3,7,9,1,3]` over the first 10 digits;
1372/// `check = (10 − (Σ mod 10)) mod 10`.
1373///
1374/// ```
1375/// use worker_matcher::identifiers::parse_pl_pesel;
1376/// assert_eq!(parse_pl_pesel("80011500014"), Some("80011500014".to_string()));
1377/// assert_eq!(parse_pl_pesel("80011500015"), None);
1378/// ```
1379pub fn parse_pl_pesel(s: &str) -> Option<String> {
1380    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1381    if digits.len() != 11 {
1382        return None;
1383    }
1384    const WEIGHTS: [u32; 10] = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
1385    let mut sum: u32 = 0;
1386    for (i, c) in digits.chars().take(10).enumerate() {
1387        sum += c.to_digit(10)? * WEIGHTS[i];
1388    }
1389    let expected = (10 - (sum % 10)) % 10;
1390    if digits.chars().nth(10)?.to_digit(10)? == expected {
1391        Some(digits)
1392    } else {
1393        None
1394    }
1395}
1396
1397/// Parse a Romania CNP (*Cod Numeric Workeral*).
1398///
1399/// 13 digits `SYYMMDDJJNNNK`. Check uses weights "279146358279" (`[2,7,9,1,
1400/// 4,6,3,5,8,2,7,9]`) over the first 12 digits; `r = Σ mod 11`; check is
1401/// `1` if `r == 10`, else `r`.
1402///
1403/// ```
1404/// use worker_matcher::identifiers::parse_ro_cnp;
1405/// assert_eq!(parse_ro_cnp("1800115400012"), Some("1800115400012".to_string()));
1406/// assert_eq!(parse_ro_cnp("1800115400015"), None);
1407/// ```
1408pub fn parse_ro_cnp(s: &str) -> Option<String> {
1409    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1410    if digits.len() != 13 {
1411        return None;
1412    }
1413    const WEIGHTS: [u32; 12] = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9];
1414    let mut sum: u32 = 0;
1415    for (i, c) in digits.chars().take(12).enumerate() {
1416        sum += c.to_digit(10)? * WEIGHTS[i];
1417    }
1418    let r = sum % 11;
1419    let expected = if r == 10 { 1 } else { r };
1420    if digits.chars().nth(12)?.to_digit(10)? == expected {
1421        Some(digits)
1422    } else {
1423        None
1424    }
1425}
1426
1427/// Parse a Slovenia EMŠO (*Enotna Matična Številka Občana*).
1428///
1429/// 13 digits `DDMMYYYRRGGGK`. Check uses weights `[7,6,5,4,3,2,7,6,5,4,3,2]`
1430/// over the first 12 digits; `r = Σ mod 11`; check is `0` if `r == 0`,
1431/// else `11 − r` (rejected if 10).
1432///
1433/// ```
1434/// use worker_matcher::identifiers::parse_si_emso;
1435/// assert_eq!(parse_si_emso("1501980500015"), Some("1501980500015".to_string()));
1436/// assert_eq!(parse_si_emso("1501980500014"), None);
1437/// ```
1438pub fn parse_si_emso(s: &str) -> Option<String> {
1439    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1440    if digits.len() != 13 {
1441        return None;
1442    }
1443    const WEIGHTS: [u32; 12] = [7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
1444    let mut sum: u32 = 0;
1445    for (i, c) in digits.chars().take(12).enumerate() {
1446        sum += c.to_digit(10)? * WEIGHTS[i];
1447    }
1448    let r = sum % 11;
1449    let expected = if r == 0 { 0 } else { 11 - r };
1450    if expected == 10 {
1451        return None;
1452    }
1453    if digits.chars().nth(12)?.to_digit(10)? == expected {
1454        Some(digits)
1455    } else {
1456        None
1457    }
1458}
1459
1460/// Parse a Slovakia *Rodné číslo*. Same algorithm as Czech RČ.
1461///
1462/// ```
1463/// use worker_matcher::identifiers::parse_sk_rc;
1464/// assert_eq!(parse_sk_rc("8051150019"), Some("8051150019".to_string()));
1465/// assert_eq!(parse_sk_rc("8051150010"), None);
1466/// ```
1467pub fn parse_sk_rc(s: &str) -> Option<String> {
1468    parse_cz_rc(s)
1469}
1470
1471/// Parse a United Kingdom National Insurance Number (NINO).
1472///
1473/// Format `AA999999A`: 2 prefix letters + 6 digits + 1 suffix letter.
1474/// Banned first prefix letters: `D F I Q U V`.
1475/// Banned second prefix letters: `D F I O Q U V`.
1476/// Banned admin prefixes: `OO CR FY MW NC PP PZ TN`.
1477/// Suffix MUST be one of `A B C D`. Format-only; no checksum.
1478///
1479/// ```
1480/// use worker_matcher::identifiers::parse_uk_nino;
1481/// assert_eq!(parse_uk_nino("AB123456A"), Some("AB123456A".to_string()));
1482/// assert_eq!(parse_uk_nino("ab 12 34 56 a"), Some("AB123456A".to_string()));
1483/// assert_eq!(parse_uk_nino("DA123456A"), None);  // banned first letter
1484/// assert_eq!(parse_uk_nino("ABCDEFGHI"), None);
1485/// ```
1486pub fn parse_uk_nino(s: &str) -> Option<String> {
1487    let cleaned: String = s
1488        .chars()
1489        .filter(|c| c.is_ascii_alphanumeric())
1490        .collect::<String>()
1491        .to_uppercase();
1492    if cleaned.len() != 9 {
1493        return None;
1494    }
1495    let bytes = cleaned.as_bytes();
1496    let p1 = bytes[0] as char;
1497    let p2 = bytes[1] as char;
1498    if !p1.is_ascii_alphabetic() || !p2.is_ascii_alphabetic() {
1499        return None;
1500    }
1501    if matches!(p1, 'D' | 'F' | 'I' | 'Q' | 'U' | 'V') {
1502        return None;
1503    }
1504    if matches!(p2, 'D' | 'F' | 'I' | 'O' | 'Q' | 'U' | 'V') {
1505        return None;
1506    }
1507    let prefix = &cleaned[..2];
1508    if matches!(
1509        prefix,
1510        "OO" | "CR" | "FY" | "MW" | "NC" | "PP" | "PZ" | "TN"
1511    ) {
1512        return None;
1513    }
1514    if !cleaned[2..8].chars().all(|c| c.is_ascii_digit()) {
1515        return None;
1516    }
1517    let suffix = bytes[8] as char;
1518    if !matches!(suffix, 'A' | 'B' | 'C' | 'D') {
1519        return None;
1520    }
1521    Some(cleaned)
1522}
1523
1524// ----------------------------------------------------------------------------
1525// Five additional workeral national identifiers (T-28).
1526// ----------------------------------------------------------------------------
1527
1528/// Parse a Greece DSS (Dematerialised Securities System) investor share code.
1529///
1530/// 10-digit investor identifier issued by the Hellenic Central Securities
1531/// Depository (ATHEXCSD). Format-only validation: 10 ASCII digits.
1532///
1533/// ```
1534/// use worker_matcher::identifiers::parse_gr_dss;
1535/// assert_eq!(parse_gr_dss("1234567890"), Some("1234567890".to_string()));
1536/// assert_eq!(parse_gr_dss("12345"), None);
1537/// assert_eq!(parse_gr_dss("ABCDEFGHIJ"), None);
1538/// ```
1539pub fn parse_gr_dss(s: &str) -> Option<String> {
1540    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1541    if digits.len() == 10 {
1542        Some(digits)
1543    } else {
1544        None
1545    }
1546}
1547
1548/// Parse a Liechtenstein National Identity Card Number.
1549///
1550/// Combination of 2 letters and 8 digits (the example in the spec
1551/// `ID022143586` shows a 9-digit run, so the parser accepts 8 *or* 9
1552/// trailing digits — total length 10 or 11). Note: per Liechtenstein
1553/// practice the number is **regenerated on each renewal**, so for
1554/// cross-renewal matching consumers should prefer
1555/// [`crate::PassportBook`] with `country = "LI"`.
1556///
1557/// ```
1558/// use worker_matcher::identifiers::parse_li_id;
1559/// assert_eq!(parse_li_id("ID12345678"), Some("ID12345678".to_string()));
1560/// assert_eq!(parse_li_id("ID022143586"), Some("ID022143586".to_string()));
1561/// assert_eq!(parse_li_id("12 34 56 78"), None);  // missing letters
1562/// assert_eq!(parse_li_id(""), None);
1563/// ```
1564pub fn parse_li_id(s: &str) -> Option<String> {
1565    let cleaned: String = s
1566        .chars()
1567        .filter(|c| c.is_ascii_alphanumeric())
1568        .collect::<String>()
1569        .to_uppercase();
1570    if !(10..=11).contains(&cleaned.len()) {
1571        return None;
1572    }
1573    let chars: Vec<char> = cleaned.chars().collect();
1574    if !chars[0].is_ascii_alphabetic() || !chars[1].is_ascii_alphabetic() {
1575        return None;
1576    }
1577    if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
1578        return None;
1579    }
1580    Some(cleaned)
1581}
1582
1583/// Parse a Netherlands National Identity Card Number.
1584///
1585/// 9 characters: positions 1 and 2 are uppercase letters `[A-Z]` except
1586/// `O`; positions 3–8 are alphanumeric `[A-Z0-9]` except `O`; position 9
1587/// is a digit `[0-9]`. The character `O` is disallowed (to avoid
1588/// confusion with `0`), but the digit `0` is allowed.
1589///
1590/// ```
1591/// use worker_matcher::identifiers::parse_nl_id;
1592/// assert_eq!(parse_nl_id("AB1234567"), Some("AB1234567".to_string()));
1593/// assert_eq!(parse_nl_id("ab 12 34 567"), Some("AB1234567".to_string()));
1594/// assert_eq!(parse_nl_id("AO1234567"), None);   // O is banned
1595/// assert_eq!(parse_nl_id("AB12345AB"), None);   // last must be digit
1596/// assert_eq!(parse_nl_id("12345AB67"), None);   // leading must be letters
1597/// ```
1598pub fn parse_nl_id(s: &str) -> Option<String> {
1599    let cleaned: String = s
1600        .chars()
1601        .filter(|c| c.is_ascii_alphanumeric())
1602        .collect::<String>()
1603        .to_uppercase();
1604    if cleaned.len() != 9 {
1605        return None;
1606    }
1607    let chars: Vec<char> = cleaned.chars().collect();
1608    for c in chars.iter().take(2) {
1609        if !c.is_ascii_alphabetic() || *c == 'O' {
1610            return None;
1611        }
1612    }
1613    for c in chars.iter().take(8).skip(2) {
1614        if !c.is_ascii_alphanumeric() || *c == 'O' {
1615            return None;
1616        }
1617    }
1618    if !chars[8].is_ascii_digit() {
1619        return None;
1620    }
1621    Some(cleaned)
1622}
1623
1624/// Parse a Poland NIP (*Numer Identyfikacji Podatkowej*).
1625///
1626/// 10 digits with a weighted Mod-11 check. Weights for the first 9
1627/// digits: `[6, 5, 7, 2, 3, 4, 5, 6, 7]`. `r = Σ mod 11`; a remainder
1628/// of 10 indicates an invalid NIP; otherwise the 10th digit MUST equal
1629/// `r`.
1630///
1631/// ```
1632/// use worker_matcher::identifiers::parse_pl_nip;
1633/// assert_eq!(parse_pl_nip("1234567802"), Some("1234567802".to_string()));
1634/// assert_eq!(parse_pl_nip("123-456-78-02"), Some("1234567802".to_string()));
1635/// assert_eq!(parse_pl_nip("1234567803"), None);    // wrong check
1636/// assert_eq!(parse_pl_nip("1234567890"), None);    // r = 10 — invalid by spec
1637/// ```
1638pub fn parse_pl_nip(s: &str) -> Option<String> {
1639    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1640    if digits.len() != 10 {
1641        return None;
1642    }
1643    const WEIGHTS: [u32; 9] = [6, 5, 7, 2, 3, 4, 5, 6, 7];
1644    let mut sum: u32 = 0;
1645    for (i, c) in digits.chars().take(9).enumerate() {
1646        sum += c.to_digit(10)? * WEIGHTS[i];
1647    }
1648    let r = sum % 11;
1649    if r == 10 {
1650        return None;
1651    }
1652    if digits.chars().nth(9)?.to_digit(10)? == r {
1653        Some(digits)
1654    } else {
1655        None
1656    }
1657}
1658
1659/// Parse a Portugal NIF (*Número de Identificação Fiscal*).
1660///
1661/// 9 digits with a weighted Mod-11 check. Weights for the first 8
1662/// digits: `[9, 8, 7, 6, 5, 4, 3, 2]`. `r = Σ mod 11`; check is `0` if
1663/// `r < 2`, else `11 − r`. The 9th digit MUST equal the check.
1664///
1665/// ```
1666/// use worker_matcher::identifiers::parse_pt_nif;
1667/// assert_eq!(parse_pt_nif("123456789"), Some("123456789".to_string()));
1668/// assert_eq!(parse_pt_nif("123 456 789"), Some("123456789".to_string()));
1669/// assert_eq!(parse_pt_nif("123456780"), None);
1670/// ```
1671pub fn parse_pt_nif(s: &str) -> Option<String> {
1672    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1673    if digits.len() != 9 {
1674        return None;
1675    }
1676    const WEIGHTS: [u32; 8] = [9, 8, 7, 6, 5, 4, 3, 2];
1677    let mut sum: u32 = 0;
1678    for (i, c) in digits.chars().take(8).enumerate() {
1679        sum += c.to_digit(10)? * WEIGHTS[i];
1680    }
1681    let r = sum % 11;
1682    let expected = if r < 2 { 0 } else { 11 - r };
1683    if digits.chars().nth(8)?.to_digit(10)? == expected {
1684        Some(digits)
1685    } else {
1686        None
1687    }
1688}
1689
1690// ----------------------------------------------------------------------------
1691// T-17.1 — seven next-batch national identifier schemes.
1692//
1693// Per the T-17 spike (§21.4 / §23.2): one parser per jurisdiction the
1694// crate already supports phones for but not national IDs.
1695// ----------------------------------------------------------------------------
1696
1697/// Parse a Brazil CPF (*Cadastro de Pessoas Físicas*).
1698///
1699/// The CPF is 11 digits, often formatted `NNN.NNN.NNN-DD`. The last two
1700/// digits are computed check digits using weighted Mod-11. The parser
1701/// strips non-digits, requires exactly 11 digits, rejects all-equal
1702/// sequences (the canonical sentinel / test vectors a real CPF MUST
1703/// NOT take), and validates both check digits.
1704///
1705/// ```
1706/// use worker_matcher::identifiers::parse_br_cpf;
1707/// assert_eq!(parse_br_cpf("123.456.789-09"), Some("12345678909".to_string()));
1708/// assert_eq!(parse_br_cpf("12345678909"),    Some("12345678909".to_string()));
1709/// assert_eq!(parse_br_cpf("12345678900"),    None);             // wrong D2
1710/// assert_eq!(parse_br_cpf("11111111111"),    None);             // all-equal sentinel
1711/// assert_eq!(parse_br_cpf("1234567890"),     None);             // too short
1712/// ```
1713pub fn parse_br_cpf(s: &str) -> Option<String> {
1714    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1715    if digits.len() != 11 {
1716        return None;
1717    }
1718    let bytes = digits.as_bytes();
1719    if bytes.iter().all(|&b| b == bytes[0]) {
1720        return None;
1721    }
1722    let d = |i: usize| (bytes[i] - b'0') as u32;
1723    let mut sum1: u32 = 0;
1724    for i in 0..9 {
1725        sum1 += d(i) * (10 - i as u32);
1726    }
1727    let r1 = sum1 % 11;
1728    let exp1 = if r1 < 2 { 0 } else { 11 - r1 };
1729    if d(9) != exp1 {
1730        return None;
1731    }
1732    let mut sum2: u32 = 0;
1733    for i in 0..10 {
1734        sum2 += d(i) * (11 - i as u32);
1735    }
1736    let r2 = sum2 % 11;
1737    let exp2 = if r2 < 2 { 0 } else { 11 - r2 };
1738    if d(10) != exp2 {
1739        return None;
1740    }
1741    Some(digits)
1742}
1743
1744/// Parse a China Resident Identity Card number (*居民身份证*, 18-digit
1745/// 1999 reform).
1746///
1747/// 18 characters: 17 digits + a check character (digit or `X`). The
1748/// substring at positions 6..14 encodes the date of birth (`YYYYMMDD`)
1749/// and MUST be a valid calendar date. The check character is computed
1750/// from a weighted Mod-11 sum over the 17 leading digits with the
1751/// lookup `1,0,X,9,8,7,6,5,4,3,2`. Lowercase `x` is accepted and
1752/// canonicalised to uppercase. The 15-digit pre-1999 form is NOT
1753/// accepted; consumers MUST migrate to the 18-digit form before
1754/// matching (the conversion is well-documented but jurisdiction-locked
1755/// and out of scope for this parser).
1756///
1757/// ```
1758/// use worker_matcher::identifiers::parse_cn_rrn;
1759/// assert_eq!(
1760///     parse_cn_rrn("11010519491231002X"),
1761///     Some("11010519491231002X".to_string()),
1762/// );
1763/// assert_eq!(
1764///     parse_cn_rrn("11010519491231002x"),
1765///     Some("11010519491231002X".to_string()),
1766/// );
1767/// assert_eq!(parse_cn_rrn("11010519491231002Y"), None);          // wrong check
1768/// assert_eq!(parse_cn_rrn("11010513491231002X"), None);          // invalid month
1769/// assert_eq!(parse_cn_rrn("110105194912310020"), None);          // wrong check
1770/// assert_eq!(parse_cn_rrn("11010519491231"),     None);          // too short
1771/// ```
1772pub fn parse_cn_rrn(s: &str) -> Option<String> {
1773    let cleaned: String = s
1774        .chars()
1775        .filter(|c| c.is_ascii_alphanumeric())
1776        .map(|c| c.to_ascii_uppercase())
1777        .collect();
1778    if cleaned.len() != 18 {
1779        return None;
1780    }
1781    let bytes = cleaned.as_bytes();
1782    for &b in &bytes[..17] {
1783        if !b.is_ascii_digit() {
1784            return None;
1785        }
1786    }
1787    if !bytes[17].is_ascii_digit() && bytes[17] != b'X' {
1788        return None;
1789    }
1790    let yyyy: i32 = cleaned[6..10].parse().ok()?;
1791    let mm: u32 = cleaned[10..12].parse().ok()?;
1792    let dd: u32 = cleaned[12..14].parse().ok()?;
1793    chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
1794    const WEIGHTS: [u32; 17] = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
1795    const CHECK: [u8; 11] = [
1796        b'1', b'0', b'X', b'9', b'8', b'7', b'6', b'5', b'4', b'3', b'2',
1797    ];
1798    let mut sum: u32 = 0;
1799    for i in 0..17 {
1800        sum += u32::from(bytes[i] - b'0') * WEIGHTS[i];
1801    }
1802    if bytes[17] != CHECK[(sum % 11) as usize] {
1803        return None;
1804    }
1805    Some(cleaned)
1806}
1807
1808/// Parse an India Aadhaar number (12 digits, Verhoeff check digit).
1809///
1810/// The Verhoeff algorithm uses two precomputed lookup tables (the
1811/// dihedral-group multiplication table `D` and the permutation table
1812/// `P`) and runs in linear time over the input. The parser strips
1813/// non-digits, requires exactly 12 digits, rejects all-equal sequences
1814/// and the UIDAI-test-prefix ranges (numbers beginning with `0` or
1815/// `1`, which UIDAI guidance reserves and never issues to real
1816/// citizens), and validates the Verhoeff check digit at the rightmost
1817/// position.
1818///
1819/// ```
1820/// use worker_matcher::identifiers::parse_in_aadhaar;
1821/// assert_eq!(parse_in_aadhaar("234123412346"),   Some("234123412346".to_string()));
1822/// assert_eq!(parse_in_aadhaar("2341 2341 2346"), Some("234123412346".to_string()));
1823/// assert_eq!(parse_in_aadhaar("234123412347"),   None);  // wrong check
1824/// assert_eq!(parse_in_aadhaar("234123412"),      None);  // too short
1825/// assert_eq!(parse_in_aadhaar("222222222222"),   None);  // all-equal sentinel
1826/// assert_eq!(parse_in_aadhaar("034123412346"),   None);  // reserved prefix
1827/// ```
1828pub fn parse_in_aadhaar(s: &str) -> Option<String> {
1829    const VERHOEFF_D: [[u8; 10]; 10] = [
1830        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
1831        [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
1832        [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
1833        [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
1834        [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
1835        [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
1836        [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
1837        [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
1838        [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
1839        [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
1840    ];
1841    const VERHOEFF_P: [[u8; 10]; 8] = [
1842        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
1843        [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
1844        [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
1845        [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
1846        [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
1847        [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
1848        [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
1849        [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
1850    ];
1851    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1852    if digits.len() != 12 {
1853        return None;
1854    }
1855    let bytes = digits.as_bytes();
1856    if bytes.iter().all(|&b| b == bytes[0]) {
1857        return None;
1858    }
1859    if bytes[0] == b'0' || bytes[0] == b'1' {
1860        return None;
1861    }
1862    let mut c: u8 = 0;
1863    for i in 0..12 {
1864        let d = bytes[11 - i] - b'0';
1865        c = VERHOEFF_D[c as usize][VERHOEFF_P[i % 8][d as usize] as usize];
1866    }
1867    if c == 0 { Some(digits) } else { None }
1868}
1869
1870/// Parse a Japan My Number (個人番号, 12 digits).
1871///
1872/// The check digit is computed by a weighted Mod-11 sum over the
1873/// first 11 digits using the weights `[6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2]`
1874/// (per the Japanese e-Gov Cabinet Order specification). If the
1875/// remainder is `< 2`, the check digit is `0`; otherwise it is
1876/// `11 - remainder`.
1877///
1878/// ```
1879/// use worker_matcher::identifiers::parse_jp_my_number;
1880/// assert_eq!(parse_jp_my_number("123456789018"),   Some("123456789018".to_string()));
1881/// assert_eq!(parse_jp_my_number("1234 5678 9018"), Some("123456789018".to_string()));
1882/// assert_eq!(parse_jp_my_number("123456789010"),   None);  // wrong check
1883/// assert_eq!(parse_jp_my_number("12345678901"),    None);  // too short
1884/// ```
1885pub fn parse_jp_my_number(s: &str) -> Option<String> {
1886    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
1887    if digits.len() != 12 {
1888        return None;
1889    }
1890    let bytes = digits.as_bytes();
1891    const WEIGHTS: [u32; 11] = [6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
1892    let mut sum: u32 = 0;
1893    for i in 0..11 {
1894        sum += u32::from(bytes[i] - b'0') * WEIGHTS[i];
1895    }
1896    let r = sum % 11;
1897    let expected = if r < 2 { 0 } else { 11 - r };
1898    if u32::from(bytes[11] - b'0') != expected {
1899        return None;
1900    }
1901    Some(digits)
1902}
1903
1904/// Parse a Mexico CURP (*Clave Única de Registro de Población*).
1905///
1906/// 18 characters with rich internal structure: 4 letters (surname /
1907/// given-name initials), 6 digits (`YYMMDD`), 1 sex char (`H` or `M`),
1908/// 2 letters (state code), 3 letters (internal consonants), 1
1909/// alphanumeric (homonym discriminator), 1 check digit. The parser
1910/// uppercases, validates the structural shape, verifies the embedded
1911/// date of birth is a valid calendar date (century inferred per the
1912/// usual Mexican convention: `YY <= 29 → 20YY`, else `19YY`), and
1913/// validates the Mod-10 weighted check digit using the standard CURP
1914/// value table (`0..9` literal, `A..N` = 10..23, `Ñ` = 24,
1915/// `O..Z` = 25..36).
1916///
1917/// Ñ in the input is accepted; non-ASCII characters other than Ñ are
1918/// rejected.
1919///
1920/// ```
1921/// use worker_matcher::identifiers::parse_mx_curp;
1922/// assert_eq!(
1923///     parse_mx_curp("HEGG560427MVZRRL04"),
1924///     Some("HEGG560427MVZRRL04".to_string()),
1925/// );
1926/// assert_eq!(
1927///     parse_mx_curp("hegg560427mvzrrl04"),
1928///     Some("HEGG560427MVZRRL04".to_string()),
1929/// );
1930/// assert_eq!(parse_mx_curp("HEGG560427MVZRRL05"), None);   // wrong check
1931/// assert_eq!(parse_mx_curp("HEGG561327MVZRRL04"), None);   // invalid month
1932/// assert_eq!(parse_mx_curp("HEGG560427"),         None);   // too short
1933/// ```
1934pub fn parse_mx_curp(s: &str) -> Option<String> {
1935    let cleaned: String = s
1936        .chars()
1937        .filter(|c| !c.is_whitespace())
1938        .map(|c| c.to_uppercase().next().unwrap_or(c))
1939        .collect();
1940    if cleaned.chars().count() != 18 {
1941        return None;
1942    }
1943    let chars: Vec<char> = cleaned.chars().collect();
1944    let is_letter_or_n_tilde = |c: char| c.is_ascii_uppercase() || c == 'Ñ';
1945    if !chars[..4].iter().copied().all(is_letter_or_n_tilde) {
1946        return None;
1947    }
1948    if !chars[4..10].iter().all(|c| c.is_ascii_digit()) {
1949        return None;
1950    }
1951    if chars[10] != 'H' && chars[10] != 'M' {
1952        return None;
1953    }
1954    if !chars[11..16].iter().copied().all(is_letter_or_n_tilde) {
1955        return None;
1956    }
1957    if !chars[16].is_ascii_alphanumeric() && chars[16] != 'Ñ' {
1958        return None;
1959    }
1960    if !chars[17].is_ascii_digit() {
1961        return None;
1962    }
1963    let yy: i32 = cleaned[4..6].parse().ok()?;
1964    let mm: u32 = cleaned[6..8].parse().ok()?;
1965    let dd: u32 = cleaned[8..10].parse().ok()?;
1966    let yyyy = if yy <= 29 { 2000 + yy } else { 1900 + yy };
1967    chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
1968    let value = |c: char| -> Option<u32> {
1969        Some(match c {
1970            '0'..='9' => (c as u32) - ('0' as u32),
1971            'A'..='N' => 10 + ((c as u32) - ('A' as u32)),
1972            'Ñ' => 24,
1973            'O'..='Z' => 25 + ((c as u32) - ('O' as u32)),
1974            _ => return None,
1975        })
1976    };
1977    let mut sum: u32 = 0;
1978    for (i, &c) in chars.iter().enumerate().take(17) {
1979        sum += value(c)? * (18 - i as u32);
1980    }
1981    let expected = (10 - (sum % 10)) % 10;
1982    if u32::from(chars[17] as u8 - b'0') != expected {
1983        return None;
1984    }
1985    Some(cleaned)
1986}
1987
1988/// Parse a New Zealand NHI Number (original 7-character format:
1989/// 3 letters + 4 digits, where the last digit is a Mod-11 check).
1990///
1991/// The letter values are: `A..Z` minus `I` and `O` (excluded because
1992/// they collide visually with `1` and `0`), assigned consecutively:
1993/// `A=1, B=2, C=3, D=4, E=5, F=6, G=7, H=8, J=9, K=10, L=11, M=12,
1994/// N=13, P=14, Q=15, R=16, S=17, T=18, U=19, V=20, W=21, X=22, Y=23,
1995/// Z=24`. The weighted sum applies weights `[7, 6, 5, 4, 3, 2]` to
1996/// the first six positions (3 letters + 3 digits); the remainder mod
1997/// 11 yields the expected check digit (`0` if remainder is `0`,
1998/// otherwise `11 - remainder`; if the result is `10` the NHI is
1999/// invalid because no single decimal digit can encode `10`).
2000///
2001/// The 2019 7-character alphanumeric NHI revision (3 letters + 2
2002/// digits + 2 letters) uses a different algorithm and is **not**
2003/// supported by this parser; calls fall through to `None` for the new
2004/// format. Consumers handling 2019-format NHIs SHOULD validate
2005/// upstream and pass the value through as a third-party identifier.
2006///
2007/// ```
2008/// use worker_matcher::identifiers::parse_nz_nhi;
2009/// assert_eq!(parse_nz_nhi("ZAA0083"), Some("ZAA0083".to_string()));
2010/// assert_eq!(parse_nz_nhi("zaa0083"), Some("ZAA0083".to_string()));
2011/// assert_eq!(parse_nz_nhi("ZAA0082"), None);          // wrong check
2012/// assert_eq!(parse_nz_nhi("ZAI0083"), None);          // I excluded
2013/// assert_eq!(parse_nz_nhi("ZAA008"),  None);          // too short
2014/// ```
2015pub fn parse_nz_nhi(s: &str) -> Option<String> {
2016    let cleaned: String = s
2017        .chars()
2018        .filter(|c| c.is_ascii_alphanumeric())
2019        .map(|c| c.to_ascii_uppercase())
2020        .collect();
2021    if cleaned.len() != 7 {
2022        return None;
2023    }
2024    let bytes = cleaned.as_bytes();
2025    for &b in &bytes[..3] {
2026        if !b.is_ascii_uppercase() || b == b'I' || b == b'O' {
2027            return None;
2028        }
2029    }
2030    for &b in &bytes[3..] {
2031        if !b.is_ascii_digit() {
2032            return None;
2033        }
2034    }
2035    let letter_value = |b: u8| -> u32 {
2036        let idx = u32::from(b - b'A') + 1;
2037        if b > b'O' {
2038            idx - 2
2039        } else if b > b'I' {
2040            idx - 1
2041        } else {
2042            idx
2043        }
2044    };
2045    const WEIGHTS: [u32; 6] = [7, 6, 5, 4, 3, 2];
2046    let mut sum: u32 = 0;
2047    for i in 0..3 {
2048        sum += letter_value(bytes[i]) * WEIGHTS[i];
2049    }
2050    for i in 0..3 {
2051        sum += u32::from(bytes[3 + i] - b'0') * WEIGHTS[3 + i];
2052    }
2053    let r = sum % 11;
2054    if r == 1 {
2055        return None;
2056    }
2057    let expected = if r == 0 { 0 } else { 11 - r };
2058    if u32::from(bytes[6] - b'0') != expected {
2059        return None;
2060    }
2061    Some(cleaned)
2062}
2063
2064/// Parse a South Africa ID Number (13 digits, Luhn check digit + a
2065/// date-of-birth substring at positions 0..6).
2066///
2067/// The first 6 digits encode `YYMMDD`; the century is conventionally
2068/// inferred (`YY <= 29 → 20YY`, else `19YY`). The check digit at
2069/// position 12 is computed by the standard Luhn algorithm over all
2070/// 13 digits.
2071///
2072/// The remaining substrings (sequence at positions 6..10, citizenship
2073/// at position 10, and the legacy race indicator at position 11) are
2074/// intentionally NOT validated by this parser — they are demographic
2075/// information the worker-matcher layer does not use.
2076///
2077/// ```
2078/// use worker_matcher::identifiers::parse_za_id;
2079/// assert_eq!(parse_za_id("8001015009087"),   Some("8001015009087".to_string()));
2080/// assert_eq!(parse_za_id("800101 5009 087"), Some("8001015009087".to_string()));
2081/// assert_eq!(parse_za_id("8001015009088"),   None);  // wrong Luhn
2082/// assert_eq!(parse_za_id("8013015009087"),   None);  // invalid month
2083/// assert_eq!(parse_za_id("80010150090"),     None);  // too short
2084/// ```
2085pub fn parse_za_id(s: &str) -> Option<String> {
2086    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
2087    if digits.len() != 13 {
2088        return None;
2089    }
2090    let yy: i32 = digits[0..2].parse().ok()?;
2091    let mm: u32 = digits[2..4].parse().ok()?;
2092    let dd: u32 = digits[4..6].parse().ok()?;
2093    let yyyy = if yy <= 29 { 2000 + yy } else { 1900 + yy };
2094    chrono::NaiveDate::from_ymd_opt(yyyy, mm, dd)?;
2095    let bytes = digits.as_bytes();
2096    let mut sum: u32 = 0;
2097    // Standard Luhn: process right-to-left, double every second digit
2098    // starting from the second-to-last (i.e. positions 1, 3, 5, … from
2099    // the right). For a 13-digit ID this doubles positions 11, 9, 7,
2100    // 5, 3, 1 (counting from the left, 0-indexed).
2101    for i in 0..13 {
2102        let mut d = u32::from(bytes[12 - i] - b'0');
2103        if i % 2 == 1 {
2104            d *= 2;
2105            if d > 9 {
2106                d -= 9;
2107            }
2108        }
2109        sum += d;
2110    }
2111    if !sum.is_multiple_of(10) {
2112        return None;
2113    }
2114    Some(digits)
2115}
2116
2117// ----------------------------------------------------------------------------
2118// Nine per-country passport-number format validators (T-28).
2119//
2120// These are pure format validators that consumers may call before
2121// constructing a `PassportBook`. They do NOT correspond to `Worker`
2122// fields — passport-book numbers belong to the `PassportBook` model
2123// because they change with each renewal and a worker may carry
2124// passports from multiple countries simultaneously.
2125// ----------------------------------------------------------------------------
2126
2127/// Parse a Cyprus passport number.
2128///
2129/// Pre-2010 passports: letter `E` + 6 digits (e.g. `E123456`).
2130/// Biometric passports issued from 13 December 2010 onwards: letter `K`
2131/// + 8 digits (e.g. `K12345678`).
2132///
2133/// ```
2134/// use worker_matcher::identifiers::parse_cy_passport;
2135/// assert_eq!(parse_cy_passport("E123456"),   Some("E123456".to_string()));
2136/// assert_eq!(parse_cy_passport("k12345678"), Some("K12345678".to_string()));
2137/// assert_eq!(parse_cy_passport("A123456"),   None);
2138/// assert_eq!(parse_cy_passport("E12345"),    None);  // too short
2139/// ```
2140pub fn parse_cy_passport(s: &str) -> Option<String> {
2141    let cleaned: String = s
2142        .chars()
2143        .filter(|c| c.is_ascii_alphanumeric())
2144        .collect::<String>()
2145        .to_uppercase();
2146    let chars: Vec<char> = cleaned.chars().collect();
2147    match (chars.first(), chars.len()) {
2148        (Some('E'), 7) if chars[1..].iter().all(|c| c.is_ascii_digit()) => Some(cleaned),
2149        (Some('K'), 9) if chars[1..].iter().all(|c| c.is_ascii_digit()) => Some(cleaned),
2150        _ => None,
2151    }
2152}
2153
2154/// Parse a Czech Republic passport number.
2155///
2156/// Usually an 8-digit number; per the TSV it may be longer. The parser
2157/// accepts 8 to 12 ASCII digits.
2158///
2159/// ```
2160/// use worker_matcher::identifiers::parse_cz_passport;
2161/// assert_eq!(parse_cz_passport("12345678"), Some("12345678".to_string()));
2162/// assert_eq!(parse_cz_passport("123-456-78"), Some("12345678".to_string()));
2163/// assert_eq!(parse_cz_passport("123"), None);  // too short
2164/// ```
2165pub fn parse_cz_passport(s: &str) -> Option<String> {
2166    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
2167    if (8..=12).contains(&digits.len()) {
2168        Some(digits)
2169    } else {
2170        None
2171    }
2172}
2173
2174/// Parse a Liechtenstein passport number. 1 letter + 5 digits (e.g. `R00536`).
2175///
2176/// ```
2177/// use worker_matcher::identifiers::parse_li_passport;
2178/// assert_eq!(parse_li_passport("R00536"), Some("R00536".to_string()));
2179/// assert_eq!(parse_li_passport("r00536"), Some("R00536".to_string()));
2180/// assert_eq!(parse_li_passport("123456"), None);
2181/// ```
2182pub fn parse_li_passport(s: &str) -> Option<String> {
2183    let cleaned: String = s
2184        .chars()
2185        .filter(|c| c.is_ascii_alphanumeric())
2186        .collect::<String>()
2187        .to_uppercase();
2188    if cleaned.len() != 6 {
2189        return None;
2190    }
2191    let chars: Vec<char> = cleaned.chars().collect();
2192    if !chars[0].is_ascii_alphabetic() {
2193        return None;
2194    }
2195    if !chars[1..].iter().all(|c| c.is_ascii_digit()) {
2196        return None;
2197    }
2198    Some(cleaned)
2199}
2200
2201/// Parse a Lithuania passport number. 8 ASCII digits (also used on the
2202/// national ID card).
2203///
2204/// ```
2205/// use worker_matcher::identifiers::parse_lt_passport;
2206/// assert_eq!(parse_lt_passport("12345678"), Some("12345678".to_string()));
2207/// assert_eq!(parse_lt_passport("1234567"), None);
2208/// ```
2209pub fn parse_lt_passport(s: &str) -> Option<String> {
2210    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
2211    if digits.len() == 8 {
2212        Some(digits)
2213    } else {
2214        None
2215    }
2216}
2217
2218/// Parse a Malta passport number. 7 ASCII digits.
2219///
2220/// ```
2221/// use worker_matcher::identifiers::parse_mt_passport;
2222/// assert_eq!(parse_mt_passport("1234567"), Some("1234567".to_string()));
2223/// assert_eq!(parse_mt_passport("123"), None);
2224/// ```
2225pub fn parse_mt_passport(s: &str) -> Option<String> {
2226    let digits: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
2227    if digits.len() == 7 {
2228        Some(digits)
2229    } else {
2230        None
2231    }
2232}
2233
2234/// Parse a Netherlands passport number. Same shape as the NL ID card
2235/// (see [`parse_nl_id`]).
2236///
2237/// ```
2238/// use worker_matcher::identifiers::parse_nl_passport;
2239/// assert_eq!(parse_nl_passport("AB1234567"), Some("AB1234567".to_string()));
2240/// assert_eq!(parse_nl_passport("AO1234567"), None);  // O is banned
2241/// ```
2242pub fn parse_nl_passport(s: &str) -> Option<String> {
2243    parse_nl_id(s)
2244}
2245
2246/// Parse a Portugal passport number. 1 letter + 6 digits.
2247///
2248/// ```
2249/// use worker_matcher::identifiers::parse_pt_passport;
2250/// assert_eq!(parse_pt_passport("A123456"), Some("A123456".to_string()));
2251/// assert_eq!(parse_pt_passport("AA12345"), None);
2252/// ```
2253pub fn parse_pt_passport(s: &str) -> Option<String> {
2254    let cleaned: String = s
2255        .chars()
2256        .filter(|c| c.is_ascii_alphanumeric())
2257        .collect::<String>()
2258        .to_uppercase();
2259    if cleaned.len() != 7 {
2260        return None;
2261    }
2262    let chars: Vec<char> = cleaned.chars().collect();
2263    if !chars[0].is_ascii_alphabetic() {
2264        return None;
2265    }
2266    if !chars[1..].iter().all(|c| c.is_ascii_digit()) {
2267        return None;
2268    }
2269    Some(cleaned)
2270}
2271
2272/// Parse a Romania passport number. 2 letters + 6 digits.
2273///
2274/// ```
2275/// use worker_matcher::identifiers::parse_ro_passport;
2276/// assert_eq!(parse_ro_passport("AB123456"), Some("AB123456".to_string()));
2277/// assert_eq!(parse_ro_passport("A1234567"), None);
2278/// ```
2279pub fn parse_ro_passport(s: &str) -> Option<String> {
2280    let cleaned: String = s
2281        .chars()
2282        .filter(|c| c.is_ascii_alphanumeric())
2283        .collect::<String>()
2284        .to_uppercase();
2285    if cleaned.len() != 8 {
2286        return None;
2287    }
2288    let chars: Vec<char> = cleaned.chars().collect();
2289    if !chars[..2].iter().all(|c| c.is_ascii_alphabetic()) {
2290        return None;
2291    }
2292    if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
2293        return None;
2294    }
2295    Some(cleaned)
2296}
2297
2298/// Parse a Slovakia passport number. 2 letters + 7 digits.
2299///
2300/// ```
2301/// use worker_matcher::identifiers::parse_sk_passport;
2302/// assert_eq!(parse_sk_passport("AB1234567"), Some("AB1234567".to_string()));
2303/// assert_eq!(parse_sk_passport("AB12345"), None);
2304/// ```
2305pub fn parse_sk_passport(s: &str) -> Option<String> {
2306    let cleaned: String = s
2307        .chars()
2308        .filter(|c| c.is_ascii_alphanumeric())
2309        .collect::<String>()
2310        .to_uppercase();
2311    if cleaned.len() != 9 {
2312        return None;
2313    }
2314    let chars: Vec<char> = cleaned.chars().collect();
2315    if !chars[..2].iter().all(|c| c.is_ascii_alphabetic()) {
2316        return None;
2317    }
2318    if !chars[2..].iter().all(|c| c.is_ascii_digit()) {
2319        return None;
2320    }
2321    Some(cleaned)
2322}
2323
2324#[cfg(test)]
2325mod tests {
2326    use super::*;
2327
2328    // ---------- parse_uk_nhs_number ----------
2329
2330    #[test]
2331    fn uk_nhs_number_compact_form_parses() {
2332        assert_eq!(parse_uk_nhs_number("9434765919"), Some("9434765919".into()));
2333    }
2334
2335    #[test]
2336    fn uk_nhs_number_spaced_form_parses_to_same_canonical() {
2337        assert_eq!(
2338            parse_uk_nhs_number("943 476 5919"),
2339            parse_uk_nhs_number("9434765919"),
2340        );
2341    }
2342
2343    #[test]
2344    fn uk_nhs_number_rejects_letters_and_short_input() {
2345        assert_eq!(parse_uk_nhs_number("ABCDEFGHIJ"), None);
2346        assert_eq!(parse_uk_nhs_number("123"), None);
2347        assert_eq!(parse_uk_nhs_number(""), None);
2348    }
2349
2350    // ---------- parse_fr_nir ----------
2351
2352    #[test]
2353    fn fr_nir_round_trip_for_a_constructed_valid_value() {
2354        // Body 1801275123456 → key = 97 - (N mod 97) = 42. Verified by parse.
2355        let valid = "180127512345642";
2356        assert_eq!(parse_fr_nir(valid), Some(valid.into()));
2357    }
2358
2359    #[test]
2360    fn fr_nir_whitespace_is_tolerated() {
2361        assert_eq!(
2362            parse_fr_nir("1 80 12 75 123 456 42"),
2363            Some("180127512345642".into()),
2364        );
2365    }
2366
2367    #[test]
2368    fn fr_nir_rejects_wrong_check_key() {
2369        assert_eq!(parse_fr_nir("180127512345699"), None);
2370    }
2371
2372    #[test]
2373    fn fr_nir_rejects_wrong_length() {
2374        assert_eq!(parse_fr_nir("12345"), None);
2375        assert_eq!(parse_fr_nir("1234567890123456"), None); // 16 chars
2376        assert_eq!(parse_fr_nir(""), None);
2377    }
2378
2379    #[test]
2380    fn fr_nir_rejects_letters_in_digit_positions() {
2381        assert_eq!(parse_fr_nir("A80127512345642"), None);
2382    }
2383
2384    #[test]
2385    fn fr_nir_handles_corsica_2a() {
2386        let body = "184032A001234";
2387        let numeric: u64 = "1840319001234".parse().unwrap();
2388        let key = 97 - (numeric % 97);
2389        let nir = format!("{body}{key:02}");
2390        assert_eq!(parse_fr_nir(&nir), Some(nir.clone()));
2391    }
2392
2393    #[test]
2394    fn fr_nir_handles_corsica_2b() {
2395        let body = "184032B001234";
2396        let numeric: u64 = "1840318001234".parse().unwrap();
2397        let key = 97 - (numeric % 97);
2398        let nir = format!("{body}{key:02}");
2399        assert_eq!(parse_fr_nir(&nir), Some(nir.clone()));
2400    }
2401
2402    #[test]
2403    fn fr_nir_canonical_form_is_uppercased() {
2404        let body = "184032a001234";
2405        let numeric: u64 = "1840319001234".parse().unwrap();
2406        let key = 97 - (numeric % 97);
2407        let nir = format!("{body}{key:02}");
2408        let canonical = nir.to_uppercase();
2409        assert_eq!(parse_fr_nir(&nir), Some(canonical));
2410    }
2411
2412    // ---------- parse_es_tsi ----------
2413
2414    #[test]
2415    fn es_tsi_canonical_cip_sns_parses() {
2416        assert_eq!(
2417            parse_es_tsi("ABCD123456XY1234"),
2418            Some("ABCD123456XY1234".into()),
2419        );
2420    }
2421
2422    #[test]
2423    fn es_tsi_whitespace_and_hyphens_stripped() {
2424        assert_eq!(
2425            parse_es_tsi("abcd 123 456-xy1234"),
2426            Some("ABCD123456XY1234".into()),
2427        );
2428    }
2429
2430    #[test]
2431    fn es_tsi_rejects_too_short_or_too_long() {
2432        assert_eq!(parse_es_tsi("ABC123"), None);
2433        assert_eq!(parse_es_tsi("ABCDEF123456XY12345678"), None);
2434    }
2435
2436    #[test]
2437    fn es_tsi_rejects_non_alphanumerics() {
2438        assert_eq!(parse_es_tsi("ABC@123!XYZ"), None);
2439    }
2440
2441    #[test]
2442    fn es_tsi_rejects_non_ascii() {
2443        assert_eq!(parse_es_tsi("ABCDÑ12345XYZ"), None);
2444    }
2445
2446    // ---------- parse_ie_ihi ----------
2447
2448    #[test]
2449    fn ie_ihi_seven_digits_parses() {
2450        assert_eq!(parse_ie_ihi("1234567"), Some("1234567".into()));
2451    }
2452
2453    #[test]
2454    fn ie_ihi_punctuation_and_spaces_stripped() {
2455        assert_eq!(parse_ie_ihi("123 4567"), Some("1234567".into()));
2456        assert_eq!(parse_ie_ihi("123-45-67"), Some("1234567".into()));
2457    }
2458
2459    #[test]
2460    fn ie_ihi_rejects_wrong_digit_count() {
2461        assert_eq!(parse_ie_ihi("123456"), None);
2462        assert_eq!(parse_ie_ihi("12345678"), None);
2463        assert_eq!(parse_ie_ihi(""), None);
2464    }
2465
2466    #[test]
2467    fn ie_ihi_rejects_when_no_digits_present() {
2468        assert_eq!(parse_ie_ihi("ABCDEFG"), None);
2469    }
2470
2471    // ---------- parse_uk_hc_number ----------
2472
2473    #[test]
2474    fn uk_hc_number_matches_nhs_number_semantics() {
2475        assert_eq!(
2476            parse_uk_hc_number("9434765919"),
2477            parse_uk_nhs_number("9434765919"),
2478        );
2479        assert_eq!(
2480            parse_uk_hc_number("943 476 5919"),
2481            parse_uk_nhs_number("943 476 5919"),
2482        );
2483    }
2484
2485    #[test]
2486    fn uk_hc_number_rejects_letters() {
2487        assert_eq!(parse_uk_hc_number("ABCDEFGHIJ"), None);
2488    }
2489
2490    // ---------- parse_us_ssn ----------
2491
2492    #[test]
2493    fn us_ssn_canonical_compact_form_parses() {
2494        assert_eq!(parse_us_ssn("123456789"), Some("123456789".into()));
2495    }
2496
2497    #[test]
2498    fn us_ssn_hyphenated_form_parses_to_same_canonical() {
2499        assert_eq!(parse_us_ssn("123-45-6789"), parse_us_ssn("123456789"),);
2500    }
2501
2502    #[test]
2503    fn us_ssn_whitespace_variants_canonicalise_identically() {
2504        assert_eq!(parse_us_ssn("123 45 6789"), Some("123456789".into()),);
2505        assert_eq!(parse_us_ssn(" 123  45 6789 "), Some("123456789".into()),);
2506    }
2507
2508    #[test]
2509    fn us_ssn_rejects_invalid_area_numbers() {
2510        assert_eq!(parse_us_ssn("000-12-3456"), None);
2511        assert_eq!(parse_us_ssn("666-12-3456"), None);
2512        assert_eq!(parse_us_ssn("900-12-3456"), None);
2513        assert_eq!(parse_us_ssn("987-65-4321"), None); // 987 is in 900..=999
2514        assert_eq!(parse_us_ssn("999-99-9999"), None);
2515    }
2516
2517    #[test]
2518    fn us_ssn_accepts_boundary_areas() {
2519        // 001 and 899 are the lowest and highest valid area numbers.
2520        assert_eq!(parse_us_ssn("001-23-4567"), Some("001234567".into()));
2521        assert_eq!(parse_us_ssn("899-23-4567"), Some("899234567".into()));
2522        // 665 just below the 666 carve-out; 667 just above.
2523        assert_eq!(parse_us_ssn("665-23-4567"), Some("665234567".into()));
2524        assert_eq!(parse_us_ssn("667-23-4567"), Some("667234567".into()));
2525    }
2526
2527    #[test]
2528    fn us_ssn_rejects_zero_group() {
2529        assert_eq!(parse_us_ssn("123-00-4567"), None);
2530    }
2531
2532    #[test]
2533    fn us_ssn_rejects_zero_serial() {
2534        assert_eq!(parse_us_ssn("123-45-0000"), None);
2535    }
2536
2537    #[test]
2538    fn us_ssn_rejects_wrong_length() {
2539        assert_eq!(parse_us_ssn("12345"), None);
2540        assert_eq!(parse_us_ssn("1234567890"), None);
2541        assert_eq!(parse_us_ssn(""), None);
2542    }
2543
2544    #[test]
2545    fn us_ssn_rejects_letters() {
2546        assert_eq!(parse_us_ssn("ABC-DE-FGHI"), None);
2547        assert_eq!(parse_us_ssn("ABCDEFGHI"), None);
2548    }
2549
2550    #[test]
2551    fn us_ssn_strips_arbitrary_punctuation() {
2552        assert_eq!(parse_us_ssn("(123).45.6789"), Some("123456789".into()),);
2553    }
2554
2555    // ---------- parse_de_kvnr ----------
2556
2557    #[test]
2558    fn de_kvnr_canonical_form_parses() {
2559        assert_eq!(parse_de_kvnr("A123456780"), Some("A123456780".into()));
2560    }
2561
2562    #[test]
2563    fn de_kvnr_accepts_lowercase_letter_canonicalises_to_upper() {
2564        assert_eq!(parse_de_kvnr("a123456780"), Some("A123456780".into()));
2565    }
2566
2567    #[test]
2568    fn de_kvnr_accepts_internal_whitespace() {
2569        assert_eq!(parse_de_kvnr("A 123 456 780"), Some("A123456780".into()));
2570    }
2571
2572    #[test]
2573    fn de_kvnr_second_valid_vector() {
2574        assert_eq!(parse_de_kvnr("B987654320"), Some("B987654320".into()));
2575    }
2576
2577    #[test]
2578    fn de_kvnr_rejects_wrong_check_digit() {
2579        assert_eq!(parse_de_kvnr("A123456789"), None);
2580    }
2581
2582    #[test]
2583    fn de_kvnr_rejects_missing_letter() {
2584        assert_eq!(parse_de_kvnr("1234567890"), None);
2585    }
2586
2587    #[test]
2588    fn de_kvnr_rejects_wrong_length() {
2589        assert_eq!(parse_de_kvnr("A12345"), None);
2590        assert_eq!(parse_de_kvnr("A1234567890"), None);
2591        assert_eq!(parse_de_kvnr(""), None);
2592    }
2593
2594    #[test]
2595    fn de_kvnr_rejects_non_digit_in_body() {
2596        assert_eq!(parse_de_kvnr("A12345A780"), None);
2597    }
2598
2599    // ---------- parse_it_cf ----------
2600
2601    #[test]
2602    fn it_cf_canonical_form_parses() {
2603        assert_eq!(
2604            parse_it_cf("RSSMRA85T10A562S"),
2605            Some("RSSMRA85T10A562S".into()),
2606        );
2607    }
2608
2609    #[test]
2610    fn it_cf_accepts_lowercase_and_whitespace() {
2611        assert_eq!(
2612            parse_it_cf("rss mra 85t 10a 562s"),
2613            Some("RSSMRA85T10A562S".into()),
2614        );
2615    }
2616
2617    #[test]
2618    fn it_cf_second_valid_vector() {
2619        assert_eq!(
2620            parse_it_cf("MNRMRC75H17H501I"),
2621            Some("MNRMRC75H17H501I".into()),
2622        );
2623    }
2624
2625    #[test]
2626    fn it_cf_rejects_wrong_check_character() {
2627        assert_eq!(parse_it_cf("RSSMRA85T10A562X"), None);
2628    }
2629
2630    #[test]
2631    fn it_cf_rejects_wrong_length() {
2632        assert_eq!(parse_it_cf("RSSMRA85T10A562"), None);
2633        assert_eq!(parse_it_cf("RSSMRA85T10A562SS"), None);
2634        assert_eq!(parse_it_cf(""), None);
2635    }
2636
2637    #[test]
2638    fn it_cf_rejects_non_alphanumeric() {
2639        assert_eq!(parse_it_cf("RSSMRA85T10A562!"), None);
2640        assert_eq!(parse_it_cf("RSSMRA-85T-10A562S"), None);
2641    }
2642
2643    // ---------- parse_nl_bsn ----------
2644
2645    #[test]
2646    fn nl_bsn_canonical_form_parses() {
2647        assert_eq!(parse_nl_bsn("111222333"), Some("111222333".into()));
2648    }
2649
2650    #[test]
2651    fn nl_bsn_second_valid_vector() {
2652        assert_eq!(parse_nl_bsn("123456782"), Some("123456782".into()));
2653    }
2654
2655    #[test]
2656    fn nl_bsn_strips_separators() {
2657        assert_eq!(parse_nl_bsn("111 222 333"), Some("111222333".into()));
2658        assert_eq!(parse_nl_bsn("111-222-333"), Some("111222333".into()));
2659    }
2660
2661    #[test]
2662    fn nl_bsn_rejects_wrong_eleven_test() {
2663        assert_eq!(parse_nl_bsn("111222334"), None);
2664    }
2665
2666    #[test]
2667    fn nl_bsn_rejects_all_zeros() {
2668        assert_eq!(parse_nl_bsn("000000000"), None);
2669    }
2670
2671    #[test]
2672    fn nl_bsn_rejects_wrong_length() {
2673        assert_eq!(parse_nl_bsn("12345"), None);
2674        assert_eq!(parse_nl_bsn("1234567890"), None);
2675        assert_eq!(parse_nl_bsn(""), None);
2676    }
2677
2678    #[test]
2679    fn nl_bsn_rejects_letters() {
2680        assert_eq!(parse_nl_bsn("ABCDEFGHI"), None);
2681    }
2682
2683    // ---------- parse_se_workernummer ----------
2684
2685    #[test]
2686    fn se_pnr_ten_digit_form_parses() {
2687        assert_eq!(
2688            parse_se_workernummer("4603243850"),
2689            Some("4603243850".into()),
2690        );
2691    }
2692
2693    #[test]
2694    fn se_pnr_with_separator_canonicalises_to_ten_digit() {
2695        assert_eq!(
2696            parse_se_workernummer("460324-3850"),
2697            Some("4603243850".into()),
2698        );
2699        assert_eq!(
2700            parse_se_workernummer("460324+3850"),
2701            Some("4603243850".into()),
2702        );
2703    }
2704
2705    #[test]
2706    fn se_pnr_twelve_digit_form_preserves_century() {
2707        assert_eq!(
2708            parse_se_workernummer("19460324-3850"),
2709            Some("194603243850".into()),
2710        );
2711        assert_eq!(
2712            parse_se_workernummer("194603243850"),
2713            Some("194603243850".into()),
2714        );
2715    }
2716
2717    #[test]
2718    fn se_pnr_second_valid_vector() {
2719        assert_eq!(
2720            parse_se_workernummer("8112310092"),
2721            Some("8112310092".into()),
2722        );
2723    }
2724
2725    #[test]
2726    fn se_pnr_rejects_wrong_luhn() {
2727        assert_eq!(parse_se_workernummer("4603243851"), None);
2728    }
2729
2730    #[test]
2731    fn se_pnr_rejects_wrong_length() {
2732        assert_eq!(parse_se_workernummer("12345"), None);
2733        assert_eq!(parse_se_workernummer("12345678901"), None);
2734        assert_eq!(parse_se_workernummer(""), None);
2735    }
2736
2737    #[test]
2738    fn se_pnr_rejects_letters() {
2739        assert_eq!(parse_se_workernummer("ABCDEFGHIJ"), None);
2740    }
2741
2742    // ---------- parse_au_ihi ----------
2743
2744    #[test]
2745    fn au_ihi_canonical_form_parses() {
2746        assert_eq!(
2747            parse_au_ihi("8003601234567894"),
2748            Some("8003601234567894".into()),
2749        );
2750    }
2751
2752    #[test]
2753    fn au_ihi_strips_whitespace() {
2754        assert_eq!(
2755            parse_au_ihi("8003 6012 3456 7894"),
2756            Some("8003601234567894".into()),
2757        );
2758    }
2759
2760    #[test]
2761    fn au_ihi_second_valid_vector() {
2762        assert_eq!(
2763            parse_au_ihi("8003619876543213"),
2764            Some("8003619876543213".into()),
2765        );
2766    }
2767
2768    #[test]
2769    fn au_ihi_rejects_wrong_luhn() {
2770        assert_eq!(parse_au_ihi("8003601234567890"), None);
2771    }
2772
2773    #[test]
2774    fn au_ihi_rejects_wrong_length() {
2775        assert_eq!(parse_au_ihi("12345"), None);
2776        assert_eq!(parse_au_ihi("80036012345678941"), None);
2777        assert_eq!(parse_au_ihi(""), None);
2778    }
2779
2780    #[test]
2781    fn au_ihi_rejects_letters() {
2782        assert_eq!(parse_au_ihi("ABCDEFGHIJKLMNOP"), None);
2783    }
2784
2785    // ---------- parse_uk_chi_number ----------
2786
2787    #[test]
2788    fn uk_chi_canonical_form_parses() {
2789        assert_eq!(parse_uk_chi_number("0101701233"), Some("0101701233".into()),);
2790    }
2791
2792    #[test]
2793    fn uk_chi_strips_whitespace() {
2794        assert_eq!(
2795            parse_uk_chi_number("010 170 1233"),
2796            Some("0101701233".into()),
2797        );
2798    }
2799
2800    #[test]
2801    fn uk_chi_second_valid_vector() {
2802        assert_eq!(parse_uk_chi_number("0101701241"), Some("0101701241".into()),);
2803    }
2804
2805    #[test]
2806    fn uk_chi_rejects_wrong_check_digit() {
2807        assert_eq!(parse_uk_chi_number("0101701234"), None);
2808    }
2809
2810    #[test]
2811    fn uk_chi_rejects_wrong_length() {
2812        assert_eq!(parse_uk_chi_number("12345"), None);
2813        assert_eq!(parse_uk_chi_number("01017012339"), None);
2814        assert_eq!(parse_uk_chi_number(""), None);
2815    }
2816
2817    #[test]
2818    fn uk_chi_rejects_letters() {
2819        assert_eq!(parse_uk_chi_number("ABCDEFGHIJ"), None);
2820    }
2821
2822    // ----------------------------------------------------------------------
2823    // Eighteen additional national workeral identifiers (T-27).
2824    // ----------------------------------------------------------------------
2825
2826    // ---------- parse_be_nn ----------
2827
2828    #[test]
2829    fn be_nn_canonical_form_parses() {
2830        assert_eq!(parse_be_nn("80010100107"), Some("80010100107".into()));
2831    }
2832    #[test]
2833    fn be_nn_strips_punctuation() {
2834        assert_eq!(parse_be_nn("80.01.01-001.07"), Some("80010100107".into()),);
2835    }
2836    #[test]
2837    fn be_nn_rejects_wrong_check() {
2838        assert_eq!(parse_be_nn("80010100100"), None);
2839    }
2840    #[test]
2841    fn be_nn_rejects_wrong_length() {
2842        assert_eq!(parse_be_nn("12345"), None);
2843        assert_eq!(parse_be_nn(""), None);
2844    }
2845
2846    // ---------- parse_bg_egn ----------
2847
2848    #[test]
2849    fn bg_egn_canonical_form_parses() {
2850        assert_eq!(parse_bg_egn("8001010013"), Some("8001010013".into()));
2851    }
2852    #[test]
2853    fn bg_egn_rejects_wrong_check() {
2854        assert_eq!(parse_bg_egn("8001010014"), None);
2855    }
2856    #[test]
2857    fn bg_egn_rejects_wrong_length() {
2858        assert_eq!(parse_bg_egn("80010100"), None);
2859        assert_eq!(parse_bg_egn(""), None);
2860    }
2861
2862    // ---------- parse_cz_rc ----------
2863
2864    #[test]
2865    fn cz_rc_ten_digit_divisible_by_eleven() {
2866        assert_eq!(parse_cz_rc("8001150014"), Some("8001150014".into()));
2867    }
2868    #[test]
2869    fn cz_rc_nine_digit_pre_1954_accepted_as_is() {
2870        assert_eq!(parse_cz_rc("800115001"), Some("800115001".into()));
2871    }
2872    #[test]
2873    fn cz_rc_rejects_wrong_check() {
2874        assert_eq!(parse_cz_rc("8001150015"), None);
2875    }
2876    #[test]
2877    fn cz_rc_rejects_bad_length() {
2878        assert_eq!(parse_cz_rc("12345"), None);
2879        assert_eq!(parse_cz_rc("12345678901"), None);
2880    }
2881
2882    // ---------- parse_dk_cpr ----------
2883
2884    #[test]
2885    fn dk_cpr_canonical_parses() {
2886        assert_eq!(parse_dk_cpr("1501801234"), Some("1501801234".into()));
2887    }
2888    #[test]
2889    fn dk_cpr_strips_separator() {
2890        assert_eq!(parse_dk_cpr("150180-1234"), Some("1501801234".into()));
2891    }
2892    #[test]
2893    fn dk_cpr_rejects_bad_length() {
2894        assert_eq!(parse_dk_cpr("12345"), None);
2895        assert_eq!(parse_dk_cpr(""), None);
2896    }
2897
2898    // ---------- parse_ee_ik ----------
2899
2900    #[test]
2901    fn ee_ik_canonical_form_parses() {
2902        assert_eq!(parse_ee_ik("48001150011"), Some("48001150011".into()));
2903    }
2904    #[test]
2905    fn ee_ik_rejects_wrong_check() {
2906        assert_eq!(parse_ee_ik("48001150012"), None);
2907    }
2908    #[test]
2909    fn ee_ik_rejects_bad_length() {
2910        assert_eq!(parse_ee_ik("4800115001"), None);
2911    }
2912
2913    // ---------- parse_es_dni ----------
2914
2915    #[test]
2916    fn es_dni_canonical_form_parses() {
2917        assert_eq!(parse_es_dni("12345678Z"), Some("12345678Z".into()));
2918    }
2919    #[test]
2920    fn es_dni_rejects_wrong_letter() {
2921        assert_eq!(parse_es_dni("12345678A"), None);
2922    }
2923    #[test]
2924    fn es_dni_lowercase_letter_canonicalises_upper() {
2925        assert_eq!(parse_es_dni("12345678z"), Some("12345678Z".into()));
2926    }
2927    #[test]
2928    fn es_dni_handles_nie_prefix_x() {
2929        // NIE X1234567L → number is "01234567" mod 23 = (01234567 % 23).
2930        // 1234567 mod 23: 23 × 53676 = 1234548. 1234567 - 1234548 = 19.
2931        // LETTERS[19] = 'L'. So "X1234567L" is valid.
2932        assert_eq!(parse_es_dni("X1234567L"), Some("X1234567L".into()));
2933    }
2934
2935    // ---------- parse_fi_hetu ----------
2936
2937    #[test]
2938    fn fi_hetu_canonical_form_parses() {
2939        assert_eq!(parse_fi_hetu("150180-999B"), Some("150180-999B".into()));
2940    }
2941    #[test]
2942    fn fi_hetu_rejects_wrong_check() {
2943        assert_eq!(parse_fi_hetu("150180-999C"), None);
2944    }
2945    #[test]
2946    fn fi_hetu_rejects_bad_length() {
2947        assert_eq!(parse_fi_hetu("12345"), None);
2948    }
2949
2950    // ---------- parse_hr_oib ----------
2951
2952    #[test]
2953    fn hr_oib_canonical_form_parses() {
2954        assert_eq!(parse_hr_oib("12345678903"), Some("12345678903".into()));
2955    }
2956    #[test]
2957    fn hr_oib_rejects_wrong_check() {
2958        assert_eq!(parse_hr_oib("12345678901"), None);
2959    }
2960    #[test]
2961    fn hr_oib_rejects_bad_length() {
2962        assert_eq!(parse_hr_oib("123456789"), None);
2963    }
2964
2965    // ---------- parse_is_kt ----------
2966
2967    #[test]
2968    fn is_kt_canonical_form_parses() {
2969        assert_eq!(parse_is_kt("1501802529"), Some("1501802529".into()));
2970    }
2971    #[test]
2972    fn is_kt_rejects_wrong_check() {
2973        assert_eq!(parse_is_kt("1501802539"), None);
2974    }
2975    #[test]
2976    fn is_kt_rejects_bad_length() {
2977        assert_eq!(parse_is_kt("12345"), None);
2978    }
2979
2980    // ---------- parse_lt_ak ----------
2981
2982    #[test]
2983    fn lt_ak_canonical_form_parses() {
2984        assert_eq!(parse_lt_ak("48001150011"), Some("48001150011".into()));
2985    }
2986    #[test]
2987    fn lt_ak_rejects_wrong_check() {
2988        assert_eq!(parse_lt_ak("48001150012"), None);
2989    }
2990
2991    // ---------- parse_lv_pk ----------
2992
2993    #[test]
2994    fn lv_pk_canonical_form_parses() {
2995        assert_eq!(parse_lv_pk("15018010007"), Some("15018010007".into()));
2996    }
2997    #[test]
2998    fn lv_pk_rejects_wrong_check() {
2999        assert_eq!(parse_lv_pk("15018010008"), None);
3000    }
3001    #[test]
3002    fn lv_pk_rejects_bad_length() {
3003        assert_eq!(parse_lv_pk("1501801000"), None);
3004    }
3005
3006    // ---------- parse_mt_id ----------
3007
3008    #[test]
3009    fn mt_id_canonical_form_parses() {
3010        assert_eq!(parse_mt_id("1234567M"), Some("1234567M".into()));
3011    }
3012    #[test]
3013    fn mt_id_accepts_all_valid_letters() {
3014        for letter in ['M', 'G', 'A', 'P', 'L', 'H', 'B', 'Z'] {
3015            let s = format!("1234567{letter}");
3016            assert!(parse_mt_id(&s).is_some(), "letter {letter} should be valid");
3017        }
3018    }
3019    #[test]
3020    fn mt_id_rejects_invalid_letter() {
3021        assert_eq!(parse_mt_id("1234567X"), None);
3022        assert_eq!(parse_mt_id("1234567K"), None);
3023    }
3024    #[test]
3025    fn mt_id_rejects_bad_length() {
3026        assert_eq!(parse_mt_id("12345M"), None);
3027    }
3028
3029    // ---------- parse_no_fnr ----------
3030
3031    #[test]
3032    fn no_fnr_canonical_form_parses() {
3033        assert_eq!(parse_no_fnr("15018012399"), Some("15018012399".into()));
3034    }
3035    #[test]
3036    fn no_fnr_rejects_wrong_check() {
3037        assert_eq!(parse_no_fnr("15018012390"), None);
3038        assert_eq!(parse_no_fnr("15018012398"), None);
3039    }
3040    #[test]
3041    fn no_fnr_rejects_bad_length() {
3042        assert_eq!(parse_no_fnr("12345"), None);
3043    }
3044
3045    // ---------- parse_pl_pesel ----------
3046
3047    #[test]
3048    fn pl_pesel_canonical_form_parses() {
3049        assert_eq!(parse_pl_pesel("80011500014"), Some("80011500014".into()));
3050    }
3051    #[test]
3052    fn pl_pesel_rejects_wrong_check() {
3053        assert_eq!(parse_pl_pesel("80011500015"), None);
3054    }
3055    #[test]
3056    fn pl_pesel_rejects_bad_length() {
3057        assert_eq!(parse_pl_pesel("1234"), None);
3058    }
3059
3060    // ---------- parse_ro_cnp ----------
3061
3062    #[test]
3063    fn ro_cnp_canonical_form_parses() {
3064        assert_eq!(parse_ro_cnp("1800115400012"), Some("1800115400012".into()));
3065    }
3066    #[test]
3067    fn ro_cnp_rejects_wrong_check() {
3068        assert_eq!(parse_ro_cnp("1800115400015"), None);
3069    }
3070    #[test]
3071    fn ro_cnp_rejects_bad_length() {
3072        assert_eq!(parse_ro_cnp("180011540001"), None);
3073    }
3074
3075    // ---------- parse_si_emso ----------
3076
3077    #[test]
3078    fn si_emso_canonical_form_parses() {
3079        assert_eq!(parse_si_emso("1501980500015"), Some("1501980500015".into()));
3080    }
3081    #[test]
3082    fn si_emso_rejects_wrong_check() {
3083        assert_eq!(parse_si_emso("1501980500014"), None);
3084    }
3085
3086    // ---------- parse_sk_rc ----------
3087
3088    #[test]
3089    fn sk_rc_canonical_form_parses() {
3090        assert_eq!(parse_sk_rc("8051150019"), Some("8051150019".into()));
3091    }
3092    #[test]
3093    fn sk_rc_rejects_wrong_check() {
3094        assert_eq!(parse_sk_rc("8051150010"), None);
3095    }
3096
3097    // ---------- parse_uk_nino ----------
3098
3099    #[test]
3100    fn uk_nino_canonical_form_parses() {
3101        assert_eq!(parse_uk_nino("AB123456A"), Some("AB123456A".into()));
3102    }
3103    #[test]
3104    fn uk_nino_accepts_lowercase_and_whitespace() {
3105        assert_eq!(parse_uk_nino("ab 12 34 56 a"), Some("AB123456A".into()),);
3106    }
3107    #[test]
3108    fn uk_nino_rejects_banned_first_letter() {
3109        for ch in ['D', 'F', 'I', 'Q', 'U', 'V'] {
3110            let s = format!("{ch}A123456A");
3111            assert!(parse_uk_nino(&s).is_none(), "letter {ch} should be banned");
3112        }
3113    }
3114    #[test]
3115    fn uk_nino_rejects_banned_admin_prefix() {
3116        for prefix in ["OO", "CR", "FY", "MW", "NC", "PP", "PZ", "TN"] {
3117            let s = format!("{prefix}123456A");
3118            assert!(
3119                parse_uk_nino(&s).is_none(),
3120                "prefix {prefix} should be banned"
3121            );
3122        }
3123    }
3124    #[test]
3125    fn uk_nino_rejects_bad_suffix() {
3126        for ch in ['E', 'F', 'X', 'Z'] {
3127            let s = format!("AB123456{ch}");
3128            assert!(parse_uk_nino(&s).is_none(), "suffix {ch} should be invalid");
3129        }
3130    }
3131    #[test]
3132    fn uk_nino_rejects_bad_length() {
3133        assert_eq!(parse_uk_nino("AB12345A"), None);
3134    }
3135
3136    // ----------------------------------------------------------------------
3137    // T-28: Five additional workeral identifiers.
3138    // ----------------------------------------------------------------------
3139
3140    // ---------- parse_gr_dss ----------
3141
3142    #[test]
3143    fn gr_dss_canonical_form_parses() {
3144        assert_eq!(parse_gr_dss("1234567890"), Some("1234567890".into()));
3145    }
3146    #[test]
3147    fn gr_dss_strips_punctuation() {
3148        assert_eq!(parse_gr_dss("12 34-56 78 90"), Some("1234567890".into()));
3149    }
3150    #[test]
3151    fn gr_dss_rejects_bad_length() {
3152        assert_eq!(parse_gr_dss("12345"), None);
3153        assert_eq!(parse_gr_dss("12345678901"), None);
3154        assert_eq!(parse_gr_dss(""), None);
3155    }
3156    #[test]
3157    fn gr_dss_rejects_letters() {
3158        assert_eq!(parse_gr_dss("ABCDEFGHIJ"), None);
3159    }
3160
3161    // ---------- parse_li_id ----------
3162
3163    #[test]
3164    fn li_id_eight_digit_form_parses() {
3165        assert_eq!(parse_li_id("ID12345678"), Some("ID12345678".into()));
3166    }
3167    #[test]
3168    fn li_id_nine_digit_example_from_spec_parses() {
3169        assert_eq!(parse_li_id("ID022143586"), Some("ID022143586".into()));
3170    }
3171    #[test]
3172    fn li_id_lowercase_letters_uppercased() {
3173        assert_eq!(parse_li_id("id12345678"), Some("ID12345678".into()));
3174    }
3175    #[test]
3176    fn li_id_rejects_missing_letters() {
3177        assert_eq!(parse_li_id("1234567890"), None);
3178        assert_eq!(parse_li_id("I12345678"), None); // only one leading letter
3179    }
3180    #[test]
3181    fn li_id_rejects_bad_length() {
3182        assert_eq!(parse_li_id(""), None);
3183        assert_eq!(parse_li_id("ID1234"), None);
3184        assert_eq!(parse_li_id("ID123456789012"), None);
3185    }
3186
3187    // ---------- parse_nl_id ----------
3188
3189    #[test]
3190    fn nl_id_canonical_form_parses() {
3191        assert_eq!(parse_nl_id("AB1234567"), Some("AB1234567".into()));
3192    }
3193    #[test]
3194    fn nl_id_lowercase_and_whitespace_canonicalise() {
3195        assert_eq!(parse_nl_id("ab 12 34 567"), Some("AB1234567".into()));
3196    }
3197    #[test]
3198    fn nl_id_rejects_letter_o_in_disallowed_positions() {
3199        assert_eq!(parse_nl_id("AO1234567"), None);
3200        assert_eq!(parse_nl_id("OB1234567"), None);
3201        assert_eq!(parse_nl_id("ABO234567"), None);
3202    }
3203    #[test]
3204    fn nl_id_allows_digit_zero() {
3205        assert_eq!(parse_nl_id("AB0234567"), Some("AB0234567".into()));
3206    }
3207    #[test]
3208    fn nl_id_rejects_bad_shape() {
3209        assert_eq!(parse_nl_id("12345AB67"), None);
3210        assert_eq!(parse_nl_id("AB12345AB"), None);
3211        assert_eq!(parse_nl_id(""), None);
3212    }
3213
3214    // ---------- parse_pl_nip ----------
3215
3216    #[test]
3217    fn pl_nip_canonical_form_parses() {
3218        assert_eq!(parse_pl_nip("1234567802"), Some("1234567802".into()));
3219    }
3220    #[test]
3221    fn pl_nip_strips_separators() {
3222        assert_eq!(parse_pl_nip("123-456-78-02"), Some("1234567802".into()));
3223    }
3224    #[test]
3225    fn pl_nip_rejects_wrong_check() {
3226        assert_eq!(parse_pl_nip("1234567803"), None);
3227    }
3228    #[test]
3229    fn pl_nip_rejects_check_value_ten_per_spec() {
3230        // For "123456789" body the weighted sum mod 11 is 10, which the
3231        // Polish NIP spec defines as invalid.
3232        assert_eq!(parse_pl_nip("1234567890"), None);
3233    }
3234    #[test]
3235    fn pl_nip_rejects_bad_length() {
3236        assert_eq!(parse_pl_nip("12345"), None);
3237    }
3238
3239    // ---------- parse_pt_nif ----------
3240
3241    #[test]
3242    fn pt_nif_canonical_form_parses() {
3243        assert_eq!(parse_pt_nif("123456789"), Some("123456789".into()));
3244    }
3245    #[test]
3246    fn pt_nif_rejects_wrong_check() {
3247        assert_eq!(parse_pt_nif("123456780"), None);
3248    }
3249    #[test]
3250    fn pt_nif_rejects_bad_length() {
3251        assert_eq!(parse_pt_nif("12345"), None);
3252        assert_eq!(parse_pt_nif("1234567890"), None);
3253    }
3254
3255    // ----------------------------------------------------------------------
3256    // T-17.1: Seven next-batch national identifier schemes.
3257    // ----------------------------------------------------------------------
3258
3259    // ---------- parse_br_cpf ----------
3260    #[test]
3261    fn br_cpf_canonical_form_parses() {
3262        assert_eq!(parse_br_cpf("12345678909"), Some("12345678909".into()));
3263    }
3264    #[test]
3265    fn br_cpf_formatted_input_strips_punctuation() {
3266        assert_eq!(parse_br_cpf("123.456.789-09"), Some("12345678909".into()));
3267    }
3268    #[test]
3269    fn br_cpf_rejects_wrong_check() {
3270        assert_eq!(parse_br_cpf("12345678900"), None);
3271    }
3272    #[test]
3273    fn br_cpf_rejects_all_equal_sequences() {
3274        for d in '0'..='9' {
3275            let s: String = std::iter::repeat_n(d, 11).collect();
3276            assert_eq!(parse_br_cpf(&s), None, "{s}");
3277        }
3278    }
3279    #[test]
3280    fn br_cpf_rejects_bad_length() {
3281        assert_eq!(parse_br_cpf("1234567890"), None);
3282        assert_eq!(parse_br_cpf("123456789090"), None);
3283    }
3284    #[test]
3285    fn br_cpf_rejects_non_digit_only_input() {
3286        assert_eq!(parse_br_cpf("abcdefghijk"), None);
3287    }
3288
3289    // ---------- parse_cn_rrn ----------
3290    #[test]
3291    fn cn_rrn_canonical_form_parses() {
3292        assert_eq!(
3293            parse_cn_rrn("11010519491231002X"),
3294            Some("11010519491231002X".into()),
3295        );
3296    }
3297    #[test]
3298    fn cn_rrn_uppercases_lowercase_x() {
3299        assert_eq!(
3300            parse_cn_rrn("11010519491231002x"),
3301            Some("11010519491231002X".into()),
3302        );
3303    }
3304    #[test]
3305    fn cn_rrn_rejects_wrong_check_char() {
3306        assert_eq!(parse_cn_rrn("11010519491231002Y"), None);
3307        assert_eq!(parse_cn_rrn("110105194912310020"), None);
3308    }
3309    #[test]
3310    fn cn_rrn_rejects_invalid_date_substring() {
3311        assert_eq!(parse_cn_rrn("11010513491231002X"), None);
3312        assert_eq!(parse_cn_rrn("110105194913320002X"), None);
3313    }
3314    #[test]
3315    fn cn_rrn_rejects_bad_length() {
3316        assert_eq!(parse_cn_rrn("11010519491231"), None);
3317        assert_eq!(parse_cn_rrn("11010519491231002XY"), None);
3318    }
3319    #[test]
3320    fn cn_rrn_rejects_non_alnum_letters() {
3321        // A non-X non-digit at the check position is rejected.
3322        assert_eq!(parse_cn_rrn("11010519491231002A"), None);
3323    }
3324
3325    // ---------- parse_in_aadhaar ----------
3326    #[test]
3327    fn in_aadhaar_canonical_form_parses() {
3328        assert_eq!(
3329            parse_in_aadhaar("234123412346"),
3330            Some("234123412346".into())
3331        );
3332    }
3333    #[test]
3334    fn in_aadhaar_strips_whitespace() {
3335        assert_eq!(
3336            parse_in_aadhaar("2341 2341 2346"),
3337            Some("234123412346".into()),
3338        );
3339    }
3340    #[test]
3341    fn in_aadhaar_rejects_wrong_verhoeff_check() {
3342        assert_eq!(parse_in_aadhaar("234123412347"), None);
3343        assert_eq!(parse_in_aadhaar("234123412345"), None);
3344    }
3345    #[test]
3346    fn in_aadhaar_rejects_all_equal_sequences() {
3347        for d in '2'..='9' {
3348            let s: String = std::iter::repeat_n(d, 12).collect();
3349            assert_eq!(parse_in_aadhaar(&s), None, "{s}");
3350        }
3351    }
3352    #[test]
3353    fn in_aadhaar_rejects_reserved_prefixes() {
3354        // UIDAI never issues numbers starting with 0 or 1.
3355        assert_eq!(parse_in_aadhaar("034123412346"), None);
3356        assert_eq!(parse_in_aadhaar("134123412346"), None);
3357    }
3358    #[test]
3359    fn in_aadhaar_rejects_bad_length() {
3360        assert_eq!(parse_in_aadhaar("234123412"), None);
3361        assert_eq!(parse_in_aadhaar("2341234123466"), None);
3362    }
3363
3364    // ---------- parse_jp_my_number ----------
3365    #[test]
3366    fn jp_my_number_canonical_form_parses() {
3367        assert_eq!(
3368            parse_jp_my_number("123456789018"),
3369            Some("123456789018".into()),
3370        );
3371    }
3372    #[test]
3373    fn jp_my_number_strips_whitespace() {
3374        assert_eq!(
3375            parse_jp_my_number("1234 5678 9018"),
3376            Some("123456789018".into()),
3377        );
3378    }
3379    #[test]
3380    fn jp_my_number_rejects_wrong_check() {
3381        assert_eq!(parse_jp_my_number("123456789010"), None);
3382        assert_eq!(parse_jp_my_number("123456789019"), None);
3383    }
3384    #[test]
3385    fn jp_my_number_rejects_bad_length() {
3386        assert_eq!(parse_jp_my_number("12345678901"), None);
3387        assert_eq!(parse_jp_my_number("1234567890123"), None);
3388    }
3389    #[test]
3390    fn jp_my_number_rejects_non_digit_only_input() {
3391        assert_eq!(parse_jp_my_number("abcdefghijkl"), None);
3392    }
3393
3394    // ---------- parse_mx_curp ----------
3395    #[test]
3396    fn mx_curp_canonical_form_parses() {
3397        assert_eq!(
3398            parse_mx_curp("HEGG560427MVZRRL04"),
3399            Some("HEGG560427MVZRRL04".into()),
3400        );
3401    }
3402    #[test]
3403    fn mx_curp_uppercases_input() {
3404        assert_eq!(
3405            parse_mx_curp("hegg560427mvzrrl04"),
3406            Some("HEGG560427MVZRRL04".into()),
3407        );
3408    }
3409    #[test]
3410    fn mx_curp_rejects_wrong_check() {
3411        assert_eq!(parse_mx_curp("HEGG560427MVZRRL05"), None);
3412    }
3413    #[test]
3414    fn mx_curp_rejects_invalid_date_substring() {
3415        assert_eq!(parse_mx_curp("HEGG561327MVZRRL04"), None);
3416        assert_eq!(parse_mx_curp("HEGG569927MVZRRL04"), None);
3417    }
3418    #[test]
3419    fn mx_curp_rejects_bad_sex_char() {
3420        assert_eq!(parse_mx_curp("HEGG560427XVZRRL04"), None);
3421    }
3422    #[test]
3423    fn mx_curp_rejects_bad_length() {
3424        assert_eq!(parse_mx_curp("HEGG560427"), None);
3425        assert_eq!(parse_mx_curp("HEGG560427MVZRRL04EXTRA"), None);
3426    }
3427
3428    // ---------- parse_nz_nhi ----------
3429    #[test]
3430    fn nz_nhi_canonical_form_parses() {
3431        assert_eq!(parse_nz_nhi("ZAA0083"), Some("ZAA0083".into()));
3432    }
3433    #[test]
3434    fn nz_nhi_uppercases_input() {
3435        assert_eq!(parse_nz_nhi("zaa0083"), Some("ZAA0083".into()));
3436    }
3437    #[test]
3438    fn nz_nhi_rejects_wrong_check() {
3439        assert_eq!(parse_nz_nhi("ZAA0082"), None);
3440    }
3441    #[test]
3442    fn nz_nhi_rejects_excluded_letters_i_and_o() {
3443        assert_eq!(parse_nz_nhi("ZAI0083"), None);
3444        assert_eq!(parse_nz_nhi("ZAO0083"), None);
3445        assert_eq!(parse_nz_nhi("IZA0083"), None);
3446    }
3447    #[test]
3448    fn nz_nhi_rejects_bad_length() {
3449        assert_eq!(parse_nz_nhi("ZAA008"), None);
3450        assert_eq!(parse_nz_nhi("ZAA00830"), None);
3451    }
3452    #[test]
3453    fn nz_nhi_rejects_non_letter_prefix() {
3454        assert_eq!(parse_nz_nhi("Z1A0083"), None);
3455    }
3456
3457    // ---------- parse_za_id ----------
3458    #[test]
3459    fn za_id_canonical_form_parses() {
3460        assert_eq!(parse_za_id("8001015009087"), Some("8001015009087".into()));
3461    }
3462    #[test]
3463    fn za_id_strips_whitespace() {
3464        assert_eq!(parse_za_id("800101 5009 087"), Some("8001015009087".into()),);
3465    }
3466    #[test]
3467    fn za_id_rejects_wrong_luhn() {
3468        assert_eq!(parse_za_id("8001015009088"), None);
3469        assert_eq!(parse_za_id("8001015009086"), None);
3470    }
3471    #[test]
3472    fn za_id_rejects_invalid_date_substring() {
3473        assert_eq!(parse_za_id("8013015009087"), None);
3474        assert_eq!(parse_za_id("8002305009087"), None);
3475    }
3476    #[test]
3477    fn za_id_rejects_bad_length() {
3478        assert_eq!(parse_za_id("80010150090"), None);
3479        assert_eq!(parse_za_id("80010150090870"), None);
3480    }
3481
3482    // ----------------------------------------------------------------------
3483    // T-28: Nine per-country passport-number format validators.
3484    // ----------------------------------------------------------------------
3485
3486    #[test]
3487    fn cy_passport_pre_2010_form_parses() {
3488        assert_eq!(parse_cy_passport("E123456"), Some("E123456".into()));
3489    }
3490    #[test]
3491    fn cy_passport_biometric_form_parses() {
3492        assert_eq!(parse_cy_passport("K12345678"), Some("K12345678".into()));
3493    }
3494    #[test]
3495    fn cy_passport_rejects_wrong_prefix() {
3496        assert_eq!(parse_cy_passport("A123456"), None);
3497        assert_eq!(parse_cy_passport("Z12345678"), None);
3498    }
3499    #[test]
3500    fn cy_passport_rejects_bad_length() {
3501        assert_eq!(parse_cy_passport("E12345"), None);
3502        assert_eq!(parse_cy_passport("K1234567"), None);
3503    }
3504
3505    #[test]
3506    fn cz_passport_eight_digit_form_parses() {
3507        assert_eq!(parse_cz_passport("12345678"), Some("12345678".into()));
3508    }
3509    #[test]
3510    fn cz_passport_accepts_longer_forms() {
3511        assert_eq!(
3512            parse_cz_passport("123456789012"),
3513            Some("123456789012".into())
3514        );
3515    }
3516    #[test]
3517    fn cz_passport_rejects_short_forms() {
3518        assert_eq!(parse_cz_passport("1234567"), None);
3519        assert_eq!(parse_cz_passport(""), None);
3520    }
3521
3522    #[test]
3523    fn li_passport_canonical_form_parses() {
3524        assert_eq!(parse_li_passport("R00536"), Some("R00536".into()));
3525    }
3526    #[test]
3527    fn li_passport_lowercases_to_upper() {
3528        assert_eq!(parse_li_passport("r00536"), Some("R00536".into()));
3529    }
3530    #[test]
3531    fn li_passport_rejects_bad_format() {
3532        assert_eq!(parse_li_passport("RR0536"), None);
3533        assert_eq!(parse_li_passport("123456"), None);
3534    }
3535
3536    #[test]
3537    fn lt_passport_eight_digit_parses() {
3538        assert_eq!(parse_lt_passport("12345678"), Some("12345678".into()));
3539    }
3540    #[test]
3541    fn lt_passport_rejects_wrong_length() {
3542        assert_eq!(parse_lt_passport("1234567"), None);
3543        assert_eq!(parse_lt_passport("123456789"), None);
3544    }
3545
3546    #[test]
3547    fn mt_passport_seven_digit_parses() {
3548        assert_eq!(parse_mt_passport("1234567"), Some("1234567".into()));
3549    }
3550    #[test]
3551    fn mt_passport_rejects_letters() {
3552        assert_eq!(parse_mt_passport("123456M"), None);
3553    }
3554
3555    #[test]
3556    fn nl_passport_uses_nl_id_format() {
3557        assert_eq!(parse_nl_passport("AB1234567"), Some("AB1234567".into()));
3558        assert_eq!(parse_nl_passport("AO1234567"), None);
3559    }
3560
3561    #[test]
3562    fn pt_passport_canonical_form_parses() {
3563        assert_eq!(parse_pt_passport("A123456"), Some("A123456".into()));
3564    }
3565    #[test]
3566    fn pt_passport_rejects_bad_shape() {
3567        assert_eq!(parse_pt_passport("AA12345"), None);
3568        assert_eq!(parse_pt_passport("1234567"), None);
3569    }
3570
3571    #[test]
3572    fn ro_passport_canonical_form_parses() {
3573        assert_eq!(parse_ro_passport("AB123456"), Some("AB123456".into()));
3574    }
3575    #[test]
3576    fn ro_passport_rejects_bad_shape() {
3577        assert_eq!(parse_ro_passport("A1234567"), None);
3578        assert_eq!(parse_ro_passport("ABC12345"), None);
3579    }
3580
3581    #[test]
3582    fn sk_passport_canonical_form_parses() {
3583        assert_eq!(parse_sk_passport("AB1234567"), Some("AB1234567".into()));
3584    }
3585    #[test]
3586    fn sk_passport_rejects_bad_shape() {
3587        assert_eq!(parse_sk_passport("ABC123456"), None);
3588        assert_eq!(parse_sk_passport("AB12345"), None);
3589    }
3590}