Skip to content

Commit 3af431f

Browse files
committed
pd.Timedelta(integer, unit=unit) give the requested unit
1 parent 6e59a2d commit 3af431f

File tree

10 files changed

+53
-30
lines changed

10 files changed

+53
-30
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ In cases with mixed-resolution inputs, the highest resolution is used:
380380

381381
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.
382382

383+
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").
384+
383385
.. _whatsnew_300.api_breaking.concat_datetime_sorting:
384386

385387
:func:`concat` no longer ignores ``sort`` when all objects have a :class:`DatetimeIndex`

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -290,22 +290,24 @@ cpdef int64_t delta_to_nanoseconds(
290290
) from err
291291

292292

293-
cdef _numeric_to_td64ns(object item, str unit):
293+
cdef int64_t _numeric_to_td64ns(
294+
object item, str unit, NPY_DATETIMEUNIT out_reso=NPY_FR_ns
295+
):
294296
# caller is responsible for checking
295297
# assert unit not in ["Y", "y", "M"]
296298
# assert is_integer_object(item) or is_float_object(item)
297299
if is_integer_object(item) and item == NPY_NAT:
298-
return np.timedelta64(NPY_NAT, "ns")
300+
return NPY_NAT
299301

300302
try:
301-
item = cast_from_unit(item, unit)
303+
ival = cast_from_unit(item, unit, out_reso)
302304
except OutOfBoundsDatetime as err:
305+
abbrev = npy_unit_to_abbrev(out_reso)
303306
raise OutOfBoundsTimedelta(
304-
f"Cannot cast {item} from {unit} to 'ns' without overflow."
307+
f"Cannot cast {item} from {unit} to '{abbrev}' without overflow."
305308
) from err
306309

307-
ts = np.timedelta64(item, "ns")
308-
return ts
310+
return ival
309311

310312

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

473475
elif is_integer_object(item) or is_float_object(item):
474-
td64ns_obj = _numeric_to_td64ns(item, parsed_unit)
475-
ival = cnp.get_timedelta64_value(td64ns_obj)
476+
ival = _numeric_to_td64ns(item, parsed_unit, NPY_FR_ns)
476477

477478
item_reso = NPY_FR_ns
478479
state.update_creso(item_reso)
@@ -2230,7 +2231,18 @@ class Timedelta(_Timedelta):
22302231
elif checknull_with_nat_and_na(value):
22312232
return NaT
22322233

2233-
elif is_integer_object(value) or is_float_object(value):
2234+
elif is_integer_object(value):
2235+
# unit=None is de-facto 'ns'
2236+
if value != NPY_NAT:
2237+
unit = parse_timedelta_unit(unit)
2238+
if unit != "ns":
2239+
# Return with the closest-to-supported unit by going through
2240+
# the timedelta64 path
2241+
td = np.timedelta64(value, unit)
2242+
return cls(td)
2243+
value = _numeric_to_td64ns(value, unit)
2244+
2245+
elif is_float_object(value):
22342246
# unit=None is de-facto 'ns'
22352247
unit = parse_timedelta_unit(unit)
22362248
value = _numeric_to_td64ns(value, unit)

pandas/tests/arithmetic/test_timedelta64.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
from pandas._libs.tslibs import timezones
1212
from pandas.compat import WASM
13-
from pandas.errors import OutOfBoundsDatetime
1413
import pandas.util._test_decorators as td
1514

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

736731
_NaT = NaT._value + 1
737732
msg = "Overflow in int64 addition"

pandas/tests/frame/indexing/test_mask.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,15 @@ def test_mask_where_dtype_timedelta():
127127
# https://github.com/pandas-dev/pandas/issues/39548
128128
df = DataFrame([Timedelta(i, unit="D") for i in range(5)])
129129

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

133133
expected = DataFrame(
134134
[np.nan, np.nan, np.nan, Timedelta("3 day"), Timedelta("4 day")],
135-
dtype="m8[ns]",
135+
dtype="m8[s]",
136136
)
137-
tm.assert_frame_equal(df.where(df > Timedelta(2, unit="D")), expected)
137+
result = df.where(df > Timedelta(2, unit="D"))
138+
tm.assert_frame_equal(result, expected)
138139

139140

140141
def test_mask_return_dtype():

