. * * This file is a part of iCalcreator. */ namespace Kigkonsult\Icalcreator; use Kigkonsult\Icalcreator\Util\Util; use DateTimeZone; use DateTime; use Exception; use function array_keys; use function date; use function end; use function explode; use function floor; use function is_array; use function is_int; use function is_null; use function ksort; use function reset; use function sprintf; use function str_replace; use function strcasecmp; use function strpos; use function strtolower; use function substr; use function trim; /** * iCalcreator timezone management class * * Manages loosely coupled iCalcreator Vcalendar (timezone) functions * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.26 - 2018-11-10 */ class TimezoneHandler { private static $FMTTIMESTAMP = '@%s'; private static $OFFSET = 'offset'; private static $TIME = 'time'; /** * Create a calendar timezone and standard/daylight components * * Result when 'Europe/Stockholm' and no from/to arguments is used as timezone: * BEGIN:VTIMEZONE * TZID:Europe/Stockholm * BEGIN:STANDARD * DTSTART:20101031T020000 * TZOFFSETFROM:+0200 * TZOFFSETTO:+0100 * TZNAME:CET * END:STANDARD * BEGIN:DAYLIGHT * DTSTART:20100328T030000 * TZOFFSETFROM:+0100 * TZOFFSETTO:+0200 * TZNAME:CEST * END:DAYLIGHT * END:VTIMEZONE * * Generates components for all transitions in a date range, * based on contribution by Yitzchok Lavi * Additional changes jpirkey * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.26 - 2018-11-10 * @param Vcalendar $calendar iCalcreator calendar instance * @param string $timezone valid timezone avveptable by PHP5 DateTimeZone * @param array $xProp *[x-propName => x-propValue] * @param int $from unix timestamp * @param int $to unix timestamp * @return bool * @static */ public static function createTimezone( Vcalendar $calendar, $timezone, $xProp = [], $from = null, $to = null ) { static $Y = 'Y '; static $YMD = 'Ymd'; static $T000000 = 'T000000'; static $MINUS7MONTH = '-7 month'; static $YMD2 = 'Y-m-d'; static $T235959 = 'T235959'; static $PLUS18MONTH = '+18 month'; static $TS = 'ts'; static $YMDHIS3 = 'Y-m-d-H-i-s'; static $SECONDS = 'seconds'; static $ABBR = 'abbr'; static $ISDST = 'isdst'; static $NOW = 'now'; static $YMDTHISO = 'Y-m-d\TH:i:s O'; if( empty( $timezone )) { return false; } if( ! empty( $from ) && ! is_int( $from )) { return false; } if( ! empty( $to ) && ! is_int( $to )) { return false; } try { $newTz = new DateTimeZone( $timezone ); $utcTz = new DateTimeZone( Util::$UTC ); } catch( Exception $e ) { return false; } if( empty( $from ) || empty( $to )) { $dates = array_keys( $calendar->getProperty( Util::$DTSTART )); if( empty( $dates )) { $dates = [ date( $YMD ) ]; } } if( ! empty( $from )) { try { $timestamp = sprintf( self::$FMTTIMESTAMP, $from ); $dateFrom = new DateTime( $timestamp ); // set lowest date (UTC) } catch( Exception $e ) { return false; } } else { try { $from = reset( $dates ); // set lowest date to the lowest dtstart date $dateFrom = new DateTime( $from . $T000000, $newTz ); $dateFrom->modify( $MINUS7MONTH ); // set $dateFrom to seven month before the lowest date $dateFrom->setTimezone( $utcTz ); // convert local date to UTC } catch( Exception $e ) { return false; } } $dateFromYmd = $dateFrom->format( $YMD2 ); if( ! empty( $to )) { try { $timestamp = sprintf( self::$FMTTIMESTAMP, $to ); $dateTo = new DateTime( $timestamp ); // set end date (UTC) } catch( Exception $e ) { return false; } } else { try { $to = end( $dates ); // set highest date to the highest dtstart date $dateTo = new DateTime( $to . $T235959, $newTz ); } catch( Exception $e ) { return false; } $dateTo->modify( $PLUS18MONTH ); // set $dateTo to 18 month after the highest date $dateTo->setTimezone( $utcTz ); // convert local date to UTC } $dateToYmd = $dateTo->format( $YMD2 ); $transTemp = []; $prevOffsetfrom = 0; $stdIx = $dlghtIx = null; $prevTrans = false; $transitions = $newTz->getTransitions(); // For generic timezones, there is no transition if ( ! $transitions ) { try { $timestamp = sprintf( self::$FMTTIMESTAMP, $newTz->timezone ); $date = new \DateTime( $timestamp ); } catch( \Exception $e ) { return false; } } else { foreach( $transitions as $tix => $trans ) { // all transitions in date-time order!! if( 0 > (int) date( $Y, $trans[$TS] )) { // skip negative year... but save offset $prevOffsetfrom = $trans[self::$OFFSET]; // previous trans offset will be 'next' trans offsetFrom continue; } try { $timestamp = sprintf( self::$FMTTIMESTAMP, $trans[$TS] ); $date = new DateTime( $timestamp ); // set transition date (UTC) } catch( Exception $e ) { return false; } $transDateYmd = $date->format( $YMD2 ); if( $transDateYmd < $dateFromYmd ) { $prevOffsetfrom = $trans[self::$OFFSET]; // previous trans offset will be 'next' trans offsetFrom $prevTrans = $trans; // we save it in case we don't find any that match $prevTrans[Util::$TZOFFSETFROM] = ( 0 < $tix ) ? $transitions[$tix - 1][self::$OFFSET] : 0; continue; } if( $transDateYmd > $dateToYmd ) { break; } // loop always (?) breaks here if( ! empty( $prevOffsetfrom ) || ( 0 == $prevOffsetfrom )) { $trans[Util::$TZOFFSETFROM] = $prevOffsetfrom; // i.e. set previous offsetto as offsetFrom $date->modify( $trans[Util::$TZOFFSETFROM] . $SECONDS ); // convert utc date to local date $d = \explode( Util::$MINUS, $date->format( $YMDHIS3 )); $trans[self::$TIME] = [ Util::$LCYEAR => (int) $d[0], // set date to array Util::$LCMONTH => (int) $d[1], // to ease up dtstart and (opt) rdate setting Util::$LCDAY => (int) $d[2], Util::$LCHOUR => (int) $d[3], Util::$LCMIN => (int) $d[4], Util::$LCSEC => (int) $d[5], ]; } $prevOffsetfrom = $trans[self::$OFFSET]; if( true !== $trans[$ISDST] ) { // standard timezone if( ! empty( $stdIx ) && isset( $transTemp[$stdIx][Util::$TZOFFSETFROM] ) && ( $transTemp[$stdIx][$ABBR] == $trans[$ABBR] ) && ( $transTemp[$stdIx][Util::$TZOFFSETFROM] == $trans[Util::$TZOFFSETFROM] ) && ( $transTemp[$stdIx][self::$OFFSET] == $trans[self::$OFFSET] )) { $transTemp[$stdIx][Util::$RDATE][] = $trans[self::$TIME]; continue; // check for any repeating rdate's (in order) } $stdIx = $tix; } // end standard timezone else { // daylight timezone if( ! empty( $dlghtIx ) && isset( $transTemp[$dlghtIx][Util::$TZOFFSETFROM] ) && ( $transTemp[$dlghtIx][$ABBR] == $trans[$ABBR] ) && ( $transTemp[$dlghtIx][Util::$TZOFFSETFROM] == $trans[Util::$TZOFFSETFROM] ) && ( $transTemp[$dlghtIx][self::$OFFSET] == $trans[self::$OFFSET] )) { $transTemp[$dlghtIx][Util::$RDATE][] = $trans[self::$TIME]; continue; // check for any repeating rdate's (in order) } $dlghtIx = $tix; } // end daylight timezone $transTemp[$tix] = $trans; } // end foreach( $transitions as $tix => $trans ) } $timezoneComp = $calendar->newVtimezone(); $timezoneComp->setproperty( Util::$TZID, $timezone ); if( ! empty( $xProp )) { foreach( $xProp as $xPropName => $xPropValue ) { if( Util::isXprefixed( $xPropName )) { $timezoneComp->setproperty( $xPropName, $xPropValue ); } } } if( empty( $transTemp )) { // if no match is found if( $prevTrans ) { // we use the last transition (before startdate) for the tz info try { $timestamp = sprintf( self::$FMTTIMESTAMP, $prevTrans[$TS] ); $date = new DateTime( $timestamp ); // set transition date (UTC) } catch( Exception $e ) { return false; } $date->modify( $prevTrans[Util::$TZOFFSETFROM] . $SECONDS );// convert utc date to local date $d = explode( Util::$MINUS, $date->format( $YMDHIS3 ) ); // set arr-date to ease up dtstart setting $prevTrans[self::$TIME] = [ Util::$LCYEAR => (int) $d[0], Util::$LCMONTH => (int) $d[1], Util::$LCDAY => (int) $d[2], Util::$LCHOUR => (int) $d[3], Util::$LCMIN => (int) $d[4], Util::$LCSEC => (int) $d[5], ]; $transTemp[0] = $prevTrans; } // end if( $prevTrans ) else { // or we use the timezone identifier to BUILD the standard tz info (?) try { $date = new DateTime( $NOW, $newTz ); } catch( Exception $e ) { return false; } $transTemp[0] = [ self::$TIME => $date->format( $YMDTHISO ), self::$OFFSET => $date->format( Util::$Z ), Util::$TZOFFSETFROM => $date->format( Util::$Z ), $ISDST => false, ]; } } // end if( empty( $transTemp )) foreach( $transTemp as $tix => $trans ) { // create standard/daylight subcomponents $subComp = ( true !== $trans[$ISDST] ) ? $timezoneComp->newStandard() : $timezoneComp->newDaylight(); $subComp->setProperty( Util::$DTSTART, $trans[self::$TIME] ); if( ! empty( $trans[$ABBR] )) { $subComp->setProperty( Util::$TZNAME, $trans[$ABBR] ); } if( isset( $trans[Util::$TZOFFSETFROM] )) { $subComp->setProperty( Util::$TZOFFSETFROM, self::offsetSec2His( $trans[Util::$TZOFFSETFROM] )); } $subComp->setProperty( Util::$TZOFFSETTO, self::offsetSec2His( $trans[self::$OFFSET] )); if( isset( $trans[Util::$RDATE] )) { $subComp->setProperty( Util::$RDATE, $trans[Util::$RDATE] ); } } return true; } /** * Return iCal offset [-/+]hhmm[ss] (string) from UTC offset seconds * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.26 - 2018-11-10 * @param string $seconds * @return string * @static */ public static function offsetSec2His( $seconds ) { static $FMT = '%02d'; switch( substr( $seconds, 0, 1 )) { case Util::$MINUS : $output = Util::$MINUS; $seconds = substr( $seconds, 1 ); break; case Util::$PLUS : $output = Util::$PLUS; $seconds = substr( $seconds, 1 ); break; default : $output = Util::$PLUS; break; } $output .= sprintf( $FMT, ((int) floor( $seconds / 3600 ))); // hour $seconds = $seconds % 3600; $output .= sprintf( $FMT, ((int) floor( $seconds / 60 ))); // min $seconds = $seconds % 60; if( 0 < $seconds ) { $output .= sprintf( $FMT, $seconds ); // sec } return $output; } /** * Very basic conversion of a MS timezone to a PHP5 valid (Date-)timezone * matching (MS) UCT offset and time zone descriptors * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.23.8 - 2017-04-17 * @param string $timezone to convert * @return bool * @static */ public static function ms2phpTZ( & $timezone ) { static $REPL1 = [ 'GMT', 'gmt', 'utc' ]; static $REPL2 = [ '(', ')', '&', ',', ' ' ]; static $PUTC = '(UTC'; static $ENDP = ')'; static $TIMEZONE_ID = 'timezone_id'; if( empty( $timezone )) { return false; } $search = str_replace( Util::$QQ, null, $timezone ); $search = str_replace( $REPL1, Util::$UTC, $search ); if( $PUTC != substr( $search, 0, 4 )) { return false; } if( false === ( $pos = strpos( $search, $ENDP ))) { return false; } $searchOffset = substr( $search, 4, ( $pos - 4 )); $searchOffset = Util::tz2offset( str_replace( Util::$COLON, null, $searchOffset )); while( Util::$SP1 == $search[( $pos + 1 )] ) { $pos += 1; } $searchText = trim( str_replace( $REPL2, Util::$SP1, substr( $search, ( $pos + 1 )))); $searchWords = explode( Util::$SP1, $searchText ); try { $timezoneAbbreviations = DateTimeZone::listAbbreviations(); } catch( Exception $e ) { return false; } $hits = []; foreach( $timezoneAbbreviations as $name => $transitions ) { foreach( $transitions as $cnt => $transition ) { if( empty( $transition[self::$OFFSET] ) || empty( $transition[$TIMEZONE_ID] ) || ( $transition[self::$OFFSET] != $searchOffset )) { continue; } $cWords = explode( Util::$L, $transition[$TIMEZONE_ID] ); $cPrio = $hitCnt = $rank = 0; foreach( $cWords as $cWord ) { if( empty( $cWord )) { continue; } $cPrio += 1; $sPrio = 0; foreach( $searchWords as $sWord ) { if( empty( $sWord ) || ( self::$TIME == strtolower( $sWord ))) { continue; } $sPrio += 1; if( 0 == strcasecmp( $cWord, $sWord )) { $hitCnt += 1; $rank += ( $cPrio + $sPrio ); } else { $rank += 10; } } } if( 0 < $hitCnt ) { $hits[$rank][] = $transition[$TIMEZONE_ID]; } } // end foreach( $transitions as $cnt => $transition ) } // end foreach( $timezoneAbbreviations as $name => $transitions ) if( empty( $hits )) { return false; } ksort( $hits ); foreach( $hits as $rank => $tzs ) { if( ! empty( $tzs )) { $timezone = reset( $tzs ); return true; } } return false; } /** * Transforms a dateTime from a timezone to another * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.22.23 - 2017-03-04 * @param mixed $date date to alter * @param string $tzFrom PHP valid 'from' timezone * @param string $tzTo PHP valid 'to' timezone, default Util::$UTC * @param string $format date output format, default 'Ymd\THis' * @return bool true on success, false on error * @static */ public static function transformDateTime( & $date, $tzFrom, $tzTo = null, $format = null ) { static $YMDTHIS = 'Ymd\THis'; if( is_null( $tzTo )) { $tzTo = Util::$UTC; } elseif( Util::$Z == $tzTo ) { $tzTo = Util::$UTC; } if( is_null( $format )) { $format = $YMDTHIS; } if( is_array( $date ) && isset( $date[Util::$LCTIMESTAMP] )) { try { $timestamp = sprintf( self::$FMTTIMESTAMP, $date[Util::$LCTIMESTAMP] ); $d = new DateTime( $timestamp ); // set UTC date $newTz = new DateTimeZone( $tzFrom ); $d->setTimezone( $newTz ); // convert to 'from' date } catch( Exception $e ) { return false; } } else { if( Util::isArrayDate( $date )) { if( isset( $date[Util::$LCtz] )) { unset( $date[Util::$LCtz] ); } $date = Util::date2strdate( Util::chkDateArr( $date )); } if( Util::$Z == substr( $date, -1 )) { $date = substr( $date, 0, ( \strlen( $date ) - 2 )); } try { $d = new DateTime( $date, new DateTimeZone( $tzFrom )); } catch( Exception $e ) { return false; } } try { $newTz = new DateTimeZone( $tzTo ); $d->setTimezone( $newTz ); } catch( Exception $e ) { return false; } $date = $d->format( $format ); return true; } }