From 870be6003ff2e3071ee11273cfbf2da2131d4b5f Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Mon, 14 Jan 2019 09:46:16 -0500 Subject: [PATCH 1/4] Enable subtracting a scalar cftime.datetime object from a CFTimeIndex --- doc/whats-new.rst | 4 ++++ xarray/coding/cftimeindex.py | 3 ++- xarray/tests/test_cftimeindex.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index b50df2af10e..0459787c847 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -48,6 +48,10 @@ Bug fixes - Saving files with times encoded with reference dates with timezones (e.g. '2000-01-01T00:00:00-05:00') no longer raises an error (:issue:`2649`). By `Spencer Clark `_. +- Subtracting a scalar ``cftime.datetime`` object from a + :py:class:`CFTimeIndex` now results in a :py:class:`pandas.TimedeltaIndex` + instead of raising a ``TypeError`` (:issue:`2671`). By `Spencer Clark + `_. .. _whats-new.0.11.2: diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index af22a3219ad..6f267bf9123 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -411,7 +411,8 @@ def __radd__(self, other): return CFTimeIndex(other + np.array(self)) def __sub__(self, other): - if isinstance(other, CFTimeIndex): + import cftime + if isinstance(other, (CFTimeIndex, cftime.datetime)): return pd.TimedeltaIndex(np.array(self) - np.array(other)) elif isinstance(other, pd.TimedeltaIndex): return CFTimeIndex(np.array(self) - other.to_pytimedelta()) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 3fe014bdaba..6e8c6f5d4f3 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -701,6 +701,17 @@ def test_cftimeindex_sub_cftimeindex(calendar): 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_cftime_datetime(calendar): + a = xr.cftime_range('2000', periods=5, calendar=calendar) + b = a[0] + result = a - b + expected = pd.TimedeltaIndex([timedelta(days=i) for i 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): From 255d6e57d1e927ece4720b0d4718bb764fcd33f0 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Mon, 14 Jan 2019 09:48:56 -0500 Subject: [PATCH 2/4] lint --- xarray/tests/test_cftimeindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 6e8c6f5d4f3..6e72b28ffba 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -710,7 +710,7 @@ def test_cftimeindex_sub_cftime_datetime(calendar): expected = pd.TimedeltaIndex([timedelta(days=i) for i 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) From d585078b8ed7ecf0bad97b02f96e2550e0a2a299 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Tue, 15 Jan 2019 07:28:09 -0500 Subject: [PATCH 3/4] Test cftime.datetime minus CFTimeIndex as well --- xarray/tests/test_cftimeindex.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 6e72b28ffba..93b302870eb 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -705,12 +705,16 @@ def test_cftimeindex_sub_cftimeindex(calendar): @pytest.mark.parametrize('calendar', _CFTIME_CALENDARS) def test_cftimeindex_sub_cftime_datetime(calendar): a = xr.cftime_range('2000', periods=5, calendar=calendar) - b = a[0] - result = a - b + result = a - a[0] expected = pd.TimedeltaIndex([timedelta(days=i) for i in range(5)]) assert result.equals(expected) assert isinstance(result, pd.TimedeltaIndex) + result = a[0] - a + expected = pd.TimedeltaIndex([timedelta(days=-i) for i 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) From 56d54b0d2799282ed131789e63d1c78a52447f34 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Tue, 15 Jan 2019 08:11:22 -0500 Subject: [PATCH 4/4] Fix cftime minus CFTimeIndex --- xarray/coding/cftimeindex.py | 3 +++ xarray/tests/test_cftimeindex.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 6f267bf9123..bfe40970f7e 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -419,6 +419,9 @@ def __sub__(self, other): else: return CFTimeIndex(np.array(self) - other) + def __rsub__(self, other): + return pd.TimedeltaIndex(other - np.array(self)) + def _add_delta(self, deltas): # To support TimedeltaIndex + CFTimeIndex with older versions of # pandas. No longer used as of pandas 0.23. diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 93b302870eb..c71a1c42f6b 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -710,6 +710,11 @@ def test_cftimeindex_sub_cftime_datetime(calendar): 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_cftime_datetime_sub_cftimeindex(calendar): + a = xr.cftime_range('2000', periods=5, calendar=calendar) result = a[0] - a expected = pd.TimedeltaIndex([timedelta(days=-i) for i in range(5)]) assert result.equals(expected)