Skip to content

Commit

Permalink
Update observed rules: add holiday removal support (#1796)
Browse files Browse the repository at this point in the history
  • Loading branch information
arkid15r authored May 17, 2024
1 parent 353d86d commit c8c408d
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 15 deletions.
4 changes: 2 additions & 2 deletions holidays/countries/angola.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from datetime import date
from gettext import gettext as tr
from typing import Tuple
from typing import Optional, Tuple

from holidays.calendars.gregorian import AUG, SEP
from holidays.groups import ChristianHolidays, InternationalHolidays, StaticHolidays
Expand Down Expand Up @@ -59,7 +59,7 @@ def _is_observed(self, dt: date) -> bool:
# it rolls over to the following Monday.
return dt >= date(1996, SEP, 27)

def _add_observed(self, dt: date, **kwargs) -> Tuple[bool, date]:
def _add_observed(self, dt: date, **kwargs) -> Tuple[bool, Optional[date]]:
# As per Law # #11/18, from 2018/9/10, when public holiday falls on Tuesday or Thursday,
# the Monday or Friday is also a holiday.
kwargs.setdefault(
Expand Down
3 changes: 2 additions & 1 deletion holidays/countries/canada.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from datetime import date
from gettext import gettext as tr
from typing import Optional

from holidays.calendars.gregorian import MAR, APR, JUN, JUL, SEP
from holidays.constants import GOVERNMENT, OPTIONAL, PUBLIC
Expand Down Expand Up @@ -69,7 +70,7 @@ def __init__(self, *args, **kwargs):
kwargs.setdefault("observed_rule", SAT_SUN_TO_NEXT_MON)
super().__init__(*args, **kwargs)

def _get_nearest_monday(self, *args) -> date:
def _get_nearest_monday(self, *args) -> Optional[date]:
return self._get_observed_date(date(self._year, *args), rule=ALL_TO_NEAREST_MON)

def _add_statutory_holidays(self):
Expand Down
4 changes: 2 additions & 2 deletions holidays/countries/jersey.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# License: MIT (see LICENSE file)

from datetime import date
from typing import Tuple
from typing import Optional, Tuple

from holidays.calendars.gregorian import JAN, APR, MAY, JUN, JUL, SEP, OCT, DEC
from holidays.groups import ChristianHolidays, InternationalHolidays, StaticHolidays
Expand Down Expand Up @@ -54,7 +54,7 @@ def __init__(self, *args, **kwargs):
kwargs.setdefault("observed_rule", SAT_SUN_TO_NEXT_WORKDAY)
ObservedHolidayBase.__init__(self, *args, **kwargs)

def _add_observed(self, dt: date, **kwargs) -> Tuple[bool, date]:
def _add_observed(self, dt: date, **kwargs) -> Tuple[bool, Optional[date]]:
# Prior to 2004, in-lieu are only given for Sundays.
# https://www.jerseylaw.je/laws/enacted/Pages/RO-123-2004.aspx
kwargs.setdefault(
Expand Down
3 changes: 2 additions & 1 deletion holidays/countries/new_zealand.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# License: MIT (see LICENSE file)

from datetime import date
from typing import Optional

from holidays.calendars.gregorian import JAN, FEB, MAR, JUN, JUL, SEP, NOV, DEC, _timedelta
from holidays.groups import ChristianHolidays, InternationalHolidays, StaticHolidays
Expand Down Expand Up @@ -74,7 +75,7 @@ def __init__(self, *args, **kwargs):
kwargs.setdefault("observed_rule", SAT_SUN_TO_NEXT_MON)
super().__init__(*args, **kwargs)

def _get_nearest_monday(self, *args) -> date:
def _get_nearest_monday(self, *args) -> Optional[date]:
dt = args if len(args) > 1 else args[0]
dt = dt if isinstance(dt, date) else date(self._year, *dt)
return self._get_observed_date(dt, rule=ALL_TO_NEAREST_MON)
Expand Down
30 changes: 21 additions & 9 deletions holidays/observed_holiday_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from holidays.holiday_base import DateArg, HolidayBase


class ObservedRule(Dict[int, int]):
class ObservedRule(Dict[int, Optional[int]]):
__slots__ = ()

def __add__(self, other):
Expand Down Expand Up @@ -113,18 +113,23 @@ def _get_next_workday(self, dt: date, delta: int = +1) -> date:
return dt_work
return dt

def _get_observed_date(self, dt: date, rule: ObservedRule) -> date:
def _get_observed_date(self, dt: date, rule: ObservedRule) -> Optional[date]:
delta = rule.get(dt.weekday(), 0)
if delta != 0:
if abs(delta) == 7:
dt = self._get_next_workday(dt, delta // 7)
else:
dt = _timedelta(dt, delta)
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: DateArg, name: Optional[str] = None, rule: Optional[ObservedRule] = None
) -> Tuple[bool, date]:
) -> Tuple[bool, Optional[date]]:
dt = dt if isinstance(dt, date) else date(self._year, *dt)

if not self.observed or not self._is_observed(dt):
Expand All @@ -134,6 +139,11 @@ def _add_observed(
if dt_observed == dt:
return False, dt

# SAT_TO_NONE and similar cases.
if dt_observed is None:
self.pop(dt)
return False, None

estimated_label = self.tr(getattr(self, "estimated_label", ""))
observed_label = self.tr(
getattr(
Expand All @@ -158,7 +168,9 @@ def _add_observed(

return True, dt_observed

def _move_holiday(self, dt: date, rule: Optional[ObservedRule] = None) -> Tuple[bool, date]:
def _move_holiday(
self, dt: date, rule: Optional[ObservedRule] = None
) -> Tuple[bool, Optional[date]]:
is_observed, dt_observed = self._add_observed(dt, rule=rule)
if is_observed:
self.pop(dt)
Expand Down
56 changes: 56 additions & 0 deletions tests/test_observed_holiday_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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 AUTHORS file)
# dr-prodigy <dr.prodigy.github@gmail.com> (c) 2017-2023
# ryanss <ryanssdev@icloud.com> (c) 2014-2017
# Website: https://github.com/vacanza/python-holidays
# License: MIT (see LICENSE file)

from datetime import date
from unittest import TestCase

from holidays.calendars.gregorian import MON, SUN
from holidays.observed_holiday_base import ObservedHolidayBase, ObservedRule


class TestObservedHolidayBase(TestCase):
SUNDAY = date(2024, 5, 12)
MONDAY = date(2024, 5, 13)
SUN_TO_NONE = ObservedRule({SUN: None})
MON_TO_TUE = ObservedRule({MON: +1})

def setUp(self):
self.ohb = ObservedHolidayBase(observed_rule=ObservedRule({MON: None}))
self.ohb.observed_label = "%s (Observed Label)"
self.ohb._populate(2024)

def test_get_observed_date(self):
self.assertIsNone(self.ohb._get_observed_date(self.SUNDAY, rule=self.SUN_TO_NONE))

def test_observed_rules(self):
self.assertEqual(
(False, None),
self.ohb._add_observed(
self.ohb._add_holiday("Test Holiday", self.SUNDAY), rule=self.SUN_TO_NONE
),
)

self.assertEqual(
(True, date(2024, 5, 14)),
self.ohb._add_observed(
self.ohb._add_holiday("Test Holiday", self.MONDAY), rule=self.MON_TO_TUE
),
)

self.assertEqual(
dict(self.ohb),
{
date(2024, 5, 13): "Test Holiday",
date(2024, 5, 14): "Test Holiday (Observed Label)",
},
self.ohb,
)

0 comments on commit c8c408d

Please sign in to comment.