Skip to content

Common Use Cases

Philipp Kewisch edited this page May 5, 2024 · 2 revisions

Expanding Recurring Events

When dealing with iCalendar or jCal, we sometimes want to expand occurrences from a recurring event rule. Given the multi-layer approach of ICAL.js there are multiple ways to achieve this.

Via RecurExpansion

This is a higher layer on the model that attempts to abstract away some of the complexity of bringing together RRULEs, RDATEs and EXDATEs.

Occurrences are iterated from the first occurrence, because there are some rule parts in the spec that make it not straightforward to start somewhere in the middle. This means you'll need to skip occurrences before your range.

let expand = new ICAL.RecurExpansion({
  component: event,
  dtstart: event.getFirstPropertyValue('dtstart')
});

let rangeStart = ICAL.Time.fromString("2013-04-19T00:00:00");
let rangeEnd = ICAL.Time.fromString("2013-04-21T00:00:00");

let next = iterator.next()
for (let next = iterator.next(); next && next.compare(rangeEnd) < 0; next = iterator.next()) {
 if (next.compare(rangeStart) < 0) {
    continue;
  }
  // Do something with the date
}

Via Components and Properties

On the lower level, working with components and properties, you can iterate occurrences for a specific RRULE, you can grab alle RDATE and EXDATE properties to get their date or period. This gives you more control, but also requires you to piece together the dates and exceptions on your own.

let vcalendar = new ICAL.Component(ICAL.parse(ics));
let vevent = vcalendar.getFirstSubcomponent("vevent");

// Let's start with RRULEs
let recur = vevent.getFirstPropertyValue("rrule");

// When creating the iterator, you must use the DTSTART of the event, it is used for the basis of some calculations
let dtstart = vevent.getFirstPropertyValue("dtstart");
let iterator = recur.iterator(dtstart); 

let rangeStart = ICAL.Time.fromString("2013-04-19T00:00:00");
let rangeEnd = ICAL.Time.fromString("2013-04-21T00:00:00");

// Iterate through the start dates in the range.
let next = iterator.next()
for (let next = iterator.next(); next && next.compare(rangeEnd) < 0; next = iterator.next()) {
  if (next.compare(rangeStart) < 0) {
    continue;
  }
  // Do something with the date
}

// Now grab some RDATEs (additional occurrences) and EXDATEs (removed occurrences)
let rdates = vevent.getAllProperties("rdate").reduce((acc, prop) => acc.concat(prop.getValues()));
let exdates = vevent.getAllProperties("rdate").reduce((acc, prop) => acc.concat(prop.getValues()));
// Do somthing with the dates

For completeness, keep in mind that there can be modified occurrences (recurrence exceptions) in the series. These are events where a certain aspect of a specific occurrence has changed, e.g. this week's meeting is 30 minutes earlier. You can identify them in that they have a RECURRENCE-ID property on the VEVENT, which points to the original start date of the occurrence.

Time zones

Back in the days, it was pretty much mandatory to have a VTIMEZONE component on each calendar you have to define when DST changes. There were a bunch of very common time zones such as Europe/Berlin, and people realized it doesn't make sense to maintain the DST data on each calendar. So it was agreed upon that for the standardized names from the IANA time zone database, there isn't a need for VTIMEZONEs.

ICAL.js by default doesn't contain the IANA time zone database, as time zones change often. Instead, there is a TimezoneService which you can use to register your own time zones. This is particularly helpful if you are building as part of an app that already provides time zone information. Alternatively, If you do your own build from ICAL.js source, you can use the available tooling to import the IANA time zone database. We may be able to automate releases in the future.

There are two built-in time zones:

  • Floating time / local time: This is essentially "no time zone". You might use it to express that it should be that time in any local time zone in the world. Example: DTSTART:20240102T030405
  • UTC: This is universal coordinated time as you know it, Example: DTSTART:20240102T030405Z
  • All other time zones are fed in with a TZID parameter and a local time representation, e.g. DTSTART;TZID=Europe/Berlin:20240102T030405
  • Dates inherently have no time zone and are floating time, otherwise it would be a timed event from midnight to midnight.

Here is a simple time zone conversion.

// We're going to start by registering a few time zones. You'll want to import these from the IANA database.
let berlinComp = new ICAL.Component(ICAL.parse(`
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE`));
ICAL.TimezoneService.register("Europe/Berlin", new ICAL.Timezone({ component: berlinComp, tzid: "Europe/Berlin" }));

let losAngelesComp = new ICAL.Component(ICAL.parse(
`BEGIN:VTIMEZONE
TZID:America/Los_Angeles
X-LIC-LOCATION:America/Los_Angeles
BEGIN:DAYLIGHT
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
TZNAME:PDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
TZNAME:PST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE`));
ICAL.TimezoneService.register("America/Los_Angeles", new ICAL.Timezone({ component: losAngelesComp, tzid: "America/Los_Angeles" }));

// You can grab a time with a time zone from a property value
let property = ICAL.Property.fromString("DTSTART;TZID=Europe/Berlin:20240102T030405");
let fromDate = property.getFirstValue();

let toDate = fromDate.convertToZone(ICAL.TimezoneService.get("America/Los_Angeles"));
console.log(fromDate.toString(), "->", toDate.toString());
/* 2024-01-02T03:04:05 -> 2024-01-01T18:04:05 */


// If you convert a local time to a time zone, it will assign the time zone
fromDate = ICAL.Time.fromString("2024-03-04T10:11:12");
toDate = fromDate.convertToZone(ICAL.TimezoneService.get("America/Los_Angeles"));
console.log(fromDate.toString(), "->", toDate.toString());
/* 2024-03-04T10:11:12 -> 2024-03-04T10:11:12 */

fromDate = toDate;
toDate = fromDate.convertToZone(ICAL.TimezoneService.get("Europe/Berlin"));
console.log(fromDate.toString(), "->", toDate.toString());
/* 2024-03-04T10:11:12 -> 2024-03-04T19:11:12 */