Skip to content

REF: simplify Timedelta arithmetic methods #33978

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 57 additions & 113 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ PyDateTime_IMPORT
cimport pandas._libs.tslibs.util as util
from pandas._libs.tslibs.util cimport (
is_timedelta64_object, is_datetime64_object, is_integer_object,
is_float_object)
is_float_object, is_array
)

from pandas._libs.tslibs.c_timestamp cimport _Timestamp

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

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

def __mul__(self, other):
if hasattr(other, '_typ'):
# Series, DataFrame, ...
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
# Tick offset; this op will raise TypeError
return other.delta * self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is covered by rmul of DateOffset?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will go through DateOffset.__rmul__ which will raise TypeError (correctly), yes.

return NotImplemented
if is_integer_object(other) or is_float_object(other):
return Timedelta(other * self.value, unit='ns')

elif util.is_nan(other):
# i.e. np.nan, but also catch np.float64("NaN") which would
# otherwise get caught by the hasattr(other, "dtype") branch
# incorrectly return a np.timedelta64 object.
return NaT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not checking this, does that give a change in behaviour?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the util.is_nan case is subsumed by the is_float_object case on L1351

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see!


elif hasattr(other, 'dtype'):
elif is_array(other):
# ndarray-like
return other * self.to_timedelta64()

elif other is NaT:
raise TypeError('Cannot multiply Timedelta with NaT')

elif not (is_integer_object(other) or is_float_object(other)):
# only integers and floats allowed
return NotImplemented

return Timedelta(other * self.value, unit='ns')
return NotImplemented

__rmul__ = __mul__

def __truediv__(self, other):
if hasattr(other, '_typ'):
# Series, DataFrame, ...
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
# Tick offset
return self / other.delta
return NotImplemented

elif is_timedelta64_object(other):
# convert to Timedelta below
pass

elif util.is_nan(other):
# i.e. np.nan, but also catch np.float64("NaN") which would
# otherwise get caught by the hasattr(other, "dtype") branch
# incorrectly return a np.timedelta64 object.
return NaT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, subsumed by the is_float_object case on L1370


elif hasattr(other, 'dtype'):
return self.to_timedelta64() / other
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
if other is NaT:
return np.nan
return self.value / float(other.value)

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

elif not _validate_ops_compat(other):
return NotImplemented
elif is_array(other):
return self.to_timedelta64() / other

other = Timedelta(other)
if other is NaT:
return np.nan
return self.value / float(other.value)
return NotImplemented

def __rtruediv__(self, other):
if hasattr(other, '_typ'):
# Series, DataFrame, ...
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
# Tick offset
return other.delta / self
return NotImplemented

elif is_timedelta64_object(other):
# convert to Timedelta below
pass

elif util.is_nan(other):
# i.e. np.nan or np.float64("NaN")
raise TypeError("Cannot divide float by Timedelta")
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
if other is NaT:
return np.nan
return float(other.value) / self.value

elif hasattr(other, 'dtype'):
elif is_array(other):
if other.dtype.kind == "O":
# GH#31869
return np.array([x / self for x in other])
return other / self.to_timedelta64()

elif not _validate_ops_compat(other):
return NotImplemented

other = Timedelta(other)
if other is NaT:
# In this context we treat NaT as timedelta-like
return np.nan
return float(other.value) / self.value
return NotImplemented

def __floordiv__(self, other):
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
# just defer
if hasattr(other, '_typ'):
# Series, DataFrame, ...
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
# Tick offset
return self // other.delta
return NotImplemented
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
if other is NaT:
return np.nan
return self.value // other.value

