diff --git a/doc/source/v0.15.0.txt b/doc/source/v0.15.0.txt index 148cf85d0b5ab..8ab66ebd2de18 100644 --- a/doc/source/v0.15.0.txt +++ b/doc/source/v0.15.0.txt @@ -299,6 +299,8 @@ Enhancements - ``PeriodIndex`` supports ``resolution`` as the same as ``DatetimeIndex`` (:issue:`7708`) +-``pandas.tseries.holiday`` has added support for additional holidays and ways to observe holidays (:issue: `7070`) +-``pandas.tseries.holiday.Holiday`` now supports a list of offsets in Python3 (:issue: `7070`) diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py index 6291be340d651..f42ad174b8f0f 100644 --- a/pandas/tseries/holiday.py +++ b/pandas/tseries/holiday.py @@ -2,6 +2,7 @@ from pandas.compat import add_metaclass from datetime import datetime, timedelta from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU +from pandas.tseries.offsets import Easter, Day def next_monday(dt): """ @@ -46,6 +47,20 @@ def sunday_to_monday(dt): return dt + timedelta(1) return dt + +def weekend_to_monday(dt): + """ + If holiday falls on Sunday or Saturday, + use day thereafter (Monday) instead. + Needed for holidays such as Christmas observation in Europe + """ + if dt.weekday() == 6: + return dt + timedelta(1) + elif dt.weekday() == 5: + return dt + timedelta(2) + return dt + + def nearest_workday(dt): """ If holiday falls on Saturday, use day before (Friday) instead; @@ -57,6 +72,44 @@ def nearest_workday(dt): return dt + timedelta(1) return dt + +def next_workday(dt): + """ + returns next weekday used for observances + """ + dt += timedelta(days=1) + while dt.weekday() > 4: + # Mon-Fri are 0-4 + dt += timedelta(days=1) + return dt + + +def previous_workday(dt): + """ + returns previous weekday used for observances + """ + dt -= timedelta(days=1) + while dt.weekday() > 4: + # Mon-Fri are 0-4 + dt -= timedelta(days=1) + return dt + + +def before_nearest_workday(dt): + """ + returns previous workday after nearest workday + """ + return previous_workday(nearest_workday(dt)) + + +def after_nearest_workday(dt): + """ + returns next workday after nearest workday + needed for Boxing day or multiple holidays in a series + """ + return next_workday(nearest_workday(dt)) + + class Holiday(object): """ Class that defines a holiday with start/end dates and rules @@ -64,6 +117,17 @@ class Holiday(object): """ def __init__(self, name, year=None, month=None, day=None, offset=None, observance=None, start_date=None, end_date=None): + """ + Parameters + ---------- + name : str + Name of the holiday , defaults to class name + 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 + """ self.name = name self.year = year self.month = month @@ -149,7 +213,7 @@ def _apply_rule(self, dates): offsets = self.offset for offset in offsets: - dates = map(lambda d: d + offset, dates) + dates = list(map(lambda d: d + offset, dates)) return dates @@ -330,6 +394,11 @@ def merge(self, other, inplace=False): offset=DateOffset(weekday=MO(3))) USPresidentsDay = Holiday('President''s Day', month=2, day=1, offset=DateOffset(weekday=MO(3))) +GoodFriday = Holiday("Good Friday", month=1, day=1, offset=[Easter(), Day(-2)]) + +EasterMonday = Holiday("Easter Monday", month=1, day=1, offset=[Easter(), Day(1)]) + + class USFederalHolidayCalendar(AbstractHolidayCalendar): """ diff --git a/pandas/tseries/tests/test_holiday.py b/pandas/tseries/tests/test_holiday.py index 0d5cc11bea7da..adc2c0d237265 100644 --- a/pandas/tseries/tests/test_holiday.py +++ b/pandas/tseries/tests/test_holiday.py @@ -6,7 +6,10 @@ nearest_workday, next_monday_or_tuesday, next_monday, previous_friday, sunday_to_monday, Holiday, DateOffset, MO, Timestamp, AbstractHolidayCalendar, get_calendar, - HolidayCalendarFactory) + HolidayCalendarFactory, next_workday, previous_workday, + before_nearest_workday, EasterMonday, GoodFriday, + after_nearest_workday, weekend_to_monday) +import nose class TestCalendar(tm.TestCase): @@ -69,6 +72,37 @@ def test_usmemorialday(self): ] self.assertEqual(list(holidays), holidayList) + def test_easter(self): + holidays = EasterMonday.dates(self.start_date, + self.end_date) + holidayList = [Timestamp('2011-04-25 00:00:00'), + Timestamp('2012-04-09 00:00:00'), + Timestamp('2013-04-01 00:00:00'), + Timestamp('2014-04-21 00:00:00'), + Timestamp('2015-04-06 00:00:00'), + Timestamp('2016-03-28 00:00:00'), + Timestamp('2017-04-17 00:00:00'), + Timestamp('2018-04-02 00:00:00'), + Timestamp('2019-04-22 00:00:00'), + Timestamp('2020-04-13 00:00:00')] + + + self.assertEqual(list(holidays), holidayList) + holidays = GoodFriday.dates(self.start_date, + self.end_date) + holidayList = [Timestamp('2011-04-22 00:00:00'), + Timestamp('2012-04-06 00:00:00'), + Timestamp('2013-03-29 00:00:00'), + Timestamp('2014-04-18 00:00:00'), + Timestamp('2015-04-03 00:00:00'), + Timestamp('2016-03-25 00:00:00'), + Timestamp('2017-04-14 00:00:00'), + Timestamp('2018-03-30 00:00:00'), + Timestamp('2019-04-19 00:00:00'), + Timestamp('2020-04-10 00:00:00')] + self.assertEqual(list(holidays), holidayList) + + def test_usthanksgivingday(self): holidays = USThanksgivingDay.dates(self.start_date, self.end_date) @@ -166,3 +200,33 @@ def test_nearest_workday(self): self.assertEqual(nearest_workday(self.su), self.mo) self.assertEqual(nearest_workday(self.mo), self.mo) + def test_weekend_to_monday(self): + self.assertEqual(weekend_to_monday(self.sa), self.mo) + self.assertEqual(weekend_to_monday(self.su), self.mo) + self.assertEqual(weekend_to_monday(self.mo), self.mo) + + def test_next_workday(self): + self.assertEqual(next_workday(self.sa), self.mo) + self.assertEqual(next_workday(self.su), self.mo) + self.assertEqual(next_workday(self.mo), self.tu) + + def test_previous_workday(self): + self.assertEqual(previous_workday(self.sa), self.fr) + self.assertEqual(previous_workday(self.su), self.fr) + self.assertEqual(previous_workday(self.tu), self.mo) + + def test_before_nearest_workday(self): + self.assertEqual(before_nearest_workday(self.sa), self.th) + self.assertEqual(before_nearest_workday(self.su), self.fr) + self.assertEqual(before_nearest_workday(self.tu), self.mo) + + def test_after_nearest_workday(self): + self.assertEqual(after_nearest_workday(self.sa), self.mo) + self.assertEqual(after_nearest_workday(self.su), self.tu) + self.assertEqual(after_nearest_workday(self.fr), self.mo) + + +if __name__ == '__main__': + nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], + exit=False) +