Problem: `pending.Metadata` used shorthand field names (`cat`, `rec`, `rec_mode`) matching user-facing token syntax, coupling internal representation to config. `RecurSpec.from_completion` used a boolean where a `pending.RecurMode` alias exists. `category_syntax` was hardcoded to `'cat'` with no config option. Solution: rename `Metadata` fields to `category`/`recur`/`recur_mode`, add `category_syntax` config option (default `'cat'`), rename `ParsedEntry` fields to match, replace `RecurSpec.from_completion` with `mode: pending.RecurMode`, and restore `[string]` indexer on `pending.ForgeConfig` alongside explicit fields.
223 lines
6.7 KiB
Lua
223 lines
6.7 KiB
Lua
require('spec.helpers')
|
|
|
|
describe('recur', function()
|
|
local recur = require('pending.recur')
|
|
|
|
describe('parse', function()
|
|
it('parses daily', function()
|
|
local r = recur.parse('daily')
|
|
assert.are.equal('daily', r.freq)
|
|
assert.are.equal(1, r.interval)
|
|
assert.are.equal('scheduled', r.mode)
|
|
end)
|
|
|
|
it('parses weekdays', function()
|
|
local r = recur.parse('weekdays')
|
|
assert.are.equal('weekly', r.freq)
|
|
assert.are.same({ 'MO', 'TU', 'WE', 'TH', 'FR' }, r.byday)
|
|
end)
|
|
|
|
it('parses weekly', function()
|
|
local r = recur.parse('weekly')
|
|
assert.are.equal('weekly', r.freq)
|
|
assert.are.equal(1, r.interval)
|
|
end)
|
|
|
|
it('parses biweekly', function()
|
|
local r = recur.parse('biweekly')
|
|
assert.are.equal('weekly', r.freq)
|
|
assert.are.equal(2, r.interval)
|
|
end)
|
|
|
|
it('parses monthly', function()
|
|
local r = recur.parse('monthly')
|
|
assert.are.equal('monthly', r.freq)
|
|
assert.are.equal(1, r.interval)
|
|
end)
|
|
|
|
it('parses quarterly', function()
|
|
local r = recur.parse('quarterly')
|
|
assert.are.equal('monthly', r.freq)
|
|
assert.are.equal(3, r.interval)
|
|
end)
|
|
|
|
it('parses yearly', function()
|
|
local r = recur.parse('yearly')
|
|
assert.are.equal('yearly', r.freq)
|
|
assert.are.equal(1, r.interval)
|
|
end)
|
|
|
|
it('parses annual as yearly', function()
|
|
local r = recur.parse('annual')
|
|
assert.are.equal('yearly', r.freq)
|
|
end)
|
|
|
|
it('parses 3d as every 3 days', function()
|
|
local r = recur.parse('3d')
|
|
assert.are.equal('daily', r.freq)
|
|
assert.are.equal(3, r.interval)
|
|
end)
|
|
|
|
it('parses 2w as biweekly', function()
|
|
local r = recur.parse('2w')
|
|
assert.are.equal('weekly', r.freq)
|
|
assert.are.equal(2, r.interval)
|
|
end)
|
|
|
|
it('parses 6m as every 6 months', function()
|
|
local r = recur.parse('6m')
|
|
assert.are.equal('monthly', r.freq)
|
|
assert.are.equal(6, r.interval)
|
|
end)
|
|
|
|
it('parses 2y as every 2 years', function()
|
|
local r = recur.parse('2y')
|
|
assert.are.equal('yearly', r.freq)
|
|
assert.are.equal(2, r.interval)
|
|
end)
|
|
|
|
it('parses ! prefix as completion-based', function()
|
|
local r = recur.parse('!weekly')
|
|
assert.are.equal('weekly', r.freq)
|
|
assert.are.equal('completion', r.mode)
|
|
end)
|
|
|
|
it('parses raw RRULE fragment', function()
|
|
local r = recur.parse('FREQ=MONTHLY;BYDAY=1MO')
|
|
assert.is_not_nil(r)
|
|
end)
|
|
|
|
it('returns nil for invalid input', function()
|
|
assert.is_nil(recur.parse(''))
|
|
assert.is_nil(recur.parse('garbage'))
|
|
assert.is_nil(recur.parse('0d'))
|
|
end)
|
|
|
|
it('is case insensitive', function()
|
|
local r = recur.parse('Weekly')
|
|
assert.are.equal('weekly', r.freq)
|
|
end)
|
|
end)
|
|
|
|
describe('validate', function()
|
|
it('returns true for valid specs', function()
|
|
assert.is_true(recur.validate('daily'))
|
|
assert.is_true(recur.validate('2w'))
|
|
assert.is_true(recur.validate('!monthly'))
|
|
end)
|
|
|
|
it('returns false for invalid specs', function()
|
|
assert.is_false(recur.validate('garbage'))
|
|
assert.is_false(recur.validate(''))
|
|
end)
|
|
end)
|
|
|
|
describe('next_due', function()
|
|
it('advances daily by 1 day', function()
|
|
local result = recur.next_due('2099-03-01', 'daily', 'scheduled')
|
|
assert.are.equal('2099-03-02', result)
|
|
end)
|
|
|
|
it('advances weekly by 7 days', function()
|
|
local result = recur.next_due('2099-03-01', 'weekly', 'scheduled')
|
|
assert.are.equal('2099-03-08', result)
|
|
end)
|
|
|
|
it('advances monthly and clamps day', function()
|
|
local result = recur.next_due('2099-01-31', 'monthly', 'scheduled')
|
|
assert.are.equal('2099-02-28', result)
|
|
end)
|
|
|
|
it('advances yearly and handles leap year', function()
|
|
local result = recur.next_due('2096-02-29', 'yearly', 'scheduled')
|
|
assert.are.equal('2097-02-28', result)
|
|
end)
|
|
|
|
it('advances biweekly by 14 days', function()
|
|
local result = recur.next_due('2099-03-01', 'biweekly', 'scheduled')
|
|
assert.are.equal('2099-03-15', result)
|
|
end)
|
|
|
|
it('advances quarterly by 3 months', function()
|
|
local result = recur.next_due('2099-01-15', 'quarterly', 'scheduled')
|
|
assert.are.equal('2099-04-15', result)
|
|
end)
|
|
|
|
it('scheduled mode skips to future if overdue', function()
|
|
local result = recur.next_due('2020-01-01', 'yearly', 'scheduled')
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
assert.is_true(result > today)
|
|
end)
|
|
|
|
it('completion mode advances 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 + 7,
|
|
})
|
|
)
|
|
local result = recur.next_due('2020-01-01', 'weekly', 'completion')
|
|
assert.are.equal(expected, result)
|
|
end)
|
|
|
|
it('advances 3d by 3 days', function()
|
|
local result = recur.next_due('2099-06-10', '3d', 'scheduled')
|
|
assert.are.equal('2099-06-13', result)
|
|
end)
|
|
end)
|
|
|
|
describe('to_rrule', function()
|
|
it('converts daily', function()
|
|
assert.are.equal('RRULE:FREQ=DAILY', recur.to_rrule('daily'))
|
|
end)
|
|
|
|
it('converts weekly', function()
|
|
assert.are.equal('RRULE:FREQ=WEEKLY', recur.to_rrule('weekly'))
|
|
end)
|
|
|
|
it('converts biweekly with interval', function()
|
|
assert.are.equal('RRULE:FREQ=WEEKLY;INTERVAL=2', recur.to_rrule('biweekly'))
|
|
end)
|
|
|
|
it('converts weekdays with BYDAY', function()
|
|
assert.are.equal('RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', recur.to_rrule('weekdays'))
|
|
end)
|
|
|
|
it('converts monthly', function()
|
|
assert.are.equal('RRULE:FREQ=MONTHLY', recur.to_rrule('monthly'))
|
|
end)
|
|
|
|
it('converts quarterly with interval', function()
|
|
assert.are.equal('RRULE:FREQ=MONTHLY;INTERVAL=3', recur.to_rrule('quarterly'))
|
|
end)
|
|
|
|
it('converts yearly', function()
|
|
assert.are.equal('RRULE:FREQ=YEARLY', recur.to_rrule('yearly'))
|
|
end)
|
|
|
|
it('converts 2w with interval', function()
|
|
assert.are.equal('RRULE:FREQ=WEEKLY;INTERVAL=2', recur.to_rrule('2w'))
|
|
end)
|
|
|
|
it('prefixes raw RRULE fragment', function()
|
|
assert.are.equal('RRULE:FREQ=MONTHLY;BYDAY=1MO', recur.to_rrule('FREQ=MONTHLY;BYDAY=1MO'))
|
|
end)
|
|
|
|
it('returns empty string for invalid spec', function()
|
|
assert.are.equal('', recur.to_rrule('garbage'))
|
|
end)
|
|
end)
|
|
|
|
describe('shorthand_list', function()
|
|
it('returns a list of named shorthands', function()
|
|
local list = recur.shorthand_list()
|
|
assert.is_true(#list >= 8)
|
|
assert.is_true(vim.tbl_contains(list, 'daily'))
|
|
assert.is_true(vim.tbl_contains(list, 'weekly'))
|
|
assert.is_true(vim.tbl_contains(list, 'monthly'))
|
|
end)
|
|
end)
|
|
end)
|