From 228efbc70a8dd01c08f6380e9649d266f2b19cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mira=C3=A7=20Duran?= <230626673+Ohualtex@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:51:18 +0300 Subject: [PATCH] fix(calendar): accept time-first datetimes in _parse_dt Accept calendar datetime phrases such as "3pm tomorrow" by adding a time-first natural-language parser branch mirroring the reminder parser. Add regression coverage proving time-first forms match their existing day-first equivalents. --- routes/calendar_routes.py | 14 ++++++++++ tests/test_calendar_parse_dt_time_first.py | 31 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/test_calendar_parse_dt_time_first.py diff --git a/routes/calendar_routes.py b/routes/calendar_routes.py index 65f4339f7..fdcd97f07 100644 --- a/routes/calendar_routes.py +++ b/routes/calendar_routes.py @@ -452,6 +452,20 @@ def _parse_dt(s: str) -> datetime: if t is not None: return base.replace(hour=t[0], minute=t[1]) + # time-first: "3pm today", "9am tomorrow", "11pm tonight" + # (parity with parse_due_for_user, which handles these via the same form) + m = _re.match(r'^(.+?)\s+(today|tonight|tomorrow|tmrw|yesterday)$', lower) + if m: + time_part, word = m.group(1).strip(), m.group(2) + base = today + if word in ("tomorrow", "tmrw"): + base = today + timedelta(days=1) + elif word == "yesterday": + base = today - timedelta(days=1) + t = _parse_time(time_part) + if t is not None: + return base.replace(hour=t[0], minute=t[1]) + # next [at] TIME weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] m = _re.match(r'^next\s+(\w+)(?:\s+at)?\s*(.*)$', lower) diff --git a/tests/test_calendar_parse_dt_time_first.py b/tests/test_calendar_parse_dt_time_first.py new file mode 100644 index 000000000..530236550 --- /dev/null +++ b/tests/test_calendar_parse_dt_time_first.py @@ -0,0 +1,31 @@ +"""Regression: _parse_dt must understand "time-first" phrasings like parse_due_for_user does. + +parse_due_for_user accepts both day-first ("tomorrow at 9am") and time-first +("9am tomorrow") forms, but _parse_dt (the parser _parse_dt_pair falls back to +for calendar event start/end) only handled the day-first form. A time-first +start like "3pm tomorrow" missed every branch and fell through to dateutil, +which raises ParserError on "3pm tomorrow", so creating an event with that +phrasing failed. Time-first is now handled identically to its day-first +equivalent, mirroring the sibling reminder parser. +""" +from routes.calendar_routes import _parse_dt + + +def test_time_first_today_equals_day_first(): + assert _parse_dt("3pm today") == _parse_dt("today at 3pm") + + +def test_time_first_tomorrow_equals_day_first(): + assert _parse_dt("9am tomorrow") == _parse_dt("tomorrow at 9am") + + +def test_time_first_with_minutes_equals_day_first(): + assert _parse_dt("2:30pm tomorrow") == _parse_dt("tomorrow at 2:30pm") + + +def test_time_first_tonight_maps_to_today(): + assert _parse_dt("11pm tonight") == _parse_dt("today at 11pm") + + +def test_time_first_yesterday_equals_day_first(): + assert _parse_dt("8am yesterday") == _parse_dt("yesterday at 8am")