From 968857803136dc3a10e6e012c39de313e0deb07a Mon Sep 17 00:00:00 2001 From: Yashwant Bezawada Date: Thu, 6 Nov 2025 16:49:17 -0600 Subject: [PATCH] BUG: Fix index.union failure at DST boundary (GH#62915) When concatenating DatetimeIndex objects across DST transitions, the frequency preservation logic was using naive addition that didn't account for timezone offsets changing at DST boundaries. This caused the assertion `pair[0][-1] + obj.freq == pair[1][0]` to fail even when the indexes were legitimately consecutive. For fixed (Tick) frequencies like Day, Hour, etc., the fix compares the underlying int64 values (UTC nanoseconds) instead of relying on timezone-aware arithmetic. This correctly identifies consecutive timestamps regardless of DST transitions. For non-fixed frequencies like MonthEnd or BusinessDay, the code falls back to the original comparison method since freq.nanos is not available for these offset types. Closes #62915 --- pandas/core/arrays/datetimelike.py | 19 ++++++++++++++++++- pandas/tests/indexes/datetimes/test_setops.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 1fbcd0665c467..811fbf9b926ed 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -2383,7 +2383,24 @@ def _concat_same_type( if obj.freq is not None and all(x.freq == obj.freq for x in to_concat): pairs = zip(to_concat[:-1], to_concat[1:], strict=True) - if all(pair[0][-1] + obj.freq == pair[1][0] for pair in pairs): + # GH#62915: For timezone-aware datetimes with fixed frequencies, + # DST transitions can cause naive addition (pair[0][-1] + freq) to + # not equal pair[1][0] even when they're legitimately consecutive. + # For Tick frequencies, compare using underlying int64 values. + # For non-Tick frequencies, use the original comparison. + try: + freq_nanos = obj.freq.nanos + pairs_match = all( + pair[1][0]._value - pair[0][-1]._value == freq_nanos + for pair in pairs + ) + except (ValueError, AttributeError): + # Non-fixed frequency, fall back to original comparison + pairs_match = all( + pair[0][-1] + obj.freq == pair[1][0] for pair in pairs + ) + + if pairs_match: new_freq = obj.freq new_obj._freq = new_freq return new_obj diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index 7a68cb867c94e..62d76abe76587 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -729,6 +729,20 @@ def test_intersection_dst_transition(self, tz): expected = date_range("2021-10-28", periods=6, freq="D", tz="Europe/London") tm.assert_index_equal(result, expected) + def test_union_dst_boundary(self): + # GH#62915: index.union fails at DST boundary + # When one index ends at DST transition and the other crosses it, + # the union should succeed and preserve frequency + index1 = date_range("2025-10-25", "2025-10-26", freq="D", tz="Europe/Helsinki") + index2 = date_range("2025-10-25", "2025-10-28", freq="D", tz="Europe/Helsinki") + + result = index1.union(index2) + expected = date_range( + "2025-10-25", "2025-10-28", freq="D", tz="Europe/Helsinki" + ) + tm.assert_index_equal(result, expected) + assert result.freq == expected.freq + def test_union_non_nano_rangelike(): # GH 59036