mirror of https://github.com/exaloop/codon.git
1115 lines
34 KiB
Python
1115 lines
34 KiB
Python
# Copyright (C) 2022-2023 Exaloop Inc. <https://exaloop.io>
|
|
# 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
|