from .validation_uk_ni_number_types import UkNiNumber #: HMRC NIM39110: D, F, I, Q, U and V are never either letter of a prefix, #: and O is never the second. _BARRED_FIRST = "DFIQUV" _BARRED_SECOND = "DFIOQUV" #: Prefixes made of allowed letters that HMRC still never issues. _BARRED_PREFIXES = ("BG", "GB", "KN", "NK", "NT", "TN", "ZZ") #: The suffix records the quarter a contribution card was once returned in, #: and is always one of these four. _SUFFIXES = "ABCD" def _invalid(reason: str) -> UkNiNumber: return UkNiNumber(valid=False, normalised=None, reason=reason) def _is_letter(ch: str) -> bool: return "A" <= ch <= "Z" def _is_digit(ch: str) -> bool: return "0" <= ch <= "9" def uk_ni_number(value: str) -> UkNiNumber: """Check a National Insurance number's shape and prefix rules. There is no check digit in a NINO, so a pass means "could have been issued", never "belongs to this person". The checks run in a fixed order (characters, length, shape, prefix, suffix) so that every language reports the same first reason. """ if not isinstance(value, str): return _invalid("empty") compact = "" for ch in value: # Only the ASCII space is ignored: NINOs are printed in pairs. if ch == " ": continue # ASCII-only upper-casing, so no language's Unicode rules can turn a # stray character into a letter that passes. if "a" <= ch <= "z": ch = chr(ord(ch) - 32) if not _is_letter(ch) and not _is_digit(ch): return _invalid("bad-character") compact += ch if compact == "": return _invalid("empty") if len(compact) != 9: return _invalid("bad-length") if not (_is_letter(compact[0]) and _is_letter(compact[1]) and _is_letter(compact[8])): return _invalid("bad-format") if not all(_is_digit(ch) for ch in compact[2:8]): return _invalid("bad-format") if compact[0] in _BARRED_FIRST or compact[1] in _BARRED_SECOND or compact[0:2] in _BARRED_PREFIXES: return _invalid("bad-prefix") if compact[8] not in _SUFFIXES: return _invalid("bad-suffix") return UkNiNumber(valid=True, normalised=compact, reason=None)