3,161 bytes · the Python implementation · view raw
from typing import Optional, Sequence, Tuple
from .time_duration_types import Duration
MAX_DIGITS = 6def _not_a_duration(text: object) -> ValueError:
return ValueError('"%s" is not a duration: write it as 1h30m, 90m or 01:30' % (text,))
def _is_digit(ch: str) -> bool:
# Not str.isdigit(): that accepts superscripts and other scripts' digits.return"0" <= ch <= "9"def _read_number(text: str, s: str, at: int) -> Optional[Tuple[int, int]]:
"""Read a run of digits from ``at``; returns (value, next index), or None if there is none."""
end = at
while end < len(s) and _is_digit(s[end]):
end += 1if end == at:
returnNoneif end - at > MAX_DIGITS:
raise _not_a_duration(text)
return int(s[at:end]), end
def _skip_spaces(s: str, at: int) -> int:
while at < len(s) and s[at] in (" ", "\t"):
at += 1return at
def _parse_one(text: str) -> int:
"""One written duration, in minutes."""ifnot isinstance(text, str):
raise _not_a_duration(text)
s = text[_skip_spaces(text, 0):].rstrip(" \t")
colon = s.find(":")
if colon >= 0:
hours = _read_number(text, s, 0)
if hours isNoneor hours[1] != colon:
raise _not_a_duration(text)
minutes = _read_number(text, s, colon + 1)
if minutes isNoneor minutes[1] != len(s) or minutes[1] - colon - 1 != 2:
raise _not_a_duration(text)
if minutes[0] >= 60:
raise ValueError('minutes must be under 60 when hours are given, in "%s"' % (text,))
return hours[0] * 60 + minutes[0]
at = 0
hours_value: Optional[int] = None
minutes_value: Optional[int] = Nonewhile at < len(s):
read = _read_number(text, s, at)
if read isNone:
raise _not_a_duration(text)
at = _skip_spaces(s, read[1])
unit = s[at] if at < len(s) else""if unit in ("h", "H") and hours_value isNoneand minutes_value isNone:
hours_value = read[0]
elif unit in ("m", "M") and minutes_value isNone:
minutes_value = read[0]
else:
raise _not_a_duration(text)
at = _skip_spaces(s, at + 1)
if hours_value isNoneand minutes_value isNone:
raise _not_a_duration(text)
if hours_value isnotNoneand minutes_value isnotNoneand minutes_value >= 60:
raise ValueError('minutes must be under 60 when hours are given, in "%s"' % (text,))
return (hours_value or0) * 60 + (minutes_value or0)
def duration(parts: Sequence[str]) -> Duration:
"""Parse each written duration, add them, and give the total in minutes and in both written forms. The clock form does not wrap at 24 hours, because a total of work is not a time of day. """
total = 0for part in parts:
total += _parse_one(part)
hours, minutes = divmod(total, 60)
if hours > 0and minutes > 0:
text = "%dh%dm" % (hours, minutes)
elif hours > 0:
text = "%dh" % (hours,)
else:
text = "%dm" % (minutes,)
return Duration(minutes=total, text=text, clock="%02d:%02d" % (hours, minutes))