Skip to content

Commit bd88fa0

Browse files
BUG: honor ambiguous/nonexistent for tz-aware endpoints in date_range (#62493)
Co-authored-by: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com>
1 parent 7d7ee41 commit bd88fa0

File tree

3 files changed

+34
-7
lines changed

3 files changed

+34
-7
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,8 @@ Timezones
981981
^^^^^^^^^
982982
- Bug in :meth:`DatetimeIndex.union`, :meth:`DatetimeIndex.intersection`, and :meth:`DatetimeIndex.symmetric_difference` changing timezone to UTC when merging two DatetimeIndex objects with the same timezone but different units (:issue:`60080`)
983983
- Bug in :meth:`Series.dt.tz_localize` with a timezone-aware :class:`ArrowDtype` incorrectly converting to UTC when ``tz=None`` (:issue:`61780`)
984-
-
984+
- Fixed bug in :func:`date_range` where tz-aware endpoints with calendar offsets (e.g. ``"MS"``) failed on DST fall-back. These now respect ``ambiguous``/ ``nonexistent``. (:issue:`52908`)
985+
985986

986987
Numeric
987988
^^^^^^^

pandas/core/arrays/datetimes.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,14 @@ def _generate_range(
456456
end = _maybe_localize_point(end, freq, tz, ambiguous, nonexistent)
457457

458458
if freq is not None:
459-
# We break Day arithmetic (fixed 24 hour) here and opt for
460-
# Day to mean calendar day (23/24/25 hour). Therefore, strip
461-
# tz info from start and day to avoid DST arithmetic
462-
if isinstance(freq, Day):
463-
if start is not None:
459+
# Offset handling:
460+
# Ticks (fixed-duration like hours/minutes): keep tz; do absolute-time math.
461+
# Other calendar offsets: drop tz; do naive wall time; localize once later
462+
# so `ambiguous`/`nonexistent` are applied correctly.
463+
if not isinstance(freq, Tick):
464+
if start is not None and start.tz is not None:
464465
start = start.tz_localize(None)
465-
if end is not None:
466+
if end is not None and end.tz is not None:
466467
end = end.tz_localize(None)
467468

468469
if isinstance(freq, (Tick, Day)):

pandas/tests/indexes/datetimes/test_date_range.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,3 +1740,28 @@ def test_date_range_negative_freq_year_end_inbounds(self, unit):
17401740
freq="-1YE",
17411741
)
17421742
tm.assert_index_equal(rng, exp)
1743+
1744+
def test_date_range_tzaware_endpoints_accept_ambiguous(self):
1745+
# https://github.com/pandas-dev/pandas/issues/52908
1746+
start = Timestamp("1916-08-01", tz="Europe/Oslo")
1747+
end = Timestamp("1916-12-01", tz="Europe/Oslo")
1748+
res = date_range(start, end, freq="MS", ambiguous=True)
1749+
exp = date_range(
1750+
"1916-08-01", "1916-12-01", freq="MS", tz="Europe/Oslo", ambiguous=True
1751+
)
1752+
tm.assert_index_equal(res, exp)
1753+
1754+
def test_date_range_tzaware_endpoints_accept_nonexistent(self):
1755+
# Europe/London spring-forward: 2015-03-29 01:30 does not exist.
1756+
tz = "Europe/London"
1757+
start = Timestamp("2015-03-28 01:30", tz=tz)
1758+
end = Timestamp("2015-03-30 01:30", tz=tz)
1759+
1760+
result = date_range(start, end, freq="D", nonexistent="shift_forward")
1761+
1762+
# Build expected by generating naive daily times, then tz_localize so
1763+
# the nonexistent handling is applied during localization.
1764+
expected = date_range(
1765+
"2015-03-28 01:30", "2015-03-30 01:30", freq="D"
1766+
).tz_localize(tz, nonexistent="shift_forward")
1767+
tm.assert_index_equal(result, expected)

0 commit comments

Comments
 (0)