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. """ifnot 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 == "-":
continueif ch < "0"or ch > "9":
return _invalid("bad-character")
digits.append(ord(ch) - 48)
ifnot 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 = 0if check == 10or 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)