diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index c479c59082464..8faa11c047af8 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -992,6 +992,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.resolution` incorrectly returning "day" instead of "nanosecond" for nanosecond-resolution indexes (:issue:`46903`) - Bug in :class:`Timestamp` with an integer or float value and ``unit="Y"`` or ``unit="M"`` giving slightly-wrong results (:issue:`47266`) - Bug in :class:`.DatetimeArray` construction when passed another :class:`.DatetimeArray` and ``freq=None`` incorrectly inferring the freq from the given array (:issue:`47296`) +- Bug in :class:`RelativeDeltaOffset` which caused inconsistent behavior upon multiplying a :class:`DateOffset` (:issue:`47953`) - Bug in :func:`to_datetime` where ``OutOfBoundsDatetime`` would be thrown even if ``errors=coerce`` if there were more than 50 rows (:issue:`45319`) - Bug when adding a :class:`DateOffset` to a :class:`Series` would not add the ``nanoseconds`` field (:issue:`47856`) - diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index d799770a57be2..ec07c51663916 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -1283,12 +1283,7 @@ cdef class RelativeDeltaOffset(BaseOffset): else: td_nano = Timedelta(0) - if self.n > 0: - for i in range(self.n): - other = other + self._offset + td_nano - else: - for i in range(-self.n): - other = other - self._offset - td_nano + other = other + ((self._offset + td_nano) * self.n) if tzinfo is not None and self._use_relativedelta: # bring tz back from UTC calculation diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index bca4ba98f37b7..659161fe34da0 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -32,6 +32,7 @@ from pandas.errors import PerformanceWarning from pandas import ( + DataFrame, DatetimeIndex, Series, date_range, @@ -1035,6 +1036,38 @@ def test_construct_int_arg_no_kwargs_assumed_days(n): assert result == expected +def test_offset_multiplication(): + # GH#47953 + mo1 = DateOffset(months=1) + mo2 = DateOffset(months=2) + + startscalar = Timestamp("2020-01-30") + startarray = Series([Timestamp("2020-01-30")]) + + resultscalar1 = startscalar + (mo1 * 2) + resultscalar2 = startscalar + mo2 + resultarray1 = startarray + (mo1 * 2) + resultarray2 = startarray + mo2 + + expectedscalar = Timestamp("2020-03-30") + expectedarray = Series([Timestamp("2020-03-30")]) + assert resultscalar1 == expectedscalar + assert resultscalar2 == expectedscalar + tm.assert_series_equal(resultarray1, expectedarray) + tm.assert_series_equal(resultarray2, expectedarray) + + df = DataFrame({"T": [Timestamp("2019-04-30")], "D": [DateOffset(months=1)]}) + frameresult1 = df["T"] + 26 * df["D"] + df2 = DataFrame( + { + "T": [Timestamp("2019-04-30"), Timestamp("2019-04-30")], + "D": [DateOffset(months=1), DateOffset(months=1)], + } + ) + frameresult2 = df2["T"] + 26 * df2["D"] + assert frameresult1[0] == frameresult2[0] + + @pytest.mark.parametrize( "offset, expected", [