Functional Weave
Code in TypeScript

legal.statutory-interest

Statutory interest on a judgment debt (8% simple, daily) or a late commercial debt, England and Wales.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

Not professional advice. This capability calculates legal figures from published rules. It is a software component for developers, not 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 solicitor review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Interest a creditor is entitled to by statute, in England and Wales, on one of three bases:

- `judgment-high-court`: a High Court judgment debt carries simple interest at the rate under section 17 of the Judgments Act 1838, 8% a year since 1 April 1993 (Judgment Debts (Rate of Interest) Order 1993, SI 1993/564), from the date of judgment until payment. - `judgment-county-court`: a County Court judgment carries interest at the same rate (County Courts (Interest on Judgment Debts) Order 1991, SI 1991/1184, art. 5) only if it is a "relevant judgment", one for **not less than GBP 5,000** (art. 1(2)), and from the date it was given (art. 2(1)). A smaller judgment returns zero interest and a rate of 0, which is the right answer, not a failure. The threshold is data. - `late-payment`: statutory interest and the fixed sum under the Late Payment of Commercial Debts (Interest) Act 1998. This is not re-implemented here: it is finance.late-payment-interest, whose interest, rate and fixed sum are passed straight through, with `fromDate` as the due date. Its rate is fixed when interest starts (8% over Bank Rate on the previous 30 June or 31 December), so `rateBasisPoints` is that rate.

For example

  • statutoryInterest(£10,000.00, 2025-01-01, 2025-12-31, judgment-high-court) → days 364, rate basis points 8%, daily interest £2.19, interest £797.81, compensation £0.00, total £797.81 GBP 10,000 High Court judgment, paid 364 days later: 797.81 at 2.19 a day
  • statutoryInterest(£10,000.00, 2025-01-01, 2026-01-01, judgment-high-court) → days 365, rate basis points 8%, daily interest £2.19, interest £800.00, compensation £0.00, total £800.00 a whole non-leap year is exactly 8%
  • statutoryInterest(£10,000.00, 2024-01-01, 2025-01-01, judgment-high-court) → days 366, rate basis points 8%, daily interest £2.19, interest £802.19, compensation £0.00, total £802.19 a year spanning 29 February is 366 days at 1/365: 802.19, not 800.00

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 statutoryInterest(amount: Money, fromDate: string, toDate: string, basis: InterestBasis): StatutoryInterest
amountMoneythe judgment debt, or for late-payment the qualifying debt, in GBP
fromDatedatethe date of judgment; for late-payment the due date (the relevant day)
toDatedatethe date of payment, or the date interest is claimed to
basisInterestBasisjudgment-high-court, judgment-county-court, or late-payment (finance.late-payment-interest)
returnsStatutoryInterest

The types it declares, generated into your project

export type InterestBasis = "judgment-high-court" | "judgment-county-court" | "late-payment";

/** Interest to a date, and the daily rate to quote after it. */
export interface StatutoryInterest {
  /** days interest runs for: toDate less fromDate */
  readonly days: number;
  /** the yearly rate in force on toDate; 0 when no interest runs */
  readonly rateBasisPoints: number;
  /** one day's interest at that rate, rounded half-up: the "daily rate" a claim form asks for */
  readonly dailyInterest: Money;
  /** simple interest for the whole period, 1/365 of a year's per day, rounded half-up once */
  readonly interest: Money;
  /** late-payment only: the fixed sum; zero for a judgment */
  readonly compensation: Money;
  /** interest plus compensation */
  readonly total: Money;
}

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

import { statutoryInterest } from "#fune/legal.statutory-interest@^1";
impl/typescript.ts · 111 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { addDays, daysBetween } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { latePaymentInterest } from "./finance_late_payment_interest.ts";  ← from finance.late-payment-interest ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { COUNTY_COURT_THRESHOLDS, COUNTY_COURT_THRESHOLDS_HISTORY, JUDGMENT_RATES, JUDGMENT_RATES_HISTORY } from "./legal_statutory_interest_data.ts";  ← this capability’s own data, compiled from data/judgment-rates.json into the same file by fune build
import { type InterestBasis, type StatutoryInterest } from "./legal_statutory_interest_types.ts";

