From 9cd67fe798088223b517e459feaaea3df095caff Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 16 Jan 2021 19:34:56 -0800 Subject: [PATCH 1/2] BUG: Timestamp comparison with barely-out-of-bounds datetime64 --- pandas/_libs/tslibs/timestamps.pyx | 29 +++++++++++++++++-- .../scalar/timestamp/test_comparisons.py | 22 +++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index df4677a242758..34fce0b56aeb4 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -25,7 +25,16 @@ from cpython.datetime cimport ( # alias bc `tzinfo` is a kwarg below time, tzinfo as tzinfo_type, ) -from cpython.object cimport Py_EQ, Py_NE, PyObject_RichCompare, PyObject_RichCompareBool +from cpython.object cimport ( + Py_EQ, + Py_GE, + Py_GT, + Py_LE, + Py_LT, + Py_NE, + PyObject_RichCompare, + PyObject_RichCompareBool, +) PyDateTime_IMPORT @@ -256,6 +265,9 @@ cdef class _Timestamp(ABCTimestamp): try: ots = type(self)(other) except ValueError: + if is_datetime64_object(other): + # cast non-nano dt64 to pydatetime + other = other.astype(object) return self._compare_outside_nanorange(other, op) elif is_array(other): @@ -310,12 +322,23 @@ cdef class _Timestamp(ABCTimestamp): cdef bint _compare_outside_nanorange(_Timestamp self, datetime other, int op) except -1: cdef: - datetime dtval = self.to_pydatetime() + datetime dtval = self.to_pydatetime(warn=False) if not self._can_compare(other): return NotImplemented - return PyObject_RichCompareBool(dtval, other, op) + if self.nanosecond == 0: + return PyObject_RichCompareBool(dtval, other, op) + + # otherwise we have dtval < self + if op == Py_NE: + return True + if op == Py_EQ: + return False + if op == Py_LE or op == Py_LT: + return other.year <= self.year + if op == Py_GE or op == Py_GT: + return other.year >= self.year cdef bint _can_compare(self, datetime other): if self.tzinfo is not None: diff --git a/pandas/tests/scalar/timestamp/test_comparisons.py b/pandas/tests/scalar/timestamp/test_comparisons.py index 285733dc2c7af..92d6b19878548 100644 --- a/pandas/tests/scalar/timestamp/test_comparisons.py +++ b/pandas/tests/scalar/timestamp/test_comparisons.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta import operator import numpy as np @@ -244,6 +244,26 @@ def test_timestamp_compare_with_early_datetime(self): assert stamp < datetime(2700, 1, 1) assert stamp <= datetime(2700, 1, 1) + other = Timestamp.min.to_pydatetime(warn=False) + assert other - timedelta(microseconds=1) < Timestamp.min + + def test_timestamp_compare_oob_dt64(self): + us = np.timedelta64(1, "us") + other = np.datetime64(Timestamp.min).astype("M8[us]") + + # This may change if the implementation bound is dropped to match + # DatetimeArray/DatetimeIndex GH#24124 + assert Timestamp.min == other + assert Timestamp.min > other - us + # Note: numpy gets the reversed comparison wrong + + other = np.datetime64(Timestamp.max).astype("M8[us]") + assert Timestamp.max > other # not actually OOB + assert other < Timestamp.max + + assert Timestamp.max < other + us + # Note: numpy gets the reversed comparison wrong + def test_compare_zerodim_array(self): # GH#26916 ts = Timestamp.now() From 1362a885f34b66f76e88d8b0f8665ccfaa4a6023 Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 16 Jan 2021 19:37:46 -0800 Subject: [PATCH 2/2] whatsnew --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index ab00b749d5725..be4f3fba07e24 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -230,6 +230,7 @@ Datetimelike - 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`) - Bug in :meth:`Series.where` incorrectly casting ``datetime64`` values to ``int64`` (:issue:`37682`) - Bug in :class:`Categorical` incorrectly typecasting ``datetime`` object to ``Timestamp`` (:issue:`38878`) +- Bug in comparisons between :class:`Timestamp` object and ``datetime64`` objects just outside the implementation bounds for nanosecond ``datetime64`` (:issue:`39221`) Timedelta ^^^^^^^^^