|
31 | 31 | ISO_TIMESTAMP = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d{1,6})?") |
32 | 32 | ISO_TIMESTAMP_NANO = re.compile(r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(.\d{1,6})?(\d{1,3})?") |
33 | 33 | EPOCH_TIMESTAMPTZ = datetime.fromisoformat("1970-01-01T00:00:00.000000+00:00") |
34 | | -ISO_TIMESTAMPTZ = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d{1,6})?([-+]\d{2}:\d{2}|Z)") |
35 | | -ISO_TIMESTAMPTZ_NANO = re.compile(r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(.\d{1,6})?(\d{1,3})?([-+]\d{2}:\d{2}|Z)") |
| 34 | +# Seconds are optional and the zone can be an offset or the UTC designator, as in Java's ISO_DATE_TIME |
| 35 | +ISO_TIMESTAMPTZ = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(.\d{1,6})?)?([-+]\d{2}:\d{2}|[Zz])") |
| 36 | +ISO_TIMESTAMPTZ_NANO = re.compile(r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})(?:(:\d{2})(.\d{1,6})?(\d{1,3})?)?([-+]\d{2}:\d{2}|[Zz])") |
36 | 37 |
|
37 | 38 |
|
38 | 39 | def micros_to_days(timestamp: int) -> int: |
@@ -135,19 +136,20 @@ def timestamp_to_nanos(timestamp_str: str) -> int: |
135 | 136 |
|
136 | 137 | def _utc_offset(offset: str) -> str: |
137 | 138 | # datetime.fromisoformat only accepts the "Z" designator from Python 3.11 |
138 | | - return "+00:00" if offset == "Z" else offset |
| 139 | + return "+00:00" if offset in ("Z", "z") else offset |
139 | 140 |
|
140 | 141 |
|
141 | 142 | def timestamptz_to_nanos(timestamptz_str: str) -> int: |
142 | 143 | """Convert an ISO-8601 formatted timestamp with zone to nanoseconds from 1970-01-01T00:00:00.000000000+00:00.""" |
143 | 144 | if match := ISO_TIMESTAMPTZ_NANO.fullmatch(timestamptz_str): |
144 | 145 | # Python datetime does not have native nanoseconds support |
145 | 146 | # Hence we need to extract nanoseconds timestamp manually |
146 | | - # group(3) holds the sub-microsecond digits (fraction positions 7-9), so |
| 147 | + # group(4) holds the sub-microsecond digits (fraction positions 7-9), so |
147 | 148 | # right-pad to 3 digits before reading them as nanoseconds (e.g. "7" -> 700). |
148 | | - ns_str = (match.group(3) or "0").ljust(3, "0") |
149 | | - ms_str = match.group(2) if match.group(2) else "" |
150 | | - timestamptz_str_without_ns_str = match.group(1) + ms_str + _utc_offset(match.group(4)) |
| 149 | + ns_str = (match.group(4) or "0").ljust(3, "0") |
| 150 | + seconds_str = match.group(2) or "" |
| 151 | + ms_str = match.group(3) or "" |
| 152 | + timestamptz_str_without_ns_str = match.group(1) + seconds_str + ms_str + _utc_offset(match.group(5)) |
151 | 153 | return datetime_to_nanos(datetime.fromisoformat(timestamptz_str_without_ns_str)) + int(ns_str) |
152 | 154 | if ISO_TIMESTAMP_NANO.fullmatch(timestamptz_str): |
153 | 155 | # When we can match a timestamp without a zone, we can give a more specific error |
|
0 commit comments