impl/typescript.ts
2,292 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 validateUkNiNumber(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 };
}