Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

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 @@ -280,6 +280,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
41 changes: 34 additions & 7 deletions pandas/tseries/holiday.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
datetime,
timedelta,
)
from typing import (
TYPE_CHECKING,
Callable,
SupportsIndex,
)
import warnings

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

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


def next_monday(dt: datetime) -> datetime:
"""
Expand Down Expand Up @@ -156,25 +164,33 @@ class Holiday:
def __init__(
self,
name: str,
year=None,
month=None,
day=None,
offset=None,
observance=None,
year: SupportsIndex | None = None,
month: SupportsIndex | None = None,
day: SupportsIndex | None = None,
offset: None | BaseOffset | list[BaseOffset | list[BaseOffset]] = None,
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
Year of the holiday
month: int
Month of the holiday
day: int
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
start_date : datetime-like, optional
end_date : datetime-like, optional
days_of_week:
provide a tuple of days e.g (0,1,2,3,) for Monday Through Thursday
Monday=0,..,Sunday=6
Expand Down Expand Up @@ -223,7 +239,18 @@ class from pandas.tseries.offsets
self.year = year
self.month = month
self.day = day
self.offset = offset
if isinstance(offset, list):
self.offset = []
for off in offset:
# check if we are handling offsets of another holiday
if isinstance(off, list):
self.offset.extend(np.ravel(off))
else:
# otherwise it should be a DateOffset, we do not support other
# array-like types
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