elif is_timedelta64_object(other):
# convert to Timedelta below
pass
elif is_integer_object(other) or is_float_object(other):
return Timedelta(self.value // other, unit='ns')

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

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

elif is_integer_object(other) or is_float_object(other):
return Timedelta(self.value // other, unit='ns')

elif not _validate_ops_compat(other):
return NotImplemented

other = Timedelta(other)
if other is NaT:
return np.nan
return self.value // other.value
return NotImplemented

def __rfloordiv__(self, other):
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
# just defer
if hasattr(other, '_typ'):
# Series, DataFrame, ...
if other._typ == 'dateoffset' and hasattr(other, 'delta'):
# Tick offset
return other.delta // self
return NotImplemented

elif is_timedelta64_object(other):
# convert to Timedelta below
pass
if _should_cast_to_timedelta(other):
# We interpret NaT as timedelta64("NaT")
other = Timedelta(other)
if other is NaT:
return np.nan
return other.value // self.value

elif hasattr(other, 'dtype'):
elif is_array(other):
if other.dtype.kind == 'm':
# also timedelta-like
return _broadcast_floordiv_td64(self.value, other, _rfloordiv)

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

elif is_float_object(other) and util.is_nan(other):
# i.e. np.nan
return NotImplemented

elif not _validate_ops_compat(other):
return NotImplemented

other = Timedelta(other)
if other is NaT:
return np.nan
return other.value // self.value
return NotImplemented

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


cdef bint is_any_td_scalar(object obj):
return (
PyDelta_Check(obj) or is_timedelta64_object(obj) or isinstance(obj, Tick)
)


cdef bint _should_cast_to_timedelta(object obj):
"""
Should we treat this object as a Timedelta for the purpose of a binary op
"""
return (
is_any_td_scalar(obj) or obj is None or obj is NaT or isinstance(obj, str)
)


cdef _floordiv(int64_t value, right):
return value // right

Expand Down
3 changes: 2 additions & 1 deletion pandas/tests/scalar/test_nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ def test_nat_arithmetic_scalar(op_name, value, val_type):
and "times" in op_name
and isinstance(value, Timedelta)
):
msg = "Cannot multiply"
typs = "(Timedelta|NaTType)"
msg = rf"unsupported operand type\(s\) for \*: '{typs}' and '{typs}'"
elif val_type == "str":
# un-specific check here because the message comes from str
# and varies by method
Expand Down
35 changes: 25 additions & 10 deletions pandas/tests/scalar/timedelta/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,13 @@ class TestTimedeltaMultiplicationDivision:
def test_td_mul_nat(self, op, td_nat):
# GH#19819
td = Timedelta(10, unit="d")
msg = "cannot use operands with types|Cannot multiply Timedelta with NaT"
typs = "|".join(["numpy.timedelta64", "NaTType", "Timedelta"])
msg = "|".join(
[
rf"unsupported operand type\(s\) for \*: '{typs}' and '{typs}'",
r"ufunc '?multiply'? cannot use operands with types",
]
)
with pytest.raises(TypeError, match=msg):
op(td, td_nat)

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

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

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

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

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

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

dt64 = np.datetime64("2016-01-01", "us")
msg = r"Invalid dtype datetime64\[us\] for __floordiv__"

assert td.__rfloordiv__(dt64) is NotImplemented

msg = (
r"unsupported operand type\(s\) for //: 'numpy.datetime64' and 'Timedelta'"
)
with pytest.raises(TypeError, match=msg):
td.__rfloordiv__(dt64)
dt64 // td

def test_td_rfloordiv_numeric_scalar(self):
# GH#18846
Expand All @@ -606,15 +618,18 @@ def test_td_rfloordiv_numeric_scalar(self):
assert td.__rfloordiv__(np.nan) is NotImplemented
assert td.__rfloordiv__(3.5) is NotImplemented
assert td.__rfloordiv__(2) is NotImplemented
assert td.__rfloordiv__(np.float64(2.0)) is NotImplemented
assert td.__rfloordiv__(np.uint8(9)) is NotImplemented
assert td.__rfloordiv__(np.int32(2.0)) is NotImplemented

msg = "Invalid dtype"
msg = r"unsupported operand type\(s\) for //: '.*' and 'Timedelta"
with pytest.raises(TypeError, match=msg):
td.__rfloordiv__(np.float64(2.0))
np.float64(2.0) // td
with pytest.raises(TypeError, match=msg):
td.__rfloordiv__(np.uint8(9))
np.uint8(9) // td
with pytest.raises(TypeError, match=msg):
# deprecated GH#19761, enforced GH#29797
td.__rfloordiv__(np.int32(2.0))
np.int32(2.0) // td

def test_td_rfloordiv_timedeltalike_array(self):
# GH#18846
Expand Down