impl/typescript.ts
1,983 bytes · the TypeScript implementation · view raw
import { type NhsNumber } from "./validation_nhs_number_types.ts";
/** NHS Data Dictionary, NHS NUMBER: the first nine digits are weighted 10 down to 2. */
const WEIGHTS = [10, 9, 8, 7, 6, 5, 4, 3, 2];
function invalid(reason: string): NhsNumber {
return { valid: false, normalised: null, formatted: null, reason };
}
/**
* 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.
*/
export function validateNhsNumber(value: string): NhsNumber {
if (typeof value !== "string") return invalid("empty");
const digits: number[] = [];
for (const ch of value) {
// NHS numbers are displayed 3-3-4 with spaces, and some systems print
// hyphens; both are ignored anywhere.
if (ch === " " || ch === "-") continue;
if (ch < "0" || ch > "9") return invalid("bad-character");
digits.push(ch.charCodeAt(0) - 48);
}
if (digits.length === 0) return invalid("empty");
if (digits.length !== 10) return invalid("bad-length");
let total = 0;
for (let i = 0; i < 9; i++) total += digits[i] * WEIGHTS[i];
let 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 || 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 (digits.every((d) => d === digits[0])) return invalid("placeholder");
const text = digits.join("");
return { valid: true, normalised: text, formatted: `${text.slice(0, 3)} ${text.slice(3, 6)} ${text.slice(6)}`, reason: null };
}