Functional Weave
Code in Rust

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 Wednesday
  • add_business_days(2026-09-25, 1, ) → 2026-09-28 Friday plus one working day is Monday
  • add_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
isodatethe starting date, ISO; it may itself be a weekend or holiday
daysintworking days to move; negative moves backwards, 0 returns iso unchanged
holidaysdate[]non-working dates, e.g. from dates.bank-holidays; order and duplicates do not matter
returnsdatethe 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(…)
impl/rust.rs · 44 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 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
Download for Rust dates.add-business-days-1.0.0-rust.fune · 8,248 bytes sha256 846d18703ca8ca8c499f8d728e9cadc84bfd0111d960669d713c50fb928971e0

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,689
impl/python.py1,196
impl/rust.rs1,836
impl/typescript.ts1,196
vectors.json2,634