Skip to content
Open
54 changes: 54 additions & 0 deletions holidays/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
"list_localized_financial",
"list_supported_countries",
"list_supported_financial",
"list_long_weekends",
)

import warnings
from collections.abc import Iterable
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

Expand Down Expand Up @@ -435,3 +437,55 @@ 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(
instance: HolidayBase,
minimum_holiday_length: int = 3,
require_weekend_overlap: Optional[bool] = True,
) -> list:
Comment on lines +442 to +446
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def list_long_weekends(
instance: HolidayBase,
minimum_holiday_length: int = 3,
require_weekend_overlap: Optional[bool] = True,
) -> list:
def list_long_weekends(
instance: HolidayBase, *, minimum_holiday_length: int = 3, require_weekend_overlap: bool = True
) -> list[list[date]]:

To make boolean parameters keyword-only in new code.

"""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.
Comment on lines +457 to +459
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Param description seems reversed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the param name is changed I changed its default value to True and made changes to the code wherever required.

require_weekend_overlap makes more sense with its default value being True.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that the description "Whether to include consecutive holidays that do not contain any weekend days" can be understood as "If True, include consecutive holidays that do not contain..."

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require_weekend_overlap:
Whether to include consecutive holidays that do not contain any weekend days.
Defaults to True.
require_weekend_overlap:
Whether to include only consecutive holidays that overlap with a weekend.
Defaults to True.


Returns:
A list of long consecutive holiday periods longer than or equal
to the specified minimum length.
"""
checked = set()
long_weekends = []

for holiday in sorted(instance.keys()):
if holiday in checked:
continue

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

if length < minimum_holiday_length:
continue

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
72 changes: 72 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import pytest

import holidays
from holidays.holiday_base import HolidayBase
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from holidays.holiday_base import HolidayBase
from holidays.calendars.gregorian import FRI, SAT, SUN
from holidays.holiday_base import HolidayBase

from holidays.utils import (
CountryHoliday,
country_holidays,
Expand All @@ -28,6 +29,7 @@
list_localized_financial,
list_supported_countries,
list_supported_financial,
list_long_weekends,
)
from tests.common import PYTHON_LATEST_SUPPORTED_VERSION, PYTHON_VERSION

Expand Down Expand Up @@ -219,3 +221,73 @@ 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))

Comment on lines +224 to +228
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Redundant parity check; remove duplicate assertion

You already assert parity at Lines 219-222. Lines 223-227 repeat the same check. Trim the duplicate to keep tests lean.

-        financial_files = [
-            path for path in Path("holidays/financial").glob("*.py") if path.stem != "__init__"
-        ]
-        self.assertEqual(len(financial_files), len(supported_financial))
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
financial_files = [
path for path in Path("holidays/financial").glob("*.py") if path.stem != "__init__"
]
self.assertEqual(len(financial_files), len(supported_financial))
🧰 Tools
🪛 Ruff (0.13.3)

226-226: Use a regular assert instead of unittest-style assertEqual

Replace assertEqual(...) with assert ...

(PT009)

🤖 Prompt for AI Agents
In tests/test_utils.py around lines 223 to 227, there's a duplicate parity
assertion that repeats the check already made at lines 219-222; remove the
redundant block (lines 223-227) so only the original parity assertion remains,
keeping the test lean and avoiding duplicate checks.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the bot says, this section is currently duplicate of the refactored tests above and should be removed

Suggested change
financial_files = [
path for path in Path("holidays/financial").glob("*.py") if path.stem != "__init__"
]
self.assertEqual(len(financial_files), len(supported_financial))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PPsyrius, understood! The duplicate assertion in lines 223-227 should indeed be removed since it repeats the same parity check already performed at lines 219-222. Both count the financial files and verify they match supported_financial, making the second one redundant.


class MockHolidayBase(HolidayBase):
def __init__(self, holidays, weekend={5, 6}):
super().__init__()
self._holidays = set(holidays)
self.weekend = weekend
Comment on lines +231 to +234
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __init__(self, holidays, weekend={5, 6}):
super().__init__()
self._holidays = set(holidays)
self.weekend = weekend
def __init__(self, holidays: set[date], weekend: set[int] | None = None) -> None:
super().__init__()
self._holidays = set(holidays)
self.weekend = weekend or {SAT, SUN}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would apply the same approach here as in test_holiday_base - country stubs with the necessary sets of holidays and weekends.


def keys(self):
return self._holidays

def __contains__(self, d):
return d in self._holidays
Comment on lines +239 to +240
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __contains__(self, d):
return d in self._holidays
def __contains__(self, day):
return day in self._holidays



class TestListLongWeekends(unittest.TestCase):
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, 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, 5), date(2025, 7, 6)],
[date(2025, 12, 26), date(2025, 12, 27), 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, require_weekend_overlap=False)
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.assertEqual(result, [[date(2025, 4, 10), date(2025, 4, 11), date(2025, 4, 12)]])

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, [])
Comment on lines +243 to +293
Copy link
Collaborator

@PPsyrius PPsyrius Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this to use a common helper method, merge some similar test cases and add some missing ones (the ones which has long weekends straddling across 2 years, as well as real country and financial holidays examples)

Suggested change
class TestListLongWeekends(unittest.TestCase):
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, 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, 5), date(2025, 7, 6)],
[date(2025, 12, 26), date(2025, 12, 27), 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, require_weekend_overlap=False)
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.assertEqual(result, [[date(2025, 4, 10), date(2025, 4, 11), date(2025, 4, 12)]])
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, [])
class TestListLongWeekends(unittest.TestCase):
def assertLongWeekendsEqual( # noqa: N802
self,
holidays,
expected,
weekend=None,
minimum_holiday_length=3,
*,
require_weekend_overlap=True,
):
instance = MockHolidayBase(holidays, weekend=weekend or {SAT, SUN})
result = list_long_weekends(
instance,
minimum_holiday_length=minimum_holiday_length,
require_weekend_overlap=require_weekend_overlap,
)
self.assertEqual(result, expected)
def test_long_weekend_single(self):
self.assertLongWeekendsEqual(
[date(2025, 7, 4)],
[[date(2025, 7, 4), date(2025, 7, 5), date(2025, 7, 6)]],
)
def test_long_weekend_multiple(self):
self.assertLongWeekendsEqual(
[date(2025, 7, 4), date(2025, 12, 26)],
[
[date(2025, 7, 4), date(2025, 7, 5), date(2025, 7, 6)],
[date(2025, 12, 26), date(2025, 12, 27), date(2025, 12, 28)],
],
)
def test_long_weekend_require_weekend_overlap(self):
self.assertLongWeekendsEqual(
[date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)],
[],
)
self.assertLongWeekendsEqual(
[date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)],
[[date(2025, 3, 4), date(2025, 3, 5), date(2025, 3, 6)]],
require_weekend_overlap=False,
)
def test_long_weekend_custom_weekend(self):
self.assertLongWeekendsEqual(
[date(2025, 4, 10)],
[[date(2025, 4, 10), date(2025, 4, 11), date(2025, 4, 12)]],
weekend={FRI, SAT},
)
def test_long_weekend_no_holidays(self):
self.assertLongWeekendsEqual(
[],
[],
)
def test_long_weekend_custom_minimum_length(self):
self.assertLongWeekendsEqual(
[date(2025, 8, 11), date(2025, 8, 12), date(2025, 12, 5)],
[[date(2025, 8, 9), date(2025, 8, 10), date(2025, 8, 11), date(2025, 8, 12)]],
minimum_holiday_length=4,
)
def test_long_weekend_across_years(self):
self.assertLongWeekendsEqual(
[date(2024, 1, 1)],
[[date(2023, 12, 30), date(2023, 12, 31), date(2024, 1, 1)]],
)
self.assertLongWeekendsEqual(
[date(2021, 12, 31)],
[[date(2021, 12, 31), date(2022, 1, 1), date(2022, 1, 2)]],
)
def test_long_weekend_real_country_holidays_data(self):
self.assertEqual(
list_long_weekends(country_holidays("AU", subdiv="NSW", years=2024)),
[
[date(2023, 12, 30), date(2023, 12, 31), date(2024, 1, 1)],
[date(2024, 1, 26), date(2024, 1, 27), date(2024, 1, 28)],
[date(2024, 3, 29), date(2024, 3, 30), date(2024, 3, 31), date(2024, 4, 1)],
[date(2024, 6, 8), date(2024, 6, 9), date(2024, 6, 10)],
[date(2024, 10, 5), date(2024, 10, 6), date(2024, 10, 7)],
],
)
def test_long_weekend_real_financial_holidays_data(self):
self.assertEqual(
list_long_weekends(financial_holidays("XNSE", years=2024)),
[
[date(2024, 1, 26), date(2024, 1, 27), date(2024, 1, 28)],
[date(2024, 3, 8), date(2024, 3, 9), date(2024, 3, 10)],
[date(2024, 3, 23), date(2024, 3, 24), date(2024, 3, 25)],
[date(2024, 3, 29), date(2024, 3, 30), date(2024, 3, 31)],
[date(2024, 6, 15), date(2024, 6, 16), date(2024, 6, 17)],
[date(2024, 11, 1), date(2024, 11, 2), date(2024, 11, 3)],
[date(2024, 11, 15), date(2024, 11, 16), date(2024, 11, 17)],
],
)