| 1 | """ |
|---|
| 2 | Time formatting utilities. |
|---|
| 3 | |
|---|
| 4 | ISO-8601: |
|---|
| 5 | http://www.cl.cam.ac.uk/~mgk25/iso-time.html |
|---|
| 6 | """ |
|---|
| 7 | |
|---|
| 8 | import calendar, datetime, re, time |
|---|
| 9 | from typing import Optional |
|---|
| 10 | from enum import Enum |
|---|
| 11 | |
|---|
| 12 | |
|---|
| 13 | class ParseDurationUnitFormat(str, Enum): |
|---|
| 14 | SECONDS0 = "s" |
|---|
| 15 | SECONDS1 = "second" |
|---|
| 16 | SECONDS2 = "seconds" |
|---|
| 17 | DAYS0 = "day" |
|---|
| 18 | DAYS1 = "days" |
|---|
| 19 | MONTHS0 = "mo" |
|---|
| 20 | MONTHS1 = "month" |
|---|
| 21 | MONTHS2 = "months" |
|---|
| 22 | YEARS0 = "year" |
|---|
| 23 | YEARS1 = "years" |
|---|
| 24 | |
|---|
| 25 | @classmethod |
|---|
| 26 | def list_values(cls): |
|---|
| 27 | return list(map(lambda c: c.value, cls)) |
|---|
| 28 | |
|---|
| 29 | |
|---|
| 30 | def format_time(t): |
|---|
| 31 | return time.strftime("%Y-%m-%d %H:%M:%S", t) |
|---|
| 32 | |
|---|
| 33 | def iso_utc_date( |
|---|
| 34 | now: Optional[float] = None, |
|---|
| 35 | t=time.time |
|---|
| 36 | ) -> str: |
|---|
| 37 | if now is None: |
|---|
| 38 | now = t() |
|---|
| 39 | return datetime.datetime.utcfromtimestamp(now).isoformat()[:10] |
|---|
| 40 | |
|---|
| 41 | def iso_utc( |
|---|
| 42 | now: Optional[float] = None, |
|---|
| 43 | sep: str = '_', |
|---|
| 44 | t=time.time |
|---|
| 45 | ) -> str: |
|---|
| 46 | if now is None: |
|---|
| 47 | now = t() |
|---|
| 48 | sep = str(sep) # should already be a str |
|---|
| 49 | return datetime.datetime.utcfromtimestamp(now).isoformat(sep) |
|---|
| 50 | |
|---|
| 51 | def iso_utc_time_to_seconds(isotime, _conversion_re=re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})[T_ ](?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?P<subsecond>\.\d+)?")): |
|---|
| 52 | """ |
|---|
| 53 | The inverse of iso_utc(). |
|---|
| 54 | |
|---|
| 55 | Real ISO-8601 is "2003-01-08T06:30:59". We also accept the widely |
|---|
| 56 | used variants "2003-01-08_06:30:59" and "2003-01-08 06:30:59". |
|---|
| 57 | """ |
|---|
| 58 | m = _conversion_re.match(isotime) |
|---|
| 59 | if not m: |
|---|
| 60 | raise ValueError(isotime, "not a complete ISO8601 timestamp") |
|---|
| 61 | year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day')) |
|---|
| 62 | hour, minute, second = int(m.group('hour')), int(m.group('minute')), int(m.group('second')) |
|---|
| 63 | subsecstr = m.group('subsecond') |
|---|
| 64 | if subsecstr: |
|---|
| 65 | subsecfloat = float(subsecstr) |
|---|
| 66 | else: |
|---|
| 67 | subsecfloat = 0 |
|---|
| 68 | |
|---|
| 69 | return calendar.timegm( (year, month, day, hour, minute, second, 0, 1, 0) ) + subsecfloat |
|---|
| 70 | |
|---|
| 71 | |
|---|
| 72 | def parse_duration(s): |
|---|
| 73 | """ |
|---|
| 74 | Parses a duration string and converts it to seconds. The unit format is case insensitive |
|---|
| 75 | |
|---|
| 76 | Args: |
|---|
| 77 | s (str): The duration string to parse. Expected format: `<number><unit>` |
|---|
| 78 | where `unit` can be one of the values defined in `ParseDurationUnitFormat`. |
|---|
| 79 | |
|---|
| 80 | Returns: |
|---|
| 81 | int: The duration in seconds. |
|---|
| 82 | |
|---|
| 83 | Raises: |
|---|
| 84 | ValueError: If the input string does not match the expected format or contains invalid units. |
|---|
| 85 | """ |
|---|
| 86 | SECOND = 1 |
|---|
| 87 | DAY = 24*60*60 |
|---|
| 88 | MONTH = 31*DAY |
|---|
| 89 | YEAR = 365*DAY |
|---|
| 90 | time_map = { |
|---|
| 91 | ParseDurationUnitFormat.SECONDS0: SECOND, |
|---|
| 92 | ParseDurationUnitFormat.SECONDS1: SECOND, |
|---|
| 93 | ParseDurationUnitFormat.SECONDS2: SECOND, |
|---|
| 94 | ParseDurationUnitFormat.DAYS0: DAY, |
|---|
| 95 | ParseDurationUnitFormat.DAYS1: DAY, |
|---|
| 96 | ParseDurationUnitFormat.MONTHS0: MONTH, |
|---|
| 97 | ParseDurationUnitFormat.MONTHS1: MONTH, |
|---|
| 98 | ParseDurationUnitFormat.MONTHS2: MONTH, |
|---|
| 99 | ParseDurationUnitFormat.YEARS0: YEAR, |
|---|
| 100 | ParseDurationUnitFormat.YEARS1: YEAR, |
|---|
| 101 | } |
|---|
| 102 | |
|---|
| 103 | # Build a regex pattern dynamically from the list of valid values |
|---|
| 104 | unit_pattern = "|".join(re.escape(unit) for unit in ParseDurationUnitFormat.list_values()) |
|---|
| 105 | pattern = rf"^\s*(\d+)\s*({unit_pattern})\s*$" |
|---|
| 106 | |
|---|
| 107 | # case-insensitive regex matching |
|---|
| 108 | match = re.match(pattern, s, re.IGNORECASE) |
|---|
| 109 | if not match: |
|---|
| 110 | # Generate dynamic error message |
|---|
| 111 | valid_units = ", ".join(f"'{value}'" for value in ParseDurationUnitFormat.list_values()) |
|---|
| 112 | raise ValueError(f"No valid unit in '{s}'. Expected one of: ({valid_units})") |
|---|
| 113 | |
|---|
| 114 | number = int(match.group(1)) # Extract the numeric value |
|---|
| 115 | unit = match.group(2).lower() # Extract the unit & normalize the unit to lowercase |
|---|
| 116 | |
|---|
| 117 | return number * time_map[unit] |
|---|
| 118 | |
|---|
| 119 | def parse_date(s): |
|---|
| 120 | # return seconds-since-epoch for the UTC midnight that starts the given |
|---|
| 121 | # day |
|---|
| 122 | return int(iso_utc_time_to_seconds(s + "T00:00:00")) |
|---|
| 123 | |
|---|
| 124 | def format_delta(time_1, time_2): |
|---|
| 125 | if time_1 is None: |
|---|
| 126 | return "N/A" |
|---|
| 127 | if time_1 > time_2: |
|---|
| 128 | return '-' |
|---|
| 129 | delta = int(time_2 - time_1) |
|---|
| 130 | seconds = delta % 60 |
|---|
| 131 | delta -= seconds |
|---|
| 132 | minutes = (delta // 60) % 60 |
|---|
| 133 | delta -= minutes * 60 |
|---|
| 134 | hours = delta // (60*60) % 24 |
|---|
| 135 | delta -= hours * 24 |
|---|
| 136 | days = delta // (24*60*60) |
|---|
| 137 | if not days: |
|---|
| 138 | if not hours: |
|---|
| 139 | if not minutes: |
|---|
| 140 | return "%ss" % (seconds) |
|---|
| 141 | else: |
|---|
| 142 | return "%sm %ss" % (minutes, seconds) |
|---|
| 143 | else: |
|---|
| 144 | return "%sh %sm %ss" % (hours, minutes, seconds) |
|---|
| 145 | else: |
|---|
| 146 | return "%sd %sh %sm %ss" % (days, hours, minutes, seconds) |
|---|
| 147 | |
|---|