dates.bank-holidays
UK bank and public holidays for a region and year, as published by gov.uk, including one-off holidays.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 15 tests, run in TypeScript, Python and Rust.
What it does
The bank and public holidays for one UK region and one year, in date order, as the UK government publishes them. The three regions are the ones gov.uk uses: england-and-wales, scotland and northern-ireland. They differ: Scotland has 2 January and St Andrew's Day but no Easter Monday, and a different summer holiday; Northern Ireland adds St Patrick's Day and the Battle of the Boyne.
Source: "UK bank holidays", GOV.UK, https://www.gov.uk/bank-holidays, taken from its machine-readable form https://www.gov.uk/bank-holidays.json on 2026-09-22. That feed covers 2019 to 2028 for every region, and so does this version. Every row is copied from it: dates, titles (gov.uk's wording, curly apostrophes included) and the "Substitute day" note, which becomes `substitute: true`.
For example
bankHolidays(england-and-wales, 2,022)→ ×10 England and Wales 2022: the Platinum Jubilee moved the spring holiday and added one, and the state funeral added anotherbankHolidays(england-and-wales, 2,023)→ ×9 England and Wales 2023, with the coronation bank holidaybankHolidays(england-and-wales, 2,026)→ ×8 England and Wales 2026: Boxing Day falls on a Saturday so the substitute is Monday 28 December
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 bankHolidays(region: UkRegion, year: number): readonly BankHoliday[]
| region | UkRegion | england-and-wales, scotland or northern-ireland, as gov.uk divides them |
| year | int | calendar year; must be one gov.uk publishes |
| returns | BankHoliday[] | the holidays in date order; substitute days are the day actually taken off |
The types it declares, generated into your project
export type UkRegion = "england-and-wales" | "scotland" | "northern-ireland";
/** One day off, as gov.uk lists it. */
export interface BankHoliday {
readonly date: string;
/** gov.uk's own wording, e.g. "Boxing Day" */
readonly title: string;
/** true when this is a substitute weekday for a holiday that fell at a weekend */
readonly substitute: boolean;
}
Your code names it in one line, in the file that uses it
import { bankHolidays } from "#fune/dates.bank-holidays@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { BANK_HOLIDAYS } from "./dates_bank_holidays_data.ts"; ← this capability’s own data, compiled from data/bank-holidays.json into the same file by fune build
import { type BankHoliday, type UkRegion } from "./dates_bank_holidays_types.ts";
/**
* The bank holidays for a UK region and year, in date order, as gov.uk
* publishes them. A year the data does not cover is an error rather than an
* empty list, because an empty list reads as "no holidays" and a deadline
* calculation would then count Christmas Day as a working day.
*/
export function bankHolidays(region: UkRegion, year: number): readonly BankHoliday[] {
if (!Number.isInteger(year)) {
throw new TypeError(`year must be an integer, received ${year}`);
}
let first: number | null = null;
let last: number | null = null;
const found: BankHoliday[] = [];
const prefix = `${String(year).padStart(4, "0")}-`;
for (const row of BANK_HOLIDAYS) {
if (row.region !== region) continue;
const rowYear = Number(row.date.slice(0, 4));
if (first === null || rowYear < first) first = rowYear;
if (last === null || rowYear > last) last = rowYear;
if (row.date.startsWith(prefix)) found.push({ date: row.date, title: row.title, substitute: row.substitute });
}
if (first === null || last === null) {
throw new RangeError(`unknown region "${region}": expected england-and-wales, scotland or northern-ireland`);
}
if (year < first || year > last) {
throw new RangeError(`no bank holiday data for ${region} in ${year}: the published calendar covers ${first} to ${last}`);
}
// ISO dates sort correctly as strings.
return found.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, 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 dates.bank-holidays
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./dates.bank-holidays-1.0.0-typescript.fune, or fetch it from a terminal with fune pull dates.bank-holidays@1.0.0:typescript.
The whole function, every language, is one file too: dates.bank-holidays-1.0.0.fune, 59,370 bytes, sha256 916e69dedcd37c4d3383c26ae79478060310892b261b8a7e8bb073817c9f6298. 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 dates.bank-holidays
after — your function gets the result and the arguments, and returns the final result.
// fune: after dates.bank-holidays
replace — it requires no other capability, so there is no dependency to replace.
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 dates.bank-holidays --steps.
// fune: step dates.bank-holidays 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 | |
|---|---|---|---|
| England and Wales 2022: the Platinum Jubilee moved the spring holiday and added one, and the state funeral added another | england-and-wales, 2,022 | → | ×10 |
| England and Wales 2023, with the coronation bank holiday | england-and-wales, 2,023 | → | ×9 |
| England and Wales 2026: Boxing Day falls on a Saturday so the substitute is Monday 28 December | england-and-wales, 2,026 | → | ×8 |
| England and Wales 2019, the first year gov.uk publishes | england-and-wales, 2,019 | → | ×8 |
| England and Wales 2028, the last year gov.uk publishes | england-and-wales, 2,028 | → | ×8 |
| Scotland 2026: 2 January and St Andrew’s Day, no Easter Monday, and the one-off World Cup bank holiday | scotland, 2,026 | → | ×10 |
| Scotland 2020: the early May holiday moved to Friday 8 May for VE day | scotland, 2,020 | → | ×9 |
| Scotland 2022: New Year and 2 January both substituted | scotland, 2,022 | → | ×11 |
| Northern Ireland 2025: the Battle of the Boyne substituted to Monday 14 July | northern-ireland, 2,025 | → | ×10 |
| Northern Ireland 2024: St Patrick’s Day substituted to Monday 18 March | northern-ireland, 2,024 | → | ×10 |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a year before the published calendar is an error, not an empty list | england-and-wales, 2,018 | → | error: no bank holiday data for england-and-wales in 2018 |
| a year after the published calendar is an error, not an empty list | scotland, 2,029 | → | error: no bank holiday data for scotland in 2029 |
| Wales is not its own region in the gov.uk calendar | wales, 2,026 | → | error: unknown region "wales" |
| a two-letter code is not a region | GB, 2,026 | → | error: unknown region "GB" |
| a fractional year is an error | scotland, 2,026.5 | → | error: year must be an integer |
More from the author
The holidays are data, not rules in code. Easter moves, holidays falling on a weekend are substituted by proclamation, and one-off holidays are added at a few months' notice: the Platinum Jubilee (3 June 2022, which also moved the spring holiday to 2 June), the State Funeral of Queen Elizabeth II (19 September 2022), the coronation of King Charles III (8 May 2023), the VE day move of the early May holiday (8 May 2020) and Scotland's World Cup bank holiday (15 June 2026). No formula produces those, which is why this is a table. When gov.uk publishes another year or another one-off holiday, that is a new version of this capability, not a code change.
A year outside the published range is an error, not an empty list. An empty list would read as "no holidays that year", and a deadline calculation would silently count Christmas Day as a working day.
The data deliberately has no `effective` columns. A bank holiday is an event on a date, not a rule that stays in force until superseded, so there is nothing for `history=current` to keep; declaring the columns would let a project-wide history setting prune the whole calendar away.
This lists days, it does not count them. To skip holidays in working-day arithmetic, pass the dates to dates.add-business-days or dates.business-days-between, which take an explicit holiday list so they work for any country and any employer's closure days.
Files
| Path | Bytes |
|---|---|
| README.md | 2,198 |
| data/bank-holidays.json | 29,358 |
| impl/python.py | 1,665 |
| impl/rust.rs | 2,595 |
| impl/typescript.ts | 1,609 |
| vectors.json | 13,225 |