Functional Weave
Code in Python

validation.uk-vat-number@1.0.0

impl/rust.rs

4,095 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

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