diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 92b904bc683f4..cb5bee75b83a2 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -563,6 +563,7 @@ frequency increment. Specific offset logic like "month", "business day", or BYearBegin, "business year begin" FY5253, "retail (aka 52-53 week) year" BusinessHour, "business hour" + CustomBusinessHour, "custom business hour" Hour, "one hour" Minute, "one minute" Second, "one second" @@ -883,6 +884,40 @@ under the default business hours (9:00 - 17:00), there is no gap (0 minutes) bet # The result is the same as rollworward because BusinessDay never overlap. BusinessHour().apply(Timestamp('2014-08-02')) +``BusinessHour`` regards Saturday and Sunday as holidays. To use arbitrary holidays, +you can use ``CustomBusinessHour`` offset, see :ref:`Custom Business Hour `: + +.. _timeseries.custombusinesshour: + +Custom Business Hour +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 0.18.1 + +The ``CustomBusinessHour`` is a mixture of ``BusinessHour`` and ``CustomBusinessDay`` which +allows you to specify arbitrary holidays. ``CustomBusinessHour`` works as the same +as ``BusinessHour`` except that it skips specified custom holidays. + +.. ipython:: python + + from pandas.tseries.holiday import USFederalHolidayCalendar + bhour_us = CustomBusinessHour(calendar=USFederalHolidayCalendar()) + # Friday before MLK Day + dt = datetime(2014, 1, 17, 15) + + dt + bhour_us + + # Tuesday after MLK Day (Monday is skipped because it's a holiday) + dt + bhour_us * 2 + +You can use keyword arguments suported by either ``BusinessHour`` and ``CustomBusinessDay``. + +.. ipython:: python + + bhour_mon = CustomBusinessHour(start='10:00', weekmask='Tue Wed Thu Fri') + + # Monday is skipped because it's a holiday, business hour starts from 10:00 + dt + bhour_mon * 2 Offset Aliases ~~~~~~~~~~~~~~ diff --git a/doc/source/whatsnew/v0.18.1.txt b/doc/source/whatsnew/v0.18.1.txt index edbaeb65c45eb..530ffd130d660 100644 --- a/doc/source/whatsnew/v0.18.1.txt +++ b/doc/source/whatsnew/v0.18.1.txt @@ -9,6 +9,9 @@ We recommend that all users upgrade to this version. Highlights include: +- Custom business hour offset, see :ref:`here `. + + .. contents:: What's new in v0.18.1 :local: :backlinks: none @@ -18,11 +21,27 @@ Highlights include: New features ~~~~~~~~~~~~ +.. _whatsnew_0181.enhancements.custombusinesshour: + +Custom Business Hour +^^^^^^^^^^^^^^^^^^^^ +The ``CustomBusinessHour`` is a mixture of ``BusinessHour`` and ``CustomBusinessDay`` which +allows you to specify arbitrary holidays. For details, +see :ref:`Custom Business Hour ` (:issue:`11514`) +.. ipython:: python + from pandas.tseries.offsets import CustomBusinessHour + from pandas.tseries.holiday import USFederalHolidayCalendar + bhour_us = CustomBusinessHour(calendar=USFederalHolidayCalendar()) + # Friday before MLK Day + dt = datetime(2014, 1, 17, 15) + dt + bhour_us + # Tuesday after MLK Day (Monday is skipped because it's a holiday) + dt + bhour_us * 2 .. _whatsnew_0181.enhancements: @@ -216,6 +235,7 @@ Bug Fixes + - Bug in ``value_counts`` when ``normalize=True`` and ``dropna=True`` where nulls still contributed to the normalized count (:issue:`12558`) - Bug in ``Panel.fillna()`` ignoring ``inplace=True`` (:issue:`12633`) - Bug in ``read_csv`` when specifying ``names``, ```usecols``, and ``parse_dates`` simultaneously with the C engine (:issue:`9755`) @@ -231,6 +251,7 @@ Bug Fixes - Bug in ``.str`` accessor methods may raise ``ValueError`` if input has ``name`` and the result is ``DataFrame`` or ``MultiIndex`` (:issue:`12617`) + - Bug in ``CategoricalIndex.get_loc`` returns different result from regular ``Index`` (:issue:`12531`) - Bug in ``PeriodIndex.resample`` where name not propagated (:issue:`12769`) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 1a666f5ed012b..01ed4b65fbaee 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -18,7 +18,7 @@ __all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay', 'CBMonthEnd', 'CBMonthBegin', 'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd', - 'BusinessHour', + 'BusinessHour', 'CustomBusinessHour', 'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd', 'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd', 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253', @@ -669,20 +669,9 @@ def onOffset(self, dt): return dt.weekday() < 5 -class BusinessHour(BusinessMixin, SingleConstructorOffset): - """ - DateOffset subclass representing possibly n business days - - .. versionadded: 0.16.1 - - """ - _prefix = 'BH' - _anchor = 0 - - def __init__(self, n=1, normalize=False, **kwds): - self.n = int(n) - self.normalize = normalize +class BusinessHourMixin(BusinessMixin): + def __init__(self, **kwds): # must be validated here to equality check kwds['start'] = self._validate_time(kwds.get('start', '09:00')) kwds['end'] = self._validate_time(kwds.get('end', '17:00')) @@ -691,12 +680,6 @@ def __init__(self, n=1, normalize=False, **kwds): self.start = kwds.get('start', '09:00') self.end = kwds.get('end', '17:00') - # used for moving to next businessday - if self.n >= 0: - self.next_bday = BusinessDay(n=1) - else: - self.next_bday = BusinessDay(n=-1) - def _validate_time(self, t_input): from datetime import time as dt_time import time @@ -722,13 +705,6 @@ def _get_daytime_flag(self): else: return False - def _repr_attrs(self): - out = super(BusinessHour, self)._repr_attrs() - attrs = ['BH=%s-%s' % (self.start.strftime('%H:%M'), - self.end.strftime('%H:%M'))] - out += ': ' + ', '.join(attrs) - return out - def _next_opening_time(self, other): """ If n is positive, return tomorrow's business day opening time. @@ -905,6 +881,38 @@ def _onOffset(self, dt, businesshours): else: return False + def _repr_attrs(self): + out = super(BusinessHourMixin, self)._repr_attrs() + start = self.start.strftime('%H:%M') + end = self.end.strftime('%H:%M') + attrs = ['{prefix}={start}-{end}'.format(prefix=self._prefix, + start=start, end=end)] + out += ': ' + ', '.join(attrs) + return out + + +class BusinessHour(BusinessHourMixin, SingleConstructorOffset): + """ + DateOffset subclass representing possibly n business days + + .. versionadded: 0.16.1 + + """ + _prefix = 'BH' + _anchor = 0 + + def __init__(self, n=1, normalize=False, **kwds): + self.n = int(n) + self.normalize = normalize + super(BusinessHour, self).__init__(**kwds) + + # used for moving to next businessday + if self.n >= 0: + nb_offset = 1 + else: + nb_offset = -1 + self.next_bday = BusinessDay(n=nb_offset) + class CustomBusinessDay(BusinessDay): """ @@ -976,18 +984,7 @@ def get_calendar(self, weekmask, holidays, calendar): if holidays: kwargs['holidays'] = holidays - try: - busdaycalendar = np.busdaycalendar(**kwargs) - except: - # Check we have the required numpy version - from distutils.version import LooseVersion - - if LooseVersion(np.__version__) < '1.7.0': - raise NotImplementedError( - "CustomBusinessDay requires numpy >= " - "1.7.0. Current version: " + np.__version__) - else: - raise + busdaycalendar = np.busdaycalendar(**kwargs) return busdaycalendar, holidays def __getstate__(self): @@ -1067,6 +1064,36 @@ def onOffset(self, dt): return np.is_busday(day64, busdaycal=self.calendar) +class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset): + """ + DateOffset subclass representing possibly n custom business days + + .. versionadded: 0.18.1 + + """ + _prefix = 'CBH' + _anchor = 0 + + def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', + holidays=None, calendar=None, **kwds): + self.n = int(n) + self.normalize = normalize + super(CustomBusinessHour, self).__init__(**kwds) + # used for moving to next businessday + if self.n >= 0: + nb_offset = 1 + else: + nb_offset = -1 + self.next_bday = CustomBusinessDay(n=nb_offset, + weekmask=weekmask, + holidays=holidays, + calendar=calendar) + + self.kwds['weekmask'] = self.next_bday.weekmask + self.kwds['holidays'] = self.next_bday.holidays + self.kwds['calendar'] = self.next_bday.calendar + + class MonthOffset(SingleConstructorOffset): _adjust_dst = True @@ -2673,31 +2700,32 @@ def generate_range(start=None, end=None, periods=None, cur = next_date prefix_mapping = dict((offset._prefix, offset) for offset in [ - YearBegin, # 'AS' - YearEnd, # 'A' - BYearBegin, # 'BAS' - BYearEnd, # 'BA' - BusinessDay, # 'B' - BusinessMonthBegin, # 'BMS' - BusinessMonthEnd, # 'BM' - BQuarterEnd, # 'BQ' - BQuarterBegin, # 'BQS' - BusinessHour, # 'BH' - CustomBusinessDay, # 'C' - CustomBusinessMonthEnd, # 'CBM' + YearBegin, # 'AS' + YearEnd, # 'A' + BYearBegin, # 'BAS' + BYearEnd, # 'BA' + BusinessDay, # 'B' + BusinessMonthBegin, # 'BMS' + BusinessMonthEnd, # 'BM' + BQuarterEnd, # 'BQ' + BQuarterBegin, # 'BQS' + BusinessHour, # 'BH' + CustomBusinessDay, # 'C' + CustomBusinessMonthEnd, # 'CBM' CustomBusinessMonthBegin, # 'CBMS' - MonthEnd, # 'M' - MonthBegin, # 'MS' - Week, # 'W' - Second, # 'S' - Minute, # 'T' - Micro, # 'U' - QuarterEnd, # 'Q' - QuarterBegin, # 'QS' - Milli, # 'L' - Hour, # 'H' - Day, # 'D' - WeekOfMonth, # 'WOM' + CustomBusinessHour, # 'CBH' + MonthEnd, # 'M' + MonthBegin, # 'MS' + Week, # 'W' + Second, # 'S' + Minute, # 'T' + Micro, # 'U' + QuarterEnd, # 'Q' + QuarterBegin, # 'QS' + Milli, # 'L' + Hour, # 'H' + Day, # 'D' + WeekOfMonth, # 'WOM' FY5253, FY5253Quarter, ]) diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py index 726c777535315..fe025d2249add 100644 --- a/pandas/tseries/tests/test_offsets.py +++ b/pandas/tseries/tests/test_offsets.py @@ -10,7 +10,8 @@ from pandas.compat.numpy_compat import np_datetime64_compat from pandas.core.datetools import (bday, BDay, CDay, BQuarterEnd, BMonthEnd, - BusinessHour, CBMonthEnd, CBMonthBegin, + BusinessHour, CustomBusinessHour, + CBMonthEnd, CBMonthBegin, BYearEnd, MonthEnd, MonthBegin, BYearBegin, QuarterBegin, BQuarterBegin, BMonthBegin, DateOffset, @@ -134,7 +135,7 @@ def test_apply_out_of_range(self): # try to create an out-of-bounds result timestamp; if we can't create # the offset skip try: - if self._offset is BusinessHour: + if self._offset in (BusinessHour, CustomBusinessHour): # Using 10000 in BusinessHour fails in tz check because of DST # difference offset = self._get_offset(self._offset, value=100000) @@ -163,8 +164,8 @@ def test_apply_out_of_range(self): class TestCommon(Base): - def setUp(self): + def setUp(self): # exected value created by Base._get_offset # are applied to 2011/01/01 09:00 (Saturday) # used for .apply and .rollforward @@ -191,6 +192,8 @@ def setUp(self): 'QuarterEnd': Timestamp('2011-03-31 09:00:00'), 'BQuarterEnd': Timestamp('2011-03-31 09:00:00'), 'BusinessHour': Timestamp('2011-01-03 10:00:00'), + 'CustomBusinessHour': + Timestamp('2011-01-03 10:00:00'), 'WeekOfMonth': Timestamp('2011-01-08 09:00:00'), 'LastWeekOfMonth': Timestamp('2011-01-29 09:00:00'), 'FY5253Quarter': Timestamp('2011-01-25 09:00:00'), @@ -315,6 +318,7 @@ def test_rollforward(self): expecteds[n] = Timestamp('2011/01/01 09:00') expecteds['BusinessHour'] = Timestamp('2011-01-03 09:00:00') + expecteds['CustomBusinessHour'] = Timestamp('2011-01-03 09:00:00') # but be changed when normalize=True norm_expected = expecteds.copy() @@ -363,6 +367,7 @@ def test_rollback(self): 'QuarterEnd': Timestamp('2010-12-31 09:00:00'), 'BQuarterEnd': Timestamp('2010-12-31 09:00:00'), 'BusinessHour': Timestamp('2010-12-31 17:00:00'), + 'CustomBusinessHour': Timestamp('2010-12-31 17:00:00'), 'WeekOfMonth': Timestamp('2010-12-11 09:00:00'), 'LastWeekOfMonth': Timestamp('2010-12-25 09:00:00'), 'FY5253Quarter': Timestamp('2010-10-26 09:00:00'), @@ -413,7 +418,7 @@ def test_onOffset(self): offset_n = self._get_offset(offset, normalize=True) self.assertFalse(offset_n.onOffset(dt)) - if offset is BusinessHour: + if offset in (BusinessHour, CustomBusinessHour): # In default BusinessHour (9:00-17:00), normalized time # cannot be in business hour range continue @@ -750,7 +755,8 @@ def testEQ(self): BusinessHour(start='17:00', end='09:01')) def test_hash(self): - self.assertEqual(hash(self.offset2), hash(self.offset2)) + for offset in [self.offset1, self.offset2, self.offset3, self.offset4]: + self.assertEqual(hash(offset), hash(offset)) def testCall(self): self.assertEqual(self.offset1(self.d), datetime(2014, 7, 1, 11)) @@ -1389,6 +1395,267 @@ def test_datetimeindex(self): tm.assert_index_equal(idx, expected) +class TestCustomBusinessHour(Base): + _multiprocess_can_split_ = True + _offset = CustomBusinessHour + + def setUp(self): + # 2014 Calendar to check custom holidays + # Sun Mon Tue Wed Thu Fri Sat + # 6/22 23 24 25 26 27 28 + # 29 30 7/1 2 3 4 5 + # 6 7 8 9 10 11 12 + self.d = datetime(2014, 7, 1, 10, 00) + self.offset1 = CustomBusinessHour(weekmask='Tue Wed Thu Fri') + + self.holidays = ['2014-06-27', datetime(2014, 6, 30), + np.datetime64('2014-07-02')] + self.offset2 = CustomBusinessHour(holidays=self.holidays) + + def test_constructor_errors(self): + from datetime import time as dt_time + with tm.assertRaises(ValueError): + CustomBusinessHour(start=dt_time(11, 0, 5)) + with tm.assertRaises(ValueError): + CustomBusinessHour(start='AAA') + with tm.assertRaises(ValueError): + CustomBusinessHour(start='14:00:05') + + def test_different_normalize_equals(self): + # equivalent in this special case + offset = self._offset() + offset2 = self._offset() + offset2.normalize = True + self.assertEqual(offset, offset2) + + def test_repr(self): + self.assertEqual(repr(self.offset1), + '') + self.assertEqual(repr(self.offset2), + '') + + def test_with_offset(self): + expected = Timestamp('2014-07-01 13:00') + + self.assertEqual(self.d + CustomBusinessHour() * 3, expected) + self.assertEqual(self.d + CustomBusinessHour(n=3), expected) + + def testEQ(self): + for offset in [self.offset1, self.offset2]: + self.assertEqual(offset, offset) + + self.assertNotEqual(CustomBusinessHour(), CustomBusinessHour(-1)) + self.assertEqual(CustomBusinessHour(start='09:00'), + CustomBusinessHour()) + self.assertNotEqual(CustomBusinessHour(start='09:00'), + CustomBusinessHour(start='09:01')) + self.assertNotEqual(CustomBusinessHour(start='09:00', end='17:00'), + CustomBusinessHour(start='17:00', end='09:01')) + + self.assertNotEqual(CustomBusinessHour(weekmask='Tue Wed Thu Fri'), + CustomBusinessHour(weekmask='Mon Tue Wed Thu Fri')) + self.assertNotEqual(CustomBusinessHour(holidays=['2014-06-27']), + CustomBusinessHour(holidays=['2014-06-28'])) + + def test_hash(self): + self.assertEqual(hash(self.offset1), hash(self.offset1)) + self.assertEqual(hash(self.offset2), hash(self.offset2)) + + def testCall(self): + self.assertEqual(self.offset1(self.d), datetime(2014, 7, 1, 11)) + self.assertEqual(self.offset2(self.d), datetime(2014, 7, 1, 11)) + + def testRAdd(self): + self.assertEqual(self.d + self.offset2, self.offset2 + self.d) + + def testSub(self): + off = self.offset2 + self.assertRaises(Exception, off.__sub__, self.d) + self.assertEqual(2 * off - off, off) + + self.assertEqual(self.d - self.offset2, self.d - (2 * off - off)) + + def testRSub(self): + self.assertEqual(self.d - self.offset2, (-self.offset2).apply(self.d)) + + def testMult1(self): + self.assertEqual(self.d + 5 * self.offset1, self.d + self._offset(5)) + + def testMult2(self): + self.assertEqual(self.d + (-3 * self._offset(-2)), + self.d + self._offset(6)) + + def testRollback1(self): + self.assertEqual(self.offset1.rollback(self.d), self.d) + self.assertEqual(self.offset2.rollback(self.d), self.d) + + d = datetime(2014, 7, 1, 0) + # 2014/07/01 is Tuesday, 06/30 is Monday(holiday) + self.assertEqual(self.offset1.rollback(d), datetime(2014, 6, 27, 17)) + + # 2014/6/30 and 2014/6/27 are holidays + self.assertEqual(self.offset2.rollback(d), datetime(2014, 6, 26, 17)) + + def testRollback2(self): + self.assertEqual(self._offset(-3) + .rollback(datetime(2014, 7, 5, 15, 0)), + datetime(2014, 7, 4, 17, 0)) + + def testRollforward1(self): + self.assertEqual(self.offset1.rollforward(self.d), self.d) + self.assertEqual(self.offset2.rollforward(self.d), self.d) + + d = datetime(2014, 7, 1, 0) + self.assertEqual(self.offset1.rollforward(d), datetime(2014, 7, 1, 9)) + self.assertEqual(self.offset2.rollforward(d), datetime(2014, 7, 1, 9)) + + def testRollforward2(self): + self.assertEqual(self._offset(-3) + .rollforward(datetime(2014, 7, 5, 16, 0)), + datetime(2014, 7, 7, 9)) + + def test_roll_date_object(self): + offset = BusinessHour() + + dt = datetime(2014, 7, 6, 15, 0) + + result = offset.rollback(dt) + self.assertEqual(result, datetime(2014, 7, 4, 17)) + + result = offset.rollforward(dt) + self.assertEqual(result, datetime(2014, 7, 7, 9)) + + def test_normalize(self): + tests = [] + + tests.append((CustomBusinessHour(normalize=True, + holidays=self.holidays), + {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 3), + datetime(2014, 7, 1, 16): datetime(2014, 7, 3), + datetime(2014, 7, 1, 23): datetime(2014, 7, 3), + datetime(2014, 7, 1, 0): datetime(2014, 7, 1), + datetime(2014, 7, 4, 15): datetime(2014, 7, 4), + datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7), + datetime(2014, 7, 5, 23): datetime(2014, 7, 7), + datetime(2014, 7, 6, 10): datetime(2014, 7, 7)})) + + tests.append((CustomBusinessHour(-1, normalize=True, + holidays=self.holidays), + {datetime(2014, 7, 1, 8): datetime(2014, 6, 26), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1), + datetime(2014, 7, 1, 10): datetime(2014, 6, 26), + datetime(2014, 7, 1, 0): datetime(2014, 6, 26), + datetime(2014, 7, 7, 10): datetime(2014, 7, 4), + datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7), + datetime(2014, 7, 5, 23): datetime(2014, 7, 4), + datetime(2014, 7, 6, 10): datetime(2014, 7, 4)})) + + tests.append((CustomBusinessHour(1, normalize=True, start='17:00', + end='04:00', holidays=self.holidays), + {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1), + datetime(2014, 7, 1, 23): datetime(2014, 7, 2), + datetime(2014, 7, 2, 2): datetime(2014, 7, 2), + datetime(2014, 7, 2, 3): datetime(2014, 7, 3), + datetime(2014, 7, 4, 23): datetime(2014, 7, 5), + datetime(2014, 7, 5, 2): datetime(2014, 7, 5), + datetime(2014, 7, 7, 2): datetime(2014, 7, 7), + datetime(2014, 7, 7, 17): datetime(2014, 7, 7)})) + + for offset, cases in tests: + for dt, expected in compat.iteritems(cases): + self.assertEqual(offset.apply(dt), expected) + + def test_onOffset(self): + tests = [] + + tests.append((CustomBusinessHour(start='10:00', end='15:00', + holidays=self.holidays), + {datetime(2014, 7, 1, 9): False, + datetime(2014, 7, 1, 10): True, + datetime(2014, 7, 1, 15): True, + datetime(2014, 7, 1, 15, 1): False, + datetime(2014, 7, 5, 12): False, + datetime(2014, 7, 6, 12): False})) + + for offset, cases in tests: + for dt, expected in compat.iteritems(cases): + self.assertEqual(offset.onOffset(dt), expected) + + def test_apply(self): + tests = [] + + tests.append(( + CustomBusinessHour(holidays=self.holidays), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16), + datetime(2014, 7, 1, 19): datetime(2014, 7, 3, 10), + datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 9), + datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 3, 9, 30, 15), + datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 10), + # out of business hours + datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10), + # saturday + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30), + datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, + 30)})) + + tests.append(( + CustomBusinessHour(4, holidays=self.holidays), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 1, 13): datetime(2014, 7, 3, 9), + datetime(2014, 7, 1, 15): datetime(2014, 7, 3, 11), + datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 12), + datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30), + datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, + 30)})) + + for offset, cases in tests: + for base, expected in compat.iteritems(cases): + assertEq(offset, base, expected) + + def test_apply_nanoseconds(self): + tests = [] + + tests.append((CustomBusinessHour(holidays=self.holidays), + {Timestamp('2014-07-01 15:00') + Nano(5): Timestamp( + '2014-07-01 16:00') + Nano(5), + Timestamp('2014-07-01 16:00') + Nano(5): Timestamp( + '2014-07-03 09:00') + Nano(5), + Timestamp('2014-07-01 16:00') - Nano(5): Timestamp( + '2014-07-01 17:00') - Nano(5)})) + + tests.append((CustomBusinessHour(-1, holidays=self.holidays), + {Timestamp('2014-07-01 15:00') + Nano(5): Timestamp( + '2014-07-01 14:00') + Nano(5), + Timestamp('2014-07-01 10:00') + Nano(5): Timestamp( + '2014-07-01 09:00') + Nano(5), + Timestamp('2014-07-01 10:00') - Nano(5): Timestamp( + '2014-06-26 17:00') - Nano(5), })) + + for offset, cases in tests: + for base, expected in compat.iteritems(cases): + assertEq(offset, base, expected) + + class TestCustomBusinessDay(Base): _multiprocess_can_split_ = True _offset = CDay