Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,8 @@ Timezones
^^^^^^^^^
- 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`)
- Bug in :meth:`Series.dt.tz_localize` with a timezone-aware :class:`ArrowDtype` incorrectly converting to UTC when ``tz=None`` (:issue:`61780`)
-
- 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`)


Numeric
^^^^^^^
Expand Down
13 changes: 7 additions & 6 deletions pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,13 +456,14 @@ def _generate_range(
end = _maybe_localize_point(end, freq, tz, ambiguous, nonexistent)

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

if isinstance(freq, (Tick, Day)):
Expand Down
26 changes: 26 additions & 0 deletions pandas/tests/indexes/datetimes/test_date_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -1740,3 +1740,29 @@ def test_date_range_negative_freq_year_end_inbounds(self, unit):
freq="-1YE",
)
tm.assert_index_equal(rng, exp)

def test_date_range_tzaware_endpoints_accept_ambiguous(self):
# https://github.com/pandas-dev/pandas/issues/52908
start = Timestamp("1916-08-01", tz="Europe/Oslo")
end = Timestamp("1916-12-01", tz="Europe/Oslo")
res = date_range(start, end, freq="MS", ambiguous=True)
exp = date_range(
"1916-08-01", "1916-12-01", freq="MS", tz="Europe/Oslo", ambiguous=True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add another test that uses nonexistent?

)
tm.assert_index_equal(res, exp)

def test_date_range_tzaware_endpoints_accept_nonexistent(self):
# Europe/London spring-forward: 2015-03-29 01:30 does not exist.
start = Timestamp("2015-03-28 01:30", tz="Europe/London")
end = Timestamp("2015-03-30 01:30", tz="Europe/London")

result = date_range(start, end, freq="D", nonexistent="shift_forward")
expected = [
Timestamp("2015-03-28 01:30:00+00:00"),
Timestamp(
"2015-03-29 02:00:00+01:00"
), # shifted forward over next valid wall time
Timestamp("2015-03-30 01:30:00+01:00"),
]

assert list(result) == expected
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use tm.assert_index.equal to compare here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the expected block to ensure dtype and freq match the date_range result.

Loading