From 73b9f3581b6682844b79cfccd37d180836fee1e8 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 18:29:28 -0800 Subject: [PATCH 1/5] implement missing tests, fix offset floordiv, rfloordiv, fix return types --- pandas/_libs/tslibs/timedeltas.pyx | 13 +- pandas/tests/scalar/test_timedelta.py | 165 ++++++++++++++++++++++---- 2 files changed, 157 insertions(+), 21 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 37693068e0974..884b70a8c4609 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -478,11 +478,16 @@ def _binary_op_method_timedeltalike(op, name): elif other is NaT: return NaT + elif is_timedelta64_object(other): + # convert to Timedelta below; avoid catching this in + # has-dtype check before then + pass + elif is_datetime64_object(other) or PyDateTime_CheckExact(other): # the PyDateTime_CheckExact case is for a datetime object that # is specifically *not* a Timestamp, as the Timestamp case will be # handled after `_validate_ops_compat` returns False below - from ..tslib import Timestamp + from timestamps import Timestamp return op(self, Timestamp(other)) # We are implicitly requiring the canonical behavior to be # defined by Timestamp methods. @@ -1096,6 +1101,9 @@ class Timedelta(_Timedelta): # just defer if hasattr(other, '_typ'): # Series, DataFrame, ... + if other._typ == 'dateoffset' and hasattr(other, 'delta'): + # Tick offset + return self // other.delta return NotImplemented if hasattr(other, 'dtype'): @@ -1128,6 +1136,9 @@ class Timedelta(_Timedelta): # just defer if hasattr(other, '_typ'): # Series, DataFrame, ... + if other._typ == 'dateoffset' and hasattr(other, 'delta'): + # Tick offset + return other.delta // self return NotImplemented if hasattr(other, 'dtype'): diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 667266be2a89b..e712971c52816 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -1,10 +1,13 @@ """ test the scalar Timedelta """ +from datetime import datetime, timedelta +import operator + import pytest import numpy as np -from datetime import timedelta import pandas as pd +from pandas.core import ops import pandas.util.testing as tm from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type as ct from pandas import (Timedelta, TimedeltaIndex, timedelta_range, Series, @@ -13,6 +16,137 @@ class TestTimedeltaArithmetic(object): + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_datetimelike(self, op): + # GH#19365 + td = Timedelta(10, unit='d') + + result = op(td, datetime(2016, 1, 1)) + if op is operator.add: + # datetime + Timedelta does _not_ call Timedelta.__radd__, + # so we get a datetime back instead of a Timestamp + assert isinstance(result, pd.Timestamp) + assert result == pd.Timestamp(2016, 1, 11) + + result = op(td, pd.Timestamp('2018-01-12 18:09')) + assert isinstance(result, pd.Timestamp) + assert result == pd.Timestamp('2018-01-22 18:09') + + result = op(td, np.datetime64('2018-01-12')) + assert isinstance(result, pd.Timestamp) + assert result == pd.Timestamp('2018-01-22') + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_timedeltalike(self, op): + td = Timedelta(10, unit='d') + + result = op(td, Timedelta(days=10)) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=20) + + result = op(td, timedelta(days=9)) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=19) + + result = op(td, np.timedelta64(-4, 'D')) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=6) + + result = op(td, pd.offsets.Hour(6)) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=10, hours=6) + + def test_td_sub_timedeltalike(self): + td = Timedelta(10, unit='d') + + expected = Timedelta(0, unit='ns') + result = td - td + assert isinstance(result, Timedelta) + assert result == expected + + result = td - td.to_pytimedelta() + assert isinstance(result, Timedelta) + assert result == expected + + result = td - td.to_timedelta64() + assert isinstance(result, Timedelta) + assert result == expected + + result = td - pd.offsets.Hour(1) + assert isinstance(result, Timedelta) + assert result == Timedelta(239, unit='h') + + def test_td_rsub_timedeltalike(self): + td = Timedelta(10, unit='d') + + expected = Timedelta(0, unit='ns') + + result = td.to_pytimedelta() - td + assert isinstance(result, Timedelta) + assert result == expected + + result = td.to_timedelta64() - td + assert isinstance(result, Timedelta) + assert result == expected + + result = pd.offsets.Hour(1) - td + assert isinstance(result, Timedelta) + assert result == Timedelta(-239, unit='h') + + @pytest.mark.parametrize('op', [operator.mul, ops.rmul]) + def test_td_mul_scalar(self, op): + # GH#19365 + td = Timedelta(minutes=3) + + result = op(td, 2) + assert result == Timedelta(minutes=6) + + result = op(td, 1.5) + assert result == Timedelta(minutes=4, seconds=30) + + assert op(td, np.nan) is NaT + + with pytest.raises(TypeError): + # timedelta * datetime is gibberish + op(td, pd.Timestamp(2016, 1, 2)) + + with pytest.raises(TypeError): + # invalid multiply with another timedelta + op(td, td) + + def test_td_div_timedeltalike_scalar(self): + # GH#19365 + td = Timedelta(10, unit='d') + + result = td / pd.offsets.Hour(1) + assert result == 240 + + assert td / td == 1 + assert td / np.timedelta64(60, 'h') == 4 + + assert np.isnan(td / np.timedelta64('NaT')) + assert np.isnan(td / NaT) + + def test_td_div_numeric_scalar(self): + # GH#19365 + td = Timedelta(10, unit='d') + + result = td / 2 + assert isinstance(result, Timedelta) + assert result == Timedelta(days=5) + + result = td / 5.0 + assert isinstance(result, Timedelta) + assert result == Timedelta(days=2) + + def test_td_rdiv_timedeltalike_scalar(self): + # GH#19365 + td = Timedelta(10, unit='d') + result = pd.offsets.Hour(1) / td + assert result == 1 / 240.0 + + assert np.isnan(np.timedelta64('NaT') / td) + assert np.timedelta64(60, 'h') / td == 0.25 def test_arithmetic_overflow(self): with pytest.raises(OverflowError): @@ -105,15 +239,6 @@ def test_timedelta_ops_scalar(self): result = base - offset assert result == expected_sub - def test_ops_offsets(self): - td = Timedelta(10, unit='d') - assert Timedelta(241, unit='h') == td + pd.offsets.Hour(1) - assert Timedelta(241, unit='h') == pd.offsets.Hour(1) + td - assert 240 == td / pd.offsets.Hour(1) - assert 1 / 240.0 == pd.offsets.Hour(1) / td - assert Timedelta(239, unit='h') == td - pd.offsets.Hour(1) - assert Timedelta(-239, unit='h') == pd.offsets.Hour(1) - td - def test_unary_ops(self): td = Timedelta(10, unit='d') @@ -152,15 +277,11 @@ def test_binary_ops_integers(self): pytest.raises(TypeError, lambda: td + 2) pytest.raises(TypeError, lambda: td - 2) - def test_binary_ops_with_timedelta(self): - td = Timedelta(10, unit='d') - - assert td - td == Timedelta(0, unit='ns') - assert td + td == Timedelta(20, unit='d') - assert td / td == 1 - - # invalid multiply with another timedelta - pytest.raises(TypeError, lambda: td * td) + def test_td_floordiv_offsets(self): + # GH19365 + td = Timedelta(hours=3, minutes=4) + assert td // pd.offsets.Hour(1) == 3 + assert td // pd.offsets.Minute(2) == 92 def test_floordiv(self): # GH#18846 @@ -202,6 +323,10 @@ def test_floordiv(self): res = td // ser assert res.dtype.kind == 'm' + def test_td_rfloordiv_offsets(self): + # GH#19365 + assert pd.offsets.Hour(1) // Timedelta(minutes=25) == 2 + def test_rfloordiv(self): # GH#18846 td = Timedelta(hours=3, minutes=3) @@ -370,7 +495,7 @@ def test_construction(self): days=10, hours=1, minutes=1, seconds=1) assert Timedelta('-10 days 1 h 1m 1s 3us') == -timedelta( days=10, hours=1, minutes=1, seconds=1, microseconds=3) - assert Timedelta('-10 days 1 h 1.5m 1s 3us'), -timedelta( + assert Timedelta('-10 days 1 h 1.5m 1s 3us') == -timedelta( days=10, hours=1, minutes=1, seconds=31, microseconds=3) # Currently invalid as it has a - on the hh:mm:dd part From 136d699544ec569e62a47ef905830156b1c94199 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 18:43:00 -0800 Subject: [PATCH 2/5] whatsnew --- doc/source/whatsnew/v0.23.0.txt | 2 ++ pandas/tests/scalar/test_timedelta.py | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index a2198d9103528..9a4c5b42c858f 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -716,6 +716,8 @@ Datetimelike - Bug in :class:`Timestamp` and :func:`to_datetime` where a string representing a barely out-of-bounds timestamp would be incorrectly rounded down instead of raising ``OutOfBoundsDatetime`` (:issue:`19382`) - Bug in :func:`Timestamp.floor` :func:`DatetimeIndex.floor` where time stamps far in the future and past were not rounded correctly (:issue:`19206`) - Bug in :func:`to_datetime` where passing an out-of-bounds datetime with ``errors='coerce'`` and ``utc=True`` would raise ``OutOfBoundsDatetime`` instead of parsing to ``NaT`` (:issue:`19612`) +- Bug in :func:`Timedelta.__add__`, :func:`Timedelta.__sub__` where adding or subtracting a ``np.timedelta64`` object would return another ``np.timedelta64`` instead of a ``Timedelta`` (:issue:`19738`) +- Bug in :func:`Timedelta.__floordiv__`, :func:`Timedelta.__rfloordiv__` where operating with a ``Tick`` object would raise a ``TypeError`` instead of returning a numeric value (:issue:`19738`) - Timezones diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index e712971c52816..111544d22e11e 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -18,7 +18,7 @@ class TestTimedeltaArithmetic(object): @pytest.mark.parametrize('op', [operator.add, ops.radd]) def test_td_add_datetimelike(self, op): - # GH#19365 + # GH#19738 td = Timedelta(10, unit='d') result = op(td, datetime(2016, 1, 1)) @@ -95,7 +95,7 @@ def test_td_rsub_timedeltalike(self): @pytest.mark.parametrize('op', [operator.mul, ops.rmul]) def test_td_mul_scalar(self, op): - # GH#19365 + # GH#19738 td = Timedelta(minutes=3) result = op(td, 2) @@ -115,7 +115,7 @@ def test_td_mul_scalar(self, op): op(td, td) def test_td_div_timedeltalike_scalar(self): - # GH#19365 + # GH#19738 td = Timedelta(10, unit='d') result = td / pd.offsets.Hour(1) @@ -128,7 +128,7 @@ def test_td_div_timedeltalike_scalar(self): assert np.isnan(td / NaT) def test_td_div_numeric_scalar(self): - # GH#19365 + # GH#19738 td = Timedelta(10, unit='d') result = td / 2 @@ -140,7 +140,7 @@ def test_td_div_numeric_scalar(self): assert result == Timedelta(days=2) def test_td_rdiv_timedeltalike_scalar(self): - # GH#19365 + # GH#19738 td = Timedelta(10, unit='d') result = pd.offsets.Hour(1) / td assert result == 1 / 240.0 @@ -278,7 +278,7 @@ def test_binary_ops_integers(self): pytest.raises(TypeError, lambda: td - 2) def test_td_floordiv_offsets(self): - # GH19365 + # GH#19738 td = Timedelta(hours=3, minutes=4) assert td // pd.offsets.Hour(1) == 3 assert td // pd.offsets.Minute(2) == 92 @@ -324,7 +324,7 @@ def test_floordiv(self): assert res.dtype.kind == 'm' def test_td_rfloordiv_offsets(self): - # GH#19365 + # GH#19738 assert pd.offsets.Hour(1) // Timedelta(minutes=25) == 2 def test_rfloordiv(self): From c7599a868ee3f73745c2fb92ad8f9451c02fc8e1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 18:47:26 -0800 Subject: [PATCH 3/5] delete redundant tests --- pandas/tests/scalar/test_timedelta.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 111544d22e11e..96f306b6c00d9 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -265,10 +265,6 @@ def test_binary_ops_nat(self): def test_binary_ops_integers(self): td = Timedelta(10, unit='d') - assert td * 2 == Timedelta(20, unit='d') - assert td / 2 == Timedelta(5, unit='d') - assert td // 2 == Timedelta(5, unit='d') - # invert assert td * -1 == Timedelta('-10d') assert -1 * td == Timedelta('-10d') From 150e5e372442aaa59030c684c88cfb4624d622b0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 21:17:03 -0800 Subject: [PATCH 4/5] Remove test that fails under older numpy --- pandas/tests/scalar/test_timedelta.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 96f306b6c00d9..bae642f46ac04 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -145,7 +145,6 @@ def test_td_rdiv_timedeltalike_scalar(self): result = pd.offsets.Hour(1) / td assert result == 1 / 240.0 - assert np.isnan(np.timedelta64('NaT') / td) assert np.timedelta64(60, 'h') / td == 0.25 def test_arithmetic_overflow(self): From edcd7e04a3002d0f3830d7a59817a6997ee7caf7 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 17 Feb 2018 09:17:07 -0800 Subject: [PATCH 5/5] remove test broken on appveyor --- pandas/tests/scalar/test_timedelta.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index bae642f46ac04..4db00015d97c3 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -124,7 +124,6 @@ def test_td_div_timedeltalike_scalar(self): assert td / td == 1 assert td / np.timedelta64(60, 'h') == 4 - assert np.isnan(td / np.timedelta64('NaT')) assert np.isnan(td / NaT) def test_td_div_numeric_scalar(self):