"""Holidays module, contains the holiday information and related functions."""
from __future__ import annotations
import datetime as dt
from bisect import bisect_left
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass, field
from enum import Enum
from functools import lru_cache
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 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
FilterType = Union[list[HolidayTypes], HolidayTypes]
@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 _get_filtered_holidays(
self, types: Optional[FilterType]
) -> 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(
self, date: HebrewDate, types: Optional[FilterType] = None
) -> list[Holiday]:
"""Lookup the holidays for a given date."""
filtered_holidays = self._get_filtered_holidays(types)
if all(_date != date for _date in filtered_holidays):
return []
holidays = filtered_holidays[date.replace(year=0)]
return [
holiday
for holiday in holidays
if all(func(date) for func in holiday.date_functions_list)
]
def lookup_holidays_for_year(
self, date: HebrewDate, types: Optional[FilterType] = 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) -> list[str]:
"""Return all the holiday names."""
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
holiday_strs = list(dict.fromkeys(str(holiday) for holiday in holidays))
result.add(", ".join(holiday_strs))
return list(sorted(result))
@lru_cache
def is_yom_tov(date: Union[dt.date, HebrewDate], diaspora: bool = False) -> bool:
"""Helper method to check if a given date is a Yom Tov"""
if isinstance(date, dt.date):
date = HebrewDate.from_gdate(date)
holidays = HolidayDatabase(diaspora).lookup(date, types=HolidayTypes.YOM_TOV)
return len(holidays) > 0
def not_on_dow(dow: list[Weekday]) -> Callable[[HebrewDate], bool]:
"""
Return a lambda function.
Lambda checks that dow is not on one of the given weekdays.
"""
return lambda x: x.dow() not in dow
def only_on_dow(dow: Weekday) -> Callable[[HebrewDate], bool]:
"""
Return a lambda function.
Lambda checks that dow is equal to the givem weekday.
"""
return lambda x: x.dow() == dow
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),
[not_on_dow([Weekday.SATURDAY])],
),
Holiday(
HolidayTypes.FAST_DAY,
"tzom_gedaliah",
(Months.TISHREI, 4),
[only_on_dow(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),
[only_on_dow(Weekday.THURSDAY)],
),
Holiday(
HolidayTypes.FAST_DAY,
"taanit_esther",
((Months.ADAR, Months.ADAR_II), 13),
[not_on_dow([Weekday.SATURDAY])],
),
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)),
[year_is_after(5708), only_on_dow(Weekday.THURSDAY)],
),
Holiday(
HolidayTypes.MODERN_HOLIDAY,
"yom_haatzmaut",
(Months.IYYAR, 5),
[
year_is_after(5708),
year_is_before(5764),
not_on_dow([Weekday.FRIDAY, Weekday.SATURDAY]),
],
),
Holiday(
HolidayTypes.MODERN_HOLIDAY,
"yom_haatzmaut",
(Months.IYYAR, 5),
[
year_is_after(5763),
not_on_dow([Weekday.FRIDAY, Weekday.SATURDAY, Weekday.MONDAY]),
],
),
Holiday(
HolidayTypes.MODERN_HOLIDAY,
"yom_haatzmaut",
(Months.IYYAR, 6),
[year_is_after(5763), only_on_dow(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),
[not_on_dow([Weekday.SATURDAY])],
),
Holiday(
HolidayTypes.FAST_DAY,
"tzom_tammuz",
(Months.TAMMUZ, 18),
[only_on_dow(Weekday.SUNDAY)],
),
Holiday(
HolidayTypes.FAST_DAY,
"tisha_bav",
(Months.AV, 9),
[not_on_dow([Weekday.SATURDAY])],
),
Holiday(
HolidayTypes.FAST_DAY,
"tisha_bav",
(Months.AV, 10),
[only_on_dow(Weekday.SUNDAY)],
),
Holiday(HolidayTypes.MINOR_HOLIDAY, "tu_bav", (Months.AV, 15)),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hashoah",
(Months.NISAN, 26),
[only_on_dow(Weekday.THURSDAY), year_is_after(5718)],
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hashoah",
(Months.NISAN, 27),
[not_on_dow([Weekday.SUNDAY, Weekday.FRIDAY]), year_is_after(5718)],
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hashoah",
(Months.NISAN, 28),
[only_on_dow(Weekday.MONDAY), year_is_after(5718)],
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hazikaron",
(Months.IYYAR, (2, 3)),
[year_is_after(5708), only_on_dow(Weekday.WEDNESDAY)],
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hazikaron",
(Months.IYYAR, 4),
[
year_is_after(5708),
year_is_before(5764),
not_on_dow([Weekday.THURSDAY, Weekday.FRIDAY]),
],
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hazikaron",
(Months.IYYAR, 4),
[
year_is_after(5763),
not_on_dow([Weekday.THURSDAY, Weekday.FRIDAY, Weekday.SUNDAY]),
],
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"yom_hazikaron",
(Months.IYYAR, 5),
[year_is_after(5763), only_on_dow(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),
[not_on_dow([Weekday.FRIDAY]), year_is_after(5757)],
"ISRAEL",
),
Holiday(
HolidayTypes.MEMORIAL_DAY,
"rabin_memorial_day",
(Months.MARCHESHVAN, 12),
[only_on_dow(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))