use super::funejson::Value; /// NHS Data Dictionary, NHS NUMBER: the first nine digits are weighted 10 /// down to 2. const WEIGHTS: [i64; 9] = [10, 9, 8, 7, 6, 5, 4, 3, 2]; fn invalid(reason: &str) -> NhsNumber { NhsNumber { valid: false, normalised: None, formatted: None, reason: Some(reason.to_string()), } } /// Check an NHS number's modulus 11 check digit. /// /// A pass means the number is well formed, not that it was issued or that it /// belongs to the patient in front of you: that is a Personal Demographics /// Service trace. Checks run in a fixed order (characters, length, check /// digit, placeholder) so every language reports the same first reason. pub fn validate_nhs_number(value: &str) -> NhsNumber { let mut digits: Vec = Vec::new(); for ch in value.chars() { // NHS numbers are displayed 3-3-4 with spaces, and some systems print // hyphens; both are ignored anywhere. if ch == ' ' || ch == '-' { continue; } if !ch.is_ascii_digit() { return invalid("bad-character"); } digits.push(ch as i64 - 48); } if digits.is_empty() { return invalid("empty"); } if digits.len() != 10 { return invalid("bad-length"); } let total: i64 = digits.iter().zip(WEIGHTS.iter()).map(|(d, w)| d * w).sum(); let mut check = 11 - total % 11; // 11 means a check digit of 0. 10 means no number with these first nine // digits is ever issued, so there is nothing the tenth digit can match. if check == 11 { check = 0; } if check == 10 || check != digits[9] { return invalid("bad-check-digit"); } // 0000000000 and 1111111111 both satisfy the arithmetic and are the // placeholders people type to get past a mandatory field. if digits.iter().all(|d| *d == digits[0]) { return invalid("placeholder"); } let text: String = digits.iter().map(|d| char::from(b'0' + *d as u8)).collect(); let formatted = format!("{} {} {}", &text[0..3], &text[3..6], &text[6..10]); NhsNumber { valid: true, normalised: Some(text), formatted: Some(formatted), 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 nhs_number_to_value(result: &NhsNumber) -> 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)), ("formatted", text(&result.formatted)), ("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. nhs_number_to_value(&validate_nhs_number(args[0].as_str())) }