Skip to content

Commit 36a71eb

Browse files
jbrockmendeljreback
authored andcommitted
Fix Timedelta.__floordiv__, __rfloordiv__ (#18961)
1 parent 5fb018b commit 36a71eb

File tree

4 files changed

+188
-11
lines changed

4 files changed

+188
-11
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ Numeric
449449
- Bug in :func:`Series.__sub__` subtracting a non-nanosecond ``np.datetime64`` object from a ``Series`` gave incorrect results (:issue:`7996`)
450450
- Bug in :class:`DatetimeIndex`, :class:`TimedeltaIndex` addition and subtraction of zero-dimensional integer arrays gave incorrect results (:issue:`19012`)
451451
- Bug in :func:`Series.__add__` adding Series with dtype ``timedelta64[ns]`` to a timezone-aware ``DatetimeIndex`` incorrectly dropped timezone information (:issue:`13905`)
452+
- Bug in :func:`Timedelta.__floordiv__` and :func:`Timedelta.__rfloordiv__` dividing by many incompatible numpy objects was incorrectly allowed (:issue:`18846`)
452453
-
453454

454455
Categorical

pandas/_libs/tslibs/timedeltas.pyx

+84-11
Original file line numberDiff line numberDiff line change
@@ -1031,13 +1031,27 @@ class Timedelta(_Timedelta):
10311031
__rdiv__ = __rtruediv__
10321032

10331033
def __floordiv__(self, other):
1034+
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
1035+
# just defer
1036+
if hasattr(other, '_typ'):
1037+
# Series, DataFrame, ...
1038+
return NotImplemented
1039+
10341040
if hasattr(other, 'dtype'):
1035-
# work with i8
1036-
other = other.astype('m8[ns]').astype('i8')
1037-
return self.value // other
1041+
if other.dtype.kind == 'm':
1042+
# also timedelta-like
1043+
return _broadcast_floordiv_td64(self.value, other, _floordiv)
1044+
elif other.dtype.kind in ['i', 'u', 'f']:
1045+
if other.ndim == 0:
1046+
return Timedelta(self.value // other)
1047+
else:
1048+
return self.to_timedelta64() // other
1049+
1050+
raise TypeError('Invalid dtype {dtype} for '
1051+
'{op}'.format(dtype=other.dtype,
1052+
op='__floordiv__'))
10381053

1039-
elif is_integer_object(other):
1040-
# integers only
1054+
elif is_integer_object(other) or is_float_object(other):
10411055
return Timedelta(self.value // other, unit='ns')
10421056

10431057
elif not _validate_ops_compat(other):
@@ -1049,20 +1063,79 @@ class Timedelta(_Timedelta):
10491063
return self.value // other.value
10501064

10511065
def __rfloordiv__(self, other):
1052-
if hasattr(other, 'dtype'):
1053-
# work with i8
1054-
other = other.astype('m8[ns]').astype('i8')
1055-
return other // self.value
1066+
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
1067+
# just defer
1068+
if hasattr(other, '_typ'):
1069+
# Series, DataFrame, ...
1070+
return NotImplemented
10561071

1072+
if hasattr(other, 'dtype'):
1073+
if other.dtype.kind == 'm':
1074+
# also timedelta-like
1075+
return _broadcast_floordiv_td64(self.value, other, _rfloordiv)
1076+
raise TypeError('Invalid dtype {dtype} for '
1077+
'{op}'.format(dtype=other.dtype,
1078+
op='__floordiv__'))
1079+
1080+
if is_float_object(other) and util._checknull(other):
1081+
# i.e. np.nan
1082+
return NotImplemented
10571083
elif not _validate_ops_compat(other):
10581084
return NotImplemented
10591085

10601086
other = Timedelta(other)
10611087
if other is NaT:
1062-
return NaT
1088+
return np.nan
10631089
return other.value // self.value
10641090

10651091

1092+
cdef _floordiv(int64_t value, right):
1093+
return value // right
1094+
1095+
1096+
cdef _rfloordiv(int64_t value, right):
1097+
# analogous to referencing operator.div, but there is no operator.rfloordiv
1098+
return right // value
1099+
1100+
1101+
cdef _broadcast_floordiv_td64(int64_t value, object other,
1102+
object (*operation)(int64_t value,
1103+
object right)):
1104+
"""Boilerplate code shared by Timedelta.__floordiv__ and
1105+
Timedelta.__rfloordiv__ because np.timedelta64 does not implement these.
1106+
1107+
Parameters
1108+
----------
1109+
value : int64_t; `self.value` from a Timedelta object
1110+
other : object
1111+
operation : function, either _floordiv or _rfloordiv
1112+
1113+
Returns
1114+
-------
1115+
result : varies based on `other`
1116+
"""
1117+
# assumes other.dtype.kind == 'm', i.e. other is timedelta-like
1118+
cdef:
1119+
int ndim = getattr(other, 'ndim', -1)
1120+
1121+
# We need to watch out for np.timedelta64('NaT').
1122+
mask = other.view('i8') == NPY_NAT
1123+
1124+
if ndim == 0:
1125+
if mask:
1126+
return np.nan
1127+
1128+
return operation(value, other.astype('m8[ns]').astype('i8'))
1129+
1130+
else:
1131+
res = operation(value, other.astype('m8[ns]').astype('i8'))
1132+
1133+
if mask.any():
1134+
res = res.astype('f8')
1135+
res[mask] = np.nan
1136+
return res
1137+
1138+
10661139
# resolution in ns
1067-
Timedelta.min = Timedelta(np.iinfo(np.int64).min +1)
1140+
Timedelta.min = Timedelta(np.iinfo(np.int64).min + 1)
10681141
Timedelta.max = Timedelta(np.iinfo(np.int64).max)

pandas/tests/scalar/test_nat.py

+10
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,16 @@ def test_nat_arithmetic():
273273
assert right - left is NaT
274274

275275

276+
def test_nat_rfloordiv_timedelta():
277+
# GH#18846
278+
# See also test_timedelta.TestTimedeltaArithmetic.test_floordiv
279+
td = Timedelta(hours=3, minutes=4)
280+
281+
assert td // np.nan is NaT
282+
assert np.isnan(td // NaT)
283+
assert np.isnan(td // np.timedelta64('NaT'))
284+
285+
276286
def test_nat_arithmetic_index():
277287
# GH 11718
278288

pandas/tests/scalar/test_timedelta.py

+93
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def test_binary_ops_nat(self):
136136
assert (td * pd.NaT) is pd.NaT
137137
assert (td / pd.NaT) is np.nan
138138
assert (td // pd.NaT) is np.nan
139+
assert (td // np.timedelta64('NaT')) is np.nan
139140

140141
def test_binary_ops_integers(self):
141142
td = Timedelta(10, unit='d')
@@ -162,6 +163,98 @@ def test_binary_ops_with_timedelta(self):
162163
# invalid multiply with another timedelta
163164
pytest.raises(TypeError, lambda: td * td)
164165

166+
def test_floordiv(self):
167+
# GH#18846
168+
td = Timedelta(hours=3, minutes=4)
169+
scalar = Timedelta(hours=3, minutes=3)
170+
171+
# scalar others
172+
assert td // scalar == 1
173+
assert -td // scalar.to_pytimedelta() == -2
174+
assert (2 * td) // scalar.to_timedelta64() == 2
175+
176+
assert td // np.nan is pd.NaT
177+
assert np.isnan(td // pd.NaT)
178+
assert np.isnan(td // np.timedelta64('NaT'))
179+
180+
with pytest.raises(TypeError):
181+
td // np.datetime64('2016-01-01', dtype='datetime64[us]')
182+
183+
expected = Timedelta(hours=1, minutes=32)
184+
assert td // 2 == expected
185+
assert td // 2.0 == expected
186+
assert td // np.float64(2.0) == expected
187+
assert td // np.int32(2.0) == expected
188+
assert td // np.uint8(2.0) == expected
189+
190+
# Array-like others
191+
assert td // np.array(scalar.to_timedelta64()) == 1
192+
193+
res = (3 * td) // np.array([scalar.to_timedelta64()])
194+
expected = np.array([3], dtype=np.int64)
195+
tm.assert_numpy_array_equal(res, expected)
196+
197+
res = (10 * td) // np.array([scalar.to_timedelta64(),
198+
np.timedelta64('NaT')])
199+
expected = np.array([10, np.nan])
200+
tm.assert_numpy_array_equal(res, expected)
201+
202+
ser = pd.Series([1], dtype=np.int64)
203+
res = td // ser
204+
assert res.dtype.kind == 'm'
205+
206+
def test_rfloordiv(self):
207+
# GH#18846
208+
td = Timedelta(hours=3, minutes=3)
209+
scalar = Timedelta(hours=3, minutes=4)
210+
211+
# scalar others
212+
# x // Timedelta is defined only for timedelta-like x. int-like,
213+
# float-like, and date-like, in particular, should all either
214+
# a) raise TypeError directly or
215+
# b) return NotImplemented, following which the reversed
216+
# operation will raise TypeError.
217+
assert td.__rfloordiv__(scalar) == 1
218+
assert (-td).__rfloordiv__(scalar.to_pytimedelta()) == -2
219+
assert (2 * td).__rfloordiv__(scalar.to_timedelta64()) == 0
220+
221+
assert np.isnan(td.__rfloordiv__(pd.NaT))
222+
assert np.isnan(td.__rfloordiv__(np.timedelta64('NaT')))
223+
224+
dt64 = np.datetime64('2016-01-01', dtype='datetime64[us]')
225+
with pytest.raises(TypeError):
226+
td.__rfloordiv__(dt64)
227+
228+
assert td.__rfloordiv__(np.nan) is NotImplemented
229+
assert td.__rfloordiv__(3.5) is NotImplemented
230+
assert td.__rfloordiv__(2) is NotImplemented
231+
232+
with pytest.raises(TypeError):
233+
td.__rfloordiv__(np.float64(2.0))
234+
with pytest.raises(TypeError):
235+
td.__rfloordiv__(np.int32(2.0))
236+
with pytest.raises(TypeError):
237+
td.__rfloordiv__(np.uint8(9))
238+
239+
# Array-like others
240+
assert td.__rfloordiv__(np.array(scalar.to_timedelta64())) == 1
241+
242+
res = td.__rfloordiv__(np.array([(3 * scalar).to_timedelta64()]))
243+
expected = np.array([3], dtype=np.int64)
244+
tm.assert_numpy_array_equal(res, expected)
245+
246+
arr = np.array([(10 * scalar).to_timedelta64(),
247+
np.timedelta64('NaT')])
248+
res = td.__rfloordiv__(arr)
249+
expected = np.array([10, np.nan])
250+
tm.assert_numpy_array_equal(res, expected)
251+
252+
ser = pd.Series([1], dtype=np.int64)
253+
res = td.__rfloordiv__(ser)
254+
assert res is NotImplemented
255+
with pytest.raises(TypeError):
256+
ser // td
257+
165258

166259
class TestTimedeltaComparison(object):
167260
def test_comparison_object_array(self):

0 commit comments

Comments
 (0)