feat(cli): add natural language date parsing via dateparser
- Add _parse_natural() to utils.py using dateparser as a fallback when structured date formats (YYYY-MM-DD, MM-DD, DD-MM) don't match - Supports expressions like 'today', 'yesterday', 'monday', '3 days ago' - Change day argument to nargs='*' and join tokens so unquoted multi-word expressions like: uv run timesheets 3 days ago work correctly - Pin dateparser to English to avoid locale-dependent behaviour - Update tests to cover natural language cases and fix test_last_monday (dateparser does not support 'last monday'; use 'monday' instead)
This commit is contained in:
parent
29698b1241
commit
615bfe30e0
6 changed files with 207 additions and 13 deletions
|
|
@ -51,12 +51,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
)
|
||||
parser.add_argument(
|
||||
"day",
|
||||
nargs="?",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Day to extract timesheets for. Accepts YYYY-MM-DD, MM-DD, or DD-MM "
|
||||
"(- or / as separator). Defaults to today."
|
||||
"Day to extract timesheets for. Accepts YYYY-MM-DD, MM-DD, DD-MM, "
|
||||
"or a natural language expression like 'yesterday' or '3 days ago'. "
|
||||
"Defaults to today."
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--map",
|
||||
|
|
@ -93,11 +93,13 @@ def main() -> None:
|
|||
config_path = args.config if args.config is not None else find_default_config()
|
||||
config = load_config(config_path)
|
||||
|
||||
if args.day is None:
|
||||
day_input = " ".join(args.day) if args.day else None
|
||||
|
||||
if day_input is None:
|
||||
target_date = date.today()
|
||||
else:
|
||||
try:
|
||||
target_date = parse_date_arg(args.day)
|
||||
target_date = parse_date_arg(day_input)
|
||||
except AmbiguousDateError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -63,10 +63,29 @@ def parse_date_arg(value: str) -> date:
|
|||
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)."
|
||||
# Nothing matched structured formats — try natural language
|
||||
return _parse_natural(value)
|
||||
|
||||
|
||||
def _parse_natural(value: str) -> date:
|
||||
"""
|
||||
Parse a natural language date string using dateparser.
|
||||
Raises ValueError if the string cannot be interpreted.
|
||||
"""
|
||||
import dateparser
|
||||
|
||||
result = dateparser.parse(
|
||||
value,
|
||||
languages=["en"],
|
||||
settings={"PREFER_DATES_FROM": "past", "RETURN_AS_TIMEZONE_AWARE": False},
|
||||
)
|
||||
if result is None:
|
||||
raise ValueError(
|
||||
f"Could not interpret {value!r} as a date. "
|
||||
"Try a structured format like '2026-05-22' or '05-22', "
|
||||
"or a relative expression like 'today', 'yesterday', 'monday', or '3 days ago'."
|
||||
)
|
||||
return result.date()
|
||||
|
||||
|
||||
def parse_duration(duration_str: str) -> float:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue