diff --git a/src/lib/ical-io.js b/src/lib/ical-io.js new file mode 100644 index 0000000..1414528 --- /dev/null +++ b/src/lib/ical-io.js @@ -0,0 +1,121 @@ +import ICAL from 'ical.js' + +// ------------------------------------------------------------------ // +// Timezone registration (ical.js does not bundle IANA zones) +// ------------------------------------------------------------------ // +function registerTimezones(vcalendar) { + for (const vtz of vcalendar.getAllSubcomponents('vtimezone')) { + const tzid = vtz.getFirstPropertyValue('tzid') + if (tzid) { + ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtz, tzid })) + } + } +} + +// ------------------------------------------------------------------ // +// Helpers +// ------------------------------------------------------------------ // +function pad2(n) { + return String(n).padStart(2, '0') +} + +function timeToISODate(t) { + return `${t.year}-${pad2(t.month)}-${pad2(t.day)}` +} + +function timeToHHMM(t) { + return `${pad2(t.hour)}:${pad2(t.minute)}` +} + +function getTzid(component, propName) { + const prop = component.getFirstProperty(propName) + if (!prop) return 'UTC' + const tzid = prop.getParameter('tzid') + return tzid || 'UTC' +} + +// ------------------------------------------------------------------ // +// Parse a single VEVENT component -> plain EventModel +// ------------------------------------------------------------------ // +function parseEvent(veventComp) { + const event = new ICAL.Event(veventComp) + const start = event.startDate + const end = event.endDate + + // Determine tzid + const tzid = getTzid(veventComp, 'dtstart') + + // RRULE + const recur = veventComp.getFirstPropertyValue('rrule') + let rrule = null + if (recur) { + const bydayArr = recur.getComponent('byday') // string[] + let untilDate = null + if (recur.until) { + const u = recur.until + untilDate = `${u.year}-${pad2(u.month)}-${pad2(u.day)}` + } + rrule = { + freq: recur.freq, + interval: recur.interval || 1, + byday: bydayArr.length > 0 ? bydayArr[0] : null, + untilDate, + } + } + + // EXDATEs + const exdates = [] + for (const prop of veventComp.getAllProperties('exdate')) { + for (const t of prop.getValues()) { + exdates.push(timeToISODate(t)) + } + } + + // _raw: capture DTSTAMP and any other unmapped properties + // Use toICALString() to preserve the compact RFC5545 form ("20260711T085008Z") + // rather than toString()'s ISO 8601 form ("2026-07-11T08:50:08Z"). + const _raw = {} + const dtstamp = veventComp.getFirstPropertyValue('dtstamp') + if (dtstamp) _raw.dtstamp = dtstamp.toICALString() + + return { + uid: veventComp.getFirstPropertyValue('uid') || '', + summary: veventComp.getFirstPropertyValue('summary') || '', + location: veventComp.getFirstPropertyValue('location') || '', + description: veventComp.getFirstPropertyValue('description') || '', + dtstartDate: timeToISODate(start), + dtstartTime: timeToHHMM(start), + dtendTime: timeToHHMM(end), + tzid, + rrule, + exdates, + _raw, + } +} + +// ------------------------------------------------------------------ // +// Parse full ICS text -> { meta, events } +// ------------------------------------------------------------------ // +export function parseICS(text) { + const jcal = ICAL.parse(text) + const vcalendar = new ICAL.Component(jcal) + + // Register timezones from VTIMEZONEs before constructing zoned Times + registerTimezones(vcalendar) + + // Meta + const meta = { + prodid: vcalendar.getFirstPropertyValue('prodid') || '', + version: vcalendar.getFirstPropertyValue('version') || '2.0', + calscale: vcalendar.getFirstPropertyValue('calscale') || 'GREGORIAN', + method: vcalendar.getFirstPropertyValue('method') || null, + xWrCalname: vcalendar.getFirstPropertyValue('x-wr-calname') || null, + xWrTimezone: vcalendar.getFirstPropertyValue('x-wr-timezone') || null, + vtimezones: vcalendar.getAllSubcomponents('vtimezone'), + } + + // Events + const events = vcalendar.getAllSubcomponents('vevent').map(parseEvent) + + return { meta, events } +} diff --git a/test/golden-org.ics b/test/golden-org.ics new file mode 100644 index 0000000..abfa14a --- /dev/null +++ b/test/golden-org.ics @@ -0,0 +1,574 @@ +BEGIN:VCALENDAR +PRODID:-//Allocate//iCal4j 1.0//EN +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Australia/Melbourne +LAST-MODIFIED:20260306T231828Z +TZURL:https://www.tzurl.org/zoneinfo-outlook/Australia/Melbourne +X-LIC-LOCATION:Australia/Melbourne +BEGIN:STANDARD +TZNAME:AEST +TZOFFSETFROM:+1100 +TZOFFSETTO:+1000 +DTSTART:19700405T030000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:AEDT +TZOFFSETFROM:+1000 +TZOFFSETTO:+1100 +DTSTART:19701004T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260727T180000 +DTEND;TZID=Australia/Melbourne:20260727T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid0 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260803T180000 +DTEND;TZID=Australia/Melbourne:20260803T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid1 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260810T180000 +DTEND;TZID=Australia/Melbourne:20260810T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid2 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260817T180000 +DTEND;TZID=Australia/Melbourne:20260817T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid3 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260824T180000 +DTEND;TZID=Australia/Melbourne:20260824T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid4 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260831T180000 +DTEND;TZID=Australia/Melbourne:20260831T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid5 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260907T180000 +DTEND;TZID=Australia/Melbourne:20260907T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid6 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260914T180000 +DTEND;TZID=Australia/Melbourne:20260914T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid7 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260928T180000 +DTEND;TZID=Australia/Melbourne:20260928T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid8 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261005T180000 +DTEND;TZID=Australia/Melbourne:20261005T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid9 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261012T180000 +DTEND;TZID=Australia/Melbourne:20261012T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid10 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261019T180000 +DTEND;TZID=Australia/Melbourne:20261019T200000 +SUMMARY:ACC INFO SYS\, Tutorial +LOCATION:CA_B_B471 +DESCRIPTION:ACF2400_CA_S2_ON-CAMPUS\, Tutorial\, 04_OnCampus\nACC INFO SYS\nStaff: Staff_151\nLocation: CA_B_B471 +UID:uid11 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260727T093000 +DTEND;TZID=Australia/Melbourne:20260727T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid12 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260803T093000 +DTEND;TZID=Australia/Melbourne:20260803T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid13 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260810T093000 +DTEND;TZID=Australia/Melbourne:20260810T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid14 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260817T093000 +DTEND;TZID=Australia/Melbourne:20260817T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid15 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260824T093000 +DTEND;TZID=Australia/Melbourne:20260824T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid16 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260907T093000 +DTEND;TZID=Australia/Melbourne:20260907T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid17 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260914T093000 +DTEND;TZID=Australia/Melbourne:20260914T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid18 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260928T093000 +DTEND;TZID=Australia/Melbourne:20260928T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid19 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261005T093000 +DTEND;TZID=Australia/Melbourne:20261005T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid20 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261012T093000 +DTEND;TZID=Australia/Melbourne:20261012T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid21 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261019T093000 +DTEND;TZID=Australia/Melbourne:20261019T110000 +SUMMARY:BUS LAW\, Tutorial +LOCATION:CA_H_H212 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Tutorial\, 23_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_H_H212 +UID:uid22 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260812T130000 +DTEND;TZID=Australia/Melbourne:20260812T150000 +SUMMARY:BUS LAW\, Workshop +LOCATION:CA_K_K309 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Workshop\, 01_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_K_K309 +UID:uid23 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260902T130000 +DTEND;TZID=Australia/Melbourne:20260902T150000 +SUMMARY:BUS LAW\, Workshop +LOCATION:CA_K_K309 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Workshop\, 01_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_K_K309 +UID:uid24 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260930T130000 +DTEND;TZID=Australia/Melbourne:20260930T150000 +SUMMARY:BUS LAW\, Workshop +LOCATION:CA_K_K309 +DESCRIPTION:BTF1010_CA_S2_ON-CAMPUS\, Workshop\, 01_OnCampus\nBUS LAW\nStaff: -\nLocation: CA_K_K309 +UID:uid25 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260729T100000 +DTEND;TZID=Australia/Melbourne:20260729T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid26 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260805T100000 +DTEND;TZID=Australia/Melbourne:20260805T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid27 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260812T100000 +DTEND;TZID=Australia/Melbourne:20260812T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid28 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260819T100000 +DTEND;TZID=Australia/Melbourne:20260819T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid29 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260826T100000 +DTEND;TZID=Australia/Melbourne:20260826T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid30 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260909T100000 +DTEND;TZID=Australia/Melbourne:20260909T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid31 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260916T100000 +DTEND;TZID=Australia/Melbourne:20260916T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid32 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260930T100000 +DTEND;TZID=Australia/Melbourne:20260930T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid33 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261007T100000 +DTEND;TZID=Australia/Melbourne:20261007T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid34 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261014T100000 +DTEND;TZID=Australia/Melbourne:20261014T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid35 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261021T100000 +DTEND;TZID=Australia/Melbourne:20261021T120000 +SUMMARY:MICRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1100_CA_S2_ON-CAMPUS\, Workshop\, 04_OnCampus\nMICRO\nStaff: -\nLocation: CA_H_H116 +UID:uid36 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260731T120000 +DTEND;TZID=Australia/Melbourne:20260731T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid37 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260807T120000 +DTEND;TZID=Australia/Melbourne:20260807T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid38 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260814T120000 +DTEND;TZID=Australia/Melbourne:20260814T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid39 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260821T120000 +DTEND;TZID=Australia/Melbourne:20260821T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid40 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260828T120000 +DTEND;TZID=Australia/Melbourne:20260828T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid41 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260904T120000 +DTEND;TZID=Australia/Melbourne:20260904T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid42 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260911T120000 +DTEND;TZID=Australia/Melbourne:20260911T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid43 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260918T120000 +DTEND;TZID=Australia/Melbourne:20260918T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid44 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261002T120000 +DTEND;TZID=Australia/Melbourne:20261002T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid45 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261009T120000 +DTEND;TZID=Australia/Melbourne:20261009T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid46 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261016T120000 +DTEND;TZID=Australia/Melbourne:20261016T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid47 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261023T120000 +DTEND;TZID=Australia/Melbourne:20261023T130000 +SUMMARY:MACRO\, Applied +LOCATION:Online\, Melbourne +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Applied\, 01_OnlineRealTime\nMACRO\nStaff: -\nLocation: Online\, Melbourne +UID:uid48 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260730T100000 +DTEND;TZID=Australia/Melbourne:20260730T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid49 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260806T100000 +DTEND;TZID=Australia/Melbourne:20260806T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid50 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260813T100000 +DTEND;TZID=Australia/Melbourne:20260813T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid51 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260820T100000 +DTEND;TZID=Australia/Melbourne:20260820T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid52 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260827T100000 +DTEND;TZID=Australia/Melbourne:20260827T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid53 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260903T100000 +DTEND;TZID=Australia/Melbourne:20260903T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid54 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260910T100000 +DTEND;TZID=Australia/Melbourne:20260910T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid55 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20260917T100000 +DTEND;TZID=Australia/Melbourne:20260917T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid56 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261001T100000 +DTEND;TZID=Australia/Melbourne:20261001T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid57 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261008T100000 +DTEND;TZID=Australia/Melbourne:20261008T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid58 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261015T100000 +DTEND;TZID=Australia/Melbourne:20261015T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid59 +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20260711T085008Z +DTSTART;TZID=Australia/Melbourne:20261022T100000 +DTEND;TZID=Australia/Melbourne:20261022T120000 +SUMMARY:MACRO\, Workshop +LOCATION:CA_H_H116 +DESCRIPTION:ECF1200_CA_S2_ON-CAMPUS\, Workshop\, 05_OnCampus\nMACRO\nStaff: -\nLocation: CA_H_H116 +UID:uid60 +END:VEVENT +END:VCALENDAR diff --git a/test/ical-io.test.js b/test/ical-io.test.js new file mode 100644 index 0000000..44cdbe6 --- /dev/null +++ b/test/ical-io.test.js @@ -0,0 +1,50 @@ +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' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const GOLDEN = readFileSync(join(__dirname, 'golden-org.ics'), 'utf-8') + +describe('parseICS', () => { + const { meta, events } = parseICS(GOLDEN) + + it('parses calendar meta', () => { + expect(meta.prodid).toBe('-//Allocate//iCal4j 1.0//EN') + expect(meta.version).toBe('2.0') + expect(meta.calscale).toBe('GREGORIAN') + expect(meta.vtimezones.length).toBe(1) + }) + + it('parses all 61 events', () => { + expect(events).toHaveLength(61) + }) + + it('parses first event fields correctly', () => { + const e = events[0] + expect(e.summary).toBe('ACC INFO SYS, Tutorial') + expect(e.location).toBe('CA_B_B471') + expect(e.dtstartDate).toBe('2026-07-27') + expect(e.dtstartTime).toBe('18:00') + expect(e.dtendTime).toBe('20:00') + expect(e.tzid).toBe('Australia/Melbourne') + expect(e.rrule).toBeNull() + expect(e.exdates).toEqual([]) + }) + + it('preserves description with escaped commas', () => { + const e = events[0] + expect(e.description).toContain('ACF2400_CA_S2_ON-CAMPUS') + expect(e.description).toContain('ACC INFO SYS') + }) + + it('captures dtstamp in _raw', () => { + expect(events[0]._raw.dtstamp).toBe('20260711T085008Z') + }) + + it('events have UIDs', () => { + expect(events[0].uid).toBe('uid0') + expect(events[60].uid).toBe('uid60') + }) +})