开发者

Generating an iCalender VTIMEZONE Component from PHP's Timezone Value

开发者 https://www.devze.com 2023-03-19 20:55 出处:网络
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

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
TZOFFSE开发者_开发百科TFROM:-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?


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.


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.


PHP has an Internationalization extension called intl, it's a simple wrapper to an underlying library from IBM called ICU - International Components for Unicode that does all the dirty work.

Now, ICU implements com.ibm.icu.util.VTimeZone, capable of generating iCal VTIMEZONE components with daylight saving time rules (using the Olson tz db of transitions).

Great!! But.. that class did not earn a wrapper in PHP.

0

精彩评论

暂无评论...
验证码 换一张
取 消