Functional Weave
Code in Python

validation.uk-ni-number@1.0.0

impl/typescript.ts

2,284 bytes · the TypeScript implementation · view raw

import { type UkNiNumber } from "./validation_uk_ni_number_types.ts";

/** HMRC NIM39110: D, F, I, Q, U and V are never either letter of a prefix, and O is never the second. */
const BARRED_FIRST = "DFIQUV";
const BARRED_SECOND = "DFIOQUV";

/** Prefixes made of allowed letters that HMRC still never issues. */
const BARRED_PREFIXES = ["BG", "GB", "KN", "NK", "NT", "TN", "ZZ"];

/** The suffix records the quarter a contribution card was once returned in, and is always one of these four. */
const SUFFIXES = "ABCD";

function invalid(reason: string): UkNiNumber {
  return { valid: false, normalised: null, reason };
}

function isLetter(ch: string): boolean {
  return ch >= "A" && ch <= "Z";
}

function isDigit(ch: string): boolean {
  return ch >= "0" && ch <= "9";
}

/**
 * Check a National Insurance number's shape and prefix rules.
 *
 * There is no check digit in a NINO, so a pass means "could have been issued",
 * never "belongs to this person". The checks run in a fixed order (characters,
 * length, shape, prefix, suffix) so that every language reports the same first
 * reason.
 */
export function ukNiNumber(value: string): UkNiNumber {
  if (typeof value !== "string") return invalid("empty");
  let compact = "";
  for (let ch of value) {
    // Only the ASCII space is ignored: NINOs are printed in pairs.
    if (ch === " ") continue;
    // ASCII-only upper-casing, so no language's Unicode rules can turn a stray
    // character into a letter that passes.
    if (ch >= "a" && ch <= "z") ch = String.fromCharCode(ch.charCodeAt(0) - 32);
    if (!isLetter(ch) && !isDigit(ch)) return invalid("bad-character");
    compact += ch;
  }
  if (compact === "") return invalid("empty");
  if (compact.length !== 9) return invalid("bad-length");
  if (!isLetter(compact[0]) || !isLetter(compact[1]) || !isLetter(compact[8])) return invalid("bad-format");
  for (let i = 2; i < 8; i++) {
    if (!isDigit(compact[i])) return invalid("bad-format");
  }
  if (
    BARRED_FIRST.includes(compact[0]) ||
    BARRED_SECOND.includes(compact[1]) ||
    BARRED_PREFIXES.includes(compact.slice(0, 2))
  ) {
    return invalid("bad-prefix");
  }
  if (!SUFFIXES.includes(compact[8])) return invalid("bad-suffix");
  return { valid: true, normalised: compact, reason: null };
}