Skip to content

Commit e9f9ca1

Browse files
authored
BUG: Fix handling of ambiguous or nonexistent of start and end times in date_range (#27088)
* BUG: Raise AmbiguousTimeError for date_range with ambiguous start time. * Clarify comment * Add nonexistent tests * xfail one case after discovered bug * Add whatsnew issue number * Missing backtick * Misspelling
1 parent c3133db commit e9f9ca1

File tree

3 files changed

+39
-13
lines changed

3 files changed

+39
-13
lines changed

doc/source/whatsnew/v0.25.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,7 @@ Timezones
691691
- 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`)
692692
- Bug in :func:`to_datetime` with ``unit='ns'`` would drop timezone information from the parsed argument (:issue:`26168`)
693693
- 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`)
694+
- 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`)
694695

695696
Numeric
696697
^^^^^^^

pandas/core/arrays/datetimes.py

+15-7
Original file line numberDiff line numberDiff line change
@@ -433,10 +433,12 @@ def _generate_range(cls, start, end, periods, freq, tz=None,
433433
if tz is not None:
434434
# Localize the start and end arguments
435435
start = _maybe_localize_point(
436-
start, getattr(start, 'tz', None), start, freq, tz
436+
start, getattr(start, 'tz', None), start, freq, tz,
437+
ambiguous, nonexistent
437438
)
438439
end = _maybe_localize_point(
439-
end, getattr(end, 'tz', None), end, freq, tz
440+
end, getattr(end, 'tz', None), end, freq, tz,
441+
ambiguous, nonexistent
440442
)
441443
if freq is not None:
442444
# We break Day arithmetic (fixed 24 hour) here and opt for
@@ -2121,7 +2123,8 @@ def _maybe_normalize_endpoints(start, end, normalize):
21212123
return start, end, _normalized
21222124

21232125

2124-
def _maybe_localize_point(ts, is_none, is_not_none, freq, tz):
2126+
def _maybe_localize_point(ts, is_none, is_not_none, freq, tz, ambiguous,
2127+
nonexistent):
21252128
"""
21262129
Localize a start or end Timestamp to the timezone of the corresponding
21272130
start or end Timestamp
@@ -2133,6 +2136,8 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz):
21332136
is_not_none : argument that should not be None
21342137
freq : Tick, DateOffset, or None
21352138
tz : str, timezone object or None
2139+
ambiguous: str, localization behavior for ambiguous times
2140+
nonexistent: str, localization behavior for nonexistent times
21362141
21372142
Returns
21382143
-------
@@ -2141,10 +2146,13 @@ def _maybe_localize_point(ts, is_none, is_not_none, freq, tz):
21412146
# Make sure start and end are timezone localized if:
21422147
# 1) freq = a Timedelta-like frequency (Tick)
21432148
# 2) freq = None i.e. generating a linspaced range
2144-
if isinstance(freq, Tick) or freq is None:
2145-
localize_args = {'tz': tz, 'ambiguous': False}
2146-
else:
2147-
localize_args = {'tz': None}
21482149
if is_none is None and is_not_none is not None:
2150+
# Note: We can't ambiguous='infer' a singular ambiguous time; however,
2151+
# we have historically defaulted ambiguous=False
2152+
ambiguous = ambiguous if ambiguous != 'infer' else False
2153+
localize_args = {'ambiguous': ambiguous, 'nonexistent': nonexistent,
2154+
'tz': None}
2155+
if isinstance(freq, Tick) or freq is None:
2156+
localize_args['tz'] = tz
21492157
ts = ts.tz_localize(**localize_args)
21502158
return ts

pandas/tests/indexes/datetimes/test_timezones.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -541,12 +541,9 @@ def test_dti_construction_ambiguous_endpoint(self, tz):
541541
# construction with an ambiguous end-point
542542
# GH#11626
543543

544-
# FIXME: This next block fails to raise; it was taken from an older
545-
# version of this test that had an indention mistake that caused it
546-
# to not get executed.
547-
# with pytest.raises(pytz.AmbiguousTimeError):
548-
# date_range("2013-10-26 23:00", "2013-10-27 01:00",
549-
# tz="Europe/London", freq="H")
544+
with pytest.raises(pytz.AmbiguousTimeError):
545+
date_range("2013-10-26 23:00", "2013-10-27 01:00",
546+
tz="Europe/London", freq="H")
550547

551548
times = date_range("2013-10-26 23:00", "2013-10-27 01:00", freq="H",
552549
tz=tz, ambiguous='infer')
@@ -561,6 +558,26 @@ def test_dti_construction_ambiguous_endpoint(self, tz):
561558
assert times[-1] == Timestamp('2013-10-27 01:00:00+0000',
562559
tz=tz, freq="H")
563560

561+
@pytest.mark.parametrize('tz, option, expected', [
562+
['US/Pacific', 'shift_forward', "2019-03-10 03:00"],
563+
['dateutil/US/Pacific', 'shift_forward', "2019-03-10 03:00"],
564+
['US/Pacific', 'shift_backward', "2019-03-10 01:00"],
565+
pytest.param('dateutil/US/Pacific', 'shift_backward',
566+
"2019-03-10 01:00",
567+
marks=pytest.mark.xfail(reason="GH 24329")),
568+
['US/Pacific', timedelta(hours=1), "2019-03-10 03:00"]
569+
])
570+
def test_dti_construction_nonexistent_endpoint(self, tz, option, expected):
571+
# construction with an nonexistent end-point
572+
573+
with pytest.raises(pytz.NonExistentTimeError):
574+
date_range("2019-03-10 00:00", "2019-03-10 02:00",
575+
tz="US/Pacific", freq="H")
576+
577+
times = date_range("2019-03-10 00:00", "2019-03-10 02:00", freq="H",
578+
tz=tz, nonexistent=option)
579+
assert times[-1] == Timestamp(expected, tz=tz, freq="H")
580+
564581
def test_dti_tz_localize_bdate_range(self):
565582
dr = pd.bdate_range('1/1/2009', '1/1/2010')
566583
dr_utc = pd.bdate_range('1/1/2009', '1/1/2010', tz=pytz.utc)

0 commit comments

Comments
 (0)