# 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 (c) 2017-2023 # ryanss (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, )