From e0f2ceede29087854edfa498cfd23d36bcf4178a Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Mon, 6 May 2024 13:37:37 -0400 Subject: [PATCH] Port negative frequency fix for `pandas.date_range` to `cftime_range` (#8999) --- doc/whats-new.rst | 6 ++++++ xarray/coding/cftime_offsets.py | 7 ++++++- xarray/tests/__init__.py | 1 + xarray/tests/test_cftime_offsets.py | 17 ++++++++++++++--- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 9a4601a776f..a846c1b8a01 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -60,6 +60,12 @@ Breaking changes Bug fixes ~~~~~~~~~ +- Following `an upstream bug fix + `_ to + :py:func:`pandas.date_range`, date ranges produced by + :py:func:`xarray.cftime_range` with negative frequencies will now fall fully + within the bounds of the provided start and end dates (:pull:`8999`). By + `Spencer Clark `_. Internal Changes diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 2e594455874..0af75f404a2 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -845,7 +845,12 @@ def _generate_range(start, end, periods, offset): A generator object """ if start: - start = offset.rollforward(start) + # From pandas GH 56147 / 56832 to account for negative direction and + # range bounds + if offset.n >= 0: + start = offset.rollforward(start) + else: + start = offset.rollback(start) if periods is None and end < start and offset.n >= 0: end = None diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index 94c44544fb5..59fcdca8ffa 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -130,6 +130,7 @@ def _importorskip( has_numexpr, requires_numexpr = _importorskip("numexpr") has_flox, requires_flox = _importorskip("flox") has_pandas_ge_2_2, __ = _importorskip("pandas", "2.2") +has_pandas_3, requires_pandas_3 = _importorskip("pandas", "3.0.0.dev0") # some special cases diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index 0110afe40ac..eabb7d2f4d6 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -42,6 +42,7 @@ has_cftime, has_pandas_ge_2_2, requires_cftime, + requires_pandas_3, ) cftime = pytest.importorskip("cftime") @@ -1354,7 +1355,7 @@ def test_calendar_specific_month_end_negative_freq( ) -> None: year = 2000 # Use a leap-year to highlight calendar differences result = cftime_range( - start="2000-12", + start="2001", end="2000", freq="-2ME", calendar=calendar, @@ -1464,7 +1465,7 @@ def test_date_range_errors() -> None: ("2020-02-01", "QE-DEC", "noleap", "gregorian", True, "2020-03-31", True), ("2020-02-01", "YS-FEB", "noleap", "gregorian", True, "2020-02-01", True), ("2020-02-01", "YE-FEB", "noleap", "gregorian", True, "2020-02-29", True), - ("2020-02-01", "-1YE-FEB", "noleap", "gregorian", True, "2020-02-29", True), + ("2020-02-01", "-1YE-FEB", "noleap", "gregorian", True, "2019-02-28", True), ("2020-02-28", "3h", "all_leap", "gregorian", False, "2020-02-28", True), ("2020-03-30", "ME", "360_day", "gregorian", False, "2020-03-31", True), ("2020-03-31", "ME", "gregorian", "360_day", None, "2020-03-30", False), @@ -1724,7 +1725,17 @@ def test_new_to_legacy_freq_pd_freq_passthrough(freq, expected): @pytest.mark.parametrize("start", ("2000", "2001")) @pytest.mark.parametrize("end", ("2000", "2001")) @pytest.mark.parametrize( - "freq", ("MS", "-1MS", "YS", "-1YS", "ME", "-1ME", "YE", "-1YE") + "freq", + ( + "MS", + pytest.param("-1MS", marks=requires_pandas_3), + "YS", + pytest.param("-1YS", marks=requires_pandas_3), + "ME", + pytest.param("-1ME", marks=requires_pandas_3), + "YE", + pytest.param("-1YE", marks=requires_pandas_3), + ), ) def test_cftime_range_same_as_pandas(start, end, freq): result = date_range(start, end, freq=freq, calendar="standard", use_cftime=True)