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

@ -154,6 +154,171 @@ describe('parse', function()
local result = parse.resolve_date('')
assert.is_nil(result)
end)
it("returns yesterday's date for 'yesterday'", function()
local expected = os.date('%Y-%m-%d', os.time() - 86400)
local result = parse.resolve_date('yesterday')
assert.are.equal(expected, result)
end)
it("returns today's date for 'eod'", function()
local result = parse.resolve_date('eod')
assert.are.equal(os.date('%Y-%m-%d'), result)
end)
it('returns Monday of current week for sow', function()
local result = parse.resolve_date('sow')
assert.is_not_nil(result)
local y, m, d = result:match('^(%d+)-(%d+)-(%d+)$')
local t = os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d) })
local wday = os.date('*t', t).wday
assert.are.equal(2, wday)
end)
it('returns Sunday of current week for eow', function()
local result = parse.resolve_date('eow')
assert.is_not_nil(result)
local y, m, d = result:match('^(%d+)-(%d+)-(%d+)$')
local t = os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d) })
local wday = os.date('*t', t).wday
assert.are.equal(1, wday)
end)
it('returns first day of current month for som', function()
local today = os.date('*t') --[[@as osdate]]
local expected = string.format('%04d-%02d-01', today.year, today.month)
local result = parse.resolve_date('som')
assert.are.equal(expected, result)
end)
it('returns last day of current month for eom', function()
local today = os.date('*t') --[[@as osdate]]
local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 }))
local result = parse.resolve_date('eom')
assert.are.equal(expected, result)
end)
it('returns first day of current quarter for soq', function()
local today = os.date('*t') --[[@as osdate]]
local q = math.ceil(today.month / 3)
local first_month = (q - 1) * 3 + 1
local expected = string.format('%04d-%02d-01', today.year, first_month)
local result = parse.resolve_date('soq')
assert.are.equal(expected, result)
end)
it('returns last day of current quarter for eoq', function()
local today = os.date('*t') --[[@as osdate]]
local q = math.ceil(today.month / 3)
local last_month = q * 3
local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 }))
local result = parse.resolve_date('eoq')
assert.are.equal(expected, result)
end)
it('returns Jan 1 of current year for soy', function()
local today = os.date('*t') --[[@as osdate]]
local expected = string.format('%04d-01-01', today.year)
local result = parse.resolve_date('soy')
assert.are.equal(expected, result)
end)
it('returns Dec 31 of current year for eoy', function()
local today = os.date('*t') --[[@as osdate]]
local expected = string.format('%04d-12-31', today.year)
local result = parse.resolve_date('eoy')
assert.are.equal(expected, result)
end)
it('resolves +2w to 14 days from today', function()
local today = os.date('*t') --[[@as osdate]]
local expected = os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day + 14 })
)
local result = parse.resolve_date('+2w')
assert.are.equal(expected, result)
end)
it('resolves +3m to 3 months from today', function()
local today = os.date('*t') --[[@as osdate]]
local expected = os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month + 3, day = today.day })
)
local result = parse.resolve_date('+3m')
assert.are.equal(expected, result)
end)
it('resolves -2d to 2 days ago', function()
local today = os.date('*t') --[[@as osdate]]
local expected = os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day - 2 })
)
local result = parse.resolve_date('-2d')
assert.are.equal(expected, result)
end)
it('resolves -1w to 7 days ago', function()
local today = os.date('*t') --[[@as osdate]]
local expected = os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day - 7 })
)
local result = parse.resolve_date('-1w')
assert.are.equal(expected, result)
end)
it("resolves 'later' to someday_date", function()
local result = parse.resolve_date('later')
assert.are.equal('9999-12-30', result)
end)
it("resolves 'someday' to someday_date", function()
local result = parse.resolve_date('someday')
assert.are.equal('9999-12-30', result)
end)
it('resolves 15th to next 15th of month', function()
local result = parse.resolve_date('15th')
assert.is_not_nil(result)
local _, _, d = result:match('^(%d+)-(%d+)-(%d+)$')
assert.are.equal('15', d)
end)
it('resolves 1st to next 1st of month', function()
local result = parse.resolve_date('1st')
assert.is_not_nil(result)
local _, _, d = result:match('^(%d+)-(%d+)-(%d+)$')
assert.are.equal('01', d)
end)
it('resolves jan to next January 1st', function()
local today = os.date('*t') --[[@as osdate]]
local result = parse.resolve_date('jan')
assert.is_not_nil(result)
local y, m, d = result:match('^(%d+)-(%d+)-(%d+)$')
assert.are.equal('01', m)
assert.are.equal('01', d)
if today.month >= 1 then
assert.are.equal(tostring(today.year + 1), y)
end
end)
it('resolves dec to next December 1st', function()
local today = os.date('*t') --[[@as osdate]]
local result = parse.resolve_date('dec')
assert.is_not_nil(result)
local y, m, d = result:match('^(%d+)-(%d+)-(%d+)$')
assert.are.equal('12', m)
assert.are.equal('01', d)
if today.month >= 12 then
assert.are.equal(tostring(today.year + 1), y)
else
assert.are.equal(tostring(today.year), y)
end
end)
end)
describe('command_add', function()