From 786e8daf109801c29a2790d25122f45ab385a29a Mon Sep 17 00:00:00 2001 From: Brock Date: Tue, 14 Jun 2022 16:35:42 -0700 Subject: [PATCH 1/3] ENH: Timedelta/Timestamp round support non-nano --- pandas/_libs/tslibs/timedeltas.pyx | 9 ++++----- pandas/_libs/tslibs/timestamps.pyx | 8 ++++---- pandas/core/arrays/datetimelike.py | 3 ++- pandas/tests/scalar/timedelta/test_timedelta.py | 16 ++++++++++++++++ pandas/tests/scalar/timestamp/test_unary_ops.py | 6 ++++-- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 028371633a2c1..119a733cde606 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1653,15 +1653,14 @@ class Timedelta(_Timedelta): int64_t result, unit, remainder ndarray[int64_t] arr - if self._reso != NPY_FR_ns: - raise NotImplementedError - from pandas._libs.tslibs.offsets import to_offset - unit = to_offset(freq).nanos + + to_offset(freq).nanos # raises on non-fixed freq + unit = delta_to_nanoseconds(to_offset(freq), self._reso) arr = np.array([self.value], dtype="i8") result = round_nsint64(arr, mode, unit)[0] - return Timedelta(result, unit="ns") + return Timedelta._from_value_and_reso(result, self._reso) def round(self, freq): """ diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 349cb0d50c46e..a4370f5a4d633 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -1634,10 +1634,10 @@ class Timestamp(_Timestamp): def _round(self, freq, mode, ambiguous='raise', nonexistent='raise'): cdef: - int64_t nanos = to_offset(freq).nanos + int64_t nanos - if self._reso != NPY_FR_ns: - raise NotImplementedError(self._reso) + to_offset(freq).nanos # raises on non-fixed freq + nanos = delta_to_nanoseconds(to_offset(freq), self._reso) if self.tz is not None: value = self.tz_localize(None).value @@ -1648,7 +1648,7 @@ class Timestamp(_Timestamp): # Will only ever contain 1 element for timestamp r = round_nsint64(value, mode, nanos)[0] - result = Timestamp(r, unit='ns') + result = Timestamp._from_value_and_reso(r, self._reso, None) if self.tz is not None: result = result.tz_localize( self.tz, ambiguous=ambiguous, nonexistent=nonexistent diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 1dfb070e29c30..52d8e80bf9fb8 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1932,7 +1932,8 @@ def _round(self, freq, mode, ambiguous, nonexistent): values = self.view("i8") values = cast(np.ndarray, values) - nanos = to_offset(freq).nanos + nanos = to_offset(freq).nanos # raises on non-fixed frequencies + nanos = delta_to_nanoseconds(to_offset(freq), self._reso) result_i8 = round_nsint64(values, mode, nanos) result = self._maybe_mask_results(result_i8, fill_value=iNaT) result = result.view(self._ndarray.dtype) diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 5ae6ed9f13ece..ce33c6f74561d 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -603,6 +603,22 @@ def test_round_sanity(self, val, method): assert np.abs((res - td).value) < nanos assert res.value % nanos == 0 + @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"]) + def test_round_non_nano(self, unit): + td = Timedelta("1 days 02:34:57")._as_unit(unit) + + res = td.round("min") + assert res == Timedelta("1 days 02:35:00") + assert res._reso == td._reso + + res = td.floor("min") + assert res == Timedelta("1 days 02:34:00") + assert res._reso == td._reso + + res = td.ceil("min") + assert res == Timedelta("1 days 02:35:00") + assert res._reso == td._reso + def test_contains(self): # Checking for any NaT-like objects # GH 13603 diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index 35065a3c9877c..c2e5e03c644c8 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -148,11 +148,13 @@ def test_round_minute_freq(self, test_input, freq, expected, rounder): result = func(freq) assert result == expected - def test_ceil(self): - dt = Timestamp("20130101 09:10:11") + @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"]) + def test_ceil(self, unit): + dt = Timestamp("20130101 09:10:11")._as_unit(unit) result = dt.ceil("D") expected = Timestamp("20130102") assert result == expected + assert result._reso == dt._reso def test_floor(self): dt = Timestamp("20130101 09:10:11") From ca6c7e210fdf0d6d1cb88196ebe3c1697cb442e8 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 15 Jun 2022 14:30:24 -0700 Subject: [PATCH 2/3] parametrize test_floor --- pandas/tests/scalar/timestamp/test_unary_ops.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index c2e5e03c644c8..8683d9cf67207 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -156,11 +156,13 @@ def test_ceil(self, unit): assert result == expected assert result._reso == dt._reso - def test_floor(self): - dt = Timestamp("20130101 09:10:11") + @pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"]) + def test_floor(self, unit): + dt = Timestamp("20130101 09:10:11")._as_unit(unit) result = dt.floor("D") expected = Timestamp("20130101") assert result == expected + assert result._reso == dt._reso @pytest.mark.parametrize("method", ["ceil", "round", "floor"]) def test_round_dst_border_ambiguous(self, method): From 05fb02da8262b9dcb9efd6b17132eaca5948baa8 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 15 Jun 2022 15:53:12 -0700 Subject: [PATCH 3/3] un-xfail --- pandas/tests/scalar/timestamp/test_unary_ops.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index fc3260fd7bbbc..2146e32a437a9 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -167,12 +167,7 @@ def test_floor(self, unit): @pytest.mark.parametrize("method", ["ceil", "round", "floor"]) @pytest.mark.parametrize( "unit", - [ - "ns", - pytest.param("us", marks=pytest.mark.xfail(reason="round not implemented")), - pytest.param("ms", marks=pytest.mark.xfail(reason="round not implemented")), - pytest.param("s", marks=pytest.mark.xfail(reason="round not implemented")), - ], + ["ns", "us", "ms", "s"], ) def test_round_dst_border_ambiguous(self, method, unit): # GH 18946 round near "fall back" DST @@ -207,12 +202,7 @@ def test_round_dst_border_ambiguous(self, method, unit): ) @pytest.mark.parametrize( "unit", - [ - "ns", - pytest.param("us", marks=pytest.mark.xfail(reason="round not implemented")), - pytest.param("ms", marks=pytest.mark.xfail(reason="round not implemented")), - pytest.param("s", marks=pytest.mark.xfail(reason="round not implemented")), - ], + ["ns", "us", "ms", "s"], ) def test_round_dst_border_nonexistent(self, method, ts_str, freq, unit): # GH 23324 round near "spring forward" DST