ENH: Parse %z and %Z directive in format for to_datetime
State: closed

- [x] closes #13486
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

The implimentiontion is near identical to https://github.com/python/cpython/blob/master/Lib/_strptime.py an currently works as `datetime.strptime` would:

```
In [3]: f = '%Y-%m-%d %H:%M:%S %Z %z'

# datetime will parse the case below even though UTC with +0100 should be bogus
In [4]: d = ['2010-01-01 12:00:00 UTC +0100'] * 10

In [5]: pd._libs.tslibs.strptime.array_strptime(np.array(d, dtype='object'), f)
Out[5]:
(array(['2010-01-01T12:00:00.000000000', '2010-01-01T12:00:00.000000000',
        '2010-01-01T12:00:00.000000000', '2010-01-01T12:00:00.000000000',
        '2010-01-01T12:00:00.000000000', '2010-01-01T12:00:00.000000000',
        '2010-01-01T12:00:00.000000000', '2010-01-01T12:00:00.000000000',
        '2010-01-01T12:00:00.000000000', '2010-01-01T12:00:00.000000000'],
       dtype='datetime64[ns]'),
 array([datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC'),
        datetime.timezone(datetime.timedelta(0, 3600), 'UTC')],
       dtype=object))

In [29]: datetime.strptime('2010-01-01 12:00:00 UTC +0100', '%Y-%m-%d %H:%M:%S %Z %z')
Out[29]: datetime.datetime(2010, 1, 1, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(0, 3600), 'UTC'))
```
Currently, an offset needs to get passed (%z) in order for the tzname used be used (%Z).

I'd like to get feedback of what this function should return having parsed %z or %Z. It may be difficult to return a normal DatimeIndex/Series/array given the following edge cases:

1.  User passes strings with different tz offsets (`[date] +0100, [date]. -0600, [date] +1530`)
2.  User passes strings with different tz names (`[date] UTC, [date]. EST, [date] CET`)
3.  User passes strings with incompatable tz name and offset (see example above)

I suppose the most agnostic thing to is to return an array of Timestamps?

```
In [27]: [pd.Timestamp(val, tzinfo=tz) for val, tz in zip(*pd._libs.tslibs.strptime.array_strptime(np.array(d, dtype='object'), f))]
Out[27]:
[Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC'),
 Timestamp('2010-01-01 13:00:00+0100', tz='UTC')]
```

Labels:
name: Enhancement
name: Datetime
description: Datetime data dtype

Comments:
# [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=h1) Report
> Merging [#19979](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=desc) into [master](https://codecov.io/gh/pandas-dev/pandas/commit/36c1f6bfeb0d5915e1cd0bca1c91ee5672d945e7?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pandas-dev/pandas/pull/19979/graphs/tree.svg?token=eZ4WkYLtcO&width=650&src=pr&height=150)](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master   #19979      +/-   ##
==========================================
+ Coverage   91.84%   91.84%   +<.01%     
==========================================
  Files         153      153              
  Lines       49506    49516      +10     
==========================================
+ Hits        45467    45477      +10     
  Misses       4039     4039
```

| Flag | Coverage Δ | |
|---|---|---|
| #multiple | `90.24% <100%> (ø)` | :arrow_up: |
| #single | `41.87% <23.07%> (-0.01%)` | :arrow_down: |

| [Impacted Files](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pandas/core/tools/datetimes.py](https://codecov.io/gh/pandas-dev/pandas/pull/19979/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvdG9vbHMvZGF0ZXRpbWVzLnB5) | `84.98% <100%> (+0.54%)` | :arrow_up: |
| [pandas/core/arrays/categorical.py](https://codecov.io/gh/pandas-dev/pandas/pull/19979/diff?src=pr&el=tree#diff-cGFuZGFzL2NvcmUvYXJyYXlzL2NhdGVnb3JpY2FsLnB5) | `95.67% <0%> (-0.01%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=footer). Last update [36c1f6b...757458d](https://codecov.io/gh/pandas-dev/pandas/pull/19979?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Summary of the logic thus far and open to feedback:

`array_strptime` will now return 3 arrays of the datetime64 value, tzname, and tzoffset separately.

1) If the tzname directive is only passed, `%Z`, dispatch to `Datimetime`'s tz arg or `Timestamp`'s tz arg if there are multiple different tznames are parsed. 

2) If the tzoffset directive is only passed, `%z`, create a `pytz.FixedOffset` and dispatch to `Datimetime`'s tz arg or `Timestamp`'s tz arg if there are multiple different tzoffset are parsed.

3) If a tzname and tzoffet directives are passed, create a `datetime.timezone` object (similar to the cpython implementation) and just dispatch to `Timestamp`. One question here is if we should invalidate bogus timezones (e.g. UTC +0600, PST -1500, etc)


I should be able to transfer this logic into cython on the next iteration. 
@jreback I was able to simplify a lot of my logic ( I underestimated how `Index` would get cast to `DatetimeIndex` when passed `Timestamps` with the same tz).

I created separate functions to parse the timezone directive and then box the result.
I've changed a couple of things after your review @jreback 

1) Either %z or %Z can be parsed (not together)

2) %z will return a `pytz.FixedOffset`

```
In [3]: pd.to_datetime(['2010-01-01 12:00:00 +0100'], format='%Y-%m-%d %H:%M:%S %z')
Out[3]: DatetimeIndex(['2010-01-01 12:00:00+01:00'], dtype='datetime64[ns, pytz.FixedOffset(60)]', freq=None)
```

3) %Z can parse any timezone specified by `pytz.all_timezones`

```
In [2]: pd.to_datetime(['2010-01-01 12:00:00 US/Pacific'], format='%Y-%m-%d %H:%M:%S %Z')
Out[2]: DatetimeIndex(['2010-01-01 12:00:00-08:00'], dtype='datetime64[ns, US/Pacific]', freq=None)
```
lgtm @mroeschke 

merge on green.
New features are for 0.24.0, @mroeschke can you move the whatsnew?
thanks @mroeschke nice patch!

betcha didn't think it would be this long when you first put it up! hahah. tests and code look great!
ha no problem, thanks!