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.is_false(r.from_completion) 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.is_true(r.from_completion) 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)