Functional Weave
Code in TypeScript

validation.uk-company-number

Check a Companies House company number's format and prefix (SC, NI, OC, LP...) and zero-pad it to eight characters.

2.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 18 tests, run in TypeScript, Python and Rust.

What it does

Checks that a Companies House company registration number (CRN) has the shape Companies House issues, with a prefix it uses, and returns it as the eight characters the register and its API expect, with what the prefix means.

A PASS IS NOT A COMPANY. Company numbers carry no check digit. A pass means the number is well formed; whether it is on the register, and whether it is still active, is a lookup on the Companies House register or API.

For example

  • validateUkCompanyNumber(02050399) → valid true, normalised 02050399, prefix , jurisdiction england-wales, company type Company registered in England and Wales, reason — Companies House's own example of a plain England and Wales number
  • validateUkCompanyNumber(2050399) → valid true, normalised 02050399, prefix , jurisdiction england-wales, company type Company registered in England and Wales, reason — the same number with its leading zero dropped is padded back
  • validateUkCompanyNumber(SC002180) → valid true, normalised SC002180, prefix SC, jurisdiction scotland, company type Company registered in Scotland, reason — Companies House's own Scottish example

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function validateUkCompanyNumber(value: string): UkCompanyNumber
valuestringa company number in any case, optionally spaced; leading zeros may be missing
returnsUkCompanyNumber

The type it declares, generated into your project

/** Everything but valid and reason is null when valid is false. */
export interface UkCompanyNumber {
  readonly valid: boolean;
  /** eight characters, upper case: "SC002180", "02050399" */
  readonly normalised: string | null;
  /** the two-character prefix, or "" for a plain eight-digit number */
  readonly prefix: string | null;
  /** england-wales, scotland, northern-ireland or united-kingdom */
  readonly jurisdiction: string | null;
  /** what the prefix registers, in words */
  readonly companyType: string | null;
  /** null when valid; empty, bad-character, bad-length, bad-format or unknown-prefix */
  readonly reason: string | null;
}

Your code names it in one line, in the file that uses it

import { validateUkCompanyNumber } from "#fune/validation.uk-company-number@^2";
impl/typescript.ts · 68 lines · open · 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 validateUkCompanyNumber(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,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add validation.uk-company-number
Download for TypeScript validation.uk-company-number-2.0.1-typescript.fune · 17,620 bytes sha256 ba59b36fcacc33410f7b1c06cb5de27def394a64535fcbfea0d25a249b6cba09

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./validation.uk-company-number-2.0.1-typescript.fune, or fetch it from a terminal with fune pull validation.uk-company-number@2.0.1:typescript.

The whole function, every language, is one file too: validation.uk-company-number-2.0.1.fune, 24,336 bytes, sha256 4d2865bda769e6a35cd0e59dded3a4d0bd0bbe286318e4d41df49acf58204951. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before validation.uk-company-number

after — your function gets the result and the arguments, and returns the final result.

// fune: after validation.uk-company-number

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show validation.uk-company-number --steps.

// fune: step validation.uk-company-number after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
Companies House's own example of a plain England and Wales number 02050399 → valid true, normalised 02050399, prefix , jurisdiction england-wales, company type Company registered in England and Wales, reason —
the same number with its leading zero dropped is padded back 2050399 → valid true, normalised 02050399, prefix , jurisdiction england-wales, company type Company registered in England and Wales, reason —
Companies House's own Scottish example SC002180 → valid true, normalised SC002180, prefix SC, jurisdiction scotland, company type Company registered in Scotland, reason —
lower case, spaced and short of its zeros sc 2180 → valid true, normalised SC002180, prefix SC, jurisdiction scotland, company type Company registered in Scotland, reason —
a Northern Ireland company NI012345 → valid true, normalised NI012345, prefix NI, jurisdiction northern-ireland, company type Company registered in Northern Ireland, reason —
an England and Wales LLP OC301234 → valid true, normalised OC301234, prefix OC, jurisdiction england-wales, company type Limited liability partnership, reason —
a Scottish limited partnership SL012345 → valid true, normalised SL012345, prefix SL, jurisdiction scotland, company type Limited partnership, reason —
an overseas company FC012345 → valid true, normalised FC012345, prefix FC, jurisdiction united-kingdom, company type Overseas company, reason —
R0, the one prefix with a digit in it, for pre-partition Northern Ireland companies R0000123 → valid true, normalised R0000123, prefix R0, jurisdiction northern-ireland, company type Northern Ireland company (pre-partition), reason —
a two-letter prefix Companies House does not use XX123456 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason unknown-prefix
Show the other 8 tests
CaseArgumentsExpected
nine digits 123456789 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-length
a prefix followed by seven digits SC1234567 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-length
a prefix with no digits LP → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-length
a single letter is not a prefix S1234567 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-format
letters in the middle of the number 12AB3456 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-format
all zeros is no company 00000000 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-format
a hyphen is not accepted SC-002180 → valid false, normalised —, prefix —, jurisdiction —, company type —, reason bad-character
the empty string → valid false, normalised —, prefix —, jurisdiction —, company type —, reason empty

More from the author

## The format

Eight characters: eight digits for a company registered in England and Wales, or a two-character prefix and six digits for everything else (SC Scotland, NI Northern Ireland, OC/SO/NC limited liability partnerships, LP/SL/NL limited partnerships, FC overseas companies, and so on). R0, for pre-partition Northern Ireland companies, is the one prefix with a digit in it.

Leading zeros are part of the number, and dropping them names a different company or none. Because people drop them when typing, a number short of its zeros is padded back: "2050399" becomes "02050399" and "sc 2180" becomes "SC002180". A number of nothing but zeros is refused.

## The prefix table (data)

`data/company-prefixes.json` holds every prefix in Companies House's list of company numbers and prefixes, including the ones for which Companies House holds only the name and number (industrial and provident societies, ICVCs, royal charter companies, credit unions). A prefix not in the table is `unknown-prefix`. Companies House does add prefixes from time to time, so an unknown prefix can be a new one. A prefix added in future is a data release of this capability.

## Input and result

Case is ignored and ASCII spaces are ignored anywhere. Anything else that is not an ASCII letter or digit, hyphens included, is `bad-character`. Validators answer rather than throw.

| reason | meaning (checked in this order) | |---|---| | `empty` | nothing but spaces, or not a string | | `bad-character` | a character other than an ASCII letter, digit or space | | `bad-format` | a single leading letter, letters after the prefix, or all zeros | | `bad-length` | no digits, or more than eight characters in all | | `unknown-prefix` | a prefix not in the table |

## Source

Companies House, "Uniform Resource Identifiers (URI) Customer Guide", version 1.1, section "List of Company Numbers and Prefixes": https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/426891/uniformResourceIdentifiersCustomerGuide.pdf

2.0.0 renames the function from `ukCompanyNumber` to `validateUkCompanyNumber` (`validate_uk_company_number` in Python and Rust), so every validator that returns a result record is `validateX` and every one that returns a bool is `isX`. Nothing else changed; 1.0.0 stays published under the old name.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

2.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE191
README.md3,075
data/company-prefixes.json3,737
impl/python.py2,639
impl/rust.rs3,800
impl/typescript.ts2,590
vectors.json4,058