Source code for hdate.holidays

"""Holidays module, contains the holiday information and related functions."""

from __future__ import annotations

from bisect import bisect_left
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass, field
from enum import Enum
from itertools import product
from typing import Callable, ClassVar, Iterable, Literal, Optional, Union

from hdate.hebrew_date import CHANGING_MONTHS, LONG_MONTHS, HebrewDate, Months, Weekday
from hdate.translator import Language, TranslatorMixin


[docs] class HolidayTypes(Enum): """Container class for holiday type integer mappings.""" YOM_TOV = 1 EREV_YOM_TOV = 2 HOL_HAMOED = 3 MELACHA_PERMITTED_HOLIDAY = 4 FAST_DAY = 5 MODERN_HOLIDAY = 6 MINOR_HOLIDAY = 7 MEMORIAL_DAY = 8 ISRAEL_NATIONAL_HOLIDAY = 9 ROSH_CHODESH = 10
@dataclass(frozen=True) class Holiday(TranslatorMixin): """Container class for holiday information.""" type: HolidayTypes name: str date: Union[tuple[Union[Months, tuple[Months, ...]], Union[int, tuple[int, ...]]]] date_functions_list: list[ Callable[[HebrewDate], Union[bool, Callable[[], bool]]] ] = field(default_factory=list) israel_diaspora: Literal["ISRAEL", "DIASPORA", ""] = "" @dataclass class HolidayDatabase: """Container class for holiday information.""" diaspora: bool _diaspora_holidays: ClassVar[dict[HebrewDate, list[Holiday]]] _israel_holidays: ClassVar[dict[HebrewDate, list[Holiday]]] _all_holidays: ClassVar[dict[HebrewDate, list[Holiday]]] def __post_init__(self) -> None: self._instance_holidays = deepcopy(self._all_holidays) if self.diaspora: for date, holidays in self._diaspora_holidays.items(): self._instance_holidays[date].extend(holidays) else: for date, holidays in self._israel_holidays.items(): self._instance_holidays[date].extend(holidays) self._instance_holidays = dict(sorted(self._instance_holidays.items())) @classmethod def register_holidays(cls, holidays: list[Holiday]) -> None: """Register a list of holidays with the holiday manager.""" cls._diaspora_holidays = defaultdict(list) cls._israel_holidays = defaultdict(list) cls._all_holidays = defaultdict(list) def holiday_dates_cross_product( dates: Union[ tuple[Union[Months, tuple[Months, ...]], Union[int, tuple[int, ...]]] ], ) -> Iterable[tuple[Months, int]]: """Given a (days, months) pair, compute the cross product. If days and/or months are singletons, they are converted to a list. """ months = (dates[0],) if isinstance(dates[0], Months) else dates[0] days = (dates[1],) if isinstance(dates[1], int) else dates[1] return product(months, days) for holiday in holidays: for date in holiday_dates_cross_product(holiday.date): index = HebrewDate(0, *date) if holiday.israel_diaspora == "ISRAEL": cls._israel_holidays[index].append(holiday) elif holiday.israel_diaspora == "DIASPORA": cls._diaspora_holidays[index].append(holiday) else: cls._all_holidays[index].append(holiday) def lookup(self, date: HebrewDate) -> list[Holiday]: """Lookup the holidays for a given date.""" if all(_date != date for _date in self._instance_holidays): return [] holidays = next( h for _date, h in self._instance_holidays.items() if _date == date ) return [ holiday for holiday in holidays if all(func(date) for func in holiday.date_functions_list) ] def _get_filtered_holidays( self, types: Optional[Union[list[HolidayTypes], HolidayTypes]] ) -> dict[HebrewDate, list[Holiday]]: """Return a list of filtered holidays, based on type.""" filtered_holidays = deepcopy(self._instance_holidays) if types: types = [types] if isinstance(types, HolidayTypes) else types filtered_holidays = { _date: [holiday for holiday in holidays if holiday.type in types] for _date, holidays in filtered_holidays.items() if any(holiday.type in types for holiday in holidays) } return filtered_holidays def lookup_holidays_for_year( self, date: HebrewDate, types: Optional[Union[list[HolidayTypes], HolidayTypes]] = None, ) -> dict[HebrewDate, list[Holiday]]: """Lookup the holidays for a given year.""" filtered_holidays = self._get_filtered_holidays(types) result = { (real_date := _date.replace(year=date.year)): [ holiday for holiday in holidays if all(func(real_date) for func in holiday.date_functions_list) ] for _date, holidays in filtered_holidays.items() if _date.valid_for_year(date.year) } return { _date: _holidays for _date, _holidays in result.items() if len(_holidays) > 0 } def lookup_next_holiday( self, date: HebrewDate, types: Optional[Union[list[HolidayTypes], HolidayTypes]] = None, ) -> HebrewDate: """Lookup the next holiday for a given date (with optional type filter).""" filtered_holidays = self._get_filtered_holidays(types) valid_dates = [ _date for _date in list(filtered_holidays.keys()) if _date.valid_for_year(date.year) ] next_date_idx = bisect_left(valid_dates, date) if next_date_idx == len(valid_dates): return HebrewDate(year=date.year + 1) next_date = valid_dates[next_date_idx] return next_date.replace(year=date.year) def get_all_names(self, language: Language) -> list[str]: """Return all the holiday names in a given language.""" result = {""} # Empty string for case of no holiday for holidays in self._instance_holidays.values(): holiday_names = {holiday.name for holiday in holidays} if {"yom_haatzmaut", "yom_hazikaron"} == holiday_names: continue for holiday in holidays: holiday.set_language(language) holiday_strs = list(dict.fromkeys(str(holiday) for holiday in holidays)) result.add(", ".join(holiday_strs)) return list(sorted(result)) def move_if_not_on_dow( original: int, replacement: int, dow_not_orig: Weekday, dow_replacement: Weekday ) -> Callable[[HebrewDate], bool]: """ Return a lambda function. Lambda checks that either the original day does not fall on a given weekday, or that the replacement day does fall on the expected weekday. """ return lambda x: ( (x.day == original and x.dow() != dow_not_orig) or (x.day == replacement and x.dow() == dow_replacement) ) def year_is_before(year: int) -> Callable[[HebrewDate], bool]: """ Return a lambda function. Lambda checks that a given HDate object's hebrew year is before the requested year. """ return lambda x: x.year < year def year_is_after(year: int) -> Callable[[HebrewDate], bool]: """ Return a lambda function. Lambda checks that a given HDate object's hebrew year is after the requested year. """ return lambda x: x.year > year HOLIDAYS = ( Holiday(HolidayTypes.EREV_YOM_TOV, "erev_rosh_hashana", (Months.ELUL, 29)), Holiday(HolidayTypes.YOM_TOV, "rosh_hashana_i", (Months.TISHREI, 1)), Holiday(HolidayTypes.YOM_TOV, "rosh_hashana_ii", (Months.TISHREI, 2)), Holiday( HolidayTypes.FAST_DAY, "tzom_gedaliah", (Months.TISHREI, (3, 4)), [move_if_not_on_dow(3, 4, Weekday.SATURDAY, Weekday.SUNDAY)], ), Holiday(HolidayTypes.EREV_YOM_TOV, "erev_yom_kippur", (Months.TISHREI, 9)), Holiday(HolidayTypes.YOM_TOV, "yom_kippur", (Months.TISHREI, 10)), Holiday(HolidayTypes.EREV_YOM_TOV, "erev_sukkot", (Months.TISHREI, 14)), Holiday(HolidayTypes.YOM_TOV, "sukkot", (Months.TISHREI, 15)), Holiday( HolidayTypes.HOL_HAMOED, "hol_hamoed_sukkot", (Months.TISHREI, 16), [], "ISRAEL" ), Holiday( HolidayTypes.HOL_HAMOED, "hol_hamoed_sukkot", (Months.TISHREI, (17, 18, 19, 20)) ), Holiday(HolidayTypes.EREV_YOM_TOV, "hoshana_raba", (Months.TISHREI, 21)), Holiday( HolidayTypes.YOM_TOV, "simchat_torah", (Months.TISHREI, 23), [], "DIASPORA" ), Holiday(HolidayTypes.YOM_TOV, "simchat_torah", (Months.TISHREI, 22), [], "ISRAEL"), Holiday( HolidayTypes.MELACHA_PERMITTED_HOLIDAY, "chanukah", (Months.KISLEV, (25, 26, 27, 28, 29, 30)), ), Holiday( HolidayTypes.MELACHA_PERMITTED_HOLIDAY, "chanukah", (Months.TEVET, (1, 2, 3)), [lambda x: ((x.short_kislev() and x.day == 3) or (x.day in [1, 2]))], ), Holiday(HolidayTypes.FAST_DAY, "asara_btevet", (Months.TEVET, 10)), Holiday(HolidayTypes.MINOR_HOLIDAY, "tu_bshvat", (Months.SHVAT, 15)), Holiday( HolidayTypes.FAST_DAY, "taanit_esther", ((Months.ADAR, Months.ADAR_II), (11, 13)), [move_if_not_on_dow(13, 11, Weekday.SATURDAY, Weekday.THURSDAY)], ), Holiday( HolidayTypes.MELACHA_PERMITTED_HOLIDAY, "purim", ((Months.ADAR, Months.ADAR_II), 14), ), Holiday( HolidayTypes.MELACHA_PERMITTED_HOLIDAY, "shushan_purim", ((Months.ADAR, Months.ADAR_II), 15), ), Holiday(HolidayTypes.EREV_YOM_TOV, "erev_pesach", (Months.NISAN, 14)), Holiday(HolidayTypes.YOM_TOV, "pesach", (Months.NISAN, 15)), Holiday( HolidayTypes.HOL_HAMOED, "hol_hamoed_pesach", (Months.NISAN, 16), [], "ISRAEL" ), Holiday(HolidayTypes.HOL_HAMOED, "hol_hamoed_pesach", (Months.NISAN, (17, 18, 19))), Holiday(HolidayTypes.EREV_YOM_TOV, "hol_hamoed_pesach", (Months.NISAN, 20)), Holiday(HolidayTypes.YOM_TOV, "pesach_vii", (Months.NISAN, 21)), Holiday( HolidayTypes.MODERN_HOLIDAY, "yom_haatzmaut", (Months.IYYAR, (3, 4, 5)), [ year_is_after(5708), year_is_before(5764), move_if_not_on_dow(5, 4, Weekday.FRIDAY, Weekday.THURSDAY) # type: ignore or move_if_not_on_dow(5, 3, Weekday.SATURDAY, Weekday.THURSDAY), ], ), Holiday( HolidayTypes.MODERN_HOLIDAY, "yom_haatzmaut", (Months.IYYAR, (3, 4, 5, 6)), [ year_is_after(5763), move_if_not_on_dow(5, 4, Weekday.FRIDAY, Weekday.THURSDAY) # type: ignore or move_if_not_on_dow(5, 3, Weekday.SATURDAY, Weekday.THURSDAY) or move_if_not_on_dow(5, 6, Weekday.MONDAY, Weekday.TUESDAY), ], ), Holiday(HolidayTypes.MINOR_HOLIDAY, "lag_bomer", (Months.IYYAR, 18)), Holiday(HolidayTypes.EREV_YOM_TOV, "erev_shavuot", (Months.SIVAN, 5)), Holiday(HolidayTypes.YOM_TOV, "shavuot", (Months.SIVAN, 6)), Holiday( HolidayTypes.FAST_DAY, "tzom_tammuz", (Months.TAMMUZ, (17, 18)), [move_if_not_on_dow(17, 18, Weekday.SATURDAY, Weekday.SUNDAY)], ), Holiday( HolidayTypes.FAST_DAY, "tisha_bav", (Months.AV, (9, 10)), [move_if_not_on_dow(9, 10, Weekday.SATURDAY, Weekday.SUNDAY)], ), Holiday(HolidayTypes.MINOR_HOLIDAY, "tu_bav", (Months.AV, 15)), Holiday( HolidayTypes.MEMORIAL_DAY, "yom_hashoah", (Months.NISAN, (26, 27, 28)), [ move_if_not_on_dow(27, 28, Weekday.SUNDAY, Weekday.MONDAY) # type: ignore or move_if_not_on_dow(27, 26, Weekday.FRIDAY, Weekday.THURSDAY), year_is_after(5718), ], ), Holiday( HolidayTypes.MEMORIAL_DAY, "yom_hazikaron", (Months.IYYAR, (2, 3, 4)), [ year_is_after(5708), year_is_before(5764), move_if_not_on_dow( 4, 3, Weekday.THURSDAY, Weekday.WEDNESDAY ) # type: ignore or move_if_not_on_dow(4, 2, Weekday.FRIDAY, Weekday.WEDNESDAY), ], ), Holiday( HolidayTypes.MEMORIAL_DAY, "yom_hazikaron", (Months.IYYAR, (2, 3, 4, 5)), [ year_is_after(5763), move_if_not_on_dow( 4, 3, Weekday.THURSDAY, Weekday.WEDNESDAY ) # type: ignore or move_if_not_on_dow(4, 2, Weekday.FRIDAY, Weekday.WEDNESDAY) or move_if_not_on_dow(4, 5, Weekday.SUNDAY, Weekday.MONDAY), ], ), Holiday( HolidayTypes.MODERN_HOLIDAY, "yom_yerushalayim", (Months.IYYAR, 28), [year_is_after(5727)], ), Holiday(HolidayTypes.YOM_TOV, "shmini_atzeret", (Months.TISHREI, 22)), Holiday(HolidayTypes.YOM_TOV, "pesach_viii", (Months.NISAN, 22), [], "DIASPORA"), Holiday(HolidayTypes.YOM_TOV, "shavuot_ii", (Months.SIVAN, 7), [], "DIASPORA"), Holiday(HolidayTypes.YOM_TOV, "sukkot_ii", (Months.TISHREI, 16), [], "DIASPORA"), Holiday(HolidayTypes.YOM_TOV, "pesach_ii", (Months.NISAN, 16), [], "DIASPORA"), Holiday( HolidayTypes.ISRAEL_NATIONAL_HOLIDAY, "family_day", (Months.SHVAT, 30), [year_is_after(5733)], "ISRAEL", ), Holiday( HolidayTypes.MEMORIAL_DAY, "memorial_day_unknown", ((Months.ADAR, Months.ADAR_II), 7), [], "ISRAEL", ), Holiday( HolidayTypes.MEMORIAL_DAY, "rabin_memorial_day", (Months.MARCHESHVAN, (11, 12)), [ move_if_not_on_dow(12, 11, Weekday.FRIDAY, Weekday.THURSDAY), year_is_after(5757), ], "ISRAEL", ), Holiday( HolidayTypes.MEMORIAL_DAY, "zeev_zhabotinsky_day", (Months.TAMMUZ, 29), [year_is_after(5764)], "ISRAEL", ), Holiday( HolidayTypes.ROSH_CHODESH, "rosh_chodesh", (tuple(set(Months) - {Months.TISHREI}), 1), ), Holiday( HolidayTypes.ROSH_CHODESH, "rosh_chodesh", (LONG_MONTHS + CHANGING_MONTHS, 30) ), ) HolidayDatabase.register_holidays(list(HOLIDAYS))