feat: implement parseICS with ical.js

This commit is contained in:
timeTable2 dev
2026-07-13 18:26:14 +08:00
parent 76e08d67d8
commit 138b89e3bf
3 changed files with 745 additions and 0 deletions

121
src/lib/ical-io.js Normal file
View File

@@ -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 }
}

574
test/golden-org.ics Normal file
View File

@@ -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

50
test/ical-io.test.js Normal file
View File

@@ -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')
})
})