pydata/pandas — pull request #7905 by sinhrks (2014-08-02 14:56:55+00:00)
Title: (WIP) ENH: Add BusinessHour offset

Closes #2469. Create ``BusinessHour`` offset to specify business hours on ``BusinessDay``. Appreciated if any feedbacks regarding bahaviours, keywords, etc.

### Basic
By default, use 9:00 - 17:00 as business hours. Adding ``BusinessHour`` will increment timestamp by hourly on the days belong to ``BusinessDay`` offset.
```
bh = pd.offsets.BusinessHour()
repr(bh)
#

pd.Timestamp('2014-08-01 10:00') + bh
# 2014-08-01 11:00:00
pd.Timestamp('2014-08-01 08:00') + bh
# 2014-08-01 10:00:00

# move to next Business Day
pd.Timestamp('2014-08-01 19:00') + bh
# 2014-08-04 10:00:00

# if exceeds the closing time, remaining are added to next day
pd.Timestamp('2014-08-01 16:45') + bh
# 2014-08-04 09:45:00

# if n != 1
pd.Timestamp('2014-08-01 10:00') + pd.offsets.BusinessHour(2)
# 2014-08-01 12:00:00

pd.Timestamp('2014-08-01 10:00') + pd.offsets.BusinessHour(-3)
# 2014-07-31 15:00:00

# Do not allow to create offset exceeding total business hours in a day.
# Because it gets very complex and assumed to be less used.
pd.offsets.BusinessHour(10)
# ValueError: n must be smaller than businesshours in a day: 8.0 hours
```

### Specify Opening/Closing Hour
Allow to specify opening/closing time using ``hstart`` and ``huntil`` keyword by ``hour:minute`` string or ``datetime.time``.
(referring to ``dateutil`` 's ``dtstart`` and ``until``, but may better one?)

```
bh = pd.offsets.BusinessHour(hstart='11:00', huntil=datetime.time(20, 0))
repr(bh)
#

pd.Timestamp('2014-08-01 10:00') + bh
# 2014-08-01 12:00:00
pd.Timestamp('2014-08-01 13:00') + bh
# 2014-08-01 14:00:00
pd.Timestamp('2014-08-01 19:00') + bh
# 2014-08-01 20:00:00

# if huntil > hstart, it will be midnight business hour
bh = pd.offsets.BusinessHour(hstart='17:00', huntil='9:00')
repr(bh)
#

pd.Timestamp('2014-07-31 10:00') + bh
# 2014-07-31 18:00:00
pd.Timestamp('2014-07-31 23:00') + bh
# 2014-08-01 00:00:00
pd.Timestamp('2014-07-31 01:00') + bh
# 2014-07-31 02:00:00
pd.Timestamp('2014-08-01 13:00') + bh
# 2014-08-01 18:00:00
```

### Edge cases
``onOffset`` should include both edges.
```
pd.offsets.BusinessHour().onOffset(pd.Timestamp('2014-08-01 09:00'))
# True

pd.offsets.BusinessHour().onOffset(pd.Timestamp('2014-08-01 17:00'))
# True
```

If result is on the end-edge of business hour, move to next
```
pd.Timestamp('2014-08-01 08:00') + pd.offsets.BusinessHour()
# 2014-08-01 09:00

# not 2014-08-01 17:00
pd.Timestamp('2014-08-01 16:00') + pd.offsets.BusinessHour()
# 2014-08-04 09:00

pd.Timestamp('2014-08-01 18:00') - pd.offsets.BusinessHour()
# 2014-08:01 17:00

# not 2014-08-01 09:00
pd.Timestamp('2014-08-01 10:00') - pd.offsets.BusinessHour()
# 2014-07:31 17:00
```

In case of midnight business hour, distinguish the date by its opening hour
```
# 2014-08-02 is Saturday, but handled as valid because its business hour starts on 08-01
pd.Timestamp('2014-08-02 01:00') - pd.offsets.BusinessHour(hstart='17:00', huntil='9:00')
# 2014-08:02 02:00
```

### Remainings:
- [ ] doc
- [ ] impl & tests
- [ ] Index creation
- [ ] ``normalize``
- [ ] Error cases for ops (check ``n`` in case of ``mult``, ``add``)
- [ ] tsplot
- [ ] tests
- [ ] Specifying ``hstart`` and ``huntil``
- [ ] midnight business hours
- [ ] Error cases for initialization