Skip to content

Commit 7fd7daf

Browse files
authored
Add "unit"-parameter to date_range, enhance iso time parser to us (#9885)
* Add "unit"-parameter to date_range, enhance iso time parser to microseconds * add microsecond to test_parse_iso8601_like * fix comment on microsecond * add whats-new.rst entry * add test for microsecond
1 parent 0945e0e commit 7fd7daf

File tree

5 files changed

+69
-12
lines changed

5 files changed

+69
-12
lines changed

doc/whats-new.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ New Features
2424
- Better support wrapping additional array types (e.g. ``cupy`` or ``jax``) by calling generalized
2525
duck array operations throughout more xarray methods. (:issue:`7848`, :pull:`9798`).
2626
By `Sam Levang <https://github.com/slevang>`_.
27-
27+
- Add ``unit`` - keyword argument to :py:func:`date_range` and ``microsecond`` parsing to
28+
iso8601-parser (:pull:`9885`).
29+
By `Kai Mühlbauer <https://github.com/kmuehlbauer>`_.
2830

2931
Breaking changes
3032
~~~~~~~~~~~~~~~~

xarray/coding/cftime_offsets.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@
6868
from xarray.core.utils import attempt_import, emit_user_level_warning
6969

7070
if TYPE_CHECKING:
71-
from xarray.core.types import InclusiveOptions, Self, TypeAlias
71+
from xarray.core.types import (
72+
InclusiveOptions,
73+
PDDatetimeUnitOptions,
74+
Self,
75+
TypeAlias,
76+
)
7277

7378

7479
DayOption: TypeAlias = Literal["start", "end"]
@@ -971,7 +976,6 @@ def cftime_range(
971976
Include boundaries; whether to set each bound as closed or open.
972977
973978
.. versionadded:: 2023.02.0
974-
975979
calendar : str, default: "standard"
976980
Calendar type for the datetimes.
977981
@@ -1180,6 +1184,7 @@ def date_range(
11801184
normalize=False,
11811185
name=None,
11821186
inclusive: InclusiveOptions = "both",
1187+
unit: PDDatetimeUnitOptions = "ns",
11831188
calendar="standard",
11841189
use_cftime=None,
11851190
):
@@ -1210,6 +1215,10 @@ def date_range(
12101215
Include boundaries; whether to set each bound as closed or open.
12111216
12121217
.. versionadded:: 2023.02.0
1218+
unit : {"s", "ms", "us", "ns"}, default "ns"
1219+
Specify the desired resolution of the result.
1220+
1221+
.. versionadded:: 2024.12.0
12131222
calendar : str, default: "standard"
12141223
Calendar type for the datetimes.
12151224
use_cftime : boolean, optional
@@ -1245,6 +1254,7 @@ def date_range(
12451254
normalize=normalize,
12461255
name=name,
12471256
inclusive=inclusive,
1257+
unit=unit,
12481258
)
12491259
except pd.errors.OutOfBoundsDatetime as err:
12501260
if use_cftime is False:

xarray/coding/cftimeindex.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,15 @@ def trailing_optional(xs):
9292
return xs[0] + optional(trailing_optional(xs[1:]))
9393

9494

95-
def build_pattern(date_sep=r"\-", datetime_sep=r"T", time_sep=r"\:"):
95+
def build_pattern(date_sep=r"\-", datetime_sep=r"T", time_sep=r"\:", micro_sep=r"."):
9696
pieces = [
9797
(None, "year", r"\d{4}"),
9898
(date_sep, "month", r"\d{2}"),
9999
(date_sep, "day", r"\d{2}"),
100100
(datetime_sep, "hour", r"\d{2}"),
101101
(time_sep, "minute", r"\d{2}"),
102102
(time_sep, "second", r"\d{2}"),
103+
(micro_sep, "microsecond", r"\d{1,6}"),
103104
]
104105
pattern_list = []
105106
for sep, name, sub_pattern in pieces:
@@ -131,11 +132,12 @@ def _parse_iso8601_with_reso(date_type, timestr):
131132
result = parse_iso8601_like(timestr)
132133
replace = {}
133134

134-
for attr in ["year", "month", "day", "hour", "minute", "second"]:
135+
for attr in ["year", "month", "day", "hour", "minute", "second", "microsecond"]:
135136
value = result.get(attr, None)
136137
if value is not None:
137-
# Note ISO8601 conventions allow for fractional seconds.
138-
# TODO: Consider adding support for sub-second resolution?
138+
if attr == "microsecond":
139+
# convert match string into valid microsecond value
140+
value = 10 ** (6 - len(value)) * int(value)
139141
replace[attr] = int(value)
140142
resolution = attr
141143
return default.replace(**replace), resolution

xarray/core/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ def copy(
249249
"Y", "M", "W", "D", "h", "m", "s", "ms", "us", "μs", "ns", "ps", "fs", "as", None
250250
]
251251
NPDatetimeUnitOptions = Literal["D", "h", "m", "s", "ms", "us", "ns"]
252+
PDDatetimeUnitOptions = Literal["s", "ms", "us", "ns"]
252253

253254
QueryEngineOptions = Literal["python", "numexpr", None]
254255
QueryParserOptions = Literal["pandas", "python"]

xarray/tests/test_cftimeindex.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,23 @@
3535
standard_or_gregorian = "standard"
3636

3737

38-
def date_dict(year=None, month=None, day=None, hour=None, minute=None, second=None):
38+
def date_dict(
39+
year=None,
40+
month=None,
41+
day=None,
42+
hour=None,
43+
minute=None,
44+
second=None,
45+
microsecond=None,
46+
):
3947
return dict(
40-
year=year, month=month, day=day, hour=hour, minute=minute, second=second
48+
year=year,
49+
month=month,
50+
day=day,
51+
hour=hour,
52+
minute=minute,
53+
second=second,
54+
microsecond=microsecond,
4155
)
4256

4357

@@ -86,6 +100,30 @@ def date_dict(year=None, month=None, day=None, hour=None, minute=None, second=No
86100
year="1999", month="01", day="01", hour="12", minute="34", second="56"
87101
),
88102
),
103+
"microsecond-1": (
104+
"19990101T123456.123456",
105+
date_dict(
106+
year="1999",
107+
month="01",
108+
day="01",
109+
hour="12",
110+
minute="34",
111+
second="56",
112+
microsecond="123456",
113+
),
114+
),
115+
"microsecond-2": (
116+
"19990101T123456.1",
117+
date_dict(
118+
year="1999",
119+
month="01",
120+
day="01",
121+
hour="12",
122+
minute="34",
123+
second="56",
124+
microsecond="1",
125+
),
126+
),
89127
}
90128

91129

@@ -98,9 +136,12 @@ def test_parse_iso8601_like(string, expected):
98136
result = parse_iso8601_like(string)
99137
assert result == expected
100138

101-
with pytest.raises(ValueError):
102-
parse_iso8601_like(string + "3")
103-
parse_iso8601_like(string + ".3")
139+
if result["microsecond"] is None:
140+
with pytest.raises(ValueError):
141+
parse_iso8601_like(string + "3")
142+
if result["second"] is None:
143+
with pytest.raises(ValueError):
144+
parse_iso8601_like(string + ".3")
104145

105146

106147
_CFTIME_CALENDARS = [
@@ -301,6 +342,7 @@ def test_cftimeindex_days_in_month_accessor(index):
301342
("19990202T01", (1999, 2, 2, 1), "hour"),
302343
("19990202T0101", (1999, 2, 2, 1, 1), "minute"),
303344
("19990202T010156", (1999, 2, 2, 1, 1, 56), "second"),
345+
("19990202T010156.123456", (1999, 2, 2, 1, 1, 56, 123456), "microsecond"),
304346
],
305347
)
306348
def test_parse_iso8601_with_reso(date_type, string, date_args, reso):

0 commit comments

Comments
 (0)