diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 93166759d8dbd..6e3c579ed7b17 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -299,7 +299,26 @@ class TimelikeOps: def _round(self, freq, mode, ambiguous, nonexistent): # round the local times values = _ensure_datetimelike_to_i8(self) - result = round_nsint64(values, mode, freq) + try: + result = round_nsint64(values, mode, freq) + except ValueError as e: + if "non-fixed" in str(e): + offset = frequencies.to_offset(freq) + if mode == RoundTo.PLUS_INFTY: + result = self.to_period(offset) \ + .to_timestamp(how='end').asi8 + elif mode == RoundTo.MINUS_INFTY: + result = self.to_period(offset) \ + .to_timestamp(how='start').asi8 + elif mode == RoundTo.NEAREST_HALF_EVEN: + msg = ("round only supports fixed offsets " + "(i.e. 'Day' is ok, 'MonthEnd' is not). " + "You can use dti.snap or floor/ceil if " + "applicable.") + raise ValueError(msg) + else: + raise e + result = self._maybe_mask_results(result, fill_value=NaT) dtype = self.dtype diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index c7c0e1180ce46..1c1990a037ee7 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -11,9 +11,10 @@ import pandas as pd from pandas.core.arrays import DatetimeArray from pandas.core.arrays.datetimes import sequence_to_dt64ns +from pandas.tseries.offsets import Nano +from pandas import Timestamp import pandas.util.testing as tm - class TestDatetimeArrayConstructor: def test_only_1dim_accepted(self): @@ -301,3 +302,98 @@ def test_min_max_empty(self, skipna, tz): result = arr.max(skipna=skipna) assert result is pd.NaT + + @pytest.mark.parametrize( + 'op, freq, dates, expected_dates', + [ + ( + 'floor', + 'M', + ("2001-02-01", + Timestamp("2001-02-14 12:00") - Nano(), + "2001-02-14 12:00", + "2001-02-15", + Timestamp("2001-03-01") - Nano(), + "2001-03-01" + ), + [ + Timestamp("2001-02-01") - Nano(), + Timestamp("2001-02-01") - Nano(), + Timestamp("2001-02-01") - Nano(), + Timestamp("2001-02-01") - Nano(), + Timestamp("2001-03-01") - Nano(), + Timestamp("2001-03-01") - Nano(), + ] + ), + ( + 'ceil', + 'M', + ("2001-02-01", + Timestamp( + "2001-02-14 12:00") - Nano(), + "2001-02-14 12:00", + "2001-02-15", + Timestamp("2001-03-01") - Nano(), + "2001-03-01" + ), + [ + Timestamp("2001-03-01") - Nano(), + Timestamp("2001-03-01") - Nano(), + Timestamp("2001-03-01") - Nano(), + Timestamp("2001-03-01") - Nano(), + Timestamp("2001-03-01") - Nano(), + Timestamp("2001-04-01") - Nano(), + ] + ), + ( + 'floor', + 'MS', + ("2001-02-01", + Timestamp("2001-02-14 12:00") - Nano(), + "2001-02-14 12:00", + "2001-02-15", + Timestamp("2001-03-01") - Nano(), + "2001-03-01" + ), + [ + "2001-02-01", + "2001-02-01", + "2001-02-01", + "2001-02-01", + "2001-02-01", + "2001-03-01" + ] + ), + ( + 'ceil', + 'MS', + ("2001-02-01", + Timestamp("2001-02-14 12:00") - Nano(), + "2001-02-14 12:00", + "2001-02-15", + Timestamp("2001-03-01") - Nano(), + "2001-03-01" + ), + [ + "2001-02-01", + "2001-03-01", + "2001-03-01", + "2001-03-01", + "2001-03-01", + "2001-03-01", + ] + ) + ] + ) + def test_ceil_floor(self, op, freq, dates, expected_dates): + dta = DatetimeArray._from_sequence(dates) + dta[1] -= pd.offsets.Nano() + dta[-2] -= pd.offsets.Nano() + dti = pd.DatetimeIndex(dta) + result = getattr(dti, op)(freq) + expected = pd.DatetimeIndex(expected_dates) + tm.assert_index_equal(result, expected) + + # Idempotent + result = getattr(result, op)(freq) + tm.assert_index_equal(result, expected)