diff --git a/doc/whats-new.rst b/doc/whats-new.rst index c8ae5ac43c8..de7e6c8f6ff 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -71,7 +71,7 @@ Enhancements :py:meth:`~xarray.Dataset.differentiate`, :py:meth:`~xarray.DataArray.interp`, and :py:meth:`~xarray.Dataset.interp`. - By `Spencer Clark `_ + By `Spencer Clark `_. Bug fixes ~~~~~~~~~ @@ -97,6 +97,13 @@ Bug fixes ``open_rasterio`` to error (:issue:`2454`). By `Stephan Hoyer `_. +- Subtracting one CFTimeIndex from another now returns a + ``pandas.TimedeltaIndex``, analogous to the behavior for DatetimeIndexes + (:issue:`2484`). By `Spencer Clark `_. +- Adding a TimedeltaIndex to, or subtracting a TimedeltaIndex from a + CFTimeIndex is now allowed (:issue:`2484`). + By `Spencer Clark `_. + .. _whats-new.0.10.9: v0.10.9 (21 September 2018) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index dea896c199a..5de055c1b9a 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -359,13 +359,27 @@ def shift(self, n, freq): "str or datetime.timedelta, got {}.".format(freq)) def __add__(self, other): + if isinstance(other, pd.TimedeltaIndex): + other = other.to_pytimedelta() return CFTimeIndex(np.array(self) + other) def __radd__(self, other): + if isinstance(other, pd.TimedeltaIndex): + other = other.to_pytimedelta() return CFTimeIndex(other + np.array(self)) def __sub__(self, other): - return CFTimeIndex(np.array(self) - other) + if isinstance(other, CFTimeIndex): + return pd.TimedeltaIndex(np.array(self) - np.array(other)) + elif isinstance(other, pd.TimedeltaIndex): + return CFTimeIndex(np.array(self) - other.to_pytimedelta()) + else: + return CFTimeIndex(np.array(self) - other) + + def _add_delta(self, deltas): + # To support TimedeltaIndex + CFTimeIndex with older versions of + # pandas. No longer used as of pandas 0.23. + return self + deltas def _parse_iso8601_without_reso(date_type, datetime_str): diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index d1726ab3313..e18c55d2fae 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -629,6 +629,17 @@ def test_cftimeindex_add(index): assert isinstance(result, CFTimeIndex) +@pytest.mark.skipif(not has_cftime, reason='cftime not installed') +@pytest.mark.parametrize('calendar', _CFTIME_CALENDARS) +def test_cftimeindex_add_timedeltaindex(calendar): + a = xr.cftime_range('2000', periods=5, calendar=calendar) + deltas = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) + result = a + deltas + expected = a.shift(2, 'D') + assert result.equals(expected) + assert isinstance(result, CFTimeIndex) + + @pytest.mark.skipif(not has_cftime, reason='cftime not installed') def test_cftimeindex_radd(index): date_type = index.date_type @@ -640,6 +651,17 @@ def test_cftimeindex_radd(index): assert isinstance(result, CFTimeIndex) +@pytest.mark.skipif(not has_cftime, reason='cftime not installed') +@pytest.mark.parametrize('calendar', _CFTIME_CALENDARS) +def test_timedeltaindex_add_cftimeindex(calendar): + a = xr.cftime_range('2000', periods=5, calendar=calendar) + deltas = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) + result = deltas + a + expected = a.shift(2, 'D') + assert result.equals(expected) + assert isinstance(result, CFTimeIndex) + + @pytest.mark.skipif(not has_cftime, reason='cftime not installed') def test_cftimeindex_sub(index): date_type = index.date_type @@ -652,6 +674,28 @@ def test_cftimeindex_sub(index): assert isinstance(result, CFTimeIndex) +@pytest.mark.skipif(not has_cftime, reason='cftime not installed') +@pytest.mark.parametrize('calendar', _CFTIME_CALENDARS) +def test_cftimeindex_sub_cftimeindex(calendar): + a = xr.cftime_range('2000', periods=5, calendar=calendar) + b = a.shift(2, 'D') + result = b - a + expected = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) + assert result.equals(expected) + assert isinstance(result, pd.TimedeltaIndex) + + +@pytest.mark.skipif(not has_cftime, reason='cftime not installed') +@pytest.mark.parametrize('calendar', _CFTIME_CALENDARS) +def test_cftimeindex_sub_timedeltaindex(calendar): + a = xr.cftime_range('2000', periods=5, calendar=calendar) + deltas = pd.TimedeltaIndex([timedelta(days=2) for _ in range(5)]) + result = a - deltas + expected = a.shift(-2, 'D') + assert result.equals(expected) + assert isinstance(result, CFTimeIndex) + + @pytest.mark.skipif(not has_cftime, reason='cftime not installed') def test_cftimeindex_rsub(index): with pytest.raises(TypeError):