use super::funejson::Value; /// 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 uk_utr(value: &str) -> UkUtr { let mut compact: Vec = 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 = 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| 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(&uk_utr(args[0].as_str())) }