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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With