2022-03-08 09:26:13 +01:00
|
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
|
|
|
# Licensed to PSF under a Contributor Agreement.
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from datetime import date, datetime, time, timedelta, timezone, tzinfo
|
|
|
|
from functools import lru_cache
|
|
|
|
import re
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
from ._types import ParseFloat
|
|
|
|
|
|
|
|
# E.g.
|
|
|
|
# - 00:32:00.999999
|
|
|
|
# - 00:32:00
|
|
|
|
_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?"
|
|
|
|
|
|
|
|
RE_NUMBER = re.compile(
|
|
|
|
r"""
|
|
|
|
0
|
|
|
|
(?:
|
|
|
|
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
|
|
|
|
|
|
|
|
b[01](?:_?[01])* # bin
|
|
|
|
|
|
|
|
|
o[0-7](?:_?[0-7])* # oct
|
|
|
|
)
|
|
|
|
|
|
|
|
|
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
|
|
|
(?P<floatpart>
|
|
|
|
(?:\.[0-9](?:_?[0-9])*)? # optional fractional part
|
|
|
|
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
|
|
|
)
|
|
|
|
""",
|
|
|
|
flags=re.VERBOSE,
|
|
|
|
)
|
|
|
|
RE_LOCALTIME = re.compile(_TIME_RE_STR)
|
|
|
|
RE_DATETIME = re.compile(
|
|
|
|
rf"""
|
|
|
|
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
|
|
|
(?:
|
|
|
|
[Tt ]
|
|
|
|
{_TIME_RE_STR}
|
|
|
|
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
|
|
|
)?
|
|
|
|
""",
|
|
|
|
flags=re.VERBOSE,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def match_to_datetime(match: re.Match) -> datetime | date:
|
|
|
|
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
|
|
|
|
|
|
|
|
Raises ValueError if the match does not correspond to a valid date
|
|
|
|
or datetime.
|
|
|
|
"""
|
|
|
|
(
|
|
|
|
year_str,
|
|
|
|
month_str,
|
|
|
|
day_str,
|
|
|
|
hour_str,
|
|
|
|
minute_str,
|
|
|
|
sec_str,
|
|
|
|
micros_str,
|
|
|
|
zulu_time,
|
|
|
|
offset_sign_str,
|
|
|
|
offset_hour_str,
|
|
|
|
offset_minute_str,
|
|
|
|
) = match.groups()
|
|
|
|
year, month, day = int(year_str), int(month_str), int(day_str)
|
|
|
|
if hour_str is None:
|
|
|
|
return date(year, month, day)
|
|
|
|
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
|
|
|
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
|
|
|
|
if offset_sign_str:
|
|
|
|
tz: tzinfo | None = cached_tz(
|
|
|
|
offset_hour_str, offset_minute_str, offset_sign_str
|
|
|
|
)
|
|
|
|
elif zulu_time:
|
|
|
|
tz = timezone.utc
|
|
|
|
else: # local date-time
|
|
|
|
tz = None
|
|
|
|
return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
|
|
|
|
|
|
|
|
2024-10-29 11:36:14 +01:00
|
|
|
# No need to limit cache size. This is only ever called on input
|
|
|
|
# that matched RE_DATETIME, so there is an implicit bound of
|
|
|
|
# 24 (hours) * 60 (minutes) * 2 (offset direction) = 2880.
|
2022-03-08 09:26:13 +01:00
|
|
|
@lru_cache(maxsize=None)
|
|
|
|
def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
|
|
|
|
sign = 1 if sign_str == "+" else -1
|
|
|
|
return timezone(
|
|
|
|
timedelta(
|
|
|
|
hours=sign * int(hour_str),
|
|
|
|
minutes=sign * int(minute_str),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def match_to_localtime(match: re.Match) -> time:
|
|
|
|
hour_str, minute_str, sec_str, micros_str = match.groups()
|
|
|
|
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
|
|
|
|
return time(int(hour_str), int(minute_str), int(sec_str), micros)
|
|
|
|
|
|
|
|
|
|
|
|
def match_to_number(match: re.Match, parse_float: ParseFloat) -> Any:
|
|
|
|
if match.group("floatpart"):
|
|
|
|
return parse_float(match.group())
|
|
|
|
return int(match.group(), 0)
|