From f7b59e413acee1059452e79f42ccb60668deda4e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 10 Jan 2019 09:51:56 -0800 Subject: [PATCH 1/5] implement Tick division, fix Timedelta.__cmp__ tick --- pandas/_libs/tslibs/offsets.pyx | 32 ++++++++++++++++- pandas/_libs/tslibs/timedeltas.pyx | 3 +- pandas/tests/arithmetic/test_numeric.py | 4 --- .../tests/scalar/timedelta/test_timedelta.py | 23 +++++++++++++ pandas/tests/tseries/offsets/test_ticks.py | 34 +++++++++++++++++++ pandas/tseries/offsets.py | 3 +- 6 files changed, 92 insertions(+), 7 deletions(-) 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..8a83f50cd087a 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 @@ -764,7 +765,7 @@ cdef class _Timedelta(timedelta): if ndim != -1: if ndim == 0: - if is_timedelta64_object(other): + if is_timedelta64_object(other) or isinstance(other, Tick): other = Timedelta(other) else: if op == Py_EQ: 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..559e47fc23ffb 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -13,6 +13,8 @@ from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type as ct import pandas.util.testing as tm +from pandas.tseries.offsets import Day, Hour, Micro, Mili, Minute, Nano, Second + class TestTimedeltaArithmetic(object): @@ -78,6 +80,27 @@ def test_unary_ops(self): class TestTimedeltaComparison(object): + @pytest.mark.parametrize('tick_cls', [Nano, Micro, Mili, Second, + Minute, Hour, Day]) + def test_compare_tick(self, tick_cls): + off = tick_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: From 420dfc8f8af6df469adbf72956b9cc0b6100924a Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 10 Jan 2019 11:02:30 -0800 Subject: [PATCH 2/5] typo fixup --- pandas/tests/scalar/timedelta/test_timedelta.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 559e47fc23ffb..4c86269696d7f 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -13,7 +13,8 @@ from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type as ct import pandas.util.testing as tm -from pandas.tseries.offsets import Day, Hour, Micro, Mili, Minute, Nano, Second +from pandas.tseries.offsets import ( + Day, Hour, Micro, Milli, Minute, Nano, Second) class TestTimedeltaArithmetic(object): @@ -80,7 +81,7 @@ def test_unary_ops(self): class TestTimedeltaComparison(object): - @pytest.mark.parametrize('tick_cls', [Nano, Micro, Mili, Second, + @pytest.mark.parametrize('tick_cls', [Nano, Micro, Milli, Second, Minute, Hour, Day]) def test_compare_tick(self, tick_cls): off = tick_cls(4) From 15eb30e89ee25a60b26a529ab63b1867c1e141d1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 10 Jan 2019 12:07:26 -0800 Subject: [PATCH 3/5] Fix comparison --- pandas/_libs/tslibs/timedeltas.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 8a83f50cd087a..0476ba1c78efc 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -758,14 +758,14 @@ 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) if ndim != -1: if ndim == 0: - if is_timedelta64_object(other) or isinstance(other, Tick): + if is_timedelta64_object(other): other = Timedelta(other) else: if op == Py_EQ: From b4976d8108946273367acde365a743eb01d45981 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 10 Jan 2019 12:09:00 -0800 Subject: [PATCH 4/5] use fixture --- pandas/tests/scalar/timedelta/test_timedelta.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 4c86269696d7f..bc753c45c803a 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -13,9 +13,6 @@ from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type as ct import pandas.util.testing as tm -from pandas.tseries.offsets import ( - Day, Hour, Micro, Milli, Minute, Nano, Second) - class TestTimedeltaArithmetic(object): @@ -81,10 +78,10 @@ def test_unary_ops(self): class TestTimedeltaComparison(object): - @pytest.mark.parametrize('tick_cls', [Nano, Micro, Milli, Second, - Minute, Hour, Day]) - def test_compare_tick(self, tick_cls): - off = tick_cls(4) + def test_compare_tick(self, tick_classes): + cls = tick_classes + + off = cls(4) td = off.delta assert isinstance(td, Timedelta) From 0a9bb746eb723d5897010024e9bf06a4b6513f11 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 11 Jan 2019 09:48:35 -0800 Subject: [PATCH 5/5] whatsnew --- doc/source/whatsnew/v0.24.0.rst | 1 + 1 file changed, 1 insertion(+) 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 ^^^^^^^^^