From e85c67dda399d684e71929dfedcaaab9c394eaa6 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 25 Dec 2022 16:27:20 -0800 Subject: [PATCH 1/3] REF: de-duplicate TimedeltaArray division code --- pandas/core/arrays/timedeltas.py | 187 ++++++++++++------------------- 1 file changed, 73 insertions(+), 114 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index aa1b826ef0876..58209672813ba 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import timedelta +import operator from typing import ( TYPE_CHECKING, Iterator, @@ -63,6 +64,7 @@ from pandas.core.arrays import datetimelike as dtl from pandas.core.arrays._ranges import generate_regular_range import pandas.core.common as com +from pandas.core.ops import roperator from pandas.core.ops.common import unpack_zerodim_and_defer if TYPE_CHECKING: @@ -489,10 +491,11 @@ def __mul__(self, other) -> TimedeltaArray: __rmul__ = __mul__ - @unpack_zerodim_and_defer("__truediv__") - def __truediv__(self, other): - # timedelta / X is well-defined for timedelta-like or numeric X - + def _scalar_divlike_op(self, other, op): + """ + Shared logic for __truediv__, __rtruediv__, __floordiv__, __rfloordiv__ + with scalar 'other'. + """ if isinstance(other, self._recognized_scalars): other = Timedelta(other) # mypy assumes that __new__ returns an instance of the class @@ -504,28 +507,72 @@ def __truediv__(self, other): return result # otherwise, dispatch to Timedelta implementation - return self._ndarray / other + return op(self._ndarray, other) - elif lib.is_scalar(other): - # assume it is numeric - result = self._ndarray / other + else: + # caller is responsible for checking lib.is_scalar(other) + # assume other is numeric, otherwise numpy will raise + + if op in [roperator.rtruediv, roperator.rfloordiv]: + raise TypeError( + f"Cannot divide {type(other).__name__} by {type(self).__name__}" + ) + + result = op(self._ndarray, other) freq = None + if self.freq is not None: - # Tick division is not implemented, so operate on Timedelta - freq = self.freq.delta / other - freq = to_offset(freq) + # Note: freq gets division, not floor-division, even if op + # is floordiv. + freq = self.freq / other + + # TODO: 2022-12-24 test_ufunc_coercions, test_tdi_ops_attributes + # get here for truediv, no tests for floordiv + + if op is operator.floordiv: + if freq.nanos == 0 and self.freq.nanos != 0: + # e.g. if self.freq is Nano(1) then dividing by 2 + # rounds down to zero + # TODO: 2022-12-24 should implement the same check + # for truediv case + freq = None + return type(self)._simple_new(result, dtype=result.dtype, freq=freq) + def _cast_divlike_op(self, other): if not hasattr(other, "dtype"): # e.g. list, tuple other = np.array(other) if len(other) != len(self): raise ValueError("Cannot divide vectors with unequal lengths") + return other + + def _vector_divlike_op(self, other, op) -> np.ndarray: + """ + Shared logic for __truediv__, __floordiv__, and their reversed versions + with timedelta64-dtype ndarray other. + """ + # Let numpy handle it + result = op(self._ndarray, np.asarray(other)) + + if op in [operator.floordiv, roperator.rfloordiv]: + mask = self.isna() | isna(other) + if mask.any(): + result = result.astype(np.float64) + np.putmask(result, mask, np.nan) + return result + + @unpack_zerodim_and_defer("__truediv__") + def __truediv__(self, other): + # timedelta / X is well-defined for timedelta-like or numeric X + op = operator.truediv + if is_scalar(other): + return self._scalar_divlike_op(other, op) + other = self._cast_divlike_op(other) if is_timedelta64_dtype(other.dtype): - # let numpy handle it - return self._ndarray / other + return self._vector_divlike_op(other, op) elif is_object_dtype(other.dtype): # We operate on raveled arrays to avoid problems in inference @@ -562,34 +609,13 @@ def __truediv__(self, other): @unpack_zerodim_and_defer("__rtruediv__") def __rtruediv__(self, other): # X / timedelta is defined only for timedelta-like X - if isinstance(other, self._recognized_scalars): - other = Timedelta(other) - # mypy assumes that __new__ returns an instance of the class - # github.com/python/mypy/issues/1020 - if cast("Timedelta | NaTType", other) is NaT: - # specifically timedelta64-NaT - result = np.empty(self.shape, dtype=np.float64) - result.fill(np.nan) - return result - - # otherwise, dispatch to Timedelta implementation - return other / self._ndarray - - elif lib.is_scalar(other): - raise TypeError( - f"Cannot divide {type(other).__name__} by {type(self).__name__}" - ) - - if not hasattr(other, "dtype"): - # e.g. list, tuple - other = np.array(other) - - if len(other) != len(self): - raise ValueError("Cannot divide vectors with unequal lengths") + op = roperator.rtruediv + if is_scalar(other): + return self._scalar_divlike_op(other, op) + other = self._cast_divlike_op(other) if is_timedelta64_dtype(other.dtype): - # let numpy handle it - return other / self._ndarray + return self._vector_divlike_op(other, op) elif is_object_dtype(other.dtype): # Note: unlike in __truediv__, we do not _need_ to do type @@ -605,51 +631,13 @@ def __rtruediv__(self, other): @unpack_zerodim_and_defer("__floordiv__") def __floordiv__(self, other): - + op = operator.floordiv if is_scalar(other): - if isinstance(other, self._recognized_scalars): - other = Timedelta(other) - # mypy assumes that __new__ returns an instance of the class - # github.com/python/mypy/issues/1020 - if cast("Timedelta | NaTType", other) is NaT: - # treat this specifically as timedelta-NaT - result = np.empty(self.shape, dtype=np.float64) - result.fill(np.nan) - return result - - # dispatch to Timedelta implementation - return other.__rfloordiv__(self._ndarray) - - # at this point we should only have numeric scalars; anything - # else will raise - result = self._ndarray // other - freq = None - if self.freq is not None: - # Note: freq gets division, not floor-division - freq = self.freq / other - if freq.nanos == 0 and self.freq.nanos != 0: - # e.g. if self.freq is Nano(1) then dividing by 2 - # rounds down to zero - freq = None - return type(self)(result, freq=freq) - - if not hasattr(other, "dtype"): - # list, tuple - other = np.array(other) - if len(other) != len(self): - raise ValueError("Cannot divide with unequal lengths") + return self._scalar_divlike_op(other, op) + other = self._cast_divlike_op(other) if is_timedelta64_dtype(other.dtype): - other = type(self)(other) - - # numpy timedelta64 does not natively support floordiv, so operate - # on the i8 values - result = self.asi8 // other.asi8 - mask = self._isnan | other._isnan - if mask.any(): - result = result.astype(np.float64) - np.putmask(result, mask, np.nan) - return result + return self._vector_divlike_op(other, op) elif is_object_dtype(other.dtype): # error: Incompatible types in assignment (expression has type @@ -682,42 +670,13 @@ def __floordiv__(self, other): @unpack_zerodim_and_defer("__rfloordiv__") def __rfloordiv__(self, other): - + op = roperator.rfloordiv if is_scalar(other): - if isinstance(other, self._recognized_scalars): - other = Timedelta(other) - # mypy assumes that __new__ returns an instance of the class - # github.com/python/mypy/issues/1020 - if cast("Timedelta | NaTType", other) is NaT: - # treat this specifically as timedelta-NaT - result = np.empty(self.shape, dtype=np.float64) - result.fill(np.nan) - return result - - # dispatch to Timedelta implementation - return other.__floordiv__(self._ndarray) - - raise TypeError( - f"Cannot divide {type(other).__name__} by {type(self).__name__}" - ) - - if not hasattr(other, "dtype"): - # list, tuple - other = np.array(other) - - if len(other) != len(self): - raise ValueError("Cannot divide with unequal lengths") + return self._scalar_divlike_op(other, op) + other = self._cast_divlike_op(other) if is_timedelta64_dtype(other.dtype): - other = type(self)(other) - # numpy timedelta64 does not natively support floordiv, so operate - # on the i8 values - result = other.asi8 // self.asi8 - mask = self._isnan | other._isnan - if mask.any(): - result = result.astype(np.float64) - np.putmask(result, mask, np.nan) - return result + return self._vector_divlike_op(other, op) elif is_object_dtype(other.dtype): result_list = [other[n] // self[n] for n in range(len(self))] From 185927e5b52eaafc3d858dc3dac253cfaf7d4625 Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 26 Dec 2022 10:43:09 -0800 Subject: [PATCH 2/3] Share more --- pandas/core/arrays/timedeltas.py | 38 ++++++++++++--------- pandas/tests/arithmetic/test_numeric.py | 3 +- pandas/tests/arithmetic/test_timedelta64.py | 1 + 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 58209672813ba..b46a34151d2af 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -548,7 +548,7 @@ def _cast_divlike_op(self, other): raise ValueError("Cannot divide vectors with unequal lengths") return other - def _vector_divlike_op(self, other, op) -> np.ndarray: + def _vector_divlike_op(self, other, op) -> np.ndarray | TimedeltaArray: """ Shared logic for __truediv__, __floordiv__, and their reversed versions with timedelta64-dtype ndarray other. @@ -556,11 +556,18 @@ def _vector_divlike_op(self, other, op) -> np.ndarray: # Let numpy handle it result = op(self._ndarray, np.asarray(other)) + if (is_integer_dtype(other.dtype) or is_float_dtype(other.dtype)) and op in [ + operator.truediv, + operator.floordiv, + ]: + return type(self)._simple_new(result, dtype=result.dtype) + if op in [operator.floordiv, roperator.rfloordiv]: mask = self.isna() | isna(other) if mask.any(): result = result.astype(np.float64) np.putmask(result, mask, np.nan) + return result @unpack_zerodim_and_defer("__truediv__") @@ -571,7 +578,11 @@ def __truediv__(self, other): return self._scalar_divlike_op(other, op) other = self._cast_divlike_op(other) - if is_timedelta64_dtype(other.dtype): + if ( + is_timedelta64_dtype(other.dtype) + or is_integer_dtype(other.dtype) + or is_float_dtype(other.dtype) + ): return self._vector_divlike_op(other, op) elif is_object_dtype(other.dtype): @@ -603,8 +614,7 @@ def __truediv__(self, other): return result else: - result = self._ndarray / other - return type(self)._simple_new(result, dtype=result.dtype) + return NotImplemented @unpack_zerodim_and_defer("__rtruediv__") def __rtruediv__(self, other): @@ -625,9 +635,7 @@ def __rtruediv__(self, other): return np.array(result_list) else: - raise TypeError( - f"Cannot divide {other.dtype} data by {type(self).__name__}" - ) + return NotImplemented @unpack_zerodim_and_defer("__floordiv__") def __floordiv__(self, other): @@ -636,7 +644,11 @@ def __floordiv__(self, other): return self._scalar_divlike_op(other, op) other = self._cast_divlike_op(other) - if is_timedelta64_dtype(other.dtype): + if ( + is_timedelta64_dtype(other.dtype) + or is_integer_dtype(other.dtype) + or is_float_dtype(other.dtype) + ): return self._vector_divlike_op(other, op) elif is_object_dtype(other.dtype): @@ -660,13 +672,8 @@ def __floordiv__(self, other): return self * np.nan return result - elif is_integer_dtype(other.dtype) or is_float_dtype(other.dtype): - result = self._ndarray // other - return type(self)(result) - else: - dtype = getattr(other, "dtype", type(other).__name__) - raise TypeError(f"Cannot divide {dtype} by {type(self).__name__}") + return NotImplemented @unpack_zerodim_and_defer("__rfloordiv__") def __rfloordiv__(self, other): @@ -684,8 +691,7 @@ def __rfloordiv__(self, other): return result else: - dtype = getattr(other, "dtype", type(other).__name__) - raise TypeError(f"Cannot divide {dtype} by {type(self).__name__}") + return NotImplemented @unpack_zerodim_and_defer("__mod__") def __mod__(self, other): diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 529dd6baa70c0..84dfac4b9e8a7 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -183,10 +183,11 @@ def test_div_td64arr(self, left, box_cls): result = right // left tm.assert_equal(result, expected) - msg = "Cannot divide" + msg = "ufunc 'divide' cannot use operands with types" with pytest.raises(TypeError, match=msg): left / right + msg = "ufunc 'floor_divide' cannot use operands with types" with pytest.raises(TypeError, match=msg): left // right diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index f3ea741607692..c5143ff2b2b07 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1984,6 +1984,7 @@ def test_td64arr_div_numeric_array( "cannot perform __truediv__", "unsupported operand", "Cannot divide", + "ufunc 'divide' cannot use operands with types", ] ) with pytest.raises(TypeError, match=pattern): From a5b6ff3215016c2fba9c3df152120013c36da67e Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 26 Dec 2022 17:17:03 -0800 Subject: [PATCH 3/3] min versions build --- pandas/tests/arithmetic/test_numeric.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 84dfac4b9e8a7..5dcd78af9712d 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -183,7 +183,8 @@ def test_div_td64arr(self, left, box_cls): result = right // left tm.assert_equal(result, expected) - msg = "ufunc 'divide' cannot use operands with types" + # (true_) needed for min-versions build 2022-12-26 + msg = "ufunc '(true_)?divide' cannot use operands with types" with pytest.raises(TypeError, match=msg): left / right