Skip to content

Commit e6f4096

Browse files
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
1 parent 5641979 commit e6f4096

File tree

2 files changed

+30
-1
lines changed

2 files changed

+30
-1
lines changed

pandas/core/arrays/datetimelike.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2383,7 +2383,24 @@ def _concat_same_type(
23832383

23842384
if obj.freq is not None and all(x.freq == obj.freq for x in to_concat):
23852385
pairs = zip(to_concat[:-1], to_concat[1:], strict=True)
2386-
if all(pair[0][-1] + obj.freq == pair[1][0] for pair in pairs):
2386+
# GH#62915: For timezone-aware datetimes with fixed frequencies,
2387+
# DST transitions can cause naive addition (pair[0][-1] + freq) to
2388+
# not equal pair[1][0] even when they're legitimately consecutive.
2389+
# For Tick frequencies, compare using underlying int64 values.
2390+
# For non-Tick frequencies, use the original comparison.
2391+
try:
2392+
freq_nanos = obj.freq.nanos
2393+
pairs_match = all(
2394+
pair[1][0]._value - pair[0][-1]._value == freq_nanos
2395+
for pair in pairs
2396+
)
2397+
except (ValueError, AttributeError):
2398+
# Non-fixed frequency, fall back to original comparison
2399+
pairs_match = all(
2400+
pair[0][-1] + obj.freq == pair[1][0] for pair in pairs
2401+
)
2402+
2403+
if pairs_match:
23872404
new_freq = obj.freq
23882405
new_obj._freq = new_freq
23892406
return new_obj

pandas/tests/indexes/datetimes/test_setops.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,18 @@ def test_intersection_dst_transition(self, tz):
729729
expected = date_range("2021-10-28", periods=6, freq="D", tz="Europe/London")
730730
tm.assert_index_equal(result, expected)
731731

732+
def test_union_dst_boundary(self):
733+
# GH#62915: index.union fails at DST boundary
734+
# When one index ends at DST transition and the other crosses it,
735+
# the union should succeed and preserve frequency
736+
index1 = date_range("2025-10-25", "2025-10-26", freq="D", tz="Europe/Helsinki")
737+
index2 = date_range("2025-10-25", "2025-10-28", freq="D", tz="Europe/Helsinki")
738+
739+
result = index1.union(index2)
740+
expected = date_range("2025-10-25", "2025-10-28", freq="D", tz="Europe/Helsinki")
741+
tm.assert_index_equal(result, expected)
742+
assert result.freq == expected.freq
743+
732744

733745
def test_union_non_nano_rangelike():
734746
# GH 59036

0 commit comments

Comments
 (0)