feat: port GCD dedup algorithm from ical_organizer.py
This commit is contained in:
100
test/organize.test.js
Normal file
100
test/organize.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { parseICS } from '../src/lib/ical-io.js'
|
||||
import { organize, seriesKey, detectInterval, fitSeries } from '../src/lib/organize.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const GOLDEN = readFileSync(join(__dirname, 'golden-org.ics'), 'utf-8')
|
||||
const { events } = parseICS(GOLDEN)
|
||||
|
||||
describe('seriesKey', () => {
|
||||
it('groups events with same summary/location/time/duration/weekday', () => {
|
||||
const e1 = events[0] // ACC INFO SYS Tutorial, Mon 18:00
|
||||
const e2 = events[1] // same series, next week
|
||||
expect(seriesKey(e1)).toBe(seriesKey(e2))
|
||||
})
|
||||
|
||||
it('separates events with different summary', () => {
|
||||
const e1 = events[0] // ACC INFO SYS
|
||||
// Find a BUS LAW event
|
||||
const eBus = events.find(e => e.summary.startsWith('BUS LAW, Tutorial'))
|
||||
expect(seriesKey(e1)).not.toBe(seriesKey(eBus))
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectInterval', () => {
|
||||
it('returns 7 for weekly dates', () => {
|
||||
const dates = ['2026-07-27', '2026-08-03', '2026-08-10', '2026-08-17']
|
||||
expect(detectInterval(dates)).toBe(7)
|
||||
})
|
||||
|
||||
it('returns null for single date', () => {
|
||||
expect(detectInterval(['2026-07-27'])).toBeNull()
|
||||
})
|
||||
|
||||
it('returns 7 for dates with a skip (gaps 7,7,14,7)', () => {
|
||||
const dates = ['2026-07-27', '2026-08-03', '2026-08-10', '2026-08-24', '2026-08-31']
|
||||
expect(detectInterval(dates)).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fitSeries', () => {
|
||||
it('fits ACC INFO SYS series (12 events, 1 skip)', () => {
|
||||
const accEvents = events.filter(e => e.summary === 'ACC INFO SYS, Tutorial')
|
||||
const result = fitSeries(accEvents)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result.rrule.freq).toBe('WEEKLY')
|
||||
expect(result.rrule.interval).toBe(1)
|
||||
expect(result.rrule.byday).toBe('MO')
|
||||
expect(result.exdates).toContain('2026-09-21')
|
||||
})
|
||||
|
||||
it('rejects BUS LAW Workshop (too sparse: 3 events, 5 missing)', () => {
|
||||
const wsEvents = events.filter(e => e.summary === 'BUS LAW, Workshop')
|
||||
expect(wsEvents).toHaveLength(3)
|
||||
expect(fitSeries(wsEvents)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('organize (golden test against org.ics)', () => {
|
||||
const { events: organized, stats } = organize(events)
|
||||
|
||||
it('collapses 61 events into 8', () => {
|
||||
expect(organized).toHaveLength(8)
|
||||
expect(stats.series).toBe(5)
|
||||
expect(stats.flat).toBe(3)
|
||||
})
|
||||
|
||||
it('produces 5 recurring events with RRULE', () => {
|
||||
const recurring = organized.filter(e => e.rrule !== null)
|
||||
expect(recurring).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('produces 3 flat BUS LAW Workshop events', () => {
|
||||
const flatWs = organized.filter(
|
||||
e => e.summary === 'BUS LAW, Workshop' && e.rrule === null,
|
||||
)
|
||||
expect(flatWs).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('ACC INFO SYS series has 1 exdate (Sep 21)', () => {
|
||||
const acc = organized.find(e => e.summary === 'ACC INFO SYS, Tutorial')
|
||||
expect(acc.rrule).not.toBeNull()
|
||||
expect(acc.exdates).toEqual(['2026-09-21'])
|
||||
})
|
||||
|
||||
it('BUS LAW Tutorial series has 2 exdates', () => {
|
||||
const bl = organized.find(e => e.summary === 'BUS LAW, Tutorial')
|
||||
expect(bl.rrule).not.toBeNull()
|
||||
expect(bl.exdates).toHaveLength(2)
|
||||
expect(bl.exdates).toContain('2026-08-31')
|
||||
expect(bl.exdates).toContain('2026-09-21')
|
||||
})
|
||||
|
||||
it('preserves original UID of base event', () => {
|
||||
const acc = organized.find(e => e.summary === 'ACC INFO SYS, Tutorial')
|
||||
expect(acc.uid).toBe('uid0') // first event in series
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user