Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions dateutils/dateutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,8 @@ def get_us_federal_holidays(year: int, holiday_types: tuple[str, ...] | None = N
"VETERANS_DAY", "THANKSGIVING", "CHRISTMAS"

Returns:
List[date]: A list of date objects for the holidays in that year.
List[date]: A list of date objects for the holidays in that year,
sorted in chronological order.

Examples:
>>> from datetime import date
Expand Down Expand Up @@ -781,17 +782,17 @@ def find_nth_weekday(year: int, month: int, weekday: int, n: int, from_end: bool
# 4th Thursday in November (Thanksgiving Day)
all_holiday_types["THANKSGIVING"] = find_nth_weekday(year, 11, 3, 4) # Thursday = 3

# If holiday_types is None, return all holidays
# If holiday_types is None, return all holidays in chronological order
if holiday_types is None:
return tuple(all_holiday_types.values())
return tuple(sorted(all_holiday_types.values()))

# Otherwise, return only the specified holiday types
result = []
for holiday_type in holiday_types:
if holiday_type in all_holiday_types:
result.append(all_holiday_types[holiday_type])

return tuple(result)
return tuple(sorted(result))


def get_us_federal_holidays_list(year: int, holiday_types: list[str] | None = None) -> list[date]:
Expand Down Expand Up @@ -935,6 +936,12 @@ def add_business_days(dt: date, num_days: int, holidays: Iterable[date] | None =
if not -10000 <= num_days <= 10000: # noqa: PLR2004
raise ValueError(f"num_days must be between -10000 and 10000, got {num_days}")

if num_days == 0:
return dt

if holidays is None or (isinstance(holidays, (set, frozenset, list, tuple)) and len(holidays) == 0):
return _add_business_days_no_holidays(dt, num_days)

holidays_set: set[date] = set(holidays) if holidays is not None else set()
current = dt
added = 0
Expand All @@ -948,6 +955,50 @@ def add_business_days(dt: date, num_days: int, holidays: Iterable[date] | None =
return current


def _add_business_days_no_holidays(dt: date, num_days: int) -> date:
"""Fast path for add_business_days when there are no holidays."""
if num_days == 0:
return dt
direction = 1 if num_days > 0 else -1
remaining = abs(num_days)
dt, weekday = _normalize_business_day_start(dt, direction)
days = _business_days_to_calendar_days(weekday, remaining, direction)
return dt + timedelta(days=days * direction)


def _normalize_business_day_start(dt: date, direction: int) -> tuple[date, int]:
"""Normalize weekend start dates for business-day arithmetic."""
weekday = dt.weekday()
if direction > 0:
if weekday == calendar.SATURDAY:
dt -= timedelta(days=1)
weekday = calendar.FRIDAY
elif weekday == calendar.SUNDAY:
dt -= timedelta(days=2)
weekday = calendar.FRIDAY
return dt, weekday

if weekday == calendar.SATURDAY:
dt += timedelta(days=2)
weekday = calendar.MONDAY
elif weekday == calendar.SUNDAY:
dt += timedelta(days=1)
weekday = calendar.MONDAY
return dt, weekday


def _business_days_to_calendar_days(weekday: int, remaining: int, direction: int) -> int:
"""Convert a business-day count into calendar days, excluding holidays."""
weeks, extra = divmod(remaining, WEEKDAYS_IN_WEEK)
days = weeks * DAYS_IN_WEEK
if extra:
if direction > 0:
days += extra + 2 if weekday + extra >= WEEKDAYS_IN_WEEK else extra
else:
days += extra + 2 if weekday - extra < 0 else extra
return days


def next_business_day(dt: date, holidays: Iterable[date] | None = None) -> date:
"""
Find the next business day from a given date, skipping weekends and holidays.
Expand Down
1 change: 1 addition & 0 deletions tests/test_dateutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,7 @@ def test_get_us_federal_holidays_all_2024() -> None:
holidays_2024 = get_us_federal_holidays(2024)
# Expected count: 5 fixed + 6 floating = 11
assert len(holidays_2024) == 11
assert holidays_2024 == sorted(holidays_2024)

# Check fixed holidays
assert datetime.date(2024, 1, 1) in holidays_2024 # New Year's Day
Expand Down