diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index 3950ff3c8863d..6bb27d03835d7 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -1614,6 +1614,7 @@ Timedelta - Bug in :class:`Timedelta` and :func:`to_timedelta()` have inconsistencies in supported unit string (:issue:`21762`) - Bug in :class:`TimedeltaIndex` division where dividing by another :class:`TimedeltaIndex` raised ``TypeError`` instead of returning a :class:`Float64Index` (:issue:`23829`, :issue:`22631`) - 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`) +- Bug in :class:`Timedelta` comparisons when comparing with a ``Tick`` object incorrectly raising ``TypeError`` (:issue:`24710`) Timezones ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index c2f51436612a4..7097a702227d7 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -5,6 +5,7 @@ import cython import time from cpython.datetime cimport (PyDateTime_IMPORT, PyDateTime_Check, + PyDelta_Check, datetime, timedelta, time as dt_time) PyDateTime_IMPORT @@ -28,6 +29,9 @@ from pandas._libs.tslibs.np_datetime cimport ( npy_datetimestruct, dtstruct_to_dt64, dt64_to_dtstruct) from pandas._libs.tslibs.timezones import UTC + +PY2 = bytes == str + # --------------------------------------------------------------------- # Constants @@ -126,6 +130,26 @@ def apply_index_wraps(func): return wrapper +cdef _wrap_timedelta_result(result): + """ + Tick operations dispatch to their Timedelta counterparts. Wrap the result + of these operations in a Tick if possible. + + Parameters + ---------- + result : object + + Returns + ------- + object + """ + if PyDelta_Check(result): + # convert Timedelta back to a Tick + from pandas.tseries.offsets import _delta_to_tick + return _delta_to_tick(result) + + return result + # --------------------------------------------------------------------- # Business Helpers @@ -508,7 +532,13 @@ class _Tick(object): dummy class to mix into tseries.offsets.Tick so that in tslibs.period we can do isinstance checks on _Tick and avoid importing tseries.offsets """ - pass + + def __truediv__(self, other): + result = self.delta.__truediv__(other) + return _wrap_timedelta_result(result) + + if PY2: + __div__ = __truediv__ # ---------------------------------------------------------------------- diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 037e7de27adc3..0476ba1c78efc 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -36,6 +36,7 @@ from pandas._libs.tslibs.nattype import nat_strings from pandas._libs.tslibs.nattype cimport ( checknull_with_nat, NPY_NAT, c_NaT as NaT) from pandas._libs.tslibs.offsets cimport to_offset +from pandas._libs.tslibs.offsets import _Tick as Tick # ---------------------------------------------------------------------- # Constants @@ -757,7 +758,7 @@ cdef class _Timedelta(timedelta): if isinstance(other, _Timedelta): ots = other - elif PyDelta_Check(other): + elif PyDelta_Check(other) or isinstance(other, Tick): ots = Timedelta(other) else: ndim = getattr(other, "ndim", -1) diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 7afb90978131d..6694946902836 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -149,10 +149,6 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box): tm.assert_equal(commute, expected) def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box): - - if box is not pd.Index and isinstance(three_days, pd.offsets.Tick): - raise pytest.xfail("Tick division not implemented") - index = numeric_idx[1:3] expected = TimedeltaIndex(['3 Days', '36 Hours']) diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index db0c848eaeb4b..bc753c45c803a 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -78,6 +78,27 @@ def test_unary_ops(self): class TestTimedeltaComparison(object): + def test_compare_tick(self, tick_classes): + cls = tick_classes + + off = cls(4) + td = off.delta + assert isinstance(td, Timedelta) + + assert td == off + assert not td != off + assert td <= off + assert td >= off + assert not td < off + assert not td > off + + assert not td == 2 * off + assert td != 2 * off + assert td <= 2 * off + assert td < 2 * off + assert not td >= 2 * off + assert not td > 2 * off + def test_comparison_object_array(self): # analogous to GH#15183 td = Timedelta('2 days') diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 27ec7d9d9093a..f4b012ec1897f 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -2,6 +2,8 @@ """ Tests for offsets.Tick and subclasses """ +from __future__ import division + from datetime import datetime, timedelta from hypothesis import assume, example, given, settings, strategies as st @@ -36,6 +38,10 @@ def test_delta_to_tick(): tick = offsets._delta_to_tick(delta) assert (tick == offsets.Day(3)) + td = Timedelta(nanoseconds=5) + tick = offsets._delta_to_tick(td) + assert tick == Nano(5) + @pytest.mark.parametrize('cls', tick_classes) @settings(deadline=None) # GH 24641 @@ -228,6 +234,34 @@ def test_tick_addition(kls, expected): assert result == expected +@pytest.mark.parametrize('cls', tick_classes) +def test_tick_division(cls): + off = cls(10) + + assert off / cls(5) == 2 + assert off / 2 == cls(5) + assert off / 2.0 == cls(5) + + assert off / off.delta == 1 + assert off / off.delta.to_timedelta64() == 1 + + assert off / Nano(1) == off.delta / Nano(1).delta + + if cls is not Nano: + # A case where we end up with a smaller class + result = off / 1000 + assert isinstance(result, offsets.Tick) + assert not isinstance(result, cls) + assert result.delta == off.delta / 1000 + + if cls._inc < Timedelta(seconds=1): + # Case where we end up with a bigger class + result = off / .001 + assert isinstance(result, offsets.Tick) + assert not isinstance(result, cls) + assert result.delta == off.delta / .001 + + @pytest.mark.parametrize('cls1', tick_classes) @pytest.mark.parametrize('cls2', tick_classes) def test_tick_zero(cls1, cls2): diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 73f85d954432e..f208ce37a3b14 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2343,7 +2343,8 @@ def isAnchored(self): def _delta_to_tick(delta): - if delta.microseconds == 0: + if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0: + # nanoseconds only for pd.Timedelta if delta.seconds == 0: return Day(delta.days) else: