]>
Commit | Line | Data |
---|---|---|
1 | # SPDX-License-Identifier: MIT | |
2 | # SPDX-FileCopyrightText: 2021 Taneli Hukkinen | |
3 | # Licensed to PSF under a Contributor Agreement. | |
4 | ||
5 | from __future__ import annotations | |
6 | ||
7 | from datetime import date, datetime, time, timedelta, timezone, tzinfo | |
8 | from functools import lru_cache | |
9 | import re | |
10 | from typing import Any | |
11 | ||
12 | from ._types import ParseFloat | |
13 | ||
14 | # E.g. | |
15 | # - 00:32:00.999999 | |
16 | # - 00:32:00 | |
17 | _TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?" | |
18 | ||
19 | RE_NUMBER = re.compile( | |
20 | r""" | |
21 | 0 | |
22 | (?: | |
23 | x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex | |
24 | | | |
25 | b[01](?:_?[01])* # bin | |
26 | | | |
27 | o[0-7](?:_?[0-7])* # oct | |
28 | ) | |
29 | | | |
30 | [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part | |
31 | (?P<floatpart> | |
32 | (?:\.[0-9](?:_?[0-9])*)? # optional fractional part | |
33 | (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part | |
34 | ) | |
35 | """, | |
36 | flags=re.VERBOSE, | |
37 | ) | |
38 | RE_LOCALTIME = re.compile(_TIME_RE_STR) | |
39 | RE_DATETIME = re.compile( | |
40 | rf""" | |
41 | ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27 | |
42 | (?: | |
43 | [Tt ] | |
44 | {_TIME_RE_STR} | |
45 | (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset | |
46 | )? | |
47 | """, | |
48 | flags=re.VERBOSE, | |
49 | ) | |
50 | ||
51 | ||
52 | def match_to_datetime(match: re.Match) -> datetime | date: | |
53 | """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`. | |
54 | ||
55 | Raises ValueError if the match does not correspond to a valid date | |
56 | or datetime. | |
57 | """ | |
58 | ( | |
59 | year_str, | |
60 | month_str, | |
61 | day_str, | |
62 | hour_str, | |
63 | minute_str, | |
64 | sec_str, | |
65 | micros_str, | |
66 | zulu_time, | |
67 | offset_sign_str, | |
68 | offset_hour_str, | |
69 | offset_minute_str, | |
70 | ) = match.groups() | |
71 | year, month, day = int(year_str), int(month_str), int(day_str) | |
72 | if hour_str is None: | |
73 | return date(year, month, day) | |
74 | hour, minute, sec = int(hour_str), int(minute_str), int(sec_str) | |
75 | micros = int(micros_str.ljust(6, "0")) if micros_str else 0 | |
76 | if offset_sign_str: | |
77 | tz: tzinfo | None = cached_tz( | |
78 | offset_hour_str, offset_minute_str, offset_sign_str | |
79 | ) | |
80 | elif zulu_time: | |
81 | tz = timezone.utc | |
82 | else: # local date-time | |
83 | tz = None | |
84 | return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz) | |
85 | ||
86 | ||
87 | @lru_cache(maxsize=None) | |
88 | def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone: | |
89 | sign = 1 if sign_str == "+" else -1 | |
90 | return timezone( | |
91 | timedelta( | |
92 | hours=sign * int(hour_str), | |
93 | minutes=sign * int(minute_str), | |
94 | ) | |
95 | ) | |
96 | ||
97 | ||
98 | def match_to_localtime(match: re.Match) -> time: | |
99 | hour_str, minute_str, sec_str, micros_str = match.groups() | |
100 | micros = int(micros_str.ljust(6, "0")) if micros_str else 0 | |
101 | return time(int(hour_str), int(minute_str), int(sec_str), micros) | |
102 | ||
103 | ||
104 | def match_to_number(match: re.Match, parse_float: ParseFloat) -> Any: | |
105 | if match.group("floatpart"): | |
106 | return parse_float(match.group()) | |
107 | return int(match.group(), 0) |