Functional Weave
Code in Rust

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

  • statutory_interest(£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
  • statutory_interest(£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%
  • statutory_interest(£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.

pub fn statutory_interest(amount: &Money, from_date: &str, to_date: &str, basis: &str) -> StatutoryInterest
amountMoneythe judgment debt, or for late-payment the qualifying debt, in GBP
from_datedatethe date of judgment; for late-payment the due date (the relevant day)
to_datedatethe 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

// InterestBasis is a string in Rust, one of: "judgment-high-court", "judgment-county-court", "late-payment".
// Parameters take it as &str and results hold it as String.

/// Interest to a date, and the daily rate to quote after it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatutoryInterest {
    /// days interest runs for: toDate less fromDate
    pub days: i64,
    /// the yearly rate in force on toDate; 0 when no interest runs
    pub rate_basis_points: i64,
    /// one day's interest at that rate, rounded half-up: the "daily rate" a claim form asks for
    pub daily_interest: Money,
    /// simple interest for the whole period, 1/365 of a year's per day, rounded half-up once
    pub interest: Money,
    /// late-payment only: the fixed sum; zero for a judgment
    pub compensation: Money,
    /// interest plus compensation
    pub total: Money,
}

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

fune!(legal.statutory-interest@^1);  // then call statutory_interest(…)
impl/rust.rs · 164 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{add_days, days_between};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::finance_late_payment_interest::late_payment_interest;  ← from finance.late-payment-interest ^1.0.0 · built alongside by fune
use super::legal_statutory_interest_data::{
    COUNTY_COURT_THRESHOLDS, COUNTY_COURT_THRESHOLDS_HISTORY, JUDGMENT_RATES, JUDGMENT_RATES_HISTORY,
};
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

const YEAR: i64 = 10000 * 365;
const MAX_EXACT: i128 = 9_007_199_254_740_991;

fn horizon(history: &str, froms: &[&str], on_date: &str, what: &str) {
    // 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.is_empty() {
        return;
    }
    let earliest = froms.iter().min().unwrap();
    if on_date < *earliest {
        panic!(
            "no {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for older judgments.",
            what, on_date, history, earliest
        );
    }
}

fn in_force(on_date: &str, valid_from: &str, valid_to: Option<&str>) -> bool {
    on_date >= valid_from && valid_to.map_or(true, |to| on_date <= to)
}

fn rate_on(on_date: &str) -> i64 {
    for row in JUDGMENT_RATES {
        if in_force(on_date, row.valid_from, row.valid_to) {
            return row.basis_points;
        }
    }
    let froms: Vec<&str> = JUDGMENT_RATES.iter().map(|r| r.valid_from).collect();
    horizon(JUDGMENT_RATES_HISTORY, &froms, on_date, "judgment interest rate");
    panic!("no judgment interest rate in force on {}", on_date);
}

fn checked(a: i64, b: i64, c: i64) -> i64 {
    if (a as i128) * (b as i128) * (c as i128) > MAX_EXACT {
        panic!("the interest calculation exceeds the exact integer range");
    }
    a * b * c
}

/// Statutory interest on a judgment debt or a late commercial debt.
///
/// # Panics
/// Panics on a non-GBP or non-positive amount, an unknown basis, dates out of
/// order or outside the data, or a product beyond the exact integer range.
pub fn statutory_interest(amount: &Money, from_date: &str, to_date: &str, basis: &str) -> StatutoryInterest {
    if amount.currency != "GBP" {
        panic!("statutory interest is defined for GBP debts, received {}", amount.currency);
    }
    if amount.minor <= 0 {
        panic!("amount must be greater than zero");
    }
    let zero = money(0, "GBP");
    if basis == "late-payment" {
        let claim = late_payment_interest(amount, from_date, to_date);
        let rate = claim.interest_rate_basis_points.unwrap_or(0);
        return StatutoryInterest {
            days: claim.days_late,
            rate_basis_points: rate,
            daily_interest: money(round_div(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" {
        panic!(
            "unknown interest basis \"{}\": expected judgment-high-court, judgment-county-court or late-payment",
            basis
        );
    }
    let days = days_between(from_date, to_date);
    if days < 0 {
        panic!("toDate {} is before fromDate {}", to_date, from_date);
    }
    if basis == "judgment-county-court" {
        match COUNTY_COURT_THRESHOLDS.iter().find(|r| in_force(from_date, r.valid_from, r.valid_to)) {
            None => {
                let froms: Vec<&str> = COUNTY_COURT_THRESHOLDS.iter().map(|r| r.valid_from).collect();
                horizon(COUNTY_COURT_THRESHOLDS_HISTORY, &froms, from_date, "County Court judgment interest rule");
                panic!("no County Court judgment interest rule in force on {}", from_date);
            }
            Some(threshold) => {
                if amount.minor < threshold.min_judgment_minor {
                    // Not a "relevant judgment": no statutory interest at all.
                    return StatutoryInterest {
                        days,
                        rate_basis_points: 0,
                        daily_interest: zero.clone(),
                        interest: zero.clone(),
                        compensation: zero.clone(),
                        total: zero,
                    };
                }
            }
        }
    }
    // Each day from the day after judgment to the payment date at the rate in force on it.
    let mut numerator: i128 = 0;
    let mut covered = 0;
    let first = add_days(from_date, 1);
    for row in JUDGMENT_RATES {
        if days == 0 {
            break;
        }
        let start: &str = if first.as_str() > row.valid_from { &first } else { row.valid_from };
        let end: &str = match row.valid_to {
            Some(to) if to < to_date => to,
            _ => to_date,
        };
        if start > end {
            continue;
        }
        let segment = days_between(start, end) + 1;
        covered += segment;
        numerator += checked(amount.minor, row.basis_points, segment) as i128;
    }
    if covered != days {
        rate_on(&first);
        panic!("no judgment interest rate for every day from {} to {}", first, to_date);
    }
    if numerator > MAX_EXACT {
        panic!("the interest calculation exceeds the exact integer range");
    }
    let rate = rate_on(to_date);
    let interest = money(round_div(numerator as i64, YEAR, "half-up"), "GBP");
    StatutoryInterest {
        days,
        rate_basis_points: rate,
        daily_interest: money(round_div(amount.minor * rate, YEAR, "half-up"), "GBP"),
        interest: interest.clone(),
        compensation: zero,
        total: interest,
    }
}

pub fn statutory_interest_to_value(s: &StatutoryInterest) -> Value {
    Value::obj(vec![
        ("days", Value::Int(s.days)),
        ("rateBasisPoints", Value::Int(s.rate_basis_points)),
        ("dailyInterest", money_to_value(&s.daily_interest)),
        ("interest", money_to_value(&s.interest)),
        ("compensation", money_to_value(&s.compensation)),
        ("total", money_to_value(&s.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    statutory_interest_to_value(&statutory_interest(
        &money_from_value(&args[0]),
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_str(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add legal.statutory-interest
Download for Rust legal.statutory-interest-1.0.0-rust.fune · 24,228 bytes sha256 6b47abf1c9638da46884813a450f98be1f63a3fb4f2912b26b6bad96a141a8f4

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

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