Functional Weave
Code in Rust

validation.uk-utr@2.0.1

impl/typescript.ts

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.
 */
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 };
}