Skip to content

Commit a5e55fb

Browse files
BUG: DateOffset addition with non-nano (#55595)
* BUG: DateOffset addition with non-nano * Update doc/source/whatsnew/v2.2.0.rst Co-authored-by: Matthew Roeschke <[email protected]> --------- Co-authored-by: Matthew Roeschke <[email protected]>
1 parent 503e8e8 commit a5e55fb

File tree

5 files changed

+70
-43
lines changed

5 files changed

+70
-43
lines changed

doc/source/whatsnew/v2.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ Datetimelike
303303
- Bug in :meth:`DatetimeIndex.union` returning object dtype for tz-aware indexes with the same timezone but different units (:issue:`55238`)
304304
- Bug in :meth:`Tick.delta` with very large ticks raising ``OverflowError`` instead of ``OutOfBoundsTimedelta`` (:issue:`55503`)
305305
- 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`)
306+
- Bug in addition or subtraction of :class:`DateOffset` objects with microsecond components to ``datetime64`` :class:`Index`, :class:`Series`, or :class:`DataFrame` columns with non-nanosecond resolution (:issue:`55595`)
306307
- 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`)
307308
-
308309

pandas/_libs/tslibs/offsets.pyx

+43-27
Original file line numberDiff line numberDiff line change
@@ -1368,10 +1368,10 @@ cdef class RelativeDeltaOffset(BaseOffset):
13681368
else:
13691369
return other + timedelta(self.n)
13701370

1371-
@apply_array_wraps
1372-
def _apply_array(self, dtarr):
1373-
reso = get_unit_from_dtype(dtarr.dtype)
1374-
dt64other = np.asarray(dtarr)
1371+
@cache_readonly
1372+
def _pd_timedelta(self) -> Timedelta:
1373+
# components of _offset that can be cast to pd.Timedelta
1374+
13751375
kwds = self.kwds
13761376
relativedelta_fast = {
13771377
"years",
@@ -1385,28 +1385,26 @@ cdef class RelativeDeltaOffset(BaseOffset):
13851385
}
13861386
# relativedelta/_offset path only valid for base DateOffset
13871387
if self._use_relativedelta and set(kwds).issubset(relativedelta_fast):
1388-
1389-
months = (kwds.get("years", 0) * 12 + kwds.get("months", 0)) * self.n
1390-
if months:
1391-
shifted = shift_months(dt64other.view("i8"), months, reso=reso)
1392-
dt64other = shifted.view(dtarr.dtype)
1393-
1394-
weeks = kwds.get("weeks", 0) * self.n
1395-
if weeks:
1396-
delta = Timedelta(days=7 * weeks)
1397-
td = (<_Timedelta>delta)._as_creso(reso)
1398-
dt64other = dt64other + td
1399-
1400-
timedelta_kwds = {
1401-
k: v
1402-
for k, v in kwds.items()
1403-
if k in ["days", "hours", "minutes", "seconds", "microseconds"]
1388+
td_kwds = {
1389+
key: val
1390+
for key, val in kwds.items()
1391+
if key in ["days", "hours", "minutes", "seconds", "microseconds"]
14041392
}
1405-
if timedelta_kwds:
1406-
delta = Timedelta(**timedelta_kwds)
1407-
td = (<_Timedelta>delta)._as_creso(reso)
1408-
dt64other = dt64other + (self.n * td)
1409-
return dt64other
1393+
if "weeks" in kwds:
1394+
days = td_kwds.get("days", 0)
1395+
td_kwds["days"] = days + 7 * kwds["weeks"]
1396+
1397+
if td_kwds:
1398+
delta = Timedelta(**td_kwds)
1399+
if "microseconds" in kwds:
1400+
delta = delta.as_unit("us")
1401+
else:
1402+
delta = delta.as_unit("s")
1403+
else:
1404+
delta = Timedelta(0).as_unit("s")
1405+
1406+
return delta * self.n
1407+
14101408
elif not self._use_relativedelta and hasattr(self, "_offset"):
14111409
# timedelta
14121410
num_nano = getattr(self, "nanoseconds", 0)
@@ -1415,8 +1413,12 @@ cdef class RelativeDeltaOffset(BaseOffset):
14151413
delta = Timedelta((self._offset + rem_nano) * self.n)
14161414
else:
14171415
delta = Timedelta(self._offset * self.n)
1418-
td = (<_Timedelta>delta)._as_creso(reso)
1419-
return dt64other + td
1416+
if "microseconds" in kwds:
1417+
delta = delta.as_unit("us")
1418+
else:
1419+
delta = delta.as_unit("s")
1420+
return delta
1421+
14201422
else:
14211423
# relativedelta with other keywords
14221424
kwd = set(kwds) - relativedelta_fast
@@ -1426,6 +1428,20 @@ cdef class RelativeDeltaOffset(BaseOffset):
14261428
"applied vectorized"
14271429
)
14281430

1431+
@apply_array_wraps
1432+
def _apply_array(self, dtarr):
1433+
reso = get_unit_from_dtype(dtarr.dtype)
1434+
dt64other = np.asarray(dtarr)
1435+
1436+
delta = self._pd_timedelta # may raise NotImplementedError
1437+
1438+
kwds = self.kwds
1439+
months = (kwds.get("years", 0) * 12 + kwds.get("months", 0)) * self.n
1440+
if months:
1441+
shifted = shift_months(dt64other.view("i8"), months, reso=reso)
1442+
dt64other = shifted.view(dtarr.dtype)
1443+
return dt64other + delta
1444+
14291445
def is_on_offset(self, dt: datetime) -> bool:
14301446
if self.normalize and not _is_normalized(dt):
14311447
return False

pandas/core/arrays/datetimes.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -799,14 +799,17 @@ def _add_offset(self, offset) -> Self:
799799
values = self
800800

