Skip to content

Commit ac05d29

Browse files
authored
API: Timedelta constructor pytimedelta, Tick preserve reso (pandas-dev#48918)
* BUG: Timedelta.__new__ * remove assertion * GH refs * API: Timedelta(td64_obj) retain resolution * API: Timedelta constructor pytimedelta, Tick preserve reso * remove debugging variable * remove duplicate
1 parent 77d4c3e commit ac05d29

File tree

8 files changed

+104
-23
lines changed

8 files changed

+104
-23
lines changed

pandas/_libs/tslibs/timedeltas.pyx

+15-5
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,9 @@ cdef convert_to_timedelta64(object ts, str unit):
341341
elif isinstance(ts, _Timedelta):
342342
# already in the proper format
343343
if ts._reso != NPY_FR_ns:
344-
raise NotImplementedError
345-
ts = np.timedelta64(ts.value, "ns")
344+
ts = ts._as_unit("ns").asm8
345+
else:
346+
ts = np.timedelta64(ts.value, "ns")
346347
elif is_timedelta64_object(ts):
347348
ts = ensure_td64ns(ts)
348349
elif is_integer_object(ts):
@@ -1706,7 +1707,13 @@ class Timedelta(_Timedelta):
17061707
value = parse_timedelta_string(value)
17071708
value = np.timedelta64(value)
17081709
elif PyDelta_Check(value):
1709-
value = convert_to_timedelta64(value, 'ns')
1710+
# pytimedelta object -> microsecond resolution
1711+
new_value = delta_to_nanoseconds(
1712+
value, reso=NPY_DATETIMEUNIT.NPY_FR_us
1713+
)
1714+
return cls._from_value_and_reso(
1715+
new_value, reso=NPY_DATETIMEUNIT.NPY_FR_us
1716+
)
17101717
elif is_timedelta64_object(value):
17111718
# Retain the resolution if possible, otherwise cast to the nearest
17121719
# supported resolution.
@@ -1720,7 +1727,7 @@ class Timedelta(_Timedelta):
17201727
if reso != NPY_DATETIMEUNIT.NPY_FR_GENERIC:
17211728
try:
17221729
new_value = convert_reso(
1723-
get_timedelta64_value(value),
1730+
new_value,
17241731
reso,
17251732
new_reso,
17261733
round_ok=True,
@@ -1730,7 +1737,10 @@ class Timedelta(_Timedelta):
17301737
return cls._from_value_and_reso(new_value, reso=new_reso)
17311738

17321739
elif is_tick_object(value):
1733-
value = np.timedelta64(value.nanos, 'ns')
1740+
new_reso = get_supported_reso(value._reso)
1741+
new_value = delta_to_nanoseconds(value, reso=new_reso)
1742+
return cls._from_value_and_reso(new_value, reso=new_reso)
1743+
17341744
elif is_integer_object(value) or is_float_object(value):
17351745
# unit=None is de-facto 'ns'
17361746
unit = parse_timedelta_unit(unit)

pandas/core/arrays/masked.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
lib,
1818
missing as libmissing,
1919
)
20+
from pandas._libs.tslibs import (
21+
get_unit_from_dtype,
22+
is_supported_unit,
23+
)
2024
from pandas._typing import (
2125
ArrayLike,
2226
AstypeArg,
@@ -750,12 +754,16 @@ def _maybe_mask_result(self, result, mask):
750754

751755
return BooleanArray(result, mask, copy=False)
752756

753-
elif result.dtype == "timedelta64[ns]":
757+
elif (
758+
isinstance(result.dtype, np.dtype)
759+
and result.dtype.kind == "m"
760+
and is_supported_unit(get_unit_from_dtype(result.dtype))
761+
):
754762
# e.g. test_numeric_arr_mul_tdscalar_numexpr_path
755763
from pandas.core.arrays import TimedeltaArray
756764

757765
if not isinstance(result, TimedeltaArray):
758-
result = TimedeltaArray._simple_new(result)
766+
result = TimedeltaArray._simple_new(result, dtype=result.dtype)
759767

760768
result[mask] = result.dtype.type("NaT")
761769
return result

pandas/core/arrays/timedeltas.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,10 @@ def _generate_range(cls, start, end, periods, freq, closed=None):
257257
)
258258

259259
if start is not None:
260-
start = Timedelta(start)
260+
start = Timedelta(start)._as_unit("ns")
261261

262262
if end is not None:
263-
end = Timedelta(end)
263+
end = Timedelta(end)._as_unit("ns")
264264

265265
left_closed, right_closed = validate_endpoints(closed)
266266

pandas/tests/arithmetic/test_numeric.py

+19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
from collections import abc
7+
from datetime import timedelta
78
from decimal import Decimal
89
import operator
910
from typing import Any
@@ -27,6 +28,7 @@
2728
Int64Index,
2829
UInt64Index,
2930
)
31+
from pandas.core.arrays import TimedeltaArray
3032
from pandas.core.computation import expressions as expr
3133
from pandas.tests.arithmetic.common import (
3234
assert_invalid_addsub_type,
@@ -209,6 +211,11 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box_with_array):
209211
tda = expected._data
210212
dtype = scalar_td.dtype
211213
expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype)
214+
elif type(scalar_td) is timedelta and box not in [Index, Series]:
215+
# TODO(2.0): once TDA.astype converts to m8, just do expected.astype
216+
tda = expected._data
217+
dtype = np.dtype("m8[us]")
218+
expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype)
212219