const YEAR = 10000 * 365;
const MAX_EXACT = BigInt(Number.MAX_SAFE_INTEGER);

function horizon(history: string, froms: readonly string[], onDate: string, what: string): void {
  // A history=current build keeps only rows still in force; answering an older
  // date with today's rule is the failure dated data exists to prevent.
  if (history === "full" || froms.length === 0) return;
  const earliest = froms.reduce((a, b) => (b < a ? b : a));
  if (onDate < earliest) {
    throw new RangeError(
      `no ${what} on ${onDate}: this build was installed with history=${history}, so it only carries rules from ${earliest}. Reinstall with history=full for older judgments.`,
    );
  }
}

function inForce(onDate: string, validFrom: string, validTo: string | null): boolean {
  return onDate >= validFrom && (validTo === null || onDate <= validTo);
}

function rateOn(onDate: string): number {
  for (const row of JUDGMENT_RATES) {
    if (inForce(onDate, row.validFrom, row.validTo)) return row.basisPoints;
  }
  horizon(JUDGMENT_RATES_HISTORY, JUDGMENT_RATES.map((r) => r.validFrom), onDate, "judgment interest rate");
  throw new RangeError(`no judgment interest rate in force on ${onDate}`);
}

function checked(a: number, b: number, c: number): number {
  if (BigInt(a) * BigInt(b) * BigInt(c) > MAX_EXACT) {
    throw new RangeError("the interest calculation exceeds the exact integer range");
  }
  return a * b * c;
}

