feat(parse): expand date vocabulary with named dates

Problem: the date input only supports today, tomorrow, +Nd, and
weekday names, lacking relative offsets like weeks/months, period
boundaries, ordinals, month names, and backdating.

Solution: add yesterday, eod, sow/eow, som/eom, soq/eoq, soy/eoy,
+Nw, +Nm, -Nd, -Nw, ordinals (1st-31st), month names (jan-dec),
and later/someday to resolve_date(). Add tests for all new tokens.
This commit is contained in:
Barrett Ruth 2026-02-25 13:03:06 -05:00
parent 4703b154fc
commit fed3e7d78d
2 changed files with 344 additions and 5 deletions

View file

@ -39,14 +39,33 @@ local weekday_map = {
sat = 7,
}
local month_map = {
jan = 1, feb = 2, mar = 3, apr = 4,
may = 5, jun = 6, jul = 7, aug = 8,
sep = 9, oct = 10, nov = 11, dec = 12,
}
---@param today osdate
---@return string
local function today_str(today)
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day })) --[[@as string]]
end
---@param text string
---@return string|nil
function M.resolve_date(text)
local lower = text:lower()
local today = os.date('*t') --[[@as osdate]]
if lower == 'today' then
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day })) --[[@as string]]
if lower == 'today' or lower == 'eod' then
return today_str(today)
end
if lower == 'yesterday' then
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day - 1 })
) --[[@as string]]
end
if lower == 'tomorrow' then
@ -56,6 +75,72 @@ function M.resolve_date(text)
) --[[@as string]]
end
if lower == 'sow' then
local delta = -((today.wday - 2) % 7)
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day + delta })
) --[[@as string]]
end
if lower == 'eow' then
local delta = (1 - today.wday) % 7
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day + delta })
) --[[@as string]]
end
if lower == 'som' then
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = 1 })
) --[[@as string]]
end
if lower == 'eom' then
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month + 1, day = 0 })
) --[[@as string]]
end
if lower == 'soq' then
local q = math.ceil(today.month / 3)
local first_month = (q - 1) * 3 + 1
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = first_month, day = 1 })
) --[[@as string]]
end
if lower == 'eoq' then
local q = math.ceil(today.month / 3)
local last_month = q * 3
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = last_month + 1, day = 0 })
) --[[@as string]]
end
if lower == 'soy' then
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = 1, day = 1 })
) --[[@as string]]
end
if lower == 'eoy' then
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = 12, day = 31 })
) --[[@as string]]
end
if lower == 'later' or lower == 'someday' then
return config.get().someday_date
end
local n = lower:match('^%+(%d+)d$')
if n then
return os.date(
@ -63,13 +148,102 @@ function M.resolve_date(text)
os.time({
year = today.year,
month = today.month,
day = today.day + (
tonumber(n) --[[@as integer]]
),
day = today.day + (tonumber(n) --[[@as integer]]),
})
) --[[@as string]]
end
n = lower:match('^%+(%d+)w$')
if n then
return os.date(
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month,
day = today.day + (tonumber(n) --[[@as integer]]) * 7,
})
) --[[@as string]]
end
n = lower:match('^%+(%d+)m$')
if n then
return os.date(
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month + (tonumber(n) --[[@as integer]]),
day = today.day,
})
) --[[@as string]]
end
n = lower:match('^%-(%d+)d$')
if n then
return os.date(
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month,
day = today.day - (tonumber(n) --[[@as integer]]),
})
) --[[@as string]]
end
n = lower:match('^%-(%d+)w$')
if n then
return os.date(
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month,
day = today.day - (tonumber(n) --[[@as integer]]) * 7,
})
) --[[@as string]]
end
local ord = lower:match('^(%d+)[snrt][tdh]$')
if ord then
local day_num = tonumber(ord) --[[@as integer]]
if day_num >= 1 and day_num <= 31 then
local m, y = today.month, today.year
if today.day >= day_num then
m = m + 1
if m > 12 then
m = 1
y = y + 1
end
end
local t = os.time({ year = y, month = m, day = day_num })
local check = os.date('*t', t) --[[@as osdate]]
if check.day == day_num then
return os.date('%Y-%m-%d', t) --[[@as string]]
end
m = m + 1
if m > 12 then
m = 1
y = y + 1
end
t = os.time({ year = y, month = m, day = day_num })
check = os.date('*t', t) --[[@as osdate]]
if check.day == day_num then
return os.date('%Y-%m-%d', t) --[[@as string]]
end
return nil
end
end
local target_month = month_map[lower]
if target_month then
local y = today.year
if today.month >= target_month then
y = y + 1
end
return os.date(
'%Y-%m-%d',
os.time({ year = y, month = target_month, day = 1 })
) --[[@as string]]
end
local target_wday = weekday_map[lower]
if target_wday then
local current_wday = today.wday