Skip to content

Commit 167c86a

Browse files
committed
Initial implementation of holidays and holiday calendars.
Implementation of holidays and holiday calendars to be used with the CustomBusinessDay offset. Also add an Easter holiday for use in calendars.
1 parent 110406c commit 167c86a

File tree

4 files changed

+332
-5
lines changed

4 files changed

+332
-5
lines changed

pandas/tseries/holiday.py

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
from pandas import DateOffset, date_range, DatetimeIndex, Series
2+
from datetime import datetime
3+
from pandas.tseries.offsets import Easter
4+
from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
5+
6+
def Sunday(dt):
7+
'''
8+
If the holiday falls on Sunday, make Monday a holiday (nothing
9+
happens for Saturday.
10+
'''
11+
if dt.isoweekday() == 7:
12+
return dt + DateOffset(+1)
13+
else:
14+
return dt
15+
16+
def Nearest(dt):
17+
'''
18+
If the holiday falls on a weekend, make it a 3-day weekend by making
19+
Saturday a Friday holiday and Sunday a Monday holiday.
20+
'''
21+
if dt.isoweekday() == 6:
22+
return dt + DateOffset(-1)
23+
elif dt.isoweekday() == 7:
24+
return dt + DateOffset(+1)
25+
else:
26+
return dt
27+
28+
#TODO: Need to add an observance function when a holiday
29+
# falls on a Tuesday and get a 4-day weekend
30+
# def Nearest4(dt):
31+
# '''
32+
# If the holiday falls on Tuesday,
33+
# make Monday a holiday as well, otherwise
34+
# follow the rules for Nearest (a
35+
# 3-day weekend).
36+
# '''
37+
# if dt.isoweekday() == 2:
38+
# return dt - DateOffset()
39+
# else:
40+
# return Nearest(dt)
41+
42+
class Holiday(object):
43+
'''
44+
Class that defines a holiday with start/end dates and rules
45+
for observance.
46+
'''
47+
def __init__(self, name, year=None, month=None, day=None, offset=None,
48+
observance=None, start_date=None, end_date=None):
49+
self.name = name
50+
self.year = year
51+
self.month = month
52+
self.day = day
53+
self.offset = offset
54+
self.start_date = start_date
55+
self.end_date = end_date
56+
self.observance = observance
57+
58+
def __repr__(self):
59+
#FIXME: This should handle observance rules as well
60+
return 'Holiday %s (%s, %s, %s)' % (self.name, self.month, self.day,
61+
self.offset)
62+
63+
def dates(self, start_date, end_date):
64+
65+
if self.year is not None:
66+
return datetime(self.year, self.month, self.day)
67+
68+
if self.start_date is not None:
69+
start_date = self.start_date
70+
71+
if self.end_date is not None:
72+
end_date = self.end_date
73+
74+
year_offset = DateOffset(years=1)
75+
baseDate = datetime(start_date.year, self.month, self.day)
76+
dates = date_range(baseDate, end_date, freq=year_offset)
77+
78+
return self._apply_rule(dates)
79+
80+
def dates_with_name(self, start_date, end_date):
81+
82+
dates = self.dates(start_date, end_date)
83+
return Series(self.name, index=dates)
84+
85+
def _apply_rule(self, dates):
86+
'''
87+
Apply the given offset/observance to an
88+
iterable of dates.
89+
90+
Parameters
91+
----------
92+
dates : array-like
93+
Dates to apply the given offset/observance rule
94+
95+
Returns
96+
-------
97+
Dates with rules applied
98+
'''
99+
if self.observance is not None:
100+
return map(lambda d: self.observance(d), dates)
101+
102+
if not isinstance(self.offset, list):
103+
offsets = [self.offset]
104+
else:
105+
offsets = self.offset
106+
107+
for offset in offsets:
108+
dates = map(lambda d: d + offset, dates)
109+
110+
return dates
111+
112+
class AbstractHolidayCalendar(object):
113+
'''
114+
Abstract interface to create holidays following certain rules.
115+
'''
116+
_rule_table = []
117+
118+
def __init__(self, rules=None):
119+
'''
120+
Initializes holiday object with a given set a rules. Normally
121+
classes just have the rules defined within them.
122+
123+
Parameters
124+
----------
125+
rules : array of Holiday objects
126+
A set of rules used to create the holidays.
127+
'''
128+
super(AbstractHolidayCalendar, self).__init__()
129+
if rules is not None:
130+
self._rule_table = rules
131+
132+
@property
133+
def holiday_rules(self):
134+
return self._rule_table
135+
136+
def holidays(self, start=None, end=None, return_names=False):
137+
'''
138+
Returns a curve with holidays between start_date and end_date
139+
140+
Parameters
141+
----------
142+
start : starting date, datetime-like, optional
143+
end : ending date, datetime-like, optional
144+
return_names : bool, optional
145+
If True, return a series that has dates and holiday names.
146+
False will only return a DatetimeIndex of dates.
147+
148+
Returns
149+
-------
150+
DatetimeIndex of holidays
151+
'''
152+
#FIXME: Where should the default limits exist?
153+
if start is None:
154+
start = datetime(1970, 1, 1)
155+
156+
if end is None:
157+
end = datetime(2030, 12, 31)
158+
159+
if self.holiday_rules is None:
160+
raise Exception('Holiday Calendar %s does not have any '\
161+
'rules specified' % self.calendarName)
162+
163+
if return_names:
164+
holidays = None
165+
else:
166+
holidays = []
167+
for rule in self.holiday_rules:
168+
if return_names:
169+
rule_holidays = rule.dates_with_name(start, end)
170+
if holidays is None:
171+
holidays = rule_holidays
172+
else:
173+
holidays = holidays.append(rule_holidays)
174+
else:
175+
holidays += rule.dates(start, end)
176+
177+
if return_names:
178+
return holidays.sort_index()
179+
else:
180+
return DatetimeIndex(holidays).order(False)
181+
182+
USMemorialDay = Holiday('MemorialDay', month=5, day=24,
183+
offset=DateOffset(weekday=MO(1)))
184+
USLaborDay = Holiday('Labor Day', month=9, day=1,
185+
offset=DateOffset(weekday=MO(1)))
186+
USThanksgivingDay = Holiday('Thanksgiving', month=11, day=1,
187+
offset=DateOffset(weekday=TH(4)))
188+
USMartinLutherKingJr = Holiday('Dr. Martin Luther King Jr.', month=1, day=1,
189+
offset=DateOffset(weekday=MO(3)))
190+
USPresidentsDay = Holiday('President''s Day', month=2, day=1,
191+
offset=DateOffset(weekday=MO(3)))
192+
193+
class USFederalHolidayCalendar(AbstractHolidayCalendar):
194+
195+
_rule_table = [
196+
Holiday('New Years Day', month=1, day=1, observance=Nearest),
197+
USMartinLutherKingJr,
198+
USPresidentsDay,
199+
USMemorialDay,
200+
Holiday('July 4th', month=7, day=4, observance=Nearest),
201+
USLaborDay,
202+
Holiday('Columbus Day', month=10, day=1, offset=DateOffset(weekday=MO(2))),
203+
Holiday('Veterans Day', month=11, day=11, observance=Nearest),
204+
USThanksgivingDay,
205+
Holiday('Christmas', month=12, day=25, observance=Nearest)
206+
]
207+
208+
class NERCHolidayCalendar(AbstractHolidayCalendar):
209+
210+
_rule_table = [
211+
Holiday('New Years Day', month=1, day=1, observance=Sunday),
212+
USMemorialDay,
213+
Holiday('July 4th', month=7, day=4, observance=Sunday),
214+
USLaborDay,
215+
USThanksgivingDay,
216+
Holiday('Christmas', month=12, day=25, observance=Sunday)
217+
]

