diff --git a/doc/source/release.rst b/doc/source/release.rst index 5134130ba7865..5e5926b2eeec2 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -201,6 +201,7 @@ Improvements to existing features - Performance improvement when converting ``DatetimeIndex`` to floating ordinals using ``DatetimeConverter`` (:issue:`6636`) - Performance improvement for ``DataFrame.shift`` (:issue: `5609`) +- :ref:`Holidays and holiday calendars` are now available and can be used with CustomBusinessDay (:issue:`6719`) .. _release.bug_fixes-0.14.0: diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index b8f89f4e17df1..8529f0c27c1de 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -546,6 +546,17 @@ calendars which account for local holidays and local weekend conventions. print(dts) print(Series(dts.weekday, dts).map(Series('Mon Tue Wed Thu Fri Sat Sun'.split()))) +As of v0.14 holiday calendars can be used to provide the list of holidays. See the +:ref:`holiday calendar` section for more information. + +.. ipython:: python + + from pandas.tseries.holiday import USFederalHolidayCalendar + bday_us = CustomBusinessDay(calendar=USFederalHolidayCalendar()) + dt = datetime(2014, 1, 17) #Friday before MLK Day + print(dt + bday_us) #Tuesday after MLK Day + + .. note:: The frequency string 'C' is used to indicate that a CustomBusinessDay @@ -712,6 +723,73 @@ and business year ends. Please also note the legacy time rule for milliseconds ``ms`` versus the new offset alias for month start ``MS``. This means that offset alias parsing is case sensitive. +.. _timeseries.holiday: + +Holidays / Holiday Calendars +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Holidays and calendars provide a simple way to define holiday rules to be used +with ``CustomBusinessDay`` or in other analysis that requires a predefined +set of holidays. The ``AbstractHolidayCalendar`` class provides all the necessary +methods to return a list of holidays and only ``rules`` need to be defined +in a specific holiday calendar class. Further, ``start_date`` and ``end_date`` +class attributes determine over what date range holidays are generated. These +should be overwritten on the ``AbstractHolidayCalendar`` class to have the range +apply to all calendar subclasses. ``USFederalHolidayCalendar`` is the +only calendar that exists and primarily serves as an example for developing +other calendars. + +For holidays that occur on fixed dates (e.g., US Memorial Day or July 4th) an +observance rule determines when that holiday is observed if it falls on a weekend +or some other non-observed day. Defined observance rules are: + +.. csv-table:: + :header: "Rule", "Description" + :widths: 15, 70 + + "nearest_workday", "move Saturday to Friday and Sunday to Monday" + "sunday_to_monday", "move Sunday to following Monday" + "next_monday_or_tuesday", "move Saturday to Monday and Sunday/Monday to Tuesday" + "previous_friday", move Saturday and Sunday to previous Friday" + "next_monday", "move Saturday and Sunday to following Monday" + +An example of how holidays and holiday calendars are defined: + +.. ipython:: python + + from pandas.tseries.holiday import Holiday, USMemorialDay,\ + AbstractHolidayCalendar, nearest_workday, MO + class ExampleCalendar(AbstractHolidayCalendar): + rules = [ + USMemorialDay, + Holiday('July 4th', month=7, day=4, observance=nearest_workday), + Holiday('Columbus Day', month=10, day=1, + offset=DateOffset(weekday=MO(2))), #same as 2*Week(weekday=2) + ] + cal = ExampleCalendar() + datetime(2012, 5, 25) + CustomBusinessDay(calendar=cal) + cal.holidays(datetime(2012, 1, 1), datetime(2012, 12, 31))#holiday list + AbstractHolidayCalendar.start_date #default start date of range + AbstractHolidayCalendar.end_date #default end date of range + AbstractHolidayCalendar.start_date = datetime(2012, 1, 1)#or Timestamp + AbstractHolidayCalendar.end_date = datetime(2012, 12, 31)#or Timestamp + cal.holidays() + +Every calendar class is accessible by name using the ``get_calendar`` function +which returns a holiday class instance. Any imported calendar class will +automatically be available by this function. Also, ``HolidayCalendarFactory`` +provides an easy interface to create calendars that are combinations of calendars +or calendars with additional rules. + +.. ipython:: python + + from pandas.tseries.holiday import get_calendar, HolidayCalendarFactory,\ + USLaborDay + cal = get_calendar('ExampleCalendar') + cal.rules + new_cal = HolidayCalendarFactory('NewExampleCalendar', cal, USLaborDay) + new_cal.rules + .. _timeseries.advanced_datetime: Time series-related instance methods diff --git a/doc/source/v0.14.0.txt b/doc/source/v0.14.0.txt index 95537878871b1..e8c05a0877b5f 100644 --- a/doc/source/v0.14.0.txt +++ b/doc/source/v0.14.0.txt @@ -308,7 +308,7 @@ Asymmetrical error bars are also supported, however raw error values must be pro Prior Version Deprecations/Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Therse are prior version deprecations that are taking effect as of 0.14.0. +There are prior version deprecations that are taking effect as of 0.14.0. - Remove ``column`` keyword from ``DataFrame.sort`` (:issue:`4370`) @@ -376,6 +376,7 @@ Enhancements file. (:issue:`6545`) - ``pandas.io.gbq`` now handles reading unicode strings properly. (:issue:`5940`) - Improve performance of ``CustomBusinessDay`` (:issue:`6584`) +- :ref:`Holidays and holiday calendars` are now available and can be used with CustomBusinessDay (:issue:`6719`) Performance ~~~~~~~~~~~ diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py new file mode 100644 index 0000000000000..1a79df0f5dc04 --- /dev/null +++ b/pandas/tseries/holiday.py @@ -0,0 +1,357 @@ +from pandas import DateOffset, DatetimeIndex, Series, Timestamp +from pandas.compat import add_metaclass +from datetime import datetime, timedelta +from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU + +def next_monday(dt): + ''' + If holiday falls on Saturday, use following Monday instead; + if holiday falls on Sunday, use Monday instead + ''' + if dt.weekday() == 5: + return dt + timedelta(2) + elif dt.weekday() == 6: + return dt + timedelta(1) + return dt + +def next_monday_or_tuesday(dt): + ''' + For second holiday of two adjacent ones! + If holiday falls on Saturday, use following Monday instead; + if holiday falls on Sunday or Monday, use following Tuesday instead + (because Monday is already taken by adjacent holiday on the day before) + ''' + dow = dt.weekday() + if dow == 5 or dow == 6: + return dt + timedelta(2) + elif dow == 0: + return dt + timedelta(1) + return dt + +def previous_friday(dt): + ''' + If holiday falls on Saturday or Sunday, use previous Friday instead. + ''' + if dt.weekday() == 5: + return dt - timedelta(1) + elif dt.weekday() == 6: + return dt - timedelta(2) + return dt + +def sunday_to_monday(dt): + ''' + If holiday falls on Sunday, use day thereafter (Monday) instead. + ''' + if dt.weekday() == 6: + return dt + timedelta(1) + return dt + +def nearest_workday(dt): + ''' + If holiday falls on Saturday, use day before (Friday) instead; + if holiday falls on Sunday, use day thereafter (Monday) instead. + ''' + if dt.weekday() == 5: + return dt - timedelta(1) + elif dt.weekday() == 6: + return dt + timedelta(1) + return dt + +class Holiday(object): + ''' + Class that defines a holiday with start/end dates and rules + for observance. + ''' + def __init__(self, name, year=None, month=None, day=None, offset=None, + observance=None, start_date=None, end_date=None): + self.name = name + self.year = year + self.month = month + self.day = day + self.offset = offset + self.start_date = start_date + self.end_date = end_date + self.observance = observance + + def __repr__(self): + info = '' + if self.year is not None: + info += 'year=%s, ' % self.year + info += 'month=%s, day=%s, ' % (self.month, self.day) + + if self.offset is not None: + info += 'offset=%s' % self.offset + + if self.observance is not None: + info += 'observance=%s' % self.observance + + return 'Holiday: %s (%s)' % (self.name, info) + + def dates(self, start_date, end_date, return_name=False): + ''' + Calculate holidays between start date and end date + + Parameters + ---------- + start_date : starting date, datetime-like, optional + end_date : ending date, datetime-like, optional + return_name : bool, optional, default=False + If True, return a series that has dates and holiday names. + False will only return dates. + ''' + if self.year is not None: + dt = Timestamp(datetime(self.year, self.month, self.day)) + if return_name: + return Series(self.name, index=[dt]) + else: + return [dt] + + if self.start_date is not None: + start_date = self.start_date + + if self.end_date is not None: + end_date = self.end_date + + start_date = Timestamp(start_date) + end_date = Timestamp(end_date) + + year_offset = DateOffset(years=1) + base_date = Timestamp(datetime(start_date.year, self.month, self.day)) + dates = DatetimeIndex(start=base_date, end=end_date, freq=year_offset) + holiday_dates = list(self._apply_rule(dates)) + if return_name: + return Series(self.name, index=holiday_dates) + + return holiday_dates + + def _apply_rule(self, dates): + ''' + Apply the given offset/observance to an + iterable of dates. + + Parameters + ---------- + dates : array-like + Dates to apply the given offset/observance rule + + Returns + ------- + Dates with rules applied + ''' + if self.observance is not None: + return map(lambda d: self.observance(d), dates) + + if not isinstance(self.offset, list): + offsets = [self.offset] + else: + offsets = self.offset + + for offset in offsets: + dates = map(lambda d: d + offset, dates) + + return dates + +#---------------------------------------------------------------------- +# Calendar class registration + +holiday_calendars = {} +def register(cls): + try: + name = cls.name + except: + name = cls.__name__ + holiday_calendars[name] = cls + +def get_calendar(name): + ''' + Return an instance of a calendar based on its name. + + Parameters + ---------- + name : str + Calendar name to return an instance of + ''' + return holiday_calendars[name]() + +#---------------------------------------------------------------------- +# Holiday classes +class HolidayCalendarMetaClass(type): + def __new__(cls, clsname, bases, attrs): + calendar_class = super(HolidayCalendarMetaClass, cls).__new__(cls, clsname, bases, attrs) + register(calendar_class) + return calendar_class + +@add_metaclass(HolidayCalendarMetaClass) +class AbstractHolidayCalendar(object): + ''' + Abstract interface to create holidays following certain rules. + ''' + __metaclass__ = HolidayCalendarMetaClass + rules = [] + start_date = Timestamp(datetime(1970, 1, 1)) + end_date = Timestamp(datetime(2030, 12, 31)) + _holiday_cache = None + + def __init__(self, name=None, rules=None): + ''' + Initializes holiday object with a given set a rules. Normally + classes just have the rules defined within them. + + Parameters + ---------- + name : str + Name of the holiday calendar, defaults to class name + rules : array of Holiday objects + A set of rules used to create the holidays. + ''' + super(AbstractHolidayCalendar, self).__init__() + if name is None: + name = self.__class__.__name__ + self.name = name + + if rules is not None: + self.rules = rules + + def holidays(self, start=None, end=None, return_name=False): + ''' + Returns a curve with holidays between start_date and end_date + + Parameters + ---------- + start : starting date, datetime-like, optional + end : ending date, datetime-like, optional + return_names : bool, optional + If True, return a series that has dates and holiday names. + False will only return a DatetimeIndex of dates. + + Returns + ------- + DatetimeIndex of holidays + ''' + if self.rules is None: + raise Exception('Holiday Calendar %s does not have any '\ + 'rules specified' % self.name) + + if start is None: + start = AbstractHolidayCalendar.start_date + + if end is None: + end = AbstractHolidayCalendar.end_date + + start = Timestamp(start) + end = Timestamp(end) + + holidays = None + # If we don't have a cache or the dates are outside the prior cache, we get them again + if self._cache is None or start < self._cache[0] or end > self._cache[1]: + for rule in self.rules: + rule_holidays = rule.dates(start, end, return_name=True) + if holidays is None: + holidays = rule_holidays + else: + holidays = holidays.append(rule_holidays) + + self._cache = (start, end, holidays.sort_index()) + + holidays = self._cache[2] + holidays = holidays[start:end] + + if return_name: + return holidays + else: + return holidays.index + + @property + def _cache(self): + return self.__class__._holiday_cache + + @_cache.setter + def _cache(self, values): + self.__class__._holiday_cache = values + + @staticmethod + def merge_class(base, other): + ''' + Merge holiday calendars together. The base calendar + will take precedence to other. The merge will be done + based on each holiday's name. + + Parameters + ---------- + base : AbstractHolidayCalendar instance/subclass or array of Holiday objects + other : AbstractHolidayCalendar instance/subclass or array of Holiday objects + ''' + try: + other = other.rules + except: + pass + + if not isinstance(other, list): + other = [other] + other_holidays = dict((holiday.name, holiday) for holiday in other) + + try: + base = base.rules + except: + pass + + if not isinstance(base, list): + base = [base] + base_holidays = dict((holiday.name, holiday) for holiday in base) + + other_holidays.update(base_holidays) + return list(other_holidays.values()) + + def merge(self, other, inplace=False): + ''' + Merge holiday calendars together. The caller's class + rules take precedence. The merge will be done + based on each holiday's name. + + Parameters + ---------- + other : holiday calendar + inplace : bool (default=False) + If True set rule_table to holidays, else return array of Holidays + ''' + holidays = self.merge_class(self, other) + if inplace: + self.rules = holidays + else: + return holidays + +USMemorialDay = Holiday('MemorialDay', month=5, day=24, + offset=DateOffset(weekday=MO(1))) +USLaborDay = Holiday('Labor Day', month=9, day=1, + offset=DateOffset(weekday=MO(1))) +USColumbusDay = Holiday('Columbus Day', month=10, day=1, + offset=DateOffset(weekday=MO(2))) +USThanksgivingDay = Holiday('Thanksgiving', month=11, day=1, + offset=DateOffset(weekday=TH(4))) +USMartinLutherKingJr = Holiday('Dr. Martin Luther King Jr.', month=1, day=1, + offset=DateOffset(weekday=MO(3))) +USPresidentsDay = Holiday('President''s Day', month=2, day=1, + offset=DateOffset(weekday=MO(3))) + +class USFederalHolidayCalendar(AbstractHolidayCalendar): + ''' + US Federal Government Holiday Calendar based on rules specified + by: https://www.opm.gov/policy-data-oversight/snow-dismissal-procedures/federal-holidays/ + ''' + rules = [ + Holiday('New Years Day', month=1, day=1, observance=nearest_workday), + USMartinLutherKingJr, + USPresidentsDay, + USMemorialDay, + Holiday('July 4th', month=7, day=4, observance=nearest_workday), + USLaborDay, + USColumbusDay, + Holiday('Veterans Day', month=11, day=11, observance=nearest_workday), + USThanksgivingDay, + Holiday('Christmas', month=12, day=25, observance=nearest_workday) + ] + +def HolidayCalendarFactory(name, base, other, base_class=AbstractHolidayCalendar): + rules = AbstractHolidayCalendar.merge_class(base, other) + calendar_class = type(name, (base_class,), {"rules": rules, "name": name}) + return calendar_class diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index eb40f1f520cff..a7f5a6ecf0a23 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -7,6 +7,7 @@ # import after tools, dateutil check from dateutil.relativedelta import relativedelta, weekday +from dateutil.easter import easter import pandas.tslib as tslib from pandas.tslib import Timestamp, OutOfBoundsDatetime @@ -17,7 +18,7 @@ 'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd', 'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd', 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253', - 'Week', 'WeekOfMonth', + 'Week', 'WeekOfMonth', 'Easter', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano'] # convert to/from datetime/timestamp to allow invalid Timestamp ranges to pass thru @@ -447,6 +448,8 @@ class CustomBusinessDay(BusinessDay): holidays : list list/array of dates to exclude from the set of valid business days, passed to ``numpy.busdaycalendar`` + calendar : HolidayCalendar instance + instance of AbstractHolidayCalendar that provide the list of holidays """ _cacheable = False @@ -458,8 +461,11 @@ def __init__(self, n=1, **kwds): self.offset = kwds.get('offset', timedelta(0)) self.normalize = kwds.get('normalize', False) self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri') - holidays = kwds.get('holidays', []) - + + if 'calendar' in kwds: + holidays = kwds['calendar'].holidays() + else: + holidays = kwds.get('holidays', []) holidays = [self._to_dt64(dt, dtype='datetime64[D]') for dt in holidays] self.holidays = tuple(sorted(holidays)) @@ -1677,7 +1683,40 @@ def _from_name(cls, *args): return cls(**dict(FY5253._parse_suffix(*args[:-1]), qtr_with_extra_week=int(args[-1]))) - +class Easter(DateOffset): + ''' + DateOffset for the Easter holiday using + logic defined in dateutil. Right now uses + the revised method which is valid in years + 1583-4099. + ''' + def __init__(self, n=1, **kwds): + super(Easter, self).__init__(n, **kwds) + + def apply(self, other): + + currentEaster = easter(other.year) + currentEaster = datetime(currentEaster.year, currentEaster.month, currentEaster.day) + + # NOTE: easter returns a datetime.date so we have to convert to type of other + if other >= currentEaster: + new = easter(other.year + self.n) + elif other < currentEaster: + new = easter(other.year + self.n - 1) + else: + new = other + + # FIXME: There has to be a better way to do this, but I don't know what it is + if isinstance(other, Timestamp): + return as_timestamp(new) + elif isinstance(other, datetime): + return datetime(new.year, new.month, new.day) + else: + return new + + @classmethod + def onOffset(cls, dt): + return date(dt.year, dt.month, dt.day) == easter(dt.year) #---------------------------------------------------------------------- # Ticks diff --git a/pandas/tseries/tests/test_holiday.py b/pandas/tseries/tests/test_holiday.py new file mode 100644 index 0000000000000..62cae8ac98508 --- /dev/null +++ b/pandas/tseries/tests/test_holiday.py @@ -0,0 +1,166 @@ + +from datetime import datetime +import pandas.util.testing as tm +from pandas.tseries.holiday import ( + USFederalHolidayCalendar, USMemorialDay, USThanksgivingDay, + nearest_workday, next_monday_or_tuesday, next_monday, + previous_friday, sunday_to_monday, Holiday, DateOffset, + MO, Timestamp, AbstractHolidayCalendar, get_calendar, + HolidayCalendarFactory) + +class TestCalendar(tm.TestCase): + + def setUp(self): + self.holiday_list = [ + datetime(2012, 1, 2), + datetime(2012, 1, 16), + datetime(2012, 2, 20), + datetime(2012, 5, 28), + datetime(2012, 7, 4), + datetime(2012, 9, 3), + datetime(2012, 10, 8), + datetime(2012, 11, 12), + datetime(2012, 11, 22), + datetime(2012, 12, 25)] + self.start_date = datetime(2012, 1, 1) + self.end_date = datetime(2012, 12, 31) + + def test_calendar(self): + + calendar = USFederalHolidayCalendar() + holidays = calendar.holidays(self.start_date, + self.end_date) + + holidays_1 = calendar.holidays( + self.start_date.strftime('%Y-%m-%d'), + self.end_date.strftime('%Y-%m-%d')) + holidays_2 = calendar.holidays( + Timestamp(self.start_date), + Timestamp(self.end_date)) + + self.assertEqual(list(holidays.to_pydatetime()), + self.holiday_list) + self.assertEqual(list(holidays_1.to_pydatetime()), + self.holiday_list) + self.assertEqual(list(holidays_2.to_pydatetime()), + self.holiday_list) + +class TestHoliday(tm.TestCase): + + def setUp(self): + self.start_date = datetime(2011, 1, 1) + self.end_date = datetime(2020, 12, 31) + + def test_usmemorialday(self): + holidays = USMemorialDay.dates(self.start_date, + self.end_date) + holidayList = [ + datetime(2011, 5, 30), + datetime(2012, 5, 28), + datetime(2013, 5, 27), + datetime(2014, 5, 26), + datetime(2015, 5, 25), + datetime(2016, 5, 30), + datetime(2017, 5, 29), + datetime(2018, 5, 28), + datetime(2019, 5, 27), + datetime(2020, 5, 25), + ] + self.assertEqual(list(holidays), holidayList) + + def test_usthanksgivingday(self): + holidays = USThanksgivingDay.dates(self.start_date, + self.end_date) + holidayList = [ + datetime(2011, 11, 24), + datetime(2012, 11, 22), + datetime(2013, 11, 28), + datetime(2014, 11, 27), + datetime(2015, 11, 26), + datetime(2016, 11, 24), + datetime(2017, 11, 23), + datetime(2018, 11, 22), + datetime(2019, 11, 28), + datetime(2020, 11, 26), + ] + + self.assertEqual(list(holidays), holidayList) + + def test_argument_types(self): + holidays = USThanksgivingDay.dates(self.start_date, + self.end_date) + + holidays_1 = USThanksgivingDay.dates( + self.start_date.strftime('%Y-%m-%d'), + self.end_date.strftime('%Y-%m-%d')) + + holidays_2 = USThanksgivingDay.dates( + Timestamp(self.start_date), + Timestamp(self.end_date)) + + self.assertEqual(holidays, holidays_1) + self.assertEqual(holidays, holidays_2) + + def test_special_holidays(self): + base_date = [datetime(2012, 5, 28)] + holiday_1 = Holiday('One-Time', year=2012, month=5, day=28) + holiday_2 = Holiday('Range', month=5, day=28, + start_date=datetime(2012, 1, 1), + end_date=datetime(2012, 12, 31), + offset=DateOffset(weekday=MO(1))) + + self.assertEqual(base_date, + holiday_1.dates(self.start_date, self.end_date)) + self.assertEqual(base_date, + holiday_2.dates(self.start_date, self.end_date)) + + def test_get_calendar(self): + class TestCalendar(AbstractHolidayCalendar): + rules = [] + + calendar = get_calendar('TestCalendar') + self.assertEqual(TestCalendar, calendar.__class__) + + def test_factory(self): + class_1 = HolidayCalendarFactory('MemorialDay', AbstractHolidayCalendar, + USMemorialDay) + class_2 = HolidayCalendarFactory('Thansksgiving', AbstractHolidayCalendar, + USThanksgivingDay) + class_3 = HolidayCalendarFactory('Combined', class_1, class_2) + + self.assertEqual(len(class_1.rules), 1) + self.assertEqual(len(class_2.rules), 1) + self.assertEqual(len(class_3.rules), 2) + + +class TestObservanceRules(tm.TestCase): + + def setUp(self): + self.we = datetime(2014, 4, 9) + self.th = datetime(2014, 4, 10) + self.fr = datetime(2014, 4, 11) + self.sa = datetime(2014, 4, 12) + self.su = datetime(2014, 4, 13) + self.mo = datetime(2014, 4, 14) + self.tu = datetime(2014, 4, 15) + + def test_next_monday(self): + self.assertEqual(next_monday(self.sa), self.mo) + self.assertEqual(next_monday(self.su), self.mo) + + def test_next_monday_or_tuesday(self): + self.assertEqual(next_monday_or_tuesday(self.sa), self.mo) + self.assertEqual(next_monday_or_tuesday(self.su), self.tu) + self.assertEqual(next_monday_or_tuesday(self.mo), self.tu) + + def test_previous_friday(self): + self.assertEqual(previous_friday(self.sa), self.fr) + self.assertEqual(previous_friday(self.su), self.fr) + + def test_sunday_to_monday(self): + self.assertEqual(sunday_to_monday(self.su), self.mo) + + def test_nearest_workday(self): + self.assertEqual(nearest_workday(self.sa), self.fr) + self.assertEqual(nearest_workday(self.su), self.mo) + self.assertEqual(nearest_workday(self.mo), self.mo) diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py index b303b7bb50526..97c2f2a2b5b57 100644 --- a/pandas/tseries/tests/test_offsets.py +++ b/pandas/tseries/tests/test_offsets.py @@ -11,7 +11,7 @@ bday, BDay, cday, CDay, BQuarterEnd, BMonthEnd, BYearEnd, MonthEnd, MonthBegin, BYearBegin, QuarterBegin, BQuarterBegin, BMonthBegin, DateOffset, Week, YearBegin, YearEnd, Hour, Minute, Second, Day, Micro, - Milli, Nano, + Milli, Nano, Easter, WeekOfMonth, format, ole2datetime, QuarterEnd, to_datetime, normalize_date, get_offset, get_offset_name, get_standard_freq) @@ -26,6 +26,7 @@ import pandas.util.testing as tm from pandas.tseries.offsets import BusinessMonthEnd, CacheableOffset, \ LastWeekOfMonth, FY5253, FY5253Quarter, WeekDay +from pandas.tseries.holiday import USFederalHolidayCalendar from pandas import _np_version_under1p7 @@ -546,6 +547,10 @@ def test_weekmask_and_holidays(self): xp_egypt = datetime(2013, 5, 5) self.assertEqual(xp_egypt, dt + 2 * bday_egypt) + def test_calendar(self): + calendar = USFederalHolidayCalendar() + dt = datetime(2014, 1, 17) + assertEq(CDay(calendar=calendar), dt, datetime(2014, 1, 21)) def assertOnOffset(offset, date, expected): actual = offset.onOffset(date) @@ -2167,6 +2172,8 @@ def assertEq(offset, base, expected): "\nAt Date: %s" % (expected, actual, offset, base)) +def test_Easter(): + assertEq(Easter(), datetime(2010, 1, 1), datetime(2010, 4, 4)) def test_Hour(): assertEq(Hour(), datetime(2010, 1, 1), datetime(2010, 1, 1, 1))