diff --git a/pandas/_libs/tslibs/dtypes.pxd b/pandas/_libs/tslibs/dtypes.pxd index 9c0eb0f9b0945..e16a389bc5459 100644 --- a/pandas/_libs/tslibs/dtypes.pxd +++ b/pandas/_libs/tslibs/dtypes.pxd @@ -7,7 +7,7 @@ cdef str npy_unit_to_abbrev(NPY_DATETIMEUNIT unit) cdef NPY_DATETIMEUNIT freq_group_code_to_npy_unit(int freq) nogil cpdef int64_t periods_per_day(NPY_DATETIMEUNIT reso=*) except? -1 cdef int64_t periods_per_second(NPY_DATETIMEUNIT reso) except? -1 -cdef int64_t get_conversion_factor(NPY_DATETIMEUNIT from_unit, NPY_DATETIMEUNIT to_unit) +cdef int64_t get_conversion_factor(NPY_DATETIMEUNIT from_unit, NPY_DATETIMEUNIT to_unit) except? -1 cdef dict attrname_to_abbrevs diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index 8758d70b1a266..cb2de79cd8b26 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -384,7 +384,7 @@ cdef int64_t periods_per_second(NPY_DATETIMEUNIT reso) except? -1: @cython.overflowcheck(True) -cdef int64_t get_conversion_factor(NPY_DATETIMEUNIT from_unit, NPY_DATETIMEUNIT to_unit): +cdef int64_t get_conversion_factor(NPY_DATETIMEUNIT from_unit, NPY_DATETIMEUNIT to_unit) except? -1: """ Find the factor by which we need to multiply to convert from from_unit to to_unit. """ diff --git a/pandas/_libs/tslibs/timedeltas.pxd b/pandas/_libs/tslibs/timedeltas.pxd index e851665c49d89..3251e10a88b73 100644 --- a/pandas/_libs/tslibs/timedeltas.pxd +++ b/pandas/_libs/tslibs/timedeltas.pxd @@ -6,10 +6,11 @@ from .np_datetime cimport NPY_DATETIMEUNIT # Exposed for tslib, not intended for outside use. cpdef int64_t delta_to_nanoseconds( - delta, NPY_DATETIMEUNIT reso=*, bint round_ok=*, bint allow_year_month=* + delta, NPY_DATETIMEUNIT reso=*, bint round_ok=* ) except? -1 cdef convert_to_timedelta64(object ts, str unit) cdef bint is_any_td_scalar(object obj) +cdef object ensure_td64ns(object ts) cdef class _Timedelta(timedelta): diff --git a/pandas/_libs/tslibs/timedeltas.pyi b/pandas/_libs/tslibs/timedeltas.pyi index 70770e1c0fdc9..cc649e5a62660 100644 --- a/pandas/_libs/tslibs/timedeltas.pyi +++ b/pandas/_libs/tslibs/timedeltas.pyi @@ -76,7 +76,6 @@ def delta_to_nanoseconds( delta: np.timedelta64 | timedelta | Tick, reso: int = ..., # NPY_DATETIMEUNIT round_ok: bool = ..., - allow_year_month: bool = ..., ) -> int: ... class Timedelta(timedelta): diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 0fc1476b43c42..28a6480f368d9 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -206,44 +206,24 @@ cpdef int64_t delta_to_nanoseconds( delta, NPY_DATETIMEUNIT reso=NPY_FR_ns, bint round_ok=True, - bint allow_year_month=False, ) except? -1: + # Note: this will raise on timedelta64 with Y or M unit + cdef: - _Timedelta td NPY_DATETIMEUNIT in_reso - int64_t n + int64_t n, value, factor if is_tick_object(delta): n = delta.n in_reso = delta._reso - if in_reso == reso: - return n - else: - td = Timedelta._from_value_and_reso(delta.n, reso=in_reso) elif isinstance(delta, _Timedelta): - td = delta n = delta.value in_reso = delta._reso - if in_reso == reso: - return n elif is_timedelta64_object(delta): in_reso = get_datetime64_unit(delta) n = get_timedelta64_value(delta) - if in_reso == reso: - return n - else: - # _from_value_and_reso does not support Year, Month, or unit-less, - # so we have special handling if speciifed - try: - td = Timedelta._from_value_and_reso(n, reso=in_reso) - except NotImplementedError: - if allow_year_month: - td64 = ensure_td64ns(delta) - return delta_to_nanoseconds(td64, reso=reso) - else: - raise elif PyDelta_Check(delta): in_reso = NPY_DATETIMEUNIT.NPY_FR_us @@ -256,21 +236,31 @@ cpdef int64_t delta_to_nanoseconds( except OverflowError as err: raise OutOfBoundsTimedelta(*err.args) from err - if in_reso == reso: - return n - else: - td = Timedelta._from_value_and_reso(n, reso=in_reso) - else: raise TypeError(type(delta)) - try: - return td._as_reso(reso, round_ok=round_ok).value - except OverflowError as err: - unit_str = npy_unit_to_abbrev(reso) - raise OutOfBoundsTimedelta( - f"Cannot cast {str(delta)} to unit={unit_str} without overflow." - ) from err + if reso < in_reso: + # e.g. ns -> us + factor = get_conversion_factor(reso, in_reso) + div, mod = divmod(n, factor) + if mod > 0 and not round_ok: + raise ValueError("Cannot losslessly convert units") + + # Note that when mod > 0, we follow np.timedelta64 in always + # rounding down. + value = div + else: + factor = get_conversion_factor(in_reso, reso) + try: + with cython.overflowcheck(True): + value = n * factor + except OverflowError as err: + unit_str = npy_unit_to_abbrev(reso) + raise OutOfBoundsTimedelta( + f"Cannot cast {str(delta)} to unit={unit_str} without overflow." + ) from err + + return value @cython.overflowcheck(True) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 8706d59b084b9..67aae23f7fdd1 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -99,6 +99,7 @@ from pandas._libs.tslibs.offsets cimport ( ) from pandas._libs.tslibs.timedeltas cimport ( delta_to_nanoseconds, + ensure_td64ns, is_any_td_scalar, ) @@ -353,16 +354,25 @@ cdef class _Timestamp(ABCTimestamp): raise NotImplementedError(self._reso) if is_any_td_scalar(other): - if ( - is_timedelta64_object(other) - and get_datetime64_unit(other) == NPY_DATETIMEUNIT.NPY_FR_GENERIC - ): - # TODO: deprecate allowing this? We only get here - # with test_timedelta_add_timestamp_interval - other = np.timedelta64(other.view("i8"), "ns") - # TODO: disallow round_ok, allow_year_month? + if is_timedelta64_object(other): + other_reso = get_datetime64_unit(other) + if ( + other_reso == NPY_DATETIMEUNIT.NPY_FR_GENERIC + ): + # TODO: deprecate allowing this? We only get here + # with test_timedelta_add_timestamp_interval + other = np.timedelta64(other.view("i8"), "ns") + elif ( + other_reso == NPY_DATETIMEUNIT.NPY_FR_Y or other_reso == NPY_DATETIMEUNIT.NPY_FR_M + ): + # TODO: deprecate allowing these? or handle more like the + # corresponding DateOffsets? + # TODO: no tests get here + other = ensure_td64ns(other) + + # TODO: disallow round_ok nanos = delta_to_nanoseconds( - other, reso=self._reso, round_ok=True, allow_year_month=True + other, reso=self._reso, round_ok=True ) try: result = type(self)(self.value + nanos, tz=self.tzinfo) diff --git a/pandas/tests/scalar/timestamp/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py index 788d6f3504760..65610bbe14e41 100644 --- a/pandas/tests/scalar/timestamp/test_arithmetic.py +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -45,12 +45,6 @@ def test_overflow_offset_raises(self): r"\<-?\d+ \* Days\> and \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} " "will overflow" ) - lmsg = "|".join( - [ - "Python int too large to convert to C (long|int)", - "int too big to convert", - ] - ) lmsg2 = r"Cannot cast <-?20169940 \* Days> to unit=ns without overflow" with pytest.raises(OutOfBoundsTimedelta, match=lmsg2): @@ -68,13 +62,14 @@ def test_overflow_offset_raises(self): stamp = Timestamp("2000/1/1") offset_overflow = to_offset("D") * 100**5 - with pytest.raises(OverflowError, match=lmsg): + lmsg3 = r"Cannot cast <-?10000000000 \* Days> to unit=ns without overflow" + with pytest.raises(OutOfBoundsTimedelta, match=lmsg3): stamp + offset_overflow with pytest.raises(OverflowError, match=msg): offset_overflow + stamp - with pytest.raises(OverflowError, match=lmsg): + with pytest.raises(OutOfBoundsTimedelta, match=lmsg3): stamp - offset_overflow def test_overflow_timestamp_raises(self): diff --git a/pandas/tests/tslibs/test_timedeltas.py b/pandas/tests/tslibs/test_timedeltas.py index d9e86d53f2587..661bb113e9549 100644 --- a/pandas/tests/tslibs/test_timedeltas.py +++ b/pandas/tests/tslibs/test_timedeltas.py @@ -55,6 +55,18 @@ def test_delta_to_nanoseconds_error(): delta_to_nanoseconds(np.int32(3)) +def test_delta_to_nanoseconds_td64_MY_raises(): + td = np.timedelta64(1234, "Y") + + with pytest.raises(ValueError, match="0, 10"): + delta_to_nanoseconds(td) + + td = np.timedelta64(1234, "M") + + with pytest.raises(ValueError, match="1, 10"): + delta_to_nanoseconds(td) + + def test_huge_nanoseconds_overflow(): # GH 32402 assert delta_to_nanoseconds(Timedelta(1e10)) == 1e10