iCal Exports for Multi-day Events

One critical feature for GoZuus is to ensure that data is available not just within the site, but also portable in usable formats.  One obvious use of this is the group calendar.  We’ve built a nice online calendar, but we’re not out to replace Google Calendar or iCal or your iPhone.  Cleanly exporting data in iCalendar format lets us feed data into whatever scheduling application our users choose to embrace, so that their GoZuus calendar works where they want it.

There’s plenty of resources online discussing how easy it is to use the Ruby Icalendar library.  However, just following those directions didn’t produce quite the results I was looking for when exporting multi-day events.  Using the directions from the previous links, Apple’s iCal app displayed every event as ending one day early.  And Google Calendar put “(12:00am?)” in front of the titles.  Not exactly seamless integration.  A quick Google search didn’t turn up anything useful, so hopefully this solution will save someone else some time.

The first one is easy.  If you dive into RFC2445, section 4.8.2.2 states that the DTEND parameter “defines the date and time by which the event ends.”  That’s an up-to-but-not-including statement.  If an event starts Feb 2 and ends Feb 5, then that means the event is over by Feb 5, and it displays as occupying only Feb 2, 3, and 4.  This is different than our model, where we define the end date to be included.  (That made more sense to me – most users who enter “Feb 2-5″ are describing a four day event.)  The solution here is just to use event.end_on.advance(:days => 1) to push it one day forward.

The second one is a little more difficult.  Google’s iCal parser interprets all DTSTART and DTEND values as times unless specifically told otherwise.  If you look at the output from the Icalendar library, it lists dates and date/times as:

    DTSTART:20090205
    DTSTART:20090205T083000

That’s valid, but according to the RFC you can add an additional parameter in there to specify what type of value you’re providing.  This looks like:

    DTSTART;VALUE=DATE:20090205
    DTSTART;VALUE=DATE-TIME:20090205T083000

I couldn’t find any documentation for how to do this with the Icalendar library, so I began digging into the source for Icalendar’s component.rb.  Turns out there’s a function called print_parameters(val) that is called during generation, and that it prints these pairs from hash called val.ical_params.  So if you assign event.start.ical_params = {'VALUE' => 'DATE'}, the Icalendar library will add in these additional descriptors.  Seems to be undocumented, but it works for me.

Note that since ical_params is a parameter of the object itself, you can only assign this hash after you’ve assigned the object itself.  Also, it didn’t seem to work for me when the value I had assigned was a String.  It did work when I assigned a Date or DateTime object directly.

All in all, our Event#to_ical looks like this:

def to_ical
  ical = Icalendar::Event.new
  if self.multi_day? || self.all_day?
    ical.start = self.start_on
    ical.start.ical_params = {'VALUE' => 'DATE'}
  else
    ical.start = self.start_at
    ical.start.ical_params = {'VALUE' => 'DATE-TIME'}
  end
  if self.multi_day?
    ical.end = self.end_on.advance(:days => 1)
    ical.end.ical_params = {'VALUE' => 'DATE'}
  end
  ical.summary = self.name
  ical.description = self.details
  ical.uid = "#{self.id}@gozuus"
  ical.created = self.created_at.utc
  ical.last_modified = self.updated_at.utc
  return ical
end

For completeness, here’s the output portion of our controller action:

format.ics do
  @ical = Icalendar::Calendar.new
  @events.each { |event| @ical.add event.to_ical }
  @ical.publish
  render :text => @ical.to_ical, :layout => false
end

One Response to “iCal Exports for Multi-day Events”

  1. Mazuhl  on October 28th, 2009

    A great write up – very useful. And thanks for sharing your code snippets too.

Leave a Reply