diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index a593a03de5c25..a1ac19b815e22 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -538,7 +538,7 @@ Datetimelike - Bug in inplace addition and subtraction of :class:`DatetimeIndex` or :class:`TimedeltaIndex` with :class:`DatetimeArray` or :class:`TimedeltaArray` (:issue:`43904`) - Bug in in calling ``np.isnan``, ``np.isfinite``, or ``np.isinf`` on a timezone-aware :class:`DatetimeIndex` incorrectly raising ``TypeError`` (:issue:`43917`) - Bug in constructing a :class:`Series` from datetime-like strings with mixed timezones incorrectly partially-inferring datetime values (:issue:`40111`) -- +- Bug in addition with a :class:`Tick` object and a ``np.timedelta64`` object incorrectly raising instead of returning :class:`Timedelta` (:issue:`44474`) Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 00d02e096c976..f689b8ce242e5 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -72,7 +72,10 @@ from pandas._libs.tslibs.np_datetime cimport ( from pandas._libs.tslibs.tzconversion cimport tz_convert_from_utc_single from .dtypes cimport PeriodDtypeCode -from .timedeltas cimport delta_to_nanoseconds +from .timedeltas cimport ( + delta_to_nanoseconds, + is_any_td_scalar, +) from .timedeltas import Timedelta @@ -154,7 +157,11 @@ def apply_wraps(func): if other is NaT: return NaT - elif isinstance(other, BaseOffset) or PyDelta_Check(other): + elif ( + isinstance(other, BaseOffset) + or PyDelta_Check(other) + or util.is_timedelta64_object(other) + ): # timedelta path return func(self, other) elif is_datetime64_object(other) or PyDate_Check(other): @@ -902,7 +909,7 @@ cdef class Tick(SingleConstructorOffset): # PyDate_Check includes date, datetime return Timestamp(other) + self - if PyDelta_Check(other): + if util.is_timedelta64_object(other) or PyDelta_Check(other): return other + self.delta elif isinstance(other, type(self)): # TODO: this is reached in tests that specifically call apply, @@ -1396,9 +1403,10 @@ cdef class BusinessDay(BusinessMixin): result = result + self.offset return result - elif PyDelta_Check(other) or isinstance(other, Tick): + elif is_any_td_scalar(other): + td = Timedelta(self.offset) + other return BusinessDay( - self.n, offset=self.offset + other, normalize=self.normalize + self.n, offset=td.to_pytimedelta(), normalize=self.normalize ) else: raise ApplyTypeError( @@ -3265,8 +3273,9 @@ cdef class CustomBusinessDay(BusinessDay): result = result + self.offset return result - elif PyDelta_Check(other) or isinstance(other, Tick): - return BDay(self.n, offset=self.offset + other, normalize=self.normalize) + elif is_any_td_scalar(other): + td = Timedelta(self.offset) + other + return BDay(self.n, offset=td.to_pytimedelta(), normalize=self.normalize) else: raise ApplyTypeError( "Only know how to combine trading day with " diff --git a/pandas/tests/tseries/offsets/test_business_day.py b/pandas/tests/tseries/offsets/test_business_day.py index 92daafaf469cd..ffc2a04334ffc 100644 --- a/pandas/tests/tseries/offsets/test_business_day.py +++ b/pandas/tests/tseries/offsets/test_business_day.py @@ -7,6 +7,7 @@ timedelta, ) +import numpy as np import pytest from pandas._libs.tslibs.offsets import ( @@ -17,6 +18,7 @@ from pandas import ( DatetimeIndex, + Timedelta, _testing as tm, ) from pandas.tests.tseries.offsets.common import ( @@ -57,11 +59,30 @@ def test_with_offset(self): assert (self.d + offset) == datetime(2008, 1, 2, 2) - def test_with_offset_index(self): - dti = DatetimeIndex([self.d]) - result = dti + (self.offset + timedelta(hours=2)) + @pytest.mark.parametrize("reverse", [True, False]) + @pytest.mark.parametrize( + "td", + [ + Timedelta(hours=2), + Timedelta(hours=2).to_pytimedelta(), + Timedelta(hours=2).to_timedelta64(), + ], + ids=lambda x: type(x), + ) + def test_with_offset_index(self, reverse, td, request): + if reverse and isinstance(td, np.timedelta64): + mark = pytest.mark.xfail( + reason="need __array_priority__, but that causes other errors" + ) + request.node.add_marker(mark) + dti = DatetimeIndex([self.d]) expected = DatetimeIndex([datetime(2008, 1, 2, 2)]) + + if reverse: + result = dti + (td + self.offset) + else: + result = dti + (self.offset + td) tm.assert_index_equal(result, expected) def test_eq(self): diff --git a/pandas/tests/tseries/offsets/test_custom_business_day.py b/pandas/tests/tseries/offsets/test_custom_business_day.py index b8014f7112435..5847bd11f09df 100644 --- a/pandas/tests/tseries/offsets/test_custom_business_day.py +++ b/pandas/tests/tseries/offsets/test_custom_business_day.py @@ -19,6 +19,7 @@ from pandas import ( DatetimeIndex, + Timedelta, _testing as tm, read_pickle, ) @@ -62,11 +63,30 @@ def test_with_offset(self): assert (self.d + offset) == datetime(2008, 1, 2, 2) - def test_with_offset_index(self): - dti = DatetimeIndex([self.d]) - result = dti + (self.offset + timedelta(hours=2)) + @pytest.mark.parametrize("reverse", [True, False]) + @pytest.mark.parametrize( + "td", + [ + Timedelta(hours=2), + Timedelta(hours=2).to_pytimedelta(), + Timedelta(hours=2).to_timedelta64(), + ], + ids=lambda x: type(x), + ) + def test_with_offset_index(self, reverse, td, request): + if reverse and isinstance(td, np.timedelta64): + mark = pytest.mark.xfail( + reason="need __array_priority__, but that causes other errors" + ) + request.node.add_marker(mark) + dti = DatetimeIndex([self.d]) expected = DatetimeIndex([datetime(2008, 1, 2, 2)]) + + if reverse: + result = dti + (td + self.offset) + else: + result = dti + (self.offset + td) tm.assert_index_equal(result, expected) def test_eq(self): diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 52a2f3aeee850..ae6bd2d85579a 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -230,9 +230,16 @@ def test_Nanosecond(): ) def test_tick_addition(kls, expected): offset = kls(3) - result = offset + Timedelta(hours=2) - assert isinstance(result, Timedelta) - assert result == expected + td = Timedelta(hours=2) + + for other in [td, td.to_pytimedelta(), td.to_timedelta64()]: + result = offset + other + assert isinstance(result, Timedelta) + assert result == expected + + result = other + offset + assert isinstance(result, Timedelta) + assert result == expected @pytest.mark.parametrize("cls", tick_classes)