801801
try:
802-
result = offset._apply_array(values).view(values.dtype)
802+
result = offset._apply_array(values)
803+
if result.dtype.kind == "i":
804+
result = result.view(values.dtype)
803805
except NotImplementedError:
804806
warnings.warn(
805807
"Non-vectorized DateOffset being applied to Series or DatetimeIndex.",
806808
PerformanceWarning,
807809
stacklevel=find_stack_level(),
808810
)
809811
result = self.astype("O") + offset
812+
# TODO(GH#55564): as_unit will be unnecessary
810813
result = type(self)._from_sequence(result).as_unit(self.unit)
811814
if not len(self):
812815
# GH#30336 _from_sequence won't be able to infer self.tz

pandas/tests/arithmetic/test_datetime64.py

+20-11
Original file line numberDiff line numberDiff line change
@@ -1223,13 +1223,16 @@ class TestDatetime64DateOffsetArithmetic:
12231223
# Tick DateOffsets
12241224

12251225
# TODO: parametrize over timezone?
1226-
def test_dt64arr_series_add_tick_DateOffset(self, box_with_array):
1226+
@pytest.mark.parametrize("unit", ["s", "ms", "us", "ns"])
1227+
def test_dt64arr_series_add_tick_DateOffset(self, box_with_array, unit):
12271228
# GH#4532
12281229
# operate with pd.offsets
1229-
ser = Series([Timestamp("20130101 9:01"), Timestamp("20130101 9:02")])
1230+
ser = Series(
1231+
[Timestamp("20130101 9:01"), Timestamp("20130101 9:02")]
1232+
).dt.as_unit(unit)
12301233
expected = Series(
12311234
[Timestamp("20130101 9:01:05"), Timestamp("20130101 9:02:05")]
1232-
)
1235+
).dt.as_unit(unit)
12331236

12341237
ser = tm.box_expected(ser, box_with_array)
12351238
expected = tm.box_expected(expected, box_with_array)
@@ -1310,7 +1313,8 @@ def test_dti_add_tick_tzaware(self, tz_aware_fixture, box_with_array):
13101313
# -------------------------------------------------------------
13111314
# RelativeDelta DateOffsets
13121315

1313-
def test_dt64arr_add_sub_relativedelta_offsets(self, box_with_array):
1316+
@pytest.mark.parametrize("unit", ["s", "ms", "us", "ns"])
1317+
def test_dt64arr_add_sub_relativedelta_offsets(self, box_with_array, unit):
13141318
# GH#10699
13151319
vec = DatetimeIndex(
13161320
[
@@ -1323,7 +1327,7 @@ def test_dt64arr_add_sub_relativedelta_offsets(self, box_with_array):
13231327
Timestamp("2000-05-15"),
13241328
Timestamp("2001-06-15"),
13251329
]
1326-
)
1330+
).as_unit(unit)
13271331
vec = tm.box_expected(vec, box_with_array)
13281332
vec_items = vec.iloc[0] if box_with_array is pd.DataFrame else vec
13291333

@@ -1337,24 +1341,29 @@ def test_dt64arr_add_sub_relativedelta_offsets(self, box_with_array):
13371341
("seconds", 2),
13381342
("microseconds", 5),
13391343
]
1340-
for i, (unit, value) in enumerate(relative_kwargs):
1341-
off = DateOffset(**{unit: value})
1344+
for i, (offset_unit, value) in enumerate(relative_kwargs):
1345+
off = DateOffset(**{offset_unit: value})
1346+
1347+
exp_unit = unit
1348+
if offset_unit == "microseconds" and unit != "ns":
1349+
exp_unit = "us"
13421350

1343-
expected = DatetimeIndex([x + off for x in vec_items])
1351+
# TODO(GH#55564): as_unit will be unnecessary
1352+
expected = DatetimeIndex([x + off for x in vec_items]).as_unit(exp_unit)
13441353
expected = tm.box_expected(expected, box_with_array)
13451354
tm.assert_equal(expected, vec + off)
13461355

1347-
expected = DatetimeIndex([x - off for x in vec_items])
1356+
expected = DatetimeIndex([x - off for x in vec_items]).as_unit(exp_unit)
13481357
expected = tm.box_expected(expected, box_with_array)
13491358
tm.assert_equal(expected, vec - off)
13501359

13511360
off = DateOffset(**dict(relative_kwargs[: i + 1]))
13521361

1353-
expected = DatetimeIndex([x + off for x in vec_items])
1362+
expected = DatetimeIndex([x + off for x in vec_items]).as_unit(exp_unit)
13541363
expected = tm.box_expected(expected, box_with_array)
13551364
tm.assert_equal(expected, vec + off)
13561365

1357-
expected = DatetimeIndex([x - off for x in vec_items])
1366+
expected = DatetimeIndex([x - off for x in vec_items]).as_unit(exp_unit)
13581367
expected = tm.box_expected(expected, box_with_array)
13591368
tm.assert_equal(expected, vec - off)
13601369
msg = "(bad|unsupported) operand type for unary"

pandas/tests/tseries/offsets/test_offsets.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,8 @@ def test_add_dt64_ndarray_non_nano(self, offset_types, unit, request):
568568
# check that the result with non-nano matches nano
569569
off = _create_offset(offset_types)
570570

571-
dti = date_range("2016-01-01", periods=35, freq="D")
572-
573-
arr = dti._data._ndarray.astype(f"M8[{unit}]")
574-
dta = type(dti._data)._simple_new(arr, dtype=arr.dtype)
571+
dti = date_range("2016-01-01", periods=35, freq="D", unit=unit)
572+
dta = dti._data
575573

576574
expected = dti._data + off
577575
result = dta + off

0 commit comments

Comments
 (0)