Skip to content

Instantly share code, notes, and snippets.

@thomascube
Last active February 15, 2023 21:15
Show Gist options
  • Save thomascube/47ff7d530244c669825736b10877a200 to your computer and use it in GitHub Desktop.
Save thomascube/47ff7d530244c669825736b10877a200 to your computer and use it in GitHub Desktop.
VTIMEZONE component for a Olson timezone identifier with daylight transitions.Solution to this StackOverflow question: https://stackoverflow.com/questions/6682304/generating-an-icalender-vtimezone-component-from-phps-timezone-value/25971680
<?php
use \Sabre\VObject;
// use composer autoloader
require_once 'vendor/autoload.php';
/**
* 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);
$vcalendar = new VObject\Component\VCalendar();
$vt = $vcalendar->createComponent('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 = $vcalendar->createComponent('DAYLIGHT');
$cmp = $dst;
}
// standard time definition
else {
$t_std = $trans['ts'];
$std = $vcalendar->createComponent('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 ? '+' : '-', abs(floor($tzfrom)), ($tzfrom - floor($tzfrom)) * 60);
$cmp->TZOFFSETTO = sprintf('%s%02d%02d', $offset >= 0 ? '+' : '-', abs(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;
}
$vtimezone = generate_vtimezone('Europe/Berlin');
print $vtimezone->serialize();
@jbusuttil
Copy link

Thank you for this really helpful code! I have a slight patch to make sure that negative offsets are correctly padded with 0s. There may be a more elegant way of doing this, but this seems to work.

Change these lines:

$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);

to

$cmp->TZOFFSETFROM = sprintf($tzfrom >= 0 ? '%s%02d%02d' : '%s%03d%02d', $tzfrom >= 0 ? '+' : '', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
$cmp->TZOFFSETTO = sprintf($tzfrom >= 0 ? '%s%02d%02d' : '%s%03d%02d', $offset >= 0 ? '+' : '', floor($offset), ($offset - floor($offset)) * 60);

@thomascube
Copy link
Author

I have a slight patch to make sure that negative offsets are correctly padded with 0s

Thanks! I have updated the Gist in a slightly different way but with the same result.

@alies-dev
Copy link

// get all transitions for one year back/ahead

Do you guys know, why we need transitions exactly for 2 years? When I build a ics file for a single event, can I just specify transitions for [$dateFrom; $dateTo]?

@alies-dev
Copy link

also, condition if ($cmp) { is unneeded, $cmp is always a Component instance

@mikkorantalainen
Copy link

You can use sprintf() to do both the padding and the sign. However, to get correct rounding towards zero for the hour part, you cannot use floor() because it doesn't go towards the zero:

sprintf('%+03d%02d', (int)$tzfrom, (abs(fmod($tzfrom, 1))) * 60)

The %+03d uses value 3 because that number counts the whole width of the field including the sign.

Also note the fixed rounding for e.g. $tzfrom = -7.25.

That would still be buggy for hypothetical timezones between -0059 and -0001 but I'm pretty sure those do not exist in reality. This happens because sprintf() cannot emit -00 for "negative zero". If you want to handle such values, too, I think you will have to use ternary operator (? :) or a proper if. In that case, the best I can think of is

sprintf("%s%02d%02d", $tzfrom < 0 ? "-" : "+", abs($tzfrom), (fmod(abs($tzfrom), 1)) * 60)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment