Skip to content

Timezones

Michael Angstadt edited this page Sep 20, 2019 · 7 revisions

Note: This page refers to versions 0.5.0 and later. See the Timezones-0.4.6 if you are using an earlier version.

The Data Model

TimezoneInfo class

biweekly keeps all timezone-related information separate from the ICalendar data model. It stores this information in a TimezoneInfo object, which can be retrieved by calling ICalendar.getTimezoneInfo().

TimezoneInfo can be used to retrieve the timezone information from a parsed iCalendar object. For example, it records what timezone a particular property was originally formatted in. It also stores the original VTIMEZONE components that were in the parsed iCalendar object.

ICalReader reader = ...
ICalendar ical = reader.readNext();
reader.close();

TimezoneInfo tzinfo = ical.getTimezoneInfo();
Collection<VTimezone> components = tzinfo.getComponents();
TimezoneAssignment dstartTimezone = tzinfo.getTimezone(ical.getEvents(0).getDateStart());

//this will always be empty because biweekly does not consider VTIMEZONE components to be part of the actual iCalendar data
assertTrue(ical.getComponents(VTimezone.class).isEmpty());

TimezoneInfo can also be used to define what timezones to use when writing an ICalendar object to an output stream. For example, you can parse an iCalendar file that uses New York time, and then write it back out using Paris time.

ICalendar ical = ...

TimezoneInfo tzinfo = ical.getTimezoneInfo();
TimezoneAssignment paris = ...
tzinfo.setDefaultTimezone(paris);

ICalWriter writer = ...
writer.write(ical);
writer.close();

TimezoneAssignment class

The TimezoneAssignment class is used to define each timezone. This object stores the following information:

  1. A java.util.TimeZone object that is used to perform the actual formatting and parsing of the date-time values.
  2. ONE of the following:
    • A VTIMEZONE component that contains the equivalent timezone definition in iCalendar form.
    • OR a global ID that represents the timezone.

The next section describes the difference between VTIMEZONE components and global IDs.

Two Approaches to Defining Timezones

VTIMEZONE Component

A VTIMEZONE component contains the actual definition of a timezone. Most iCalendar consumers/producers seem use this approach.

Below is an iCalendar object that contains an event with a start date that is formatted in New York time. Note that the VTIMEZONE component in this example has been shortened dramatically--the actual definition (taken from tzurl.org) is 237 lines long. The DTSTART property uses the TZID parameter to link it to the VTIMEZONE component (the TZID parameter matches the TZID property).

BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/New_York
TZURL:http://tzurl.org/zoneinfo/America/New_York
X-LIC-LOCATION:America/New_York
BEGIN:DAYLIGHT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
...
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/New_York:20160712T203500
END:VEVENT
END:VCALENDAR

Global ID

By contrast, global IDs are much more light-weight. The reason for this is that they do not include the actual timezone definition like VTIMEZONE components do. They rely on the client parsing the iCalendar file to know how to parse a date in that timezone.

The downside to this approach is that the iCalendar specification does not define a list of these IDs. Therefore, the client consuming the iCalendar object must know how to interpret such an ID. However, my guess is that, if a client does support global IDs, they will use IDs found the public-domain TZ database, as this is a commonly-used standard (examples: "America/New_York", "Europe/Paris").

The iCalendar object below is the same as the one above, but with a global ID instead of a VTIMEZONE component. Note the / character prefixing the TZID parameter value--this is what marks it as a global ID.

BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART;TZID=/America/New_York:20160712T203500
END:VEVENT
END:VCALENDAR

Writing iCalendar objects with timezones

By default, when you instantiate a fresh ICalendar object, biweekly will output all of its date-time property values in UTC time. However, it is possible to change this to a timezone of your choosing.

The example below writes an iCalendar object using New York time. It downloads an appropriate VTIMEZONE component from tzurl.org.

//build a TimezoneAssignment object
TimeZone javaTz = TimeZone.getTimeZone("America/New_York");
TimezoneAssignment newYork = TimezoneAssignment.download(javaTz, false);

