Skip to content

Fix Timedelta.__floordiv__, __rfloordiv__ #18961

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 17 commits into from
Jan 7, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
70 changes: 61 additions & 9 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -975,13 +975,40 @@ class Timedelta(_Timedelta):
__rdiv__ = __rtruediv__

def __floordiv__(self, other):
# numpy does not implement floordiv for timedelta64 dtype, so we cannot
# just defer
if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if hasattr(other, 'dtype'):
# work with i8
other = other.astype('m8[ns]').astype('i8')
return self.value // other
if other.dtype.kind == 'm':
# also timedelta-like
# We need to watch out for np.timedelta64('NaT')
if np.ndim(other) == 0:
if np.isnat(other):
Copy link
Contributor

Choose a reason for hiding this comment

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

pls try to be DRY
don’t use np.ndim ; we don’t use it anywhere else
don’t use np.isnat

Copy link
Member Author

Choose a reason for hiding this comment

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

don’t use np.isnat

This is specifically trying to catch np.timedelta64('NaT') and because of np.NaT rules we can't use is timedelta64('NaT') and we can't use == timedelta64('NaT')

return np.nan
else:
other = other.astype('m8[ns]').astype('i8')
return self.value // other
else:
mask = np.isnat(other)
res = self.value // other.astype('m8[ns]').astype('i8')
if mask.any():
res = res.astype('f8')
res[mask] = np.nan
return res
elif other.dtype.kind in ['i', 'u', 'f']:
if np.ndim(other) == 0:
return Timedelta(self.value // other)
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

you don't need the else

also pls use other.ndim

return self.to_timedelta64() // other
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

just raise you don't need an else

raise TypeError('Invalid dtype {dtype} for '
'{op}'.format(dtype=other.dtype,
op='__floordiv__'))

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

elif not _validate_ops_compat(other):
Expand All @@ -993,17 +1020,42 @@ class Timedelta(_Timedelta):
return self.value // other.value

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

if hasattr(other, 'dtype'):
# work with i8
other = other.astype('m8[ns]').astype('i8')
return other // self.value
if other.dtype.kind == 'm':
# also timedelta-like
Copy link
Contributor

Choose a reason for hiding this comment

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

iirc for division we defer to. TDI to handle his logic
does this code path follow?

Copy link
Member Author

Choose a reason for hiding this comment

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

iirc for division we defer to. TDI to handle his logic

I would find that design choice surprising.

    def __truediv__(self, other):
        if hasattr(other, 'dtype'):
            return self.to_timedelta64() / other

        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

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

I think the relevant difference here is for cases where other is an array this returns NotImplemented, but that isn't a viable option for floordiv because numpy doesn't implement floordiv for timedelta64.

# We need to watch out for np.timedelta64('NaT')
if np.ndim(other) == 0:
if np.isnat(other):
return np.nan
else:
other = other.astype('m8[ns]').astype('i8')
return other // self.value
else:
mask = np.isnat(other)
res = other.astype('m8[ns]').astype('i8') // self.value
if mask.any():
res = res.astype('f8')
res[mask] = np.nan
return res
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

same

raise TypeError('Invalid dtype {dtype} for '
'{op}'.format(dtype=other.dtype,
op='__floordiv__'))

if isinstance(other, float) and np.isnan(other):
Copy link
Contributor

Choose a reason for hiding this comment

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

huh?
why are you not just checking isna

Copy link
Member Author

Choose a reason for hiding this comment

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

Because np.nan (which can't be detected with other is np.nan) gets treated differently from pd.NaT.

Copy link
Contributor

Choose a reason for hiding this comment

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

why? nan is equiv of nat 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.

why? nan is equiv of nat here

That's slightly ambiguous, but I lean towards "no it isn't". Timedelta(nan) does return NaT, but Timedelta / float is meaningful on its own, so we should treat nan as a float for the purposes of this operation.

Copy link
Contributor

Choose a reason for hiding this comment

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

checknull(other) and is_float_object(other) is more clear here (and add a comment)

return NotImplemented
elif not _validate_ops_compat(other):
return NotImplemented

other = Timedelta(other)
if other is NaT:
return NaT
return np.nan
Copy link
Contributor

Choose a reason for hiding this comment

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

is this what we do for division?
is it not tested now?

Copy link
Member Author

Choose a reason for hiding this comment

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

is this what we do for division?

Yes.

The status quo is... weird.

    def __floordiv__(self, other):
        if hasattr(other, 'dtype'):
            # work with i8
            other = other.astype('m8[ns]').astype('i8')
            return self.value // other

        elif is_integer_object(other):
            # integers only
            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

    def __rfloordiv__(self, other):
        if hasattr(other, 'dtype'):
            # work with i8
            other = other.astype('m8[ns]').astype('i8')
            return other // self.value

        elif not _validate_ops_compat(other):
            return NotImplemented

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

is it not tested now?

Not that I'm aware of.

return other.value // self.value


Expand Down
87 changes: 87 additions & 0 deletions pandas/tests/scalar/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,93 @@ def test_binary_ops_with_timedelta(self):
# invalid multiply with another timedelta
pytest.raises(TypeError, lambda: td * td)

def test_floordiv(self):
# GH#18846
td = Timedelta(hours=3, minutes=4)
scalar = Timedelta(hours=3, minutes=3)

# scalar others
assert td // scalar == 1
assert -td // scalar.to_pytimedelta() == -2
assert (2 * td) // scalar.to_timedelta64() == 2

assert td // np.nan is pd.NaT
assert np.isnan(td // pd.NaT)
Copy link
Contributor

Choose a reason for hiding this comment

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

these should prob be in test_nat with other nat tests

Copy link
Member Author

Choose a reason for hiding this comment

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

TestTimedeltaArithmetic doesn't have a dedicated test/section for nat tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

pandas/tests/scalar/test_nat is where the nat tests go (i don't mind them here), but should have some (even if slightly duplicative)

assert np.isnan(td // np.timedelta64('NaT'))

with pytest.raises(TypeError):
td // np.datetime64('2016-01-01', dtype='datetime64[us]')

expected = Timedelta(hours=1, minutes=32)
assert td // 2 == expected
assert td // 2.0 == expected
assert td // np.float64(2.0) == expected
assert td // np.int32(2.0) == expected
assert td // np.uint8(2.0) == expected

# Array-like others
assert td // np.array(scalar.to_timedelta64()) == 1

res = (3 * td) // np.array([scalar.to_timedelta64()])
expected = np.array([3])
np.testing.assert_array_equal(res, expected)

res = (10 * td) // np.array([scalar.to_timedelta64(),
np.timedelta64('NaT')])
expected = np.array([10, np.nan])
np.testing.assert_array_equal(res, expected)

ser = pd.Series([1], dtype=np.int64)
res = td // ser
assert res.dtype.kind == 'm'

def test_rfloordiv(self):
# GH#18846
td = Timedelta(hours=3, minutes=3)
scalar = Timedelta(hours=3, minutes=4)

# scalar others
assert td.__rfloordiv__(scalar) == 1
assert (-td).__rfloordiv__(scalar.to_pytimedelta()) == -2
assert (2 * td).__rfloordiv__(scalar.to_timedelta64()) == 0

assert np.isnan(td.__rfloordiv__(pd.NaT))
assert np.isnan(td.__rfloordiv__(np.timedelta64('NaT')))

dt64 = np.datetime64('2016-01-01', dtype='datetime64[us]')
with pytest.raises(TypeError):
td.__rfloordiv__(dt64)
Copy link
Contributor

Choose a reason for hiding this comment

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

adding a bit more comments here enumerating the cases would be useful


assert td.__rfloordiv__(np.nan) is NotImplemented
assert td.__rfloordiv__(3.5) is NotImplemented
assert td.__rfloordiv__(2) is NotImplemented

with pytest.raises(TypeError):
td.__rfloordiv__(np.float64(2.0))
with pytest.raises(TypeError):
td.__rfloordiv__(np.int32(2.0))
with pytest.raises(TypeError):
td.__rfloordiv__(np.uint8(9))

# Array-like others
assert td.__rfloordiv__(np.array(scalar.to_timedelta64())) == 1

res = td.__rfloordiv__(np.array([(3 * scalar).to_timedelta64()]))
expected = np.array([3])
np.testing.assert_array_equal(res, expected)

arr = np.array([(10 * scalar).to_timedelta64(),
np.timedelta64('NaT')])
res = td.__rfloordiv__(arr)
expected = np.array([10, np.nan])
np.testing.assert_array_equal(res, expected)

ser = pd.Series([1], dtype=np.int64)
res = td.__rfloordiv__(ser)
assert res is NotImplemented
with pytest.raises(TypeError):
ser // td


class TestTimedeltas(object):
_multiprocess_can_split_ = True
Expand Down