Functional Weave
Code in Python

validation.uk-utr@2.0.0

impl/rust.rs

2,892 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

/// Weights for the second to tenth digits; the first digit is the check digit.
/// From HMRC's own reference checker (hmrc/domain, UtrReferenceChecker).
const WEIGHTS: [i64; 9] = [6, 7, 8, 9, 10, 5, 4, 3, 2];

/// The check digit for each remainder of the weighted sum modulo 11. It is
/// 11 - remainder except that remainders 0 and 1 give 2 and 1, so a check
/// digit is always a single digit and no reference is impossible.
const CHECK_DIGITS: [i64; 11] = [2, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1];

fn invalid(reason: &str) -> UkUtr {
    UkUtr {
        valid: false,
        normalised: None,
        reason: Some(reason.to_string()),
    }
}

/// Check a Unique Taxpayer Reference (Self Assessment or Corporation Tax).
///
/// A pass means the reference is well formed, not that HMRC issued it to
/// this taxpayer. Checks run in a fixed order (characters, length, check
/// digit) so every language gives the same first reason.
pub fn validate_uk_utr(value: &str) -> UkUtr {
    let mut compact: Vec<char> = value.chars().filter(|ch| *ch != ' ').collect();
    if compact.is_empty() {
        return invalid("empty");
    }
    // Self Assessment shows the UTR with a K after it and some letters put
    // it in front; one K at either end is stripped, as HMRC's checker does.
    if compact[0] == 'K' || compact[0] == 'k' {
        compact.remove(0);
    } else if compact[compact.len() - 1] == 'K' || compact[compact.len() - 1] == 'k' {
        compact.pop();
    }

    let mut digits: Vec<i64> = Vec::new();
    for ch in &compact {
        if !ch.is_ascii_digit() {
            return invalid("bad-character");
        }
        digits.push(*ch as i64 - 48);
    }
    if digits.len() == 13 {
        return invalid("thirteen-digits");
    }
    if digits.len() != 10 {
        return invalid("bad-length");
    }

    let total: i64 = digits[1..].iter().zip(WEIGHTS.iter()).map(|(d, w)| d * w).sum();
    if CHECK_DIGITS[(total % 11) as usize] != digits[0] {
        return invalid("bad-check-digit");
    }
    UkUtr {
        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_utr_to_value(result: &UkUtr) -> 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_utr_to_value(&validate_uk_utr(args[0].as_str()))
}