Functional Weave
Code in Python

dates.business-days-between@1.0.0

impl/python.py

2,344 bytes · the Python implementation · view 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, Set

from .dates_add_days import epoch_day_from_iso  ← 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 business_days_between(start_iso: str, end_iso: str, holidays: Sequence[str] = ()) -> int:
    """Working days between two dates, excluding weekends and listed holidays.

    The interval is half-open: the start date counts, the end date does not.
    That is the convention that makes ranges compose -
    business_days_between(a, b) + business_days_between(b, c) equals
    business_days_between(a, c) - and it makes "how many working days until the
    deadline" come out at zero on the deadline itself rather than one. If you
    want the end date included, ask for the day after it.

    A start later than the end returns a negative count, with the same
    magnitude as the forward direction, so business_days_between(a, b) is
    exactly -business_days_between(b, a). Returning zero or raising would both
    hide a caller's argument-order bug.

    Holidays are an argument, not built in, because no library knows which days
    your company is closed. Weekend holidays are not double counted, duplicates
    in the list are harmless, and holidays outside the interval are ignored.
    """
    start = epoch_day_from_iso(start_iso)
    end = epoch_day_from_iso(end_iso)

    # Every holiday is validated even when it falls outside the interval: a typo
    # in a holiday calendar should fail loudly on the next run, not lie dormant
    # until the year the date is finally inside a query.
    excluded = {epoch_day_from_iso(holiday) for holiday in holidays}

    if start > end:
        return -_count_working_days(end, end_iso, start, excluded)
    return _count_working_days(start, start_iso, end, excluded)


def _count_working_days(from_day: int, from_iso: str, to_day: int, excluded: Set[int]) -> int:
    """Working days in the half-open interval [from_day, to_day)."""
    # The weekday is carried forward rather than recomputed per day: one date
    # parse, then a seven-day cycle, which is the same loop in all three
    # languages and cannot drift between them.
    weekday = day_of_week(from_iso)
    count = 0
    for day in range(from_day, to_day):
        if weekday <= 5 and day not in excluded:
            count += 1
        weekday = 1 if weekday == 7 else weekday + 1
    return count