Functional Weave
Code in TypeScript

validation.nhs-number@1.0.0

impl/typescript.ts

1,975 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 nhsNumber(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 };
}