diff --git a/pandas/_libs/tslibs/timedeltas.pxd b/pandas/_libs/tslibs/timedeltas.pxd index 3251e10a88b73..7c597cb4b102b 100644 --- a/pandas/_libs/tslibs/timedeltas.pxd +++ b/pandas/_libs/tslibs/timedeltas.pxd @@ -25,3 +25,4 @@ cdef class _Timedelta(timedelta): cdef _ensure_components(_Timedelta self) cdef inline bint _compare_mismatched_resos(self, _Timedelta other, op) cdef _Timedelta _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=*) + cpdef _maybe_cast_to_matching_resos(self, _Timedelta other) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 545de31159930..8a879e00a6781 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1530,12 +1530,7 @@ cdef class _Timedelta(timedelta): def _as_unit(self, str unit, bint round_ok=True): dtype = np.dtype(f"m8[{unit}]") reso = get_unit_from_dtype(dtype) - try: - return self._as_reso(reso, round_ok=round_ok) - except OverflowError as err: - raise OutOfBoundsTimedelta( - f"Cannot cast {self} to unit='{unit}' without overflow." - ) from err + return self._as_reso(reso, round_ok=round_ok) @cython.cdivision(False) cdef _Timedelta _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=True): @@ -1545,9 +1540,26 @@ cdef class _Timedelta(timedelta): if reso == self._reso: return self - value = convert_reso(self.value, self._reso, reso, round_ok=round_ok) + try: + value = convert_reso(self.value, self._reso, reso, round_ok=round_ok) + except OverflowError as err: + unit = npy_unit_to_abbrev(reso) + raise OutOfBoundsTimedelta( + f"Cannot cast {self} to unit='{unit}' without overflow." + ) from err + return type(self)._from_value_and_reso(value, reso=reso) + cpdef _maybe_cast_to_matching_resos(self, _Timedelta other): + """ + If _resos do not match, cast to the higher resolution, raising on overflow. + """ + if self._reso > other._reso: + other = other._as_reso(self._reso) + elif self._reso < other._reso: + self = self._as_reso(other._reso) + return self, other + # Python front end to C extension type _Timedelta # This serves as the box for timedelta64 @@ -1827,11 +1839,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if other._reso != self._reso: - raise ValueError( - "division between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return self.value / float(other.value) elif is_integer_object(other) or is_float_object(other): @@ -1858,11 +1866,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if self._reso != other._reso: - raise ValueError( - "division between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return float(other.value) / self.value elif is_array(other): @@ -1890,11 +1894,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if self._reso != other._reso: - raise ValueError( - "floordivision between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return self.value // other.value elif is_integer_object(other) or is_float_object(other): @@ -1913,6 +1913,7 @@ class Timedelta(_Timedelta): if self._reso != NPY_FR_ns: raise NotImplementedError return _broadcast_floordiv_td64(self.value, other, _floordiv) + elif other.dtype.kind in ['i', 'u', 'f']: if other.ndim == 0: return self // other.item() @@ -1932,11 +1933,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if self._reso != other._reso: - raise ValueError( - "floordivision between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return other.value // self.value elif is_array(other): @@ -2022,7 +2019,7 @@ cdef _broadcast_floordiv_td64( Parameters ---------- value : int64_t; `self.value` from a Timedelta object - other : object + other : ndarray[timedelta64[ns]] operation : function, either _floordiv or _rfloordiv Returns diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 8d3738d40601b..2ec9ed122b559 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -183,14 +183,23 @@ def test_truediv_timedeltalike(self, td): assert (2.5 * td) / td == 2.5 other = Timedelta(td.value) - msg = "with mismatched resolutions are not supported" - with pytest.raises(ValueError, match=msg): + msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow" + with pytest.raises(OutOfBoundsTimedelta, match=msg): td / other - with pytest.raises(ValueError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): # __rtruediv__ other.to_pytimedelta() / td + # if there's no overflow, we cast to the higher reso + left = Timedelta._from_value_and_reso(50, NpyDatetimeUnit.NPY_FR_us.value) + right = Timedelta._from_value_and_reso(50, NpyDatetimeUnit.NPY_FR_ms.value) + result = left / right + assert result == 0.001 + + result = right / left + assert result == 1000 + def test_truediv_numeric(self, td): assert td / np.nan is NaT @@ -207,14 +216,22 @@ def test_floordiv_timedeltalike(self, td): assert (2.5 * td) // td == 2 other = Timedelta(td.value) - msg = "with mismatched resolutions are not supported" - with pytest.raises(ValueError, match=msg): + msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow" + with pytest.raises(OutOfBoundsTimedelta, match=msg): td // other with pytest.raises(ValueError, match=msg): # __rfloordiv__ other.to_pytimedelta() // td + # if there's no overflow, we cast to the higher reso + left = Timedelta._from_value_and_reso(50050, NpyDatetimeUnit.NPY_FR_us.value) + right = Timedelta._from_value_and_reso(50, NpyDatetimeUnit.NPY_FR_ms.value) + result = left // right + assert result == 1 + result = right // left + assert result == 0 + def test_floordiv_numeric(self, td): assert td // np.nan is NaT @@ -259,15 +276,14 @@ def test_addsub_mismatched_reso(self, td): assert result.days == 1 - td.days other2 = Timedelta(500) - # TODO: should be OutOfBoundsTimedelta - msg = "value too large" - with pytest.raises(OverflowError, match=msg): + msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow" + with pytest.raises(OutOfBoundsTimedelta, match=msg): td + other2 - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): other2 + td - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): td - other2 - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): other2 - td def test_min(self, td):