Skip to content

Commit 8771c25

Browse files
authored
REF: simplify Timedelta arithmetic methods (#33978)
1 parent 5aadb12 commit 8771c25

File tree

3 files changed

+84
-124
lines changed

3 files changed

+84
-124
lines changed

pandas/_libs/tslibs/timedeltas.pyx

+57-113
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ PyDateTime_IMPORT
1818
cimport pandas._libs.tslibs.util as util
1919
from pandas._libs.tslibs.util cimport (
2020
is_timedelta64_object, is_datetime64_object, is_integer_object,
21-
is_float_object)
21+
is_float_object, is_array
22+
)
2223

2324
from pandas._libs.tslibs.c_timestamp cimport _Timestamp
2425

@@ -606,7 +607,7 @@ def _binary_op_method_timedeltalike(op, name):
606607
# We are implicitly requiring the canonical behavior to be
607608
# defined by Timestamp methods.
608609

609-
elif hasattr(other, 'dtype'):
610+
elif is_array(other):
610611
# nd-array like
611612
if other.dtype.kind in ['m', 'M']:
612613
return op(self.to_timedelta64(), other)
@@ -1347,113 +1348,64 @@ class Timedelta(_Timedelta):
13471348
__rsub__ = _binary_op_method_timedeltalike(lambda x, y: y - x, '__rsub__')
13481349

13491350
def __mul__(self, other):
1350-
if hasattr(other, '_typ'):
1351-
# Series, DataFrame, ...
1352-
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
1353-
# Tick offset; this op will raise TypeError
1354-
return other.delta * self
1355-
return NotImplemented
1351+
if is_integer_object(other) or is_float_object(other):
1352+
return Timedelta(other * self.value, unit='ns')
13561353

1357-
elif util.is_nan(other):
1358-
# i.e. np.nan, but also catch np.float64("NaN") which would
1359-
# otherwise get caught by the hasattr(other, "dtype") branch
1360-
# incorrectly return a np.timedelta64 object.
1361-
return NaT
1362-
1363-
elif hasattr(other, 'dtype'):
1354+
elif is_array(other):
13641355
# ndarray-like
13651356
return other * self.to_timedelta64()
13661357

1367-
elif other is NaT:
1368-
raise TypeError('Cannot multiply Timedelta with NaT')
1369-
1370-
elif not (is_integer_object(other) or is_float_object(other)):
1371-
# only integers and floats allowed
1372-
return NotImplemented
1373-
1374-
return Timedelta(other * self.value, unit='ns')
1358+
return NotImplemented
13751359

13761360
__rmul__ = __mul__
13771361

13781362
def __truediv__(self, other):
1379-
if hasattr(other, '_typ'):
1380-
# Series, DataFrame, ...
1381-
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
1382-
# Tick offset
1383-
return self / other.delta
1384-
return NotImplemented
1385-
1386-
elif is_timedelta64_object(other):
1387-
# convert to Timedelta below
1388-
pass
1389-
1390-
elif util.is_nan(other):
1391-
# i.e. np.nan, but also catch np.float64("NaN") which would
1392-
# otherwise get caught by the hasattr(other, "dtype") branch
1393-
# incorrectly return a np.timedelta64 object.
1394-
return NaT
1395-
1396-
elif hasattr(other, 'dtype'):
1397-
return self.to_timedelta64() / other
1363+
if _should_cast_to_timedelta(other):
1364+
# We interpret NaT as timedelta64("NaT")
1365+
other = Timedelta(other)
1366+
if other is NaT:
1367+
return np.nan
1368+
return self.value / float(other.value)
13981369

13991370
elif is_integer_object(other) or is_float_object(other):
14001371
# integers or floats
14011372
return Timedelta(self.value / other, unit='ns')
14021373

1403-
elif not _validate_ops_compat(other):
1404-
return NotImplemented
1374+
elif is_array(other):
1375+
return self.to_timedelta64() / other
14051376

1406-
other = Timedelta(other)
1407-
if other is NaT:
1408-
return np.nan
1409-
return self.value / float(other.value)
1377+
return NotImplemented
14101378

14111379
def __rtruediv__(self, other):
1412-
if hasattr(other, '_typ'):
1413-
# Series, DataFrame, ...
1414-
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
1415-
# Tick offset
1416-
return other.delta / self
1417-
return NotImplemented
1418-
1419-
elif is_timedelta64_object(other):
1420-
# convert to Timedelta below
1421-
pass
1422-
1423-
elif util.is_nan(other):
1424-
# i.e. np.nan or np.float64("NaN")
1425-
raise TypeError("Cannot divide float by Timedelta")
1380+
if _should_cast_to_timedelta(other):
1381+
# We interpret NaT as timedelta64("NaT")
1382+
other = Timedelta(other)
1383+
if other is NaT:
1384+
return np.nan
1385+
return float(other.value) / self.value
14261386

