From 787a1ddf442da64741af0f730ed39c40a7d8eb6a Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 15:38:09 -0400 Subject: [PATCH 1/7] BUG: Raise AmbiguousTimeError for date_range with ambiguous start time. --- pandas/core/arrays/datetimes.py | 24 +++++++++++++------ .../tests/indexes/datetimes/test_timezones.py | 9 +++---- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 6e7217762a3fb..da3458061eef9 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -431,10 +431,12 @@ def _generate_range(cls, start, end, periods, freq, tz=None, if tz is not None: # Localize the start and end arguments start = _maybe_localize_point( - start, getattr(start, 'tz', None), start, freq, tz + start, getattr(start, 'tz', None), start, freq, tz, + ambiguous, nonexistent ) end = _maybe_localize_point( - end, getattr(end, 'tz', None), end, freq, tz + end, getattr(end, 'tz', None), end, freq, tz, + ambiguous, nonexistent ) if freq is not None: # We break Day arithmetic (fixed 24 hour) here and opt for @@ -2119,7 +2121,8 @@ def _maybe_normalize_endpoints(start, end, normalize): return start, end, _normalized -def _maybe_localize_point(ts, is_none, is_not_none, freq, tz): +def _maybe_localize_point(ts, is_none, is_not_none, freq, tz, ambiguous, + nonexistent): """ Localize a start or end Timestamp to the timezone of the corresponding start or end Timestamp @@ -2131,6 +2134,8 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz): is_not_none : argument that should not be None freq : Tick, DateOffset, or None tz : str, timezone object or None + ambiguous: str, localization behavior for ambiguous times + nonexistent: str, localization behavior for nonexistent times Returns ------- @@ -2139,10 +2144,15 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz): # Make sure start and end are timezone localized if: # 1) freq = a Timedelta-like frequency (Tick) # 2) freq = None i.e. generating a linspaced range - if isinstance(freq, Tick) or freq is None: - localize_args = {'tz': tz, 'ambiguous': False} - else: - localize_args = {'tz': None} if is_none is None and is_not_none is not None: + if ambiguous == 'infer': + # Note: We can't infer a singular ambiguous time; however, + # we have historically decided that 'infer' in this case means + # ambiguous = False... + ambiguous = False + localize_args = {'ambiguous': ambiguous, 'nonexistent': nonexistent, + 'tz': None} + if isinstance(freq, Tick) or freq is None: + localize_args['tz'] = tz ts = ts.tz_localize(**localize_args) return ts diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index 368dc68e516df..6e7bab24b3bfd 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -542,12 +542,9 @@ def test_dti_construction_ambiguous_endpoint(self, tz): # construction with an ambiguous end-point # GH#11626 - # FIXME: This next block fails to raise; it was taken from an older - # version of this test that had an indention mistake that caused it - # to not get executed. - # with pytest.raises(pytz.AmbiguousTimeError): - # date_range("2013-10-26 23:00", "2013-10-27 01:00", - # tz="Europe/London", freq="H") + with pytest.raises(pytz.AmbiguousTimeError): + date_range("2013-10-26 23:00", "2013-10-27 01:00", + tz="Europe/London", freq="H") times = date_range("2013-10-26 23:00", "2013-10-27 01:00", freq="H", tz=tz, ambiguous='infer') From 287e8f654c4eee81b225c28dd6468e2fd0024eda Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 17:28:38 -0500 Subject: [PATCH 2/7] Clarify comment --- pandas/core/arrays/datetimes.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 1f30f5feff008..6b554ddf25c96 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -2147,11 +2147,9 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz, ambiguous, # 1) freq = a Timedelta-like frequency (Tick) # 2) freq = None i.e. generating a linspaced range if is_none is None and is_not_none is not None: - if ambiguous == 'infer': - # Note: We can't infer a singular ambiguous time; however, - # we have historically decided that 'infer' in this case means - # ambiguous = False... - ambiguous = False + # Note: We can't ambiguous='infer' a singular ambiguous time; however, + # we have historically defaulted ambiguous=False + ambiguous = ambiguous if ambiguous != 'infer' else False localize_args = {'ambiguous': ambiguous, 'nonexistent': nonexistent, 'tz': None} if isinstance(freq, Tick) or freq is None: From 06c2a7163ee44e70d56c5b9ecc2d7c25e085c51a Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 18:01:15 -0500 Subject: [PATCH 3/7] Add nonexistent tests --- .../tests/indexes/datetimes/test_timezones.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index 6e7bab24b3bfd..16dd92e001534 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -563,6 +563,24 @@ def test_dti_construction_ambiguous_endpoint(self, tz): assert times[-1] == Timestamp('2013-10-27 01:00:00+0000', tz=tz, freq="H") + @pytest.mark.parametrize('tz, option, expected', [ + ['US/Pacific', 'shift_forward', "2019-03-10 03:00"], + ['dateutil/US/Pacific', 'shift_forward', "2019-03-10 03:00"], + ['US/Pacific', 'shift_backward', "2019-03-10 01:00"], + ['dateutil/US/Pacific', 'shift_backward', "2019-03-10 00:00"], + ['US/Pacific', timedelta(hours=1), "2019-03-10 03:00"] + ]) + def test_dti_construction_nonexistent_endpoint(self, tz, option, expected): + # construction with an nonexistent end-point + + with pytest.raises(pytz.NonExistentTimeError): + date_range("2019-03-10 00:00", "2019-03-10 02:00", + tz="US/Pacific", freq="H") + + times = date_range("2019-03-10 00:00", "2019-03-10 02:00", freq="H", + tz=tz, nonexistent=option) + assert times[-1] == Timestamp(expected, tz=tz, freq="H") + def test_dti_tz_localize_bdate_range(self): dr = pd.bdate_range('1/1/2009', '1/1/2010') dr_utc = pd.bdate_range('1/1/2009', '1/1/2010', tz=pytz.utc) From 27add0d145c18285befba188fff4f338bc7f6dda Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 18:31:06 -0500 Subject: [PATCH 4/7] xfail one case after discovered bug --- pandas/tests/indexes/datetimes/test_timezones.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index 16dd92e001534..5893de7024077 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -567,7 +567,9 @@ def test_dti_construction_ambiguous_endpoint(self, tz): ['US/Pacific', 'shift_forward', "2019-03-10 03:00"], ['dateutil/US/Pacific', 'shift_forward', "2019-03-10 03:00"], ['US/Pacific', 'shift_backward', "2019-03-10 01:00"], - ['dateutil/US/Pacific', 'shift_backward', "2019-03-10 00:00"], + pytest.param('dateutil/US/Pacific', 'shift_backward', + "2019-03-10 01:00", + marks=pytest.mark.xfail(reason="GH 24329")), ['US/Pacific', timedelta(hours=1), "2019-03-10 03:00"] ]) def test_dti_construction_nonexistent_endpoint(self, tz, option, expected): From a22b08ef46138908db5a03ec763f6533231d5d84 Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 18:36:23 -0500 Subject: [PATCH 5/7] Add whatsnew issue number --- doc/source/whatsnew/v0.25.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index da939687500b6..6ee5fe863cdcd 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -691,6 +691,7 @@ Timezones - Bug in :func:`to_datetime` where an uninformative ``RuntimeError`` was raised when passing a naive :class:`Timestamp` with datetime strings with mixed UTC offsets (:issue:`25978`) - Bug in :func:`to_datetime` with ``unit='ns'`` would drop timezone information from the parsed argument (:issue:`26168`) - Bug in :func:`DataFrame.join` where joining a timezone aware index with a timezone aware column would result in a column of ``NaN`` (:issue:`26335`) +- Bug in :fucn:`date_range` where ambiguous or nonexistent start or end times were not handled by the ``ambiguous`` or ``nonexistent` keywords respectively (:issue:`27088`) Numeric ^^^^^^^ From 0c55aec48bac785e98e0c2acebde7d7c743b1fcd Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 18:38:44 -0500 Subject: [PATCH 6/7] Missing backtick --- doc/source/whatsnew/v0.25.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 6ee5fe863cdcd..5989f00f04075 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -691,7 +691,7 @@ Timezones - Bug in :func:`to_datetime` where an uninformative ``RuntimeError`` was raised when passing a naive :class:`Timestamp` with datetime strings with mixed UTC offsets (:issue:`25978`) - Bug in :func:`to_datetime` with ``unit='ns'`` would drop timezone information from the parsed argument (:issue:`26168`) - Bug in :func:`DataFrame.join` where joining a timezone aware index with a timezone aware column would result in a column of ``NaN`` (:issue:`26335`) -- Bug in :fucn:`date_range` where ambiguous or nonexistent start or end times were not handled by the ``ambiguous`` or ``nonexistent` keywords respectively (:issue:`27088`) +- Bug in :fucn:`date_range` where ambiguous or nonexistent start or end times were not handled by the ``ambiguous`` or ``nonexistent`` keywords respectively (:issue:`27088`) Numeric ^^^^^^^ From e987c53e9185e455109f6cfc2cf0044908c4df1f Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Thu, 27 Jun 2019 22:01:36 -0500 Subject: [PATCH 7/7] Misspelling --- doc/source/whatsnew/v0.25.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index 5989f00f04075..d18b1a576f30c 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -691,7 +691,7 @@ Timezones - Bug in :func:`to_datetime` where an uninformative ``RuntimeError`` was raised when passing a naive :class:`Timestamp` with datetime strings with mixed UTC offsets (:issue:`25978`) - Bug in :func:`to_datetime` with ``unit='ns'`` would drop timezone information from the parsed argument (:issue:`26168`) - Bug in :func:`DataFrame.join` where joining a timezone aware index with a timezone aware column would result in a column of ``NaN`` (:issue:`26335`) -- Bug in :fucn:`date_range` where ambiguous or nonexistent start or end times were not handled by the ``ambiguous`` or ``nonexistent`` keywords respectively (:issue:`27088`) +- Bug in :func:`date_range` where ambiguous or nonexistent start or end times were not handled by the ``ambiguous`` or ``nonexistent`` keywords respectively (:issue:`27088`) Numeric ^^^^^^^