Org-Mode iCalendar Export with Explicit Time Zones

For several years I used Symbian phones for calendaring, and the biggest issue for me was the lack of support for time zones: there was neither a way to specify a “floating” time, nor could you select a specific time zone for an appointment. Times would be interpreted in the context of the currently selected system-wide local time zone, and shifted later when changing the time zone setting. Consequently, I avoided ever changing time zones to retain the times as entered. I've now switched to maintaining my calendar in Emacs Org mode, and I'm finding it more comfortable.

Org mode's (“active” and “inactive”) timestamps are all floating, at least as far as the org-agenda-list view is concerned—changing the system time does not modify the entries themselves or their views (showing active-timestamp entries). The semantics of such views are perfectly reasonable, long as one remembers that an Org-Agenda view is not necessarily chronological: the video conference broadcast from Tokyo at <2018-07-03 Tue 09:00> is listed before the one broadcast from Los Angeles at <2018-07-03 Tue 11:00>, even though the former session takes place later.

Other applications might present different views, however, and I want to be able to export “zoned” or floating times for them, depending on the nature of a calendar entry. For example, one may wish to state that the Fourth of July begins at midnight local time regardless of where one happens to be, whereas a meetup organized in Helsinki would begin at a specific absolute time:

* <2018-07-04 Wed 00:00> Fourth of July begins
* <2018-07-04 Wed 18:00> hackathon in Helsinki
:PROPERTIES:
:TIMEZONE: Europe/Helsinki
:LOCATION: Helsinki, Finland
:END:

iCalendar (as specified by RFC 5545) is a popular calendar data export format, and Org mode already supports it, at least to some extent; “ox-icalendar.el” is the file that defines the export back end. It also has some limited support for specifying TIMEZONE information for exported VEVENT and VTODO entries, but that support appears to be undocumented. I've just explored what it takes to fully enable and make use of that support.

My desired time zone semantics for Org calendar entries was to have either absolute times with explicit time zone information, or floating times without such information. A “DATE-TIME” (e.g., <2018-07-03 Tue 23:00>) would be absolute only with its time zone information specified using the Org TIMEZONE property for the enclosing header; in other cases it would be floating. RFC 5545 specifies that a “DATE” can have no time zone, and consequently any date without a time component (e.g., <2018-07-03 Tue>) would effectively be interpreted as floating. Any time zones apart from "UTC" would have to be defined somewhere, with any referenced definitions ultimately included in iCalendar files. UTC times need no such definitions, as the iCalendar format defines specific syntax for them.

To realize my desired semantics, I patched Org mode 9.1.13 to make the following adjustments:

I've made the above changes available as a patch:

It is existing Org behavior to translate non-"UTC" TIMEZONE specifying date-time timestamps into RFC 5545 DATE-TIME values in the “DATE WITH LOCAL TIME AND TIME ZONE REFERENCE” form. For date-time timestamps without a TIMEZONE property, in turn, one gets the floating time semantics simply by leaving the org-icalendar-date-time-format configuration variable to its default value of ":%Y%m%dT%H%M%S"; this results in a DATE-TIME “DATE WITH LOCAL TIME” translation, as desired.

I addition to adjusting time zone export semantics, the above patch also tries to ensure that CRLF (instead of LF) line breaks are emitted throughout, which appears to be required by RFC 5545. Any “VTIMEZONE” objects should also be specified with CRLF breaks. For example:

(setq org-icalendar-vtimezone-table
      '(("Europe/Helsinki" . "BEGIN:VTIMEZONE\r\nTZID:Europe/Helsinki\r\nBEGIN:DAYLIGHT\r\nDTSTART:20100328T040000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\r\nTZNAME:EEST\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20091025T030000\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\nTZNAME:EET\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n")
        ("Europe/Oslo" . "BEGIN:VTIMEZONE\r\nTZID:Europe/Oslo\r\nBEGIN:DAYLIGHT\r\nDTSTART:20100328T030000\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\r\nTZNAME:CEST\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20091025T020000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nRRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\nTZNAME:CET\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n")))

RFC 5545 requires exactly one “VTIMEZONE” definition for each “TZID” that appears in a “DATE-TIME” or “TIME” value, and we require a way to create such definitions in the expected format, similar to the ones shown above. Operating systems tend to be equipped with information about time zones, and we would ideally extract it from there into the required format, thus ensuring that our applications have roughly the same idea of time zones as their host system does.

UNIX systems, for example, typically have time zone information in the “/usr/share/zoneinfo” directory. A number of languages have a library that is able to extract that information. For Ruby, for instance, we can install TZInfo, perhaps using the RubyGems command

gem install tzinfo

allowing us to then do

require 'tzinfo'
TZInfo::Timezone.get('Europe/Oslo')

Being one of the more library-rich languages, Ruby also has an iCalendar library, which we can use for translating tzinfo-acquired time zone information into the iCalendar format. We might

gem install icalendar

and then

require 'date'
require 'icalendar'
require 'icalendar/tzinfo'
["Europe/Helsinki", "Europe/Oslo"].each do |tzid|
  tzinfo = TZInfo::Timezone.get(tzid)
  tzical = tzinfo.ical_timezone(DateTime.new(2010))
  puts("(%s .\n %s)" % [tzid.inspect, tzical.to_ical.inspect])
end

Admittedly, it is slightly inconvenient to have to generate such strings and manually set them to org-icalendar-vtimezone-table, and this is one area where the above “ox-icalendar.el” patch could be improved. It would be more convenient to generate the necessary definitions directly from within Emacs Lisp, particularly as this would remove the dependency on another language (such as Ruby) and its libraries.

Still, with the patch applied, and org-icalendar-vtimezone-table suitably configured, I am now able to export TIMEZONE equipped Org entries as appropriately time-zoned iCalendar “VEVENT” entries. For an example, compare the following Org format test data and a screenshot of the Evolution application's Calendar view showing an overview of the same data, with the desired interpretation of the times in the local time zone of Europe/Helsinki.

test-data.org

test-evolution.png

This information is current as of Org mode version 9.1.13, Ruby tzinfo version 1.2.2, and Ruby icalendar version 2.4.1.

Update (23 August 2018): Org 9.1.14 appears to have more support for time zones built in, but without yet emitting “VTIMEZONE” records, as required for full RFC 5545 compliance.