Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating an iCalender VTIMEZONE Component from PHP's Timezone Value

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?

like image 991
Sonny Avatar asked Jul 13 '11 16:07

Sonny


2 Answers

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.

like image 182
brotherli Avatar answered Oct 29 '22 13:10

brotherli


Another way of approaching this, instead of trying to generate a VTIMEZONE config for the targeted timezone, is to take one fixed base time zone (for instance, the "America/New_York" / "Eastern Time (US & Canada)" zone above), and convert the VEVENT's values to it using PHP's DateTime class.

$Date = new DateTime( $event_date ); // this will be in the server's time zone

// convert it to the 'internal' time zone
$Date->setTimezone( new DateTimeZone( 'America/New_York' ) );

// ...

echo "BEGIN:VEVENT\n";
echo "DTSTART;TZID=America/New_York:" . $Date->format( 'Ymd\THis' ) . "\n"

The recipient's calendar client will automatically take care of converting the times to the targeted time zone!

Not a direct answer, but it solved my problem.

like image 32
Rijk Avatar answered Oct 29 '22 11:10

Rijk