Functional Weave
Code in Python

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.

1.0.0 (not the latest) · 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

  • uk_company_number(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
  • uk_company_number(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
  • uk_company_number(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.

def uk_company_number(value: str) -> UkCompanyNumber
valuestringa company number in any case, optionally spaced; leading zeros may be missing
returnsUkCompanyNumber

The type it declares, generated into your project

@dataclass(frozen=True)
class UkCompanyNumber:
    """Everything but valid and reason is null when valid is false."""

    valid: bool
    #: eight characters, upper case: "SC002180", "02050399"
    normalised: Optional[str]
    #: the two-character prefix, or "" for a plain eight-digit number
    prefix: Optional[str]
    #: england-wales, scotland, northern-ireland or united-kingdom
    jurisdiction: Optional[str]
    #: what the prefix registers, in words
    company_type: Optional[str]
    #: null when valid; empty, bad-character, bad-length, bad-format or unknown-prefix
    reason: Optional[str]

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

from fune.validation.uk_company_number import uk_company_number  # validation.uk-company-number@^1
impl/python.py · 72 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.

from .validation_uk_company_number_data import COMPANY_PREFIXES  ← this capability’s own data, compiled from data/company-prefixes.json into the same file by fune build
from .validation_uk_company_number_types import UkCompanyNumber

#: Every company number is eight characters: eight digits, or a two-character
#: prefix and six digits.
_LENGTH = 8


def _invalid(reason: str) -> UkCompanyNumber:
    return UkCompanyNumber(valid=False, normalised=None, prefix=None, jurisdiction=None, company_type=None, reason=reason)


def _is_letter(ch: str) -> bool:
    return "A" <= ch <= "Z"


def _is_digit(ch: str) -> bool:
    return "0" <= ch <= "9"


def uk_company_number(value: str) -> UkCompanyNumber:
    """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.
    """
    if not isinstance(value, str):
        return _invalid("empty")
    compact = ""
    for ch in value:
        if ch == " ":
            continue
        # ASCII-only upper-casing, so every language folds identically.
        if "a" <= ch <= "z":
            ch = chr(ord(ch) - 32)
        if not _is_letter(ch) and not _is_digit(ch):
            return _invalid("bad-character")
        compact += ch
    if compact == "":
        return _invalid("empty")

    # R0 (pre-partition Northern Ireland) is the one prefix containing a digit.
    if compact.startswith("R0") or (len(compact) >= 2 and _is_letter(compact[0]) and _is_letter(compact[1])):
        prefix = compact[0:2]
    elif _is_letter(compact[0]):
        return _invalid("bad-format")
    else:
        prefix = ""
    digits = compact[len(prefix):]
    if not all(_is_digit(ch) for ch in digits):
        return _invalid("bad-format")
    width = _LENGTH - len(prefix)
    if len(digits) == 0 or len(digits) > 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 all(ch == "0" for ch in digits):
        return _invalid("bad-format")

    for row in COMPANY_PREFIXES:
        if row.prefix == prefix:
            return UkCompanyNumber(
                valid=True,
                normalised=prefix + digits.rjust(width, "0"),
                prefix=prefix,
                jurisdiction=row.jurisdiction,
                company_type=row.company_type,
                reason=None,
            )
    return _invalid("unknown-prefix")

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python 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 Python validation.uk-company-number-1.0.0-python.fune · 16,847 bytes sha256 bc60357a036ad0b269101d93621c00b92a5e8b15851853d618e8a3b10a82c5eb

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

The whole function, every language, is one file too: validation.uk-company-number-1.0.0.fune, 23,490 bytes, sha256 74bc6328c07a7c0020b5f2e8564b98c5de6506b72a6ead13b6d7260fbebd260a. 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

Files

PathBytes
README.md2,515
data/company-prefixes.json3,737
impl/python.py2,630
impl/rust.rs3,782
impl/typescript.ts2,582
vectors.json4,058