1427-
elif hasattr(other, 'dtype'):
1387+
elif is_array(other):
14281388
if other.dtype.kind == "O":
14291389
# GH#31869
14301390
return np.array([x / self for x in other])
14311391
return other / self.to_timedelta64()
14321392

1433-
elif not _validate_ops_compat(other):
1434-
return NotImplemented
1435-
1436-
other = Timedelta(other)
1437-
if other is NaT:
1438-
# In this context we treat NaT as timedelta-like
1439-
return np.nan
1440-
return float(other.value) / self.value
1393+
return NotImplemented
14411394

14421395
def __floordiv__(self, other):
14431396
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
14441397
# just defer
1445-
if hasattr(other, '_typ'):
1446-
# Series, DataFrame, ...
1447-
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
1448-
# Tick offset
1449-
return self // other.delta
1450-
return NotImplemented
1398+
if _should_cast_to_timedelta(other):
1399+
# We interpret NaT as timedelta64("NaT")
1400+
other = Timedelta(other)
1401+
if other is NaT:
1402+
return np.nan
1403+
return self.value // other.value
14511404

1452-
elif is_timedelta64_object(other):
1453-
# convert to Timedelta below
1454-
pass
1405+
elif is_integer_object(other) or is_float_object(other):
1406+
return Timedelta(self.value // other, unit='ns')
14551407

1456-
elif hasattr(other, 'dtype'):
1408+
elif is_array(other):
14571409
if other.dtype.kind == 'm':
14581410
# also timedelta-like
14591411
return _broadcast_floordiv_td64(self.value, other, _floordiv)
@@ -1465,50 +1417,27 @@ class Timedelta(_Timedelta):
14651417

14661418
raise TypeError(f'Invalid dtype {other.dtype} for __floordiv__')
14671419

1468-
elif is_integer_object(other) or is_float_object(other):
1469-
return Timedelta(self.value // other, unit='ns')
1470-
1471-
elif not _validate_ops_compat(other):
1472-
return NotImplemented
1473-
1474-
other = Timedelta(other)
1475-
if other is NaT:
1476-
return np.nan
1477-
return self.value // other.value
1420+
return NotImplemented
14781421

14791422
def __rfloordiv__(self, other):
14801423
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
14811424
# just defer
1482-
if hasattr(other, '_typ'):
1483-
# Series, DataFrame, ...
1484-
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
1485-
# Tick offset
1486-
return other.delta // self
1487-
return NotImplemented
1488-
1489-
elif is_timedelta64_object(other):
1490-
# convert to Timedelta below
1491-
pass
1425+
if _should_cast_to_timedelta(other):
1426+
# We interpret NaT as timedelta64("NaT")
1427+
other = Timedelta(other)
1428+
if other is NaT:
1429+
return np.nan
1430+
return other.value // self.value
14921431

1493-
elif hasattr(other, 'dtype'):
1432+
elif is_array(other):
14941433
if other.dtype.kind == 'm':
14951434
# also timedelta-like
14961435
return _broadcast_floordiv_td64(self.value, other, _rfloordiv)
14971436

14981437
# Includes integer array // Timedelta, disallowed in GH#19761
14991438
raise TypeError(f'Invalid dtype {other.dtype} for __floordiv__')
15001439

1501-
elif is_float_object(other) and util.is_nan(other):
1502-
# i.e. np.nan
1503-
return NotImplemented
1504-
1505-
elif not _validate_ops_compat(other):
1506-
return NotImplemented
1507-
1508-
other = Timedelta(other)
1509-
if other is NaT:
1510-
return np.nan
1511-
return other.value // self.value
1440+
return NotImplemented
15121441

15131442
def __mod__(self, other):
15141443
# Naive implementation, room for optimization
@@ -1529,6 +1458,21 @@ class Timedelta(_Timedelta):
15291458
return div, other - div * self
15301459

15311460

1461+
cdef bint is_any_td_scalar(object obj):
1462+
return (
1463+
PyDelta_Check(obj) or is_timedelta64_object(obj) or isinstance(obj, Tick)
1464+
)
1465+
1466+
1467+
cdef bint _should_cast_to_timedelta(object obj):
1468+
"""
1469+
Should we treat this object as a Timedelta for the purpose of a binary op
1470+
"""
1471+
return (
1472+
is_any_td_scalar(obj) or obj is None or obj is NaT or isinstance(obj, str)
1473+
)
1474+
1475+
15321476
cdef _floordiv(int64_t value, right):
15331477
return value // right
15341478

pandas/tests/scalar/test_nat.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ def test_nat_arithmetic_scalar(op_name, value, val_type):
389389
and "times" in op_name
390390
and isinstance(value, Timedelta)
391391
):
392-
msg = "Cannot multiply"
392+
typs = "(Timedelta|NaTType)"
393+
msg = rf"unsupported operand type\(s\) for \*: '{typs}' and '{typs}'"
393394
elif val_type == "str":
394395
# un-specific check here because the message comes from str
395396
# and varies by method

pandas/tests/scalar/timedelta/test_arithmetic.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,13 @@ class TestTimedeltaMultiplicationDivision:
325325
def test_td_mul_nat(self, op, td_nat):
326326
# GH#19819
327327
td = Timedelta(10, unit="d")
328-
msg = "cannot use operands with types|Cannot multiply Timedelta with NaT"
328+
typs = "|".join(["numpy.timedelta64", "NaTType", "Timedelta"])
329+
msg = "|".join(
330+
[
331+
rf"unsupported operand type\(s\) for \*: '{typs}' and '{typs}'",
332+
r"ufunc '?multiply'? cannot use operands with types",
333+
]
334+
)
329335
with pytest.raises(TypeError, match=msg):
330336
op(td, td_nat)
331337

@@ -457,11 +463,11 @@ def test_td_rdiv_na_scalar(self):
457463
result = np.timedelta64("NaT") / td
458464
assert np.isnan(result)
459465

460-
msg = "cannot use operands with types dtype"
466+
msg = r"unsupported operand type\(s\) for /: 'numpy.datetime64' and 'Timedelta'"
461467
with pytest.raises(TypeError, match=msg):
462468
np.datetime64("NaT") / td
463469

464-
msg = "Cannot divide float by Timedelta"
470+
msg = r"unsupported operand type\(s\) for /: 'float' and 'Timedelta'"
465471
with pytest.raises(TypeError, match=msg):
466472
np.nan / td
467473

@@ -479,7 +485,7 @@ def test_td_rdiv_ndarray(self):
479485
tm.assert_numpy_array_equal(result, expected)
480486

481487
arr = np.array([np.nan], dtype=object)
482-
msg = "Cannot divide float by Timedelta"
488+
msg = r"unsupported operand type\(s\) for /: 'float' and 'Timedelta'"
483489
with pytest.raises(TypeError, match=msg):
484490
arr / td
485491

@@ -522,6 +528,7 @@ def test_td_floordiv_invalid_scalar(self):
522528
[
523529
r"Invalid dtype datetime64\[D\] for __floordiv__",
524530
"'dtype' is an invalid keyword argument for this function",
531+
r"ufunc '?floor_divide'? cannot use operands with types",
525532
]
526533
)
527534
with pytest.raises(TypeError, match=msg):
@@ -595,9 +602,14 @@ def test_td_rfloordiv_invalid_scalar(self):
595602
td = Timedelta(hours=3, minutes=3)
596603

