Skip to content

Commit 9189271

Browse files
committed
BUG: Timedelta from Tick offsets now behaves like keyword arguments (GH#62310)
1 parent 2f26644 commit 9189271

File tree

3 files changed

+58
-3
lines changed

3 files changed

+58
-3
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,7 @@ Timedelta
919919
^^^^^^^^^
920920
- Accuracy improvement in :meth:`Timedelta.to_pytimedelta` to round microseconds consistently for large nanosecond based Timedelta (:issue:`57841`)
921921
- Bug in :class:`Timedelta` constructor failing to raise when passed an invalid keyword (:issue:`53801`)
922+
- Bug in :class:`Timedelta` when constructed from Tick offsets (e.g., ``Timedelta(offsets.Hour(1))``) that could produce incorrect results in arithmetic. Timedelta now handles Hour, Minute, and Second offsets consistently with keyword arguments (:issue:`62310`)
922923
- Bug in :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`)
923924
- Bug in multiplication operations with ``timedelta64`` dtype failing to raise ``TypeError`` when multiplying by ``bool`` objects or dtypes (:issue:`58054`)
924925

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,8 @@ class Timedelta(_Timedelta):
20122012
"milliseconds", "microseconds", "nanoseconds"}
20132013

20142014
def __new__(cls, object value=_no_input, unit=None, **kwargs):
2015+
cdef NPY_DATETIMEUNIT in_reso
2016+
cdef NPY_DATETIMEUNIT target_reso
20152017
unsupported_kwargs = set(kwargs)
20162018
unsupported_kwargs.difference_update(cls._req_any_kwargs_new)
20172019
if unsupported_kwargs or (
@@ -2134,9 +2136,30 @@ class Timedelta(_Timedelta):
21342136
return cls._from_value_and_reso(new_value, reso=new_reso)
21352137

21362138
elif is_tick_object(value):
2137-
new_reso = get_supported_reso(value._creso)
2138-
new_value = delta_to_nanoseconds(value, reso=new_reso)
2139-
return cls._from_value_and_reso(new_value, reso=new_reso)
2139+
# GH#62310
2140+
# Handle Tick offsets
2141+
in_reso = value._creso
2142+
2143+
if in_reso == NPY_DATETIMEUNIT.NPY_FR_ns:
2144+
target_reso = NPY_DATETIMEUNIT.NPY_FR_ns
2145+
elif in_reso == NPY_DATETIMEUNIT.NPY_FR_us:
2146+
target_reso = NPY_DATETIMEUNIT.NPY_FR_us
2147+
elif in_reso == NPY_DATETIMEUNIT.NPY_FR_ms:
2148+
target_reso = NPY_DATETIMEUNIT.NPY_FR_ms
2149+
elif in_reso in (
2150+
NPY_DATETIMEUNIT.NPY_FR_s,
2151+
NPY_DATETIMEUNIT.NPY_FR_m,
2152+
NPY_DATETIMEUNIT.NPY_FR_h,
2153+
):
2154+
target_reso = NPY_DATETIMEUNIT.NPY_FR_s
2155+
else:
2156+
raise ValueError(
2157+
f"Value must be Timedelta, string, integer, float, timedelta "
2158+
f"or convertible, not {type(value).__name__}"
2159+
)
2160+
2161+
new_value = delta_to_nanoseconds(value, reso=target_reso)
2162+
return cls._from_value_and_reso(new_value, reso=target_reso)
21402163

21412164
elif is_integer_object(value) or is_float_object(value):
21422165
# unit=None is de-facto 'ns'

pandas/tests/scalar/timedelta/test_constructors.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
NaT,
1414
Timedelta,
1515
TimedeltaIndex,
16+
Timestamp,
1617
offsets,
1718
to_timedelta,
1819
)
@@ -263,6 +264,36 @@ def test_from_tick_reso():
263264
Timedelta(tick)
264265

265266

267+
def test_tick_offset_arithmetic_consistency():
268+
# GH#62310: Timedelta construction from Tick offset objects should
269+
# behave consistently with Timedelta keyword arguments
270+
271+
# Construct Timedelta from Tick offset vs keyword argument
272+
tick_td = Timedelta(offsets.Hour(1))
273+
kwarg_td = Timedelta(hours=1)
274+
275+
# Both should represent the same duration
276+
assert tick_td == kwarg_td
277+
assert tick_td.value == kwarg_td.value
278+
279+
# Subtraction with Timestamp
280+
ts = Timestamp("2020-01-01 02:00:00")
281+
result1 = ts - tick_td
282+
result2 = ts - kwarg_td
283+
assert result1 == result2 == Timestamp("2020-01-01 01:00:00")
284+
285+
# Addition with Timestamp
286+
ts2 = Timestamp("2020-01-01 00:00:00")
287+
result3 = ts2 + tick_td
288+
result4 = ts2 + kwarg_td
289+
assert result3 == result4 == Timestamp("2020-01-01 01:00:00")
290+
291+
# Timedelta arithmetic
292+
assert tick_td - kwarg_td == Timedelta(0)
293+
assert kwarg_td - tick_td == Timedelta(0)
294+
assert tick_td + kwarg_td == Timedelta(hours=2)
295+
296+
266297
def test_construction():
267298
expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8")
268299
assert Timedelta(10, unit="D")._value == expected

0 commit comments

Comments
 (0)