diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index ac3302ae40fa7..a78fcf5224fc2 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -4,7 +4,7 @@ .. ipython:: python :suppress: - from datetime import datetime, timedelta + from datetime import datetime, timedelta, time import numpy as np np.random.seed(123456) from pandas import * @@ -482,6 +482,7 @@ frequency increment. Specific offset logic like "month", "business day", or BYearEnd, "business year end" BYearBegin, "business year begin" FY5253, "retail (aka 52-53 week) year" + BusinessHour, "business hour" Hour, "one hour" Minute, "one minute" Second, "one second" @@ -667,6 +668,102 @@ in the usual way. have to change to fix the timezone issues, the behaviour of the ``CustomBusinessDay`` class may have to change in future versions. +.. _timeseries.businesshour: + +Business Hour +~~~~~~~~~~~~~ + +The ``BusinessHour`` class provides a business hour representation on ``BusinessDay``, +allowing to use specific start and end times. + +By default, ``BusinessHour`` uses 9:00 - 17:00 as business hours. +Adding ``BusinessHour`` will increment ``Timestamp`` by hourly. +If target ``Timestamp`` is out of business hours, move to the next business hour then increment it. +If the result exceeds the business hours end, remaining is added to the next business day. + +.. ipython:: python + + bh = BusinessHour() + bh + + # 2014-08-01 is Friday + Timestamp('2014-08-01 10:00').weekday() + Timestamp('2014-08-01 10:00') + bh + + # Below example is the same as Timestamp('2014-08-01 09:00') + bh + Timestamp('2014-08-01 08:00') + bh + + # If the results is on the end time, move to the next business day + Timestamp('2014-08-01 16:00') + bh + + # Remainings are added to the next day + Timestamp('2014-08-01 16:30') + bh + + # Adding 2 business hours + Timestamp('2014-08-01 10:00') + BusinessHour(2) + + # Subtracting 3 business hours + Timestamp('2014-08-01 10:00') + BusinessHour(-3) + +Also, you can specify ``start`` and ``end`` time by keywords. +Argument must be ``str`` which has ``hour:minute`` representation or ``datetime.time`` instance. +Specifying seconds, microseconds and nanoseconds as business hour results in ``ValueError``. + +.. ipython:: python + + bh = BusinessHour(start='11:00', end=time(20, 0)) + bh + + Timestamp('2014-08-01 13:00') + bh + Timestamp('2014-08-01 09:00') + bh + Timestamp('2014-08-01 18:00') + bh + +Passing ``start`` time later than ``end`` represents midnight business hour. +In this case, business hour exceeds midnight and overlap to the next day. +Valid business hours are distinguished by whether it started from valid ``BusinessDay``. + +.. ipython:: python + + bh = BusinessHour(start='17:00', end='09:00') + bh + + Timestamp('2014-08-01 17:00') + bh + Timestamp('2014-08-01 23:00') + bh + + # Although 2014-08-02 is Satuaday, + # it is valid because it starts from 08-01 (Friday). + Timestamp('2014-08-02 04:00') + bh + + # Although 2014-08-04 is Monday, + # it is out of business hours because it starts from 08-03 (Sunday). + Timestamp('2014-08-04 04:00') + bh + +Applying ``BusinessHour.rollforward`` and ``rollback`` to out of business hours results in +the next business hour start or previous day's end. Different from other offsets, ``BusinessHour.rollforward`` +may output different results from ``apply`` by definition. + +This is because one day's business hour end is equal to next day's business hour start. For example, +under the default business hours (9:00 - 17:00), there is no gap (0 minutes) between ``2014-08-01 17:00`` and +``2014-08-04 09:00``. + +.. ipython:: python + + # This adjusts a Timestamp to business hour edge + BusinessHour().rollback(Timestamp('2014-08-02 15:00')) + BusinessHour().rollforward(Timestamp('2014-08-02 15:00')) + + # It is the same as BusinessHour().apply(Timestamp('2014-08-01 17:00')). + # And it is the same as BusinessHour().apply(Timestamp('2014-08-04 09:00')) + BusinessHour().apply(Timestamp('2014-08-02 15:00')) + + # BusinessDay results (for reference) + BusinessHour().rollforward(Timestamp('2014-08-02')) + + # It is the same as BusinessDay().apply(Timestamp('2014-08-01')) + # The result is the same as rollworward because BusinessDay never overlap. + BusinessHour().apply(Timestamp('2014-08-02')) + + Offset Aliases ~~~~~~~~~~~~~~ @@ -696,6 +793,7 @@ frequencies. We will refer to these aliases as *offset aliases* "BA", "business year end frequency" "AS", "year start frequency" "BAS", "business year start frequency" + "BH", "business hour frequency" "H", "hourly frequency" "T", "minutely frequency" "S", "secondly frequency" diff --git a/doc/source/whatsnew/v0.16.1.txt b/doc/source/whatsnew/v0.16.1.txt index 3d5c95aee2e92..932a5ae86b219 100755 --- a/doc/source/whatsnew/v0.16.1.txt +++ b/doc/source/whatsnew/v0.16.1.txt @@ -11,16 +11,25 @@ Highlights include: - Support for a ``CategoricalIndex``, a category based index, see :ref:`here ` +- ``BusinessHour`` offset is supported, see :ref:`here ` + .. contents:: What's new in v0.16.1 :local: :backlinks: none - .. _whatsnew_0161.enhancements: Enhancements ~~~~~~~~~~~~ +- ``BusinessHour`` offset is now supported, which represents business hours starting from 09:00 - 17:00 on ``BusinessDay`` by default. See :ref:`Here ` for details. (:issue:`7905`) + + .. ipython:: python + + Timestamp('2014-08-01 09:00') + BusinessHour() + Timestamp('2014-08-01 07:00') + BusinessHour() + Timestamp('2014-08-01 16:30') + BusinessHour() + - Added ``StringMethods.capitalize()`` and ``swapcase`` which behave as the same as standard ``str`` (:issue:`9766`) - Added ``StringMethods`` (.str accessor) to ``Index`` (:issue:`9068`) diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index b220e03fdb327..eff2a36e823d8 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -742,7 +742,7 @@ def __init__(self, index, warn=True): @cache_readonly def deltas(self): return tslib.unique_deltas(self.values) - + @cache_readonly def deltas_asi8(self): return tslib.unique_deltas(self.index.asi8) @@ -750,7 +750,7 @@ def deltas_asi8(self): @cache_readonly def is_unique(self): return len(self.deltas) == 1 - + @cache_readonly def is_unique_asi8(self): return len(self.deltas_asi8) == 1 @@ -763,10 +763,13 @@ def get_freq(self): if _is_multiple(delta, _ONE_DAY): return self._infer_daily_rule() else: - # Possibly intraday frequency. Here we use the + # Business hourly, maybe. 17: one day / 65: one weekend + if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]): + return 'BH' + # Possibly intraday frequency. Here we use the # original .asi8 values as the modified values # will not work around DST transitions. See #8772 - if not self.is_unique_asi8: + elif not self.is_unique_asi8: return None delta = self.deltas_asi8[0] if _is_multiple(delta, _ONE_HOUR): @@ -792,6 +795,10 @@ def get_freq(self): def day_deltas(self): return [x / _ONE_DAY for x in self.deltas] + @cache_readonly + def hour_deltas(self): + return [x / _ONE_HOUR for x in self.deltas] + @cache_readonly def fields(self): return tslib.build_field_sarray(self.values) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index cb6bd2fb2b250..67e27bbffbf73 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -16,6 +16,7 @@ __all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay', 'CBMonthEnd','CBMonthBegin', 'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd', + 'BusinessHour', 'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd', 'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd', 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253', @@ -404,10 +405,6 @@ def __repr__(self): if hasattr(self, '_named'): return self._named className = getattr(self, '_outputName', self.__class__.__name__) - attrs = [] - - if self.offset: - attrs = ['offset=%s' % repr(self.offset)] if abs(self.n) != 1: plural = 's' @@ -418,10 +415,17 @@ def __repr__(self): if self.n != 1: n_str = "%s * " % self.n - out = '<%s' % n_str + className + plural + out = '<%s' % n_str + className + plural + self._repr_attrs() + '>' + return out + + def _repr_attrs(self): + if self.offset: + attrs = ['offset=%s' % repr(self.offset)] + else: + attrs = None + out = '' if attrs: out += ': ' + ', '.join(attrs) - out += '>' return out class BusinessDay(BusinessMixin, SingleConstructorOffset): @@ -531,6 +535,234 @@ def onOffset(self, dt): return dt.weekday() < 5 +class BusinessHour(BusinessMixin, SingleConstructorOffset): + """ + DateOffset subclass representing possibly n business days + """ + _prefix = 'BH' + _anchor = 0 + + def __init__(self, n=1, normalize=False, **kwds): + self.n = int(n) + self.normalize = normalize + + # 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')) + self.kwds = kwds + self.offset = kwds.get('offset', timedelta(0)) + 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 + if isinstance(t_input, compat.string_types): + try: + t = time.strptime(t_input, '%H:%M') + return dt_time(hour=t.tm_hour, minute=t.tm_min) + except ValueError: + raise ValueError("time data must match '%H:%M' format") + elif isinstance(t_input, dt_time): + if t_input.second != 0 or t_input.microsecond != 0: + raise ValueError("time data must be specified only with hour and minute") + return t_input + else: + raise ValueError("time data must be string or datetime.time") + + def _get_daytime_flag(self): + if self.start == self.end: + raise ValueError('start and end must not be the same') + elif self.start < self.end: + return True + 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. + Otherwise yesterday's business day's opening time. + + Opening time always locates on BusinessDay. + Otherwise, closing time may not if business hour extends over midnight. + """ + if not self.next_bday.onOffset(other): + other = other + self.next_bday + else: + if self.n >= 0 and self.start < other.time(): + other = other + self.next_bday + elif self.n < 0 and other.time() < self.start: + other = other + self.next_bday + return datetime(other.year, other.month, other.day, + self.start.hour, self.start.minute) + + def _prev_opening_time(self, other): + """ + If n is positive, return yesterday's business day opening time. + Otherwise yesterday business day's opening time. + """ + if not self.next_bday.onOffset(other): + other = other - self.next_bday + else: + if self.n >= 0 and other.time() < self.start: + other = other - self.next_bday + elif self.n < 0 and other.time() > self.start: + other = other - self.next_bday + return datetime(other.year, other.month, other.day, + self.start.hour, self.start.minute) + + def _get_business_hours_by_sec(self): + """ + Return business hours in a day by seconds. + """ + if self._get_daytime_flag(): + # create dummy datetime to calcurate businesshours in a day + dtstart = datetime(2014, 4, 1, self.start.hour, self.start.minute) + until = datetime(2014, 4, 1, self.end.hour, self.end.minute) + return tslib.tot_seconds(until - dtstart) + else: + self.daytime = False + dtstart = datetime(2014, 4, 1, self.start.hour, self.start.minute) + until = datetime(2014, 4, 2, self.end.hour, self.end.minute) + return tslib.tot_seconds(until - dtstart) + + @apply_wraps + def rollback(self, dt): + """Roll provided date backward to next offset only if not on offset""" + if not self.onOffset(dt): + businesshours = self._get_business_hours_by_sec() + if self.n >= 0: + dt = self._prev_opening_time(dt) + timedelta(seconds=businesshours) + else: + dt = self._next_opening_time(dt) + timedelta(seconds=businesshours) + return dt + + @apply_wraps + def rollforward(self, dt): + """Roll provided date forward to next offset only if not on offset""" + if not self.onOffset(dt): + if self.n >= 0: + return self._next_opening_time(dt) + else: + return self._prev_opening_time(dt) + return dt + + @apply_wraps + def apply(self, other): + # calcurate here because offset is not immutable + daytime = self._get_daytime_flag() + businesshours = self._get_business_hours_by_sec() + bhdelta = timedelta(seconds=businesshours) + + if isinstance(other, datetime): + # used for detecting edge condition + nanosecond = getattr(other, 'nanosecond', 0) + # reset timezone and nanosecond + # other may be a Timestamp, thus not use replace + other = datetime(other.year, other.month, other.day, + other.hour, other.minute, + other.second, other.microsecond) + n = self.n + if n >= 0: + if (other.time() == self.end or + not self._onOffset(other, businesshours)): + other = self._next_opening_time(other) + else: + if other.time() == self.start: + # adjustment to move to previous business day + other = other - timedelta(seconds=1) + if not self._onOffset(other, businesshours): + other = self._next_opening_time(other) + other = other + bhdelta + + bd, r = divmod(abs(n * 60), businesshours // 60) + if n < 0: + bd, r = -bd, -r + + if bd != 0: + skip_bd = BusinessDay(n=bd) + # midnight busienss hour may not on BusinessDay + if not self.next_bday.onOffset(other): + remain = other - self._prev_opening_time(other) + other = self._next_opening_time(other + skip_bd) + remain + else: + other = other + skip_bd + + hours, minutes = divmod(r, 60) + result = other + timedelta(hours=hours, minutes=minutes) + + # because of previous adjustment, time will be larger than start + if ((daytime and (result.time() < self.start or self.end < result.time())) or + not daytime and (self.end < result.time() < self.start)): + if n >= 0: + bday_edge = self._prev_opening_time(other) + bday_edge = bday_edge + bhdelta + # calcurate remainder + bday_remain = result - bday_edge + result = self._next_opening_time(other) + result += bday_remain + else: + bday_edge = self._next_opening_time(other) + bday_remain = result - bday_edge + result = self._next_opening_time(result) + bhdelta + result += bday_remain + # edge handling + if n >= 0: + if result.time() == self.end: + result = self._next_opening_time(result) + else: + if result.time() == self.start and nanosecond == 0: + # adjustment to move to previous business day + result = self._next_opening_time(result- timedelta(seconds=1)) +bhdelta + + return result + else: + raise ApplyTypeError('Only know how to combine business hour with ') + + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + + if dt.tzinfo is not None: + dt = datetime(dt.year, dt.month, dt.day, dt.hour, + dt.minute, dt.second, dt.microsecond) + # Valid BH can be on the different BusinessDay during midnight + # Distinguish by the time spent from previous opening time + businesshours = self._get_business_hours_by_sec() + return self._onOffset(dt, businesshours) + + def _onOffset(self, dt, businesshours): + """ + Slight speedups using calcurated values + """ + # if self.normalize and not _is_normalized(dt): + # return False + # Valid BH can be on the different BusinessDay during midnight + # Distinguish by the time spent from previous opening time + if self.n >= 0: + op = self._prev_opening_time(dt) + else: + op = self._next_opening_time(dt) + span = tslib.tot_seconds(dt - op) + if span <= businesshours: + return True + else: + return False + + class CustomBusinessDay(BusinessDay): """ **EXPERIMENTAL** DateOffset subclass representing possibly n business days @@ -2250,6 +2482,7 @@ def generate_range(start=None, end=None, periods=None, BusinessMonthEnd, # 'BM' BQuarterEnd, # 'BQ' BQuarterBegin, # 'BQS' + BusinessHour, # 'BH' CustomBusinessDay, # 'C' CustomBusinessMonthEnd, # 'CBM' CustomBusinessMonthBegin, # 'CBMS' diff --git a/pandas/tseries/tests/test_frequencies.py b/pandas/tseries/tests/test_frequencies.py index 965c198eb7c95..2f2d249539b81 100644 --- a/pandas/tseries/tests/test_frequencies.py +++ b/pandas/tseries/tests/test_frequencies.py @@ -196,6 +196,7 @@ def _check_tick(self, base_delta, code): index = _dti([b + base_delta * j for j in range(3)] + [b + base_delta * 7]) + self.assertIsNone(frequencies.infer_freq(index)) def test_weekly(self): @@ -324,10 +325,40 @@ def test_infer_freq_tz_transition(self): idx = date_range(date_pair[0], date_pair[1], freq=freq, tz=tz) print(idx) self.assertEqual(idx.inferred_freq, freq) - + index = date_range("2013-11-03", periods=5, freq="3H").tz_localize("America/Chicago") self.assertIsNone(index.inferred_freq) + def test_infer_freq_businesshour(self): + # GH 7905 + idx = DatetimeIndex(['2014-07-01 09:00', '2014-07-01 10:00', '2014-07-01 11:00', + '2014-07-01 12:00', '2014-07-01 13:00', '2014-07-01 14:00']) + # hourly freq in a day must result in 'H' + self.assertEqual(idx.inferred_freq, 'H') + + idx = DatetimeIndex(['2014-07-01 09:00', '2014-07-01 10:00', '2014-07-01 11:00', + '2014-07-01 12:00', '2014-07-01 13:00', '2014-07-01 14:00', + '2014-07-01 15:00', '2014-07-01 16:00', + '2014-07-02 09:00', '2014-07-02 10:00', '2014-07-02 11:00']) + self.assertEqual(idx.inferred_freq, 'BH') + + idx = DatetimeIndex(['2014-07-04 09:00', '2014-07-04 10:00', '2014-07-04 11:00', + '2014-07-04 12:00', '2014-07-04 13:00', '2014-07-04 14:00', + '2014-07-04 15:00', '2014-07-04 16:00', + '2014-07-07 09:00', '2014-07-07 10:00', '2014-07-07 11:00']) + self.assertEqual(idx.inferred_freq, 'BH') + + idx = DatetimeIndex(['2014-07-04 09:00', '2014-07-04 10:00', '2014-07-04 11:00', + '2014-07-04 12:00', '2014-07-04 13:00', '2014-07-04 14:00', + '2014-07-04 15:00', '2014-07-04 16:00', + '2014-07-07 09:00', '2014-07-07 10:00', '2014-07-07 11:00', + '2014-07-07 12:00', '2014-07-07 13:00', '2014-07-07 14:00', + '2014-07-07 15:00', '2014-07-07 16:00', + '2014-07-08 09:00', '2014-07-08 10:00', '2014-07-08 11:00', + '2014-07-08 12:00', '2014-07-08 13:00', '2014-07-08 14:00', + '2014-07-08 15:00', '2014-07-08 16:00']) + self.assertEqual(idx.inferred_freq, 'BH') + def test_not_monotonic(self): rng = _dti(['1/31/2000', '1/31/2001', '1/31/2002']) rng = rng[::-1] diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py index 0793508b4912c..a051560617604 100644 --- a/pandas/tseries/tests/test_offsets.py +++ b/pandas/tseries/tests/test_offsets.py @@ -10,7 +10,7 @@ import numpy as np from pandas.core.datetools import ( - bday, BDay, CDay, BQuarterEnd, BMonthEnd, + bday, BDay, CDay, BQuarterEnd, BMonthEnd, BusinessHour, CBMonthEnd, CBMonthBegin, BYearEnd, MonthEnd, MonthBegin, BYearBegin, CustomBusinessDay, QuarterBegin, BQuarterBegin, BMonthBegin, DateOffset, Week, @@ -23,7 +23,6 @@ from pandas.tseries.index import _to_m8, DatetimeIndex, _daterange_cache, date_range from pandas.tseries.tools import parse_time_string, DateParseError import pandas.tseries.offsets as offsets - from pandas.io.pickle import read_pickle from pandas.tslib import NaT, Timestamp, Timedelta import pandas.tslib as tslib @@ -133,7 +132,11 @@ 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: - offset = self._get_offset(self._offset, value=10000) + if self._offset is BusinessHour: + # Using 10000 in BusinessHour fails in tz check because of DST difference + offset = self._get_offset(self._offset, value=100000) + else: + offset = self._get_offset(self._offset, value=10000) result = Timestamp('20080101') + offset self.assertIsInstance(result, datetime) @@ -179,6 +182,7 @@ def setUp(self): 'BQuarterBegin': Timestamp('2011-03-01 09:00:00'), 'QuarterEnd': Timestamp('2011-03-31 09:00:00'), 'BQuarterEnd': Timestamp('2011-03-31 09:00:00'), + 'BusinessHour': 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'), @@ -278,6 +282,8 @@ def test_rollforward(self): for n in no_changes: expecteds[n] = Timestamp('2011/01/01 09:00') + expecteds['BusinessHour'] = Timestamp('2011-01-03 09:00:00') + # but be changed when normalize=True norm_expected = expecteds.copy() for k in norm_expected: @@ -321,6 +327,7 @@ def test_rollback(self): 'BQuarterBegin': Timestamp('2010-12-01 09:00:00'), 'QuarterEnd': Timestamp('2010-12-31 09:00:00'), 'BQuarterEnd': Timestamp('2010-12-31 09:00:00'), + 'BusinessHour': 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'), @@ -371,6 +378,10 @@ def test_onOffset(self): offset_n = self._get_offset(offset, normalize=True) self.assertFalse(offset_n.onOffset(dt)) + if offset is BusinessHour: + # In default BusinessHour (9:00-17:00), normalized time + # cannot be in business hour range + continue date = datetime(dt.year, dt.month, dt.day) self.assertTrue(offset_n.onOffset(date)) @@ -642,6 +653,593 @@ def test_offsets_compare_equal(self): self.assertFalse(offset1 != offset2) +class TestBusinessHour(Base): + _multiprocess_can_split_ = True + _offset = BusinessHour + + def setUp(self): + self.d = datetime(2014, 7, 1, 10, 00) + + self.offset1 = BusinessHour() + self.offset2 = BusinessHour(n=3) + + self.offset3 = BusinessHour(n=-1) + self.offset4 = BusinessHour(n=-4) + + from datetime import time as dt_time + self.offset5 = BusinessHour(start=dt_time(11, 0), end=dt_time(14, 30)) + self.offset6 = BusinessHour(start='20:00', end='05:00') + self.offset7 = BusinessHour(n=-2, start=dt_time(21, 30), end=dt_time(6, 30)) + + def test_constructor_errors(self): + from datetime import time as dt_time + with tm.assertRaises(ValueError): + BusinessHour(start=dt_time(11, 0, 5)) + with tm.assertRaises(ValueError): + BusinessHour(start='AAA') + with tm.assertRaises(ValueError): + BusinessHour(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), '<3 * BusinessHours: BH=09:00-17:00>') + self.assertEqual(repr(self.offset3), '<-1 * BusinessHour: BH=09:00-17:00>') + self.assertEqual(repr(self.offset4), '<-4 * BusinessHours: BH=09:00-17:00>') + + self.assertEqual(repr(self.offset5), '') + self.assertEqual(repr(self.offset6), '') + self.assertEqual(repr(self.offset7), '<-2 * BusinessHours: BH=21:30-06:30>') + + def test_with_offset(self): + expected = Timestamp('2014-07-01 13:00') + + self.assertEqual(self.d + BusinessHour() * 3, expected) + self.assertEqual(self.d + BusinessHour(n=3), expected) + + def testEQ(self): + for offset in [self.offset1, self.offset2, self.offset3, self.offset4]: + self.assertEqual(offset, offset) + + self.assertNotEqual(BusinessHour(), BusinessHour(-1)) + self.assertEqual(BusinessHour(start='09:00'), BusinessHour()) + self.assertNotEqual(BusinessHour(start='09:00'), BusinessHour(start='09:01')) + self.assertNotEqual(BusinessHour(start='09:00', end='17:00'), + BusinessHour(start='17:00', end='09:01')) + + def test_hash(self): + 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, 13)) + self.assertEqual(self.offset3(self.d), datetime(2014, 6, 30, 17)) + self.assertEqual(self.offset4(self.d), datetime(2014, 6, 30, 14)) + + 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 + self._offset(-3)) + + 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) + self.assertEqual(self.offset3.rollback(self.d), self.d) + self.assertEqual(self.offset4.rollback(self.d), self.d) + self.assertEqual(self.offset5.rollback(self.d), datetime(2014, 6, 30, 14, 30)) + self.assertEqual(self.offset6.rollback(self.d), datetime(2014, 7, 1, 5, 0)) + self.assertEqual(self.offset7.rollback(self.d), datetime(2014, 7, 1, 6, 30)) + + d = datetime(2014, 7, 1, 0) + self.assertEqual(self.offset1.rollback(d), datetime(2014, 6, 30, 17)) + self.assertEqual(self.offset2.rollback(d), datetime(2014, 6, 30, 17)) + self.assertEqual(self.offset3.rollback(d), datetime(2014, 6, 30, 17)) + self.assertEqual(self.offset4.rollback(d), datetime(2014, 6, 30, 17)) + self.assertEqual(self.offset5.rollback(d), datetime(2014, 6, 30, 14, 30)) + self.assertEqual(self.offset6.rollback(d), d) + self.assertEqual(self.offset7.rollback(d), d) + + self.assertEqual(self._offset(5).rollback(self.d), self.d) + + 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) + self.assertEqual(self.offset3.rollforward(self.d), self.d) + self.assertEqual(self.offset4.rollforward(self.d), self.d) + self.assertEqual(self.offset5.rollforward(self.d), datetime(2014, 7, 1, 11, 0)) + self.assertEqual(self.offset6.rollforward(self.d), datetime(2014, 7, 1, 20, 0)) + self.assertEqual(self.offset7.rollforward(self.d), datetime(2014, 7, 1, 21, 30)) + + 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)) + self.assertEqual(self.offset3.rollforward(d), datetime(2014, 7, 1, 9)) + self.assertEqual(self.offset4.rollforward(d), datetime(2014, 7, 1, 9)) + self.assertEqual(self.offset5.rollforward(d), datetime(2014, 7, 1, 11)) + self.assertEqual(self.offset6.rollforward(d), d) + self.assertEqual(self.offset7.rollforward(d), d) + + self.assertEqual(self._offset(5).rollforward(self.d), self.d) + + 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((BusinessHour(normalize=True), + {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 2), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2), + datetime(2014, 7, 1, 23): datetime(2014, 7, 2), + 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((BusinessHour(-1, normalize=True), + {datetime(2014, 7, 1, 8): datetime(2014, 6, 30), + 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, 30), + datetime(2014, 7, 1, 0): datetime(2014, 6, 30), + 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((BusinessHour(1, normalize=True, start='17:00', end='04:00'), + {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, 2), + 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((BusinessHour(), + {datetime(2014, 7, 1, 9): True, + datetime(2014, 7, 1, 8, 59): False, + datetime(2014, 7, 1, 8): False, + datetime(2014, 7, 1, 17): True, + datetime(2014, 7, 1, 17, 1): False, + datetime(2014, 7, 1, 18): False, + datetime(2014, 7, 5, 9): False, + datetime(2014, 7, 6, 12): False})) + + tests.append((BusinessHour(start='10:00', end='15:00'), + {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})) + + tests.append((BusinessHour(start='19:00', end='05:00'), + {datetime(2014, 7, 1, 9, 0): False, + datetime(2014, 7, 1, 10, 0): False, + datetime(2014, 7, 1, 15): False, + datetime(2014, 7, 1, 15, 1): False, + datetime(2014, 7, 5, 12, 0): False, + datetime(2014, 7, 6, 12, 0): False, + datetime(2014, 7, 1, 19, 0): True, + datetime(2014, 7, 2, 0, 0): True, + datetime(2014, 7, 4, 23): True, + datetime(2014, 7, 5, 1): True, + datetime(2014, 7, 5, 5, 0): True, + datetime(2014, 7, 6, 23, 0): False, + datetime(2014, 7, 7, 3, 0): False})) + + for offset, cases in tests: + for dt, expected in compat.iteritems(cases): + self.assertEqual(offset.onOffset(dt), expected) + + def test_opening_time(self): + tests = [] + + # opening time should be affected by sign of n, not by n's value and end + tests.append(([BusinessHour(), BusinessHour(n=2), BusinessHour(n=4), + BusinessHour(end='10:00'), BusinessHour(n=2, end='4:00'), + BusinessHour(n=4, end='15:00')], + {datetime(2014, 7, 1, 11): (datetime(2014, 7, 2, 9), datetime(2014, 7, 1, 9)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 9), datetime(2014, 7, 1, 9)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 9), datetime(2014, 7, 1, 9)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 9), datetime(2014, 7, 1, 9)), + # if timestamp is on opening time, next opening time is as it is + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 9), datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 10): (datetime(2014, 7, 3, 9), datetime(2014, 7, 2, 9)), + # 2014-07-05 is saturday + datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 9), datetime(2014, 7, 4, 9)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 7, 9), datetime(2014, 7, 4, 9)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 9), datetime(2014, 7, 4, 9)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 9), datetime(2014, 7, 4, 9)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 9), datetime(2014, 7, 4, 9)), + datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 8, 9), datetime(2014, 7, 7, 9))})) + + tests.append(([BusinessHour(start='11:15'), BusinessHour(n=2, start='11:15'), + BusinessHour(n=3, start='11:15'), + BusinessHour(start='11:15', end='10:00'), + BusinessHour(n=2, start='11:15', end='4:00'), + BusinessHour(n=3, start='11:15', end='15:00')], + {datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 11, 15), datetime(2014, 6, 30, 11, 15)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 10): (datetime(2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 11, 15): (datetime(2014, 7, 2, 11, 15), datetime(2014, 7, 2, 11, 15)), + datetime(2014, 7, 2, 11, 15, 1): (datetime(2014, 7, 3, 11, 15), datetime(2014, 7, 2, 11, 15)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 11, 15), datetime(2014, 7, 3, 11, 15)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15))})) + + tests.append(([BusinessHour(-1), BusinessHour(n=-2), BusinessHour(n=-4), + BusinessHour(n=-1, end='10:00'), BusinessHour(n=-2, end='4:00'), + BusinessHour(n=-4, end='15:00')], + {datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 9), datetime(2014, 7, 2, 9)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 1, 9), datetime(2014, 7, 2, 9)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 1, 9), datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 1, 9), datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 9), datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 10): (datetime(2014, 7, 2, 9), datetime(2014, 7, 3, 9)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 4, 9), datetime(2014, 7, 7, 9)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 9), datetime(2014, 7, 7, 9)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 4, 9), datetime(2014, 7, 7, 9)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 4, 9), datetime(2014, 7, 7, 9)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 4, 9), datetime(2014, 7, 7, 9)), + datetime(2014, 7, 7, 9): (datetime(2014, 7, 7, 9), datetime(2014, 7, 7, 9)), + datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 7, 9), datetime(2014, 7, 8, 9))})) + + tests.append(([BusinessHour(start='17:00', end='05:00'), + BusinessHour(n=3, start='17:00', end='03:00')], + {datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 17), datetime(2014, 6, 30, 17)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 17), datetime(2014, 7, 1, 17)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 17), datetime(2014, 7, 1, 17)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 17), datetime(2014, 7, 1, 17)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 17), datetime(2014, 7, 1, 17)), + datetime(2014, 7, 4, 17): (datetime(2014, 7, 4, 17), datetime(2014, 7, 4, 17)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 17), datetime(2014, 7, 4, 17)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 17), datetime(2014, 7, 3, 17)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 17), datetime(2014, 7, 4, 17)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 17), datetime(2014, 7, 4, 17)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 17), datetime(2014, 7, 4, 17)), + datetime(2014, 7, 7, 17, 1): (datetime(2014, 7, 8, 17), datetime(2014, 7, 7, 17)),})) + + tests.append(([BusinessHour(-1, start='17:00', end='05:00'), + BusinessHour(n=-2, start='17:00', end='03:00')], + {datetime(2014, 7, 1, 11): (datetime(2014, 6, 30, 17), datetime(2014, 7, 1, 17)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 1, 17), datetime(2014, 7, 2, 17)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 1, 17), datetime(2014, 7, 2, 17)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 1, 17), datetime(2014, 7, 2, 17)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 1, 17), datetime(2014, 7, 2, 17)), + datetime(2014, 7, 2, 16, 59): (datetime(2014, 7, 1, 17), datetime(2014, 7, 2, 17)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 4, 17), datetime(2014, 7, 7, 17)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 3, 17), datetime(2014, 7, 4, 17)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 4, 17), datetime(2014, 7, 7, 17)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 4, 17), datetime(2014, 7, 7, 17)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 4, 17), datetime(2014, 7, 7, 17)), + datetime(2014, 7, 7, 18): (datetime(2014, 7, 7, 17), datetime(2014, 7, 8, 17))})) + + for offsets, cases in tests: + for offset in offsets: + for dt, (exp_next, exp_prev) in compat.iteritems(cases): + self.assertEqual(offset._next_opening_time(dt), exp_next) + self.assertEqual(offset._prev_opening_time(dt), exp_prev) + + def test_apply(self): + tests = [] + + tests.append((BusinessHour(), + {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, 2, 10), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 2, 9, 30, 15), + datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 10), + datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 12), + # out of business hours + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 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((BusinessHour(4), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 1, 13): datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 11), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 12), + datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 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)})) + + tests.append((BusinessHour(-1), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 10), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 15), + datetime(2014, 7, 1, 10): datetime(2014, 6, 30, 17), + datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 1, 15, 30, 15), + datetime(2014, 7, 1, 9, 30, 15): datetime(2014, 6, 30, 16, 30, 15), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 16), + datetime(2014, 7, 1, 5): datetime(2014, 6, 30, 16), + datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 10), + # out of business hours + datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 16), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 16), + datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 16), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 16), + # saturday + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 16), + datetime(2014, 7, 7, 9): datetime(2014, 7, 4, 16), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 16, 30), + datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 16, 30, 30)})) + + tests.append((BusinessHour(-4), + {datetime(2014, 7, 1, 11): datetime(2014, 6, 30, 15), + datetime(2014, 7, 1, 13): datetime(2014, 6, 30, 17), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 11), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 13), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13), + datetime(2014, 7, 4, 18): datetime(2014, 7, 4, 13), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 13, 30), + datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 13, 30, 30)})) + + tests.append((BusinessHour(start='13:00', end='16:00'), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 13), + datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 14), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 14), + datetime(2014, 7, 1, 15, 30, 15): datetime(2014, 7, 2, 13, 30, 15), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 14), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 14)})) + + tests.append((BusinessHour(n=2, start='13:00', end='16:00'), + {datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 14): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 15), + datetime(2014, 7, 2, 14, 30): datetime(2014, 7, 3, 13, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 15), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 15), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 15), + datetime(2014, 7, 4, 14, 30): datetime(2014, 7, 7, 13, 30), + datetime(2014, 7, 4, 14, 30, 30): datetime(2014, 7, 7, 13, 30, 30)})) + + tests.append((BusinessHour(n=-1, start='13:00', end='16:00'), + {datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 15), + datetime(2014, 7, 2, 14): datetime(2014, 7, 1, 16), + datetime(2014, 7, 2, 15): datetime(2014, 7, 2, 14), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 16): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 13, 30, 15): datetime(2014, 7, 1, 15, 30, 15), + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 15), + datetime(2014, 7, 7, 11): datetime(2014, 7, 4, 15)})) + + tests.append((BusinessHour(n=-3, start='10:00', end='16:00'), + {datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 11), + datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 16), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 11, 30): datetime(2014, 7, 1, 14, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13), + datetime(2014, 7, 4, 10): datetime(2014, 7, 3, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13), + datetime(2014, 7, 4, 16): datetime(2014, 7, 4, 13), + datetime(2014, 7, 4, 12, 30): datetime(2014, 7, 3, 15, 30), + datetime(2014, 7, 4, 12, 30, 30): datetime(2014, 7, 3, 15, 30, 30)})) + + tests.append((BusinessHour(start='19:00', end='05:00'), + {datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 20), + datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 4, 30): datetime(2014, 7, 2, 19, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 1), + datetime(2014, 7, 4, 10): datetime(2014, 7, 4, 20), + datetime(2014, 7, 4, 23): datetime(2014, 7, 5, 0), + datetime(2014, 7, 5, 0): datetime(2014, 7, 5, 1), + datetime(2014, 7, 5, 4): datetime(2014, 7, 7, 19), + datetime(2014, 7, 5, 4, 30): datetime(2014, 7, 7, 19, 30), + datetime(2014, 7, 5, 4, 30, 30): datetime(2014, 7, 7, 19, 30, 30)})) + + tests.append((BusinessHour(n=-1, start='19:00', end='05:00'), + {datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 4), + datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 20): datetime(2014, 7, 2, 5), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 19, 30): datetime(2014, 7, 2, 4, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 23), + datetime(2014, 7, 3, 6): datetime(2014, 7, 3, 4), + datetime(2014, 7, 4, 23): datetime(2014, 7, 4, 22), + datetime(2014, 7, 5, 0): datetime(2014, 7, 4, 23), + datetime(2014, 7, 5, 4): datetime(2014, 7, 5, 3), + datetime(2014, 7, 7, 19, 30): datetime(2014, 7, 5, 4, 30), + datetime(2014, 7, 7, 19, 30, 30): datetime(2014, 7, 5, 4, 30, 30)})) + + for offset, cases in tests: + for base, expected in compat.iteritems(cases): + assertEq(offset, base, expected) + + def test_apply_large_n(self): + tests = [] + + tests.append((BusinessHour(40), # A week later + {datetime(2014, 7, 1, 11): datetime(2014, 7, 8, 11), + datetime(2014, 7, 1, 13): datetime(2014, 7, 8, 13), + datetime(2014, 7, 1, 15): datetime(2014, 7, 8, 15), + datetime(2014, 7, 1, 16): datetime(2014, 7, 8, 16), + datetime(2014, 7, 1, 17): datetime(2014, 7, 9, 9), + datetime(2014, 7, 2, 11): datetime(2014, 7, 9, 11), + datetime(2014, 7, 2, 8): datetime(2014, 7, 9, 9), + datetime(2014, 7, 2, 19): datetime(2014, 7, 10, 9), + datetime(2014, 7, 2, 23): datetime(2014, 7, 10, 9), + datetime(2014, 7, 3, 0): datetime(2014, 7, 10, 9), + datetime(2014, 7, 5, 15): datetime(2014, 7, 14, 9), + datetime(2014, 7, 4, 18): datetime(2014, 7, 14, 9), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 14, 9, 30), + datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 14, 9, 30, 30)})) + + tests.append((BusinessHour(-25), # 3 days and 1 hour before + {datetime(2014, 7, 1, 11): datetime(2014, 6, 26, 10), + datetime(2014, 7, 1, 13): datetime(2014, 6, 26, 12), + datetime(2014, 7, 1, 9): datetime(2014, 6, 25, 16), + datetime(2014, 7, 1, 10): datetime(2014, 6, 25, 17), + datetime(2014, 7, 3, 11): datetime(2014, 6, 30, 10), + datetime(2014, 7, 3, 8): datetime(2014, 6, 27, 16), + datetime(2014, 7, 3, 19): datetime(2014, 6, 30, 16), + datetime(2014, 7, 3, 23): datetime(2014, 6, 30, 16), + datetime(2014, 7, 4, 9): datetime(2014, 6, 30, 16), + datetime(2014, 7, 5, 15): datetime(2014, 7, 1, 16), + datetime(2014, 7, 6, 18): datetime(2014, 7, 1, 16), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 1, 16, 30), + datetime(2014, 7, 7, 10, 30, 30): datetime(2014, 7, 2, 9, 30, 30)})) + + tests.append((BusinessHour(28, start='21:00', end='02:00'), # 5 days and 3 hours later + {datetime(2014, 7, 1, 11): datetime(2014, 7, 9, 0), + datetime(2014, 7, 1, 22): datetime(2014, 7, 9, 1), + datetime(2014, 7, 1, 23): datetime(2014, 7, 9, 21), + datetime(2014, 7, 2, 2): datetime(2014, 7, 10, 0), + datetime(2014, 7, 3, 21): datetime(2014, 7, 11, 0), + datetime(2014, 7, 4, 1): datetime(2014, 7, 11, 23), + datetime(2014, 7, 4, 2): datetime(2014, 7, 12, 0), + datetime(2014, 7, 4, 3): datetime(2014, 7, 12, 0), + datetime(2014, 7, 5, 1): datetime(2014, 7, 14, 23), + datetime(2014, 7, 5, 15): datetime(2014, 7, 15, 0), + datetime(2014, 7, 6, 18): datetime(2014, 7, 15, 0), + datetime(2014, 7, 7, 1): datetime(2014, 7, 15, 0), + datetime(2014, 7, 7, 23, 30): datetime(2014, 7, 15, 21, 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((BusinessHour(), + {Timestamp('2014-07-04 15:00') + Nano(5): Timestamp('2014-07-04 16:00') + Nano(5), + Timestamp('2014-07-04 16:00') + Nano(5): Timestamp('2014-07-07 09:00') + Nano(5), + Timestamp('2014-07-04 16:00') - Nano(5): Timestamp('2014-07-04 17:00') - Nano(5) + })) + + tests.append((BusinessHour(-1), + {Timestamp('2014-07-04 15:00') + Nano(5): Timestamp('2014-07-04 14:00') + Nano(5), + Timestamp('2014-07-04 10:00') + Nano(5): Timestamp('2014-07-04 09:00') + Nano(5), + Timestamp('2014-07-04 10:00') - Nano(5): Timestamp('2014-07-03 17:00') - Nano(5), + })) + + for offset, cases in tests: + for base, expected in compat.iteritems(cases): + assertEq(offset, base, expected) + + def test_offsets_compare_equal(self): + # root cause of #456 + offset1 = self._offset() + offset2 = self._offset() + self.assertFalse(offset1 != offset2) + + def test_datetimeindex(self): + idx1 = DatetimeIndex(start='2014-07-04 15:00', end='2014-07-08 10:00', freq='BH') + idx2 = DatetimeIndex(start='2014-07-04 15:00', periods=12, freq='BH') + idx3 = DatetimeIndex(end='2014-07-08 10:00', periods=12, freq='BH') + expected = DatetimeIndex(['2014-07-04 15:00', '2014-07-04 16:00', '2014-07-07 09:00', + '2014-07-07 10:00', '2014-07-07 11:00', '2014-07-07 12:00', + '2014-07-07 13:00', '2014-07-07 14:00', '2014-07-07 15:00', + '2014-07-07 16:00', '2014-07-08 09:00', '2014-07-08 10:00'], + freq='BH') + for idx in [idx1, idx2, idx3]: + tm.assert_index_equal(idx, expected) + + idx1 = DatetimeIndex(start='2014-07-04 15:45', end='2014-07-08 10:45', freq='BH') + idx2 = DatetimeIndex(start='2014-07-04 15:45', periods=12, freq='BH') + idx3 = DatetimeIndex(end='2014-07-08 10:45', periods=12, freq='BH') + + expected = DatetimeIndex(['2014-07-04 15:45', '2014-07-04 16:45', '2014-07-07 09:45', + '2014-07-07 10:45', '2014-07-07 11:45', '2014-07-07 12:45', + '2014-07-07 13:45', '2014-07-07 14:45', '2014-07-07 15:45', + '2014-07-07 16:45', '2014-07-08 09:45', '2014-07-08 10:45'], + freq='BH') + expected = idx1 + for idx in [idx1, idx2, idx3]: + tm.assert_index_equal(idx, expected) + + class TestCustomBusinessDay(Base): _multiprocess_can_split_ = True _offset = CDay diff --git a/pandas/tseries/tests/test_timeseries.py b/pandas/tseries/tests/test_timeseries.py index 964e8634bc1ef..0c4961d80a5f4 100644 --- a/pandas/tseries/tests/test_timeseries.py +++ b/pandas/tseries/tests/test_timeseries.py @@ -288,7 +288,7 @@ def test_indexing(self): self.assertRaises(KeyError, df.__getitem__, df.index[2],) def test_recreate_from_data(self): - freqs = ['M', 'Q', 'A', 'D', 'B', 'T', 'S', 'L', 'U', 'H', 'N', 'C'] + freqs = ['M', 'Q', 'A', 'D', 'B', 'BH', 'T', 'S', 'L', 'U', 'H', 'N', 'C'] for f in freqs: org = DatetimeIndex(start='2001/02/01 09:00', freq=f, periods=1) @@ -3347,6 +3347,29 @@ def test_date_range_bms_bug(self): ex_first = Timestamp('2000-01-03') self.assertEqual(rng[0], ex_first) + def test_date_range_businesshour(self): + idx = DatetimeIndex(['2014-07-04 09:00', '2014-07-04 10:00', '2014-07-04 11:00', + '2014-07-04 12:00', '2014-07-04 13:00', '2014-07-04 14:00', + '2014-07-04 15:00', '2014-07-04 16:00'], freq='BH') + rng = date_range('2014-07-04 09:00', '2014-07-04 16:00', freq='BH') + tm.assert_index_equal(idx, rng) + + idx = DatetimeIndex(['2014-07-04 16:00', '2014-07-07 09:00'], freq='BH') + rng = date_range('2014-07-04 16:00', '2014-07-07 09:00', freq='BH') + tm.assert_index_equal(idx, rng) + + idx = DatetimeIndex(['2014-07-04 09:00', '2014-07-04 10:00', '2014-07-04 11:00', + '2014-07-04 12:00', '2014-07-04 13:00', '2014-07-04 14:00', + '2014-07-04 15:00', '2014-07-04 16:00', + '2014-07-07 09:00', '2014-07-07 10:00', '2014-07-07 11:00', + '2014-07-07 12:00', '2014-07-07 13:00', '2014-07-07 14:00', + '2014-07-07 15:00', '2014-07-07 16:00', + '2014-07-08 09:00', '2014-07-08 10:00', '2014-07-08 11:00', + '2014-07-08 12:00', '2014-07-08 13:00', '2014-07-08 14:00', + '2014-07-08 15:00', '2014-07-08 16:00'], freq='BH') + rng = date_range('2014-07-04 09:00', '2014-07-08 16:00', freq='BH') + tm.assert_index_equal(idx, rng) + def test_string_index_series_name_converted(self): # #1644 df = DataFrame(np.random.randn(10, 4),