-
-
Save thomascube/47ff7d530244c669825736b10877a200 to your computer and use it in GitHub Desktop.
<?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(); | |
// 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]?
also, condition if ($cmp) {
is unneeded, $cmp
is always a Component
instance
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)
Thanks! I have updated the Gist in a slightly different way but with the same result.