2,005 bytes · the TypeScript implementation · view 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. */exportfunction 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);
elseif (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 };
}