payroll.tax-code-parse Unreviewed
Parse a UK PAYE tax code (1257L, K475, BR, D0, NT, 0T, S and C prefixes, W1/M1/X) into its allowance and flags.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 32 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Takes a UK PAYE tax code apart. It does not calculate tax; `payroll.income-tax` does that, using this.
What a code is made of:
For example
parseTaxCode(1257L)→ code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true the standard personal allowance codeparseTaxCode(S1257L)→ code S1257L, region scotland, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true a Scottish taxpayerparseTaxCode(C1257L)→ code C1257L, region wales, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true a Welsh taxpayer
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 parseTaxCode(code: string): TaxCode
| code | string | as HMRC issued it; case and spaces before W1, M1 or X do not matter |
| returns | TaxCode |
The types it declares, generated into your project
export type TaxRegion = "rest-of-uk" | "scotland" | "wales";
export type TaxCodeKind = "allowance" | "negative-allowance" | "basic-rate" | "d-rate" | "no-tax";
/** A tax code taken apart. */
export interface TaxCode {
/** the code without its basis marker, uppercase: S1257L, K475, BR */
readonly code: string;
/** S prefix is Scotland, C prefix is Wales, none is the rest of the UK */
readonly region: TaxRegion;
/** allowance: nnnnL/M/N/T; negative-allowance: K codes; basic-rate: BR; d-rate: D0, D1...; no-tax: NT */
readonly kind: TaxCodeKind;
/** the numeric part: 1257 for 1257L, 475 for K475, the digit of a D code, 0 for BR and NT */
readonly number: number;
/** L, M, N or T for allowance codes; null otherwise */
readonly suffix: string | null;
/** tax-free pay a year in pence: (number x 10 + 9) pounds, negative for K codes, 0 for 0T, BR, D and NT */
readonly annualAllowance: Money;
/** false when the code carries W1, M1 or X (week 1 / month 1 basis) */
readonly cumulative: boolean;
}
Your code names it in one line, in the file that uses it
import { parseTaxCode } from "#fune/payroll.tax-code-parse@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type TaxCode, type TaxCodeKind, type TaxRegion } from "./payroll_tax_code_parse_types.ts";
// The whitespace every language trims alike: trim() and \s also take \ufeff,
// which Python's strip() and Rust's trim() keep, and those take \x85, which
// trim() keeps. So each implementation names the same set instead.
const EDGE_SPACE = /^[ \t\n\r\v\f\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+|[ \t\n\r\v\f\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+$/g;
// The basis marker may be written with or without a space: "1257L W1", "1257LM1", "1257L X".
const BASIS = /^(.*?)[ \t\n\r\v\f\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*(W1|M1|X)$/;
const ALLOWANCE = /^(0|[1-9][0-9]{0,5})([LMNT])$/;
const K_CODE = /^K([1-9][0-9]{0,5})$/;
const D_CODE = /^D([0-9])$/;
/**
* Take a UK PAYE tax code apart into its region, kind, allowance and basis.
*
* The annual allowance is number x £10 + £9, not number x £10: a code covers a
* £10 band of allowances and HMRC's routines give the employee the top of it,
* so 1257L is worth £12,579 a year to the payroll.
*/
export function parseTaxCode(code: string): TaxCode {
const original = code;
let text = code.replace(EDGE_SPACE, "").toUpperCase();
if (text === "") throw new RangeError(`unrecognised tax code "${original}"`);
let cumulative = true;
const basis = BASIS.exec(text);
if (basis !== null && basis[1] !== "") {
text = basis[1];
cumulative = false;
}
let region: TaxRegion = "rest-of-uk";
let body = text;
if (text.startsWith("S")) {
region = "scotland";
body = text.slice(1);
} else if (text.startsWith("C")) {
region = "wales";
body = text.slice(1);
}
let kind: TaxCodeKind;
let number = 0;
let suffix: string | null = null;
let allowancePounds = 0;
const allowance = ALLOWANCE.exec(body);
const k = K_CODE.exec(body);
const d = D_CODE.exec(body);
if (body === "NT") {
if (region !== "rest-of-uk") {
throw new RangeError(`there is no Scottish or Welsh NT code: "${original}"`);
}
kind = "no-tax";
} else if (body === "BR") {
kind = "basic-rate";
} else if (d !== null) {
kind = "d-rate";
number = Number(d[1]);
} else if (k !== null) {
kind = "negative-allowance";
number = Number(k[1]);
allowancePounds = -(number * 10 + 9);
} else if (allowance !== null) {
number = Number(allowance[1]);
suffix = allowance[2];
if (number === 0 && suffix !== "T") {
throw new RangeError(`unrecognised tax code "${original}": a zero allowance is only issued as 0T`);
}
kind = "allowance";
allowancePounds = number === 0 ? 0 : number * 10 + 9;
} else {
throw new RangeError(`unrecognised tax code "${original}"`);
}
const prefix = region === "scotland" ? "S" : region === "wales" ? "C" : "";
return {
code: prefix + body,
region,
kind,
number,
suffix,
annualAllowance: money(allowancePounds * 100, "GBP"),
cumulative,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, 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 payroll.tax-code-parse
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./payroll.tax-code-parse-1.0.2-typescript.fune, or fetch it from a terminal with fune pull payroll.tax-code-parse@1.0.2:typescript.
The whole function, every language, is one file too: payroll.tax-code-parse-1.0.2.fune, 27,637 bytes, sha256 a2d2166d9aec28dc6e7a42f0658b6dcc1f5ffc41287b0e4880b0b47a9860d3db. 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 payroll.tax-code-parse
after — your function gets the result and the arguments, and returns the final result.
// fune: after payroll.tax-code-parse
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace money.amount in payroll.tax-code-parse
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 payroll.tax-code-parse --steps.
// fune: step payroll.tax-code-parse 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 | |
|---|---|---|---|
| the standard personal allowance code | 1257L | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true |
| a Scottish taxpayer | S1257L | → | code S1257L, region scotland, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true |
| a Welsh taxpayer | C1257L | → | code C1257L, region wales, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true |
| lower case and stray spaces are tidied | s1257l | → | code S1257L, region scotland, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true |
| emergency code on a week 1 basis, with a space | 1257L W1 | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative false |
| month 1 basis written without a space | 1257LM1 | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative false |
| the X marker is also non-cumulative | 1257L X | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative false |
| marriage allowance recipient suffix M | 1383M | → | code 1383M, region rest-of-uk, kind allowance, number 1,383, suffix M, annual allowance £13,839.00, cumulative true |
| 0T is no allowance at all, not 9 pounds | 0T | → | code 0T, region rest-of-uk, kind allowance, number 0, suffix T, annual allowance £0.00, cumulative true |
| code 1 is worth 19 pounds | 1L | → | code 1L, region rest-of-uk, kind allowance, number 1, suffix L, annual allowance £19.00, cumulative true |
Show the other 22 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a K code is a negative allowance | K475 | → | code K475, region rest-of-uk, kind negative-allowance, number 475, suffix —, annual allowance -£4,759.00, cumulative true |
| a Scottish K code on a month 1 basis | SK475 M1 | → | code SK475, region scotland, kind negative-allowance, number 475, suffix —, annual allowance -£4,759.00, cumulative false |
| BR taxes everything at the basic rate | BR | → | code BR, region rest-of-uk, kind basic-rate, number 0, suffix —, annual allowance £0.00, cumulative true |
| Welsh BR on a week 1 basis | CBR W1 | → | code CBR, region wales, kind basic-rate, number 0, suffix —, annual allowance £0.00, cumulative false |
| D0 is the higher rate on everything | D0 | → | code D0, region rest-of-uk, kind d-rate, number 0, suffix —, annual allowance £0.00, cumulative true |
| SD2 is Scotland's advanced rate on everything | SD2 | → | code SD2, region scotland, kind d-rate, number 2, suffix —, annual allowance £0.00, cumulative true |
| NT means no tax | NT | → | code NT, region rest-of-uk, kind no-tax, number 0, suffix —, annual allowance £0.00, cumulative true |
| a six-digit code | 999999T | → | code 999999T, region rest-of-uk, kind allowance, number 999,999, suffix T, annual allowance £9,999,999.00, cumulative true |
| there is no Scottish NT | SNT | → | error: there is no Scottish or Welsh NT code |
| there is no K0 | K0 | → | error: unrecognised tax code |
| a withdrawn suffix is refused | 1257P | → | error: unrecognised tax code |
| a number without a suffix is refused | 1257 | → | error: unrecognised tax code |
| 0L is not issued | 0L | → | error: only issued as 0T |
| leading zeros are refused | 01257L | → | error: unrecognised tax code |
| an empty code is refused | → | error: unrecognised tax code | |
| a basis marker on its own is not a code | W1 | → | error: unrecognised tax code |
| a trailing newline is trimmed like any other space | 1257L | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true |
| a newline before the basis marker is a space | 1257L W1 | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative false |
| a no-break space is trimmed in every language | 1257L | → | code 1257L, region rest-of-uk, kind allowance, number 1,257, suffix L, annual allowance £12,579.00, cumulative true |
| a next-line control (U+0085) is not trimmed, so the code is refused | 1257L | → | error: unrecognised tax code |
| a unit separator (U+001F) is not trimmed, so the code is refused | 1257L | → | error: unrecognised tax code |
| a byte order mark (U+FEFF) is not trimmed, so the code is refused | 1257L | → | error: unrecognised tax code |
More from the author
- An optional country prefix: `S` for a Scottish taxpayer, `C` for a Welsh one, nothing for England and Northern Ireland (`rest-of-uk`). - The code proper, one of: - a number and a suffix letter, `1257L`, `1100M`, `1385N`, `0T`: a tax-free allowance. The number is the allowance divided by ten, so the annual allowance HMRC's routines use is **number x £10 + £9**: 1257L is £12,579, not £12,570, because the code covers a £10 range and the routines give the employee the top of it. `0T` means no allowance at all. - `K` and a number, `K475`: a negative allowance, pay to be *added* to taxable pay (£4,759 a year for K475). There is no K0. - `BR`: all pay at the basic rate. `D0`, `D1` (and `SD0` to `SD3` in Scotland): all pay at the rate one, two, three or four bands above basic. - `NT`: no tax. There is no Scottish or Welsh NT, so `SNT` is refused. - An optional basis marker, `W1`, `M1` or `X`, meaning the code is operated on a week 1 / month 1 (non-cumulative) basis. It may follow the code with or without a space: `1257L W1`, `1257LM1`, `1257L X`.
Suffix letters L, M, N and T do not change the calculation (M and N are the marriage allowance, T means HMRC wants to review the code); they are returned so a payroll can apply HMRC's bulk code uplifts, which are made by suffix. Withdrawn suffixes (P, V, Y, retired in 2016) and anything else unrecognised are errors rather than guesses. Leading zeros (`01257L`) are refused, since HMRC never issues them and they usually mean a keying error.
Sources: HMRC, "Tax codes" (https://www.gov.uk/tax-codes, and its "What your tax code means" page, https://www.gov.uk/tax-codes/what-your-tax-code-means); HMRC, "Specification for PAYE tax table routines" v24.0 (February 2026), paragraphs 4.3.1 (the £10 x number + £9 annual value, K codes, 0T), 7 and 11 (NT has no S or C version), 8 (week 1 / month 1 basis), https://www.gov.uk/government/publications/payroll-technical-specifications-income-tax.
1.0.1 fixes Python accepting a trailing newline in code (its patterns now use fullmatch), and trims the same whitespace in every language: spaces, tabs, line breaks and Unicode space separators. Before, Python also trimmed U+001C to U+001F and U+0085, Rust U+0085 and TypeScript U+FEFF; each is now refused as part of the code. Adds tests.
## Before you rely on this
**Not professional advice.** This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a payroll specialist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
## Notices
Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
1.0.2 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 185 |
| README.md | 3,804 |
| impl/python.py | 3,063 |
| impl/rust.rs | 4,541 |
| impl/typescript.ts | 3,047 |
| vectors.json | 7,757 |