diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index e8510a8d7c345..ed44aaee0b33d 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -901,6 +901,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.union` and :meth:`DatetimeIndex.intersection` when ``unit`` was non-nanosecond (:issue:`59036`) - Bug in :meth:`Index.union` with a ``pyarrow`` timestamp dtype incorrectly returning ``object`` dtype (:issue:`58421`) - Bug in :meth:`Series.dt.microsecond` producing incorrect results for pyarrow backed :class:`Series`. (:issue:`59154`) +- Bug in :meth:`Timestamp.replace` failing to update ``unit`` attribute when replacement introduces non-zero ``nanosecond`` or ``microsecond`` (:issue:`57749`) - Bug in :meth:`to_datetime` not respecting dayfirst if an uncommon date string was passed. (:issue:`58859`) - Bug in :meth:`to_datetime` on float array with missing values throwing ``FloatingPointError`` (:issue:`58419`) - Bug in :meth:`to_datetime` on float32 df with year, month, day etc. columns leads to precision issues and incorrect result. (:issue:`60506`) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 5af4c80d0de8e..df1ead215a2be 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -3357,6 +3357,7 @@ default 'raise' datetime ts_input tzinfo_type tzobj _TSObject ts + NPY_DATETIMEUNIT creso = self._creso # set to naive if needed tzobj = self.tzinfo @@ -3396,8 +3397,12 @@ default 'raise' dts.sec = validate("second", second) if microsecond is not None: dts.us = validate("microsecond", microsecond) + if creso < NPY_DATETIMEUNIT.NPY_FR_us: + # GH#57749 + creso = NPY_DATETIMEUNIT.NPY_FR_us if nanosecond is not None: dts.ps = validate("nanosecond", nanosecond) * 1000 + creso = NPY_FR_ns # GH#57749 if tzinfo is not object: tzobj = tzinfo @@ -3407,17 +3412,17 @@ default 'raise' # to datetimes outside of pydatetime range. ts = _TSObject() try: - ts.value = npy_datetimestruct_to_datetime(self._creso, &dts) + ts.value = npy_datetimestruct_to_datetime(creso, &dts) except OverflowError as err: fmt = dts_to_iso_string(&dts) raise OutOfBoundsDatetime( f"Out of bounds timestamp: {fmt} with frequency '{self.unit}'" ) from err ts.dts = dts - ts.creso = self._creso + ts.creso = creso ts.fold = fold return create_timestamp_from_ts( - ts.value, dts, tzobj, fold, reso=self._creso + ts.value, dts, tzobj, fold, reso=creso ) elif tzobj is not None and treat_tz_as_pytz(tzobj): @@ -3436,10 +3441,10 @@ default 'raise' ts_input = datetime(**kwargs) ts = convert_datetime_to_tsobject( - ts_input, tzobj, nanos=dts.ps // 1000, reso=self._creso + ts_input, tzobj, nanos=dts.ps // 1000, reso=creso ) return create_timestamp_from_ts( - ts.value, dts, tzobj, fold, reso=self._creso + ts.value, dts, tzobj, fold, reso=creso ) def to_julian_date(self) -> np.float64: diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index a41f3014bc23f..5202d320108c7 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -642,7 +642,7 @@ def test_pivot_tz_in_values(self): ) df = df.set_index("ts").reset_index() - mins = df.ts.map(lambda x: x.replace(hour=0, minute=0, second=0, microsecond=0)) + mins = df.ts.map(lambda x: x.replace(hour=0, minute=0, second=0)) result = pivot_table( df.set_index("ts").reset_index(), diff --git a/pandas/tests/scalar/timestamp/methods/test_replace.py b/pandas/tests/scalar/timestamp/methods/test_replace.py index f15ea0e485cae..0f61736161cb1 100644 --- a/pandas/tests/scalar/timestamp/methods/test_replace.py +++ b/pandas/tests/scalar/timestamp/methods/test_replace.py @@ -195,3 +195,17 @@ def test_replace_preserves_fold(self, fold): ts_replaced = ts.replace(second=1) assert ts_replaced.fold == fold + + def test_replace_updates_unit(self): + # GH#57749 + ts = Timestamp("2023-07-15 23:08:12.134567123") + ts2 = Timestamp("2023-07-15 23:08:12.000000") + assert ts2.unit == "us" + result = ts2.replace(microsecond=ts.microsecond, nanosecond=ts.nanosecond) + assert result.unit == "ns" + assert result == ts + + ts3 = Timestamp("2023-07-15 23:08:12").as_unit("s") + result = ts3.replace(microsecond=ts2.microsecond) + assert result.unit == "us" + assert result == ts2