Skip to content

Commit cd1af81

Browse files
BUG: Return type discrepancy in USFederalHolidayCalendar (#49118)
* GH49075 looks weird. Let's add a test and go from there. * Some cleanup. * First pass at the fix. * Missed a space, failed formatting check. * pre-commit would have caught that, eh. * Update test_federal.py * Well, that was subtle. * Changed the test. * Git mistake. * Backing away from keyboard now. * next: write more tests. * precommit local run. * User specified half-open date intervals can return inconsistent results. * Added logic to close open time intervals into AbstractHolidayCalender * OK, I've learned pre-commit doesn't pass wildcards all the way down a directory tree. * Ensure DatetimeIndex returned in _apply_rule * Ensure DatetimeIndex returned in _apply_rule * Caught formatting. * Missed running isort locally. * Well, that was subtle. * Changed URL to current OPM source-of-record, updated whatsnew * Update v2.0.0.rst * Add dtype to empty index ( caught by mypy in CI, not part of pre-commit ) * Updated holiday.py * ... and forgot to pre-commit run black * Update pandas/tseries/holiday.py Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> * Update v2.0.0.rst * Added test to test_federal.py to ensure comparison against known-good result in addition to type comparison * Add change to test_federal to compare against constructed DatetimeIndex, cleaned up test_half_open_interval_with_observance() in test_holiday. * Update doc/source/whatsnew/v2.0.0.rst Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com>
1 parent 490c5d0 commit cd1af81

File tree

4 files changed

+72
-3
lines changed

4 files changed

+72
-3
lines changed

doc/source/whatsnew/v2.0.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ Datetimelike
354354
- Bug in :func:`to_datetime` was raising on invalid offsets with ``errors='coerce'`` and ``infer_datetime_format=True`` (:issue:`48633`)
355355
- Bug in :class:`DatetimeIndex` constructor failing to raise when ``tz=None`` is explicitly specified in conjunction with timezone-aware ``dtype`` or data (:issue:`48659`)
356356
- Bug in subtracting a ``datetime`` scalar from :class:`DatetimeIndex` failing to retain the original ``freq`` attribute (:issue:`48818`)
357-
-
357+
- Bug in ``pandas.tseries.holiday.Holiday`` where a half-open date interval causes inconsistent return types from :meth:`USFederalHolidayCalendar.holidays` (:issue:`49075`)
358358

359359
Timedelta
360360
^^^^^^^^^

pandas/tests/tseries/holiday/test_federal.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from datetime import datetime
22

3+
from pandas import DatetimeIndex
4+
import pandas._testing as tm
5+
36
from pandas.tseries.holiday import (
47
AbstractHolidayCalendar,
8+
USFederalHolidayCalendar,
59
USMartinLutherKingJr,
610
USMemorialDay,
711
)
@@ -36,3 +40,19 @@ class MemorialDay(AbstractHolidayCalendar):
3640
datetime(1978, 5, 29, 0, 0),
3741
datetime(1979, 5, 28, 0, 0),
3842
]
43+
44+
45+
def test_federal_holiday_inconsistent_returntype():
46+
# GH 49075 test case
47+
# Instantiate two calendars to rule out _cache
48+
cal1 = USFederalHolidayCalendar()
49+
cal2 = USFederalHolidayCalendar()
50+
51+
results_2018 = cal1.holidays(start=datetime(2018, 8, 1), end=datetime(2018, 8, 31))
52+
results_2019 = cal2.holidays(start=datetime(2019, 8, 1), end=datetime(2019, 8, 31))
53+
expected_results = DatetimeIndex([], dtype="datetime64[ns]", freq=None)
54+
55+
# Check against expected results to ensure both date
56+
# ranges generate expected results as per GH49075 submission
57+
tm.assert_index_equal(results_2018, expected_results)
58+
tm.assert_index_equal(results_2019, expected_results)

pandas/tests/tseries/holiday/test_holiday.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from pytz import utc
55

6+
from pandas import DatetimeIndex
67
import pandas._testing as tm
78

89
from pandas.tseries.holiday import (
@@ -264,3 +265,49 @@ def test_both_offset_observance_raises():
264265
offset=[DateOffset(weekday=SA(4))],
265266
observance=next_monday,
266267
)
268+
269+
270+
def test_half_open_interval_with_observance():
271+
# Prompted by GH 49075
272+
# Check for holidays that have a half-open date interval where
273+
# they have either a start_date or end_date defined along
274+
# with a defined observance pattern to make sure that the return type
275+
# for Holiday.dates() remains consistent before & after the year that
276+
# marks the 'edge' of the half-open date interval.
277+
278+
holiday_1 = Holiday(
279+
"Arbitrary Holiday - start 2022-03-14",
280+
start_date=datetime(2022, 3, 14),
281+
month=3,
282+
day=14,
283+
observance=next_monday,
284+
)
285+
holiday_2 = Holiday(
286+
"Arbitrary Holiday 2 - end 2022-03-20",
287+
end_date=datetime(2022, 3, 20),
288+
month=3,
289+
day=20,
290+
observance=next_monday,
291+
)
292+
293+
class TestHolidayCalendar(AbstractHolidayCalendar):
294+
rules = [
295+
USMartinLutherKingJr,
296+
holiday_1,
297+
holiday_2,
298+
USLaborDay,
299+
]
300+
301+
start = Timestamp("2022-08-01")
302+
end = Timestamp("2022-08-31")
303+
year_offset = DateOffset(years=5)
304+
expected_results = DatetimeIndex([], dtype="datetime64[ns]", freq=None)
305+
test_cal = TestHolidayCalendar()
306+
307+
date_interval_low = test_cal.holidays(start - year_offset, end - year_offset)
308+
date_window_edge = test_cal.holidays(start, end)
309+
date_interval_high = test_cal.holidays(start + year_offset, end + year_offset)
310+
311+
tm.assert_index_equal(date_interval_low, expected_results)
312+
tm.assert_index_equal(date_window_edge, expected_results)
313+
tm.assert_index_equal(date_interval_high, expected_results)

pandas/tseries/holiday.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ def _apply_rule(self, dates):
335335
-------
336336
Dates with rules applied
337337
"""
338+
if dates.empty:
339+
return DatetimeIndex([])
340+
338341
if self.observance is not None:
339342
return dates.map(lambda d: self.observance(d))
340343

@@ -553,8 +556,7 @@ def merge(self, other, inplace: bool = False):
553556
class USFederalHolidayCalendar(AbstractHolidayCalendar):
554557
"""
555558
US Federal Government Holiday Calendar based on rules specified by:
556-
https://www.opm.gov/policy-data-oversight/
557-
snow-dismissal-procedures/federal-holidays/
559+
https://www.opm.gov/policy-data-oversight/pay-leave/federal-holidays/
558560
"""
559561

560562
rules = [

0 commit comments

Comments
 (0)