Skip to content

Commit cd5a124

Browse files
authored
FIX BUG: Timestamp __add__/__sub__ DateOffset with nanoseconds lost. (#43968)
1 parent 6d1372e commit cd5a124

File tree

4 files changed

+68
-14
lines changed

4 files changed

+68
-14
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ Datetimelike
627627
- Bug in adding a ``np.timedelta64`` object to a :class:`BusinessDay` or :class:`CustomBusinessDay` object incorrectly raising (:issue:`44532`)
628628
- Bug in :meth:`Index.insert` for inserting ``np.datetime64``, ``np.timedelta64`` or ``tuple`` into :class:`Index` with ``dtype='object'`` with negative loc adding ``None`` and replacing existing value (:issue:`44509`)
629629
- Bug in :meth:`Series.mode` with ``DatetimeTZDtype`` incorrectly returning timezone-naive and ``PeriodDtype`` incorrectly raising (:issue:`41927`)
630+
- Bug in :class:`DateOffset`` addition with :class:`Timestamp` where ``offset.nanoseconds`` would not be included in the result. (:issue:`43968`)
630631
-
631632

632633
Timedelta

pandas/_libs/tslibs/offsets.pyx

+11-5
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ def apply_wraps(func):
186186
if self.normalize:
187187
result = result.normalize()
188188

189-
# nanosecond may be deleted depending on offset process
190-
if not self.normalize and nano != 0:
189+
# If the offset object does not have a nanoseconds component,
190+
# the result's nanosecond component may be lost.
191+
if not self.normalize and nano != 0 and not hasattr(self, "nanoseconds"):
191192
if result.nanosecond != nano:
192193
if result.tz is not None:
193194
# convert to UTC
@@ -333,7 +334,7 @@ cdef _determine_offset(kwds):
333334
# sub-daily offset - use timedelta (tz-aware)
334335
offset = timedelta(**kwds_no_nanos)
335336
else:
336-
offset = timedelta(1)
337+
offset = timedelta(0)
337338
return offset, use_relativedelta
338339

339340

@@ -1068,12 +1069,17 @@ cdef class RelativeDeltaOffset(BaseOffset):
10681069
# perform calculation in UTC
10691070
other = other.replace(tzinfo=None)
10701071

1072+
if hasattr(self, "nanoseconds"):
1073+
td_nano = Timedelta(nanoseconds=self.nanoseconds)
1074+
else:
1075+
td_nano = Timedelta(0)
1076+
10711077
if self.n > 0:
10721078
for i in range(self.n):
1073-
other = other + self._offset
1079+
other = other + self._offset + td_nano
10741080
else:
10751081
for i in range(-self.n):
1076-
other = other - self._offset
1082+
other = other - self._offset - td_nano
10771083

10781084
if tzinfo is not None and self._use_relativedelta:
10791085
# bring tz back from UTC calculation

pandas/_libs/tslibs/timestamps.pyx

-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,6 @@ cdef class _Timestamp(ABCTimestamp):
307307
elif not isinstance(self, _Timestamp):
308308
# cython semantics, args have been switched and this is __radd__
309309
return other.__add__(self)
310-
311310
return NotImplemented
312311

313312
def __sub__(self, other):

pandas/tests/tseries/offsets/test_offsets.py

+56-8
Original file line numberDiff line numberDiff line change
@@ -668,14 +668,6 @@ def test_rule_code(self):
668668
assert alias == (_get_offset(alias) * 5).rule_code
669669

670670

671-
def test_dateoffset_misc():
672-
oset = offsets.DateOffset(months=2, days=4)
673-
# it works
674-
oset.freqstr
675-
676-
assert not offsets.DateOffset(months=2) == 2
677-
678-
679671
def test_freq_offsets():
680672
off = BDay(1, offset=timedelta(0, 1800))
681673
assert off.freqstr == "B+30Min"
@@ -791,6 +783,54 @@ def test_tick_normalize_raises(tick_classes):
791783
cls(n=3, normalize=True)
792784

793785

786+
@pytest.mark.parametrize(
787+
"offset_kwargs, expected_arg",
788+
[
789+
({"nanoseconds": 1}, "1970-01-01 00:00:00.000000001"),
790+
({"nanoseconds": 5}, "1970-01-01 00:00:00.000000005"),
791+
({"nanoseconds": -1}, "1969-12-31 23:59:59.999999999"),
792+
({"microseconds": 1}, "1970-01-01 00:00:00.000001"),
793+
({"microseconds": -1}, "1969-12-31 23:59:59.999999"),
794+
({"seconds": 1}, "1970-01-01 00:00:01"),
795+
({"seconds": -1}, "1969-12-31 23:59:59"),
796+
({"minutes": 1}, "1970-01-01 00:01:00"),
797+
({"minutes": -1}, "1969-12-31 23:59:00"),
798+
({"hours": 1}, "1970-01-01 01:00:00"),
799+
({"hours": -1}, "1969-12-31 23:00:00"),
800+
({"days": 1}, "1970-01-02 00:00:00"),
801+
({"days": -1}, "1969-12-31 00:00:00"),
802+
({"weeks": 1}, "1970-01-08 00:00:00"),
803+
({"weeks": -1}, "1969-12-25 00:00:00"),
804+
({"months": 1}, "1970-02-01 00:00:00"),
805+
({"months": -1}, "1969-12-01 00:00:00"),
806+
({"years": 1}, "1971-01-01 00:00:00"),
807+
({"years": -1}, "1969-01-01 00:00:00"),
808+
],
809+
)
810+
def test_dateoffset_add_sub(offset_kwargs, expected_arg):
811+
offset = DateOffset(**offset_kwargs)
812+
ts = Timestamp(0)
813+
result = ts + offset
814+
expected = Timestamp(expected_arg)
815+
assert result == expected
816+
result -= offset
817+
assert result == ts
818+
result = offset + ts
819+
assert result == expected
820+
821+
822+
def test_dataoffset_add_sub_timestamp_with_nano():
823+
offset = DateOffset(minutes=2, nanoseconds=9)
824+
ts = Timestamp(4)
825+
result = ts + offset
826+
expected = Timestamp("1970-01-01 00:02:00.000000013")
827+
assert result == expected
828+
result -= offset
829+
assert result == ts
830+
result = offset + ts
831+
assert result == expected
832+
833+
794834
@pytest.mark.parametrize(
795835
"attribute",
796836
[
@@ -806,3 +846,11 @@ def test_dateoffset_immutable(attribute):
806846
msg = "DateOffset objects are immutable"
807847
with pytest.raises(AttributeError, match=msg):
808848
setattr(offset, attribute, 5)
849+
850+
851+
def test_dateoffset_misc():
852+
oset = offsets.DateOffset(months=2, days=4)
853+
# it works
854+
oset.freqstr
855+
856+
assert not offsets.DateOffset(months=2) == 2

0 commit comments

Comments
 (0)