Skip to content

Commit f83893c

Browse files
jbrockmendelharisbal
authored and
harisbal
committed
ENH: implement Timedelta.__mod__ and __divmod__ (pandas-dev#19755)
1 parent ea14495 commit f83893c

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

doc/source/timedeltas.rst

+14
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,20 @@ Rounded division (floor-division) of a ``timedelta64[ns]`` Series by a scalar
283283
td // pd.Timedelta(days=3, hours=4)
284284
pd.Timedelta(days=3, hours=4) // td
285285
286+
.. _timedeltas.mod_divmod:
287+
288+
The mod (%) and divmod operations are defined for ``Timedelta`` when operating with another timedelta-like or with a numeric argument.
289+
290+
.. ipython:: python
291+
292+
pd.Timedelta(hours=37) % datetime.timedelta(hours=2)
293+
294+
# divmod against a timedelta-like returns a pair (int, Timedelta)
295+
divmod(datetime.timedelta(hours=2), pd.Timedelta(minutes=11))
296+
297+
# divmod against a numeric returns a pair (Timedelta, Timedelta)
298+
divmod(pd.Timedelta(hours=25), 86400000000000)
299+
286300
Attributes
287301
----------
288302

doc/source/whatsnew/v0.23.0.txt

+14
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ resetting indexes. See the :ref:`Sorting by Indexes and Values
117117
# Sort by 'second' (index) and 'A' (column)
118118
df_multi.sort_values(by=['second', 'A'])
119119

120+
.. _whatsnew_0230.enhancements.timedelta_mod
121+
122+
Timedelta mod method
123+
^^^^^^^^^^^^^^^^^^^^
124+
125+
``mod`` (%) and ``divmod`` operations are now defined on ``Timedelta`` objects
126+
when operating with either timedelta-like or with numeric arguments.
127+
See the :ref:`documentation here <timedeltas.mod_divmod>`. (:issue:`19365`)
128+
129+
.. ipython:: python
130+
131+
td = pd.Timedelta(hours=37)
132+
td % pd.Timedelta(minutes=45)
133+
120134
.. _whatsnew_0230.enhancements.ran_inf:
121135

122136
``.rank()`` handles ``inf`` values when ``NaN`` are present

pandas/_libs/tslibs/timedeltas.pyx

+18
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,24 @@ class Timedelta(_Timedelta):
11491149
return np.nan
11501150
return other.value // self.value
11511151

1152+
def __mod__(self, other):
1153+
# Naive implementation, room for optimization
1154+
return self.__divmod__(other)[1]
1155+
1156+
def __rmod__(self, other):
1157+
# Naive implementation, room for optimization
1158+
return self.__rdivmod__(other)[1]
1159+
1160+
def __divmod__(self, other):
1161+
# Naive implementation, room for optimization
1162+
div = self // other
1163+
return div, self - div * other
1164+
1165+
def __rdivmod__(self, other):
1166+
# Naive implementation, room for optimization
1167+
div = other // self
1168+
return div, other - div * self
1169+
11521170

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

pandas/tests/scalar/timedelta/test_arithmetic.py

+186
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,189 @@ def test_td_rfloordiv_numeric_series(self):
420420
assert res is NotImplemented
421421
with pytest.raises(TypeError):
422422
ser // td
423+
424+
def test_mod_timedeltalike(self):
425+
# GH#19365
426+
td = Timedelta(hours=37)
427+
428+
# Timedelta-like others
429+
result = td % Timedelta(hours=6)
430+
assert isinstance(result, Timedelta)
431+
assert result == Timedelta(hours=1)
432+
433+
result = td % timedelta(minutes=60)
434+
assert isinstance(result, Timedelta)
435+
assert result == Timedelta(0)
436+
437+
result = td % NaT
438+
assert result is NaT
439+
440+
@pytest.mark.xfail(reason='GH#19378 floordiv td64 returns td64')
441+
def test_mod_timedelta64_nat(self):
442+
# GH#19365
443+
td = Timedelta(hours=37)
444+
445+
result = td % np.timedelta64('NaT', 'ns')
446+
assert result is NaT
447+
448+
@pytest.mark.xfail(reason='GH#19378 floordiv td64 returns td64')
449+
def test_mod_timedelta64(self):
450+
# GH#19365
451+
td = Timedelta(hours=37)
452+
453+
result = td % np.timedelta64(2, 'h')
454+
assert isinstance(result, Timedelta)
455+
assert result == Timedelta(hours=1)
456+
457+
@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
458+
def test_mod_offset(self):
459+
# GH#19365
460+
td = Timedelta(hours=37)
461+
462+
result = td % pd.offsets.Hour(5)
463+
assert isinstance(result, Timedelta)
464+
assert result == Timedelta(hours=2)
465+
466+
# ----------------------------------------------------------------
467+
# Timedelta.__mod__, __rmod__
468+
469+
def test_mod_numeric(self):
470+
# GH#19365
471+
td = Timedelta(hours=37)
472+
473+
# Numeric Others
474+
result = td % 2
475+
assert isinstance(result, Timedelta)
476+
assert result == Timedelta(0)
477+
478+
result = td % 1e12
479+
assert isinstance(result, Timedelta)
480+
assert result == Timedelta(minutes=3, seconds=20)
481+
482+
result = td % int(1e12)
483+
assert isinstance(result, Timedelta)
484+
assert result == Timedelta(minutes=3, seconds=20)
485+
486+
def test_mod_invalid(self):
487+
# GH#19365
488+
td = Timedelta(hours=37)
489+
490+
with pytest.raises(TypeError):
491+
td % pd.Timestamp('2018-01-22')
492+
493+
with pytest.raises(TypeError):
494+
td % []
495+
496+
def test_rmod_pytimedelta(self):
497+
# GH#19365
498+
td = Timedelta(minutes=3)
499+
500+
result = timedelta(minutes=4) % td
501+
assert isinstance(result, Timedelta)
502+
assert result == Timedelta(minutes=1)
503+
504+
@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
505+
def test_rmod_timedelta64(self):
506+
# GH#19365
507+
td = Timedelta(minutes=3)
508+
result = np.timedelta64(5, 'm') % td
509+
assert isinstance(result, Timedelta)
510+
assert result == Timedelta(minutes=2)
511+
512+
def test_rmod_invalid(self):
513+
# GH#19365
514+
td = Timedelta(minutes=3)
515+
516+
with pytest.raises(TypeError):
517+
pd.Timestamp('2018-01-22') % td
518+
519+
with pytest.raises(TypeError):
520+
15 % td
521+
522+
with pytest.raises(TypeError):
523+
16.0 % td
524+
525+
with pytest.raises(TypeError):
526+
np.array([22, 24]) % td
527+
528+
# ----------------------------------------------------------------
529+
# Timedelta.__divmod__, __rdivmod__
530+
531+
def test_divmod_numeric(self):
532+
# GH#19365
533+
td = Timedelta(days=2, hours=6)
534+
535+
result = divmod(td, 53 * 3600 * 1e9)
536+
assert result[0] == Timedelta(1, unit='ns')
537+
assert isinstance(result[1], Timedelta)
538+
assert result[1] == Timedelta(hours=1)
539+
540+
assert result
541+
result = divmod(td, np.nan)
542+
assert result[0] is pd.NaT
543+
assert result[1] is pd.NaT
544+
545+
def test_divmod(self):
546+
# GH#19365
547+
td = Timedelta(days=2, hours=6)
548+
549+
result = divmod(td, timedelta(days=1))
550+
assert result[0] == 2
551+
assert isinstance(result[1], Timedelta)
552+
assert result[1] == Timedelta(hours=6)
553+
554+
result = divmod(td, 54)
555+
assert result[0] == Timedelta(hours=1)
556+
assert isinstance(result[1], Timedelta)
557+
assert result[1] == Timedelta(0)
558+
559+
result = divmod(td, pd.NaT)
560+
assert np.isnan(result[0])
561+
assert result[1] is pd.NaT
562+
563+
@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
564+
def test_divmod_offset(self):
565+
# GH#19365
566+
td = Timedelta(days=2, hours=6)
567+
568+
result = divmod(td, pd.offsets.Hour(-4))
569+
assert result[0] == -14
570+
assert isinstance(result[1], Timedelta)
571+
assert result[1] == Timedelta(hours=-2)
572+
573+
def test_divmod_invalid(self):
574+
# GH#19365
575+
td = Timedelta(days=2, hours=6)
576+
577+
with pytest.raises(TypeError):
578+
divmod(td, pd.Timestamp('2018-01-22'))
579+
580+
def test_rdivmod_pytimedelta(self):
581+
# GH#19365
582+
result = divmod(timedelta(days=2, hours=6), Timedelta(days=1))
583+
assert result[0] == 2
584+
assert isinstance(result[1], Timedelta)
585+
assert result[1] == Timedelta(hours=6)
586+
587+
@pytest.mark.xfail(reason='GH#19378 floordiv by Tick not implemented')
588+
def test_rdivmod_offset(self):
589+
result = divmod(pd.offsets.Hour(54), Timedelta(hours=-4))
590+
assert result[0] == -14
591+
assert isinstance(result[1], Timedelta)
592+
assert result[1] == Timedelta(hours=-2)
593+
594+
def test_rdivmod_invalid(self):
595+
# GH#19365
596+
td = Timedelta(minutes=3)
597+
598+
with pytest.raises(TypeError):
599+
divmod(pd.Timestamp('2018-01-22'), td)
600+
601+
with pytest.raises(TypeError):
602+
divmod(15, td)
603+
604+
with pytest.raises(TypeError):
605+
divmod(16.0, td)
606+
607+
with pytest.raises(TypeError):
608+
divmod(np.array([22, 24]), td)

0 commit comments

Comments
 (0)