597604
dt64 = np.datetime64("2016-01-01", "us")
598-
msg = r"Invalid dtype datetime64\[us\] for __floordiv__"
605+
606+
assert td.__rfloordiv__(dt64) is NotImplemented
607+
608+
msg = (
609+
r"unsupported operand type\(s\) for //: 'numpy.datetime64' and 'Timedelta'"
610+
)
599611
with pytest.raises(TypeError, match=msg):
600-
td.__rfloordiv__(dt64)
612+
dt64 // td
601613

602614
def test_td_rfloordiv_numeric_scalar(self):
603615
# GH#18846
@@ -606,15 +618,18 @@ def test_td_rfloordiv_numeric_scalar(self):
606618
assert td.__rfloordiv__(np.nan) is NotImplemented
607619
assert td.__rfloordiv__(3.5) is NotImplemented
608620
assert td.__rfloordiv__(2) is NotImplemented
621+
assert td.__rfloordiv__(np.float64(2.0)) is NotImplemented
622+
assert td.__rfloordiv__(np.uint8(9)) is NotImplemented
623+
assert td.__rfloordiv__(np.int32(2.0)) is NotImplemented
609624

610-
msg = "Invalid dtype"
625+
msg = r"unsupported operand type\(s\) for //: '.*' and 'Timedelta"
611626
with pytest.raises(TypeError, match=msg):
612-
td.__rfloordiv__(np.float64(2.0))
627+
np.float64(2.0) // td
613628
with pytest.raises(TypeError, match=msg):
614-
td.__rfloordiv__(np.uint8(9))
629+
np.uint8(9) // td
615630
with pytest.raises(TypeError, match=msg):
616631
# deprecated GH#19761, enforced GH#29797
617-
td.__rfloordiv__(np.int32(2.0))
632+
np.int32(2.0) // td
618633

619634
def test_td_rfloordiv_timedeltalike_array(self):
620635
# GH#18846

0 commit comments

Comments
 (0)