Skip to content

ENH: implement Timedelta.__mod__ and __divmod__ #19755

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 7 commits into from
Feb 19, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
14 changes: 14 additions & 0 deletions doc/source/timedeltas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,20 @@ Rounded division (floor-division) of a ``timedelta64[ns]`` Series by a scalar
td // pd.Timedelta(days=3, hours=4)
pd.Timedelta(days=3, hours=4) // td

.. _timedeltas.mod_divmod:

The mod (%) and divmod operations are defined for ``Timedelta`` when operating with another timedelta-like or with a numeric argument. (:issue:`19365`)

.. ipython:: python

pd.Timedelta(hours=37) % datetime.timedelta(hours=2)

# divmod against a timedelta-like returns a pair (int, Timedelta)
divmod(datetime.timedelta(hours=2), pd.Timedelta(minutes=11))

# divmod against a numeric returns a pair (Timedelta, Timedelta)
Copy link
Contributor

Choose a reason for hiding this comment

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

nice!

divmod(pd.Timedelta(hours=25), 86400000000000)

Attributes
----------

Expand Down
15 changes: 15 additions & 0 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ resetting indexes. See the :ref:`Sorting by Indexes and Values
# Sort by 'second' (index) and 'A' (column)
df_multi.sort_values(by=['second', 'A'])

.. _whatsnew_0230.enhancements.timedelta_mod

Timedelta mod method
^^^^^^^^^^^^^^^^^^^^

``mod`` (%) and ``divmod`` operations are now defined on ``Timedelta`` objects
when operating with either timedelta-like or with numeric arguments.
See the :ref:`<_timedeltas.mod_divmod>` documentation section. (:issue:`19365`)

.. ipython:: python

td = pd.Timedelta(hours=37)
td % pd.Timedelta(minutes=45)

.. _whatsnew_0230.enhancements.ran_inf:

``.rank()`` handles ``inf`` values when ``NaN`` are present
Expand Down Expand Up @@ -571,6 +585,7 @@ Other API Changes
- Set operations (union, difference...) on :class:`IntervalIndex` with incompatible index types will now raise a ``TypeError`` rather than a ``ValueError`` (:issue:`19329`)
- :class:`DateOffset` objects render more simply, e.g. "<DateOffset: days=1>" instead of "<DateOffset: kwds={'days': 1}>" (:issue:`19403`)
- :func:`pandas.merge` provides a more informative error message when trying to merge on timezone-aware and timezone-naive columns (:issue:`15800`)
- :func:`Timedelta.__mod__`, :func:`Timedelta.__divmod__` now accept timedelta-like and numeric arguments instead of raising ``TypeError`` (:issue:`19365`)
Copy link
Member

Choose a reason for hiding this comment

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

This is the same as above? Isn't it enough to mention it once?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is an attempt to follow the instructions in #19365, some of which were unclear. I'm open to suggestions.

Copy link
Member

Choose a reason for hiding this comment

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

My suggestion would be to just remove this one, but I don't know the reason for the previous instruction

Copy link
Member Author

Choose a reason for hiding this comment

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

@JRBACK is there consensus on this suggestion?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh i guess I wasn't clear. if you make a sub-section then no reason to repeat things, so kill this entry as it dupes the sub-section.


.. _whatsnew_0230.deprecations:

Expand Down
18 changes: 18 additions & 0 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,24 @@ class Timedelta(_Timedelta):
return np.nan
return other.value // self.value

def __mod__(self, other):
# Naive implementation, room for optimization
return self.__divmod__(other)[1]

def __rmod__(self, other):
# Naive implementation, room for optimization
return self.__rdivmod__(other)[1]

def __divmod__(self, other):
# Naive implementation, room for optimization
div = self // other
return div, self - div * other

def __rdivmod__(self, other):
# Naive implementation, room for optimization
div = other // self
return div, other - div * self


cdef _floordiv(int64_t value, right):
return value // right
Expand Down
180 changes: 180 additions & 0 deletions pandas/tests/scalar/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,186 @@ def test_rfloordiv(self):
with pytest.raises(TypeError):
ser // td

def test_td_mod_timedeltalike(self):
Copy link
Member

Choose a reason for hiding this comment

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

Like test_rfloordiv above, I think you can leave out the _td_ in all test names (the class name already makes clear it is about Timedelta)

Copy link
Member Author

Choose a reason for hiding this comment

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

ok

# GH#19365
td = Timedelta(hours=37)

# Timedelta-like others
result = td % Timedelta(hours=6)
assert isinstance(result, Timedelta)
assert result == Timedelta(hours=1)

result = td % timedelta(minutes=60)
assert isinstance(result, Timedelta)
assert result == Timedelta(0)

result = td % NaT
assert result is NaT

@pytest.mark.xfail(reason='GH#19378 floordiv td64 returns td64')
def test_td_mod_timedelta64_nat(self):
# GH#19365
td = Timedelta(hours=37)

result = td % np.timedelta64('NaT', 'ns')
assert result is NaT

@pytest.mark.xfail(reason='GH#19378 floordiv td64 returns td64')
def test_td_mod_timedelta64(self):
# GH#19365
td = Timedelta(hours=37)

result = td % np.timedelta64(2, 'h')
assert isinstance(result, Timedelta)
assert result == Timedelta(hours=1)

@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
def test_td_mod_offset(self):
# GH#19365
td = Timedelta(hours=37)

result = td % pd.offsets.Hour(5)
assert isinstance(result, Timedelta)
assert result == Timedelta(hours=2)

def test_td_mod_numeric(self):
# GH#19365
td = Timedelta(hours=37)

# Numeric Others
result = td % 2
assert isinstance(result, Timedelta)
assert result == Timedelta(0)

result = td % 1e12
assert isinstance(result, Timedelta)
assert result == Timedelta(minutes=3, seconds=20)

result = td % int(1e12)
assert isinstance(result, Timedelta)
assert result == Timedelta(minutes=3, seconds=20)

def test_td_mod_invalid(self):
# GH#19365
td = Timedelta(hours=37)

with pytest.raises(TypeError):
td % pd.Timestamp('2018-01-22')

with pytest.raises(TypeError):
td % []

def test_td_rmod_pytimedelta(self):
# GH#19365
td = Timedelta(minutes=3)

result = timedelta(minutes=4) % td
assert isinstance(result, Timedelta)
assert result == Timedelta(minutes=1)

@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
def test_td_rmod_timedelta64(self):
# GH#19365
td = Timedelta(minutes=3)
result = np.timedelta64(5, 'm') % td
assert isinstance(result, Timedelta)
assert result == Timedelta(minutes=2)

def test_td_rmod_invalid(self):
# GH#19365
td = Timedelta(minutes=3)

with pytest.raises(TypeError):
pd.Timestamp('2018-01-22') % td

with pytest.raises(TypeError):
15 % td

with pytest.raises(TypeError):
16.0 % td

with pytest.raises(TypeError):
np.array([22, 24]) % td

def test_td_divmod_numeric(self):
# GH#19365
td = Timedelta(days=2, hours=6)

result = divmod(td, 53 * 3600 * 1e9)
assert result[0] == Timedelta(1, unit='ns')
assert isinstance(result[1], Timedelta)
assert result[1] == Timedelta(hours=1)

assert result
result = divmod(td, np.nan)
assert result[0] is pd.NaT
assert result[1] is pd.NaT

def test_td_divmod(self):
# GH#19365
td = Timedelta(days=2, hours=6)

result = divmod(td, timedelta(days=1))
assert result[0] == 2
assert isinstance(result[1], Timedelta)
assert result[1] == Timedelta(hours=6)

result = divmod(td, 54)
assert result[0] == Timedelta(hours=1)
assert isinstance(result[1], Timedelta)
assert result[1] == Timedelta(0)

result = divmod(td, pd.NaT)
assert np.isnan(result[0])
assert result[1] is pd.NaT

@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
def test_td_divmod_offset(self):
# GH#19365
td = Timedelta(days=2, hours=6)

result = divmod(td, pd.offsets.Hour(-4))
assert result[0] == -14
assert isinstance(result[1], Timedelta)
assert result[1] == Timedelta(hours=-2)

def test_td_divmod_invalid(self):
# GH#19365
td = Timedelta(days=2, hours=6)

with pytest.raises(TypeError):
divmod(td, pd.Timestamp('2018-01-22'))

def test_td_rdivmod_pytimedelta(self):
# GH#19365
result = divmod(timedelta(days=2, hours=6), Timedelta(days=1))
assert result[0] == 2
assert isinstance(result[1], Timedelta)
assert result[1] == Timedelta(hours=6)

@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
def test_td_rdivmod_offset(self):
result = divmod(pd.offsets.Hour(54), Timedelta(hours=-4))
assert result[0] == -14
assert isinstance(result[1], Timedelta)
assert result[1] == Timedelta(hours=-2)

def test_td_rdivmod_invalid(self):
# GH#19365
td = Timedelta(minutes=3)

with pytest.raises(TypeError):
divmod(pd.Timestamp('2018-01-22'), td)

with pytest.raises(TypeError):
divmod(15, td)

with pytest.raises(TypeError):
divmod(16.0, td)

with pytest.raises(TypeError):
divmod(np.array([22, 24]), td)


class TestTimedeltaComparison(object):
def test_comparison_object_array(self):
Expand Down