diff --git a/docs/source/examples.rst b/docs/source/examples.rst index f43a76909..bf726e029 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -214,6 +214,34 @@ To calculate the number or working days between two specified dates: Here we calculate the number of working days in Q2 2024. +Getting the next/previous holiday +--------------------------------- + +You can request the next/previous holiday of your selected calendar. +The function returns the date and the name of the holiday - exluding today. + +.. code-block:: python + + >>> us_holidays = holidays.US(years=2025) + >>> us_holidays.get_next_holiday() # get the next holiday after today + (datetime.date(2025, 1, 20), 'Martin Luther King Jr. Day') + >>> us_holidays.get_next_holiday(previous=True) # get the previous holiday before today + (datetime.date(2025, 1, 1), "New Year's Day") + >>> us_holidays.get_next_holiday("2025-02-01") # get the next holiday after a specific date + (datetime.date(2025, 2, 17), "Washington's Birthday") + >>> us_holidays.get_next_holiday("2025-02-01", previous=True) # get the previous holiday before a specific date + (datetime.date(2025, 1, 20), 'Martin Luther King Jr. Day') + +If no holiday can be found (e.g. because the date would be after the end date / +before the start date), (None, None) is returned. + +.. code-block:: python + + >>> us_holidays.get_next_holiday("2100-12-31") + (None, None) + >>> us_holidays.get_next_holiday("1777-01-01", previous=True) + (None, None) + Date from holiday name ---------------------- diff --git a/holidays/holiday_base.py b/holidays/holiday_base.py index 2db65448c..20daa6014 100644 --- a/holidays/holiday_base.py +++ b/holidays/holiday_base.py @@ -960,6 +960,29 @@ def get_named( raise AttributeError(f"Unknown lookup type: {lookup}") + def get_next_holiday( + self, start: DateLike = None, previous: bool = False + ) -> Union[tuple[date, str], tuple[None, None]]: + """Return the date and name of the next holiday from provided date + (if previous is False) or the previous holiday (if previous is True). + If no date is given the search starts from current date""" + + dt = self.__keytransform__(start) if start else datetime.now().date() + if not previous: + next_date = next((x for x in self if x > dt), None) + if not next_date and dt.year < self.end_year: + self._populate(dt.year + 1) + next_date = next((x for x in self if x > dt), None) + else: + next_date = next((x for x in reversed(self) if x < dt), None) + if not next_date and dt.year > self.start_year: + self._populate(dt.year - 1) + next_date = next((x for x in reversed(self) if x < dt), None) + if next_date: + return next_date, self.get(next_date) + else: + return None, None + def get_nth_working_day(self, key: DateLike, n: int) -> date: """Return n-th working day from provided date (if n is positive) or n-th working day before provided date (if n is negative). diff --git a/tests/test_holiday_base.py b/tests/test_holiday_base.py index c5563c6b1..8844cc10c 100644 --- a/tests/test_holiday_base.py +++ b/tests/test_holiday_base.py @@ -1189,3 +1189,87 @@ def test_get_working_days_count(self): self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-04"), 3) self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-05"), 3) self.assertEqual(self.hb.get_working_days_count("2024-04-29", "2024-05-06"), 4) + + +class TestNextHoliday(unittest.TestCase): + def setUp(self): + self.this_year = datetime.now().year + self.next_year = self.this_year + 1 + self.previous_year = self.this_year - 1 + self.hb = CountryStub3(years=self.this_year) + self.next_labor_day_year = ( + self.this_year + if datetime.now().date() < self.hb.get_named("Custom May 1st Holiday")[0] + else self.next_year + ) + self.previous_labor_day_year = ( + self.this_year + if datetime.now().date() > self.hb.get_named("Custom May 1st Holiday")[0] + else self.previous_year + ) + + def test_get_next_holiday_forward(self): + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-01-01"), + (date(self.this_year, 5, 1), "Custom May 1st Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-04-30"), + (date(self.this_year, 5, 1), "Custom May 1st Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-05-01"), + (date(self.this_year, 5, 2), "Custom May 2nd Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-05-02"), + (date(self.next_year, 5, 1), "Custom May 1st Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.next_year}-01-01"), + (date(self.next_year, 5, 1), "Custom May 1st Holiday"), + ) + + self.assertIn( + self.hb.get_next_holiday(), + [ + (date(self.next_labor_day_year, 5, 1), "Custom May 1st Holiday"), + (date(self.next_labor_day_year, 5, 2), "Custom May 2nd Holiday"), + ], + ) + + def test_get_next_holiday_reverse(self): + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-12-31", previous=True), + (date(self.this_year, 5, 2), "Custom May 2nd Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-05-02", previous=True), + (date(self.this_year, 5, 1), "Custom May 1st Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.this_year}-04-30", previous=True), + (date(self.previous_year, 5, 2), "Custom May 2nd Holiday"), + ) + self.assertEqual( + self.hb.get_next_holiday(f"{self.previous_year}-12-31", previous=True), + (date(self.previous_year, 5, 2), "Custom May 2nd Holiday"), + ) + + self.assertIn( + self.hb.get_next_holiday(previous=True), + [ + (date(self.previous_labor_day_year, 5, 2), "Custom May 2nd Holiday"), + (date(self.this_year, 5, 1), "Custom May 1st Holiday"), + ], + ) + + def test_get_next_holiday_corner_cases(self): + from holidays.countries.ukraine import UA + + ua = UA() + # check for date before start of calendar + self.assertEqual(ua.get_next_holiday("1991-01-01", previous=True), (None, None)) + + # check for date after end of calendar + self.assertEqual(ua.get_next_holiday("2022-03-08"), (None, None))