213220
index = tm.box_expected(index, box)
214221
expected = tm.box_expected(expected, box)
@@ -240,6 +247,13 @@ def test_numeric_arr_mul_tdscalar_numexpr_path(
240247
obj = tm.box_expected(arr, box, transpose=False)
241248

242249
expected = arr_i8.view("timedelta64[D]").astype("timedelta64[ns]")
250+
if type(scalar_td) is timedelta and box is array:
251+
# TODO(2.0): this shouldn't depend on 'box'
252+
expected = expected.astype("timedelta64[us]")
253+
# TODO(2.0): won't be necessary to construct TimedeltaArray
254+
# explicitly.
255+
expected = TimedeltaArray._simple_new(expected, dtype=expected.dtype)
256+
243257
expected = tm.box_expected(expected, box, transpose=False)
244258

245259
result = obj * scalar_td
@@ -262,6 +276,11 @@ def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box_with_array
262276
# i.e. resolution is lower -> use lowest supported resolution
263277
dtype = np.dtype("m8[s]")
264278
expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype)
279+
elif type(three_days) is timedelta and box not in [Index, Series]:
280+
# TODO(2.0): just use expected.astype
281+
tda = expected._data
282+
dtype = np.dtype("m8[us]")
283+
expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype)
265284

266285
index = tm.box_expected(index, box)
267286
expected = tm.box_expected(expected, box)

pandas/tests/dtypes/cast/test_promote.py

+6
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ def test_maybe_promote_any_with_timedelta64(
480480
"Timedelta scalar"
481481
)
482482
request.node.add_marker(mark)
483+
elif type(fill_value) is datetime.timedelta:
484+
mark = pytest.mark.xfail(
485+
reason="maybe_promote not yet updated to handle non-nano "
486+
"Timedelta scalar"
487+
)
488+
request.node.add_marker(mark)
483489
else:
484490
expected_dtype = np.dtype(object)
485491
exp_val_for_scalar = fill_value

pandas/tests/io/json/test_pandas.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1039,11 +1039,11 @@ def test_timedelta(self):
10391039
tm.assert_frame_equal(frame, result)
10401040

10411041
def test_mixed_timedelta_datetime(self):
1042-
frame = DataFrame({"a": [timedelta(23), Timestamp("20130101")]}, dtype=object)
1042+
td = timedelta(23)
1043+
ts = Timestamp("20130101")
1044+
frame = DataFrame({"a": [td, ts]}, dtype=object)
10431045