/** Statutory interest on a judgment debt or a late commercial debt. */
export function statutoryInterest(amount: Money, fromDate: string, toDate: string, basis: InterestBasis): StatutoryInterest {
  if (amount.currency !== "GBP") {
    throw new RangeError(`statutory interest is defined for GBP debts, received ${amount.currency}`);
  }
  if (amount.minor <= 0) {
    throw new RangeError("amount must be greater than zero");
  }
  const zero = money(0, "GBP");
  if (basis === "late-payment") {
    const claim = latePaymentInterest(amount, fromDate, toDate);
    const rate = claim.interestRateBasisPoints ?? 0;
    return {
      days: claim.daysLate,
      rateBasisPoints: rate,
      dailyInterest: money(roundDiv(amount.minor * rate, YEAR, "half-up"), "GBP"),
      interest: claim.interest,
      compensation: claim.compensation,
      total: claim.total,
    };
  }
  if (basis !== "judgment-high-court" && basis !== "judgment-county-court") {
    throw new RangeError(`unknown interest basis "${basis}": expected judgment-high-court, judgment-county-court or late-payment`);
  }
  const days = daysBetween(fromDate, toDate);
  if (days < 0) {
    throw new RangeError(`toDate ${toDate} is before fromDate ${fromDate}`);
  }
  if (basis === "judgment-county-court") {
    const threshold = COUNTY_COURT_THRESHOLDS.find((r) => inForce(fromDate, r.validFrom, r.validTo));
    if (threshold === undefined) {
      horizon(COUNTY_COURT_THRESHOLDS_HISTORY, COUNTY_COURT_THRESHOLDS.map((r) => r.validFrom), fromDate, "County Court judgment interest rule");
      throw new RangeError(`no County Court judgment interest rule in force on ${fromDate}`);
    }
    if (amount.minor < threshold.minJudgmentMinor) {
      // Not a "relevant judgment": no statutory interest at all.
      return { days, rateBasisPoints: 0, dailyInterest: zero, interest: zero, compensation: zero, total: zero };
    }
  }
  // Each day from the day after judgment to the payment date at the rate in force on it.
  let numerator = 0;
  let covered = 0;
  const first = addDays(fromDate, 1);
  for (const row of JUDGMENT_RATES) {
    if (days === 0) break;
    const start = first > row.validFrom ? first : row.validFrom;
    const end = row.validTo === null || toDate < row.validTo ? toDate : row.validTo;
    if (start > end) continue;
    const segment = daysBetween(start, end) + 1;
    covered += segment;
    numerator += checked(amount.minor, row.basisPoints, segment);
  }
  if (covered !== days) {
    rateOn(first);
    throw new RangeError(`no judgment interest rate for every day from ${first} to ${toDate}`);
  }
  if (BigInt(numerator) > MAX_EXACT) {
    throw new RangeError("the interest calculation exceeds the exact integer range");
  }
  const rate = rateOn(toDate);
  const interest = money(roundDiv(numerator, YEAR, "half-up"), "GBP");
  return {
    days,
    rateBasisPoints: rate,
    dailyInterest: money(roundDiv(amount.minor * rate, YEAR, "half-up"), "GBP"),
    interest,
    compensation: zero,
    total: interest,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 4 dependencies, 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 legal.statutory-interest
Download for TypeScript legal.statutory-interest-1.0.0-typescript.fune · 22,727 bytes sha256 6f28240acbce82ae475d6d659e7eff54199871e01d170e48cb2e150efbb2c0a6

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./legal.statutory-interest-1.0.0-typescript.fune, or fetch it from a terminal with fune pull legal.statutory-interest@1.0.0:typescript.

The whole function, every language, is one file too: legal.statutory-interest-1.0.0.fune, 34,616 bytes, sha256 d0447f7a39d4fb9e41fefb96ebf58b4b42d35c3eba805bb1cf4fc90f7da7ba6c. 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 legal.statutory-interest

after — your function gets the result and the arguments, and returns the final result.

// fune: after legal.statutory-interest

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 dates.add-days in legal.statutory-interest
// fune: replace finance.late-payment-interest in legal.statutory-interest
// fune: replace math.round-div in legal.statutory-interest
// fune: replace money.amount in legal.statutory-interest

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 legal.statutory-interest --steps.

// fune: step legal.statutory-interest 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
GBP 10,000 High Court judgment, paid 364 days later: 797.81 at 2.19 a day £10,000.00, 2025-01-01, 2025-12-31, judgment-high-court → days 364, rate basis points 8%, daily interest £2.19, interest £797.81, compensation £0.00, total £797.81
a whole non-leap year is exactly 8% £10,000.00, 2025-01-01, 2026-01-01, judgment-high-court → days 365, rate basis points 8%, daily interest £2.19, interest £800.00, compensation £0.00, total £800.00
a year spanning 29 February is 366 days at 1/365: 802.19, not 800.00 £10,000.00, 2024-01-01, 2025-01-01, judgment-high-court → days 366, rate basis points 8%, daily interest £2.19, interest £802.19, compensation £0.00, total £802.19
paid on the day of judgment: no interest, but the daily rate is still quoted £12,345.67, 2026-03-01, 2026-03-01, judgment-high-court → days 0, rate basis points 8%, daily interest £2.71, interest £0.00, compensation £0.00, total £0.00
GBP 1 for one day rounds to nothing £1.00, 2026-03-01, 2026-03-02, judgment-high-court → days 1, rate basis points 8%, daily interest £0.00, interest £0.00, compensation £0.00, total £0.00
30 days on GBP 25,000: 164.38 £25,000.00, 2026-03-01, 2026-03-31, judgment-high-court → days 30, rate basis points 8%, daily interest £5.48, interest £164.38, compensation £0.00, total £164.38
rounded once over the period, not per day: 100 days of 1.10 a day on GBP 5,000.00 is 109.59 £5,000.00, 2026-01-01, 2026-04-11, judgment-county-court → days 100, rate basis points 8%, daily interest £1.10, interest £109.59, compensation £0.00, total £109.59
a County Court judgment of GBP 4,999.99 carries no statutory interest £4,999.99, 2026-01-01, 2026-12-31, judgment-county-court → days 364, rate basis points 0%, daily interest £0.00, interest £0.00, compensation £0.00, total £0.00
a County Court judgment of exactly GBP 5,000 does £5,000.00, 2026-01-01, 2027-01-01, judgment-county-court → days 365, rate basis points 8%, daily interest £1.10, interest £400.00, compensation £0.00, total £400.00
the first day of the 8% rate £36,500.00, 1993-03-31, 1993-04-01, judgment-high-court → days 1, rate basis points 8%, daily interest £8.00, interest £8.00, compensation £0.00, total £8.00
Show the other 9 tests
CaseArgumentsExpected
late payment passes through finance.late-payment-interest: 11.75% and the GBP 70 fixed sum £1,000.00, 2026-01-31, 2026-03-22, late-payment → days 50, rate basis points 11.75%, daily interest £0.32, interest £16.10, compensation £70.00, total £86.10
late payment paid on time: nothing, rate 0 £1,000.00, 2026-01-31, 2026-01-31, late-payment → days 0, rate basis points 0%, daily interest £0.00, interest £0.00, compensation £0.00, total £0.00
a judgment before the 8% rate began (1 April 1993) is not carried £1,000.00, 1993-03-01, 1993-06-01, judgment-high-court → error: judgment interest rate
a County Court judgment before 1 July 1991 is not carried £10,000.00, 1991-06-30, 1993-06-01, judgment-county-court → error: County Court judgment interest rule
paying before judgment is an error £1,000.00, 2026-03-02, 2026-03-01, judgment-high-court → error: is before fromDate
a judgment in euros is an error €1,000.00, 2026-03-01, 2026-03-31, judgment-high-court → error: statutory interest is defined for GBP debts
a zero judgment is an error £0.00, 2026-03-01, 2026-03-31, judgment-high-court → error: amount must be greater than zero
an unknown basis is an error £1,000.00, 2026-03-01, 2026-03-31, contract → error: unknown interest basis
an impossible date is an error £1,000.00, 2026-02-30, 2026-03-31, judgment-high-court → error: 2026-02-30

More from the author

**Counting.** Interest runs for `toDate - fromDate` days: judgment on 1 March and paid on 31 March is 30 days. It is simple interest, one day's interest being 1/365 of a year's in leap years too, computed exactly over the whole period and rounded half-up to the penny once, so a year that contains 29 February is 366 days' interest, slightly more than 8%. If the judgment rate ever changes, each day uses the rate in force on it; the data has one row today.

`dailyInterest` is one day's interest at the rate in force on `toDate`, rounded half-up: the "daily rate of interest" a request for judgment or a claim form states for interest after the calculation date.

## Not handled (check before relying on it)

- County Court judgments under GBP 5,000 on a debt that carries interest under the Late Payment Act or by contract: whether and at what rate they carry interest after judgment turns on the Order as amended and on the contract, which this capability does not model; use the late-payment basis for the statutory rate before judgment. - Judgments on regulated Consumer Credit Act agreements (no interest under the 1991 Order, art. 2(3)), instalment orders, suspended possession orders, and interest ceasing while enforcement is stayed or an administration order is in force. - Discretionary pre-judgment interest (s.35A Senior Courts Act 1981, s.69 County Courts Act 1984): the court sets the rate; use lending.daily-interest or money.apply-rate with the rate claimed. - Judgments in foreign currency, and rates before 1 April 1993 (the 15% under the 1985 Order is not carried), which are errors.

## Sources

- Judgments Act 1838, s.17: https://www.legislation.gov.uk/ukpga/Will4and1Vict/1-2/110/section/17 - The Judgment Debts (Rate of Interest) Order 1993 (SI 1993/564), art. 2 and explanatory note, in force 1 April 1993: https://www.legislation.gov.uk/uksi/1993/564/made - The County Courts (Interest on Judgment Debts) Order 1991 (SI 1991/1184), arts. 1, 2 and 5, in force 1 July 1991: https://www.legislation.gov.uk/uksi/1991/1184/contents - Late Payment: see finance.late-payment-interest.

Read on 23 September 2026. Both tables declare effective dates, so a `history=current` build keeps only the rules in force and refuses an earlier date instead of answering it with today's rule.

Files

PathBytes
README.md3,571
data/county-court-thresholds.json268
data/judgment-rates.json232
impl/python.py5,068
impl/rust.rs6,391
impl/typescript.ts4,947
vectors.json7,917