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:
parent
4703b154fc
commit
fed3e7d78d
2 changed files with 344 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue