feat(cli): add flexible positional day argument
- Add parse_date_arg() to utils.py supporting YYYY-MM-DD, MM-DD, and DD-MM formats with either - or / as separator - Add AmbiguousDateError for two-part dates valid as both MM-DD and DD-MM - Replace --day flag with a positional optional argument (defaults to today) - Remove old _parse_date() helper from cli.py
This commit is contained in:
parent
715e0988dc
commit
267ad5b1b5
5 changed files with 170 additions and 25 deletions
|
|
@ -1,6 +1,73 @@
|
|||
import re
|
||||
from datetime import date
|
||||
|
||||
# Separator is either - or /
|
||||
_SEP = r"[-/]"
|
||||
_DATE_PATTERNS = [
|
||||
# YYYY-MM-DD or YYYY/MM/DD
|
||||
re.compile(r"^(\d{4})" + _SEP + r"(\d{1,2})" + _SEP + r"(\d{1,2})$"),
|
||||
# Two-part: first two digits, sep, last two digits
|
||||
re.compile(r"^(\d{1,2})" + _SEP + r"(\d{1,2})$"),
|
||||
]
|
||||
|
||||
|
||||
class AmbiguousDateError(ValueError):
|
||||
"""Raised when a two-part date is valid as both MM-DD and DD-MM."""
|
||||
|
||||
|
||||
def _valid_date(year: int, month: int, day: int) -> date | None:
|
||||
"""Return a date if the triple is valid, else None."""
|
||||
try:
|
||||
return date(year, month, day)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_date_arg(value: str) -> date:
|
||||
"""
|
||||
Parse a flexible date string into a date object.
|
||||
|
||||
Accepted formats (separator is - or /):
|
||||
YYYY-MM-DD unambiguous
|
||||
MM-DD month then day; year assumed to be current year
|
||||
DD-MM day then month; year assumed to be current year
|
||||
|
||||
If a two-part value is valid as both MM-DD and DD-MM, raises
|
||||
AmbiguousDateError. If it is valid as neither, raises ValueError.
|
||||
"""
|
||||
value = value.strip()
|
||||
today = date.today()
|
||||
|
||||
# Try YYYY-MM-DD first
|
||||
m = re.match(r"^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$", value)
|
||||
if m:
|
||||
y, a, b = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
||||
d = _valid_date(y, a, b)
|
||||
if d is None:
|
||||
raise ValueError(f"Invalid date: {value!r}")
|
||||
return d
|
||||
|
||||
# Try two-part MM-DD / DD-MM
|
||||
m = re.match(r"^(\d{1,2})[-/](\d{1,2})$", value)
|
||||
if m:
|
||||
a, b = int(m.group(1)), int(m.group(2))
|
||||
as_md = _valid_date(today.year, a, b) # interpret as MM-DD
|
||||
as_dm = _valid_date(today.year, b, a) # interpret as DD-MM
|
||||
|
||||
if as_md is not None and as_dm is not None and as_md != as_dm:
|
||||
raise AmbiguousDateError(
|
||||
f"{value!r} is ambiguous: could be {as_md} (MM-DD) or {as_dm} (DD-MM)"
|
||||
)
|
||||
result = as_md or as_dm
|
||||
if result is None:
|
||||
raise ValueError(f"Invalid date: {value!r}")
|
||||
return result
|
||||
|
||||
raise ValueError(
|
||||
f"Unrecognised date format: {value!r}. "
|
||||
"Expected YYYY-MM-DD, MM-DD, or DD-MM (- or / as separator)."
|
||||
)
|
||||
|
||||
|
||||
def parse_duration(duration_str: str) -> float:
|
||||
"""Convert HH:MM duration string to decimal hours."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue