Skip to content

BUG: #29049 make holiday support offsets of offsets #57938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ Performance improvements

Bug fixes
~~~~~~~~~
- Fixed bug in :class:`Series.Holiday` that leads to a ``TypeError`` when using ``Holiday.dates`` with a ``Holiday`` that is initialized with ``offset`` of type ``list`` which itself references another Holiday's list of offsets. (:issue:`29049`)
- Fixed bug in :class:`SparseDtype` for equal comparison with na fill value. (:issue:`54770`)
- Fixed bug in :meth:`DataFrame.join` inconsistently setting result index name (:issue:`55815`)
- Fixed bug in :meth:`DataFrame.to_string` that raised ``StopIteration`` with nested DataFrames. (:issue:`16098`)
Expand Down
28 changes: 28 additions & 0 deletions pandas/tests/tseries/holiday/test_holiday.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pandas import (
DatetimeIndex,
Series,
to_datetime,
)
import pandas._testing as tm

Expand Down Expand Up @@ -198,6 +199,33 @@ def test_holidays_within_dates(holiday, start, expected):
) == [utc.localize(dt) for dt in expected]


def test_holidays_within_dates_offset_of_offset():
# see gh-29049
# Test that the offset of an offset is correctly applied to the holiday
# And that dates can be calculated
holiday1 = Holiday(
"Holiday1",
month=USThanksgivingDay.month,
day=USThanksgivingDay.day,
offset=[USThanksgivingDay.offset, DateOffset(1)],
)
holiday2 = Holiday(
"Holiday2",
month=holiday1.month,
day=holiday1.day,
offset=[holiday1.offset, DateOffset(3)],
)
# there shall be no lists of lists here
for offset in holiday2.offset:
assert isinstance(offset, DateOffset)

min_date, max_date = (to_datetime(x) for x in ["2017-11-1", "2018-11-30"])
expected_min, expected_max = DatetimeIndex(["2017-11-27", "2018-11-26"])
actual_min, actual_max = holiday2.dates(min_date, max_date)
assert actual_min == expected_min
assert actual_max == expected_max


@pytest.mark.parametrize(
"transform", [lambda x: x.strftime("%Y-%m-%d"), lambda x: Timestamp(x)]
)
Expand Down
47 changes: 37 additions & 10 deletions pandas/tseries/holiday.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
datetime,
timedelta,
)
from typing import (
TYPE_CHECKING,
Callable,
)
import warnings

from dateutil.relativedelta import (
Expand Down Expand Up @@ -33,6 +37,9 @@
Easter,
)

if TYPE_CHECKING:
from pandas._libs.tslibs.offsets import BaseOffset


def next_monday(dt: datetime) -> datetime:
"""
Expand Down Expand Up @@ -159,24 +166,34 @@ def __init__(
year=None,
month=None,
day=None,
offset=None,
observance=None,
offset: BaseOffset | list[BaseOffset | list[BaseOffset]] | None = None,
Copy link
Member

Choose a reason for hiding this comment

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

I don't think list[list[BaseOffset]] should be supported. It's not worth the complexity when the provided list[BaseOffset] can be spat into a new argument

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed the logic for handling this case and instead only throw a more informative error now. The type hint alone might already be sufficient for when someone chains Holiday.offset without considering that they can be lists, but I think the more detailed exception could also be nice to have.

observance: Callable | None = None,
start_date=None,
end_date=None,
days_of_week=None,
days_of_week: tuple | None = None,
) -> None:
"""
Parameters
----------
name : str
Name of the holiday , defaults to class name
year : int, default None
Year of the holiday
month : int, default None
Month of the holiday
day : int, default None
Day of the holiday
offset : array of pandas.tseries.offsets or
class from pandas.tseries.offsets
computes offset from date
observance: function
computes when holiday is given a pandas Timestamp
days_of_week:
provide a tuple of days e.g (0,1,2,3,) for Monday Through Thursday
class from pandas.tseries.offsets, default None
Computes offset from date
observance : function, default None
Computes when holiday is given a pandas Timestamp
start_date : datetime-like, default None
First date the holiday is observed
end_date : datetime-like, default None
Last date the holiday is observed
days_of_week : tuple of int or dateutil.relativedelta weekday strs, default None
Provide a tuple of days e.g (0,1,2,3,) for Monday Through Thursday
Monday=0,..,Sunday=6

Examples
Expand Down Expand Up @@ -223,7 +240,17 @@ class from pandas.tseries.offsets
self.year = year
self.month = month
self.day = day
self.offset = offset
self.offset: None | BaseOffset | list[BaseOffset]
if isinstance(offset, list):
self.offset = []
for off in offset:
# check if we are handling composite offsets of another holiday
if isinstance(off, list):
self.offset.extend(off)
else:
self.offset.append(off)
else:
self.offset = offset
self.start_date = (
Timestamp(start_date) if start_date is not None else start_date
)
Expand Down