# Copyright (C) 2022-2023 Exaloop Inc. # Parts of this file: (c) 2022 Python Software Foundation. All right reserved. # - Currently does not support timezones # - Timedeltas use a pure-microseconds representations for efficiency, meaning they # have a smaller range (+/- 292,471.2 years) but should be more than enough for # all practical uses # License: # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and # the Individual or Organization ("Licensee") accessing and otherwise using Python # 3.10.2 software in source or binary form and its associated documentation. # # 2. Subject to the terms and conditions of this License Agreement, PSF hereby # grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, # analyze, test, perform and/or display publicly, prepare derivative works, # distribute, and otherwise use Python 3.10.2 alone or in any derivative # version, provided, however, that PSF's License Agreement and PSF's notice of # copyright, i.e., "Copyright © 2001-2022 Python Software Foundation; All Rights # Reserved" are retained in Python 3.10.2 alone or in any derivative version # prepared by Licensee. # # 3. In the event Licensee prepares a derivative work that is based on or # incorporates Python 3.10.2 or any part thereof, and wants to make the # derivative work available to others as provided herein, then Licensee hereby # agrees to include in any such work a brief summary of the changes made to Python # 3.10.2. # # 4. PSF is making Python 3.10.2 available to Licensee on an "AS IS" basis. # PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF # EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR # WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE # USE OF PYTHON 3.10.2 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. # # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.10.2 # FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF # MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.10.2, OR ANY DERIVATIVE # THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. # # 6. This License Agreement will automatically terminate upon a material breach of # its terms and conditions. # # 7. Nothing in this License Agreement shall be deemed to create any relationship # of agency, partnership, or joint venture between PSF and Licensee. This License # Agreement does not grant permission to use PSF trademarks or trade name in a # trademark sense to endorse or promote products or services of Licensee, or any # third party. # # 8. By copying, installing or otherwise using Python 3.10.2, Licensee agrees # to be bound by the terms and conditions of this License Agreement. from time import localtime from time import struct_time ############# # constants # ############# MINYEAR = 1 MAXYEAR = 9999 MAXORDINAL = 3652059 MAX_DELTA_DAYS = 999999999 _DI4Y = 1461 _DI100Y = 36524 _DI400Y = 146097 _ROUND_HALF_EVEN = 0 _ROUND_CEILING = 1 _ROUND_FLOOR = 2 _ROUND_UP = 3 ############# # utilities # ############# def _signed_add_overflowed(result: int, i: int, j: int) -> bool: return ((result ^ i) & (result ^ j)) < 0 def _divmod(x: int, y: int) -> Tuple[int, int]: # assert y > 0 quo = x // y r = x - quo * y if r < 0: quo -= 1 r += y # assert 0 <= r < y return quo, r def _divide_nearest(m: int, n: int) -> int: return m // n # TODO def _days_in_monthx(i: int) -> int: return (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[i] def _days_before_monthx(i: int) -> int: return (0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334)[i] def _is_leap(year: int) -> bool: return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) def _days_in_month(year: int, month: int) -> int: # assert 1 <= month <= 12 if month == 2 and _is_leap(year): return 29 else: return _days_in_monthx(month) def _days_before_month(year: int, month: int) -> int: # assert 1 <= month <= 12 days = _days_before_monthx(month) if month > 2 and _is_leap(year): days += 1 return days def _days_before_year(year: int) -> int: y = year - 1 # assert year >= 1 return y * 365 + y // 4 - y // 100 + y // 400 def _ord_to_ymd(ordinal: int) -> Tuple[int, int, int]: ordinal -= 1 n400 = ordinal // _DI400Y n = ordinal % _DI400Y year = n400 * 400 + 1 n100 = n // _DI100Y n = n % _DI100Y n4 = n // _DI4Y n = n % _DI4Y n1 = n // 365 n = n % 365 year += n100 * 100 + n4 * 4 + n1 if n1 == 4 or n100 == 4: # assert n == 0 year -= 1 return (year, 12, 31) leapyear = (n1 == 3) and (n4 != 24 or n100 == 3) # assert leapyear == is_leap(year) month = (n + 50) >> 5 preceding = _days_before_monthx(month) + int(month > 2 and leapyear) if preceding > n: month -= 1 preceding -= _days_in_month(year, month) n -= preceding # assert 0 <= n # assert n < _days_in_month(year, month) day = n + 1 return (year, month, day) def _ymd_to_ord(year: int, month: int, day: int) -> int: return _days_before_year(year) + _days_before_month(year, month) + day def _weekday(year: int, month: int, day: int) -> int: return (_ymd_to_ord(year, month, day) + 6) % 7 def _iso_week1_monday(year: int) -> int: first_day = _ymd_to_ord(year, 1, 1) first_weekday = (first_day + 6) % 7 week1_monday = first_day - first_weekday if first_weekday > 3: week1_monday += 7 return week1_monday def _check_delta_day_range(days: int): if not (-MAX_DELTA_DAYS <= days <= MAX_DELTA_DAYS): raise OverflowError(f"days={days}; must have magnitude <= {MAX_DELTA_DAYS}") def _check_date_args(year: int, month: int, day: int): if not (MINYEAR <= year <= MAXYEAR): raise ValueError(f"year {year} is out of range") if not (1 <= month <= 12): raise ValueError("month must be in 1..12") if not (1 <= day <= _days_in_month(year, month)): raise ValueError("day is out of range for month") def _check_time_args(hour: int, minute: int, second: int, microsecond: int): if not (0 <= hour <= 23): raise ValueError("hour must be in 0..23") if not (0 <= minute <= 59): raise ValueError("minute must be in 0..59") if not (0 <= second <= 59): raise ValueError("second must be in 0..59") if not (0 <= microsecond <= 999999): raise ValueError("microsecond must be in 0..999999") def _normalize_pair(hi: int, lo: int, factor: int) -> Tuple[int, int]: # assert factor > 0 if lo < 0 or lo >= factor: num_hi, lo = _divmod(lo, factor) new_hi = hi + num_hi # assert not _signed_add_overflowed(new_hi, hi, num_hi) hi = new_hi # assert 0 <= lo < factor return hi, lo def _normalize_d_s_us(d: int, s: int, us: int) -> Tuple[int, int, int]: if us < 0 or us >= 1000000: s, us = _normalize_pair(s, us, 1000000) if s < 0 or s >= 24 * 3600: d, s = _normalize_pair(d, s, 24 * 3600) # assert 0 <= s < 24*3600 # assert 0 <= us < 1000000 return d, s, us def _normalize_y_m_d(y: int, m: int, d: int) -> Tuple[int, int, int]: def error(): raise OverflowError("date value out of range") # assert 1 <= m <= 12 dim = _days_in_month(y, m) if d < 1 or d > dim: if d == 0: m -= 1 if m > 0: d = _days_in_month(y, m) else: y -= 1 m = 12 d = 31 elif d == dim + 1: m += 1 d = 1 if m > 12: m = 1 y += 1 else: ordinal = _ymd_to_ord(y, m, 1) + d - 1 if ordinal < 1 or ordinal > MAXORDINAL: error() else: return _ord_to_ymd(ordinal) # assert m > 0 # assert d > 0 if not (MINYEAR <= y <= MAXYEAR): error() return y, m, d def _normalize_date(year: int, month: int, day: int) -> Tuple[int, int, int]: return _normalize_y_m_d(year, month, day) def _normalize_datetime( year: int, month: int, day: int, hour: int, minute: int, second: int, microsecond: int, ) -> Tuple[int, int, int, int, int, int, int]: second, microsecond = _normalize_pair(second, microsecond, 1000000) minute, second = _normalize_pair(minute, second, 60) hour, minute = _normalize_pair(hour, minute, 60) day, hour = _normalize_pair(day, hour, 24) year, month, day = _normalize_date(year, month, day) return year, month, day, hour, minute, second, microsecond def _parse_digits(digits: str, num_digits: int) -> Tuple[str, int]: if len(digits) < num_digits: return "", -1 p = digits.ptr var = 0 for i in range(num_digits): tmp = int(p[0]) - 48 # 48 == '0' if not (0 <= tmp <= 9): return "", -1 var *= 10 var += tmp p += 1 return str(p, len(digits) - num_digits), var def _isoformat_error(s: str): raise ValueError(f"Invalid isoformat string: {s}") def _parse_isoformat_date(dtstr: str) -> Tuple[int, int, int]: p = dtstr p, year = _parse_digits(p, 4) if year < 0: _isoformat_error(dtstr) if not p or p[0] != "-": _isoformat_error(dtstr) p = p[1:] p, month = _parse_digits(p, 2) if month < 0: _isoformat_error(dtstr) if not p or p[0] != "-": _isoformat_error(dtstr) p = p[1:] p, day = _parse_digits(p, 2) if day < 0 or p: _isoformat_error(dtstr) return year, month, day def _parse_hh_mm_ss_ff(tstr: str) -> Tuple[int, int, int, int]: hour, minute, second, microsecond = 0, 0, 0, 0 p = tstr for i in range(3): p, val = _parse_digits(p, 2) if val < 0: _isoformat_error(tstr) if i == 0: hour = val if i == 1: minute = val if i == 2: second = val if not p: return hour, minute, second, microsecond c = p[0] p = p[1:] if c == ":": continue elif c == ".": break else: _isoformat_error(tstr) len_remains = len(p) if not (len_remains == 6 or len_remains == 3): _isoformat_error(tstr) p, microsecond = _parse_digits(p, len_remains) if microsecond < 0: _isoformat_error(tstr) if len_remains == 3: microsecond *= 1000 return hour, minute, second, microsecond def _parse_isoformat_time(dtstr: str) -> Tuple[int, int, int, int, int, int]: n = len(dtstr) tzinfo_pos = 0 tzsign = 0 while tzinfo_pos < n: c = dtstr[tzinfo_pos] if c == "+": tzsign = 1 break if c == "-": tzsign = -1 break tzinfo_pos += 1 hour, minute, second, microsecond = _parse_hh_mm_ss_ff(dtstr[:tzinfo_pos]) if tzinfo_pos == n: return hour, minute, second, microsecond, 0, 0 tzlen = n - tzinfo_pos if not (tzlen == 6 or tzlen == 9 or tzlen == 16): _isoformat_error(dtstr) tzhour, tzminute, tzsecond, tzmicrosecond = _parse_hh_mm_ss_ff( dtstr[tzinfo_pos + 1 :] ) tzoffset = tzsign * ((tzhour * 3600) + (tzminute * 60) + tzsecond) tzmicrosecond *= tzsign return hour, minute, second, microsecond, tzoffset, tzmicrosecond def _format_ctime( year: int, month: int, day: int, hours: int, minutes: int, seconds: int ) -> str: DAY_NAMES = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") MONTH_NAMES = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ) wday = _weekday(year, month, day) return f"{DAY_NAMES[wday]} {MONTH_NAMES[month - 1]} {str(day).rjust(2)} {str(hours).zfill(2)}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)} {str(year).zfill(4)}" def _utc_to_seconds( year: int, month: int, day: int, hour: int, minute: int, second: int ) -> int: if year < MINYEAR or year > MAXYEAR: raise ValueError(f"year {year} is out of range") ordinal = _ymd_to_ord(year, month, day) return ((ordinal * 24 + hour) * 60 + minute) * 60 + second def _round_half_even(x: float) -> float: from math import fabs rounded = x.__round__() if fabs(x - rounded) == 0.5: rounded = 2.0 * (x / 2.0).__round__() return rounded ################ # core classes # ################ @tuple class timedelta: min: ClassVar[timedelta] = timedelta._new(-9223372036854775808) max: ClassVar[timedelta] = timedelta._new(9223372036854775807) resolution: ClassVar[timedelta] = timedelta(microseconds=1) _microseconds: int def _new(microseconds: int) -> timedelta: return (microseconds,) @inline def _accum(sofar: int, leftover: float, num: int, factor: int) -> Tuple[int, float]: sofar += num * factor return sofar, leftover @inline def _accum( sofar: int, leftover: float, num: float, factor: int ) -> Tuple[int, float]: from math import modf fracpart, intpart = modf(num) prod = int(intpart) * factor s = sofar + prod if fracpart == 0.0: return s, leftover dnum = factor * fracpart fracpart, intpart = modf(dnum) y = s + int(intpart) return y, leftover + fracpart # override default constructor def __new__(days: int) -> timedelta: return timedelta(days, 0) def __new__( days: float = 0, seconds: float = 0, microseconds: float = 0, milliseconds: float = 0, minutes: float = 0, hours: float = 0, weeks: float = 0, ) -> timedelta: us = 0 leftover = 0.0 us, leftover = timedelta._accum(us, leftover, days, 24 * 60 * 60 * 1000000) us, leftover = timedelta._accum(us, leftover, seconds, 1000000) us, leftover = timedelta._accum(us, leftover, microseconds, 1) us, leftover = timedelta._accum(us, leftover, milliseconds, 1000) us, leftover = timedelta._accum(us, leftover, minutes, 60 * 1000000) us, leftover = timedelta._accum(us, leftover, hours, 60 * 60 * 1000000) us, leftover = timedelta._accum(us, leftover, weeks, 7 * 24 * 60 * 60 * 1000000) if leftover: from math import fabs whole_us = leftover.__round__() if fabs(whole_us - leftover) == 0.5: is_odd = us & 1 whole_us = 2.0 * ((leftover + is_odd) * 0.5).__round__() - is_odd us += int(whole_us) return (us,) @property def days(self) -> int: days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds) return days @property def seconds(self) -> int: days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds) return seconds @property def microseconds(self) -> int: days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds) return microseconds def __repr__(self) -> str: days, seconds, microseconds = _normalize_d_s_us(0, 0, self._microseconds) if days == 0 and seconds == 0 and microseconds == 0: return "timedelta(0)" v = [] if days: v.append(f"days={days}") if seconds: v.append(f"seconds={seconds}") if microseconds: v.append(f"microseconds={microseconds}") return f"timedelta({', '.join(v)})" def __str__(self) -> str: days, seconds, us = _normalize_d_s_us(0, 0, self._microseconds) minutes, seconds = _divmod(seconds, 60) hours, minutes = _divmod(minutes, 60) if days: if us: return f"{days} day{'' if days == 1 or days == -1 else 's'}, {hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}.{str(us).zfill(6)}" else: return f"{days} day{'' if days == 1 or days == -1 else 's'}, {hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}" else: if us: return f"{hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}.{str(us).zfill(6)}" else: return f"{hours}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}" def __add__(self, other: timedelta) -> timedelta: return timedelta._new(self._microseconds + other._microseconds) def __sub__(self, other: timedelta) -> timedelta: return timedelta._new(self._microseconds - other._microseconds) def __mul__(self, other: int) -> timedelta: return timedelta._new(self._microseconds * other) def __rmul__(self, other: int) -> timedelta: return self * other def __mul__(self, other: float) -> timedelta: return timedelta._new(int(_round_half_even(self._microseconds * other))) def __rmul__(self, other: float) -> timedelta: return self * other def __truediv__(self, other: timedelta) -> float: return self._microseconds / other._microseconds def __truediv__(self, other: float) -> timedelta: return timedelta._new(int(_round_half_even(self._microseconds / other))) def __truediv__(self, other: int) -> timedelta: return self / float(other) def __floordiv__(self, other: timedelta) -> int: return int((self._microseconds / other._microseconds).__floor__()) def __floordiv__(self, other: int) -> timedelta: return timedelta._new(self._microseconds // other) def __mod__(self, other: timedelta) -> timedelta: n = self._microseconds M = other._microseconds m = self._microseconds % other._microseconds return timedelta._new(((n % M) + M) % M) def __divmod__(self, other: timedelta) -> Tuple[int, timedelta]: return self // other, self % other def __pos__(self) -> timedelta: return self def __neg__(self) -> timedelta: return timedelta._new(-self._microseconds) def __abs__(self) -> timedelta: return timedelta._new(abs(self._microseconds)) def __eq__(self, other: timedelta) -> bool: return self._microseconds == other._microseconds def __ne__(self, other: timedelta) -> bool: return self._microseconds != other._microseconds def __lt__(self, other: timedelta) -> bool: return self._microseconds < other._microseconds def __le__(self, other: timedelta) -> bool: return self._microseconds <= other._microseconds def __gt__(self, other: timedelta) -> bool: return self._microseconds > other._microseconds def __ge__(self, other: timedelta) -> bool: return self._microseconds >= other._microseconds def __bool__(self) -> bool: return bool(self._microseconds) def total_seconds(self) -> float: return self._microseconds / 1e6 @tuple class IsoCalendarDate: year: int week: int weekday: int def __repr__(self) -> str: return f"IsoCalendarDate(year={self.year}, week={self.week}, weekday={self.weekday})" @tuple class date: min: ClassVar[date] = date(MINYEAR, 1, 1) max: ClassVar[date] = date(MAXYEAR, 12, 31) resolution: ClassVar[timedelta] = timedelta(days=1) _value: UInt[32] def __new__(year: int, month: int, day: int) -> date: _check_date_args(year, month, day) v = (year << 16) | (month << 8) | day return date(UInt[32](v)) @property def year(self) -> int: v = int(self._value) return v >> 16 @property def month(self) -> int: v = int(self._value) return (v >> 8) & 0xFF @property def day(self) -> int: v = int(self._value) return v & 0xFF def __repr__(self) -> str: return f"date(year={self.year}, month={self.month}, day={self.day})" def today() -> date: from time import time as ttime return date.fromtimestamp(ttime()) def fromtimestamp(timestamp) -> date: ts = int(timestamp) tm = localtime(ts) return date(tm.tm_year, tm.tm_mon, tm.tm_mday) def fromordinal(ordinal: int) -> date: return date(*_ord_to_ymd(ordinal)) def fromisoformat(date_string: str) -> date: return date(*_parse_isoformat_date(date_string)) def fromisocalendar(year, week, day) -> date: if year < MINYEAR or year > MAXYEAR: raise ValueError(f"Year is out of range: {year}") if week <= 0 or week >= 53: out_of_range = True if week == 53: first_weekday = _weekday(year, 1, 1) if first_weekday == 3 or (first_weekday == 2 and _is_leap(year)): out_of_range = False if out_of_range: raise ValueError(f"Invalid week: {week}") if day <= 0 or day >= 8: raise ValueError(f"Invalid day: {day} (range is [1, 7])") day_1 = _iso_week1_monday(year) month = week day_offset = (month - 1) * 7 + day - 1 return date(*_ord_to_ymd(day_1 + day_offset)) def __add__(self, other: timedelta) -> date: days, seconds, microseconds = _normalize_d_s_us(0, 0, other._microseconds) day = self.day + days return date(*_normalize_date(self.year, self.month, day)) def __sub__(self, other: timedelta) -> date: days, seconds, microseconds = _normalize_d_s_us(0, 0, other._microseconds) day = self.day - days return date(*_normalize_date(self.year, self.month, day)) def __sub__(self, other: date) -> timedelta: left_ord = _ymd_to_ord(self.year, self.month, self.day) right_ord = _ymd_to_ord(other.year, other.month, other.day) return timedelta(days=left_ord - right_ord) def __eq__(self, other: date) -> bool: return self._value == other._value def __ne__(self, other: date) -> bool: return self._value != other._value def __lt__(self, other: date) -> bool: return self._value < other._value def __le__(self, other: date) -> bool: return self._value <= other._value def __gt__(self, other: date) -> bool: return self._value > other._value def __ge__(self, other: date) -> bool: return self._value >= other._value def __bool__(self) -> bool: return True def replace(self, year: int = -1, month: int = -1, day: int = -1) -> date: if year == -1: year = self.year if month == -1: month = self.month if day == -1: day = self.day return date(year, month, day) def timetuple(self) -> struct_time: yday = self.toordinal() - date(self.year, 1, 1).toordinal() + 1 return struct_time( self.year, self.month, self.day, 0, 0, 0, self.weekday(), yday, -1 ) def toordinal(self) -> int: return _ymd_to_ord(self.year, self.month, self.day) def weekday(self) -> int: return _weekday(self.year, self.month, self.day) def isoweekday(self) -> int: return self.weekday() + 1 def isocalendar(self) -> IsoCalendarDate: year = self.year week1_monday = _iso_week1_monday(year) today = _ymd_to_ord(year, self.month, self.day) week, day = _divmod(today - week1_monday, 7) if week < 0: year -= 1 week1_monday = _iso_week1_monday(year) week, day = _divmod(today - week1_monday, 7) elif week >= 52 and today >= _iso_week1_monday(year + 1): year += 1 week = 0 return IsoCalendarDate(year, week + 1, day + 1) def isoformat(self) -> str: return f"{str(self.year).zfill(4)}-{str(self.month).zfill(2)}-{str(self.day).zfill(2)}" def __str__(self) -> str: return self.isoformat() def ctime(self) -> str: return _format_ctime(self.year, self.month, self.day, 0, 0, 0) # strftime() / __format__() not supported @tuple class time: min: ClassVar[time] = time(0, 0, 0, 0) max: ClassVar[time] = time(23, 59, 59, 999999) resolution: ClassVar[timedelta] = timedelta(microseconds=1) _value: int def __new__( hour: int = 0, minute: int = 0, second: int = 0, microsecond: int = 0 ) -> time: _check_time_args(hour, minute, second, microsecond) v = (hour << 40) | (minute << 32) | (second << 24) | microsecond return (v,) @property def hour(self) -> int: v = self._value return v >> 40 @property def minute(self) -> int: v = self._value return (v >> 32) & 0xFF @property def second(self) -> int: v = self._value return (v >> 24) & 0xFF @property def microsecond(self) -> int: v = self._value return v & 0xFFFFFF def __repr__(self) -> str: h, m, s, us = self.hour, self.minute, self.second, self.microsecond v = [] v.append(f"hour={h}") v.append(f"minute={m}") if s or us: v.append(f"second={s}") if us: v.append(f"microsecond={us}") return f"time({', '.join(v)})" def __str__(self) -> str: return self.isoformat() def __bool__(self) -> bool: return True def fromisoformat(time_string: str) -> time: ( hour, minute, second, microsecond, tzoffset, tzmicrosecond, ) = _parse_isoformat_time(time_string) # TODO: deal with timezone return time(hour, minute, second, microsecond) def replace( self, hour: int = -1, minute: int = -1, second: int = -1, microsecond: int = -1 ) -> time: if hour == -1: hour = self.hour if second == -1: second = self.second if minute == -1: minute = self.minute if microsecond == -1: microsecond = self.microsecond return time(hour, minute, second, microsecond) def isoformat(self, timespec: Static[str] = "auto") -> str: hh = str(self.hour).zfill(2) mm = str(self.minute).zfill(2) ss = str(self.second).zfill(2) us = str(self.microsecond).zfill(6) ms = str(self.microsecond // 1000).zfill(3) if timespec == "auto": if self.microsecond: return f"{hh}:{mm}:{ss}.{us}" else: return f"{hh}:{mm}:{ss}" elif timespec == "hours": return hh elif timespec == "minutes": return f"{hh}:{mm}" elif timespec == "seconds": return f"{hh}:{mm}:{ss}" elif timespec == "milliseconds": return f"{hh}:{mm}:{ss}.{ms}" elif timespec == "microseconds": return f"{hh}:{mm}:{ss}.{us}" else: compile_error( "invalid timespec; valid ones are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'" ) @tuple class datetime: min: ClassVar[datetime] = datetime(MINYEAR, 1, 1) max: ClassVar[datetime] = datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999) resolution: ClassVar[timedelta] = timedelta(microseconds=1) _time: time _date: date def __new__( year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, microsecond: int = 0, ) -> datetime: return datetime(time(hour, minute, second, microsecond), date(year, month, day)) def date(self) -> date: return self._date def time(self) -> time: return self._time @property def year(self) -> int: return self.date().year @property def month(self) -> int: return self.date().month @property def day(self) -> int: return self.date().day @property def hour(self) -> int: return self.time().hour @property def minute(self) -> int: return self.time().minute @property def second(self) -> int: return self.time().second @property def microsecond(self) -> int: return self.time().microsecond def __repr__(self) -> str: return f"datetime(year={self.year}, month={self.month}, day={self.day}, hour={self.hour}, minute={self.minute}, second={self.second}, microsecond={self.microsecond})" def __str__(self) -> str: return self.isoformat(sep=" ") def _from_timet_and_us(timet, us) -> datetime: tm = localtime(timet) year = tm.tm_year month = tm.tm_mon day = tm.tm_mday hour = tm.tm_hour minute = tm.tm_min second = min(59, tm.tm_sec) # TODO: timezone adjustments return datetime(year, month, day, hour, minute, second, us) def today() -> datetime: from time import time as ttime return datetime.fromtimestamp(ttime()) # TODO: support timezone def now() -> datetime: return datetime.today() def utcnow() -> datetime: return datetime.now() # TODO: support timezone def fromtimestamp(timestamp) -> datetime: from time import _time_to_timeval, _ROUND_HALF_EVEN timet, us = _time_to_timeval(float(timestamp), _ROUND_HALF_EVEN) return datetime._from_timet_and_us(timet, us) def utcfromtimestamp(timestamp) -> datetime: return datetime.fromtimestamp(timestamp) def fromordinal(ordinal: int) -> datetime: return datetime.combine(date.fromordinal(ordinal), time()) # TODO: support timezone def combine(date: date, time: time) -> datetime: return datetime(time, date) def fromisoformat(date_string: str) -> datetime: time_string = "" if len(date_string) < 10 else date_string[:10] year, month, day = _parse_isoformat_date(time_string) if len(date_string) == 10: return datetime(year=year, month=month, day=day) date_string = "" if len(date_string) < 12 else date_string[11:] hour, minute, second, microsecond = _parse_hh_mm_ss_ff(date_string) return datetime( year=year, month=month, day=day, hour=hour, minute=minute, second=second, microsecond=microsecond, ) def fromisocalendar(year: int, week: int, day: int) -> datetime: return datetime.combine(date.fromisocalendar(year, week, day), time()) def __add__(self, other: timedelta) -> datetime: td_days, td_seconds, td_microseconds = _normalize_d_s_us( 0, 0, other._microseconds ) year = self.year month = self.month day = self.day + td_days hour = self.hour minute = self.minute second = self.second + td_seconds microsecond = self.microsecond + td_microseconds return datetime( *_normalize_datetime(year, month, day, hour, minute, second, microsecond) ) def __sub__(self, other: timedelta) -> datetime: td_days, td_seconds, td_microseconds = _normalize_d_s_us( 0, 0, other._microseconds ) year = self.year month = self.month day = self.day - td_days hour = self.hour minute = self.minute second = self.second - td_seconds microsecond = self.microsecond - td_microseconds return datetime( *_normalize_datetime(year, month, day, hour, minute, second, microsecond) ) def __sub__(self, other: datetime) -> timedelta: delta_d = _ymd_to_ord(self.year, self.month, self.day) - _ymd_to_ord( other.year, other.month, other.day ) delta_s = ( (self.hour - other.hour) * 3600 + (self.minute - other.minute) * 60 + (self.second - other.second) ) delta_us = self.microsecond - other.microsecond return timedelta(days=delta_d, seconds=delta_s, microseconds=delta_us) def __eq__(self, other: datetime) -> bool: return self.date() == other.date() and self.time() == other.time() def __ne__(self, other: datetime) -> bool: return not (self == other) def __lt__(self, other: datetime) -> bool: return (self.date(), self.time()) < (other.date(), other.time()) def __le__(self, other: datetime) -> bool: return (self.date(), self.time()) <= (other.date(), other.time()) def __gt__(self, other: datetime) -> bool: return (self.date(), self.time()) > (other.date(), other.time()) def __ge__(self, other: datetime) -> bool: return (self.date(), self.time()) >= (other.date(), other.time()) def __bool__(self) -> bool: return True def replace( self, year: int = -1, month: int = -1, day: int = -1, hour: int = -1, minute: int = -1, second: int = -1, microsecond: int = -1, ) -> datetime: return datetime( self.time().replace(hour, minute, second, microsecond), self.date().replace(year, month, day), ) def timetuple(self) -> struct_time: yday = self.toordinal() - date(self.year, 1, 1).toordinal() + 1 return struct_time( self.year, self.month, self.day, self.hour, self.minute, self.second, self.weekday(), yday, -1, ) def utctimetuple(self) -> struct_time: return self.timetuple() def toordinal(self) -> int: return self.date().toordinal() def timestamp(self) -> float: return (self - datetime(1970, 1, 1)).total_seconds() def weekday(self) -> int: return self.date().weekday() def isoweekday(self) -> int: return self.date().isoweekday() def isocalendar(self) -> IsoCalendarDate: return self.date().isocalendar() def isoformat(self, sep: str = "T", timespec: Static[str] = "auto") -> str: date_part = str(self.date()) time_part = self.time().isoformat(timespec=timespec) return f"{date_part}{sep}{time_part}" def ctime(self) -> str: date = self.date() time = self.time() return _format_ctime( date.year, date.month, date.day, time.hour, time.minute, time.second ) @extend class timedelta: def __add__(self, other: date) -> date: return other + self def __add__(self, other: datetime) -> datetime: return other + self