Repository: pandas-dev/pandas

Issue:
Hourly Frequency Business Days
in order to use pandas with Bloomberg time series data, I would like ability to set a time series with 
i) hourly frequency for business days
ii) a given start and end time to that day

For the first part, using
bdate_range(startdate,enddate,freq='h')
doesn't do the right thing. Using 'freq=h' overrides the business days such that the resulting time series includes weekends too. The frequency alias 'bh' doesn't exist in the same way 'bm' does for example. Would it be possible to add this functionality? I could write something manually to go back and remove weekends, but this messes up the Frequency label on the time series such that later functions don't work (such as EWMVAR etc)

For the second part, this is useful functionality when calculating volatility/variance to be able to specify time ranges for each day. So a Business Hourly frequency starting at say 7am and ending 7pm on a day.



Discussion:

Resolving patch:
diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py
--- 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
--- 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'


Test patch:
diff --git a/pandas/tseries/tests/test_frequencies.py b/pandas/tseries/tests/test_frequencies.py
--- 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
--- 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), '<BusinessHour: BH=09:00-17:00>')
+        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), '<BusinessHour: BH=11:00-14:30>')
+        self.assertEqual(repr(self.offset6), '<BusinessHour: BH=20:00-05:00>')
+        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
--- 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),