use super::funejson::Value; /// HMRC NIM39110: D, F, I, Q, U and V are never either letter of a prefix, /// and O is never the second. const BARRED_FIRST: &str = "DFIQUV"; const BARRED_SECOND: &str = "DFIOQUV"; /// Prefixes made of allowed letters that HMRC still never issues. const BARRED_PREFIXES: [&str; 7] = ["BG", "GB", "KN", "NK", "NT", "TN", "ZZ"]; /// The suffix records the quarter a contribution card was once returned in, /// and is always one of these four. const SUFFIXES: &str = "ABCD"; fn invalid(reason: &str) -> UkNiNumber { UkNiNumber { valid: false, normalised: None, reason: Some(reason.to_string()), } } /// Check a National Insurance number's shape and prefix rules. /// /// There is no check digit in a NINO, so a pass means "could have been /// issued", never "belongs to this person". The checks run in a fixed order /// (characters, length, shape, prefix, suffix) so that every language reports /// the same first reason. pub fn uk_ni_number(value: &str) -> UkNiNumber { let mut compact: Vec = Vec::new(); for ch in value.chars() { // Only the ASCII space is ignored: NINOs are printed in pairs. if ch == ' ' { continue; } // ASCII-only upper-casing, so no language's Unicode rules can turn a // stray character into a letter that passes. let ch = ch.to_ascii_uppercase(); if !ch.is_ascii_uppercase() && !ch.is_ascii_digit() { return invalid("bad-character"); } compact.push(ch); } if compact.is_empty() { return invalid("empty"); } if compact.len() != 9 { return invalid("bad-length"); } if !compact[0].is_ascii_uppercase() || !compact[1].is_ascii_uppercase() || !compact[8].is_ascii_uppercase() { return invalid("bad-format"); } if !compact[2..8].iter().all(|ch| ch.is_ascii_digit()) { return invalid("bad-format"); } let prefix: String = compact[0..2].iter().collect(); if BARRED_FIRST.contains(compact[0]) || BARRED_SECOND.contains(compact[1]) || BARRED_PREFIXES.contains(&prefix.as_str()) { return invalid("bad-prefix"); } if !SUFFIXES.contains(compact[8]) { return invalid("bad-suffix"); } UkNiNumber { valid: true, normalised: Some(compact.iter().collect()), reason: None, } } /// Object keys are camelCase to match the shared vectors, and so that a /// capability building on this one can reuse the same shape. pub fn uk_ni_number_to_value(result: &UkNiNumber) -> Value { let text = |field: &Option| match field { Some(s) => Value::str(s), None => Value::Null, }; Value::obj(vec![ ("valid", Value::Bool(result.valid)), ("normalised", text(&result.normalised)), ("reason", text(&result.reason)), ]) } pub fn fune_vector(args: &[Value]) -> Value { // A non-string argument arrives here as an empty string, which is exactly // the answer TypeScript and Python give for a non-string: empty. uk_ni_number_to_value(&uk_ni_number(args[0].as_str())) }