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