diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 1df5468869df5..d75fa5c91a3df 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -765,8 +765,12 @@ def _binary_op_method_timedeltalike(op, name): # defined by Timestamp methods. elif is_array(other): - # nd-array like - if other.dtype.kind in ['m', 'M']: + if other.ndim == 0: + # see also: item_from_zerodim + item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) + return f(self, item) + + elif other.dtype.kind in ['m', 'M']: return op(self.to_timedelta64(), other) elif other.dtype.kind == 'O': return np.array([op(self, x) for x in other]) @@ -943,14 +947,18 @@ cdef _timedelta_from_value_and_reso(int64_t value, NPY_DATETIMEUNIT reso): td_base = _Timedelta.__new__(Timedelta, milliseconds=int(value)) elif reso == NPY_DATETIMEUNIT.NPY_FR_s: td_base = _Timedelta.__new__(Timedelta, seconds=int(value)) - elif reso == NPY_DATETIMEUNIT.NPY_FR_m: - td_base = _Timedelta.__new__(Timedelta, minutes=int(value)) - elif reso == NPY_DATETIMEUNIT.NPY_FR_h: - td_base = _Timedelta.__new__(Timedelta, hours=int(value)) - elif reso == NPY_DATETIMEUNIT.NPY_FR_D: - td_base = _Timedelta.__new__(Timedelta, days=int(value)) + # 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)) + # elif reso == NPY_DATETIMEUNIT.NPY_FR_h: + # td_base = _Timedelta.__new__(Timedelta, hours=int(value)) + # elif reso == NPY_DATETIMEUNIT.NPY_FR_D: + # td_base = _Timedelta.__new__(Timedelta, days=int(value)) else: - raise NotImplementedError(reso) + raise NotImplementedError( + "Only resolutions 's', 'ms', 'us', 'ns' are supported." + ) + td_base.value = value td_base._is_populated = 0 @@ -1006,7 +1014,6 @@ cdef class _Timedelta(timedelta): def __richcmp__(_Timedelta self, object other, int op): cdef: _Timedelta ots - int ndim if isinstance(other, _Timedelta): ots = other @@ -1018,7 +1025,6 @@ cdef class _Timedelta(timedelta): return op == Py_NE elif util.is_array(other): - # TODO: watch out for zero-dim if other.dtype.kind == "m": return PyObject_RichCompare(self.asm8, other, op) elif other.dtype.kind == "O": @@ -1728,7 +1734,10 @@ class Timedelta(_Timedelta): ) elif is_array(other): - # ndarray-like + if other.ndim == 0: + # see also: item_from_zerodim + item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) + return self.__mul__(item) return other * self.to_timedelta64() return NotImplemented @@ -1736,6 +1745,9 @@ class Timedelta(_Timedelta): __rmul__ = __mul__ def __truediv__(self, other): + cdef: + int64_t new_value + if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) @@ -1758,6 +1770,10 @@ class Timedelta(_Timedelta): ) elif is_array(other): + if other.ndim == 0: + # see also: item_from_zerodim + item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) + return self.__truediv__(item) return self.to_timedelta64() / other return NotImplemented @@ -1777,9 +1793,17 @@ class Timedelta(_Timedelta): return float(other.value) / self.value elif is_array(other): - if other.dtype.kind == "O": + if other.ndim == 0: + # see also: item_from_zerodim + item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) + return self.__rtruediv__(item) + elif other.dtype.kind == "O": # GH#31869 return np.array([x / self for x in other]) + + # TODO: if other.dtype.kind == "m" and other.dtype != self.asm8.dtype + # then should disallow for consistency with scalar behavior; requires + # deprecation cycle. (or changing scalar behavior) return other / self.to_timedelta64() return NotImplemented @@ -1806,6 +1830,11 @@ class Timedelta(_Timedelta): return type(self)._from_value_and_reso(self.value // other, self._reso) elif is_array(other): + if other.ndim == 0: + # see also: item_from_zerodim + item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) + return self.__floordiv__(item) + if other.dtype.kind == 'm': # also timedelta-like if self._reso != NPY_FR_ns: @@ -1838,6 +1867,11 @@ class Timedelta(_Timedelta): return other.value // self.value elif is_array(other): + if other.ndim == 0: + # see also: item_from_zerodim + item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) + return self.__rfloordiv__(item) + if other.dtype.kind == 'm': # also timedelta-like if self._reso != NPY_FR_ns: @@ -1923,23 +1957,17 @@ cdef _broadcast_floordiv_td64( result : varies based on `other` """ # assumes other.dtype.kind == 'm', i.e. other is timedelta-like + # assumes other.ndim != 0 # We need to watch out for np.timedelta64('NaT'). mask = other.view('i8') == NPY_NAT - if other.ndim == 0: - if mask: - return np.nan - - return operation(value, other.astype('m8[ns]', copy=False).astype('i8')) - - else: - res = operation(value, other.astype('m8[ns]', copy=False).astype('i8')) + res = operation(value, other.astype('m8[ns]', copy=False).astype('i8')) - if mask.any(): - res = res.astype('f8') - res[mask] = np.nan - return res + if mask.any(): + res = res.astype('f8') + res[mask] = np.nan + return res # resolution in ns diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 8a2810825fc1d..cf36e75127d17 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -215,6 +215,11 @@ cdef class _Timestamp(ABCTimestamp): if value == NPY_NAT: return NaT + if reso < NPY_DATETIMEUNIT.NPY_FR_s or reso > NPY_DATETIMEUNIT.NPY_FR_ns: + raise NotImplementedError( + "Only resolutions 's', 'ms', 'us', 'ns' are supported." + ) + obj.value = value pandas_datetime_to_datetimestruct(value, reso, &obj.dts) maybe_localize_tso(obj, tz, reso) diff --git a/pandas/tests/indexes/timedeltas/test_indexing.py b/pandas/tests/indexes/timedeltas/test_indexing.py index b618f12e9f6c9..154a6289dfc00 100644 --- a/pandas/tests/indexes/timedeltas/test_indexing.py +++ b/pandas/tests/indexes/timedeltas/test_indexing.py @@ -14,6 +14,7 @@ TimedeltaIndex, Timestamp, notna, + offsets, timedelta_range, to_timedelta, ) @@ -346,3 +347,14 @@ def test_contains_nonunique(self): ): idx = TimedeltaIndex(vals) assert idx[0] in idx + + def test_contains(self): + # Checking for any NaT-like objects + # GH#13603 + td = to_timedelta(range(5), unit="d") + offsets.Hour(1) + for v in [NaT, None, float("nan"), np.nan]: + assert not (v in td) + + td = to_timedelta([NaT]) + for v in [NaT, None, float("nan"), np.nan]: + assert v in td diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index 614245ec7a93e..f3b84388b0f70 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -318,6 +318,26 @@ def test_td_add_sub_dt64_ndarray(self): tm.assert_numpy_array_equal(-td + other, expected) tm.assert_numpy_array_equal(other - td, expected) + def test_td_add_sub_ndarray_0d(self): + td = Timedelta("1 day") + other = np.array(td.asm8) + + result = td + other + assert isinstance(result, Timedelta) + assert result == 2 * td + + result = other + td + assert isinstance(result, Timedelta) + assert result == 2 * td + + result = other - td + assert isinstance(result, Timedelta) + assert result == 0 * td + + result = td - other + assert isinstance(result, Timedelta) + assert result == 0 * td + class TestTimedeltaMultiplicationDivision: """ @@ -395,6 +415,20 @@ def test_td_mul_numeric_ndarray(self): result = other * td tm.assert_numpy_array_equal(result, expected) + def test_td_mul_numeric_ndarray_0d(self): + td = Timedelta("1 day") + other = np.array(2) + assert other.ndim == 0 + expected = Timedelta("2 days") + + res = td * other + assert type(res) is Timedelta + assert res == expected + + res = other * td + assert type(res) is Timedelta + assert res == expected + def test_td_mul_td64_ndarray_invalid(self): td = Timedelta("1 day") other = np.array([Timedelta("2 Days").to_timedelta64()]) @@ -484,6 +518,14 @@ def test_td_div_td64_ndarray(self): result = other / td tm.assert_numpy_array_equal(result, expected * 4) + def test_td_div_ndarray_0d(self): + td = Timedelta("1 day") + + other = np.array(1) + res = td / other + assert isinstance(res, Timedelta) + assert res == td + # --------------------------------------------------------------- # Timedelta.__rdiv__ @@ -539,6 +581,13 @@ def test_td_rdiv_ndarray(self): with pytest.raises(TypeError, match=msg): arr / td + def test_td_rdiv_ndarray_0d(self): + td = Timedelta(10, unit="d") + + arr = np.array(td.asm8) + + assert arr / td == 1 + # --------------------------------------------------------------- # Timedelta.__floordiv__ diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 99b3bbf0186bb..96a60af58dec2 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -84,15 +84,15 @@ def test_as_unit_rounding(self): def test_as_unit_non_nano(self): # case where we are going neither to nor from nano - td = Timedelta(days=1)._as_unit("D") + td = Timedelta(days=1)._as_unit("ms") assert td.days == 1 - assert td.value == 1 + assert td.value == 86_400_000 assert td.components.days == 1 assert td._d == 1 assert td.total_seconds() == 86400 - res = td._as_unit("h") - assert res.value == 24 + res = td._as_unit("us") + assert res.value == 86_400_000_000 assert res.components.days == 1 assert res.components.hours == 0 assert res._d == 1 @@ -677,17 +677,6 @@ def test_round_non_nano(self, unit): assert res == Timedelta("1 days 02:35:00") assert res._reso == td._reso - def test_contains(self): - # Checking for any NaT-like objects - # GH 13603 - td = to_timedelta(range(5), unit="d") + offsets.Hour(1) - for v in [NaT, None, float("nan"), np.nan]: - assert not (v in td) - - td = to_timedelta([NaT]) - for v in [NaT, None, float("nan"), np.nan]: - assert v in td - def test_identity(self): td = Timedelta(10, unit="d") diff --git a/pandas/tests/tslibs/test_timedeltas.py b/pandas/tests/tslibs/test_timedeltas.py index 661bb113e9549..bb1efe38ea7c4 100644 --- a/pandas/tests/tslibs/test_timedeltas.py +++ b/pandas/tests/tslibs/test_timedeltas.py @@ -127,5 +127,6 @@ def test_ints_to_pytimedelta_unsupported(unit): with pytest.raises(NotImplementedError, match=r"\d{1,2}"): ints_to_pytimedelta(arr, box=False) - with pytest.raises(NotImplementedError, match=r"\d{1,2}"): + msg = "Only resolutions 's', 'ms', 'us', 'ns' are supported" + with pytest.raises(NotImplementedError, match=msg): ints_to_pytimedelta(arr, box=True)