# todo.parse-date-phrase Reads what a person types into a due-date box, such as `tomorrow`, `next friday`, `in 2 weeks` or `2026-10-01`, and returns the date it names, counted from `today`. Returns null when the text is not a date phrase, so a form can say "I didn't understand that" without catching anything. It only throws when `today` itself is not a real ISO date (dates.add-days's message, e.g. `"28/09/2026" is not an ISO date (YYYY-MM-DD)`), or when the answer would fall outside 0001-9999. `today` is an argument, not the clock: the caller passes the person's local date, so the answer is the same on every server and in every test. todo.parse-quick-add uses it to find the due date inside a whole quick-add line. ## Grammar The whole text must be one phrase. Letters are matched case-insensitively (ASCII only: `TODAY` in full-width letters is not a keyword). Words are separated by one or more ASCII whitespace characters (space, tab, CR, LF, VT, FF), and whitespace at either end is ignored. | Phrase | Means | Example, today Monday 2026-09-28 | | --- | --- | --- | | `today`, `tod` | today | 2026-09-28 | | `tomorrow`, `tmr`, `tmrw` | today + 1 day | 2026-09-29 | | `yesterday` | today - 1 day (a due date may be overdue) | 2026-09-27 | | a weekday: `monday`/`mon`, `tuesday`/`tue`/`tues`, `wednesday`/`wed`, `thursday`/`thu`/`thur`/`thurs`, `friday`/`fri`, `saturday`/`sat`, `sunday`/`sun` | the next such day strictly after today (1 to 7 days on) | `friday` = 2026-10-02; `monday` = 2026-10-05 | | `next week` | Monday of next ISO week | 2026-10-05 | | `next ` | that weekday in next ISO week (Monday to Sunday) | `next friday` = 2026-10-09 | | `next month` | the 1st of next month | 2026-10-01 | | `next year` | 1 January next year | 2027-01-01 | | `weekend`, `this weekend` | the coming Saturday; today if today is Saturday or Sunday | 2026-10-03 | | `in N day(s)` | today + N days | `in 3 days` = 2026-10-01 | | `in N week(s)` | today + 7N days | `in a week` = 2026-10-05 | | `in N month(s)` | today + N months, clamped to the month end (dates.add-months) | `in 1 month` from 2026-01-31 = 2026-02-28 | | `in N year(s)` | today + 12N months, clamped (29 Feb becomes 28 Feb) | `in a year` from 2028-02-29 = 2029-02-28 | | `YYYY-MM-DD` | that date, if it is a real date (years 0001-9999) | `2026-10-01` | | `on `, `due ` | the same as the phrase | `due friday`, `on 2026-10-01` | N is 1 to 999 in ASCII digits with no leading zero (`in 03 days` is not a phrase), or one of the words `a`, `an`, `one` ... `ten`. Units may be singular or plural whatever N is. Only one `on` or `due` is allowed, and only in front. Anything else is null: `friday week`, `in 3 lightyears`, `in 0 days`, `in 1000 days`, `2026-02-30`, `next`, an empty string, or a phrase with other words around it (`pay rent tomorrow`; finding a phrase inside a line is todo.parse-quick-add's job). ## Decisions - **A weekday name never means today.** On a Friday, `friday` is a week away: someone typing a weekday means a day to come, and `today` is the word for today. - **`next ` means next week's.** On Monday 28 September, `friday` is 2 October and `next friday` is 9 October. People disagree about this one; the ISO-week reading at least gives the two phrases different answers, and a UI can show the date it picked. - **`weekend` on a Sunday is today.** It is still the weekend, and jumping to next Saturday would push the task six days out. - **`in 1 month` clamps**, as dates.add-months does: 31 January + 1 month is 28 February (29 in a leap year), never 3 March. - **An impossible ISO date is null, not an error.** It is something typed, like any other unrecognised text; only `today` is the caller's responsibility.