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)