Functional Weave
Code in TypeScript

validation.uk-utr

Check an HMRC Unique Taxpayer Reference's modulus 11 check digit, accepting the K and spacing people type.

2.0.1 · 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

  • validateUkUtr(2234567890) → valid true, normalised 2234567890, reason — HMRC's own valid test reference, remainder 0 so the check digit is 2
  • validateUkUtr(2108834503) → valid true, normalised 2108834503, reason — another HMRC test reference, remainder 9 so the check digit is 2 again
  • validateUkUtr(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.

export function validateUkUtr(value: string): UkUtr
valuestringten digits, optionally spaced, optionally with a K at the start or end
returnsUkUtr

The type it declares, generated into your project

/** normalised is null when valid is false. */
export interface UkUtr {
  readonly valid: boolean;
  /** ten digits, no spaces and no K */
  readonly normalised: string | null;
  /** null when valid; empty, bad-character, bad-length, thirteen-digits or bad-check-digit */
  readonly reason: string | null;
}

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

import { validateUkUtr } from "#fune/validation.uk-utr@^2";
impl/typescript.ts · 49 lines · open · raw
import { type UkUtr } from "./validation_uk_utr_types.ts";

/**
 * 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 = [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 = [2, 1, 9, 8, 7, 6, 5, 4, 3, 2, 1];

function invalid(reason: string): UkUtr {
  return { valid: false, normalised: null, reason };
}

/**
 * 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.
 */
export function validateUkUtr(value: string): UkUtr {
  if (typeof value !== "string") return invalid("empty");
  let compact = "";
  for (const ch of value) if (ch !== " ") compact += ch;
  if (compact === "") 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 = compact.slice(1);
  else if (compact.endsWith("K") || compact.endsWith("k")) compact = compact.slice(0, -1);

  const digits: number[] = [];
  for (const ch of compact) {
    if (ch < "0" || ch > "9") return invalid("bad-character");
    digits.push(ch.charCodeAt(0) - 48);
  }
  if (digits.length === 13) return invalid("thirteen-digits");
  if (digits.length !== 10) return invalid("bad-length");

  let total = 0;
  for (let i = 0; i < 9; i++) total += digits[i + 1] * WEIGHTS[i];
  if (CHECK_DIGITS[total % 11] !== digits[0]) return invalid("bad-check-digit");
  return { valid: true, normalised: compact, 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-utr
Download for TypeScript validation.uk-utr-2.0.1-typescript.fune · 10,587 bytes sha256 e7764f490f70e5551bde98dfadf10b4a71eabbf063f97e0c77e14056bcf13c17

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

The whole function, every language, is one file too: validation.uk-utr-2.0.1.fune, 15,621 bytes, sha256 e5dd2f390ffd7aa5941f98bbbec6482e0408926ba489ebfb849fb1f8d16a147f. 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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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.

## Notices

The UTR check method follows HMRC's domain library (https://github.com/hmrc/domain, referencechecker/ReferenceChecker.scala), Copyright HM Revenue & Customs, licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0). This capability implements the same check in TypeScript, Python and Rust.

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

Files

PathBytes
NOTICE345
README.md3,033
impl/python.py1,936
impl/rust.rs2,892
impl/typescript.ts2,005
vectors.json2,799