Say why a date is not one in words of our own, not datetime's
Claude · 2026-10-28 · 461a7d1d94649dc3ce4c805462d15fca4068580e · parents af8588e
The conformance vectors hold the message a slot's date gets refused with, and pyrtan.js has to say the same. Python 3.14 rewords datetime's ValueError ('day 29 must be in range 1..28 for month 2 in year 2026'), so CI on 3.14 found the vectors stale. nlp._date now checks the year, month and day itself and says what it said before on every Python.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rD7jeoK8iFSW7tkCrzXLZ
Changes
src/pyrtan/nlp.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/src/pyrtan/nlp.py b/src/pyrtan/nlp.py index 9e3844a..34e33f6 100644 --- a/src/pyrtan/nlp.py +++ b/src/pyrtan/nlp.py@@ -490,10 +490,21 @@ def _weekday(word: str) -> int | None:def _date(year: int, month: int, day: int, text: str) -> _dt.date:- try:+ # Checked here rather than left to datetime, whose wording changes+ # between Python versions; pyrtan.js says the same words.+ if not 1 <= year <= 9999:+ why = f"year {year} is out of range"+ elif not 1 <= month <= 12:+ why = "month must be in 1..12"+ elif not 1 <= day <= _days_in(year, month):+ why = "day is out of range for month"+ else:return _dt.date(year, month, day)- except ValueError as e:- raise ParseError(f"{text!r}: {e}") from e+ raise ParseError(f"{text!r}: {why}")+++def _days_in(year: int, month: int) -> int:+ return [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]def _shift(day: _dt.date, n: int, unit: str) -> _dt.date:@@ -504,8 +515,7 @@ def _shift(day: _dt.date, n: int, unit: str) -> _dt.date:return day + _dt.timedelta(weeks=n) months = day.month - 1 + (n if unit == "month" else 12 * n) year, month = day.year + months // 12, months % 12 + 1- last = [31, 29 if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]- return _dt.date(year, month, min(day.day, last))+ return _dt.date(year, month, min(day.day, _days_in(year, month)))def parse_host(text: str) -> str: