diff --git a/HISTORY.rst b/HISTORY.rst index 7903def43..8337483e3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,10 @@ New features and enhancements * Added an optimized pathway for ``xclim.indices.run_length`` functions when ``window=1``. (:pull:`911`, :issue:`910`). * The data input frequency expected by ``Indicator``s is now in the ``src_freq`` attribute and is thus controlable by subclassing existing indicators. (:issue:`898`, :pull:`927`). +Breaking changes +~~~~~~~~~~~~~~~~ +* Following version 1.9 of the CF Conventions, published in September 2021, the calendar name "gregorian" is deprecated. ``core.calendar.get_calendar`` will return "standard", even if the underlying cftime objects still use "gregorian" (cftime <= 1.5.1). (:pull:`935`). + Internal changes ~~~~~~~~~~~~~~~~ * Removed some logging configurations in ``dataflags`` that were polluting python's main logging configuration. (:pull:`909`). diff --git a/xclim/core/calendar.py b/xclim/core/calendar.py index 0e5354207..c0bc065c0 100644 --- a/xclim/core/calendar.py +++ b/xclim/core/calendar.py @@ -70,19 +70,23 @@ def get_calendar(obj: Any, dim: str = "time") -> str: ------- str The cftime calendar name or "default" when the data is using numpy's or python's datetime types. + Will always return "standard" instead of "gregorian", following CF conventions 1.9. """ if isinstance(obj, (xr.DataArray, xr.Dataset)): if obj[dim].dtype == "O": obj = obj[dim].where(obj[dim].notnull(), drop=True)[0].item() elif "datetime64" in obj[dim].dtype.name: return "default" - - obj = np.take( - obj, 0 - ) # Take zeroth element, overcome cases when arrays or lists are passed. + elif isinstance(obj, xr.CFTimeIndex): + obj = obj.values[0] + else: + obj = np.take(obj, 0) + # Take zeroth element, overcome cases when arrays or lists are passed. if isinstance(obj, pydt.datetime): # Also covers pandas Timestamp return "default" if isinstance(obj, cftime.datetime): + if obj.calendar == "gregorian": + return "standard" return obj.calendar raise ValueError(f"Calendar could not be inferred from object of type {type(obj)}.") @@ -445,12 +449,10 @@ def _convert_datetime( return np.nan -def ensure_cftime_array( - time: Sequence, -) -> Union[CFTimeIndex, np.ndarray]: - """Convert an input 1D array to an array of cftime objects. +def ensure_cftime_array(time: Sequence) -> np.ndarray: + """Convert an input 1D array to a numpy array of cftime objects. - Python's datetime are converted to cftime.DatetimeGregorian. + Python's datetime are converted to cftime.DatetimeGregorian ("standard" calendar). Raises ValueError when unable to cast the input. """ @@ -458,6 +460,8 @@ def ensure_cftime_array( time = time.indexes["time"] elif isinstance(time, np.ndarray): time = pd.DatetimeIndex(time) + if isinstance(time, xr.CFTimeIndex): + return time.values if isinstance(time[0], cftime.datetime): return time if isinstance(time[0], pydt.datetime): diff --git a/xclim/testing/tests/test_calendar.py b/xclim/testing/tests/test_calendar.py index fd46282f4..10a2f1878 100644 --- a/xclim/testing/tests/test_calendar.py +++ b/xclim/testing/tests/test_calendar.py @@ -168,12 +168,19 @@ def test_get_calendar(file, cal, maxdoy): (pd.Timestamp.now(), "default"), (cftime.DatetimeAllLeap(2000, 1, 1), "all_leap"), (np.array([cftime.DatetimeNoLeap(2000, 1, 1)]), "noleap"), + (xr.cftime_range("2000-01-01", periods=4, freq="D"), "standard"), ], ) def test_get_calendar_nonxr(obj, cal): assert get_calendar(obj) == cal +@pytest.mark.parametrize("obj", ["astring", {"a": "dict"}, lambda x: x]) +def test_get_calendar_errors(obj): + with pytest.raises(ValueError, match="Calendar could not be inferred from object"): + get_calendar(obj) + + @pytest.mark.parametrize( "source,target,target_as_str,freq", [ @@ -331,7 +338,7 @@ def test_convert_calendar_missing(source, target, freq): ("standard", "noleap"), ("noleap", "default"), ("standard", "360_day"), - ("360_day", "gregorian"), + ("360_day", "standard"), ("noleap", "all_leap"), ("360_day", "noleap"), ], @@ -368,20 +375,20 @@ def test_interp_calendar(source, target): dims=("time",), name="time", ), - "gregorian", + "standard", ), - (date_range("2004-01-01", "2004-01-10", freq="D"), "gregorian"), + (date_range("2004-01-01", "2004-01-10", freq="D"), "standard"), ( xr.DataArray(date_range("2004-01-01", "2004-01-10", freq="D")).values, - "gregorian", + "standard", ), - (date_range("2004-01-01", "2004-01-10", freq="D"), "gregorian"), + (date_range("2004-01-01", "2004-01-10", freq="D").values, "standard"), (date_range("2004-01-01", "2004-01-10", freq="D", calendar="julian"), "julian"), ], ) def test_ensure_cftime_array(inp, calout): out = ensure_cftime_array(inp) - assert out[0].calendar == calout + assert get_calendar(out) == calout @pytest.mark.parametrize( @@ -391,7 +398,7 @@ def test_ensure_cftime_array(inp, calout): (2004, "noleap", 365), (2004, "all_leap", 366), (1500, "default", 365), - (1500, "gregorian", 366), + (1500, "standard", 366), (1500, "proleptic_gregorian", 365), (2030, "360_day", 360), ],