diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst index b29d35c8ce332..be565616036e6 100644 --- a/doc/source/whatsnew/v2.2.0.rst +++ b/doc/source/whatsnew/v2.2.0.rst @@ -301,8 +301,9 @@ Datetimelike ^^^^^^^^^^^^ - Bug in :meth:`DatetimeIndex.union` returning object dtype for tz-aware indexes with the same timezone but different units (:issue:`55238`) - Bug in :meth:`Tick.delta` with very large ticks raising ``OverflowError`` instead of ``OutOfBoundsTimedelta`` (:issue:`55503`) +- Bug in adding or subtracting a :class:`Week` offset to a ``datetime64`` :class:`Series`, :class:`Index`, or :class:`DataFrame` column with non-nanosecond resolution returning incorrect results (:issue:`55583`) - Bug in addition or subtraction of very large :class:`Tick` objects with :class:`Timestamp` or :class:`Timedelta` objects raising ``OverflowError`` instead of ``OutOfBoundsTimedelta`` (:issue:`55503`) - +- Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/offsets.pyi b/pandas/_libs/tslibs/offsets.pyi index 1f0fad6abb8b4..aecc8cf681cf8 100644 --- a/pandas/_libs/tslibs/offsets.pyi +++ b/pandas/_libs/tslibs/offsets.pyi @@ -277,7 +277,10 @@ def roll_qtrday( INVALID_FREQ_ERR_MSG: Literal["Invalid frequency: {0}"] def shift_months( - dtindex: npt.NDArray[np.int64], months: int, day_opt: str | None = ... + dtindex: npt.NDArray[np.int64], + months: int, + day_opt: str | None = ..., + reso: int = ..., ) -> npt.NDArray[np.int64]: ... _offset_map: dict[str, BaseOffset] diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index f20073766bc3a..cb2f99eef4226 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -3272,7 +3272,8 @@ cdef class Week(SingleConstructorOffset): def _apply_array(self, dtarr): if self.weekday is None: td = timedelta(days=7 * self.n) - td64 = np.timedelta64(td, "ns") + unit = np.datetime_data(dtarr.dtype)[0] + td64 = np.timedelta64(td, unit) return dtarr + td64 else: reso = get_unit_from_dtype(dtarr.dtype) diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 67bcafd583086..89df837315396 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -1415,8 +1415,9 @@ def test_dt64arr_add_sub_relativedelta_offsets(self, box_with_array): ) @pytest.mark.parametrize("normalize", [True, False]) @pytest.mark.parametrize("n", [0, 5]) + @pytest.mark.parametrize("unit", ["s", "ms", "us", "ns"]) def test_dt64arr_add_sub_DateOffsets( - self, box_with_array, n, normalize, cls_and_kwargs + self, box_with_array, n, normalize, cls_and_kwargs, unit ): # GH#10699 # assert vectorized operation matches pointwise operations @@ -1449,7 +1450,7 @@ def test_dt64arr_add_sub_DateOffsets( Timestamp("2000-05-15"), Timestamp("2001-06-15"), ] - ) + ).as_unit(unit) vec = tm.box_expected(vec, box_with_array) vec_items = vec.iloc[0] if box_with_array is pd.DataFrame else vec @@ -1461,15 +1462,16 @@ def test_dt64arr_add_sub_DateOffsets( offset = offset_cls(n, normalize=normalize, **kwargs) - expected = DatetimeIndex([x + offset for x in vec_items]) + # TODO(GH#55564): as_unit will be unnecessary + expected = DatetimeIndex([x + offset for x in vec_items]).as_unit(unit) expected = tm.box_expected(expected, box_with_array) tm.assert_equal(expected, vec + offset) - expected = DatetimeIndex([x - offset for x in vec_items]) + expected = DatetimeIndex([x - offset for x in vec_items]).as_unit(unit) expected = tm.box_expected(expected, box_with_array) tm.assert_equal(expected, vec - offset) - expected = DatetimeIndex([offset + x for x in vec_items]) + expected = DatetimeIndex([offset + x for x in vec_items]).as_unit(unit) expected = tm.box_expected(expected, box_with_array) tm.assert_equal(expected, offset + vec) msg = "(bad|unsupported) operand type for unary" @@ -2392,7 +2394,8 @@ def test_dti_addsub_object_arraylike( @pytest.mark.parametrize("years", [-1, 0, 1]) @pytest.mark.parametrize("months", [-2, 0, 2]) -def test_shift_months(years, months): +@pytest.mark.parametrize("unit", ["s", "ms", "us", "ns"]) +def test_shift_months(years, months, unit): dti = DatetimeIndex( [ Timestamp("2000-01-05 00:15:00"), @@ -2401,11 +2404,13 @@ def test_shift_months(years, months): Timestamp("2000-02-29"), Timestamp("2000-12-31"), ] - ) - actual = DatetimeIndex(shift_months(dti.asi8, years * 12 + months)) + ).as_unit(unit) + shifted = shift_months(dti.asi8, years * 12 + months, reso=dti._data._creso) + shifted_dt64 = shifted.view(f"M8[{dti.unit}]") + actual = DatetimeIndex(shifted_dt64) raw = [x + pd.offsets.DateOffset(years=years, months=months) for x in dti] - expected = DatetimeIndex(raw) + expected = DatetimeIndex(raw).as_unit(dti.unit) tm.assert_index_equal(actual, expected)