diff --git a/xarray/coding/cftime_offsets.py b/xarray/coding/cftime_offsets.py index 89c06e56ea7..2cd8eccd6f3 100644 --- a/xarray/coding/cftime_offsets.py +++ b/xarray/coding/cftime_offsets.py @@ -68,7 +68,12 @@ from xarray.core.utils import attempt_import, emit_user_level_warning if TYPE_CHECKING: - from xarray.core.types import InclusiveOptions, Self, TypeAlias + from xarray.core.types import ( + InclusiveOptions, + PDDatetimeUnitOptions, + Self, + TypeAlias, + ) DayOption: TypeAlias = Literal["start", "end"] @@ -971,7 +976,6 @@ def cftime_range( Include boundaries; whether to set each bound as closed or open. .. versionadded:: 2023.02.0 - calendar : str, default: "standard" Calendar type for the datetimes. @@ -1180,6 +1184,7 @@ def date_range( normalize=False, name=None, inclusive: InclusiveOptions = "both", + unit: PDDatetimeUnitOptions = "ns", calendar="standard", use_cftime=None, ): @@ -1210,6 +1215,10 @@ def date_range( Include boundaries; whether to set each bound as closed or open. .. versionadded:: 2023.02.0 + unit : {"s", "ms", "us", "ns"}, default "ns" + Specify the desired resolution of the result. + + .. versionadded:: 2024.12.0 calendar : str, default: "standard" Calendar type for the datetimes. use_cftime : boolean, optional @@ -1245,6 +1254,7 @@ def date_range( normalize=normalize, name=name, inclusive=inclusive, + unit=unit, ) except pd.errors.OutOfBoundsDatetime as err: if use_cftime is False: diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index a4524efe117..9030966a934 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -92,7 +92,7 @@ def trailing_optional(xs): return xs[0] + optional(trailing_optional(xs[1:])) -def build_pattern(date_sep=r"\-", datetime_sep=r"T", time_sep=r"\:"): +def build_pattern(date_sep=r"\-", datetime_sep=r"T", time_sep=r"\:", micro_sep=r"."): pieces = [ (None, "year", r"\d{4}"), (date_sep, "month", r"\d{2}"), @@ -100,6 +100,7 @@ def build_pattern(date_sep=r"\-", datetime_sep=r"T", time_sep=r"\:"): (datetime_sep, "hour", r"\d{2}"), (time_sep, "minute", r"\d{2}"), (time_sep, "second", r"\d{2}"), + (micro_sep, "microsecond", r"\d{1,6}"), ] pattern_list = [] for sep, name, sub_pattern in pieces: @@ -131,11 +132,13 @@ def _parse_iso8601_with_reso(date_type, timestr): result = parse_iso8601_like(timestr) replace = {} - for attr in ["year", "month", "day", "hour", "minute", "second"]: + for attr in ["year", "month", "day", "hour", "minute", "second", "microsecond"]: value = result.get(attr, None) if value is not None: # Note ISO8601 conventions allow for fractional seconds. # TODO: Consider adding support for sub-second resolution? + if attr == "microsecond": + value = 10 ** (6 - len(value)) * int(value) replace[attr] = int(value) resolution = attr return default.replace(**replace), resolution diff --git a/xarray/core/types.py b/xarray/core/types.py index 11b26da033f..d4224dcead9 100644 --- a/xarray/core/types.py +++ b/xarray/core/types.py @@ -249,6 +249,7 @@ def copy( "Y", "M", "W", "D", "h", "m", "s", "ms", "us", "μs", "ns", "ps", "fs", "as", None ] NPDatetimeUnitOptions = Literal["D", "h", "m", "s", "ms", "us", "ns"] +PDDatetimeUnitOptions = Literal["s", "ms", "us", "ns"] QueryEngineOptions = Literal["python", "numexpr", None] QueryParserOptions = Literal["pandas", "python"]