Skip to content

Commit

Permalink
warn if out-of-bound datetimes are encoded with standard calendar, fa…
Browse files Browse the repository at this point in the history
…ll back to cftime encoding, add fix for cftime issue where python datetimes are not encoded correctly with date2num.
  • Loading branch information
kmuehlbauer committed Jan 9, 2025
1 parent 700e78d commit 4525ea1
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 3 deletions.
33 changes: 31 additions & 2 deletions xarray/coding/times.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,22 @@ def _encode_datetime_with_cftime(dates, units: str, calendar: str) -> np.ndarray
# numpy's broken datetime conversion only works for us precision
dates = dates.astype("M8[us]").astype(datetime)

def wrap_dt(dt):
# convert to cftime proleptic gregorian in case of datetime.datetime
# needed because of https://github.com/Unidata/cftime/issues/354
if isinstance(dt, datetime) and not isinstance(dt, cftime.datetime):
dt = cftime.datetime(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
calendar="proleptic_gregorian",
)
return dt

def encode_datetime(d):
# Since netCDF files do not support storing float128 values, we ensure
# that float64 values are used by setting longdouble=False in num2date.
Expand All @@ -881,10 +897,10 @@ def encode_datetime(d):
return (
np.nan
if d is None
else cftime.date2num(d, units, calendar, longdouble=False)
else cftime.date2num(wrap_dt(d), units, calendar, longdouble=False)
)
except TypeError:
return np.nan if d is None else cftime.date2num(d, units, calendar)
return np.nan if d is None else cftime.date2num(wrap_dt(d), units, calendar)

return reshape(np.array([encode_datetime(d) for d in ravel(dates)]), dates.shape)

Expand Down Expand Up @@ -987,6 +1003,19 @@ def _eagerly_encode_cf_datetime(
# parse with cftime instead
raise OutOfBoundsDatetime
assert np.issubdtype(dates.dtype, "datetime64")
if calendar in ["standard", "gregorian"] and np.nanmin(dates).astype(
"=M8[us]"
).astype(datetime) < datetime(1582, 10, 15):
# if we use standard calendar and for dates before the reform
# we need to use cftime instead
emit_user_level_warning(
f"Unable to encode numpy.datetime64 objects with {calendar} calendar."
"Using cftime.datetime objects instead, reason: dates prior "
"reform date (1582-10-15). To silence this warning transform "
"numpy.datetime64 to corresponding cftime.datetime beforehand.",
SerializationWarning,
)
raise OutOfBoundsDatetime

time_unit, ref_date = _unpack_time_unit_and_ref_date(units)
# calendar equivalence only for days after the reform
Expand Down
26 changes: 25 additions & 1 deletion xarray/tests/test_coding_times.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import warnings
from datetime import timedelta
from datetime import datetime, timedelta
from itertools import product
from typing import Literal

Expand Down Expand Up @@ -1156,6 +1156,30 @@ def test__encode_datetime_with_cftime() -> None:
np.testing.assert_equal(result, expected)


@requires_cftime
def test_encode_decode_cf_datetime_outofbounds_warnings(
time_unit: PDDatetimeUnitOptions,
) -> None:
import cftime

if time_unit == "ns":
pytest.skip("does not work work out of bounds datetimes")
dates = np.array(["0001-01-01", "2001-01-01"], dtype=f"datetime64[{time_unit}]")
cfdates = np.array(
[
cftime.datetime(t0.year, t0.month, t0.day, calendar="gregorian")
for t0 in dates.astype(datetime)
]
)
with pytest.warns(
SerializationWarning, match="Unable to encode numpy.datetime64 objects"
):
encoded = encode_cf_datetime(dates, "seconds since 2000-01-01", "standard")
with pytest.warns(SerializationWarning, match="Unable to decode time axis"):
decoded = decode_cf_datetime(*encoded)
np.testing.assert_equal(decoded, cfdates)


@pytest.mark.parametrize("calendar", ["gregorian", "Gregorian", "GREGORIAN"])
def test_decode_encode_roundtrip_with_non_lowercase_letters(
calendar, time_unit: PDDatetimeUnitOptions
Expand Down

0 comments on commit 4525ea1

Please sign in to comment.