pandas/tests/frame/indexing/test_setitem.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,8 +1000,8 @@ def test_loc_expansion_with_timedelta_type(self):
10001000
index=Index([0]),
10011001
columns=(["a", "b", "c"]),
10021002
)
1003-
expected["a"] = expected["a"].astype("m8[ns]")
1004-
expected["b"] = expected["b"].astype("m8[ns]")
1003+
expected["a"] = expected["a"].astype("m8[s]")
1004+
expected["b"] = expected["b"].astype("m8[s]")
10051005
tm.assert_frame_equal(result, expected)
10061006

10071007
def test_setitem_tuple_key_in_empty_frame(self):

pandas/tests/frame/test_constructors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@ def create_data(constructor):
893893
[
894894
(lambda x: np.timedelta64(x, "D"), "m8[s]"),
895895
(lambda x: timedelta(days=x), "m8[us]"),
896-
(lambda x: Timedelta(x, "D"), "m8[ns]"),
896+
(lambda x: Timedelta(x, "D"), "m8[s]"),
897897
(lambda x: Timedelta(x, "D").as_unit("s"), "m8[s]"),
898898
],
899899
)

pandas/tests/io/json/test_pandas.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,15 +1141,15 @@ def test_timedelta(self):
11411141
)
11421142
with tm.assert_produces_warning(Pandas4Warning, match=msg):
11431143
result = read_json(StringIO(ser.to_json()), typ="series").apply(converter)
1144-
tm.assert_series_equal(result, ser)
1144+
tm.assert_series_equal(result, ser.astype("m8[ms]"))
11451145

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

11541154
frame = DataFrame([timedelta(23), timedelta(seconds=5)], dtype="m8[ns]")
11551155
assert frame[0].dtype == "timedelta64[ns]"

pandas/tests/scalar/timedelta/test_arithmetic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def test_td_add_datetimelike_scalar(self, op):
119119

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

pandas/tests/scalar/timedelta/test_constructors.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ def test_noninteger_microseconds(self):
3535

3636

3737
class TestTimedeltaConstructorUnitKeyword:
38+
def test_result_unit(self):
39+
# For supported units, we get result.unit == unit
40+
for unit in ["s", "ms", "us", "ns"]:
41+
td = Timedelta(1, unit=unit)
42+
assert td.unit == unit
43+
44+
# For non-supported units we get the closest-supported unit
45+
for unit in ["W", "D", "h", "m"]:
46+
td = Timedelta(1, unit=unit)
47+
assert td.unit == "s"
48+
3849
@pytest.mark.parametrize("unit", ["Y", "y", "M"])
3950
def test_unit_m_y_raises(self, unit):
4051
msg = "Units 'M', 'Y', and 'y' are no longer supported"
@@ -196,7 +207,8 @@ def test_construct_from_kwargs_overflow():
196207

197208
def test_construct_with_weeks_unit_overflow():
198209
# GH#47268 don't silently wrap around
199-
with pytest.raises(OutOfBoundsTimedelta, match="without overflow"):
210+
msg = "1000000000000000000 weeks"
211+
with pytest.raises(OutOfBoundsTimedelta, match=msg):
200212
Timedelta(1000000000000000000, unit="W")
201213

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

285297
def test_construction():
286298
expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8")
287-
assert Timedelta(10, unit="D")._value == expected
299+
assert Timedelta(10, unit="D")._value == expected // 10**9
288300
assert Timedelta(10.0, unit="D")._value == expected
289301
assert Timedelta("10 days")._value == expected // 1000
290302
assert Timedelta(days=10)._value == expected // 1000
@@ -464,9 +476,9 @@ def test_overflow_on_construction():
464476
Timedelta(value)
465477

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

471483
# used to overflow before non-ns support
472484
td = Timedelta(timedelta(days=13 * 19999))

pandas/tests/tools/test_to_datetime.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3263,7 +3263,8 @@ def test_epoch(self, units, epochs):
32633263
epoch_1960 = Timestamp(1960, 1, 1)
32643264
units_from_epochs = np.arange(5, dtype=np.int64)
32653265
expected = Series(
3266-
[pd.Timedelta(x, unit=units) + epoch_1960 for x in units_from_epochs]
3266+
[pd.Timedelta(x, unit=units) + epoch_1960 for x in units_from_epochs],
3267+
dtype="M8[ns]",
32673268
)
32683269

32693270
result = Series(to_datetime(units_from_epochs, unit=units, origin=epochs))

0 commit comments

Comments
 (0)