Skip to content

Commit d0608ba

Browse files
jbrockmendelyehoshuadimarsky
authored andcommitted
ENH: ints_to_pytimedelta support non-nano (pandas-dev#46828)
1 parent 7efe3bb commit d0608ba

File tree

5 files changed

+77
-9
lines changed

5 files changed

+77
-9
lines changed

pandas/_libs/tslibs/timedeltas.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ UnitChoices = Literal[
6363
_S = TypeVar("_S", bound=timedelta)
6464

6565
def ints_to_pytimedelta(
66-
arr: npt.NDArray[np.int64], # const int64_t[:]
66+
arr: npt.NDArray[np.timedelta64],
6767
box: bool = ...,
6868
) -> npt.NDArray[np.object_]: ...
6969
def array_to_timedelta64(

pandas/_libs/tslibs/timedeltas.pyx

+45-5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ from pandas._libs.tslibs.np_datetime cimport (
4949
cmp_scalar,
5050
get_datetime64_unit,
5151
get_timedelta64_value,
52+
get_unit_from_dtype,
5253
npy_datetimestruct,
5354
pandas_datetime_to_datetimestruct,
5455
pandas_timedelta_to_timedeltastruct,
@@ -141,14 +142,14 @@ _no_input = object()
141142

142143
@cython.boundscheck(False)
143144
@cython.wraparound(False)
144-
def ints_to_pytimedelta(const int64_t[:] arr, box=False):
145+
def ints_to_pytimedelta(ndarray m8values, box=False):
145146
"""
146147
convert an i8 repr to an ndarray of timedelta or Timedelta (if box ==
147148
True)
148149
149150
Parameters
150151
----------
151-
arr : ndarray[int64_t]
152+
arr : ndarray[timedelta64]
152153
box : bool, default False
153154
154155
Returns
@@ -157,9 +158,12 @@ def ints_to_pytimedelta(const int64_t[:] arr, box=False):
157158
array of Timedelta or timedeltas objects
158159
"""
159160
cdef:
160-
Py_ssize_t i, n = len(arr)
161+
Py_ssize_t i, n = m8values.size
161162
int64_t value
162163
object[::1] result = np.empty(n, dtype=object)
164+
NPY_DATETIMEUNIT reso = get_unit_from_dtype(m8values.dtype)
165+
166+
arr = m8values.view("i8")
163167

164168
for i in range(n):
165169

@@ -168,9 +172,26 @@ def ints_to_pytimedelta(const int64_t[:] arr, box=False):
168172
result[i] = <object>NaT
169173
else:
170174
if box:
171-
result[i] = Timedelta(value)
172-
else:
175+
result[i] = _timedelta_from_value_and_reso(value, reso=reso)
176+
elif reso == NPY_DATETIMEUNIT.NPY_FR_ns:
173177
result[i] = timedelta(microseconds=int(value) / 1000)
178+
elif reso == NPY_DATETIMEUNIT.NPY_FR_us:
179+
result[i] = timedelta(microseconds=value)
180+
elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
181+
result[i] = timedelta(milliseconds=value)
182+
elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
183+
result[i] = timedelta(seconds=value)
184+
elif reso == NPY_DATETIMEUNIT.NPY_FR_m:
185+
result[i] = timedelta(minutes=value)
186+
elif reso == NPY_DATETIMEUNIT.NPY_FR_h:
187+
result[i] = timedelta(hours=value)
188+
elif reso == NPY_DATETIMEUNIT.NPY_FR_D:
189+
result[i] = timedelta(days=value)
190+
elif reso == NPY_DATETIMEUNIT.NPY_FR_W:
191+
result[i] = timedelta(weeks=value)
192+
else:
193+
# Month, Year, NPY_FR_GENERIC, pico, fempto, atto
194+
raise NotImplementedError(reso)
174195

175196
return result.base # .base to access underlying np.ndarray
176197

@@ -1530,6 +1551,9 @@ class Timedelta(_Timedelta):
15301551
int64_t result, unit, remainder
15311552
ndarray[int64_t] arr
15321553

1554+
if self._reso != NPY_FR_ns:
1555+
raise NotImplementedError
1556+
15331557
from pandas._libs.tslibs.offsets import to_offset
15341558
unit = to_offset(freq).nanos
15351559

@@ -1620,6 +1644,8 @@ class Timedelta(_Timedelta):
16201644

16211645
elif is_integer_object(other) or is_float_object(other):
16221646
# integers or floats
1647+
if self._reso != NPY_FR_ns:
1648+
raise NotImplementedError
16231649
return Timedelta(self.value / other, unit='ns')
16241650

16251651
elif is_array(other):
@@ -1633,6 +1659,8 @@ class Timedelta(_Timedelta):
16331659
other = Timedelta(other)
16341660
if other is NaT:
16351661
return np.nan
1662+
if self._reso != NPY_FR_ns:
1663+
raise NotImplementedError
16361664
return float(other.value) / self.value
16371665

16381666
elif is_array(other):
@@ -1651,17 +1679,25 @@ class Timedelta(_Timedelta):
16511679
other = Timedelta(other)
16521680
if other is NaT:
16531681
return np.nan
1682+
if self._reso != NPY_FR_ns:
1683+
raise NotImplementedError
16541684
return self.value // other.value
16551685

16561686
elif is_integer_object(other) or is_float_object(other):
1687+
if self._reso != NPY_FR_ns:
1688+
raise NotImplementedError
16571689
return Timedelta(self.value // other, unit='ns')
16581690

16591691
elif is_array(other):
16601692
if other.dtype.kind == 'm':
16611693
# also timedelta-like
1694+
if self._reso != NPY_FR_ns:
1695+
raise NotImplementedError
16621696
return _broadcast_floordiv_td64(self.value, other, _floordiv)
16631697
elif other.dtype.kind in ['i', 'u', 'f']:
16641698
if other.ndim == 0:
1699+
if self._reso != NPY_FR_ns:
1700+
raise NotImplementedError
16651701
return Timedelta(self.value // other)
16661702
else:
16671703
return self.to_timedelta64() // other
@@ -1678,11 +1714,15 @@ class Timedelta(_Timedelta):
16781714
other = Timedelta(other)
16791715
if other is NaT:
16801716
return np.nan
1717+
if self._reso != NPY_FR_ns:
1718+
raise NotImplementedError
16811719
return other.value // self.value
16821720

16831721
elif is_array(other):
16841722
if other.dtype.kind == 'm':
16851723
# also timedelta-like
1724+
if self._reso != NPY_FR_ns:
1725+
raise NotImplementedError
16861726
return _broadcast_floordiv_td64(self.value, other, _rfloordiv)
16871727

16881728
# Includes integer array // Timedelta, disallowed in GH#19761

pandas/core/arrays/datetimelike.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def astype(self, dtype, copy: bool = True):
432432

433433
elif self.dtype.kind == "m":
434434
i8data = self.asi8.ravel()
435-
converted = ints_to_pytimedelta(i8data, box=True)
435+
converted = ints_to_pytimedelta(self._ndarray.ravel(), box=True)
436436
return converted.reshape(self.shape)
437437

438438
return self._box_values(self.asi8.ravel()).reshape(self.shape)

pandas/core/arrays/timedeltas.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ def __iter__(self):
369369
yield self[i]
370370
else:
371371
# convert in chunks of 10k for efficiency
372-
data = self.asi8
372+
data = self._ndarray
373373
length = len(self)
374374
chunksize = 10000
375375
chunks = (length // chunksize) + 1
@@ -886,7 +886,7 @@ def to_pytimedelta(self) -> np.ndarray:
886886
-------
887887
timedeltas : ndarray[object]
888888
"""
889-
return tslibs.ints_to_pytimedelta(self.asi8)
889+
return tslibs.ints_to_pytimedelta(self._ndarray)
890890

891891
days = _field_accessor("days", "days", "Number of days for each element.")
892892
seconds = _field_accessor(

pandas/tests/tslibs/test_timedeltas.py

+28
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from pandas._libs.tslibs.timedeltas import (
77
array_to_timedelta64,
88
delta_to_nanoseconds,
9+
ints_to_pytimedelta,
910
)
1011

1112
from pandas import (
1213
Timedelta,
1314
offsets,
1415
)
16+
import pandas._testing as tm
1517

1618

1719
@pytest.mark.parametrize(
@@ -89,3 +91,29 @@ def test_array_to_timedelta64_non_object_raises(self):
8991
msg = "'values' must have object dtype"
9092
with pytest.raises(TypeError, match=msg):
9193
array_to_timedelta64(values)
94+
95+
96+
@pytest.mark.parametrize("unit", ["s", "ms", "us"])
97+
def test_ints_to_pytimedelta(unit):
98+
# tests for non-nanosecond cases
99+
arr = np.arange(6, dtype=np.int64).view(f"m8[{unit}]")
100+
101+
res = ints_to_pytimedelta(arr, box=False)
102+
# For non-nanosecond, .astype(object) gives pytimedelta objects
103+
# instead of integers
104+
expected = arr.astype(object)
105+
tm.assert_numpy_array_equal(res, expected)
106+
107+
res = ints_to_pytimedelta(arr, box=True)
108+
expected = np.array([Timedelta(x) for x in arr], dtype=object)
109+
tm.assert_numpy_array_equal(res, expected)
110+
111+
112+
@pytest.mark.parametrize("unit", ["Y", "M", "ps", "fs", "as"])
113+
def test_ints_to_pytimedelta_unsupported(unit):
114+
arr = np.arange(6, dtype=np.int64).view(f"m8[{unit}]")
115+
116+
with pytest.raises(NotImplementedError, match=r"\d{1,2}"):
117+
ints_to_pytimedelta(arr, box=False)
118+
with pytest.raises(NotImplementedError, match=r"\d{1,2}"):
119+
ints_to_pytimedelta(arr, box=True)

0 commit comments

Comments
 (0)