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:
Jef Roosens 2026-05-22 10:42:53 +02:00
parent 715e0988dc
commit 267ad5b1b5
Signed by: Jef Roosens
GPG key ID: 119385BCAA005C21
5 changed files with 170 additions and 25 deletions

View file

@ -3,9 +3,11 @@ from datetime import date
import pytest
from timesheets.utils import (
AmbiguousDateError,
decimal_to_hhmm,
duration_from_start_end,
format_date,
parse_date_arg,
parse_duration,
strip_markdown_link,
)
@ -98,3 +100,75 @@ class TestFormatDate:
def test_single_digit_day_month(self):
assert format_date(date(2026, 1, 5)) == "05/01/26"
class TestParseDateArg:
# Fix the year used for two-part tests to avoid test brittleness
THIS_YEAR = 2026
def _parse(self, value: str) -> date:
# Patch date.today so two-part tests are year-stable
from unittest.mock import patch
with patch("timesheets.utils.date") as mock_date:
mock_date.today.return_value = date(self.THIS_YEAR, 6, 1)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
return parse_date_arg(value)
# --- YYYY-MM-DD ---
def test_full_dash(self):
assert parse_date_arg("2026-05-22") == date(2026, 5, 22)
def test_full_slash(self):
assert parse_date_arg("2026/05/22") == date(2026, 5, 22)
def test_full_invalid_day(self):
with pytest.raises(ValueError):
parse_date_arg("2026-02-30")
# --- two-part unambiguous: only valid as MM-DD ---
def test_mm_dd_unambiguous(self):
# 01-22: valid as MM=1, DD=22; invalid as MM=22, DD=1
result = self._parse("01-22")
assert result == date(self.THIS_YEAR, 1, 22)
# --- two-part unambiguous: only valid as DD-MM ---
def test_dd_mm_unambiguous(self):
# 22-01: invalid as MM=22, DD=1 is actually valid... use 22-13
# 22-13: valid as DD=22, MM=13 is invalid; DD=13 MM=22 is invalid too
# Use 30-11: valid as DD=30,MM=11; invalid as MM=30,DD=11
result = self._parse("30-11")
assert result == date(self.THIS_YEAR, 11, 30)
def test_slash_separator(self):
result = self._parse("01/22")
assert result == date(self.THIS_YEAR, 1, 22)
# --- ambiguous ---
def test_ambiguous_raises(self):
# 05-06: valid as MM=5,DD=6 and as DD=5,MM=6
with pytest.raises(AmbiguousDateError, match="ambiguous"):
self._parse("05-06")
def test_ambiguous_slash_raises(self):
with pytest.raises(AmbiguousDateError):
self._parse("05/06")
# --- identical interpretations are not ambiguous ---
def test_same_day_month_not_ambiguous(self):
# 05-05: MM=5,DD=5 and DD=5,MM=5 are the same date
result = self._parse("05-05")
assert result == date(self.THIS_YEAR, 5, 5)
# --- fully invalid ---
def test_invalid_both_parts(self):
# 13-32 is invalid as both MM-DD and DD-MM
with pytest.raises(ValueError):
self._parse("13-32")
def test_unrecognised_format(self):
with pytest.raises(ValueError, match="Unrecognised"):
parse_date_arg("not-a-date")
def test_whitespace_stripped(self):
assert parse_date_arg(" 2026-05-22 ") == date(2026, 5, 22)