Skip to content

Commit ac2fd4e

Browse files
authored
BUG: Timestamp comparison with barely-out-of-bounds datetime64 (#39221)
1 parent b31c069 commit ac2fd4e

File tree

3 files changed

+47
-4
lines changed

3 files changed

+47
-4
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ Datetimelike
234234
- Bug in :meth:`DatetimeIndex.intersection`, :meth:`DatetimeIndex.symmetric_difference`, :meth:`PeriodIndex.intersection`, :meth:`PeriodIndex.symmetric_difference` always returning object-dtype when operating with :class:`CategoricalIndex` (:issue:`38741`)
235235
- Bug in :meth:`Series.where` incorrectly casting ``datetime64`` values to ``int64`` (:issue:`37682`)
236236
- Bug in :class:`Categorical` incorrectly typecasting ``datetime`` object to ``Timestamp`` (:issue:`38878`)
237+
- Bug in comparisons between :class:`Timestamp` object and ``datetime64`` objects just outside the implementation bounds for nanosecond ``datetime64`` (:issue:`39221`)
237238
- Bug in :meth:`Timestamp.round`, :meth:`Timestamp.floor`, :meth:`Timestamp.ceil` for values near the implementation bounds of :class:`Timestamp` (:issue:`39244`)
238239
- Bug in :func:`date_range` incorrectly creating :class:`DatetimeIndex` containing ``NaT`` instead of raising ``OutOfBoundsDatetime`` in corner cases (:issue:`24124`)
239240

pandas/_libs/tslibs/timestamps.pyx

+26-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ from cpython.datetime cimport ( # alias bc `tzinfo` is a kwarg below
2727
time,
2828
tzinfo as tzinfo_type,
2929
)
30-
from cpython.object cimport Py_EQ, Py_NE, PyObject_RichCompare, PyObject_RichCompareBool
30+
from cpython.object cimport (
31+
Py_EQ,
32+
Py_GE,
33+
Py_GT,
34+
Py_LE,
35+
Py_LT,
36+
Py_NE,
37+
PyObject_RichCompare,
38+
PyObject_RichCompareBool,
39+
)
3140

3241
PyDateTime_IMPORT
3342

@@ -295,6 +304,9 @@ cdef class _Timestamp(ABCTimestamp):
295304
try:
296305
ots = type(self)(other)
297306
except ValueError:
307+
if is_datetime64_object(other):
308+
# cast non-nano dt64 to pydatetime
309+
other = other.astype(object)
298310
return self._compare_outside_nanorange(other, op)
299311

300312
elif is_array(other):
@@ -349,12 +361,23 @@ cdef class _Timestamp(ABCTimestamp):
349361
cdef bint _compare_outside_nanorange(_Timestamp self, datetime other,
350362
int op) except -1:
351363
cdef:
352-
datetime dtval = self.to_pydatetime()
364+
datetime dtval = self.to_pydatetime(warn=False)
353365

354366
if not self._can_compare(other):
355367
return NotImplemented
356368

357-
return PyObject_RichCompareBool(dtval, other, op)
369+
if self.nanosecond == 0:
370+
return PyObject_RichCompareBool(dtval, other, op)
371+
372+
# otherwise we have dtval < self
373+
if op == Py_NE:
374+
return True
375+
if op == Py_EQ:
376+
return False
377+
if op == Py_LE or op == Py_LT:
378+
return other.year <= self.year
379+
if op == Py_GE or op == Py_GT:
380+
return other.year >= self.year
358381

359382
cdef bint _can_compare(self, datetime other):
360383
if self.tzinfo is not None:

pandas/tests/scalar/timestamp/test_comparisons.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import datetime, timedelta
22
import operator
33

44
import numpy as np
@@ -244,6 +244,25 @@ def test_timestamp_compare_with_early_datetime(self):
244244
assert stamp < datetime(2700, 1, 1)
245245
assert stamp <= datetime(2700, 1, 1)
246246

247+
other = Timestamp.min.to_pydatetime(warn=False)
248+
assert other - timedelta(microseconds=1) < Timestamp.min
249+
250+
def test_timestamp_compare_oob_dt64(self):
251+
us = np.timedelta64(1, "us")
252+
other = np.datetime64(Timestamp.min).astype("M8[us]")
253+
254+
# This may change if the implementation bound is dropped to match
255+
# DatetimeArray/DatetimeIndex GH#24124
256+
assert Timestamp.min > other
257+
# Note: numpy gets the reversed comparison wrong
258+
259+
other = np.datetime64(Timestamp.max).astype("M8[us]")
260+
assert Timestamp.max > other # not actually OOB
261+
assert other < Timestamp.max
262+
263+
assert Timestamp.max < other + us
264+
# Note: numpy gets the reversed comparison wrong
265+
247266
def test_compare_zerodim_array(self):
248267
# GH#26916
249268
ts = Timestamp.now()

0 commit comments

Comments
 (0)