Functional Weave
Code in Python

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.

def add_business_days(iso: str, days: int, holidays: Sequence[str]) -> str
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

from fune.dates.add_business_days import add_business_days  # dates.add-business-days@^1
impl/python.py · 29 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.

from typing import Sequence

from .dates_add_days import epoch_day_from_iso, iso_from_epoch_day  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_of_week  ← from dates.day-of-week ^1.0.0 · built alongside by fune


def add_business_days(iso: str, days: int, holidays: Sequence[str]) -> str:
    """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.
    """
    if isinstance(days, bool) or not isinstance(days, int):
        raise TypeError("days must be an integer, received %r" % (days,))
    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.
    excluded = {epoch_day_from_iso(holiday) for holiday in holidays}

    step = -1 if days < 0 else 1
    weekday = day_of_week(iso)
    remaining = abs(days)
    while remaining > 0:
        day += step
        weekday = (weekday - 1 + step) % 7 + 1
        if weekday <= 5 and day not in excluded:
            remaining -= 1
    return iso_from_epoch_day(day)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python 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.add-business-days
Download for Python dates.add-business-days-1.0.0-python.fune · 7,603 bytes sha256 1ac183d533a2d6f804d783ef748135394c20eb8fb4dd90986a3989e5bbfbf7f9

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./dates.add-business-days-1.0.0-python.fune, or fetch it from a terminal with fune pull dates.add-business-days@1.0.0:python.

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