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