Skip to content
Open
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
3 changes: 3 additions & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ In cases with mixed-resolution inputs, the highest resolution is used:

Similarly, the :class:`Timedelta` constructor and :func:`to_timedelta` with a string input now defaults to a microsecond unit, using nanosecond unit only in cases that actually have nanosecond precision.

Moreover, passing an integer to the :class:`Timedelta` constructor or :func:`to_timedelta` along with a ``unit`` will now return an object with that unit when possible, or the closest-supported unit for non-supported units ("W", "D", "h", "m").

.. _whatsnew_300.api_breaking.concat_datetime_sorting:

:func:`concat` no longer ignores ``sort`` when all objects have a :class:`DatetimeIndex`
Expand Down Expand Up @@ -1131,6 +1133,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 :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`)
- Bug in adding or subtracting a :class:`Timedelta` object with non-nanosecond unit to a python ``datetime.datetime`` object giving incorrect results; this now works correctly for Timedeltas inside the ``datetime.timedelta`` implementation bounds (:issue:`53643`)
- Bug in multiplication operations with ``timedelta64`` dtype failing to raise ``TypeError`` when multiplying by ``bool`` objects or dtypes (:issue:`58054`)
- Bug in multiplication operations with ``timedelta64`` dtype incorrectly raising when multiplying by numpy-nullable dtypes or pyarrow integer dtypes (:issue:`58054`)

Expand Down
50 changes: 38 additions & 12 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -290,22 +290,24 @@ cpdef int64_t delta_to_nanoseconds(
) from err


cdef _numeric_to_td64ns(object item, str unit):
cdef int64_t _numeric_to_td64ns(
object item, str unit, NPY_DATETIMEUNIT out_reso=NPY_FR_ns
):
# caller is responsible for checking
# assert unit not in ["Y", "y", "M"]
# assert is_integer_object(item) or is_float_object(item)
if is_integer_object(item) and item == NPY_NAT:
return np.timedelta64(NPY_NAT, "ns")
return NPY_NAT

try:
item = cast_from_unit(item, unit)
ival = cast_from_unit(item, unit, out_reso)
except OutOfBoundsDatetime as err:
abbrev = npy_unit_to_abbrev(out_reso)
raise OutOfBoundsTimedelta(
f"Cannot cast {item} from {unit} to 'ns' without overflow."
f"Cannot cast {item} from {unit} to '{abbrev}' without overflow."
) from err

ts = np.timedelta64(item, "ns")
return ts
return ival


# TODO: de-duplicate with DatetimeParseState
Expand Down Expand Up @@ -352,7 +354,7 @@ def array_to_timedelta64(
cdef:
Py_ssize_t i, n = values.size
ndarray result = np.empty((<object>values).shape, dtype="m8[ns]")
object item, td64ns_obj
object item
int64_t ival
cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values)
cnp.flatiter it
Expand Down Expand Up @@ -471,8 +473,7 @@ def array_to_timedelta64(
ival = delta_to_nanoseconds(item, reso=creso)

elif is_integer_object(item) or is_float_object(item):
td64ns_obj = _numeric_to_td64ns(item, parsed_unit)
ival = cnp.get_timedelta64_value(td64ns_obj)
ival = _numeric_to_td64ns(item, parsed_unit, NPY_FR_ns)

item_reso = NPY_FR_ns
state.update_creso(item_reso)
Expand Down Expand Up @@ -1022,9 +1023,23 @@ cdef _timedelta_from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso):
elif reso == NPY_DATETIMEUNIT.NPY_FR_us:
td_base = _Timedelta.__new__(cls, microseconds=int(value))
elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
td_base = _Timedelta.__new__(cls, milliseconds=0)
if value > -86_399_999_913_600_000 and value < 86_400_000_000_000_000:
# i.e. we are in range for pytimedelta. By passing the
# 'correct' value here we can
# make pydatetime + Timedelta operations work correctly,
# xref GH#53643
td_base = _Timedelta.__new__(cls, milliseconds=value)
else:
td_base = _Timedelta.__new__(cls, milliseconds=0)
elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
td_base = _Timedelta.__new__(cls, seconds=0)
if value > -86_399_999_913_600 and value < 86_400_000_000_000:
# i.e. we are in range for pytimedelta. By passing the
# 'correct' value here we can
# make pydatetime + Timedelta operations work correctly,
# xref GH#53643
td_base = _Timedelta.__new__(cls, seconds=value)
else:
td_base = _Timedelta.__new__(cls, seconds=0)
# Other resolutions are disabled but could potentially be implemented here:
# elif reso == NPY_DATETIMEUNIT.NPY_FR_m:
# td_base = _Timedelta.__new__(Timedelta, minutes=int(value))
Expand Down Expand Up @@ -2216,7 +2231,18 @@ class Timedelta(_Timedelta):
elif checknull_with_nat_and_na(value):
return NaT

elif is_integer_object(value) or is_float_object(value):
elif is_integer_object(value):
# unit=None is de-facto 'ns'
if value != NPY_NAT:
unit = parse_timedelta_unit(unit)
if unit != "ns":
# Return with the closest-to-supported unit by going through
# the timedelta64 path
td = np.timedelta64(value, unit)
return cls(td)
value = _numeric_to_td64ns(value, unit)

elif is_float_object(value):
# unit=None is de-facto 'ns'
unit = parse_timedelta_unit(unit)
value = _numeric_to_td64ns(value, unit)
Expand Down
5 changes: 0 additions & 5 deletions pandas/tests/arithmetic/test_timedelta64.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

from pandas._libs.tslibs import timezones
from pandas.compat import WASM
from pandas.errors import OutOfBoundsDatetime
import pandas.util._test_decorators as td

import pandas as pd
Expand Down Expand Up @@ -728,10 +727,6 @@ def test_tdi_add_overflow(self):
# See GH#14068
# preliminary test scalar analogue of vectorized tests below
# TODO: Make raised error message more informative and test
with pytest.raises(OutOfBoundsDatetime, match="10155196800000000000"):
pd.to_timedelta(106580, "D") + Timestamp("2000")
with pytest.raises(OutOfBoundsDatetime, match="10155196800000000000"):
Timestamp("2000") + pd.to_timedelta(106580, "D")

_NaT = NaT._value + 1
msg = "Overflow in int64 addition"
Expand Down
7 changes: 4 additions & 3 deletions pandas/tests/frame/indexing/test_mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,15 @@ def test_mask_where_dtype_timedelta():
# https://github.com/pandas-dev/pandas/issues/39548
df = DataFrame([Timedelta(i, unit="D") for i in range(5)])

expected = DataFrame(np.full(5, np.nan, dtype="timedelta64[ns]"))
expected = DataFrame(np.full(5, np.nan, dtype="timedelta64[s]"))
tm.assert_frame_equal(df.mask(df.notna()), expected)

expected = DataFrame(
[np.nan, np.nan, np.nan, Timedelta("3 day"), Timedelta("4 day")],
dtype="m8[ns]",
dtype="m8[s]",
)
tm.assert_frame_equal(df.where(df > Timedelta(2, unit="D")), expected)
result = df.where(df > Timedelta(2, unit="D"))
tm.assert_frame_equal(result, expected)


def test_mask_return_dtype():
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/frame/indexing/test_setitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,8 +1000,8 @@ def test_loc_expansion_with_timedelta_type(self):
index=Index([0]),
columns=(["a", "b", "c"]),
)
expected["a"] = expected["a"].astype("m8[ns]")
expected["b"] = expected["b"].astype("m8[ns]")
expected["a"] = expected["a"].astype("m8[s]")
expected["b"] = expected["b"].astype("m8[s]")
tm.assert_frame_equal(result, expected)

def test_setitem_tuple_key_in_empty_frame(self):
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/frame/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ def create_data(constructor):
[
(lambda x: np.timedelta64(x, "D"), "m8[s]"),
(lambda x: timedelta(days=x), "m8[us]"),
(lambda x: Timedelta(x, "D"), "m8[ns]"),
(lambda x: Timedelta(x, "D"), "m8[s]"),
(lambda x: Timedelta(x, "D").as_unit("s"), "m8[s]"),
],
)
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/io/json/test_pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,15 +1141,15 @@ def test_timedelta(self):
)
with tm.assert_produces_warning(Pandas4Warning, match=msg):
result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
tm.assert_series_equal(result, ser)
tm.assert_series_equal(result, ser.astype("m8[ms]"))

ser = Series(
[timedelta(23), timedelta(seconds=5)], index=Index([0, 1]), dtype="m8[ns]"
)
assert ser.dtype == "timedelta64[ns]"
with tm.assert_produces_warning(Pandas4Warning, match=msg):
result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
tm.assert_series_equal(result, ser)
tm.assert_series_equal(result, ser.astype("m8[ms]"))

frame = DataFrame([timedelta(23), timedelta(seconds=5)], dtype="m8[ns]")
assert frame[0].dtype == "timedelta64[ns]"
Expand Down
17 changes: 16 additions & 1 deletion pandas/tests/scalar/timedelta/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ class TestTimedeltaAdditionSubtraction:
__sub__, __rsub__
"""

def test_td_add_sub_pydatetime(self, unit):
# GH#53643
td = Timedelta(hours=23).as_unit(unit)
dt = datetime(2016, 1, 1)

expected = datetime(2016, 1, 1, 23)
result = dt + td
assert result == expected
result = td + dt
assert result == expected

expected = datetime(2015, 12, 31, 1)
result = dt - td
assert result == expected

@pytest.mark.parametrize(
"ten_seconds",
[
Expand Down Expand Up @@ -104,7 +119,7 @@ def test_td_add_datetimelike_scalar(self, op):

def test_td_add_timestamp_overflow(self):
ts = Timestamp("1700-01-01").as_unit("ns")
msg = "Cannot cast 259987 from D to 'ns' without overflow."
msg = "Cannot cast 259987 days 00:00:00 to unit='ns' without overflow."
with pytest.raises(OutOfBoundsTimedelta, match=msg):
ts + Timedelta(13 * 19999, unit="D")

Expand Down
22 changes: 17 additions & 5 deletions pandas/tests/scalar/timedelta/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ def test_noninteger_microseconds(self):


class TestTimedeltaConstructorUnitKeyword:
def test_result_unit(self):
# For supported units, we get result.unit == unit
for unit in ["s", "ms", "us", "ns"]:
td = Timedelta(1, unit=unit)
assert td.unit == unit

# For non-supported units we get the closest-supported unit
for unit in ["W", "D", "h", "m"]:
td = Timedelta(1, unit=unit)
assert td.unit == "s"

@pytest.mark.parametrize("unit", ["Y", "y", "M"])
def test_unit_m_y_raises(self, unit):
msg = "Units 'M', 'Y', and 'y' are no longer supported"
Expand Down Expand Up @@ -196,7 +207,8 @@ def test_construct_from_kwargs_overflow():

def test_construct_with_weeks_unit_overflow():
# GH#47268 don't silently wrap around
with pytest.raises(OutOfBoundsTimedelta, match="without overflow"):
msg = "1000000000000000000 weeks"
with pytest.raises(OutOfBoundsTimedelta, match=msg):
Timedelta(1000000000000000000, unit="W")

with pytest.raises(OutOfBoundsTimedelta, match="without overflow"):
Expand Down Expand Up @@ -284,7 +296,7 @@ def test_from_tick_reso():

def test_construction():
expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8")
assert Timedelta(10, unit="D")._value == expected
assert Timedelta(10, unit="D")._value == expected // 10**9
assert Timedelta(10.0, unit="D")._value == expected
assert Timedelta("10 days")._value == expected // 1000
assert Timedelta(days=10)._value == expected // 1000
Expand Down Expand Up @@ -464,9 +476,9 @@ def test_overflow_on_construction():
Timedelta(value)

# xref GH#17637
msg = "Cannot cast 139993 from D to 'ns' without overflow"
with pytest.raises(OutOfBoundsTimedelta, match=msg):
Timedelta(7 * 19999, unit="D")
# used to overflows before we changed output unit to "s"
td = Timedelta(7 * 19999, unit="D")
assert td.unit == "s"

# used to overflow before non-ns support
td = Timedelta(timedelta(days=13 * 19999))
Expand Down
3 changes: 2 additions & 1 deletion pandas/tests/tools/test_to_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -3263,7 +3263,8 @@ def test_epoch(self, units, epochs):
epoch_1960 = Timestamp(1960, 1, 1)
units_from_epochs = np.arange(5, dtype=np.int64)
expected = Series(
[pd.Timedelta(x, unit=units) + epoch_1960 for x in units_from_epochs]
[pd.Timedelta(x, unit=units) + epoch_1960 for x in units_from_epochs],
dtype="M8[ns]",
)

result = Series(to_datetime(units_from_epochs, unit=units, origin=epochs))
Expand Down
Loading