Files
Time-Series-Analysis/venv/lib/python3.11/site-packages/holidays/observed_holiday_base.py
2025-08-01 04:33:03 -04:00

247 lines
9.1 KiB
Python

# holidays
# --------
# A fast, efficient Python library for generating country, province and state
# specific sets of holidays on the fly. It aims to make determining whether a
# specific date is a holiday as fast and flexible as possible.
#
# Authors: Vacanza Team and individual contributors (see CONTRIBUTORS file)
# dr-prodigy <dr.prodigy.github@gmail.com> (c) 2017-2023
# ryanss <ryanssdev@icloud.com> (c) 2014-2017
# Website: https://github.com/vacanza/holidays
# License: MIT (see LICENSE file)
from datetime import date
from typing import Optional
from holidays.calendars.gregorian import MON, TUE, WED, THU, FRI, SAT, SUN, _timedelta
from holidays.holiday_base import DateArg, HolidayBase
class ObservedRule(dict[int, Optional[int]]):
__slots__ = ()
def __add__(self, other):
return ObservedRule({**self, **other})
# Observance calculation rules: +7 - next workday, -7 - previous workday.
# Single days.
MON_TO_NEXT_TUE = ObservedRule({MON: +1})
MON_ONLY = ObservedRule({TUE: None, WED: None, THU: None, FRI: None, SAT: None, SUN: None})
TUE_TO_PREV_MON = ObservedRule({TUE: -1})
TUE_TO_PREV_FRI = ObservedRule({TUE: -4})
TUE_TO_NONE = ObservedRule({TUE: None})
WED_TO_PREV_MON = ObservedRule({WED: -2})
WED_TO_NEXT_FRI = ObservedRule({WED: +2})
THU_TO_PREV_MON = ObservedRule({THU: -3})
THU_TO_PREV_WED = ObservedRule({THU: -1})
THU_TO_NEXT_MON = ObservedRule({THU: +4})
THU_TO_NEXT_FRI = ObservedRule({THU: +1})
FRI_TO_PREV_WED = ObservedRule({FRI: -2})
FRI_TO_PREV_THU = ObservedRule({FRI: -1})
FRI_TO_NEXT_MON = ObservedRule({FRI: +3})
FRI_TO_NEXT_TUE = ObservedRule({FRI: +4})
FRI_TO_NEXT_SAT = ObservedRule({FRI: +1})
FRI_TO_NEXT_WORKDAY = ObservedRule({FRI: +7})
FRI_ONLY = ObservedRule({MON: None, TUE: None, WED: None, THU: None, SAT: None, SUN: None})
SAT_TO_PREV_THU = ObservedRule({SAT: -2})
SAT_TO_PREV_FRI = ObservedRule({SAT: -1})
SAT_TO_PREV_WORKDAY = ObservedRule({SAT: -7})
SAT_TO_NEXT_MON = ObservedRule({SAT: +2})
SAT_TO_NEXT_TUE = ObservedRule({SAT: +3})
SAT_TO_NEXT_SUN = ObservedRule({SAT: +1})
SAT_TO_NEXT_WORKDAY = ObservedRule({SAT: +7})
SAT_TO_NONE = ObservedRule({SAT: None})
SUN_TO_PREV_SAT = ObservedRule({SUN: -1})
SUN_TO_NEXT_MON = ObservedRule({SUN: +1})
SUN_TO_NEXT_TUE = ObservedRule({SUN: +2})
SUN_TO_NEXT_WED = ObservedRule({SUN: +3})
SUN_TO_NEXT_WORKDAY = ObservedRule({SUN: +7})
SUN_TO_NONE = ObservedRule({SUN: None})
# Multiple days.
ALL_TO_NEAREST_MON = ObservedRule({TUE: -1, WED: -2, THU: -3, FRI: +3, SAT: +2, SUN: +1})
ALL_TO_NEAREST_MON_LATAM = ObservedRule({TUE: -1, WED: -2, THU: 4, FRI: +3, SAT: +2, SUN: +1})
ALL_TO_NEXT_MON = ObservedRule({TUE: +6, WED: +5, THU: +4, FRI: +3, SAT: +2, SUN: +1})
ALL_TO_NEXT_SUN = ObservedRule({MON: +6, TUE: +5, WED: +4, THU: +3, FRI: +2, SAT: +1})
WORKDAY_TO_NEAREST_MON = ObservedRule({TUE: -1, WED: -2, THU: -3, FRI: +3})
WORKDAY_TO_NEXT_MON = ObservedRule({TUE: +6, WED: +5, THU: +4, FRI: +3})
WORKDAY_TO_NEXT_WORKDAY = ObservedRule({MON: +7, TUE: +7, WED: +7, THU: +7, FRI: +7})
MON_FRI_ONLY = ObservedRule({TUE: None, WED: None, THU: None, SAT: None, SUN: None})
TUE_WED_TO_PREV_MON = ObservedRule({TUE: -1, WED: -2})
TUE_WED_THU_TO_PREV_MON = ObservedRule({TUE: -1, WED: -2, THU: -3})
TUE_WED_THU_TO_NEXT_FRI = ObservedRule({TUE: +3, WED: +2, THU: +1})
WED_THU_TO_NEXT_FRI = ObservedRule({WED: +2, THU: +1})
THU_FRI_TO_NEXT_MON = ObservedRule({THU: +4, FRI: +3})
THU_FRI_TO_NEXT_WORKDAY = ObservedRule({THU: +7, FRI: +7})
THU_FRI_SUN_TO_NEXT_MON = ObservedRule({THU: +4, FRI: +3, SUN: +1})
FRI_SAT_TO_NEXT_WORKDAY = ObservedRule({FRI: +7, SAT: +7})
FRI_SUN_TO_NEXT_MON = ObservedRule({FRI: +3, SUN: +1})
FRI_SUN_TO_NEXT_SAT_MON = ObservedRule({FRI: +1, SUN: +1})
SAT_SUN_TO_PREV_FRI = ObservedRule({SAT: -1, SUN: -2})
SAT_SUN_TO_NEXT_MON = ObservedRule({SAT: +2, SUN: +1})
SAT_SUN_TO_NEXT_TUE = ObservedRule({SAT: +3, SUN: +2})
SAT_SUN_TO_NEXT_WED = ObservedRule({SAT: +4, SUN: +3})
SAT_SUN_TO_NEXT_MON_TUE = ObservedRule({SAT: +2, SUN: +2})
SAT_SUN_TO_NEXT_WORKDAY = ObservedRule({SAT: +7, SUN: +7})
class ObservedHolidayBase(HolidayBase):
"""Observed holidays implementation."""
observed_label = "%s"
def __init__(
self,
observed_rule: Optional[ObservedRule] = None,
observed_since: Optional[int] = None,
*args,
**kwargs,
):
self._observed_rule = observed_rule or ObservedRule()
self._observed_since = observed_since
super().__init__(*args, **kwargs)
def _is_observed(self, *args, **kwargs) -> bool:
return self._observed_since is None or self._year >= self._observed_since
def _get_next_workday(self, dt: date, delta: int = +1) -> date:
dt_work = _timedelta(dt, delta)
while dt_work.year == self._year:
if dt_work in self or self._is_weekend(dt_work): # type: ignore[operator]
dt_work = _timedelta(dt_work, delta)
else:
return dt_work
return dt
def _get_observed_date(self, dt: date, rule: ObservedRule) -> Optional[date]:
delta = rule.get(dt.weekday(), 0)
if delta:
return (
self._get_next_workday(dt, delta // 7)
if abs(delta) == 7
else _timedelta(dt, delta)
)
# Goes after `if delta` case as a less probable.
elif delta is None:
return None
return dt
def _add_observed(
self,
dt: Optional[DateArg] = None,
name: Optional[str] = None,
rule: Optional[ObservedRule] = None,
show_observed_label: bool = True,
) -> tuple[bool, Optional[date]]:
if dt is None:
return False, None
# Use as is if already a date.
# Convert to date: (m, d) → use self._year; (y, m, d) → use directly.
dt = dt if isinstance(dt, date) else date(self._year, *dt) if len(dt) == 2 else date(*dt)
if not self.observed or not self._is_observed(dt):
return False, dt
dt_observed = self._get_observed_date(dt, rule or self._observed_rule)
if dt_observed == dt:
return False, dt
# SAT_TO_NONE and similar cases.
if dt_observed is None:
self.pop(dt)
return False, None
if show_observed_label:
estimated_label = self.tr(getattr(self, "estimated_label", ""))
observed_label = self.tr(
getattr(
self,
"observed_label_before" if dt_observed < dt else "observed_label",
self.observed_label,
)
)
estimated_label_text = estimated_label.strip("%s ()")
# Use observed_estimated_label instead of observed_label for estimated dates.
for name in (name,) if name else self.get_list(dt):
holiday_name = self.tr(name)
observed_estimated_label = None
if estimated_label_text and estimated_label_text in holiday_name:
holiday_name = holiday_name.replace(f"({estimated_label_text})", "").strip()
observed_estimated_label = self.tr(getattr(self, "observed_estimated_label"))
super()._add_holiday(
(observed_estimated_label or observed_label) % holiday_name, dt_observed
)
else:
for name in (name,) if name else self.get_list(dt):
super()._add_holiday(name, dt_observed)
return True, dt_observed
def _move_holiday(
self, dt: date, rule: Optional[ObservedRule] = None, show_observed_label: bool = True
) -> tuple[bool, Optional[date]]:
is_observed, dt_observed = self._add_observed(
dt, rule=rule, show_observed_label=show_observed_label
)
if is_observed:
self.pop(dt)
return is_observed, dt_observed if is_observed else dt
def _populate_observed(self, dts: set[date], multiple: bool = False) -> None:
"""
When multiple is True, each holiday from a given date has its own observed date.
"""
for dt in sorted(dts):
if not self._is_observed(dt):
continue
if multiple:
for name in self.get_list(dt):
self._add_observed(dt, name)
else:
self._add_observed(dt)
def _populate_common_holidays(self):
"""Populate entity common holidays."""
super()._populate_common_holidays()
if not self.observed or not self.has_special_holidays:
return None
self._add_special_holidays(
(f"special_{category}_holidays_observed" for category in self._sorted_categories),
observed=True,
)
def _populate_subdiv_holidays(self):
"""Populate entity subdivision holidays."""
super()._populate_subdiv_holidays()
if not self.subdiv or not self.observed or not self.has_special_holidays:
return None
self._add_special_holidays(
(
f"special_{self._normalized_subdiv}_{category}_holidays_observed"
for category in self._sorted_categories
),
observed=True,
)