diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index a1de10f61306a..dda20fe8aeb21 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -1160,6 +1160,7 @@ Timedelta - Accuracy improvement in :meth:`Timedelta.to_pytimedelta` to round microseconds consistently for large nanosecond based Timedelta (:issue:`57841`) - Bug in :class:`Timedelta` constructor failing to raise when passed an invalid keyword (:issue:`53801`) - Bug in :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`) +- Bug in adding or subtracting a :class:`Timedelta` object with non-nanosecond unit to a python ``datetime.datetime`` object giving incorrect results; this now works correctly for Timedeltas inside the ``datetime.timedelta`` implementation bounds (:issue:`53643`) - Bug in multiplication operations with ``timedelta64`` dtype failing to raise ``TypeError`` when multiplying by ``bool`` objects or dtypes (:issue:`58054`) - Bug in multiplication operations with ``timedelta64`` dtype incorrectly raising when multiplying by numpy-nullable dtypes or pyarrow integer dtypes (:issue:`58054`) diff --git a/pandas/_libs/meson.build b/pandas/_libs/meson.build index 33fc65e5034d0..d84c605911f04 100644 --- a/pandas/_libs/meson.build +++ b/pandas/_libs/meson.build @@ -160,7 +160,7 @@ foreach ext_name, ext_dict : libs_sources ext_dict.get('sources'), cython_args: cython_args, include_directories: [inc_np, inc_pd], - dependencies: ext_dict.get('deps', ''), + dependencies: ext_dict.get('deps', []), subdir: 'pandas/_libs', install: true, ) diff --git a/pandas/_libs/tslibs/meson.build b/pandas/_libs/tslibs/meson.build index ac43dc7db5fb7..f76f04dbf75fe 100644 --- a/pandas/_libs/tslibs/meson.build +++ b/pandas/_libs/tslibs/meson.build @@ -40,7 +40,7 @@ foreach ext_name, ext_dict : tslibs_sources ext_dict.get('sources'), cython_args: cython_args, include_directories: [inc_np, inc_pd], - dependencies: ext_dict.get('deps', ''), + dependencies: ext_dict.get('deps', []), subdir: 'pandas/_libs/tslibs', install: true, ) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 1e01ad9246aae..81e3bc6472cf2 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1022,9 +1022,23 @@ cdef _timedelta_from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso): elif reso == NPY_DATETIMEUNIT.NPY_FR_us: td_base = _Timedelta.__new__(cls, microseconds=int(value)) elif reso == NPY_DATETIMEUNIT.NPY_FR_ms: - td_base = _Timedelta.__new__(cls, milliseconds=0) + if -86_399_999_913_600_000 <= value < 86_400_000_000_000_000: + # i.e. we are in range for pytimedelta. By passing the + # 'correct' value here we can + # make pydatetime + Timedelta operations work correctly, + # xref GH#53643 + td_base = _Timedelta.__new__(cls, milliseconds=value) + else: + td_base = _Timedelta.__new__(cls, milliseconds=0) elif reso == NPY_DATETIMEUNIT.NPY_FR_s: - td_base = _Timedelta.__new__(cls, seconds=0) + if -86_399_999_913_600 <= value < 86_400_000_000_000: + # i.e. we are in range for pytimedelta. By passing the + # 'correct' value here we can + # make pydatetime + Timedelta operations work correctly, + # xref GH#53643 + td_base = _Timedelta.__new__(cls, seconds=value) + else: + td_base = _Timedelta.__new__(cls, seconds=0) # Other resolutions are disabled but could potentially be implemented here: # elif reso == NPY_DATETIMEUNIT.NPY_FR_m: # td_base = _Timedelta.__new__(Timedelta, minutes=int(value)) diff --git a/pandas/core/arrays/_arrow_string_mixins.py b/pandas/core/arrays/_arrow_string_mixins.py index e5e8ffe409788..4e32db15b392f 100644 --- a/pandas/core/arrays/_arrow_string_mixins.py +++ b/pandas/core/arrays/_arrow_string_mixins.py @@ -203,12 +203,16 @@ def _str_swapcase(self) -> Self: return self._from_pyarrow_array(pc.utf8_swapcase(self._pa_array)) def _str_removeprefix(self, prefix: str): + if prefix == "": + return self._from_pyarrow_array(self._pa_array) starts_with = pc.starts_with(self._pa_array, pattern=prefix) removed = pc.utf8_slice_codeunits(self._pa_array, len(prefix)) result = pc.if_else(starts_with, removed, self._pa_array) return self._from_pyarrow_array(result) def _str_removesuffix(self, suffix: str): + if suffix == "": + return self._from_pyarrow_array(self._pa_array) ends_with = pc.ends_with(self._pa_array, pattern=suffix) removed = pc.utf8_slice_codeunits(self._pa_array, 0, stop=-len(suffix)) result = pc.if_else(ends_with, removed, self._pa_array) diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py index 20e46bbbe0803..d9eda82155b06 100644 --- a/pandas/tests/scalar/timedelta/test_arithmetic.py +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -35,6 +35,21 @@ class TestTimedeltaAdditionSubtraction: __sub__, __rsub__ """ + def test_td_add_sub_pydatetime(self, unit): + # GH#53643 + td = Timedelta(hours=23).as_unit(unit) + dt = datetime(2016, 1, 1) + + expected = datetime(2016, 1, 1, 23) + result = dt + td + assert result == expected + result = td + dt + assert result == expected + + expected = datetime(2015, 12, 31, 1) + result = dt - td + assert result == expected + @pytest.mark.parametrize( "ten_seconds", [ diff --git a/pandas/tests/strings/test_strings.py b/pandas/tests/strings/test_strings.py index e07ece91090df..5873800794d49 100644 --- a/pandas/tests/strings/test_strings.py +++ b/pandas/tests/strings/test_strings.py @@ -544,7 +544,12 @@ def test_strip_lstrip_rstrip_args(any_string_dtype, method, exp): @pytest.mark.parametrize( - "prefix, expected", [("a", ["b", " b c", "bc"]), ("ab", ["", "a b c", "bc"])] + "prefix, expected", + [ + ("a", ["b", " b c", "bc"]), + ("ab", ["", "a b c", "bc"]), + ("", ["ab", "a b c", "bc"]), + ], ) def test_removeprefix(any_string_dtype, prefix, expected): ser = Series(["ab", "a b c", "bc"], dtype=any_string_dtype) @@ -554,7 +559,12 @@ def test_removeprefix(any_string_dtype, prefix, expected): @pytest.mark.parametrize( - "suffix, expected", [("c", ["ab", "a b ", "b"]), ("bc", ["ab", "a b c", ""])] + "suffix, expected", + [ + ("c", ["ab", "a b ", "b"]), + ("bc", ["ab", "a b c", ""]), + ("", ["ab", "a b c", "bc"]), + ], ) def test_removesuffix(any_string_dtype, suffix, expected): ser = Series(["ab", "a b c", "bc"], dtype=any_string_dtype)