Functional Weave
Code in Rust

validation.nhs-number@2.0.0

impl/python.py

2,020 bytes · the Python implementation · view raw

from typing import List

from .validation_nhs_number_types import NhsNumber

#: NHS Data Dictionary, NHS NUMBER: the first nine digits are weighted 10
#: down to 2.
_WEIGHTS = (10, 9, 8, 7, 6, 5, 4, 3, 2)


def _invalid(reason: str) -> NhsNumber:
    return NhsNumber(valid=False, normalised=None, formatted=None, reason=reason)


def validate_nhs_number(value: str) -> NhsNumber:
    """Check an NHS number's modulus 11 check digit.

    A pass means the number is well formed, not that it was issued or that it
    belongs to the patient in front of you: that is a Personal Demographics
    Service trace. Checks run in a fixed order (characters, length, check
    digit, placeholder) so every language reports the same first reason.
    """
    if not isinstance(value, str):
        return _invalid("empty")
    digits: List[int] = []
    for ch in value:
        # NHS numbers are displayed 3-3-4 with spaces, and some systems print
        # hyphens; both are ignored anywhere.
        if ch == " " or ch == "-":
            continue
        if ch < "0" or ch > "9":
            return _invalid("bad-character")
        digits.append(ord(ch) - 48)
    if not digits:
        return _invalid("empty")
    if len(digits) != 10:
        return _invalid("bad-length")

    total = sum(d * w for d, w in zip(digits, _WEIGHTS))
    check = 11 - total % 11
    # 11 means a check digit of 0. 10 means no number with these first nine
    # digits is ever issued, so there is nothing the tenth digit can match.
    if check == 11:
        check = 0
    if check == 10 or check != digits[9]:
        return _invalid("bad-check-digit")

    # 0000000000 and 1111111111 both satisfy the arithmetic and are the
    # placeholders people type to get past a mandatory field.
    if all(d == digits[0] for d in digits):
        return _invalid("placeholder")

    text = "".join(chr(d + 48) for d in digits)
    return NhsNumber(valid=True, normalised=text, formatted="%s %s %s" % (text[0:3], text[3:6], text[6:10]), reason=None)