Skip to content
25 changes: 22 additions & 3 deletions xarray/coding/times.py
Original file line number Diff line number Diff line change
Expand Up @@ -1370,15 +1370,24 @@ class CFDatetimeCoder(VariableCoder):
May not be supported by all the backends.
time_unit : PDDatetimeUnitOptions
Target resolution when decoding dates. Defaults to "ns".
on_error : str, optional
What to do if there is an error when attempting to decode
a time variable. Options are: "raise", "warn", "ignore".
Defaults to "raise".
"""

def __init__(
self,
use_cftime: bool | None = None,
time_unit: PDDatetimeUnitOptions = "ns",
on_error: str = "raise",
) -> None:
self.use_cftime = use_cftime
self.time_unit = time_unit
if on_error in {"raise", "warn", "ignore"}:
self.on_error = on_error
else:
raise ValueError('on_error must be one of "raise", "warn", "ignore")')

def encode(self, variable: Variable, name: T_Name = None) -> Variable:
if np.issubdtype(variable.dtype, np.datetime64) or contains_cftime_datetimes(
Expand Down Expand Up @@ -1411,9 +1420,19 @@ def decode(self, variable: Variable, name: T_Name = None) -> Variable:

units = pop_to(attrs, encoding, "units")
calendar = pop_to(attrs, encoding, "calendar")
dtype = _decode_cf_datetime_dtype(
data, units, calendar, self.use_cftime, self.time_unit
)
try:
dtype = _decode_cf_datetime_dtype(
data, units, calendar, self.use_cftime, self.time_unit
)
except ValueError as err:
if self.on_error == "ignore":
return variable
elif self.on_error == "warn":
emit_user_level_warning(err.args[0])
return variable
else:
raise

transform = partial(
decode_cf_datetime,
units=units,
Expand Down
17 changes: 16 additions & 1 deletion xarray/conventions.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,22 @@ def decode_cf_variable(
" ds = xr.open_dataset(decode_times=time_coder)\n",
FutureWarning,
)
decode_times = CFDatetimeCoder(use_cftime=use_cftime)
# decode_times = CFDatetimeCoder(use_cftime=use_cftime)
decode_times_options = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this dict should be a global?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in xarray/core/types.py somewhere nearby CFCalendar would be nice.

True: "raise",
"error": "raise",
"ignore": "ignore",
"warn": "warn",
}
try:
on_error = decode_times_options[decode_times]
except KeyError:
raise ValueError(
"`decode_times` must be one of:"
"True, False, 'raise', 'warn', 'ignore'"
) from None
decode_times = CFDatetimeCoder(use_cftime=use_cftime, on_error=on_error)

elif use_cftime is not None:
raise TypeError(
"Usage of 'use_cftime' as a kwarg is not allowed "
Expand Down
54 changes: 54 additions & 0 deletions xarray/tests/test_coding_times.py
Original file line number Diff line number Diff line change
Expand Up @@ -2229,3 +2229,57 @@ def test_roundtrip_empty_datetime64_array(time_unit: PDDatetimeUnitOptions) -> N
)
assert_identical(variable, roundtripped)
assert roundtripped.dtype == variable.dtype


@requires_cftime
def test_on_error_raises():
"""
By default, decoding errors should raise
"""
array = np.array([0, 1, 2], dtype=np.dtype("int64"))
encoded = Variable(["time"], array, attrs={"units": "ms since 00:00:00"})

# default is "raise"
coder = CFDatetimeCoder()

with pytest.raises(ValueError):
coder.decode(encoded)

# setting to "raise" should do the same thing.
coder = CFDatetimeCoder(on_error="raise")

with pytest.raises(ValueError):
coder.decode(encoded)


@requires_cftime
def test_on_error_ignore():
"""
If on_error="ignore", no change.
"""
array = np.array([0, 1, 2], dtype=np.dtype("int64"))
encoded = Variable(["time"], array, attrs={"units": "ms since 00:00:00"})

coder = CFDatetimeCoder(on_error="ignore")

decoded = coder.decode(encoded)

# it shouldn't have changed the variable
assert decoded is encoded


@requires_cftime
def test_on_error_warn():
"""
If on_error="warn", no change, with a warning.
"""
array = np.array([0, 1, 2], dtype=np.dtype("int64"))
encoded = Variable(["time"], array, attrs={"units": "ms since 00:00:00"})

coder = CFDatetimeCoder(on_error="warn")

with pytest.warns(UserWarning, match="unable to decode time units"):
decoded = coder.decode(encoded)

# it shouldn't have changed the variable
assert decoded is encoded
Loading