dates.add-business-days
Move a date forward or back by working days, skipping weekends and a caller-supplied holiday list.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 19 tests, run in TypeScript, Python and Rust.
What it does
The n-th working day after a date: Friday plus one working day is Monday, and so is Saturday plus one. Working days are Monday to Friday minus the holidays you pass. This is spreadsheet WORKDAY: the start date itself is never counted, whether or not it is a working day, and a negative count walks backwards the same way (Monday minus one is the previous Friday).
Zero returns the date unchanged, even on a Saturday. "Roll to the next working day" is a different operation with its own conventions (following, modified following, preceding); zero here means no move.
For example
add_business_days(2026-09-22, 1, )→ 2026-09-23 Tuesday plus one working day is Wednesdayadd_business_days(2026-09-25, 1, )→ 2026-09-28 Friday plus one working day is Mondayadd_business_days(2026-09-26, 1, )→ 2026-09-28 Saturday plus one is also Monday: the start is never counted
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 add_business_days(iso: &str, days: i64, holidays: &[String]) -> String
| iso | date | the starting date, ISO; it may itself be a weekend or holiday |
| days | int | working days to move; negative moves backwards, 0 returns iso unchanged |
| holidays | date[] | non-working dates, e.g. from dates.bank-holidays; order and duplicates do not matter |
| returns | date | the days-th working day after iso (before, when negative) |
Your code names it in one line, in the file that uses it
fune!(dates.add-business-days@^1); // then call add_business_days(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use std::collections::HashSet;
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::{epoch_day_from_iso, iso_from_epoch_day}; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week; ← from dates.day-of-week ^1.0.0 · built alongside by fune
/// The `days`-th working day after `iso` (before it when negative), skipping
/// Saturdays, Sundays and the listed holidays. The start date is never counted,
/// so Friday + 1 and Saturday + 1 are both Monday, as in spreadsheet WORKDAY.
/// Zero returns `iso` unchanged.
///
/// # Panics
/// Panics if the date or any holiday is malformed or impossible, or the result
/// leaves 0001-9999.
pub fn add_business_days(iso: &str, days: i64, holidays: &[String]) -> String {
let mut day = epoch_day_from_iso(iso);
// Every holiday is validated even when the move never reaches it, so a typo
// in a calendar fails on the next run rather than in the year it matters.
let excluded: HashSet<i64> = holidays.iter().map(|h| epoch_day_from_iso(h)).collect();
let step: i64 = if days < 0 { -1 } else { 1 };
let mut weekday = day_of_week(iso);
let mut remaining = days.abs();
while remaining > 0 {
day += step;
weekday = (weekday - 1 + step).rem_euclid(7) + 1;
if weekday <= 5 && !excluded.contains(&day) {
remaining -= 1;
}
}
iso_from_epoch_day(day)
}
pub fn fune_vector(args: &[Value]) -> Value {
// Refuse what the typed signature cannot hold, with the wording TypeScript
// and Python use, rather than let the conversion below quietly change it.
if let Value::Float(f) = args[1] {
if f.fract() != 0.0 {
panic!("days must be an integer, received {}", f);
}
}
let holidays: Vec<String> = args[2].as_arr().iter().map(|v| v.as_str().to_string()).collect();
Value::str(&add_business_days(args[0].as_str(), args[1].as_i64(), &holidays))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 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 dates.add-business-days
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./dates.add-business-days-1.0.0-rust.fune, or fetch it from a terminal with fune pull dates.add-business-days@1.0.0:rust.
The whole function, every language, is one file too: dates.add-business-days-1.0.0.fune, 10,744 bytes, sha256 ca468a6c835a3956b1d871f90139f2c4867e6021e46ae57156363167f24361b1. 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.add-business-days
after — your function gets the result and the arguments, and returns the final result.
// fune: after dates.add-business-days
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 dates.add-business-days
// fune: replace dates.day-of-week in dates.add-business-days
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.add-business-days --steps.
// fune: step dates.add-business-days 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 | |
|---|---|---|---|
| Tuesday plus one working day is Wednesday | 2026-09-22, 1, | → | 2026-09-23 |
| Friday plus one working day is Monday | 2026-09-25, 1, | → | 2026-09-28 |
| Saturday plus one is also Monday: the start is never counted | 2026-09-26, 1, | → | 2026-09-28 |
| Tuesday plus five working days is the next Tuesday | 2026-09-22, 5, | → | 2026-09-29 |
| Monday minus one working day is the previous Friday | 2026-09-28, -1, | → | 2026-09-25 |
| Sunday minus one is Friday | 2026-09-27, -1, | → | 2026-09-25 |
| zero leaves a Saturday where it is rather than rolling it | 2026-09-26, 0, | → | 2026-09-26 |
| Christmas Eve 2026 plus one skips Christmas Day and the Boxing Day substitute | 2026-12-24, 1, 2026-12-25, 2026-12-28 | → | 2026-12-29 |
| Easter 2026 in England: Maundy Thursday plus one is the Tuesday | 2026-04-02, 1, 2026-04-03, 2026-04-06 | → | 2026-04-07 |
| and back again: Easter Tuesday minus one is Maundy Thursday | 2026-04-07, -1, 2026-04-03, 2026-04-06 | → | 2026-04-02 |
Show the other 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| ten working days over Christmas and New Year needs the next year's holidays | 2026-12-21, 10, 2026-12-25, 2026-12-28, 2027-01-01 | → | 2027-01-07 |
| a holiday on a Saturday is not skipped twice | 2026-09-25, 1, 2026-09-26 | → | 2026-09-28 |
| duplicates and order in the holiday list do not matter | 2026-12-24, 1, 2026-12-28, 2026-12-25, 2026-12-25 | → | 2026-12-29 |
| a holiday on the start date does not change a forward move | 2026-12-25, 1, 2026-12-25, 2026-12-28 | → | 2026-12-29 |
| a leap day is an ordinary working day | 2024-02-28, 1, | → | 2024-02-29 |
| twenty working days is four calendar weeks with no holidays | 2026-09-22, 20, | → | 2026-10-20 |
| a malformed holiday fails even when the move never reaches it | 2026-09-22, 1, 2030-02-30 | → | error: is not a real calendar date |
| a fractional day count is an error | 2026-09-22, 1.5, | → | error: days must be an integer |
| a UK-style start date is an error | 22/09/2026, 1, | → | error: is not an ISO date |
More from the author
It agrees with dates.business-days-between: from a working day `d`, businessDaysBetween(d, addBusinessDays(d, n, h), h) is n.
**Holidays are an argument, not a region.** Taking an explicit list keeps this capability universal: it works for any country, for an employer's own closure days, and for a court or exchange calendar, none of which a region code could express. For UK bank holidays, fetch them with dates.bank-holidays for each year the move can span and pass the dates in. That composition is a caller's choice rather than a dependency, so the capability never ships holiday data a caller did not ask for, and a year missing from that data fails in dates.bank-holidays, loudly, instead of here, quietly.
Holidays that fall at a weekend are not skipped twice, duplicates are harmless, and every holiday is validated even if the move never reaches it, so a typo in a calendar fails on the next run.
Pass enough holiday years. Moving 10 working days from 21 December needs the next January's holidays too; a list that stops at 31 December will count New Year's Day as a working day.
Files
| Path | Bytes |
|---|---|
| README.md | 1,689 |
| impl/python.py | 1,196 |
| impl/rust.rs | 1,836 |
| impl/typescript.ts | 1,196 |
| vectors.json | 2,634 |