1044-
expected = DataFrame(
1045-
{"a": [pd.Timedelta(frame.a[0]).value, Timestamp(frame.a[1]).value]}
1046-
)
1046+
expected = DataFrame({"a": [pd.Timedelta(td)._as_unit("ns").value, ts.value]})
10471047
result = read_json(frame.to_json(date_unit="ns"), dtype={"a": "int64"})
10481048
tm.assert_frame_equal(result, expected, check_index_type=False)
10491049

pandas/tests/scalar/timedelta/test_constructors.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,38 @@ def test_from_td64_retain_resolution():
6161
assert td3._reso == NpyDatetimeUnit.NPY_FR_us.value
6262

6363

64+
def test_from_pytimedelta_us_reso():
65+
# pytimedelta has microsecond resolution, so Timedelta(pytd) inherits that
66+
td = timedelta(days=4, minutes=3)
67+
result = Timedelta(td)
68+
assert result.to_pytimedelta() == td
69+
assert result._reso == NpyDatetimeUnit.NPY_FR_us.value
70+
71+
72+
def test_from_tick_reso():
73+
tick = offsets.Nano()
74+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_ns.value
75+
76+
tick = offsets.Micro()
77+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_us.value
78+
79+
tick = offsets.Milli()
80+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_ms.value
81+
82+
tick = offsets.Second()
83+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value
84+
85+
# everything above Second gets cast to the closest supported reso: second
86+
tick = offsets.Minute()
87+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value
88+
89+
tick = offsets.Hour()
90+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value
91+
92+
tick = offsets.Day()
93+
assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value
94+
95+
6496
def test_construction():
6597
expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8")
6698
assert Timedelta(10, unit="d").value == expected
@@ -248,9 +280,10 @@ def test_overflow_on_construction():
248280
with pytest.raises(OutOfBoundsTimedelta, match=msg):
249281
Timedelta(7 * 19999, unit="D")
250282

251-
msg = "Cannot cast 259987 days, 0:00:00 to unit=ns without overflow"
252-
with pytest.raises(OutOfBoundsTimedelta, match=msg):
253-
Timedelta(timedelta(days=13 * 19999))
283+
# used to overflow before non-ns support
284+
td = Timedelta(timedelta(days=13 * 19999))
285+
assert td._reso == NpyDatetimeUnit.NPY_FR_us.value
286+
assert td.days == 13 * 19999
254287

255288

256289
@pytest.mark.parametrize(

pandas/tests/scalar/timedelta/test_timedelta.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,16 @@ def test_truediv_timedeltalike(self, td):
183183
assert (2.5 * td) / td == 2.5
184184

185185
other = Timedelta(td.value)
186-
msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow"
186+
msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow."
187187
with pytest.raises(OutOfBoundsTimedelta, match=msg):
188188
td / other
189189

190-
with pytest.raises(OutOfBoundsTimedelta, match=msg):
191-
# __rtruediv__
192-
other.to_pytimedelta() / td
190+
# Timedelta(other.to_pytimedelta()) has microsecond resolution,
191+
# so the division doesn't require casting all the way to nanos,
192+
# so succeeds
193+
res = other.to_pytimedelta() / td
194+
expected = other.to_pytimedelta() / td.to_pytimedelta()
195+
assert res == expected
193196

194197
# if there's no overflow, we cast to the higher reso
195198
left = Timedelta._from_value_and_reso(50, NpyDatetimeUnit.NPY_FR_us.value)
@@ -220,9 +223,11 @@ def test_floordiv_timedeltalike(self, td):
220223
with pytest.raises(OutOfBoundsTimedelta, match=msg):
221224
td // other
222225

223-
with pytest.raises(ValueError, match=msg):
224-
# __rfloordiv__
225-
other.to_pytimedelta() // td
226+
# Timedelta(other.to_pytimedelta()) has microsecond resolution,
227+
# so the floordiv doesn't require casting all the way to nanos,
228+
# so succeeds
229+
res = other.to_pytimedelta() // td
230+
assert res == 0
226231

227232
# if there's no overflow, we cast to the higher reso
228233
left = Timedelta._from_value_and_reso(50050, NpyDatetimeUnit.NPY_FR_us.value)

0 commit comments

Comments
 (0)