diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index e6fafc8b1b14c..556245aaca353 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -668,7 +668,7 @@ Timedelta 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`) - Numeric diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index ff3879018674e..8f2d72b6fbc64 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -2961,10 +2961,14 @@ def _dti_setop_align_tzs(self, other: Index, setop: str_t) -> tuple[Index, Index and self.tz is not None and other.tz is not None ): - # GH#39328, GH#45357 - left = self.tz_convert("UTC") - right = other.tz_convert("UTC") - return left, right + # GH#39328, GH#45357, GH#60080 + # If both timezones are the same, no need to convert to UTC + if self.tz == other.tz: + return self, other + else: + left = self.tz_convert("UTC") + right = other.tz_convert("UTC") + return left, right return self, other @final diff --git a/pandas/tests/indexes/datetimes/test_setops.py b/pandas/tests/indexes/datetimes/test_setops.py index 7ef6efad0ff6f..dd314ce454e4c 100644 --- a/pandas/tests/indexes/datetimes/test_setops.py +++ b/pandas/tests/indexes/datetimes/test_setops.py @@ -201,6 +201,46 @@ def test_union_same_timezone_different_units(self): expected = date_range("2000-01-01", periods=3, tz="UTC").as_unit("us") tm.assert_index_equal(result, expected) + def test_setops_same_nonzero_timezone_different_units(self): + # GH 60080 - fix timezone being changed to UTC when units differ + # but timezone is the same + tz = "UTC+05:00" + idx1 = date_range("2000-01-01", periods=3, tz=tz).as_unit("us") + idx2 = date_range("2000-01-01", periods=3, tz=tz).as_unit("ns") + + # Check pre-conditions + assert idx1.tz == idx2.tz + assert idx1.dtype != idx2.dtype # Different units + + # Test union preserves timezone when units differ + result = idx1.union(idx2) + expected = date_range("2000-01-01", periods=3, tz=tz).as_unit("ns") + tm.assert_index_equal(result, expected) + assert result.tz == idx1.tz # Original timezone is preserved + + # Test with different dates to ensure it's not just returning one of the inputs + idx3 = date_range("2000-01-03", periods=3, tz=tz).as_unit("us") + result = idx1.union(idx3) + expected = DatetimeIndex( + ["2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04", "2000-01-05"], + tz=tz, + ).as_unit("us") + tm.assert_index_equal(result, expected) + assert result.tz == idx1.tz # Original timezone is preserved + + # Test intersection + result = idx1.intersection(idx2) + expected = date_range("2000-01-01", periods=3, tz=tz).as_unit("ns") + tm.assert_index_equal(result, expected) + assert result.tz == idx1.tz # Original timezone is preserved + + # Test symmetric_difference + idx4 = date_range("2000-01-02", periods=3, tz=tz).as_unit("ns") + result = idx1.symmetric_difference(idx4) + expected = DatetimeIndex(["2000-01-01", "2000-01-04"], tz=tz).as_unit("ns") + tm.assert_index_equal(result, expected) + assert result.tz == idx1.tz # Original timezone is preserved + # TODO: moved from test_datetimelike; de-duplicate with version below def test_intersection2(self): first = date_range("2020-01-01", periods=10)