Functional Weave
Code in TypeScript

validation.uk-vat-number@1.0.0

impl/python.py

3,627 bytes · the Python implementation · view raw

from typing import List, Optional

#: 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.
_WEIGHTS = (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.
_MODULUS_9755_OFFSET = 55


def _ascii_upper(ch: str) -> str:
    """Uppercase ASCII only.

    ``str.upper()`` is Unicode-aware: it turns "ß" into "SS" and would make
    this function disagree with the Rust sibling, which has no such machinery.
    Nothing in a VAT number is outside ASCII, so restricting the fold keeps
    all three languages honest.
    """
    return chr(ord(ch) - 32) if "a" <= ch <= "z" else ch


def _compact(value: str) -> str:
    # Only the ASCII space is ignored. Tabs and non-breaking spaces usually
    # arrive from a bad paste, and silently accepting them hides that.
    out = "".join(_ascii_upper(ch) for ch in value if ch != " ")
    return out[2:] if out.startswith("GB") else out


def is_uk_vat_number(value: str) -> bool:
    """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.
    """
    if not isinstance(value, str):
        return False

    compact = _compact(value)

    # 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 len(compact) != 9 and len(compact) != 12:
        return False

    digits: List[int] = []
    for ch in compact:
        if ch < "0" or ch > "9":
            return False
        digits.append(ord(ch) - 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 all(d == 0 for d in digits[:9]):
        return False

    total = sum(digits[i] * _WEIGHTS[i] for i in range(7))
    check = digits[7] * 10 + digits[8]
    return _mod97_matches(total, check) or _mod97_matches(total + _MODULUS_9755_OFFSET, check)


def _mod97_matches(total: int, check: int) -> bool:
    """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) % 97`` but
    this form is what a reviewer can hold against the published guidance.
    """
    remainder = total
    while remainder > 0:
        remainder -= 97
    return abs(remainder) == check


def format_uk_vat_number(value: str) -> Optional[str]:
    """The canonical form, "GB999 9999 99", or None if the number does not
    check out. Branch numbers keep their three-digit suffix.
    """
    if not is_uk_vat_number(value):
        return None
    compact = _compact(value)
    grouped = "GB%s %s %s" % (compact[0:3], compact[3:7], compact[7:9])
    return grouped + " " + compact[9:] if len(compact) == 12 else grouped