From e76334506a87def024582f85bb9b6bbf8cdd20fe Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Fri, 10 Oct 2025 16:17:03 +0000 Subject: [PATCH 1/7] Long weekend function added --- holidays/utils.py | 55 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 31 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/holidays/utils.py b/holidays/utils.py index 85b90af1a..6cd0c76d5 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -18,10 +18,12 @@ "list_localized_financial", "list_supported_countries", "list_supported_financial", + "list_long_weekends", ) import warnings from collections.abc import Iterable +from datetime import timedelta from functools import cache from typing import Optional, Union @@ -435,3 +437,56 @@ def list_supported_financial(include_aliases: bool = True) -> dict[str, list[str subdivision codes. """ return _list_supported_entities(EntityLoader.get_financial_codes(include_aliases)) + + +def list_long_weekends( + country: str, + year: int, + subdiv: Optional[str] = None, +) -> list: + try: + holidays_obj = country_holidays(country, subdiv=subdiv, years=year) + except NotImplementedError: + return [] + except Exception: + return [] + + checked = set() + long_weekends = [] + + holidays_in_year = set(holidays_obj.keys()) + + for hol in sorted(holidays_in_year): + if hol in checked: + continue + + prev_work = holidays_obj.get_nth_working_day(hol, -1) + next_work = holidays_obj.get_nth_working_day(hol, +1) + + start = prev_work + timedelta(days=1) + end = next_work - timedelta(days=1) + + if start > end: + continue + + length = (end - start).days + 1 + + has_weekend = any( + (d.weekday() in getattr(holidays_obj, "weekend", {5, 6})) + for d in (start + timedelta(days=i) for i in range(length)) + ) + + if length >= 3 and has_weekend: + long_weekends.append( + { + "start": start, + "end": end, + "length": length, + } + ) + + for d in holidays_in_year: + if start <= d <= end: + checked.add(d) + + return long_weekends diff --git a/tests/test_utils.py b/tests/test_utils.py index fde41d4ec..a09de3a3c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -28,6 +28,7 @@ list_localized_financial, list_supported_countries, list_supported_financial, + list_long_weekends, ) from tests.common import PYTHON_LATEST_SUPPORTED_VERSION, PYTHON_VERSION @@ -219,3 +220,33 @@ def test_list_supported_financial(self): 1 for path in Path("holidays/financial").glob("*.py") if path.stem != "__init__" ) self.assertEqual(financial_count, len(supported_financial)) + financial_files = [ + path for path in Path("holidays/financial").glob("*.py") if path.stem != "__init__" + ] + self.assertEqual(len(financial_files), len(supported_financial)) + + +class TestListLongWeekends(unittest.TestCase): + def test_valid_country_usa(self): + results = list_long_weekends("US", 2024) + self.assertIsInstance(results, list) + self.assertTrue(any(r["length"] >= 3 for r in results)) + + def test_invalid_country_code(self): + results = list_long_weekends("JA", 2024) + self.assertEqual(results, [], "Expected empty list for invalid country code") + + def test_subdivision(self): + results = list_long_weekends("US", 2024, subdiv="CA") + self.assertIsInstance(results, list) + + def test_long_weekend_structure(self): + results = list_long_weekends("IN", 2024) + for w in results: + self.assertIsInstance(w["start"], date) + self.assertIsInstance(w["end"], date) + self.assertIsInstance(w["length"], int) + + def test_empty_year(self): + results = list_long_weekends("US", 2500) + self.assertIsInstance(results, list) From f5f22f86c2884c74749d144cc72b40202a81d247 Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Fri, 10 Oct 2025 17:04:54 +0000 Subject: [PATCH 2/7] Long weekend function added --- holidays/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/holidays/utils.py b/holidays/utils.py index 6cd0c76d5..dad043a9b 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -448,8 +448,6 @@ def list_long_weekends( holidays_obj = country_holidays(country, subdiv=subdiv, years=year) except NotImplementedError: return [] - except Exception: - return [] checked = set() long_weekends = [] @@ -466,9 +464,6 @@ def list_long_weekends( start = prev_work + timedelta(days=1) end = next_work - timedelta(days=1) - if start > end: - continue - length = (end - start).days + 1 has_weekend = any( From 99fb88acaccd1df9edc0c8fd23db69b5398ad494 Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Sun, 19 Oct 2025 08:20:18 +0000 Subject: [PATCH 3/7] added changes to long weekend function --- holidays/utils.py | 40 +++++++++--------------- tests/test_utils.py | 76 +++++++++++++++++++++++++++++++-------------- 2 files changed, 68 insertions(+), 48 deletions(-) diff --git a/holidays/utils.py b/holidays/utils.py index dad043a9b..21fe0a827 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -440,48 +440,38 @@ def list_supported_financial(include_aliases: bool = True) -> dict[str, list[str def list_long_weekends( - country: str, - year: int, - subdiv: Optional[str] = None, + instance: HolidayBase, + include_non_long_weekends: Optional[bool] = False, ) -> list: - try: - holidays_obj = country_holidays(country, subdiv=subdiv, years=year) - except NotImplementedError: - return [] - checked = set() long_weekends = [] - holidays_in_year = set(holidays_obj.keys()) + holidays_in_year = set(instance.keys()) for hol in sorted(holidays_in_year): if hol in checked: continue - prev_work = holidays_obj.get_nth_working_day(hol, -1) - next_work = holidays_obj.get_nth_working_day(hol, +1) + prev_work = instance.get_nth_working_day(hol, -1) + next_work = instance.get_nth_working_day(hol, +1) start = prev_work + timedelta(days=1) end = next_work - timedelta(days=1) length = (end - start).days + 1 - has_weekend = any( - (d.weekday() in getattr(holidays_obj, "weekend", {5, 6})) - for d in (start + timedelta(days=i) for i in range(length)) - ) + has_weekend = True + if not include_non_long_weekends: + has_weekend = any( + (d.weekday() in getattr(instance, "weekend", {5, 6})) + for d in (start + timedelta(days=i) for i in range(length)) + ) if length >= 3 and has_weekend: - long_weekends.append( - { - "start": start, - "end": end, - "length": length, - } - ) + long_weekends.append([start, end]) - for d in holidays_in_year: - if start <= d <= end: - checked.add(d) + for d in holidays_in_year: + if start <= d <= end: + checked.add(d) return long_weekends diff --git a/tests/test_utils.py b/tests/test_utils.py index a09de3a3c..6cf138ca4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -20,6 +20,7 @@ import pytest import holidays +from holidays.holiday_base import HolidayBase from holidays.utils import ( CountryHoliday, country_holidays, @@ -226,27 +227,56 @@ def test_list_supported_financial(self): self.assertEqual(len(financial_files), len(supported_financial)) +class MockHolidayBase(HolidayBase): + def __init__(self, holidays, weekend={5, 6}): + super().__init__() + self._holidays = set(holidays) + self.weekend = weekend + + def keys(self): + return self._holidays + + def __contains__(self, d): + return d in self._holidays + + class TestListLongWeekends(unittest.TestCase): - def test_valid_country_usa(self): - results = list_long_weekends("US", 2024) - self.assertIsInstance(results, list) - self.assertTrue(any(r["length"] >= 3 for r in results)) - - def test_invalid_country_code(self): - results = list_long_weekends("JA", 2024) - self.assertEqual(results, [], "Expected empty list for invalid country code") - - def test_subdivision(self): - results = list_long_weekends("US", 2024, subdiv="CA") - self.assertIsInstance(results, list) - - def test_long_weekend_structure(self): - results = list_long_weekends("IN", 2024) - for w in results: - self.assertIsInstance(w["start"], date) - self.assertIsInstance(w["end"], date) - self.assertIsInstance(w["length"], int) - - def test_empty_year(self): - results = list_long_weekends("US", 2500) - self.assertIsInstance(results, list) + def test_simple_long_weekend(self): + holidays = [date(2025, 7, 4)] + instance = MockHolidayBase(holidays) + result = list_long_weekends(instance) + self.assertEqual(result, [[date(2025, 7, 4), date(2025, 7, 6)]]) + + def test_multiple_holidays(self): + holidays = [date(2025, 7, 4), date(2025, 12, 26)] + instance = MockHolidayBase(holidays) + result = list_long_weekends(instance) + expected = [ + [date(2025, 7, 4), date(2025, 7, 6)], + [date(2025, 12, 26), date(2025, 12, 28)], + ] + self.assertEqual(result, expected) + + def test_three_holidays_no_weekend(self): + holidays = [date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)] + instance = MockHolidayBase(holidays) + result = list_long_weekends(instance) + self.assertEqual(result, []) + + def test_three_holidays_included_with_flag(self): + holidays = [date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)] + instance = MockHolidayBase(holidays) + result = list_long_weekends(instance, include_non_long_weekends=True) + self.assertEqual(result, [[date(2025, 3, 4), date(2025, 3, 6)]]) + + def test_custom_weekend(self): + holidays = [date(2025, 4, 10)] + instance = MockHolidayBase(holidays, weekend={4, 5}) + result = list_long_weekends(instance) + self.assertTrue(any(d[0] <= date(2025, 4, 10) <= d[1] for d in result)) + self.assertTrue(any(d[0] <= date(2025, 4, 12) <= d[1] for d in result)) + + def test_no_holidays(self): + instance = MockHolidayBase([]) + result = list_long_weekends(instance) + self.assertEqual(result, []) From 1ae8ed48fa1bcd92c629a034ca10da430131a2e1 Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Mon, 20 Oct 2025 09:25:00 +0000 Subject: [PATCH 4/7] changes added to long weekend function --- holidays/utils.py | 9 ++++++--- tests/test_utils.py | 11 +++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/holidays/utils.py b/holidays/utils.py index 21fe0a827..32c83024a 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -452,6 +452,8 @@ def list_long_weekends( if hol in checked: continue + weekend_contents = [] + prev_work = instance.get_nth_working_day(hol, -1) next_work = instance.get_nth_working_day(hol, +1) @@ -463,12 +465,13 @@ def list_long_weekends( has_weekend = True if not include_non_long_weekends: has_weekend = any( - (d.weekday() in getattr(instance, "weekend", {5, 6})) - for d in (start + timedelta(days=i) for i in range(length)) + instance.is_weekend(d) for d in (start + timedelta(days=i) for i in range(length)) ) if length >= 3 and has_weekend: - long_weekends.append([start, end]) + for d in (start + timedelta(days=i) for i in range(length)): + weekend_contents.append(d) + long_weekends.append(weekend_contents) for d in holidays_in_year: if start <= d <= end: diff --git a/tests/test_utils.py b/tests/test_utils.py index 6cf138ca4..f37ad92ff 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -245,15 +245,15 @@ def test_simple_long_weekend(self): holidays = [date(2025, 7, 4)] instance = MockHolidayBase(holidays) result = list_long_weekends(instance) - self.assertEqual(result, [[date(2025, 7, 4), date(2025, 7, 6)]]) + self.assertEqual(result, [[date(2025, 7, 4), date(2025, 7, 5), date(2025, 7, 6)]]) def test_multiple_holidays(self): holidays = [date(2025, 7, 4), date(2025, 12, 26)] instance = MockHolidayBase(holidays) result = list_long_weekends(instance) expected = [ - [date(2025, 7, 4), date(2025, 7, 6)], - [date(2025, 12, 26), date(2025, 12, 28)], + [date(2025, 7, 4), date(2025, 7, 5), date(2025, 7, 6)], + [date(2025, 12, 26), date(2025, 12, 27), date(2025, 12, 28)], ] self.assertEqual(result, expected) @@ -267,14 +267,13 @@ def test_three_holidays_included_with_flag(self): holidays = [date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)] instance = MockHolidayBase(holidays) result = list_long_weekends(instance, include_non_long_weekends=True) - self.assertEqual(result, [[date(2025, 3, 4), date(2025, 3, 6)]]) + self.assertEqual(result, [[date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)]]) def test_custom_weekend(self): holidays = [date(2025, 4, 10)] instance = MockHolidayBase(holidays, weekend={4, 5}) result = list_long_weekends(instance) - self.assertTrue(any(d[0] <= date(2025, 4, 10) <= d[1] for d in result)) - self.assertTrue(any(d[0] <= date(2025, 4, 12) <= d[1] for d in result)) + self.assertEqual(result, [[date(2025, 4, 10), date(2025, 4, 11), date(2025, 4, 12)]]) def test_no_holidays(self): instance = MockHolidayBase([]) From 0eacdcfe0fe3134dc2b802321f123e2fdd051ba2 Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Thu, 23 Oct 2025 16:45:46 +0000 Subject: [PATCH 5/7] docstring added & argument names changed --- holidays/utils.py | 46 ++++++++++++++++++++++++++++++++------------- tests/test_utils.py | 2 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/holidays/utils.py b/holidays/utils.py index 32c83024a..53a8a86f6 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -441,21 +441,40 @@ def list_supported_financial(include_aliases: bool = True) -> dict[str, list[str def list_long_weekends( instance: HolidayBase, - include_non_long_weekends: Optional[bool] = False, + minimum_holiday_length: int = 3, + require_weekend_overlap: Optional[bool] = True, ) -> list: + """Get all long consecutive holidays. + + Args: + instance: + `HolidaysBase` object containing holiday data. + + minimum_holiday_length: + The minimum number of consecutive days required for a holiday period + to be considered a long weekend. Defaults to 3. + + require_weekend_overlap: + Whether to include consecutive holidays that do not contain any weekend days. + Defaults to True. + + Returns: + A list of long consecutive holiday periods longer than or equal + to the specified minimum length. + """ checked = set() long_weekends = [] holidays_in_year = set(instance.keys()) - for hol in sorted(holidays_in_year): - if hol in checked: + for holiday in sorted(holidays_in_year): + if holiday in checked: continue weekend_contents = [] - prev_work = instance.get_nth_working_day(hol, -1) - next_work = instance.get_nth_working_day(hol, +1) + prev_work = instance.get_nth_working_day(holiday, -1) + next_work = instance.get_nth_working_day(holiday, +1) start = prev_work + timedelta(days=1) end = next_work - timedelta(days=1) @@ -463,18 +482,19 @@ def list_long_weekends( length = (end - start).days + 1 has_weekend = True - if not include_non_long_weekends: + if require_weekend_overlap: has_weekend = any( - instance.is_weekend(d) for d in (start + timedelta(days=i) for i in range(length)) + instance.is_weekend(day) + for day in (start + timedelta(days=offset) for offset in range(length)) ) - if length >= 3 and has_weekend: - for d in (start + timedelta(days=i) for i in range(length)): - weekend_contents.append(d) + if length >= minimum_holiday_length and has_weekend: + for day in (start + timedelta(days=day_offset) for day_offset in range(length)): + weekend_contents.append(day) long_weekends.append(weekend_contents) - for d in holidays_in_year: - if start <= d <= end: - checked.add(d) + for day in holidays_in_year: + if start <= day <= end: + checked.add(day) return long_weekends diff --git a/tests/test_utils.py b/tests/test_utils.py index f37ad92ff..9fec95a32 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -266,7 +266,7 @@ def test_three_holidays_no_weekend(self): def test_three_holidays_included_with_flag(self): holidays = [date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)] instance = MockHolidayBase(holidays) - result = list_long_weekends(instance, include_non_long_weekends=True) + result = list_long_weekends(instance, require_weekend_overlap=False) self.assertEqual(result, [[date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)]]) def test_custom_weekend(self): From 25af78fed108c2f906e91d6aaf484a6bcb252802 Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Sun, 26 Oct 2025 07:08:35 +0000 Subject: [PATCH 6/7] optimized long weekend function --- holidays/utils.py | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/holidays/utils.py b/holidays/utils.py index 53a8a86f6..3b0a80c1d 100755 --- a/holidays/utils.py +++ b/holidays/utils.py @@ -23,10 +23,10 @@ import warnings from collections.abc import Iterable -from datetime import timedelta from functools import cache from typing import Optional, Union +from holidays.calendars.gregorian import _timedelta from holidays.holiday_base import CategoryArg, HolidayBase from holidays.registry import EntityLoader @@ -465,36 +465,27 @@ def list_long_weekends( checked = set() long_weekends = [] - holidays_in_year = set(instance.keys()) - - for holiday in sorted(holidays_in_year): + for holiday in sorted(instance.keys()): if holiday in checked: continue - weekend_contents = [] - prev_work = instance.get_nth_working_day(holiday, -1) next_work = instance.get_nth_working_day(holiday, +1) + length = (next_work - prev_work).days - 1 - start = prev_work + timedelta(days=1) - end = next_work - timedelta(days=1) - - length = (end - start).days + 1 - - has_weekend = True - if require_weekend_overlap: - has_weekend = any( - instance.is_weekend(day) - for day in (start + timedelta(days=offset) for offset in range(length)) - ) - - if length >= minimum_holiday_length and has_weekend: - for day in (start + timedelta(days=day_offset) for day_offset in range(length)): - weekend_contents.append(day) - long_weekends.append(weekend_contents) + if length < minimum_holiday_length: + continue - for day in holidays_in_year: - if start <= day <= end: - checked.add(day) + holiday_dates = [] + is_needed = not require_weekend_overlap + for day_offset in range(1, length + 1): + current_date = _timedelta(prev_work, day_offset) + if not is_needed and instance._is_weekend(current_date): + is_needed = True + holiday_dates.append(current_date) + checked.add(current_date) + + if is_needed: + long_weekends.append(holiday_dates) return long_weekends From fafc47fd79576aa8225ebd35d5acb7b7ac665c03 Mon Sep 17 00:00:00 2001 From: AryaPhansalkar Date: Sun, 26 Oct 2025 08:27:11 +0000 Subject: [PATCH 7/7] optimized long weekend function --- tests/test_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 9fec95a32..6e3c7772b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -279,3 +279,15 @@ def test_no_holidays(self): instance = MockHolidayBase([]) result = list_long_weekends(instance) self.assertEqual(result, []) + + def test_no_weekend_overlap(self): + holidays = [date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)] + instance = MockHolidayBase(holidays) + result = list_long_weekends(instance, require_weekend_overlap=False) + self.assertEqual(result, [[date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)]]) + + def test_holidays_less_than_three_days(self): + holidays = [date(2025, 5, 3)] + instance = MockHolidayBase(holidays) + result = list_long_weekends(instance) + self.assertEqual(result, [])