Skip to content

Commit 097c71f

Browse files
jbrockmendeljreback
authored andcommitted
implement Tick division, fix Timedelta.__cmp__ tick (#24710)
1 parent 33f91d8 commit 097c71f

File tree

7 files changed

+91
-7
lines changed

7 files changed

+91
-7
lines changed

doc/source/whatsnew/v0.24.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,7 @@ Timedelta
16141614
- Bug in :class:`Timedelta` and :func:`to_timedelta()` have inconsistencies in supported unit string (:issue:`21762`)
16151615
- Bug in :class:`TimedeltaIndex` division where dividing by another :class:`TimedeltaIndex` raised ``TypeError`` instead of returning a :class:`Float64Index` (:issue:`23829`, :issue:`22631`)
16161616
- Bug in :class:`TimedeltaIndex` comparison operations where comparing against non-``Timedelta``-like objects would raise ``TypeError`` instead of returning all-``False`` for ``__eq__`` and all-``True`` for ``__ne__`` (:issue:`24056`)
1617+
- Bug in :class:`Timedelta` comparisons when comparing with a ``Tick`` object incorrectly raising ``TypeError`` (:issue:`24710`)
16171618

16181619
Timezones
16191620
^^^^^^^^^

pandas/_libs/tslibs/offsets.pyx

+31-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import cython
55
import time
66
from cpython.datetime cimport (PyDateTime_IMPORT,
77
PyDateTime_Check,
8+
PyDelta_Check,
89
datetime, timedelta,
910
time as dt_time)
1011
PyDateTime_IMPORT
@@ -28,6 +29,9 @@ from pandas._libs.tslibs.np_datetime cimport (
2829
npy_datetimestruct, dtstruct_to_dt64, dt64_to_dtstruct)
2930
from pandas._libs.tslibs.timezones import UTC
3031

32+
33+
PY2 = bytes == str
34+
3135
# ---------------------------------------------------------------------
3236
# Constants
3337

@@ -126,6 +130,26 @@ def apply_index_wraps(func):
126130
return wrapper
127131

128132

133+
cdef _wrap_timedelta_result(result):
134+
"""
135+
Tick operations dispatch to their Timedelta counterparts. Wrap the result
136+
of these operations in a Tick if possible.
137+
138+
Parameters
139+
----------
140+
result : object
141+
142+
Returns
143+
-------
144+
object
145+
"""
146+
if PyDelta_Check(result):
147+
# convert Timedelta back to a Tick
148+
from pandas.tseries.offsets import _delta_to_tick
149+
return _delta_to_tick(result)
150+
151+
return result
152+
129153
# ---------------------------------------------------------------------
130154
# Business Helpers
131155

@@ -508,7 +532,13 @@ class _Tick(object):
508532
dummy class to mix into tseries.offsets.Tick so that in tslibs.period we
509533
can do isinstance checks on _Tick and avoid importing tseries.offsets
510534
"""
511-
pass
535+
536+
def __truediv__(self, other):
537+
result = self.delta.__truediv__(other)
538+
return _wrap_timedelta_result(result)
539+
540+
if PY2:
541+
__div__ = __truediv__
512542

513543

514544
# ----------------------------------------------------------------------

pandas/_libs/tslibs/timedeltas.pyx

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ from pandas._libs.tslibs.nattype import nat_strings
3636
from pandas._libs.tslibs.nattype cimport (
3737
checknull_with_nat, NPY_NAT, c_NaT as NaT)
3838
from pandas._libs.tslibs.offsets cimport to_offset
39+
from pandas._libs.tslibs.offsets import _Tick as Tick
3940

4041
# ----------------------------------------------------------------------
4142
# Constants
@@ -757,7 +758,7 @@ cdef class _Timedelta(timedelta):
757758

758759
if isinstance(other, _Timedelta):
759760
ots = other
760-
elif PyDelta_Check(other):
761+
elif PyDelta_Check(other) or isinstance(other, Tick):
761762
ots = Timedelta(other)
762763
else:
763764
ndim = getattr(other, "ndim", -1)

pandas/tests/arithmetic/test_numeric.py

-4
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,6 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box):
149149
tm.assert_equal(commute, expected)
150150

151151
def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box):
152-
153-
if box is not pd.Index and isinstance(three_days, pd.offsets.Tick):
154-
raise pytest.xfail("Tick division not implemented")
155-
156152
index = numeric_idx[1:3]
157153

158154
expected = TimedeltaIndex(['3 Days', '36 Hours'])

pandas/tests/scalar/timedelta/test_timedelta.py

+21
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,27 @@ def test_unary_ops(self):
7878

7979

8080
class TestTimedeltaComparison(object):
81+
def test_compare_tick(self, tick_classes):
82+
cls = tick_classes
83+
84+
off = cls(4)
85+
td = off.delta
86+
assert isinstance(td, Timedelta)
87+
88+
assert td == off
89+
assert not td != off
90+
assert td <= off
91+
assert td >= off
92+
assert not td < off
93+
assert not td > off
94+
95+
assert not td == 2 * off
96+
assert td != 2 * off
97+
assert td <= 2 * off
98+
assert td < 2 * off
99+
assert not td >= 2 * off
100+
assert not td > 2 * off
101+
81102
def test_comparison_object_array(self):
82103
# analogous to GH#15183
83104
td = Timedelta('2 days')

pandas/tests/tseries/offsets/test_ticks.py

+34
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"""
33
Tests for offsets.Tick and subclasses
44
"""
5+
from __future__ import division
6+
57
from datetime import datetime, timedelta
68

79
from hypothesis import assume, example, given, settings, strategies as st
@@ -36,6 +38,10 @@ def test_delta_to_tick():
3638
tick = offsets._delta_to_tick(delta)
3739
assert (tick == offsets.Day(3))
3840

41+
td = Timedelta(nanoseconds=5)
42+
tick = offsets._delta_to_tick(td)
43+
assert tick == Nano(5)
44+
3945

4046
@pytest.mark.parametrize('cls', tick_classes)
4147
@settings(deadline=None) # GH 24641
@@ -228,6 +234,34 @@ def test_tick_addition(kls, expected):
228234
assert result == expected
229235

230236

237+
@pytest.mark.parametrize('cls', tick_classes)
238+
def test_tick_division(cls):
239+
off = cls(10)
240+
241+
assert off / cls(5) == 2
242+
assert off / 2 == cls(5)
243+
assert off / 2.0 == cls(5)
244+
245+
assert off / off.delta == 1
246+
assert off / off.delta.to_timedelta64() == 1
247+
248+
assert off / Nano(1) == off.delta / Nano(1).delta
249+
250+
if cls is not Nano:
251+
# A case where we end up with a smaller class
252+
result = off / 1000
253+
assert isinstance(result, offsets.Tick)
254+
assert not isinstance(result, cls)
255+
assert result.delta == off.delta / 1000
256+
257+
if cls._inc < Timedelta(seconds=1):
258+
# Case where we end up with a bigger class
259+
result = off / .001
260+
assert isinstance(result, offsets.Tick)
261+
assert not isinstance(result, cls)
262+
assert result.delta == off.delta / .001
263+
264+
231265
@pytest.mark.parametrize('cls1', tick_classes)
232266
@pytest.mark.parametrize('cls2', tick_classes)
233267
def test_tick_zero(cls1, cls2):

pandas/tseries/offsets.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2343,7 +2343,8 @@ def isAnchored(self):
23432343

23442344

23452345
def _delta_to_tick(delta):
2346-
if delta.microseconds == 0:
2346+
if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0:
2347+
# nanoseconds only for pd.Timedelta
23472348
if delta.seconds == 0:
23482349
return Day(delta.days)
23492350
else:

0 commit comments

Comments
 (0)