From a2e67f1f34900f4478526c5e8a8582317919cf99 Mon Sep 17 00:00:00 2001 From: Akshat Gupta Date: Tue, 12 Aug 2025 23:08:51 +0530 Subject: [PATCH 1/4] Preserve day freq when subtracting timestamp --- pandas/core/arrays/datetimelike.py | 10 ++++++++++ pandas/tests/indexes/timedeltas/methods/test_shift.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 7e57b40e42430..38a95178d52f2 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1089,6 +1089,16 @@ def _get_arithmetic_result_freq(self, other) -> BaseOffset | None: # e.g. TestTimedelta64ArithmeticUnsorted::test_timedelta # Day is unambiguously 24h return self.freq + elif ( + lib.is_np_dtype(self.dtype, "M") + and isinstance(self.freq, Day) + and isinstance(other, Timestamp) + and (self.tz is None or timezones.is_utc(self.tz)) + and (other.tz is None or timezones.is_utc(other.tz)) + ): + # e.g. issue gh-62094: subtracting a Timestamp from a DTI + # with Day freq retains that freq + return self.freq return None diff --git a/pandas/tests/indexes/timedeltas/methods/test_shift.py b/pandas/tests/indexes/timedeltas/methods/test_shift.py index 9bbf06dc51a0c..14529c6b1e06d 100644 --- a/pandas/tests/indexes/timedeltas/methods/test_shift.py +++ b/pandas/tests/indexes/timedeltas/methods/test_shift.py @@ -74,3 +74,10 @@ def test_shift_no_freq(self): tdi = TimedeltaIndex(["1 days 01:00:00", "2 days 01:00:00"], freq=None) with pytest.raises(NullFrequencyError, match="Cannot shift with no freq"): tdi.shift(2) + + def test_shift_after_dti_sub_timestamp(self): + dti = pd.date_range("1/1/2021", "1/5/2021") + tdi = dti - pd.Timestamp("1/3/2019") + result = tdi.shift(1) + expected = tdi + pd.Timedelta(days=1) + tm.assert_index_equal(result, expected) From 47cd7e82ad6b01df2cd0c45597c5dbba149b153a Mon Sep 17 00:00:00 2001 From: Akshat Gupta Date: Tue, 12 Aug 2025 23:18:47 +0530 Subject: [PATCH 2/4] DOC: note fix for DatetimeIndex subtraction GH#62094 --- doc/source/whatsnew/v3.0.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 68c5556c4bcbd..50dc08456c7c7 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -758,6 +758,10 @@ Datetimelike - Bug in :meth:`to_datetime` reports incorrect index in case of any failure scenario. (:issue:`58298`) - Bug in :meth:`to_datetime` with ``format="ISO8601"`` and ``utc=True`` where naive timestamps incorrectly inherited timezone offset from previous timestamps in a series. (:issue:`61389`) - Bug in :meth:`to_datetime` wrongly converts when ``arg`` is a ``np.datetime64`` object with unit of ``ps``. (:issue:`60341`) +- Bug in subtracting a :class:`Timestamp` from a :class:`DatetimeIndex` with + ``freq="D"`` dropping the frequency and causing subsequent + :meth:`TimedeltaIndex.shift` calls to raise ``NullFrequencyError`` + (:issue:`62094`) - Bug in constructing arrays with :class:`ArrowDtype` with ``timestamp`` type incorrectly allowing ``Decimal("NaN")`` (:issue:`61773`) - Bug in constructing arrays with a timezone-aware :class:`ArrowDtype` from timezone-naive datetime objects incorrectly treating those as UTC times instead of wall times like :class:`DatetimeTZDtype` (:issue:`61775`) - Bug in setting scalar values with mismatched resolution into arrays with non-nanosecond ``datetime64``, ``timedelta64`` or :class:`DatetimeTZDtype` incorrectly truncating those scalars (:issue:`56410`) From 8ae8a06e58252366bb6b2cf745709db04840c9d7 Mon Sep 17 00:00:00 2001 From: Akshat Gupta Date: Tue, 12 Aug 2025 23:51:26 +0530 Subject: [PATCH 3/4] Cast self before tz check in _get_arithmetic_result_freq --- pandas/core/arrays/datetimelike.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 38a95178d52f2..2ffac66e120be 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1093,12 +1093,16 @@ def _get_arithmetic_result_freq(self, other) -> BaseOffset | None: lib.is_np_dtype(self.dtype, "M") and isinstance(self.freq, Day) and isinstance(other, Timestamp) - and (self.tz is None or timezones.is_utc(self.tz)) - and (other.tz is None or timezones.is_utc(other.tz)) ): - # e.g. issue gh-62094: subtracting a Timestamp from a DTI - # with Day freq retains that freq - return self.freq + self = cast("DatetimeArray", self) + if ( + self.tz is None or timezones.is_utc(self.tz) + ) and ( + other.tz is None or timezones.is_utc(other.tz) + ): + # e.g. issue gh-62094: subtracting a Timestamp from a DTI + # with Day freq retains that freq + return self.freq return None From ed516d9bac1a02514ba9a2ff21a42c2458af4a1e Mon Sep 17 00:00:00 2001 From: Akshat Gupta Date: Tue, 12 Aug 2025 18:26:24 +0000 Subject: [PATCH 4/4] Fixing format --- pandas/core/arrays/datetimelike.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 2ffac66e120be..b175c7b9d18c5 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1095,9 +1095,7 @@ def _get_arithmetic_result_freq(self, other) -> BaseOffset | None: and isinstance(other, Timestamp) ): self = cast("DatetimeArray", self) - if ( - self.tz is None or timezones.is_utc(self.tz) - ) and ( + if (self.tz is None or timezones.is_utc(self.tz)) and ( other.tz is None or timezones.is_utc(other.tz) ): # e.g. issue gh-62094: subtracting a Timestamp from a DTI