Generating an iCalender VTIMEZONE Component from PHP's Timezone Value

Sonny picture Sonny · Jul 13, 2011 · Viewed 9.3k times · Source

I am adding a feature to my event calendar application to provide iCalendar (ics) file downloads for the events. I want to generate the VTIMEZONE Component, but all I have is the PHP's Timezone value from date_default_timezone_get(). Here's an example of a VTIMEZONE Component for Eastern Time (US & Canada) that was generated by Outlook:

BEGIN:VTIMEZONE
TZID:Eastern Time (US & Canada)
BEGIN:STANDARD
DTSTART:16011104T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010311T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
END:VTIMEZONE

This would behave like PHP's "America/New_York" time zone, but how would I automate the generation of it?

Answer

brotherli picture brotherli · Sep 22, 2014

PHP's DateTimezone class works with the Olson timezone database and has some (limited) methods to access offsets, transitions and short names.

According to RFC 5545, the RRULE property is optional and therefore we should be able to generate a valid VTIMEZONE definition with the built-in utilities. Following the RFC suggestion, the following function does exactly this:

use \Sabre\VObject;

/**
 * Returns a VTIMEZONE component for a Olson timezone identifier
 * with daylight transitions covering the given date range.
 *
 * @param string Timezone ID as used in PHP's Date functions
 * @param integer Unix timestamp with first date/time in this timezone
 * @param integer Unix timestap with last date/time in this timezone
 *
 * @return mixed A Sabre\VObject\Component object representing a VTIMEZONE definition
 *               or false if no timezone information is available
 */
function generate_vtimezone($tzid, $from = 0, $to = 0)
{
    if (!$from) $from = time();
    if (!$to)   $to = $from;

    try {
        $tz = new \DateTimeZone($tzid);
    }
    catch (\Exception $e) {
        return false;
    }

    // get all transitions for one year back/ahead
    $year = 86400 * 360;
    $transitions = $tz->getTransitions($from - $year, $to + $year);

    $vt = new VObject\Component('VTIMEZONE');
    $vt->TZID = $tz->getName();

    $std = null; $dst = null;
    foreach ($transitions as $i => $trans) {
        $cmp = null;

        // skip the first entry...
        if ($i == 0) {
            // ... but remember the offset for the next TZOFFSETFROM value
            $tzfrom = $trans['offset'] / 3600;
            continue;
        }

        // daylight saving time definition
        if ($trans['isdst']) {
            $t_dst = $trans['ts'];
            $dst = new VObject\Component('DAYLIGHT');
            $cmp = $dst;
        }
        // standard time definition
        else {
            $t_std = $trans['ts'];
            $std = new VObject\Component('STANDARD');
            $cmp = $std;
        }

        if ($cmp) {
            $dt = new DateTime($trans['time']);
            $offset = $trans['offset'] / 3600;

            $cmp->DTSTART = $dt->format('Ymd\THis');
            $cmp->TZOFFSETFROM = sprintf('%s%02d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
            $cmp->TZOFFSETTO   = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '', floor($offset), ($offset - floor($offset)) * 60);

            // add abbreviated timezone name if available
            if (!empty($trans['abbr'])) {
                $cmp->TZNAME = $trans['abbr'];
            }

            $tzfrom = $offset;
            $vt->add($cmp);
        }

        // we covered the entire date range
        if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
            break;
        }
    }

    // add X-MICROSOFT-CDO-TZID if available
    $microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
    if (array_key_exists($tz->getName(), $microsoftExchangeMap)) {
        $vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
    }

    return $vt;
}

The above code example uses the Sabre VObject library to create the VTIMEZONE definition but could easily be re-written to produce plain string output.

In addition to the timezone identifier it takes two unix timestamps as arguments to define the time range we need timezone information for. Then all the relevant transitions for the given time range are listed.

I successfully tested the generated output with iTip invitations sent to Outlook which otherwise cannot match the plain Olson timezone identifiers to the Microsoft system.