. * * This file is a part of iCalcreator. */ namespace Kigkonsult\Icalcreator\Util; use DateTimeInterface; use Exception; use InvalidArgumentException; use Kigkonsult\Icalcreator\Vcalendar; use function array_keys; use function count; use function gmdate; use function end; use function is_array; use function reset; use function sprintf; /** * VtimezonePopulateFactory class, iCalcreator instance Vtimezone populate class * * Result when timezone is 'Europe/Stockholm' and no from/to arguments * 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 * * @author Kjell-Inge Gustafsson, kigkonsult * @since 2.29.16 - 2020-01-25 * * Contributors : * Yitzchok Lavi * jpirkey * */ class VtimezonePopulateFactory { /* * @var string for populate method (and descendents) * @static */ private static $ABBR = 'abbr'; private static $AT = '@'; private static $ISDST = 'isdst'; private static $OFFSET = 'offset'; private static $SECONDS = 'seconds'; private static $TIME = 'time'; private static $TS = 'ts'; private static $YMD = 'Ymd'; /** * Return calendar with timezone and standard/daylight components * * @param Vcalendar $calendar iCalcreator calendar instance * @param string $timezone valid timezone acceptable by PHP5 DateTimeZone * @param array $xProp *[x-propName => x-propValue] * @param DateTimeInterface|int $start .. or unix timestamp * @param DateTimeInterface|int $end .. or unix timestamp * @return Vcalendar * @throws Exception * @throws InvalidArgumentException * @static * @since 2.29.16 - 2020-01-25 */ public static function process( Vcalendar $calendar, $timezone = null, $xProp = [], $start = null, $end = null ) { $timezone = self::getTimezone( $calendar, $timezone, $xProp ); $foundTrans = []; if( ! DateTimeZoneFactory::isUTCtimeZone( $timezone )) { list( $start, $end ) = self::ensureStartAndEnd( $calendar, $timezone, $start, $end ); $foundTrans = self::findTransitions( $timezone, $start, $end ); } while( false !== $calendar->deleteComponent( Vcalendar::VTIMEZONE )) { continue; } $timezoneComp = $calendar->newVtimezone(); $timezoneComp->setTzid( $timezone ); if( ! empty( $xProp )) { foreach( (array) $xProp as $xPropName => $xPropValue ) { if( StringFactory::isXprefixed( $xPropName )) { $timezoneComp->setXprop( $xPropName, $xPropValue ); } } } // end if foreach( $foundTrans as $tix => $trans ) { // create standard/daylight subcomponents $subComp = ( true !== $trans[self::$ISDST] ) ? $timezoneComp->newStandard() : $timezoneComp->newDaylight(); $subComp->setDtstart( $trans[self::$TIME] ); if( ! empty( $trans[self::$ABBR] )) { $subComp->setTzname( $trans[self::$ABBR] ); } if( isset( $trans[Vcalendar::TZOFFSETFROM] )) { $subComp->setTzoffsetfrom( DateTimeZoneFactory::secondsToOffset( $trans[Vcalendar::TZOFFSETFROM] ) ); } $subComp->setTzoffsetto( DateTimeZoneFactory::secondsToOffset( $trans[self::$OFFSET] ) ); if( isset( $trans[Vcalendar::RDATE] )) { foreach( $trans[Vcalendar::RDATE] as $rDate ) { // single RDATEs, each with single date $subComp->setRdate( $rDate ); } } } // end foreach return $calendar; } /** * Return timezone from 1. tz arg, 2. xProps arg 3. calendar/vtimezone X-prop X_WR_TIMEZONE/X-LIC-LOCATION, 4. UTC * * @param Vcalendar $calendar iCalcreator calendar instance * @param string $timezone valid timezone acceptable by PHP5 DateTimeZone * @param array $xProp *[x-propName => x-propValue] * @return string * @static * @since 2.29.22 - 2019-08-26 */ private static function getTimezone( Vcalendar $calendar, $timezone = null, $xProp = [] ) { switch( true ) { case ( ! empty( $timezone )) : break; case Util::issetAndNotEmpty( $xProp, Vcalendar::X_WR_TIMEZONE ) : $timezone = $xProp[Vcalendar::X_WR_TIMEZONE]; break; case Util::issetAndNotEmpty( $xProp, Vcalendar::X_LIC_LOCATION ) : $timezone = $xProp[Vcalendar::X_LIC_LOCATION]; break; case ( false !== ( $xProp = $calendar->getXprop( Vcalendar::X_WR_TIMEZONE ))) : $timezone = $xProp[1]; break; case ( false !== ( $comp = $calendar->getComponent( Vcalendar::VTIMEZONE ))) : $calendar->reset(); if( false !== ( $xProp = $comp->getXprop( Vcalendar::X_LIC_LOCATION ))) { $timezone = $xProp[1]; break; } // fall through default : return Vcalendar::UTC; } // end switch DateTimeZoneFactory::assertDateTimeZone( $timezone ); return $timezone; } /** * Return valid (ymd-)from/tom * * @param Vcalendar $calendar * @param string $timezone valid timezone acceptable by PHP5 DateTimeZone * @param DateTimeInterface|int $start .. or unix timestamp * @param DateTimeInterface|int $end .. or unix timestamp * @return array * @throws InvalidArgumentException * @throws Exception * @static * @since 2.27.15 - 2019-03-21 */ private static function ensureStartAndEnd( Vcalendar $calendar, $timezone, $start = null, $end = null ) { static $NUMBEROFDAYSBEFORE = 365; static $FMTBEFORE = '-%d days'; static $NUMBEROFDAYSAFTER = 548; static $FMTAFTER = '+%d days'; static $ERRMSG = 'Date are not in order: %d - %d'; switch( true ) { case empty( $start ) : break; case ( $start instanceof DateTimeInterface ) : $start = $start->getTimestamp(); break; default : Util::assertInteger( $start, __METHOD__ ); break; } // end switch switch( true ) { case empty( $end ) : break; case ( $end instanceof DateTimeInterface ) : $end = $end->getTimestamp(); break; default : Util::assertInteger( $end, __METHOD__ ); break; } // end switch switch( true ) { case ( ! empty( $start ) && ! empty( $end )) : break; case ( ! empty( $start )) : // set to = +18 month (i.e 548 days) $end = $start + ( 3600 * 24 * $NUMBEROFDAYSAFTER ); break; case ( ! empty( $end )) : // set from = -12 month (i.e 365 days) $start = $end - ( 3600 * 24 * $NUMBEROFDAYSBEFORE ); break; default : $dtstarts = array_keys( $calendar->getProperty( Vcalendar::DTSTART )); switch( true ) { case ( empty( $dtstarts )) : $start = DateTimeFactory::factory( null, $timezone ); $end = ( clone $start ); break; case ( 1 == count( $dtstarts )) : $start = DateTimeFactory::factory( reset( $dtstarts ), $timezone ); $end = ( clone $start ); break; default : $start = DateTimeFactory::factory( reset( $dtstarts ), $timezone ); $end = DateTimeFactory::factory( end( $dtstarts ), $timezone ); break; } // end switch $start = $start->modify( sprintf( $FMTBEFORE, $NUMBEROFDAYSBEFORE )) ->getTimestamp(); $end = $end->modify( sprintf( $FMTAFTER, $NUMBEROFDAYSAFTER )) ->getTimestamp(); break; } // end switch if( $start > $end ) { throw new InvalidArgumentException( sprintf( $ERRMSG, $start, $end )); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } return [ $start, $end ]; } /** * Return (prep'd) datetimezone transitions * * @param string $timezone * @param int $start * @param int $end * @return array * @throws InvalidArgumentException * @throws Exception * @static * @since 2.27.15 - 2019-02-23 */ private static function findTransitions( $timezone, $start, $end ) { static $Y = 'Y'; $foundTrans = []; $prevOffsetFrom = 0; $stdIx = $dlghtIx = -1; $backupTrans = false; $dateFromYmd = DateTimeFactory::setDateTimeTimeZone( DateTimeFactory::factory( self::$AT . $start ), $timezone ) ->format( DateTimeFactory::$Ymd ); $dateToYmd = DateTimeFactory::setDateTimeTimeZone( DateTimeFactory::factory( self::$AT . $end ), $timezone ) ->format( DateTimeFactory::$Ymd ); // extend search-args to assure we start/end at daylight shifts $start -= ( 3600 * 24 * 275 ); $end += ( 3600 * 24 * 185 ); $transitions = DateTimeZoneFactory::getDateTimeZoneTransitions( $timezone, $start, $end ); // all transitions in date-time order!! foreach( $transitions as $tix => $trans ) { if( 0 > (int) gmdate( $Y, $trans[self::$TS] )) { // skip negative year... but save offset $prevOffsetFrom = $trans[self::$OFFSET]; // previous trans offset will be 'next' trans offsetFrom continue; } // end if $transDate = DateTimeFactory::factory( self::$AT . $trans[self::$TS] ); $transDateYmd = $transDate->format( self::$YMD ); if( $transDateYmd < $dateFromYmd ) { // previous trans offset will be 'next' trans offsetFrom $prevOffsetFrom = $trans[self::$OFFSET]; // we save it in case we don't find any match $backupTrans = $trans; $backupTrans[Vcalendar::TZOFFSETFROM] = ( 0 < $tix ) ? $transitions[$tix - 1][self::$OFFSET] : 0; continue; } // end if if(( $transDateYmd > $dateToYmd ) && ( -1 < ( $stdIx + $dlghtIx ))) { // loop always (?) breaks here with, at least, one standard/daylight break; } if( ! empty( $prevOffsetFrom ) || ( 0 == $prevOffsetFrom )) { // set previous offsetto as offsetFrom $trans[Vcalendar::TZOFFSETFROM] = $prevOffsetFrom; // convert utc time to local time $transDate->modify( $trans[Vcalendar::TZOFFSETFROM] . self::$SECONDS ); $trans[self::$TIME] = $transDate; } // end if $prevOffsetFrom = $trans[self::$OFFSET]; if( true !== $trans[self::$ISDST] ) { // standard timezone, build RDATEs (in date order) if(( -1 < $stdIx ) && self::matchTrans( $foundTrans[$stdIx], $trans )) { $foundTrans[$stdIx][Vcalendar::RDATE][] = clone $trans[self::$TIME]; continue; } $stdIx = $tix; } // end standard timezone else { // daylight timezone, build RDATEs (in date order) if(( -1 < $dlghtIx ) && self::matchTrans( $foundTrans[$dlghtIx], $trans )) { $foundTrans[$dlghtIx][Vcalendar::RDATE][] = clone $trans[self::$TIME]; continue; } $dlghtIx = $tix; } // end daylight timezone $foundTrans[$tix] = $trans; } // end foreach( $transitions as $tix => $trans ) if( empty( $foundTrans )) { $foundTrans[0] = self::buildTrans( $backupTrans, $timezone ); } return $foundTrans; } /** * return bool true if foundTrans matches trans * * @param array $foundTrans * @param array $trans * @return bool * @static * @since 2.27.15 - 2019-02-23 */ private static function matchTrans( array $foundTrans, array $trans ) { return (( isset( $foundTrans[Vcalendar::TZOFFSETFROM] )) && ( $foundTrans[self::$ABBR] == $trans[self::$ABBR] ) && ( $foundTrans[Vcalendar::TZOFFSETFROM] == $trans[Vcalendar::TZOFFSETFROM] ) && ( $foundTrans[self::$OFFSET] == $trans[self::$OFFSET] ) ); } /** * return (array build 'found'-trans * * @param array|bool $backupTrans * @param string $timezone * @return array * @throws InvalidArgumentException * @throws Exception * @static * @since 2.27.15 - 2019-02-23 */ private static function buildTrans( $backupTrans, $timezone ) { static $NOW = 'now'; if( is_array( $backupTrans )) { // we use the last transition (i.e. before startdate) for the tz info $prevDate = DateTimeFactory::factory( self::$AT . $backupTrans[self::$TS] ); // convert utc date to 'local' date $prevDate->modify( $backupTrans[Vcalendar::TZOFFSETFROM] . self::$SECONDS ); $backupTrans[self::$TIME] = $prevDate; } // end if( $backupTrans ) else { // or we use the timezone identifier to BUILD the standard tz info (?) $prevDate = DateTimeFactory::factory( $NOW, $timezone ); $backupTrans = [ self::$TIME => $prevDate, self::$OFFSET => $prevDate->format( Vcalendar::Z ), Vcalendar::TZOFFSETFROM => $prevDate->format( Vcalendar::Z ), self::$ISDST => false, ]; } return $backupTrans; } }