Skip to content

Commit 8fb57f7

Browse files
spencerkclarkshoyer
authored andcommitted
Add CFTimeIndex.shift (#2431)
* Add CFTimeIndex.shift Update what's new Add bug fix note * Add example to docstring * Use pycompat for basestring * Generate an API reference page for CFTimeIndex.shift
1 parent f9c4169 commit 8fb57f7

File tree

4 files changed

+124
-0
lines changed

4 files changed

+124
-0
lines changed

doc/api-hidden.rst

+2
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,5 @@
151151
plot.FacetGrid.set_titles
152152
plot.FacetGrid.set_ticks
153153
plot.FacetGrid.map
154+
155+
CFTimeIndex.shift

doc/whats-new.rst

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Enhancements
4646
- Added support for Python 3.7. (:issue:`2271`).
4747
By `Joe Hamman <https://github.com/jhamman>`_.
4848

49+
- Added :py:meth:`~xarray.CFTimeIndex.shift` for shifting the values of a
50+
CFTimeIndex by a specified frequency. (:issue:`2244`). By `Spencer Clark
51+
<https://github.com/spencerkclark>`_.
4952
- Added support for using ``cftime.datetime`` coordinates with
5053
:py:meth:`~xarray.DataArray.differentiate`,
5154
:py:meth:`~xarray.Dataset.differentiate`,
@@ -56,6 +59,8 @@ Enhancements
5659
Bug fixes
5760
~~~~~~~~~
5861

62+
- Addition and subtraction operators used with a CFTimeIndex now preserve the
63+
index's type. (:issue:`2244`). By `Spencer Clark <https://github.com/spencerkclark>`_.
5964
- ``xarray.DataArray.roll`` correctly handles multidimensional arrays.
6065
(:issue:`2445`)
6166
By `Keisuke Fujii <https://github.com/fujiisoup>`_.

xarray/coding/cftimeindex.py

+51
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,57 @@ def contains(self, key):
315315
"""Needed for .loc based partial-string indexing"""
316316
return self.__contains__(key)
317317

318+
def shift(self, n, freq):
319+
"""Shift the CFTimeIndex a multiple of the given frequency.
320+
321+
See the documentation for :py:func:`~xarray.cftime_range` for a
322+
complete listing of valid frequency strings.
323+
324+
Parameters
325+
----------
326+
n : int
327+
Periods to shift by
328+
freq : str or datetime.timedelta
329+
A frequency string or datetime.timedelta object to shift by
330+
331+
Returns
332+
-------
333+
CFTimeIndex
334+
335+
See also
336+
--------
337+
pandas.DatetimeIndex.shift
338+
339+
Examples
340+
--------
341+
>>> index = xr.cftime_range('2000', periods=1, freq='M')
342+
>>> index
343+
CFTimeIndex([2000-01-31 00:00:00], dtype='object')
344+
>>> index.shift(1, 'M')
345+
CFTimeIndex([2000-02-29 00:00:00], dtype='object')
346+
"""
347+
from .cftime_offsets import to_offset
348+
349+
if not isinstance(n, int):
350+
raise TypeError("'n' must be an int, got {}.".format(n))
351+
if isinstance(freq, timedelta):
352+
return self + n * freq
353+
elif isinstance(freq, pycompat.basestring):
354+
return self + n * to_offset(freq)
355+
else:
356+
raise TypeError(
357+
"'freq' must be of type "
358+
"str or datetime.timedelta, got {}.".format(freq))
359+
360+
def __add__(self, other):
361+
return CFTimeIndex(np.array(self) + other)
362+
363+
def __radd__(self, other):
364+
return CFTimeIndex(other + np.array(self))
365+
366+
def __sub__(self, other):
367+
return CFTimeIndex(np.array(self) - other)
368+
318369

319370
def _parse_iso8601_without_reso(date_type, datetime_str):
320371
date, _ = _parse_iso8601_with_reso(date_type, datetime_str)

xarray/tests/test_cftimeindex.py

+66
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,72 @@ def test_empty_cftimeindex():
619619
assert index.date_type is None
620620

621621

622+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
623+
def test_cftimeindex_add(index):
624+
date_type = index.date_type
625+
expected_dates = [date_type(1, 1, 2), date_type(1, 2, 2),
626+
date_type(2, 1, 2), date_type(2, 2, 2)]
627+
expected = CFTimeIndex(expected_dates)
628+
result = index + timedelta(days=1)
629+
assert result.equals(expected)
630+
assert isinstance(result, CFTimeIndex)
631+
632+
633+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
634+
def test_cftimeindex_radd(index):
635+
date_type = index.date_type
636+
expected_dates = [date_type(1, 1, 2), date_type(1, 2, 2),
637+
date_type(2, 1, 2), date_type(2, 2, 2)]
638+
expected = CFTimeIndex(expected_dates)
639+
result = timedelta(days=1) + index
640+
assert result.equals(expected)
641+
assert isinstance(result, CFTimeIndex)
642+
643+
644+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
645+
def test_cftimeindex_sub(index):
646+
date_type = index.date_type
647+
expected_dates = [date_type(1, 1, 2), date_type(1, 2, 2),
648+
date_type(2, 1, 2), date_type(2, 2, 2)]
649+
expected = CFTimeIndex(expected_dates)
650+
result = index + timedelta(days=2)
651+
result = result - timedelta(days=1)
652+
assert result.equals(expected)
653+
assert isinstance(result, CFTimeIndex)
654+
655+
656+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
657+
def test_cftimeindex_rsub(index):
658+
with pytest.raises(TypeError):
659+
timedelta(days=1) - index
660+
661+
662+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
663+
@pytest.mark.parametrize('freq', ['D', timedelta(days=1)])
664+
def test_cftimeindex_shift(index, freq):
665+
date_type = index.date_type
666+
expected_dates = [date_type(1, 1, 3), date_type(1, 2, 3),
667+
date_type(2, 1, 3), date_type(2, 2, 3)]
668+
expected = CFTimeIndex(expected_dates)
669+
result = index.shift(2, freq)
670+
assert result.equals(expected)
671+
assert isinstance(result, CFTimeIndex)
672+
673+
674+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
675+
def test_cftimeindex_shift_invalid_n():
676+
index = xr.cftime_range('2000', periods=3)
677+
with pytest.raises(TypeError):
678+
index.shift('a', 'D')
679+
680+
681+
@pytest.mark.skipif(not has_cftime, reason='cftime not installed')
682+
def test_cftimeindex_shift_invalid_freq():
683+
index = xr.cftime_range('2000', periods=3)
684+
with pytest.raises(TypeError):
685+
index.shift(1, 1)
686+
687+
622688
@requires_cftime
623689
def test_parse_array_of_cftime_strings():
624690
from cftime import DatetimeNoLeap

0 commit comments

Comments
 (0)