Functional Weave
Code in Python

validation.uk-ni-number@2.0.0

impl/rust.rs

3,158 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// 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 validate_uk_ni_number(value: &str) -> UkNiNumber {
    let mut compact: Vec<char> = 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<String>| 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(&validate_uk_ni_number(args[0].as_str()))
}