from typing import Sequence, Set from .dates_add_days import epoch_day_from_iso from .dates_day_of_week import day_of_week 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