banking.bacs-processing-date
Bacs three-day cycle: input, processing and entry (arrival) dates for a submission, skipping weekends and bank holidays.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 18 tests, run in TypeScript, Python and Rust.
What it does
The three days of the Bacs cycle a payment file joins: the input day (day 1, the file is accepted), the processing day (day 2) and the entry day (day 3, when a Direct Credit lands in the recipient's account and a Direct Debit is taken). Payroll and supplier runs are planned backwards from day 3, and a bank holiday in the middle moves it.
## The rules
For example
bacs_processing_dates(2026-09-23, false)→ input day 2026-09-23, processing day 2026-09-24, entry day 2026-09-25 a Wednesday before the cutoffbacs_processing_dates(2026-09-23, true)→ input day 2026-09-24, processing day 2026-09-25, entry day 2026-09-28 a Wednesday after the cutoff joins Thursday's cyclebacs_processing_dates(2026-09-25, false)→ input day 2026-09-25, processing day 2026-09-28, entry day 2026-09-29 a Friday submission arrives on Tuesday
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 bacs_processing_dates(submission_iso: &str, after_cutoff: bool) -> BacsCycle
| submission_iso | date | the day the file is sent to Bacs |
| after_cutoff | bool | true when it is sent after the 22:30 input deadline |
| returns | BacsCycle | day 1, day 2 and day 3 of the cycle it joins |
The type it declares, generated into your project
/// The three days of one Bacs cycle.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BacsCycle {
/// day 1: the file is accepted
pub input_day: String,
/// day 2: Bacs processes it and sends it to the receiving banks
pub processing_day: String,
/// day 3: the money leaves one account and arrives in the other
pub entry_day: String,
}
Your code names it in one line, in the file that uses it
fune!(banking.bacs-processing-date@^1); // then call bacs_processing_dates(…)
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_business_days::add_business_days; ← from dates.add-business-days ^1.0.0 · built alongside by fune
use super::dates_bank_holidays::bank_holidays; ← from dates.bank-holidays ^1.0.0 · built alongside by fune
use super::dates_day_of_week::is_weekday; ← from dates.day-of-week ^1.0.0 · built alongside by fune
fn holidays_for(years: &[i64]) -> Vec<String> {
let mut out = Vec::new();
for year in years {
for holiday in bank_holidays("england-and-wales", *year) {
out.push(holiday.date);
}
}
out
}
fn cycle_with(submission_iso: &str, after_cutoff: bool, holidays: &[String]) -> BacsCycle {
let processing_day = is_weekday(submission_iso) && !holidays.iter().any(|h| h == submission_iso);
let input_day = if processing_day && !after_cutoff {
submission_iso.to_string()
} else {
add_business_days(submission_iso, 1, holidays)
};
BacsCycle {
processing_day: add_business_days(&input_day, 1, holidays),
entry_day: add_business_days(&input_day, 2, holidays),
input_day,
}
}
/// The Bacs cycle a file joins. Bacs works on processing days - Monday to
/// Friday except England and Wales bank holidays - and a file joins the cycle
/// whose input day is the day it arrives, if that is a processing day and it
/// arrived before the 22:30 deadline, or the next processing day otherwise.
/// Processing is the next processing day and entry the one after.
///
/// # Panics
/// Panics on a malformed date or a year outside the published holiday data.
pub fn bacs_processing_dates(submission_iso: &str, after_cutoff: bool) -> BacsCycle {
// is_weekday validates the date before its year is read.
is_weekday(submission_iso);
let year: i64 = submission_iso[0..4].parse().unwrap();
// Only reach for next year's calendar when the cycle really runs into it,
// so a December submission in the last published year still works as long
// as its cycle ends inside that year.
let mut cycle = cycle_with(submission_iso, after_cutoff, &holidays_for(&[year]));
if cycle.entry_day[0..4] != submission_iso[0..4] {
cycle = cycle_with(submission_iso, after_cutoff, &holidays_for(&[year, year + 1]));
}
cycle
}
pub fn bacs_cycle_to_value(cycle: &BacsCycle) -> Value {
Value::obj(vec![
("inputDay", Value::str(&cycle.input_day)),
("processingDay", Value::str(&cycle.processing_day)),
("entryDay", Value::str(&cycle.entry_day)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
bacs_cycle_to_value(&bacs_processing_dates(args[0].as_str(), args[1].as_bool()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 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 banking.bacs-processing-date
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./banking.bacs-processing-date-1.0.0-rust.fune, or fetch it from a terminal with fune pull banking.bacs-processing-date@1.0.0:rust.
The whole function, every language, is one file too: banking.bacs-processing-date-1.0.0.fune, 16,519 bytes, sha256 d6c142dfcdfc0352df592c4b9d80d19c87383647f6a4e1a3df99f140b852d8cf. 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 banking.bacs-processing-date
after — your function gets the result and the arguments, and returns the final result.
// fune: after banking.bacs-processing-date
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-business-days in banking.bacs-processing-date
// fune: replace dates.bank-holidays in banking.bacs-processing-date
// fune: replace dates.day-of-week in banking.bacs-processing-date
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 banking.bacs-processing-date --steps.
// fune: step banking.bacs-processing-date 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 | |
|---|---|---|---|
| a Wednesday before the cutoff | 2026-09-23, false | → | input day 2026-09-23, processing day 2026-09-24, entry day 2026-09-25 |
| a Wednesday after the cutoff joins Thursday's cycle | 2026-09-23, true | → | input day 2026-09-24, processing day 2026-09-25, entry day 2026-09-28 |
| a Friday submission arrives on Tuesday | 2026-09-25, false | → | input day 2026-09-25, processing day 2026-09-28, entry day 2026-09-29 |
| a Saturday submission waits for Monday | 2026-09-26, false | → | input day 2026-09-28, processing day 2026-09-29, entry day 2026-09-30 |
| the cutoff does not matter on a non-processing day | 2026-09-26, true | → | input day 2026-09-28, processing day 2026-09-29, entry day 2026-09-30 |
| Easter: Thursday before Good Friday processes on Tuesday | 2026-04-02, false | → | input day 2026-04-02, processing day 2026-04-07, entry day 2026-04-08 |
| a file sent on Good Friday starts on the Tuesday after Easter Monday | 2026-04-03, false | → | input day 2026-04-07, processing day 2026-04-08, entry day 2026-04-09 |
| Christmas 2026: the substitute Boxing Day is skipped too | 2026-12-23, false | → | input day 2026-12-23, processing day 2026-12-24, entry day 2026-12-29 |
| Christmas Eve after the cutoff | 2026-12-24, true | → | input day 2026-12-29, processing day 2026-12-30, entry day 2026-12-31 |
| New Year's Eve runs into next year's calendar | 2026-12-31, false | → | input day 2026-12-31, processing day 2027-01-04, entry day 2027-01-05 |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| the summer bank holiday Monday is skipped | 2026-08-28, false | → | input day 2026-08-28, processing day 2026-09-01, entry day 2026-09-02 |
| Christmas 2027 falls at a weekend, both substitute days skipped | 2027-12-24, false | → | input day 2027-12-24, processing day 2027-12-29, entry day 2027-12-30 |
| a December cycle inside the last published year needs no later data | 2028-12-22, false | → | input day 2028-12-22, processing day 2028-12-27, entry day 2028-12-28 |
| the first published year | 2019-01-02, false | → | input day 2019-01-02, processing day 2019-01-03, entry day 2019-01-04 |
| a cycle that runs past the published calendar is an error | 2028-12-29, false | → | error: no bank holiday data for england-and-wales in 2029 |
| a year before the published calendar is an error | 2018-06-01, false | → | error: no bank holiday data for england-and-wales in 2018 |
| a malformed date is an error | 2026-9-23, false | → | error: is not an ISO date |
| an impossible date is an error | 2026-02-30, false | → | error: not a real calendar date |
More from the author
Bacs describes the cycle for Direct Credit as: "Input day is the latest day a business user/bureau may submit a payment file to Bacs for a processing cycle. Payment files must be transmitted to Bacs between 07.00 and 22.30"; on day 2 files are delivered to the receiving banks; on day 3 "payments are simultaneously credited to the recipients' accounts and debited from your account". Source: Bacs, "Bacs Direct Credit", https://www.bacs.co.uk/bacs-schemes/getting-started/bacs-direct-credit/ (read 2026-09-23).
The days are Bacs processing days: Monday to Friday, excluding bank holidays in England and Wales, which Bacs uses as its non-processing days for the whole of the UK (Scotland's and Northern Ireland's extra holidays do not stop Bacs). The published Bacs processing calendar is the authority; the bank holidays here come from `dates.bank-holidays` (gov.uk data, region `england-and-wales`), and the working-day steps from `dates.add-business-days`.
So:
- input day = the submission date if it is a processing day and the file was sent by 22:30 (`afterCutoff` false); otherwise the next processing day. A file sent at a weekend or on a bank holiday joins the next processing day whatever the time; - processing day = the next processing day after the input day; - entry day = the processing day after that.
## Why `afterCutoff` is a flag
The time of day is the caller's to read (this capability never reads a clock), and comparing a timestamp with 22:30 is simpler at the call site than passing a time zone in. The 22:30 deadline is the one Bacs publishes (quoted above); a bureau or bank may set an earlier internal deadline, in which case pass `afterCutoff` against that.
## Limits
The bank holiday data covers the years gov.uk publishes (2019 to 2028 in `dates.bank-holidays@1.0.0`). A cycle that needs a year outside it fails with that capability's error rather than treating an unknown Christmas as a working day. Next year's calendar is only consulted when the cycle actually runs into next year, so a December cycle in the last published year still works.
This gives the cycle for a submission. It does not work backwards from a required arrival date; for that, step back two processing days from the entry day with `dates.add-business-days` and the same holiday list.
Files
| Path | Bytes |
|---|---|
| README.md | 2,687 |
| impl/python.py | 2,100 |
| impl/rust.rs | 2,490 |
| impl/typescript.ts | 1,977 |
| vectors.json | 4,159 |