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 numberuk_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 backuk_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.
pub fn uk_company_number(value: &str) -> UkCompanyNumber
| value | string | a company number in any case, optionally spaced; leading zeros may be missing |
| returns | UkCompanyNumber |
The type it declares, generated into your project
/// Everything but valid and reason is null when valid is false.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UkCompanyNumber {
pub valid: bool,
/// eight characters, upper case: "SC002180", "02050399"
pub normalised: Option<String>,
/// the two-character prefix, or "" for a plain eight-digit number
pub prefix: Option<String>,
/// england-wales, scotland, northern-ireland or united-kingdom
pub jurisdiction: Option<String>,
/// what the prefix registers, in words
pub company_type: Option<String>,
/// null when valid; empty, bad-character, bad-length, bad-format or unknown-prefix
pub reason: Option<String>,
}
Your code names it in one line, in the file that uses it
fune!(validation.uk-company-number@^1); // then call uk_company_number(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::validation_uk_company_number_data::COMPANY_PREFIXES; ← this capability’s own data, compiled from data/company-prefixes.json into the same file by fune build
/// Every company number is eight characters: eight digits, or a two-character
/// prefix and six digits.
const LENGTH: usize = 8;
fn invalid(reason: &str) -> UkCompanyNumber {
UkCompanyNumber {
valid: false,
normalised: None,
prefix: None,
jurisdiction: None,
company_type: None,
reason: Some(reason.to_string()),
}
}
/// 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.
pub fn uk_company_number(value: &str) -> UkCompanyNumber {
let mut compact = String::new();
for ch in value.chars() {
if ch == ' ' {
continue;
}
// ASCII-only upper-casing, so every language folds identically.
let ch = ch.to_ascii_uppercase();
if !ch.is_ascii_uppercase() && !ch.is_ascii_digit() {
return invalid("bad-character");
}
compact.push(ch);
}
if compact.is_empty() {
return invalid("empty");
}
// Everything is ASCII by now, so byte indexing is character indexing.
let bytes = compact.as_bytes();
// R0 (pre-partition Northern Ireland) is the one prefix containing a digit.
let prefix_len = if compact.starts_with("R0")
|| (bytes.len() >= 2 && bytes[0].is_ascii_uppercase() && bytes[1].is_ascii_uppercase())
{
2
} else if bytes[0].is_ascii_uppercase() {
return invalid("bad-format");
} else {
0
};
let prefix = &compact[..prefix_len];
let digits = &compact[prefix_len..];
if !digits.bytes().all(|b| b.is_ascii_digit()) {
return invalid("bad-format");
}
let width = LENGTH - prefix_len;
if digits.is_empty() || digits.len() > 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 digits.bytes().all(|b| b == b'0') {
return invalid("bad-format");
}
match COMPANY_PREFIXES.iter().find(|row| row.prefix == prefix) {
Some(row) => UkCompanyNumber {
valid: true,
normalised: Some(format!("{}{}{}", prefix, "0".repeat(width - digits.len()), digits)),
prefix: Some(prefix.to_string()),
jurisdiction: Some(row.jurisdiction.to_string()),
company_type: Some(row.company_type.to_string()),
reason: None,
},
None => invalid("unknown-prefix"),
}
}
/// Object keys are camelCase to match the shared vectors, and so that a
/// capability building on this one can reuse the same shape.
pub fn uk_company_number_to_value(result: &UkCompanyNumber) -> Value {
let text = |field: &Option<String>| match field {
Some(s) => Value::str(s),
None => Value::Null,
};
Value::obj(vec![
("valid", Value::Bool(result.valid)),
("normalised", text(&result.normalised)),
("prefix", text(&result.prefix)),
("jurisdiction", text(&result.jurisdiction)),
("companyType", text(&result.company_type)),
("reason", text(&result.reason)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
// A non-string argument arrives here as an empty string, which is exactly
// the answer TypeScript and Python give for a non-string: empty.
uk_company_number_to_value(&uk_company_number(args[0].as_str()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add validation.uk-company-number
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./validation.uk-company-number-1.0.0-rust.fune, or fetch it from a terminal with fune pull validation.uk-company-number@1.0.0:rust.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 2,515 |
| data/company-prefixes.json | 3,737 |
| impl/python.py | 2,630 |
| impl/rust.rs | 3,782 |
| impl/typescript.ts | 2,582 |
| vectors.json | 4,058 |