source: trunk/src/allmydata/util/time_format.py

Last change on this file was 8aed2d5, checked in by sgerodes <sgerodes@…>, at 2025-01-10T00:00:54Z

fix(time_format): invalid comparison of strings to enums

  • Property mode set to 100644
File size: 4.5 KB
Line 
1"""
2Time formatting utilities.
3
4ISO-8601:
5http://www.cl.cam.ac.uk/~mgk25/iso-time.html
6"""
7
8import calendar, datetime, re, time
9from typing import Optional
10from enum import Enum
11
12
13class 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
30def format_time(t):
31    return time.strftime("%Y-%m-%d %H:%M:%S", t)
32
33def 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
41def 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
51def 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
72def 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
119def 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
124def 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
Note: See TracBrowser for help on using the repository browser.