pandas/tseries/offsets.py

+43-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
# import after tools, dateutil check
99
from dateutil.relativedelta import relativedelta, weekday
10+
from dateutil.easter import easter
1011
import pandas.tslib as tslib
1112
from pandas.tslib import Timestamp, OutOfBoundsDatetime
1213

@@ -17,7 +18,7 @@
1718
'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd',
1819
'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd',
1920
'LastWeekOfMonth', 'FY5253Quarter', 'FY5253',
20-
'Week', 'WeekOfMonth',
21+
'Week', 'WeekOfMonth', 'Easter',
2122
'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano']
2223

2324
# convert to/from datetime/timestamp to allow invalid Timestamp ranges to pass thru
@@ -447,6 +448,8 @@ class CustomBusinessDay(BusinessDay):
447448
holidays : list
448449
list/array of dates to exclude from the set of valid business days,
449450
passed to ``numpy.busdaycalendar``
451+
calendar : HolidayCalendar instance
452+
instance of AbstractHolidayCalendar that provide the list of holidays
450453
"""
451454

452455
_cacheable = False
@@ -458,8 +461,11 @@ def __init__(self, n=1, **kwds):
458461
self.offset = kwds.get('offset', timedelta(0))
459462
self.normalize = kwds.get('normalize', False)
460463
self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri')
461-
holidays = kwds.get('holidays', [])
462-
464+
465+
if 'calendar' in kwds:
466+
holidays = kwds['calendar'].holidays()
467+
else:
468+
holidays = kwds.get('holidays', [])
463469
holidays = [self._to_dt64(dt, dtype='datetime64[D]') for dt in
464470
holidays]
465471
self.holidays = tuple(sorted(holidays))
@@ -1677,7 +1683,40 @@ def _from_name(cls, *args):
16771683
return cls(**dict(FY5253._parse_suffix(*args[:-1]),
16781684
qtr_with_extra_week=int(args[-1])))
16791685

1680-
1686+
class Easter(DateOffset):
1687+
'''
1688+
DateOffset for the Easter holiday using
1689+
logic defined in dateutil. Right now uses
1690+
the revised method which is valid in years
1691+
1583-4099.
1692+
'''
1693+
def __init__(self, n=1, **kwds):
1694+
super(Easter, self).__init__(n, **kwds)
1695+
1696+
def apply(self, other):
1697+
1698+
currentEaster = easter(other.year)
1699+
currentEaster = datetime(currentEaster.year, currentEaster.month, currentEaster.day)
1700+
1701+
# NOTE: easter returns a datetime.date so we have to convert to type of other
1702+
if other >= currentEaster:
1703+
new = easter(other.year + self.n)
1704+
elif other < currentEaster:
1705+
new = easter(other.year + self.n - 1)
1706+
else:
1707+
new = other
1708+
1709+
# FIXME: There has to be a better way to do this, but I don't know what it is
1710+
if isinstance(other, Timestamp):
1711+
return as_timestamp(new)
1712+
elif isinstance(other, datetime):
1713+
return datetime(new.year, new.month, new.day)
1714+
else:
1715+
return new
1716+
1717+
@classmethod
1718+
def onOffset(cls, dt):
1719+
return date(dt.year, dt.month, dt.day) == easter(dt.year)
16811720
#----------------------------------------------------------------------
16821721
# Ticks
16831722

pandas/tseries/tests/test_holiday.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
from datetime import datetime
3+
import pandas.util.testing as tm
4+
from pandas.tseries.holiday import (
5+
USFederalHolidayCalendar, USMemorialDay, USThanksgivingDay)
6+
7+
class TestCalendar(tm.TestCase):
8+
9+
def test_calendar(self):
10+
11+
calendar = USFederalHolidayCalendar()
12+
holidays = calendar.holidays(datetime(2012, 1, 1), datetime(2012, 12, 31))
13+
14+
holidayList = [
15+
datetime(2012, 1, 2),
16+
datetime(2012, 1, 16),
17+
datetime(2012, 2, 20),
18+
datetime(2012, 5, 28),
19+
datetime(2012, 7, 4),
20+
datetime(2012, 9, 3),
21+
datetime(2012, 10, 8),
22+
datetime(2012, 11, 12),
23+
datetime(2012, 11, 22),
24+
datetime(2012, 12, 25)]
25+
26+
self.assertEqual(list(holidays.to_pydatetime()), holidayList)
27+
28+
class TestHoliday(tm.TestCase):
29+
30+
def setUp(self):
31+
self.start_date = datetime(2011, 1, 1)
32+
self.end_date = datetime(2020, 12, 31)
33+
34+
def test_usmemorialday(self):
35+
holidays = USMemorialDay.dates(self.start_date, self.end_date)
36+
holidayList = [
37+
datetime(2011, 5, 30),
38+
datetime(2012, 5, 28),
39+
datetime(2013, 5, 27),
40+
datetime(2014, 5, 26),
41+
datetime(2015, 5, 25),
42+
datetime(2016, 5, 30),
43+
datetime(2017, 5, 29),
44+
datetime(2018, 5, 28),
45+
datetime(2019, 5, 27),
46+
datetime(2020, 5, 25),
47+
]
48+
self.assertEqual(holidays, holidayList)
49+
50+
def test_usthanksgivingday(self):
51+
holidays = USThanksgivingDay.dates(self.start_date, self.end_date)
52+
holidayList = [
53+
datetime(2011, 11, 24),
54+
datetime(2012, 11, 22),
55+
datetime(2013, 11, 28),
56+
datetime(2014, 11, 27),
57+
datetime(2015, 11, 26),
58+
datetime(2016, 11, 24),
59+
datetime(2017, 11, 23),
60+
datetime(2018, 11, 22),
61+
datetime(2019, 11, 28),
62+
datetime(2020, 11, 26),
63+
]
64+
self.assertEqual(holidays, holidayList)

0 commit comments

Comments
 (0)