Skip to content

Commit b6eecb3

Browse files
authored
REF: share TimedeltaArray division code (#50441)
* REF: de-duplicate TimedeltaArray division code * Share more * min versions build
1 parent 5a9142e commit b6eecb3

File tree

3 files changed

+101
-134
lines changed

3 files changed

+101
-134
lines changed

pandas/core/arrays/timedeltas.py

+97-133
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from datetime import timedelta
4+
import operator
45
from typing import (
56
TYPE_CHECKING,
67
Iterator,
@@ -65,7 +66,7 @@
6566
from pandas.core.arrays import datetimelike as dtl
6667
from pandas.core.arrays._ranges import generate_regular_range
6768
import pandas.core.common as com
68-
from pandas.core.construction import extract_array
69+
from pandas.core.ops import roperator
6970
from pandas.core.ops.common import unpack_zerodim_and_defer
7071

7172
if TYPE_CHECKING:
@@ -492,10 +493,11 @@ def __mul__(self, other) -> TimedeltaArray:
492493

493494
__rmul__ = __mul__
494495

495-
@unpack_zerodim_and_defer("__truediv__")
496-
def __truediv__(self, other):
497-
# timedelta / X is well-defined for timedelta-like or numeric X
498-
496+
def _scalar_divlike_op(self, other, op):
497+
"""
498+
Shared logic for __truediv__, __rtruediv__, __floordiv__, __rfloordiv__
499+
with scalar 'other'.
500+
"""
499501
if isinstance(other, self._recognized_scalars):
500502
other = Timedelta(other)
501503
# mypy assumes that __new__ returns an instance of the class
@@ -507,31 +509,86 @@ def __truediv__(self, other):
507509
return result
508510

509511
# otherwise, dispatch to Timedelta implementation
510-
return self._ndarray / other
512+
return op(self._ndarray, other)
511513

512-
elif lib.is_scalar(other):
513-
# assume it is numeric
514-
result = self._ndarray / other
514+
else:
515+
# caller is responsible for checking lib.is_scalar(other)
516+
# assume other is numeric, otherwise numpy will raise
517+
518+
if op in [roperator.rtruediv, roperator.rfloordiv]:
519+
raise TypeError(
520+
f"Cannot divide {type(other).__name__} by {type(self).__name__}"
521+
)
522+
523+
result = op(self._ndarray, other)
515524
freq = None
525+
516526
if self.freq is not None:
517-
# Tick division is not implemented, so operate on Timedelta
518-
freq = self.freq.delta / other
519-
freq = to_offset(freq)
527+
# Note: freq gets division, not floor-division, even if op
528+
# is floordiv.
529+
freq = self.freq / other
530+
531+
# TODO: 2022-12-24 test_ufunc_coercions, test_tdi_ops_attributes
532+
# get here for truediv, no tests for floordiv
533+
534+
if op is operator.floordiv:
535+
if freq.nanos == 0 and self.freq.nanos != 0:
536+
# e.g. if self.freq is Nano(1) then dividing by 2
537+
# rounds down to zero
538+
# TODO: 2022-12-24 should implement the same check
539+
# for truediv case
540+
freq = None
541+
520542
return type(self)._simple_new(result, dtype=result.dtype, freq=freq)
521543

544+
def _cast_divlike_op(self, other):
522545
if not hasattr(other, "dtype"):
523546
# e.g. list, tuple
524547
other = np.array(other)
525548

526549
if len(other) != len(self):
527550
raise ValueError("Cannot divide vectors with unequal lengths")
551+
return other
528552

529-
if is_timedelta64_dtype(other.dtype):
530-
# let numpy handle it
531-
return self._ndarray / other
553+
def _vector_divlike_op(self, other, op) -> np.ndarray | TimedeltaArray:
554+
"""
555+
Shared logic for __truediv__, __floordiv__, and their reversed versions
556+
with timedelta64-dtype ndarray other.
557+
"""
558+
# Let numpy handle it
559+
result = op(self._ndarray, np.asarray(other))
532560

533-
elif is_object_dtype(other.dtype):
534-
other = extract_array(other, extract_numpy=True)
561+
if (is_integer_dtype(other.dtype) or is_float_dtype(other.dtype)) and op in [
562+
operator.truediv,
563+
operator.floordiv,
564+
]:
565+
return type(self)._simple_new(result, dtype=result.dtype)
566+
567+
if op in [operator.floordiv, roperator.rfloordiv]:
568+
mask = self.isna() | isna(other)
569+
if mask.any():
570+
result = result.astype(np.float64)
571+
np.putmask(result, mask, np.nan)
572+
573+
return result
574+
575+
@unpack_zerodim_and_defer("__truediv__")
576+
def __truediv__(self, other):
577+
# timedelta / X is well-defined for timedelta-like or numeric X
578+
op = operator.truediv
579+
if is_scalar(other):
580+
return self._scalar_divlike_op(other, op)
581+
582+
other = self._cast_divlike_op(other)
583+
if (
584+
is_timedelta64_dtype(other.dtype)
585+
or is_integer_dtype(other.dtype)
586+
or is_float_dtype(other.dtype)
587+
):
588+
return self._vector_divlike_op(other, op)
589+
590+
if is_object_dtype(other.dtype):
591+
other = np.asarray(other)
535592
if self.ndim > 1:
536593
res_cols = [left / right for left, right in zip(self, other)]
537594
res_cols2 = [x.reshape(1, -1) for x in res_cols]
@@ -542,40 +599,18 @@ def __truediv__(self, other):
542599
return result
543600

544601
else:
545-
result = self._ndarray / other
546-
return type(self)._simple_new(result, dtype=result.dtype)
602+
return NotImplemented
547603

548604
@unpack_zerodim_and_defer("__rtruediv__")
549605
def __rtruediv__(self, other):
550606
# X / timedelta is defined only for timedelta-like X
551-
if isinstance(other, self._recognized_scalars):
552-
other = Timedelta(other)
553-
# mypy assumes that __new__ returns an instance of the class
554-
# github.com/python/mypy/issues/1020
555-
if cast("Timedelta | NaTType", other) is NaT:
556-
# specifically timedelta64-NaT
557-
result = np.empty(self.shape, dtype=np.float64)
558-
result.fill(np.nan)
559-
return result
560-
561-
# otherwise, dispatch to Timedelta implementation
562-
return other / self._ndarray
563-
564-
elif lib.is_scalar(other):
565-
raise TypeError(
566-
f"Cannot divide {type(other).__name__} by {type(self).__name__}"
567-
)
568-
569-
if not hasattr(other, "dtype"):
570-
# e.g. list, tuple
571-
other = np.array(other)
572-
573-
if len(other) != len(self):
574-
raise ValueError("Cannot divide vectors with unequal lengths")
607+
op = roperator.rtruediv
608+
if is_scalar(other):
609+
return self._scalar_divlike_op(other, op)
575610

611+
other = self._cast_divlike_op(other)
576612
if is_timedelta64_dtype(other.dtype):
577-
# let numpy handle it
578-
return other / self._ndarray
613+
return self._vector_divlike_op(other, op)
579614

580615
elif is_object_dtype(other.dtype):
581616
# Note: unlike in __truediv__, we do not _need_ to do type
@@ -585,60 +620,24 @@ def __rtruediv__(self, other):
585620
return np.array(result_list)
586621

587622
else:
588-
raise TypeError(
589-
f"Cannot divide {other.dtype} data by {type(self).__name__}"
590-
)
623+
return NotImplemented
591624

592625
@unpack_zerodim_and_defer("__floordiv__")
593626
def __floordiv__(self, other):
594-
627+
op = operator.floordiv
595628
if is_scalar(other):
596-
if isinstance(other, self._recognized_scalars):
597-
other = Timedelta(other)
598-
# mypy assumes that __new__ returns an instance of the class
599-
# github.com/python/mypy/issues/1020
600-
if cast("Timedelta | NaTType", other) is NaT:
601-
# treat this specifically as timedelta-NaT
602-
result = np.empty(self.shape, dtype=np.float64)
603-
result.fill(np.nan)
604-
return result
605-
606-
# dispatch to Timedelta implementation
607-
return other.__rfloordiv__(self._ndarray)
608-
609-
# at this point we should only have numeric scalars; anything
610-
# else will raise
611-
result = self._ndarray // other
612-
freq = None
613-
if self.freq is not None:
614-
# Note: freq gets division, not floor-division
615-
freq = self.freq / other
616-
if freq.nanos == 0 and self.freq.nanos != 0:
617-
# e.g. if self.freq is Nano(1) then dividing by 2
618-
# rounds down to zero
619-
freq = None
620-
return type(self)(result, freq=freq)
621-
622-
if not hasattr(other, "dtype"):
623-
# list, tuple
624-
other = np.array(other)
625-
if len(other) != len(self):
626-
raise ValueError("Cannot divide with unequal lengths")
629+
return self._scalar_divlike_op(other, op)
627630

628-
if is_timedelta64_dtype(other.dtype):
629-
other = type(self)(other)
630-
631-
# numpy timedelta64 does not natively support floordiv, so operate
632-
# on the i8 values
633-
result = self.asi8 // other.asi8
634-
mask = self._isnan | other._isnan
635-
if mask.any():
636-
result = result.astype(np.float64)
637-
np.putmask(result, mask, np.nan)
638-
return result
631+
other = self._cast_divlike_op(other)
632+
if (
633+
is_timedelta64_dtype(other.dtype)
634+
or is_integer_dtype(other.dtype)
635+
or is_float_dtype(other.dtype)
636+
):
637+
return self._vector_divlike_op(other, op)
639638

640639
elif is_object_dtype(other.dtype):
641-
other = extract_array(other, extract_numpy=True)
640+
other = np.asarray(other)
642641
if self.ndim > 1:
643642
res_cols = [left // right for left, right in zip(self, other)]
644643
res_cols2 = [x.reshape(1, -1) for x in res_cols]
@@ -649,61 +648,26 @@ def __floordiv__(self, other):
649648
assert result.dtype == object
650649
return result
651650

652-
elif is_integer_dtype(other.dtype) or is_float_dtype(other.dtype):
653-
result = self._ndarray // other
654-
return type(self)(result)
655-
656651
else:
657-
dtype = getattr(other, "dtype", type(other).__name__)
658-
raise TypeError(f"Cannot divide {dtype} by {type(self).__name__}")
652+
return NotImplemented
659653

660654
@unpack_zerodim_and_defer("__rfloordiv__")
661655
def __rfloordiv__(self, other):
662-
656+
op = roperator.rfloordiv
663657
if is_scalar(other):
664-
if isinstance(other, self._recognized_scalars):
665-
other = Timedelta(other)
666-
# mypy assumes that __new__ returns an instance of the class
667-
# github.com/python/mypy/issues/1020
668-
if cast("Timedelta | NaTType", other) is NaT:
669-
# treat this specifically as timedelta-NaT
670-
result = np.empty(self.shape, dtype=np.float64)
671-
result.fill(np.nan)
672-
return result
673-
674-
# dispatch to Timedelta implementation
675-
return other.__floordiv__(self._ndarray)
676-
677-
raise TypeError(
678-
f"Cannot divide {type(other).__name__} by {type(self).__name__}"
679-
)
680-
681-
if not hasattr(other, "dtype"):
682-
# list, tuple
683-
other = np.array(other)
684-
685-
if len(other) != len(self):
686-
raise ValueError("Cannot divide with unequal lengths")
658+
return self._scalar_divlike_op(other, op)
687659

660+
other = self._cast_divlike_op(other)
688661
if is_timedelta64_dtype(other.dtype):
689-
other = type(self)(other)
690-
# numpy timedelta64 does not natively support floordiv, so operate
691-
# on the i8 values
692-
result = other.asi8 // self.asi8
693-
mask = self._isnan | other._isnan
694-
if mask.any():
695-
result = result.astype(np.float64)
696-
np.putmask(result, mask, np.nan)
697-
return result
662+
return self._vector_divlike_op(other, op)
698663

699664
elif is_object_dtype(other.dtype):
700665
result_list = [other[n] // self[n] for n in range(len(self))]
701666
result = np.array(result_list)
702667
return result
703668

704669
else:
705-
dtype = getattr(other, "dtype", type(other).__name__)
706-
raise TypeError(f"Cannot divide {dtype} by {type(self).__name__}")
670+
return NotImplemented
707671

708672
@unpack_zerodim_and_defer("__mod__")
709673
def __mod__(self, other):

pandas/tests/arithmetic/test_numeric.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,12 @@ def test_div_td64arr(self, left, box_cls):
177177
result = right // left
178178
tm.assert_equal(result, expected)
179179

180-
msg = "Cannot divide"
180+
# (true_) needed for min-versions build 2022-12-26
181+
msg = "ufunc '(true_)?divide' cannot use operands with types"
181182
with pytest.raises(TypeError, match=msg):
182183
left / right
183184

185+
msg = "ufunc 'floor_divide' cannot use operands with types"
184186
with pytest.raises(TypeError, match=msg):
185187
left // right
186188

pandas/tests/arithmetic/test_timedelta64.py

+1
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,7 @@ def test_td64arr_div_numeric_array(
20192019
"cannot perform __truediv__",
20202020
"unsupported operand",
20212021
"Cannot divide",
2022+
"ufunc 'divide' cannot use operands with types",
20222023
]
20232024
)
20242025
with pytest.raises(TypeError, match=pattern):

0 commit comments

Comments
 (0)