validation.uk-utr
Check an HMRC Unique Taxpayer Reference's modulus 11 check digit, accepting the K and spacing people type.
2.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 the check digit of an HMRC Unique Taxpayer Reference (UTR), the ten digit reference for Self Assessment, partnerships and Corporation Tax, and returns it as ten plain digits.
A PASS IS NOT A TAXPAYER. The check digit catches most typing mistakes; it does not say HMRC issued the reference, or issued it to this person or company.
For example
validate_uk_utr(2234567890)→ valid true, normalised 2234567890, reason — HMRC's own valid test reference, remainder 0 so the check digit is 2validate_uk_utr(2108834503)→ valid true, normalised 2108834503, reason — another HMRC test reference, remainder 9 so the check digit is 2 againvalidate_uk_utr(1097172564)→ valid true, normalised 1097172564, reason — HMRC test reference with remainder 1, so the check digit is 1
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 validate_uk_utr(value: &str) -> UkUtr
| value | string | ten digits, optionally spaced, optionally with a K at the start or end |
| returns | UkUtr |
The type it declares, generated into your project
/// normalised is null when valid is false.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UkUtr {
pub valid: bool,
/// ten digits, no spaces and no K
pub normalised: Option<String>,
/// null when valid; empty, bad-character, bad-length, thirteen-digits or bad-check-digit
pub reason: Option<String>,
}
Your code names it in one line, in the file that uses it
fune!(validation.uk-utr@^2); // then call validate_uk_utr(…)
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
/// Weights for the second to tenth digits; the first digit is the check digit.
/// From HMRC's own reference checker (hmrc/domain, UtrReferenceChecker).
const WEIGHTS: [i64; 9] = [6, 7, 8, 9, 10, 5, 4, 3, 2];
/// The check digit for each remainder of the weighted sum modulo 11. It is
/// 11 - remainder except that remainders 0 and 1 give 2 and 1, so a check
/// digit is always a single digit and no reference is impossible.
const CHECK_DIGITS: [i64; 11] = [2, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1];
fn invalid(reason: &str) -> UkUtr {
UkUtr {
valid: false,
normalised: None,
reason: Some(reason.to_string()),
}
}
/// Check a Unique Taxpayer Reference (Self Assessment or Corporation Tax).
///
/// A pass means the reference is well formed, not that HMRC issued it to
/// this taxpayer. Checks run in a fixed order (characters, length, check
/// digit) so every language gives the same first reason.
pub fn validate_uk_utr(value: &str) -> UkUtr {
let mut compact: Vec<char> = value.chars().filter(|ch| *ch != ' ').collect();
if compact.is_empty() {
return invalid("empty");
}
// Self Assessment shows the UTR with a K after it and some letters put
// it in front; one K at either end is stripped, as HMRC's checker does.
if compact[0] == 'K' || compact[0] == 'k' {
compact.remove(0);
} else if compact[compact.len() - 1] == 'K' || compact[compact.len() - 1] == 'k' {
compact.pop();
}
let mut digits: Vec<i64> = Vec::new();
for ch in &compact {
if !ch.is_ascii_digit() {
return invalid("bad-character");
}
digits.push(*ch as i64 - 48);
}
if digits.len() == 13 {
return invalid("thirteen-digits");
}
if digits.len() != 10 {
return invalid("bad-length");
}
let total: i64 = digits[1..].iter().zip(WEIGHTS.iter()).map(|(d, w)| d * w).sum();
if CHECK_DIGITS[(total % 11) as usize] != digits[0] {
return invalid("bad-check-digit");
}
UkUtr {
valid: true,
normalised: Some(compact.iter().collect()),
reason: None,
}
}
/// 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_utr_to_value(result: &UkUtr) -> 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)),
("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_utr_to_value(&validate_uk_utr(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-utr
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./validation.uk-utr-2.0.0-rust.fune, or fetch it from a terminal with fune pull validation.uk-utr@2.0.0:rust.
The whole function, every language, is one file too: validation.uk-utr-2.0.0.fune, 14,818 bytes, sha256 f866c1a37886789ae2812344db45fc881ddf574927be7389c4fc7aa24e409948. 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-utr
after — your function gets the result and the arguments, and returns the final result.
// fune: after validation.uk-utr
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-utr --steps.
// fune: step validation.uk-utr 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 | |
|---|---|---|---|
| HMRC's own valid test reference, remainder 0 so the check digit is 2 | 2234567890 | → | valid true, normalised 2234567890, reason — |
| another HMRC test reference, remainder 9 so the check digit is 2 again | 2108834503 | → | valid true, normalised 2108834503, reason — |
| HMRC test reference with remainder 1, so the check digit is 1 | 1097172564 | → | valid true, normalised 1097172564, reason — |
| remainder 2 gives check digit 9 | 9000000001 | → | valid true, normalised 9000000001, reason — |
| spaced as HMRC letters print it | 22345 67890 | → | valid true, normalised 2234567890, reason — |
| a trailing K, as Self Assessment online shows it | 2234567890K | → | valid true, normalised 2234567890, reason — |
| a leading lower-case k | k1097172564 | → | valid true, normalised 1097172564, reason — |
| a trailing lower-case k and stray spaces | 1097172564k | → | valid true, normalised 1097172564, reason — |
| HMRC's example 1234567890 is deliberately not a valid reference | 1234567890 | → | valid false, normalised —, reason bad-check-digit |
| a valid reference with two digits transposed | 2243567890 | → | valid false, normalised —, reason bad-check-digit |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| thirteen digits: HMRC does not publish which three are extra, so they are not guessed | 1234567890123 | → | valid false, normalised —, reason thirteen-digits |
| eleven digits | 12345678905 | → | valid false, normalised —, reason bad-length |
| six digits | 123456 | → | valid false, normalised —, reason bad-length |
| a K alone | K | → | valid false, normalised —, reason bad-length |
| a letter after the trailing K | 1097172564KZ | → | valid false, normalised —, reason bad-character |
| K at both ends: only one is ever stripped | K2234567890K | → | valid false, normalised —, reason bad-character |
| a hyphen is not accepted | 22345-67890 | → | valid false, normalised —, reason bad-character |
| the empty string | → | valid false, normalised —, reason empty |
More from the author
## The check
The first digit is the check digit. Multiply the second to tenth digits by 6, 7, 8, 9, 10, 5, 4, 3 and 2, add them up and take the remainder on dividing by 11. The check digit for remainders 0 to 10 is 2, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1: that is 11 minus the remainder, except that remainders 0 and 1 give 2 and 1. The often-quoted shortcut "(11 - remainder) mod 11" gets those two remainders wrong and rejects real references such as HMRC's own test reference 2234567890.
HMRC has not published the algorithm as a specification. The weights and the remainder table here are taken from HMRC's own open-source reference checker, and its valid and invalid test references are vectors here.
## Input
HMRC's design pattern for asking for a UTR allows spaces and a K at the start or end ("1234567890K" is how Self Assessment online shows it). ASCII spaces are ignored anywhere, and one K (either case) is removed from the start or, failing that, the end. Anything else is `bad-character`.
The same pattern allows 13-digit forms and says to remove "extra digits", but does not say which three digits are extra. Rather than guess, a 13-digit value is refused with its own reason, `thirteen-digits`, so a form can ask for the ten-digit reference.
| reason | meaning (checked in this order) | |---|---| | `empty` | nothing but spaces, or not a string | | `bad-character` | a character other than a digit or space, after one K is removed | | `thirteen-digits` | a 13-digit form (see above) | | `bad-length` | any other length than ten digits | | `bad-check-digit` | the first digit does not match |
## Sources
- HMRC, hmrc/domain on GitHub, `referencechecker/ReferenceChecker.scala` (`UtrReferenceChecker`, `SelfAssessmentReferenceChecker`) and `ModulusCheckerSpec.scala`, read 23 September 2026: https://github.com/hmrc/domain - HMRC design patterns, "Unique Taxpayer Reference": https://design.tax.service.gov.uk/hmrc-design-patterns/unique-taxpayer-reference/
2.0.0 renames the function from `ukUtr` to `validateUkUtr` (`validate_uk_utr` 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.
Files
| Path | Bytes |
|---|---|
| README.md | 2,610 |
| impl/python.py | 1,936 |
| impl/rust.rs | 2,892 |
| impl/typescript.ts | 2,005 |
| vectors.json | 2,799 |