validation.uk-company-number@1.0.0
impl/typescript.ts
2,582 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { COMPANY_PREFIXES } from "./validation_uk_company_number_data.ts"; ← this capability’s own data, compiled from data/company-prefixes.json into the same file by fune build
import { type UkCompanyNumber } from "./validation_uk_company_number_types.ts";
/** Every company number is eight characters: eight digits, or a two-character prefix and six digits. */
const LENGTH = 8;
function invalid(reason: string): UkCompanyNumber {
return { valid: false, normalised: null, prefix: null, jurisdiction: null, companyType: null, reason };
}
function isLetter(ch: string): boolean {
return ch >= "A" && ch <= "Z";
}
function isDigit(ch: string): boolean {
return ch >= "0" && ch <= "9";
}
/**
* Check a Companies House company number's shape and prefix.
*
* There is no check digit, so a pass means the number is shaped like one
* Companies House issues under a prefix it uses, not that the company exists:
* that is a lookup on the register. Checks run in a fixed order (characters,
* shape, length, prefix) so every language gives the same first reason.
*/
export function ukCompanyNumber(value: string): UkCompanyNumber {
if (typeof value !== "string") return invalid("empty");
let compact = "";
for (let ch of value) {
if (ch === " ") continue;
// ASCII-only upper-casing, so every language folds identically.
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");
// R0 (pre-partition Northern Ireland) is the one prefix containing a digit.
let prefix: string;
if (compact.startsWith("R0") || (compact.length >= 2 && isLetter(compact[0]) && isLetter(compact[1]))) {
prefix = compact.slice(0, 2);
} else if (isLetter(compact[0])) {
return invalid("bad-format");
} else {
prefix = "";
}
const digits = compact.slice(prefix.length);
for (const ch of digits) {
if (!isDigit(ch)) return invalid("bad-format");
}
const width = LENGTH - prefix.length;
if (digits.length === 0 || digits.length > width) return invalid("bad-length");
// Leading zeros are part of the number and are often dropped when typed,
// so they are put back; a number of nothing but zeros is no company.
if (/^0+$/.test(digits)) return invalid("bad-format");
const row = COMPANY_PREFIXES.find((r) => r.prefix === prefix);
if (row === undefined) return invalid("unknown-prefix");
return {
valid: true,
normalised: prefix + digits.padStart(width, "0"),
prefix,
jurisdiction: row.jurisdiction,
companyType: row.companyType,
reason: null,
};
}