# SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2021 Taneli Hukkinen # Licensed to PSF under a Contributor Agreement. from __future__ import annotations import re from datetime import date from datetime import datetime from datetime import time from datetime import timedelta from datetime import timezone from datetime import tzinfo from functools import lru_cache 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 (?:\.[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) @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)