use super::funejson::Value; /// The weights HMRC applies to the first seven digits, most significant first. /// They are 8 down to 2, which is why the last two digits (the check pair) get /// no weight of their own. const WEIGHTS: [i64; 7] = [8, 7, 6, 5, 4, 3, 2]; /// The offset HMRC added for registrations issued from roughly 2010 onwards, /// commonly called the "9755" variant after the constant in their published /// worked example. A number satisfies exactly one of the two checks, never /// both, because 55 is not a multiple of 97. const MODULUS_9755_OFFSET: i64 = 55; /// Strip spaces, fold to upper case and drop any "GB" prefix. /// /// The fold is ASCII-only, to match the TypeScript and Python siblings whose /// built-in case folding is Unicode-aware and would turn "ß" into "SS". /// Nothing in a VAT number is outside ASCII. fn compact(value: &str) -> String { // Only the ASCII space is ignored. Tabs and non-breaking spaces usually // arrive from a bad paste, and silently accepting them hides that. let folded: String = value .chars() .filter(|ch| *ch != ' ') .map(|ch| ch.to_ascii_uppercase()) .collect(); match folded.strip_prefix("GB") { Some(rest) => rest.to_string(), None => folded, } } /// Is this a valid UK VAT registration number? /// /// Nine digits, or twelve for a branch trader, optionally prefixed "GB" and /// optionally spaced. The last two of the first nine are a mod-97 check pair. /// /// A number that passes is arithmetically well formed. It is not proof that /// the trader is registered, still registered, or registered for the goods on /// the invoice: only HMRC's VAT number checker can tell you that, and for a /// reverse-charge or zero-rated supply you are expected to have asked. pub fn is_uk_vat_number(value: &str) -> bool { let normalised = compact(value); let chars: Vec = normalised.chars().collect(); // Nine digits is a standard registration; twelve is a branch trader, where // the final three identify the branch and take no part in the checksum. if chars.len() != 9 && chars.len() != 12 { return false; } let mut digits: Vec = Vec::with_capacity(chars.len()); for ch in &chars { if *ch < '0' || *ch > '9' { return false; } digits.push(*ch as i64 - 48); } // 000000000 satisfies the arithmetic and has never been issued to anyone. // Rejecting it costs nothing and stops an all-zero placeholder field from // reading as a valid registration. if digits[..9].iter().all(|d| *d == 0) { return false; } let mut total: i64 = 0; for i in 0..7 { total += digits[i] * WEIGHTS[i]; } let check = digits[7] * 10 + digits[8]; mod97_matches(total, check) || mod97_matches(total + MODULUS_9755_OFFSET, check) } /// HMRC's "97 check", written the way HMRC writes it: subtract 97 from the /// weighted total until the result is zero or negative, then compare the /// magnitude with the check pair. That is the same as `(-total).rem_euclid(97)` /// but this form is what a reviewer can hold against the published guidance. fn mod97_matches(total: i64, check: i64) -> bool { let mut remainder = total; while remainder > 0 { remainder -= 97; } remainder.abs() == check } /// The canonical form, "GB999 9999 99", or `None` if the number does not check /// out. Branch numbers keep their three-digit suffix. pub fn format_uk_vat_number(value: &str) -> Option { if !is_uk_vat_number(value) { return None; } let c = compact(value); let grouped = format!("GB{} {} {}", &c[0..3], &c[3..7], &c[7..9]); if c.len() == 12 { Some(format!("{} {}", grouped, &c[9..])) } else { Some(grouped) } } 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: not a VAT number. Value::Bool(is_uk_vat_number(args[0].as_str())) }