diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index f7bed2c13ef..20ef9805d6f 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -772,11 +772,18 @@ def _emit_freq_deprecation_warning(deprecated_freq): emit_user_level_warning(message, FutureWarning) -def to_offset(freq: BaseCFTimeOffset | str, warn: bool = True) -> BaseCFTimeOffset: +def to_offset( + freq: BaseCFTimeOffset | str | timedelta | pd.Timedelta | pd.DateOffset, + warn: bool = True, +) -> BaseCFTimeOffset: """Convert a frequency string to the appropriate subclass of BaseCFTimeOffset.""" if isinstance(freq, BaseCFTimeOffset): return freq + if isinstance(freq, timedelta | pd.Timedelta): + return delta_to_tick(freq) + if isinstance(freq, pd.DateOffset): + freq = freq.freqstr match = re.match(_PATTERN, freq) if match is None: @@ -791,6 +798,34 @@ def to_offset(freq: BaseCFTimeOffset | str, warn: bool = True) -> BaseCFTimeOffs return _FREQUENCIES[freq](n=multiples) +def delta_to_tick(delta: timedelta | pd.Timedelta) -> Tick: + """Adapted from pandas.tslib.delta_to_tick""" + if isinstance(delta, pd.Timedelta) and delta.nanoseconds != 0: + # pandas.Timedelta has nanoseconds, but these are not supported + raise ValueError( + "Unable to convert 'pandas.Timedelta' object with non-zero " + "nanoseconds to 'CFTimeOffset' object" + ) + if delta.microseconds == 0: + if delta.seconds == 0: + return Day(n=delta.days) + else: + seconds = delta.days * 86400 + delta.seconds + if seconds % 3600 == 0: + return Hour(n=seconds // 3600) + elif seconds % 60 == 0: + return Minute(n=seconds // 60) + else: + return Second(n=seconds) + else: + # Regardless of the days and seconds this will always be a Millsecond + # or Microsecond object + if delta.microseconds % 1_000 == 0: + return Millisecond(n=delta.microseconds // 1_000) + else: + return Microsecond(n=delta.microseconds) + + def to_cftime_datetime(date_str_or_date, calendar=None): if cftime is None: raise ModuleNotFoundError("No module named 'cftime'") diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index f17bd057c03..4a48cb2b3e9 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -7252,7 +7252,11 @@ def resample( offset: pd.Timedelta | datetime.timedelta | str | None = None, origin: str | DatetimeLike = "start_day", restore_coord_dims: bool | None = None, - **indexer_kwargs: str | Resampler, + **indexer_kwargs: str + | datetime.timedelta + | pd.Timedelta + | pd.DateOffset + | Resampler, ) -> DataArrayResample: """Returns a Resample object for performing resampling operations. diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index ae387da7e8e..63c53ed5768 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -10693,7 +10693,11 @@ def resample( offset: pd.Timedelta | datetime.timedelta | str | None = None, origin: str | DatetimeLike = "start_day", restore_coord_dims: bool | None = None, - **indexer_kwargs: str | Resampler, + **indexer_kwargs: str + | datetime.timedelta + | pd.Timedelta + | pd.DateOffset + | Resampler, ) -> DatasetResample: """Returns a Resample object for performing resampling operations. diff --git a/xarray/core/resample_cftime.py b/xarray/core/resample_cftime.py index 2149a62dfb5..2c9b13151ab 100644 --- a/xarray/core/resample_cftime.py +++ b/xarray/core/resample_cftime.py @@ -75,7 +75,11 @@ class CFTimeGrouper: def __init__( self, - freq: str | BaseCFTimeOffset, + freq: str + | datetime.timedelta + | pd.Timedelta + | pd.DateOffset + | BaseCFTimeOffset, closed: SideOptions | None = None, label: SideOptions | None = None, origin: str | CFTimeDatetime = "start_day", diff --git a/xarray/groupers.py b/xarray/groupers.py index ba215c247f7..b1a58714942 100644 --- a/xarray/groupers.py +++ b/xarray/groupers.py @@ -380,13 +380,6 @@ def _init_properties(self, group: T_Group) -> None: if isinstance(group_as_index, CFTimeIndex): from xarray.core.resample_cftime import CFTimeGrouper - if not isinstance(self.freq, str | BaseCFTimeOffset): - raise ValueError( - "Resample frequency must be a string or 'BaseCFTimeOffset' " - "object when resampling a 'CFTimeIndex'. Received " - f"{type(self.freq)} instead." - ) - self.index_grouper = CFTimeGrouper( freq=self.freq, closed=self.closed, diff --git a/xarray/tests/test_groupby.py b/xarray/tests/test_groupby.py index d7efd56560b..164d7fb0f49 100644 --- a/xarray/tests/test_groupby.py +++ b/xarray/tests/test_groupby.py @@ -758,7 +758,6 @@ def test_groupby_none_group_name() -> None: def test_groupby_getitem(dataset) -> None: - assert_identical(dataset.sel(x=["a"]), dataset.groupby("x")["a"]) assert_identical(dataset.sel(z=[1]), dataset.groupby("z")[1]) assert_identical(dataset.foo.sel(x=["a"]), dataset.foo.groupby("x")["a"]) @@ -1829,13 +1828,12 @@ def test_resample_dtype(self, use_cftime: bool) -> None: ) ], ) - test_resample_freqs = ["10min"] - if not use_cftime: - test_resample_freqs += [ - pd.Timedelta(hours=2), - pd.offsets.MonthBegin(), - datetime.timedelta(days=1, hours=6), - ] + test_resample_freqs = ( + "10min", + pd.Timedelta(hours=2), + pd.offsets.MonthBegin(), + datetime.timedelta(days=1, hours=6), + ) for freq in test_resample_freqs: array.resample(time=freq) @@ -2258,6 +2256,29 @@ def test_resample_and_first(self) -> None: result = actual.reduce(method) assert_equal(expected, result) + @pytest.mark.parametrize("use_cftime", [True, False]) + def test_resample_dtype(self, use_cftime: bool) -> None: + if use_cftime and not has_cftime: + pytest.skip() + times = xr.date_range( + "2000-01-01", freq="6h", periods=10, use_cftime=use_cftime + ) + ds = Dataset( + { + "foo": (["time", "x", "y"], np.random.randn(10, 5, 3)), + "bar": ("time", np.random.randn(10), {"meta": "data"}), + "time": times, + } + ) + test_resample_freqs = [ + "10min", + pd.Timedelta(hours=2), + pd.offsets.MonthBegin(), + datetime.timedelta(days=1, hours=6), + ] + for freq in test_resample_freqs: + ds.resample(time=freq) + def test_resample_min_count(self) -> None: times = pd.date_range("2000-01-01", freq="6h", periods=10) ds = Dataset(