//assign it to the ICalendar object
ICalendar ical = ...
ical.getTimezoneInfo().setDefaultTimezone(newYork);

//write the ICalendar object
ICalWriter writer = ...
writer.write(ical);
writer.close();

Alternatively, you could use a global ID.

//build a TimezoneAssignment object
TimeZone javaTz = TimeZone.getTimeZone("America/New_York");
String globalId = "America/New_York"; //do not prefix this with "/", biweekly will do this
TimezoneAssignment newYork = new TimezoneAssignment(javaTz, globalId);

//assign it to the ICalendar object
ICalendar ical = ...
ical.getTimezoneInfo().setDefaultTimezone(newYork);

//write the ICalendar object
ICalWriter writer = ...
writer.write(ical);
writer.close();

You can even assign timezones to individual properties if you really want to.

TimezoneAssignment newYork = TimezoneAssignment.download(
  TimeZone.getTimeZone("America/New_York"), false
);
TimezoneAssignment paris = TimezoneAssignment.download(
  TimeZone.getTimeZone("Europe/Paris"), false
);

ICalendar ical = ...
ical.getTimezoneInfo().setDefaultTimezone(newYork);
DateStart dtstart = ical.getEvents(0).getDateStart();
ical.getTimezoneInfo().setTimezone(dtstart, paris); //overrides the default timezone

ICalWriter writer = ...
writer.write(ical);
writer.close();

If you are writing multiple iCalendar objects to the same output stream, you can instruct the writer to use a global timezone for everything. The global timezone will override the timezone settings in each individual iCalendar object.

List<ICalendar> icals = ...

TimezoneAssignment newYork = TimezoneAssignment.download(
  TimeZone.getTimeZone("America/New_York"), false
);

ICalWriter writer = ...
writer.setGlobalTimezone(newYork);
for (ICalendar ical : icals){
  writer.write(ical);
}
writer.close();

Parsing iCalendar objects with timezones

The timezone information of a parsed iCalendar object is stored in its TimezoneInfo object.

ICalReader reader = ...
ICalendar ical = reader.readNext();
reader.close();

TimezoneInfo tzinfo = ical.getTimezoneInfo();
TimezoneAssignment dstartTimezone = tzinfo.getTimezone(ical.getEvents(0).getDateStart());
Collection<VTimezone> allComponents = tzinfo.getComponents();

One use for this is for calculating recurrence rules. Recurrence rules must know the timezone that the start date was originally formatted in in order to correctly compute the dates.

ICalendar ical = ...
TimezoneInfo tzinfo = ical.getTimezoneInfo();

VEvent event = ical.getEvents().get(0);
DateStart dtstart = event.getDateStart();

TimeZone timezone;
if (tzinfo.isFloating(dtstart)){
  timezone = TimeZone.getDefault();
} else {
  TimezoneAssignment dtstartTimezone = tzinfo.getTimezone(dtstart);
  timezone = (dtstartTimezone == null) ? TimeZone.getTimeZone("UTC") : dtstartTimezone.getTimeZone();
}
DateIterator it = event.getDateIterator(timezone);

X-WR-TIMEZONE

It seems many calendar systems make use of the non-standard X-WR-TIMEZONE property. This property appears to define the primary timezone of the calendar from which the iCalendar file was created.

Biweekly's timezone system does not recognize this property. However, you could use it to determine how to display event times to the end-user in order to provide a better user experience.

ICalReader reader = ...
ICalendar ical = reader.readNext();
reader.close();

RawProperty wrTimezone = ical.getExperimentalProperty("X-WR-TIMEZONE");
TimeZone displayTz = (wrTimezone == null) ? TimeZone.getDefault() : TimeZone.getTimeZone(wrTimezone.getValue());

DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm");
df.setTimeZone(displayTz);

for (VEvent event : ical.getEvents()) {
  System.out.println(
    event.getSummary().getValue() +
    " starts at " +
    df.format(event.getDateStart().getValue())
  );
}