diff --git a/doc/source/user_guide/timedeltas.rst b/doc/source/user_guide/timedeltas.rst index 3a75aa0b39b1f..a6eb96f91a4bf 100644 --- a/doc/source/user_guide/timedeltas.rst +++ b/doc/source/user_guide/timedeltas.rst @@ -259,9 +259,6 @@ an alternative is to divide by another timedelta object. Note that division by t # to days td / np.timedelta64(1, "D") - # to months (these are constant months) - td / np.timedelta64(1, "M") - Dividing or multiplying a ``timedelta64[ns]`` Series by an integer or integer Series yields another ``timedelta64[ns]`` dtypes Series. diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 877ff32107971..447a9e05a1c3a 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -365,6 +365,7 @@ Timedelta ^^^^^^^^^ - :meth:`TimedeltaIndex.map` with ``na_action="ignore"`` now works as expected (:issue:`51644`) - Bug in :class:`TimedeltaIndex` division or multiplication leading to ``.freq`` of "0 Days" instead of ``None`` (:issue:`51575`) +- Bug in :class:`Timedelta` with Numpy timedelta64 objects not properly raising ``ValueError`` (:issue:`52806`) - Bug in :meth:`Timedelta.round` with values close to the implementation bounds returning incorrect results instead of raising ``OutOfBoundsTimedelta`` (:issue:`51494`) - Bug in :meth:`arrays.TimedeltaArray.map` and :meth:`TimedeltaIndex.map`, where the supplied callable operated array-wise instead of element-wise (:issue:`51977`) - diff --git a/pandas/_libs/tslibs/dtypes.pxd b/pandas/_libs/tslibs/dtypes.pxd index 1dca498f61b0e..a8dd88c763c14 100644 --- a/pandas/_libs/tslibs/dtypes.pxd +++ b/pandas/_libs/tslibs/dtypes.pxd @@ -9,6 +9,7 @@ cdef NPY_DATETIMEUNIT freq_group_code_to_npy_unit(int freq) noexcept nogil cpdef int64_t periods_per_day(NPY_DATETIMEUNIT reso=*) except? -1 cpdef int64_t periods_per_second(NPY_DATETIMEUNIT reso) except? -1 cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso) +cpdef bint is_supported_unit(NPY_DATETIMEUNIT reso) cdef dict attrname_to_abbrevs cdef dict npy_unit_to_attrname diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index 2a49ea6760e72..19f4c83e6cecf 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -321,7 +321,7 @@ cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso): return reso -def is_supported_unit(NPY_DATETIMEUNIT reso): +cpdef bint is_supported_unit(NPY_DATETIMEUNIT reso): return ( reso == NPY_DATETIMEUNIT.NPY_FR_ns or reso == NPY_DATETIMEUNIT.NPY_FR_us diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index e68b8b210437a..7319eccf3b51f 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -42,6 +42,7 @@ from pandas._libs.tslibs.conversion cimport ( ) from pandas._libs.tslibs.dtypes cimport ( get_supported_reso, + is_supported_unit, npy_unit_to_abbrev, ) from pandas._libs.tslibs.nattype cimport ( @@ -151,10 +152,10 @@ cdef dict timedelta_abbrevs = { _no_input = object() - # ---------------------------------------------------------------------- # API + @cython.boundscheck(False) @cython.wraparound(False) def ints_to_pytimedelta(ndarray m8values, box=False): @@ -1789,7 +1790,6 @@ class Timedelta(_Timedelta): + int(kwargs.get("milliseconds", 0) * 1_000_000) + seconds ) - if unit in {"Y", "y", "M"}: raise ValueError( "Units 'M', 'Y', and 'y' are no longer supported, as they do not " @@ -1836,6 +1836,15 @@ class Timedelta(_Timedelta): return NaT reso = get_datetime64_unit(value) + if not (is_supported_unit(reso) or + reso in [NPY_DATETIMEUNIT.NPY_FR_m, + NPY_DATETIMEUNIT.NPY_FR_h, + NPY_DATETIMEUNIT.NPY_FR_D, + NPY_DATETIMEUNIT.NPY_FR_W, + NPY_DATETIMEUNIT.NPY_FR_GENERIC]): + err = npy_unit_to_abbrev(reso) + raise ValueError(f" cannot construct a Timedelta from a unit {err}") + new_reso = get_supported_reso(reso) if reso != NPY_DATETIMEUNIT.NPY_FR_GENERIC: try: diff --git a/pandas/tests/libs/test_lib.py b/pandas/tests/libs/test_lib.py index 6ad8d748d6997..8583d8bcc052c 100644 --- a/pandas/tests/libs/test_lib.py +++ b/pandas/tests/libs/test_lib.py @@ -59,7 +59,7 @@ def test_fast_multiget_timedelta_resos(self): tm.assert_numpy_array_equal(result, expected) # case that can't be cast to td64ns - td = Timedelta(np.timedelta64(400, "Y")) + td = Timedelta(np.timedelta64(146000, "D")) assert hash(td) == hash(td.as_unit("ms")) assert hash(td) == hash(td.as_unit("us")) mapping1 = {td: 1} diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index ad9dd408fbeaf..7bd9e5fc5e293 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -289,7 +289,6 @@ def test_overflow_on_construction(): @pytest.mark.parametrize( "val, unit", [ - (3508, "M"), (15251, "W"), # 1 (106752, "D"), # change from previous: (2562048, "h"), # 0 hours @@ -333,7 +332,6 @@ def test_construction_out_of_bounds_td64ns(val, unit): @pytest.mark.parametrize( "val, unit", [ - (3508 * 10**9, "M"), (15251 * 10**9, "W"), (106752 * 10**9, "D"), (2562048 * 10**9, "h"), diff --git a/pandas/tests/tslibs/test_timedeltas.py b/pandas/tests/tslibs/test_timedeltas.py index 36ca02d32dbbd..2308aa27b60ab 100644 --- a/pandas/tests/tslibs/test_timedeltas.py +++ b/pandas/tests/tslibs/test_timedeltas.py @@ -72,6 +72,15 @@ def test_delta_to_nanoseconds_td64_MY_raises(): delta_to_nanoseconds(td) +@pytest.mark.parametrize("unit", ["Y", "M"]) +def test_unsupported_td64_unit_raises(unit): + # GH 52806 + with pytest.raises( + ValueError, match=f"cannot construct a Timedelta from a unit {unit}" + ): + Timedelta(np.timedelta64(1, unit)) + + def test_huge_nanoseconds_overflow(): # GH 32402 assert delta_to_nanoseconds(Timedelta(1e10)) == 1e10