ENH: Add BusinessHour offset
State: closed

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)
# <BusinessHour: BH=09:00-17:00>

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

# Create Index
pd.date_range('2014-07-08 10:00', freq='BH', periods=40)
# [2014-07-08 10:00:00, ..., 2014-07-14 17:00:00]
# Length: 40, Freq: BH, Timezone: None
```
### Specify Opening/Closing Hour

Allow to specify opening/closing time using `start` and `end` keyword by `hour:minute` string or `datetime.time`.

```
bh = pd.offsets.BusinessHour(start='11:00', end=datetime.time(20, 0))
repr(bh)
# <BusinessHour: BH=11:00-20:00>

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-04 11:00:00

# if end < start, it will be midnight business hour
bh = pd.offsets.BusinessHour(start='17:00', end='9:00')
repr(bh)
# <BusinessHour: BH=17:00-09:00>

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 

```
# not 2014-08-01 17:00
pd.Timestamp('2014-08-01 16:00') + pd.offsets.BusinessHour()
#2014-08-04 09: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(start='17:00', end='9:00')
#2014-08:02 02:00
```
### Remainings:
- [x] doc
- impl & tests
  - [x] Index creation
    - [x] Confirm better conditions whether inferred_freq results in `BH` or `H`
  - [x] `normalize`
  - ~~tsplot~~ (This can't be done without #5148, skipped)
  - [x] Correct edge handling for `rollback` and `rollforward`
  - [x] handling `nanosecond` (minor issue, but current `apply_wraps` may add preserved `nanosecond` after the closing time)
- tests
  - [x] `n` larger than business shours 
  - [x] Specifying `start` and `end`
  - [x] midnight business hours
  - [x] Error cases for initialization
  - [x] Better fix for `test_apply_out_of_range` timezone check, which fails because of DST difference.


Labels:
name: Enhancement
name: Frequency
description: DateOffsets

Comments:
why would you not add this as an optional feature of `BusinessDay` instead? (e.g. normally `start=None,end=None`, otherwise could be specified ?

Because units are different. A day in BDay, an hour in BusinessHour.

@sinhrks ok, makes sense

OK, I think this can be reviewed now. 

Considering to simplify the internal process, but API has been fixed as far as I'm concerned.

@sinhrks can you rebase / revisit

Rebased.

@sinhrks let's rebase again; this looked ok

@jreback Thanks, rebased and now green.

@sinhrks looks good.

@jorisvandenbossche want to take a quick look thru docs?

thanks @sinhrks very nice!