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
/// 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<i64> = 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<String>| 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()))
}