639 lines
22 KiB
Python
639 lines
22 KiB
Python
#!/usr/bin/env python
|
|
|
|
import math
|
|
import re
|
|
from dataclasses import KW_ONLY, dataclass
|
|
from datetime import datetime, timedelta, timezone, tzinfo
|
|
from typing import ClassVar
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from subprocess import Popen, PIPE
|
|
|
|
from ._ansii_ import cl
|
|
from ._obj_ import Obj
|
|
from ._typing_ import *
|
|
from ._basic_ import Enumer
|
|
|
|
"""
|
|
strftime
|
|
https://manpages.debian.org/bookworm/manpages-dev/strftime.3.en.html
|
|
|
|
| fmt | Meaning | Example |
|
|
| --- | ------------------------------------------------------------------- | ------------------------------------ |
|
|
| %a | Weekday as locale's abbreviated name. | Sun, Mon, ..., Sat |
|
|
| %A | Weekday as locale's full name. | Sunday, Monday, ..., |
|
|
| %w | Weekday as a decimal number. 0=Sun and 6=sat | 0, 1, ..., 6 |
|
|
| %d | Day of the month as a zero-padded decimal number. | 01, 02, ..., 31 |
|
|
| %b | Month as locale's abbreviated name. | Jan, Feb, ..., Dec |
|
|
| %B | Month as locale's full name. | January, February, ..., |
|
|
| %m | Month as a zero-padded decimal number. | 01, 02, ..., 12 |
|
|
| %y | Year without century as a zero- padded decimal number. | 00, 01, ..., 99 |
|
|
| %Y | Year with century as a decimal number. | 0001, 0002, ..., 2013, |
|
|
| %H | Hour (24-hour clock) as a zero- padded decimal number. | 00, 01, ..., 23 |
|
|
| %I | Hour (12-hour clock) as a zero- padded decimal number. | 01, 02, ..., 12 |
|
|
| %p | Locale's equivalent of either AM or PM. | AM, PM (en_US); am, pm |
|
|
| %M | Minute as a zero-padded decimal number. | 00, 01, ..., 59 |
|
|
| %S | Second as a zero-padded decimal number. | 00, 01, ..., 59 |
|
|
| %f | Microsecond as a decimal number, zero-padded to 6 digits. | 000000, 000001, ..., |
|
|
| %z | UTC offset in the form "±HHMM[SS[.ffffff]]" | (empty), +0000, -0400, |
|
|
| %Z | Time zone name (empty string if the object is naive). | (empty), UTC, GMT |
|
|
| %j | Day of the year as a zero-padded decimal number. | 001, 002, ..., 366 |
|
|
| %U | Week # of the year (first day is Sunday ). zero-padd decimal number | 00, 01, ..., 53 |
|
|
| %W | Week # of the year (first day is Monday ). zero-padd decimal number | 00, 01, ..., 53 |
|
|
| %c | Locale's appropriate date and time representation. | Tue Aug 16 21:30:00 1988 |
|
|
| %x | Locale's appropriate date representation. | 08/16/88 (None); 08/16/1988 (en_US); |
|
|
| %X | Locale's appropriate time representation. | 21:30:00 (en_US); |
|
|
| %% | A literal "'%'" character. | % |
|
|
| %G | ISO 8601 year with century with year contain the > ISO week ("%V"). | 0001, 0002, ..., 2013, |
|
|
| %u | ISO 8601 weekday as a decimal number where 1 is Monday. | 1, 2, ..., 7 |
|
|
| %V | ISO 8601 week as a decimal number | 01, 02, ..., 53 |
|
|
"""
|
|
regex_timestamp = re.compile(r'^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})')
|
|
|
|
# | because `datetime(1970, 1, 1, tzinfo=timezone.utc).strftime("%s") != 0`
|
|
epochNormalize = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
|
|
daysInYear = 365.242086145121
|
|
|
|
|
|
@dataclass(slots=T, kw_only=T, frozen=F)
|
|
class TimeUnit:
|
|
ms: float | int = 0
|
|
s: float | int = 0
|
|
m: float | int = 0
|
|
h: float | int = 0
|
|
d: float | int = 0
|
|
# TODO if it's a percent or a while number i.e. is .999999 ms == 99.9999s or 1000/999999 like a percent of 1 second?
|
|
|
|
__add__ = Obj.__add__
|
|
__sub__ = Obj.__sub__
|
|
__mul__ = Obj.__mul__
|
|
__lt__ = Obj.__lt__
|
|
__le__ = Obj.__le__
|
|
__eq__ = Obj.__eq__
|
|
__ne__ = Obj.__ne__
|
|
__gt__ = Obj.__gt__
|
|
__ge__ = Obj.__ge__
|
|
toJson = Obj.toJson
|
|
|
|
def __math__(_, oper, func, other) -> Self:
|
|
return func(_.toSec(), other.toSec())
|
|
|
|
@classmethod
|
|
def FromString(cls, timeStr: str) -> Self:
|
|
tu = cls()
|
|
parse = timeStr[::-1]
|
|
parse = re.compile(r'[\:\-]|[0-9\.]+|[a-zA-Z]+|\s+|.*').findall(parse)
|
|
parse = [x[::-1].lower() for x in parse]
|
|
unit = None
|
|
i = 0
|
|
# print('========', parse)
|
|
while i < len(parse):
|
|
x = parse[i]
|
|
# print('==', x.encode(), res)
|
|
|
|
if re.compile(r'^\s*$').search(x) or x in [':', '-']:
|
|
# print('skip')
|
|
...
|
|
elif re.compile(r'^[a-zA-Z]+$').search(x):
|
|
# print('set unit', x)
|
|
unit = x
|
|
elif re.compile(r'^[0-9\.]+$').search(x):
|
|
x = float(x)
|
|
# print('set scaler', x)
|
|
if not unit:
|
|
for u in ['s', 'm', 'h', 'd']:
|
|
if not unit and not getattr(tu, u):
|
|
unit = u
|
|
# print('set unit', u)
|
|
if unit:
|
|
if getattr(tu, unit):
|
|
raise ValueError(f'{unit} already in {tu}')
|
|
setattr(tu, unit, x)
|
|
else:
|
|
raise ValueError(f'{parse}[{i}] == {x} | No unit for')
|
|
unit = None
|
|
else:
|
|
raise ValueError(f'{parse}[{i}] == {x.encode()} | could not parse')
|
|
i = i + 1
|
|
return tu
|
|
|
|
def toSec(_) -> float:
|
|
return (_.ms + _.s * 1000 + _.m * 1000 * 60 + _.h * 1000 * 60 * 60 + _.d * 1000 * 60 * 60 * 24) / 1000
|
|
|
|
|
|
@dataclass(slots=True, kw_only=True, frozen=False, repr=F)
|
|
class TimeDelta:
|
|
seconds: int | float = field(kw_only=F, repr=T)
|
|
|
|
__add__ = Obj.__add__
|
|
__sub__ = Obj.__sub__
|
|
__mul__ = Obj.__mul__
|
|
__lt__ = Obj.__lt__
|
|
__le__ = Obj.__le__
|
|
__eq__ = Obj.__eq__
|
|
__ne__ = Obj.__ne__
|
|
__gt__ = Obj.__gt__
|
|
__ge__ = Obj.__ge__
|
|
toJson = Obj.toJson
|
|
|
|
@property
|
|
def td(_) -> None:
|
|
return timedelta(seconds=_.seconds)
|
|
|
|
def __post_init__(_) -> None:
|
|
if isinstance(_.seconds, TimeDelta): # TODO typeMap
|
|
_.seconds = _.seconds.seconds
|
|
elif not isinstance(_.seconds, int | float):
|
|
_.seconds = _.seconds.total_seconds()
|
|
|
|
@classmethod
|
|
def FromUnits(cls, **kargs) -> Self:
|
|
return cls(timedelta(**kargs))
|
|
|
|
@classmethod
|
|
def FromString(cls, timeStr: str) -> Self:
|
|
return cls(TimeUnit.FromString(timeStr).toSec())
|
|
|
|
def toStr(_) -> str:
|
|
return str(FormatSeconds(_.seconds))
|
|
|
|
def __repr__(_) -> str:
|
|
return str(FormatSeconds(_.seconds).toStr(color=F))
|
|
|
|
__str__ = toStr
|
|
|
|
def __math__(_, oper, func, other) -> Self:
|
|
if isinstance(other, TimeDelta):
|
|
other = other.seconds
|
|
elif isinstance(other, timedelta):
|
|
other = other.total_seconds()
|
|
res = func(_.seconds, other)
|
|
if isinstance(res, bool): # TODO typeMap
|
|
return res
|
|
elif isinstance(res, timedelta | float | int):
|
|
return TimeDelta(res)
|
|
return res
|
|
|
|
def wait(_):
|
|
from ._timeUtils_ import countDownTimer
|
|
|
|
countDownTimer(_)
|
|
|
|
def asMilliSec(_) -> float:
|
|
return _.seconds * 1000
|
|
|
|
def asSecs(_) -> float:
|
|
return _.seconds
|
|
|
|
def asMins(_) -> float:
|
|
return _.seconds / 60
|
|
|
|
def asHours(_) -> float:
|
|
return _.seconds / 60 / 60
|
|
|
|
def asDays(_) -> float:
|
|
return _.seconds / 60 / 60 / 24
|
|
|
|
def asWeeks(_) -> float:
|
|
return _.seconds / 60 / 60 / 24 / 7
|
|
|
|
def asMonths(_) -> float:
|
|
return _.seconds / 60 / 60 / 24 / 7 / (365 / 13)
|
|
|
|
def asYears(_) -> float:
|
|
return _.seconds / 60 / 60 / 24 / 7 / 365
|
|
|
|
@property
|
|
def secs(_):
|
|
return _.asSecs()
|
|
|
|
@property
|
|
def mins(_):
|
|
return _.asMins()
|
|
|
|
@property
|
|
def hours(_):
|
|
return _.asHours()
|
|
|
|
@property
|
|
def days(_):
|
|
return _.asDays()
|
|
|
|
@property
|
|
def weeks(_):
|
|
return _.asWeeks()
|
|
|
|
@property
|
|
def months(_):
|
|
return _.asMonths()
|
|
|
|
@property
|
|
def years(_):
|
|
return _.asYears()
|
|
|
|
|
|
# zoneinfo.available_timezones()
|
|
@dataclass(slots=True, kw_only=False, frozen=False)
|
|
class TimeZone(tzinfo, Obj):
|
|
offset: timedelta
|
|
__: KW_ONLY
|
|
name: str | None = None
|
|
|
|
__add__ = Obj.__add__
|
|
__sub__ = Obj.__sub__
|
|
__mul__ = Obj.__mul__
|
|
__lt__ = Obj.__lt__
|
|
__le__ = Obj.__le__
|
|
__eq__ = Obj.__eq__
|
|
__ne__ = Obj.__ne__
|
|
__gt__ = Obj.__gt__
|
|
__ge__ = Obj.__ge__
|
|
toJson = Obj.toJson
|
|
|
|
def __post_init__(_):
|
|
if not isinstance(_.offset, None | timedelta): # TODO typeMap
|
|
_.offset = _.offset.td
|
|
|
|
# print(repr(_.offset))
|
|
# _.offset = typeMap(_.offset, {
|
|
# None | timedelta: lambda: _.offset.td,
|
|
# }, default=lambda: _.offset)()
|
|
|
|
def utcoffset(_, dt):
|
|
return _.offset
|
|
|
|
def dst(_, dt):
|
|
return _.offset
|
|
|
|
def tzname(_, dt):
|
|
return _.name if _.name else str(_.offset)
|
|
|
|
def __math__(_, other, func, oper) -> bool:
|
|
if isinstance(other, TimeZone):
|
|
return func(_.offset, other.offset)
|
|
elif isinstance(other, timedelta):
|
|
return func(_.offset, other)
|
|
else:
|
|
raise NotImplementedError(str(type(other)))
|
|
|
|
@staticmethod
|
|
def offsetFromDateTime(dt: datetime) -> TimeDelta:
|
|
return TimeZone.offsetFromString(dt.strftime('%z'))
|
|
|
|
@staticmethod
|
|
def offsetFromString(offset: str) -> TimeDelta:
|
|
neg = -1 if offset.startswith('-') else 1
|
|
return TimeDelta(timedelta(hours=neg * int(offset[1:3]), minutes=int(offset[3:5])))
|
|
|
|
@staticmethod
|
|
def localOffset() -> TimeDelta:
|
|
return TimeZone.Offset(zone=None)
|
|
|
|
@staticmethod
|
|
def Offset(zone: tzinfo) -> TimeDelta:
|
|
a = datetime.now(timezone.utc)
|
|
b = a.replace(tzinfo=zone)
|
|
c = a.astimezone(zone).replace(tzinfo=zone)
|
|
# print((b - c).total_seconds() / 60 / 60)
|
|
return TimeDelta(-1 * (b - c))
|
|
|
|
def toStr(_):
|
|
return f'{_.name}+{_.offset}' if _.name else f'{_.offset}'
|
|
|
|
__str__ = toStr
|
|
|
|
|
|
class FuzzyTimeParseError(Exception):
|
|
__slot__ = ()
|
|
|
|
|
|
Time = TypeVar('Time')
|
|
|
|
|
|
class TimeFormat(Enumer):
|
|
ymdhms = '%Y-%m-%d_%H-%M-%S'
|
|
ISO8601NoSep = '%Y%m%dT%H%M%S'
|
|
ymdhmsNoSpace = '%y%m%d%H%M%S'
|
|
date = '%Y-%m-%d'
|
|
time = '%H:%M:%S'
|
|
pretty = '%a %d %b %y %H:%M:%S'
|
|
prettyDay = '%a %d %b %y'
|
|
ymdhmsfDash = '%Y-%m-%d-%H-%M-%S-%f'
|
|
|
|
|
|
@dataclass(slots=True, kw_only=False, frozen=False)
|
|
class Time:
|
|
# TODO Time().epoch() - time.now()
|
|
dt: datetime = None
|
|
__: KW_ONLY
|
|
fmt: str = 'default'
|
|
LocalTz: ClassVar = TimeZone(TimeZone.localOffset())
|
|
tz: TimeZone = Undefined
|
|
autoAddTzStr: bool = True # only effects __str__
|
|
TZ: ClassVar[Type] = TimeZone
|
|
TD: ClassVar[Type] = TimeDelta
|
|
TU: ClassVar[Type] = TimeUnit
|
|
|
|
# TODO when getting dt the timezone is TimeZone and not timezone.utc type date = date.replace(tzinfo=timezone.utc)
|
|
# TODO so dataframes fail on TimeZone type
|
|
Fmt = TimeFormat
|
|
|
|
__add__ = Obj.__add__
|
|
__sub__ = Obj.__sub__
|
|
__mul__ = Obj.__mul__
|
|
__lt__ = Obj.__lt__
|
|
__le__ = Obj.__le__
|
|
__eq__ = Obj.__eq__
|
|
__ne__ = Obj.__ne__
|
|
__gt__ = Obj.__gt__
|
|
__ge__ = Obj.__ge__
|
|
|
|
def __hash__(_):
|
|
return hash(_.dt)
|
|
|
|
def __post_init__(_):
|
|
# TODO auto convert chars to %char
|
|
if _.fmt == 'default' or _.fmt == 'ymdhms':
|
|
_.fmt = TimeFormat.ymdhms
|
|
elif _.fmt == 'pretty':
|
|
_.fmt = TimeFormat.pretty
|
|
if _.dt is None:
|
|
_.dt = datetime.now(Time.LocalTz)
|
|
_._normalizeTz()
|
|
|
|
def _normalizeTz(_):
|
|
isDst = False if _.dt.tzinfo else True
|
|
if isinstance(_.tz, str) and _.tz.lower() == 'utc':
|
|
_.tz = timezone.utc
|
|
if _.tz is None:
|
|
_.dt = Time.ToTimeZone(_, Time.LocalTz, isDst=isDst)
|
|
elif _.tz is Undefined:
|
|
_.dt = Time.ToTimeZone(_, _.dt.tzinfo if _.dt.tzinfo else Time.LocalTz, isDst=isDst)
|
|
elif _.tz:
|
|
_.dt = Time.ToTimeZone(_, _.tz, isDst=isDst)
|
|
else:
|
|
raise NotImplementedError('')
|
|
_.tz = _.dt.tzinfo
|
|
assert isinstance(_.dt.tzinfo, TimeZone), (type(_.dt.tzinfo), _.dt.tzinfo) # TODO typeMap
|
|
assert isinstance(_.tz, TimeZone), (type(_.tz), _.tz)
|
|
|
|
def toTz(_, dst: timedelta | TimeZone | str, isDst: bool) -> Time:
|
|
return Time(Time.ToTimeZone(_, dst=dst, isDst=isDst))
|
|
|
|
@staticmethod
|
|
def ToTimeZone(
|
|
src: datetime | Time,
|
|
dst: timedelta | TimeZone | str,
|
|
isDst: bool,
|
|
) -> datetime:
|
|
src = src.dt if isinstance(src, Time) else src # TODO typeMap
|
|
dst = ZoneInfo(dst) if isinstance(dst, str) else dst
|
|
dst = TimeZone(dst) if isinstance(dst, TimeDelta) else dst
|
|
|
|
assert isinstance(src, datetime)
|
|
srcTz = src.tzinfo
|
|
srcNoTz = src.replace(tzinfo=None)
|
|
srcOffset = TimeZone.Offset(srcTz)
|
|
|
|
if not isinstance(dst, timedelta): # TODO typeMap
|
|
dstOffset = TimeZone.Offset(dst)
|
|
|
|
tzinfo = TimeZone(offset=dstOffset, name=str(dst))
|
|
|
|
if isDst:
|
|
return srcNoTz.replace(tzinfo=tzinfo)
|
|
|
|
diff = dstOffset - srcOffset
|
|
return (src + diff.td).replace(tzinfo=tzinfo)
|
|
|
|
def toStr(_, fmt: str = None):
|
|
fmt = fmt if fmt else _.fmt
|
|
if _.autoAddTzStr and _.tz != Time.LocalTz and '%Z' not in _.fmt and '%Z' not in _.fmt:
|
|
fmt = _.fmt + '%z'
|
|
return _.dt.strftime(fmt)
|
|
|
|
__str__ = toStr
|
|
toJson = toStr
|
|
|
|
@property
|
|
def s(_) -> str:
|
|
return _.toStr()
|
|
|
|
def child(_, frm: datetime | int | float) -> Time:
|
|
if isinstance(frm, int | float):
|
|
frm = datetime.fromtimestamp(frm)
|
|
assert isinstance(frm, datetime)
|
|
res = Obj.getObjAsDict(_) | dict(dt=frm)
|
|
return Time(**res)
|
|
|
|
def __math__(_, oper: str, func: Callable, other: int | float | TimeDelta | timedelta) -> Any:
|
|
if oper in ['add', 'sub']:
|
|
# TODO typeMap
|
|
other = other.td if isinstance(other, TimeDelta) else other
|
|
other = timedelta(seconds=other) if isinstance(other, int | float) else other
|
|
other = timedelta(**other) if isinstance(other, dict) else other
|
|
other = other.dt if isinstance(other, Time) else other
|
|
res = func(_.dt, other)
|
|
else:
|
|
# TODO typeMap
|
|
other = other.epoch() if isinstance(other, Time) else other
|
|
other = Time.epochFromDatetime(other) if isinstance(other, datetime) else other
|
|
res = func(_.epoch(), other)
|
|
if isinstance(res, datetime):
|
|
return _.child(frm=res)
|
|
elif isinstance(res, timedelta):
|
|
return TimeDelta(res)
|
|
return res
|
|
|
|
@staticmethod
|
|
def epochFromDatetime(dt: datetime):
|
|
return (dt.astimezone(timezone.utc) - epochNormalize).total_seconds()
|
|
|
|
def epoch(_):
|
|
return Time.epochFromDatetime(_.dt)
|
|
|
|
def startOfDay(_) -> Time:
|
|
dd = _.dt
|
|
dd = dd - timedelta(hours=dd.hour, minutes=dd.minute, seconds=dd.second, microseconds=dd.microsecond)
|
|
return _.child(frm=dd)
|
|
|
|
def waitUntil(_):
|
|
from ._timeUtils_ import countDownTimer
|
|
|
|
countDownTimer(_ - Time())
|
|
|
|
def add(
|
|
_, d: float = 0, h: float = 0, m: float = 0, s: float = 0, ms: float = 0, mls: float = 0, w: float = 0
|
|
) -> Time:
|
|
td = timedelta(days=d, seconds=s, microseconds=ms, milliseconds=mls, minutes=m, hours=h, weeks=w)
|
|
return _.child(frm=_.dt + td)
|
|
|
|
def replace(_, y=None, M=None, d=None, h=None, m=None, s=None, ms=None, tz=None):
|
|
return _.child(
|
|
frm=datetime(
|
|
year=y if y is not None else _.dt.year,
|
|
month=M if M is not None else _.dt.month,
|
|
day=d if d is not None else _.dt.day,
|
|
hour=h if h is not None else _.dt.hour,
|
|
minute=m if m is not None else _.dt.minute,
|
|
second=s if s is not None else _.dt.second,
|
|
microsecond=ms if ms is not None else _.dt.microsecond,
|
|
tzinfo=tz if tz is not None else _.dt.tzinfo,
|
|
)
|
|
)
|
|
|
|
def yearMonthDayStr(_, sep='-') -> str:
|
|
"{year}-{month}-{day}"
|
|
return _.toStr(f'%Y{sep}%m{sep}%d')
|
|
|
|
def hourMinSecStr(_, sep='-') -> str:
|
|
"{hours}-{min}-{sec}"
|
|
return _.toStr(f'%H{sep}%M{sep}%S')
|
|
|
|
def yearMonthDayHourMinSecStr(_, join='_', sep='-') -> str:
|
|
"{year}-{month}-{day}_{hours}-{min}-{sec}"
|
|
return _.toStr(f'%Y{sep}%m{sep}%d{join}%H{sep}%M{sep}%S')
|
|
|
|
@property
|
|
def ymd(_):
|
|
return _.yearMonthDayStr()
|
|
|
|
@property
|
|
def hms(_):
|
|
return _.hourMinSecStr()
|
|
|
|
@property
|
|
def ymdhms(_):
|
|
return _.yearMonthDayHourMinSecStr()
|
|
|
|
@staticmethod
|
|
def __sheet_fmt__(column: '_Column'):
|
|
pass
|
|
|
|
return column.sheet.sheetFormat.datetime(column)
|
|
|
|
__sheet_align__: ClassVar = '>'
|
|
|
|
@classmethod
|
|
def FromFuzzy(cls, date: str, **kargs) -> Time:
|
|
return cls(cls.FuzzyToDatetime(date=date), **kargs)
|
|
|
|
@staticmethod
|
|
def FuzzyToDatetime(date: str) -> datetime:
|
|
# TODO next/prev using timedelta(). if next and before now then + timedelta()
|
|
date = date.replace('@', ' ')
|
|
popen = Popen(['date', '--date', date, '+%y%m%d%H%M%S%z'], stdout=PIPE, stderr=PIPE, env={})
|
|
out, err = popen.communicate()
|
|
if popen.returncode != 0:
|
|
raise FuzzyTimeParseError(err.split(b'\n'))
|
|
out = out.decode('utf8').strip('\n')
|
|
|
|
# , tz=datetime.now().astimezone().tzinfo
|
|
return datetime.strptime(out, '%y%m%d%H%M%S%z')
|
|
|
|
def fuzzyTimeDelta(_, fuzzy: str):
|
|
curStr = _.toStr(fmt='%c')
|
|
return _.child(frm=_.__class__.FuzzyToDatetime(f'{curStr} {fuzzy}'))
|
|
|
|
@classmethod
|
|
def FromStr(cls, date: str, fmt: str, **kargs) -> Time:
|
|
return cls(datetime.strptime(date, fmt), **kargs)
|
|
|
|
@classmethod
|
|
def FromUnits(cls, *arg, tz, **kargs) -> Time:
|
|
arg = (int(x) for x in arg)
|
|
if tz is T:
|
|
tz = cls.LocalTz
|
|
elif tz is F:
|
|
tz = timezone.utc
|
|
return cls(datetime(*arg, tzinfo=tz), **kargs)
|
|
|
|
@classmethod
|
|
def FromEpoch(cls, epoch: int | float, **kargs) -> str:
|
|
tz = datetime.now().astimezone().tzinfo
|
|
dt = datetime.fromtimestamp(epoch, tz=tz)
|
|
return cls(dt, **kargs)
|
|
|
|
@property
|
|
def year(_):
|
|
return _.dt.year
|
|
|
|
@property
|
|
def month(_):
|
|
return _.dt.month
|
|
|
|
@property
|
|
def day(_):
|
|
return _.dt.day
|
|
|
|
@property
|
|
def hour(_):
|
|
return _.dt.hour
|
|
|
|
@property
|
|
def minute(_):
|
|
return _.dt.minute
|
|
|
|
@property
|
|
def second(_):
|
|
return _.dt.second
|
|
|
|
|
|
# https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds
|
|
# For interval units other than seconds, use the division form directly (e.g. td / timedelta(microseconds=1)).
|
|
@dataclass(slots=True, kw_only=False, frozen=False)
|
|
class FormatSeconds:
|
|
totalSec: float | int = 0
|
|
outputFmt: str = 'YDHMS' # TODO
|
|
__: KW_ONLY = KW_ONLY
|
|
units: ClassVar = ['s', 'm', 'h', 'd', 'y']
|
|
times: ClassVar = [60, 60, 24, 365, 9999]
|
|
timesCompound: ClassVar = [1, 60, 60 * 60, 60 * 60 * 24, 60 * 60 * 24 * 365]
|
|
lens: ClassVar = [2, 2, 2, 1, 1]
|
|
|
|
@staticmethod
|
|
def listToSec(a):
|
|
return sum([x * y for x, y in zip(a, FormatSeconds.timesCompound, strict=false)])
|
|
|
|
def __post_init__(_):
|
|
if isinstance(_.totalSec, timedelta): # TODO typeMap
|
|
_.totalSec = _.totalSec.total_seconds()
|
|
elif isinstance(_.totalSec, TimeDelta):
|
|
_.totalSec = _.totalSec.td.seconds
|
|
elif isinstance(_.totalSec, Iterable):
|
|
_.totalSec = FormatSeconds.listToSec(_.totalSec)
|
|
assert isinstance(_.totalSec, int | float)
|
|
|
|
def inc(_, amt: int | float | Iterable = 0, /):
|
|
if isinstance(amt, Iterable):
|
|
amt = FormatSeconds.listToSec(_.totalSec)
|
|
_.totalSec += amt
|
|
return _
|
|
|
|
def toStr(_, color: bool = T, units: bool = T):
|
|
neg = f'{cl.wht if color else ""}-{cl.r if color else ""}' if _.totalSec < 0 else ''
|
|
totalSec = _.totalSec * -1 if neg else _.totalSec
|
|
unitCast = []
|
|
|
|
for i in FormatSeconds.times:
|
|
add, totalSec = totalSec % i, math.floor(totalSec / i)
|
|
unitCast.append(add)
|
|
|
|
zeroIndex = len(unitCast) # TODO optomize
|
|
while zeroIndex > 0 and unitCast[zeroIndex - 1] == 0:
|
|
zeroIndex -= 1
|
|
unitCast = unitCast[:zeroIndex]
|
|
|
|
if not unitCast:
|
|
return f'{cl.yel if color else ""}00{cl.res if color else ""}{cl.grn if color else ""}{FormatSeconds.units[0].upper()}{cl.res if color else ""}'
|
|
|
|
elem = [
|
|
f'{cl.yel if color else ""}{int(unitCast[x]):0{FormatSeconds.lens[x]}}{cl.res if color else ""}'
|
|
+ (f'{cl.grn if color else ""}{FormatSeconds.units[x].upper()}{cl.res if color else ""}' if units else '')
|
|
for x in range(len(unitCast))
|
|
]
|
|
return neg + ':'.join(elem[::-1])
|
|
|
|
__str__ = toStr
|