Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,7 @@ Timedelta
^^^^^^^^^
- Accuracy improvement in :meth:`Timedelta.to_pytimedelta` to round microseconds consistently for large nanosecond based Timedelta (:issue:`57841`)
- Bug in :class:`Timedelta` constructor failing to raise when passed an invalid keyword (:issue:`53801`)
- 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`)
- Bug in :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`)
- Bug in multiplication operations with ``timedelta64`` dtype failing to raise ``TypeError`` when multiplying by ``bool`` objects or dtypes (:issue:`58054`)

Expand Down
29 changes: 26 additions & 3 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,8 @@ class Timedelta(_Timedelta):
"milliseconds", "microseconds", "nanoseconds"}

def __new__(cls, object value=_no_input, unit=None, **kwargs):
cdef NPY_DATETIMEUNIT in_reso
cdef NPY_DATETIMEUNIT target_reso
unsupported_kwargs = set(kwargs)
unsupported_kwargs.difference_update(cls._req_any_kwargs_new)
if unsupported_kwargs or (
Expand Down Expand Up @@ -2134,9 +2136,30 @@ class Timedelta(_Timedelta):
return cls._from_value_and_reso(new_value, reso=new_reso)

elif is_tick_object(value):
new_reso = get_supported_reso(value._creso)
new_value = delta_to_nanoseconds(value, reso=new_reso)
return cls._from_value_and_reso(new_value, reso=new_reso)
# GH#62310
# Handle Tick offsets
in_reso = value._creso

if in_reso == NPY_DATETIMEUNIT.NPY_FR_ns:
target_reso = NPY_DATETIMEUNIT.NPY_FR_ns
elif in_reso == NPY_DATETIMEUNIT.NPY_FR_us:
target_reso = NPY_DATETIMEUNIT.NPY_FR_us
elif in_reso == NPY_DATETIMEUNIT.NPY_FR_ms:
target_reso = NPY_DATETIMEUNIT.NPY_FR_ms
elif in_reso in (
NPY_DATETIMEUNIT.NPY_FR_s,
NPY_DATETIMEUNIT.NPY_FR_m,
NPY_DATETIMEUNIT.NPY_FR_h,
):
target_reso = NPY_DATETIMEUNIT.NPY_FR_s
else:
raise ValueError(
f"Value must be Timedelta, string, integer, float, timedelta "
f"or convertible, not {type(value).__name__}"
)

new_value = delta_to_nanoseconds(value, reso=target_reso)
return cls._from_value_and_reso(new_value, reso=target_reso)

elif is_integer_object(value) or is_float_object(value):
# unit=None is de-facto 'ns'
Expand Down
31 changes: 31 additions & 0 deletions pandas/tests/scalar/timedelta/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
NaT,
Timedelta,
TimedeltaIndex,
Timestamp,
offsets,
to_timedelta,
)
Expand Down Expand Up @@ -263,6 +264,36 @@ def test_from_tick_reso():
Timedelta(tick)


def test_tick_offset_arithmetic_consistency():
# GH#62310: Timedelta construction from Tick offset objects should
# behave consistently with Timedelta keyword arguments

# Construct Timedelta from Tick offset vs keyword argument
tick_td = Timedelta(offsets.Hour(1))
kwarg_td = Timedelta(hours=1)

# Both should represent the same duration
assert tick_td == kwarg_td
assert tick_td.value == kwarg_td.value

# Subtraction with Timestamp
ts = Timestamp("2020-01-01 02:00:00")
result1 = ts - tick_td
result2 = ts - kwarg_td
assert result1 == result2 == Timestamp("2020-01-01 01:00:00")

# Addition with Timestamp
ts2 = Timestamp("2020-01-01 00:00:00")
result3 = ts2 + tick_td
result4 = ts2 + kwarg_td
assert result3 == result4 == Timestamp("2020-01-01 01:00:00")

# Timedelta arithmetic
assert tick_td - kwarg_td == Timedelta(0)
assert kwarg_td - tick_td == Timedelta(0)
assert tick_td + kwarg_td == Timedelta(hours=2)


def test_construction():
expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8")
assert Timedelta(10, unit="D")._value == expected
Expand Down
Loading