Skip to content

Commit 53fdfca

Browse files
fix and test empty CFTimeIndex (#8600)
* fix empty cftimeindex repr * switch to None * require cftime * fix empty cftimindex * add tests * none not a string in repr * make it explicit * explicitely test dtype of date fields * set date_field dtype * use repr fstring to avoid conditional * whats new entry * Apply suggestions from code review Co-authored-by: Spencer Clark <[email protected]> --------- Co-authored-by: Spencer Clark <[email protected]>
1 parent 357a444 commit 53fdfca

File tree

4 files changed

+115
-8
lines changed

4 files changed

+115
-8
lines changed

doc/whats-new.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ Bug fixes
6969
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
7070
- Vendor `SerializableLock` from dask and use as default lock for netcdf4 backends (:issue:`8442`, :pull:`8571`).
7171
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
72-
72+
- Add tests and fixes for empty :py:class:`CFTimeIndex`, including broken html repr (:issue:`7298`, :pull:`8600`).
73+
By `Mathias Hauser <https://github.com/mathause>`_.
7374

7475
Documentation
7576
~~~~~~~~~~~~~

xarray/coding/cftimeindex.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def _parsed_string_to_bounds(date_type, resolution, parsed):
187187

188188
def get_date_field(datetimes, field):
189189
"""Adapted from pandas.tslib.get_date_field"""
190-
return np.array([getattr(date, field) for date in datetimes])
190+
return np.array([getattr(date, field) for date in datetimes], dtype=np.int64)
191191

192192

193193
def _field_accessor(name, docstring=None, min_cftime_version="0.0"):
@@ -272,8 +272,8 @@ def format_attrs(index, separator=", "):
272272
attrs = {
273273
"dtype": f"'{index.dtype}'",
274274
"length": f"{len(index)}",
275-
"calendar": f"'{index.calendar}'",
276-
"freq": f"'{index.freq}'" if len(index) >= 3 else None,
275+
"calendar": f"{index.calendar!r}",
276+
"freq": f"{index.freq!r}",
277277
}
278278

279279
attrs_str = [f"{k}={v}" for k, v in attrs.items()]
@@ -630,6 +630,10 @@ def to_datetimeindex(self, unsafe=False):
630630
>>> times.to_datetimeindex()
631631
DatetimeIndex(['2000-01-01', '2000-01-02'], dtype='datetime64[ns]', freq=None)
632632
"""
633+
634+
if not self._data.size:
635+
return pd.DatetimeIndex([])
636+
633637
nptimes = cftime_to_nptime(self)
634638
calendar = infer_calendar_name(self)
635639
if calendar not in _STANDARD_CALENDARS and not unsafe:
@@ -679,6 +683,9 @@ def asi8(self):
679683
"""Convert to integers with units of microseconds since 1970-01-01."""
680684
from xarray.core.resample_cftime import exact_cftime_datetime_difference
681685

686+
if not self._data.size:
687+
return np.array([], dtype=np.int64)
688+
682689
epoch = self.date_type(1970, 1, 1)
683690
return np.array(
684691
[
@@ -693,19 +700,29 @@ def calendar(self):
693700
"""The calendar used by the datetimes in the index."""
694701
from xarray.coding.times import infer_calendar_name
695702

703+
if not self._data.size:
704+
return None
705+
696706
return infer_calendar_name(self)
697707

698708
@property
699709
def freq(self):
700710
"""The frequency used by the dates in the index."""
701711
from xarray.coding.frequencies import infer_freq
702712

713+
# min 3 elemtents required to determine freq
714+
if self._data.size < 3:
715+
return None
716+
703717
return infer_freq(self)
704718

705719
def _round_via_method(self, freq, method):
706720
"""Round dates using a specified method."""
707721
from xarray.coding.cftime_offsets import CFTIME_TICKS, to_offset
708722

723+
if not self._data.size:
724+
return CFTimeIndex(np.array(self))
725+
709726
offset = to_offset(freq)
710727
if not isinstance(offset, CFTIME_TICKS):
711728
raise ValueError(f"{offset} is a non-fixed frequency")

xarray/tests/test_cftimeindex.py

+77-3
Original file line numberDiff line numberDiff line change
@@ -238,28 +238,57 @@ def test_assert_all_valid_date_type(date_type, index):
238238
)
239239
def test_cftimeindex_field_accessors(index, field, expected):
240240
result = getattr(index, field)
241+
expected = np.array(expected, dtype=np.int64)
241242
assert_array_equal(result, expected)
243+
assert result.dtype == expected.dtype
244+
245+
246+
@requires_cftime
247+
@pytest.mark.parametrize(
248+
("field"),
249+
[
250+
"year",
251+
"month",
252+
"day",
253+
"hour",
254+
"minute",
255+
"second",
256+
"microsecond",
257+
"dayofyear",
258+
"dayofweek",
259+
"days_in_month",
260+
],
261+
)
262+
def test_empty_cftimeindex_field_accessors(field):
263+
index = CFTimeIndex([])
264+
result = getattr(index, field)
265+
expected = np.array([], dtype=np.int64)
266+
assert_array_equal(result, expected)
267+
assert result.dtype == expected.dtype
242268

243269

244270
@requires_cftime
245271
def test_cftimeindex_dayofyear_accessor(index):
246272
result = index.dayofyear
247-
expected = [date.dayofyr for date in index]
273+
expected = np.array([date.dayofyr for date in index], dtype=np.int64)
248274
assert_array_equal(result, expected)
275+
assert result.dtype == expected.dtype
249276

250277

251278
@requires_cftime
252279
def test_cftimeindex_dayofweek_accessor(index):
253280
result = index.dayofweek
254-
expected = [date.dayofwk for date in index]
281+
expected = np.array([date.dayofwk for date in index], dtype=np.int64)
255282
assert_array_equal(result, expected)
283+
assert result.dtype == expected.dtype
256284

257285

258286
@requires_cftime
259287
def test_cftimeindex_days_in_month_accessor(index):
260288
result = index.days_in_month
261-
expected = [date.daysinmonth for date in index]
289+
expected = np.array([date.daysinmonth for date in index], dtype=np.int64)
262290
assert_array_equal(result, expected)
291+
assert result.dtype == expected.dtype
263292

264293

265294
@requires_cftime
@@ -959,6 +988,31 @@ def test_cftimeindex_calendar_property(calendar, expected):
959988
assert index.calendar == expected
960989

961990

991+
@requires_cftime
992+
def test_empty_cftimeindex_calendar_property():
993+
index = CFTimeIndex([])
994+
assert index.calendar is None
995+
996+
997+
@requires_cftime
998+
@pytest.mark.parametrize(
999+
"calendar",
1000+
[
1001+
"noleap",
1002+
"365_day",
1003+
"360_day",
1004+
"julian",
1005+
"gregorian",
1006+
"standard",
1007+
"proleptic_gregorian",
1008+
],
1009+
)
1010+
def test_cftimeindex_freq_property_none_size_lt_3(calendar):
1011+
for periods in range(3):
1012+
index = xr.cftime_range(start="2000", periods=periods, calendar=calendar)
1013+
assert index.freq is None
1014+
1015+
9621016
@requires_cftime
9631017
@pytest.mark.parametrize(
9641018
("calendar", "expected"),
@@ -1152,6 +1206,18 @@ def test_rounding_methods_against_datetimeindex(freq, method):
11521206
assert result.equals(expected)
11531207

11541208

1209+
@requires_cftime
1210+
@pytest.mark.parametrize("method", ["floor", "ceil", "round"])
1211+
def test_rounding_methods_empty_cftimindex(method):
1212+
index = CFTimeIndex([])
1213+
result = getattr(index, method)("2s")
1214+
1215+
expected = CFTimeIndex([])
1216+
1217+
assert result.equals(expected)
1218+
assert result is not index
1219+
1220+
11551221
@requires_cftime
11561222
@pytest.mark.parametrize("method", ["floor", "ceil", "round"])
11571223
def test_rounding_methods_invalid_freq(method):
@@ -1230,6 +1296,14 @@ def test_asi8_distant_date():
12301296
np.testing.assert_array_equal(result, expected)
12311297

12321298

1299+
@requires_cftime
1300+
def test_asi8_empty_cftimeindex():
1301+
index = xr.CFTimeIndex([])
1302+
result = index.asi8
1303+
expected = np.array([], dtype=np.int64)
1304+
np.testing.assert_array_equal(result, expected)
1305+
1306+
12331307
@requires_cftime
12341308
def test_infer_freq_valid_types():
12351309
cf_indx = xr.cftime_range("2000-01-01", periods=3, freq="D")

xarray/tests/test_formatting.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import xarray as xr
1212
from xarray.core import formatting
13-
from xarray.tests import requires_dask, requires_netCDF4
13+
from xarray.tests import requires_cftime, requires_dask, requires_netCDF4
1414

1515

1616
class TestFormatting:
@@ -803,3 +803,18 @@ def test_format_xindexes(as_dataset: bool) -> None:
803803

804804
actual = repr(obj.xindexes)
805805
assert actual == expected
806+
807+
808+
@requires_cftime
809+
def test_empty_cftimeindex_repr() -> None:
810+
index = xr.coding.cftimeindex.CFTimeIndex([])
811+
812+
expected = """\
813+
Indexes:
814+
time CFTimeIndex([], dtype='object', length=0, calendar=None, freq=None)"""
815+
expected = dedent(expected)
816+
817+
da = xr.DataArray([], coords={"time": index})
818+
819+
actual = repr(da.indexes)
820+
assert actual == expected

0 commit comments

Comments
 (0)