Functional Weave
Code in Rust

validation.uk-ni-number@1.0.0

impl/python.py

2,242 bytes · the Python implementation · view raw

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)