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()))
}