feat(parse): flexible time parsing for @ suffix

Problem: the @HH:MM time suffix required zero-padded 24-hour format,
forcing users to write due:tomorrow@14:00 instead of due:tomorrow@2pm.

Solution: add normalize_time() that accepts bare hours (9, 14),
H:MM (9:30), am/pm (2pm, 9:30am, 12am), and existing HH:MM format,
normalizing all to canonical HH:MM on save.
This commit is contained in:
Barrett Ruth 2026-02-25 20:34:51 -05:00
parent ee2d125846
commit 66eb93a6d1
4 changed files with 157 additions and 4 deletions

View file

@ -1,5 +1,4 @@
local buffer = require('pending.buffer')
local config = require('pending.config')
local diff = require('pending.diff')
local parse = require('pending.parse')
local store = require('pending.store')
@ -211,7 +210,7 @@ function M.prompt_date()
if not id then
return
end
vim.ui.input({ prompt = 'Due date (YYYY-MM-DD[Thh:mm]): ' }, function(input)
vim.ui.input({ prompt = 'Due date (today, +3d, fri@2pm, etc.): ' }, function(input)
if not input then
return
end

View file

@ -36,6 +36,60 @@ local function is_valid_time(s)
return hn >= 0 and hn <= 23 and mn >= 0 and mn <= 59
end
---@param s string
---@return string|nil
local function normalize_time(s)
local h, m, period
h, m, period = s:match('^(%d+):(%d%d)([ap]m)$')
if not h then
h, period = s:match('^(%d+)([ap]m)$')
if h then
m = '00'
end
end
if not h then
h, m = s:match('^(%d%d):(%d%d)$')
end
if not h then
h, m = s:match('^(%d):(%d%d)$')
end
if not h then
h = s:match('^(%d+)$')
if h then
m = '00'
end
end
if not h then
return nil
end
local hn = tonumber(h) --[[@as integer]]
local mn = tonumber(m) --[[@as integer]]
if period then
if hn < 1 or hn > 12 then
return nil
end
if period == 'am' then
hn = hn == 12 and 0 or hn
else
hn = hn == 12 and 12 or hn + 12
end
else
if hn < 0 or hn > 23 then
return nil
end
end
if mn < 0 or mn > 59 then
return nil
end
return string.format('%02d:%02d', hn, mn)
end
---@param s string
---@return boolean
local function is_valid_datetime(s)
@ -100,9 +154,10 @@ end
---@param text string
---@return string|nil
function M.resolve_date(text)
local date_input, time_suffix = text:match('^(.+)@(%d%d:%d%d)$')
local date_input, time_suffix = text:match('^(.+)@(.+)$')
if time_suffix then
if not is_valid_time(time_suffix) then
time_suffix = normalize_time(time_suffix)
if not time_suffix then
return nil
end
else