. * * This file is a part of iCalcreator. */ namespace Kigkonsult\Icalcreator\Util; use DateTime; use DateTimeZone; use Exception; use InvalidArgumentException; use Kigkonsult\Icalcreator\Vcalendar; use RuntimeException; use UnexpectedValueException; use function array_keys; use function arsort; use function explode; use function implode; use function key; use function reset; use function sprintf; use function str_replace; use function strlen; use function strpos; use function timezone_abbreviations_list; use function timezone_name_from_abbr; /** * Class RegulateTimezoneFactory * * Review timezones, opt. alter to PHP timezones * * @see https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones * Cover Vtimezone property TZID and component date properties DTSTART, DTEND, DUE, RECURRENCE-ID, EXDATE, RDATE * * @author Kjell-Inge Gustafsson * @since 2.29.10 - 2019-09-02 */ class RegulateTimezoneFactory { /** * Add MS timezone and offset to (internal) MStimezoneToOffset * * @param string $msTz * @param string $offset (+/-)HH:mm */ public static function addMStimezoneToOffset( $msTz, $offset ) { self::$MStimezoneToOffset[$msTz] = $offset; } /** * 4 GMT/UTC(-suffixed) * 59 matches on NOT dst, OK * 5 hits on dst (aka daylight saving time)... * 5 PHP timezones specified * * @var array MS timezones with corr. UTC offset, 73 items */ public static $MStimezoneToOffset = [ 'Afghanistan Standard Time' => '+04:30', 'Arab Standard Time' => '+03:00', 'Arabian Standard Time' => '+04:00', 'Arabic Standard Time' => '+03:00', 'Argentina Standard Time' => '-03:00', 'Atlantic Standard Time' => '-04:00', 'AUS Eastern Standard Time' => '+10:00', 'Azerbaijan Standard Time' => '+04:00', 'Bangladesh Standard Time' => '+06:00', 'Belarus Standard Time' => '+03:00', 'Cape Verde Standard Time' => '-01:00', 'Caucasus Standard Time' => '+04:00', 'Central America Standard Time' => '-06:00', 'Central Asia Standard Time' => '+06:00', 'Central Europe Standard Time' => '+01:00', 'Central European Standard Time' => '+01:00', 'Central Pacific Standard Time' => '+11:00', 'Central Standard Time (Mexico)' => '-06:00', 'China Standard Time' => '+08:00', 'E. Africa Standard Time' => '+03:00', 'E. Europe Standard Time' => '+02:00', 'E. South America Standard Time' => '-03:00', 'Eastern Standard Time' => '-05:00', 'Egypt Standard Time' => '+02:00', 'Fiji Standard Time' => '+12:00', 'FLE Standard Time' => '+02:00', 'Georgian Standard Time' => '+04:00', 'GMT Standard Time' => '', 'Greenland Standard Time' => '-03:00', 'Greenwich Standard Time' => '', 'GTB Standard Time' => '+02:00', 'Hawaiian Standard Time' => '-10:00', 'India Standard Time' => '+05:30', 'Israel Standard Time' => '+02:00', 'Jordan Standard Time' => '+02:00', 'Korea Standard Time' => '+09:00', 'Mauritius Standard Time' => '+04:00', 'Middle East Standard Time' => '+02:00', 'Montevideo Standard Time' => '-03:00', 'Morocco Standard Time' => '', 'Myanmar Standard Time' => '+06:30', 'Namibia Standard Time' => '+01:00', 'Nepal Standard Time' => '+05:45', 'New Zealand Standard Time' => '+12:00', 'Pacific SA Standard Time' => '-03:00', 'Pacific Standard Time' => '-08:00', 'Pakistan Standard Time' => '+05:00', 'Paraguay Standard Time' => '-04:00', 'Romance Standard Time' => '+01:00', 'Russian Standard Time' => '+03:00', 'SA Eastern Standard Time' => '-03:00', 'SA Pacific Standard Time' => '-05:00', 'SA Western Standard Time' => '-04:00', 'Samoa Standard Time' => '+13:00', 'SE Asia Standard Time' => '+07:00', 'Singapore Standard Time' => '+08:00', 'South Africa Standard Time' => '+02:00', 'Sri Lanka Standard Time' => '+05:30', 'Syria Standard Time' => '+02:00', 'Taipei Standard Time' => '+08:00', 'Tokyo Standard Time' => '+09:00', 'Tonga Standard Time' => '+13:00', 'Turkey Standard Time' => '+02:00', 'Ulaanbaatar Standard Time' => '+08:00', 'UTC' => '', 'UTC-02' => '-02:00', 'UTC-11' => '-11:00', 'UTC+12' => '+12:00', 'Venezuela Standard Time' => '-04:30', 'W. Central Africa Standard Time' => '+01:00', 'W. Europe Standard Time' => '+01:00', 'West Asia Standard Time' => '+05:00', 'West Pacific Standard Time' => '+10:00', ]; /** * Add other timezone map to specific PHP timezone * * @param string $otherTz * @param string $phpTz * @throws InvalidArgumentException */ public static function addOtherTzMapToPhpTz( $otherTz, $phpTz ) { DateTimeZoneFactory::assertDateTimeZone( $phpTz ); self::$otherTzToPhpTz[$otherTz] = $phpTz; } /** * @var array 7 MS timezones to PHP timezones */ public static $otherTzToPhpTz = [ 'Afghanistan Standard Time' => 'Asia/Kabul', 'Fiji Standard Time' => 'Pacific/Fiji', // also in 'UTC+12', below 'Myanmar Standard Time' => 'Asia/Yangon', 'New Zealand Standard Time' => 'Pacific/Auckland', 'UTC-02' => 'America/Noronha', // also America/Godthab - Greenland // America/Miquelon - Saint Pierre and Miquelon // Atlantic/South_Georgia - South Georgia and the South Sandwich Islands 'UTC-11' => 'Pacific/Pago_Pago', // also Pacific/Niue, Pacific/Midway 'UTC+12' => 'Pacific/Auckland', // also Antarctica/McMurdo, Asia/Anadyr, Asia/Kamchatka, // Pacific/Fiji, Pacific/Funafuti, Pacific/Kwajalein, Pacific/Majuro, // Pacific/Nauru, Pacific/Tarawa, Pacific/Wake, Pacific/Wallis ]; /** * @var array */ private $inputiCal = []; /** * @var string */ private $outputiCal = null; /** * @var array */ private $vtimezoneRows = []; /** * @var array */ private $otherTzPhpRelations = []; /** * Class constructor * * @param string|array $inputiCal strict rfc2445 formatted calendar * @param array $otherTzPhpRelations [ other => phpTz ] * @throws InvalidArgumentException */ public function __construct( $inputiCal = null, array $otherTzPhpRelations = [] ) { if( ! empty( $inputiCal )) { $this->setInputiCal( $inputiCal ); } $this->addOtherTzPhpRelations( self::$otherTzToPhpTz ); foreach( $otherTzPhpRelations as $otherTz => $phpTz ) { $this->addOtherTzPhpRelation( $otherTz, $phpTz ); } } /** * Class factory method * * @param string|array $inputiCal strict rfc2445 formatted calendar * @param array $otherTzPhpRelations [ other => phpTz ] * @return static * @throws InvalidArgumentException */ public static function factory( $inputiCal = null, array $otherTzPhpRelations = [] ) { return new self( $inputiCal, $otherTzPhpRelations ); } /** * Short static all-in-one method * * @param string|array $inputiCal strict rfc2445 formatted calendar * @param array $otherTzPhpRelations [ other => phpTz ] * @return string * @throws Exception * @throws InvalidArgumentException */ public static function process( $inputiCal, array $otherTzPhpRelations = [] ) { return self::factory( $inputiCal, $otherTzPhpRelations ) ->processCalendar() ->getOutputiCal(); } /** * @param string|array $inputiCal strict rfc2445 formatted calendar * @return static * @throws Exception * @throws InvalidArgumentException */ public function processCalendar( $inputiCal = null ) { $FMTERR = 'Calendar content required!'; if( ! empty( $inputiCal )) { $this->setInputiCal( $inputiCal ); } if( ! $this->isInputiCalSet()) { throw new InvalidArgumentException( $FMTERR ); } $vtSwitch = false; foreach( $this->getInputiCal() as $lix => $row ) { if( StringFactory::startsWith( $row, self::$BEGINVTIMEZONE )) { $this->setVtimezoneRow( $row ); $vtSwitch = true; continue; } if( StringFactory::startsWith( $row, self::$ENDVTIMEZONE )) { $this->setVtimezoneRow( $row ); $this->processVtimezone(); $this->setVtimezoneRows( [] ); $vtSwitch = false; continue; } if( $vtSwitch ) { $this->setVtimezoneRow( $row ); continue; } if( StringFactory::startsWith( $row, self::$BEGIN ) || StringFactory::startsWith( $row, self::$END )) { $this->setOutputiCalRow( $row ); continue; } /* split property name and opt.params and value */ list( $propName, $row2 ) = StringFactory::getPropName( $row ); if( ! Util::isPropInList( $propName, self::$TZIDPROPS )) { $this->setOutputiCalRow( $row ); continue; } /* Now we have only properties with propAttr TZID */ /* separate attributes from value */ list( $value, $propAttr ) = self::splitContent( $row2 ); if( ! isset( $propAttr[Vcalendar::TZID] )) { $this->setOutputiCalRow( $row ); continue; } $this->processDtProp( $propName, $value, $propAttr ); } // end foreach return $this; } /** * Process VTIMEZONE properties * * NO UTC here !! ?? * @throws InvalidArgumentException * @throws RuntimeException */ private function processVtimezone() { $currTzId = null; // empty if Vtimezone TZID is found else troublesome one $currTzIdFound = false; // true if PHP Vtimezone TZID found $stdSwitch = $dlghtSwitch = false; // process STANDARD/DAYLIGHT or not $stdArr = $dlghtArr = []; // TZOFFSETTO values (in STANDARD/DAYLIGHT) foreach( $this->getVtimezoneRows() as $lix => $row ) { switch( true ) { case ( StringFactory::startsWith( $row, self::$BEGINVTIMEZONE )) : $this->setOutputiCalRow( $row ); continue 2; break; case ( StringFactory::startsWith( $row, self::$ENDVTIMEZONE )) : if( ! empty( $currTzId )) { $this->processCurrTzId( $currTzId, $stdArr, $dlghtArr ); } $this->setOutputiCalRow( $row ); $currTzId = null; $currTzIdFound = false; $stdSwitch = $dlghtSwitch = false; // process STANDARD/DAYLIGHT or not $stdArr = $dlghtArr = []; // TZOFFSETTO values (in STANDARD/DAYLIGHT) continue 2; break; case ( StringFactory::startsWith( $row, self::$BEGINSTANDARD )) : $this->setOutputiCalRow( $row ); $stdSwitch = true; continue 2; break; case ( StringFactory::startsWith( $row, self::$ENDSTANDARD )) : $this->setOutputiCalRow( $row ); $stdSwitch = false; continue 2; break; case ( StringFactory::startsWith( $row, self::$BEGINDAYLIGHT )) : $this->setOutputiCalRow( $row ); $dlghtSwitch = true; continue 2; break; case ( StringFactory::startsWith( $row, self::$ENDDAYLIGHT )) : $this->setOutputiCalRow( $row ); $dlghtSwitch = false; continue 2; break; case $currTzIdFound : // Vtimezone TZID is found, write whatever row it is $this->setOutputiCalRow( $row ); continue 2; break; default : break; // now we go on with property rows } // end switch /* split property name and opt.params and value */ list( $propName, $row2 ) = StringFactory::getPropName( $row ); if( Vcalendar::TZOFFSETTO == $propName ) { // save offset if... if( $stdSwitch ) { $stdArr[] = StringFactory::afterLast( Util::$COLON, $row2 ); } elseif( $dlghtSwitch ) { $dlghtArr[] = StringFactory::afterLast( Util::$COLON, $row2 ); } } if( Vcalendar::TZID != $propName ) { // skip all but Vtimezone TZID $this->setOutputiCalRow( $row ); continue; } /* separate attributes from value */ list( $value, $propAttr ) = StringFactory::splitContent( $row2 ); $currTzId = $value; $valueNew = null; switch( true ) { case ( $this->hasOtherTzPHPtzMap( $value )) : $valueNew = $this->getOtherTzPhpRelations( $value ); break; case ( isset( self::$MStimezoneToOffset[$value] )) : $msTzOffset = self::$MStimezoneToOffset[$value]; if( empty( $msTzOffset )) { $valueNew = Vcalendar::UTC; } else { $valueNew = self::getTimeZoneNameFromOffset( $msTzOffset, false ); } // $valueNew is null on notFound break; default : try { DateTimeZoneFactory::assertDateTimeZone( $value ); $currTzId = null; $currTzIdFound = true; // NO process of STANDARD/DAYLIGHT offset } catch( InvalidArgumentException $e ) { $valueNew = null; // DO process of STANDARD/DAYLIGHT offset } break; } // end switch if( ! empty( $valueNew )) { $this->addOtherTzPhpRelation( $currTzId, $valueNew, false ); $this->setOutputiCalRowElements( $propName, $valueNew, $propAttr ); $currTzId = null; $currTzIdFound = true; // NO process of STANDARD/DAYLIGHT } else { $this->setOutputiCalRow( $row ); // DO process of STANDARD/DAYLIGHT } } // end foreach } /** * Find currTzId replacement using stdArr+dlghtArr offsets * * @param string $currTzId * @param array $stdArr * @param array $dlghtArr * @throws RuntimeException */ private function processCurrTzId( $currTzId, array $stdArr, array $dlghtArr ) { static $ERR = 'Timezone \'%s\' (offset std %s, dlght %s) don\'t match any PHP timezone'; $stdTzs = $dlghtTzs = []; foreach( $stdArr as $offset ) { $stdTzs = self::getTimezoneListFromOffset( $offset, 0 ); // standard } foreach( $dlghtArr as $offset ) { $dlghtTzs = self::getTimezoneListFromOffset( $offset, 1 ); // daylight } foreach( $dlghtTzs as $tz => $cnt ) { if( isset( $stdTzs[$tz] )) { $dlghtTzs[$tz] += $stdTzs[$tz]; } } foreach( $stdTzs as $tz => $cnt ) { if( ! isset( $dlghtTzs[$tz] )) { $dlghtTzs[$tz] = $cnt; } } if( empty( $dlghtTzs )) { throw new RuntimeException( sprintf( $ERR, $currTzId, implode( Util::$COMMA, $stdArr ), implode( Util::$COMMA, $dlghtArr ) ) ); } arsort( $dlghtTzs ); // reverse sort on number of hits reset( $dlghtTzs ); $tzidNew = key( $dlghtTzs ); $this->replaceTzidInOutputiCal( $currTzId, $tzidNew ); $this->addOtherTzPhpRelation( $currTzId, $tzidNew, false ); } /** * Process component properties with propAttr TZID * * @param string $propName * @param string $value * @param array $propAttr * @throws Exception * @throws InvalidArgumentException * @throws RuntimeException * @todo properties RDATE, EXDATE */ private function processDtProp( $propName, $value, array $propAttr ) { $tzId = $propAttr[Vcalendar::TZID]; switch( true ) { case ( $this->hasOtherTzPHPtzMap( $tzId ) ) : $propAttr[Vcalendar::TZID] = $this->getOtherTzPhpRelations( $tzId ); break; case ( isset( self::$MStimezoneToOffset[$tzId] ) && empty( self::$MStimezoneToOffset[$tzId] )) : $this->addOtherTzPhpRelation( $tzId, Vcalendar::UTC, false ); $propAttr[Vcalendar::TZID] = Vcalendar::UTC; break; default : /* check and (opt) alter timezones */ $this->processDatePropsTZIDattribute( $propName, $value, $propAttr ); return; } // end switch self::checkTzidForUTC( $value, $propAttr ); $this->setOutputiCalRowElements( $propName, $value, $propAttr ); } /** * If in array, alter date-properties attribute TZID fixed. PHP-check (all) timezones, throws exception on error * * @param string $propName * @param string $value * @param array $propAttr * @throws InvalidArgumentException * @throws RuntimeException */ private function processDatePropsTZIDattribute( $propName, $value, $propAttr ) { $tzId = $tzIdIn = $propAttr[Vcalendar::TZID]; if( isset( self::$MStimezoneToOffset[$tzIdIn] )) { $tzId = self::getTimeZoneNameFromOffset( self::$MStimezoneToOffset[$tzIdIn], true ); $this->addOtherTzPhpRelation( $tzIdIn, $tzId, false ); $propAttr[Vcalendar::TZID] = $tzId; self::checkTzidForUTC( $value, $propAttr ); } else { DateTimeZoneFactory::assertDateTimeZone( $tzId ); } $this->setOutputiCalRowElements( $propName, $value, $propAttr ); } /** * Return array( value, propAttr ) from property row * * @param string $row2 * @return array ( value, propAttr ) */ private static function splitContent( $row2 ) { /* separate attributes from value */ list( $value, $propAttr ) = StringFactory::splitContent( $row2 ); /* fix splitContent UTC 'bug' */ self::fixUTCx( $row2, $value, $propAttr ); return [ $value, $propAttr ]; } /** * Return (first found) timezone from offset, search on standard time (ie dst=0) first * * From DateTimeZoneFactory * @param string $offset * @param bool $throwException * @return string tzName * @throws RuntimeException on NOT found * @since 2.27.14 - 2019-02-26 */ private static function getTimeZoneNameFromOffset( $offset, $throwException = true ) { static $ERR = 'Offset \'%s\' (%+d seconds) don\'t match any PHP timezone'; $seconds = DateTimeZoneFactory::offsetToSeconds( $offset ); $res = timezone_name_from_abbr( Util::$SP0, $seconds, 0 ); if( false !== $res ) { // is NO dst return $res; } $res = timezone_name_from_abbr( Util::$SP0, $seconds ); if( false !== $res ) { // ignores dst return $res; } $res = timezone_name_from_abbr( Util::$SP0, $seconds, 1 ); if( false !== $res ) { // is dst return $res; } if( $throwException ) { throw new RuntimeException( sprintf( $ERR, $offset, $seconds )); } else { return null; } } /** * Returns (array) timezones that match offset ( and dst) * * @see https://www.php.net/manual/en/function.timezone-name-from-abbr.php#89155 * @see https://www.php.net/manual/en/datetimezone.listabbreviations.php#114161 * @param string $offset * @param int $dst * @return array * @throws RuntimeException */ private static function getTimezoneListFromOffset( $offset, $dst ) { static $DST = 'dst'; static $OFFSET = 'offset'; static $TIMEZONEID = 'timezone_id'; static $FMTERR = 'Can\'t get offset from timezone %s'; $seconds = DateTimeZoneFactory::offsetToSeconds( $offset ); $output = []; foreach( timezone_abbreviations_list() as $tzAbbrList ) { foreach( $tzAbbrList as $tzAbbrCity ) { if(((bool) $tzAbbrCity[$DST] !== (bool) $dst ) || empty( strlen( $tzAbbrCity[$TIMEZONEID] )) || ( $tzAbbrCity[$OFFSET] != $seconds )) { continue; } $dateTimeOffsetNow = 0; try { $date = new DateTime( null, new DateTimeZone( $tzAbbrCity[$TIMEZONEID] ) ); $dateTimeOffsetNow = $date->getOffset(); } catch( Exception $e ) { throw new RuntimeException( sprintf( $FMTERR, $tzAbbrCity[$TIMEZONEID] ) ); } if( $seconds == $dateTimeOffsetNow ) { $tzId = $tzAbbrCity[$TIMEZONEID]; if( isset( $output[$tzId] )) { $output[$tzId] += 1; } else { $output[$tzId] = 1; } } // end if } // end foreach } // end foreach return $output; } /** * Suffix value with 'Z'and remove propAttr TZID, IF propAttr TZID = UTC * * @param string $value * @param array $propAttr */ private static function checkTzidForUTC( & $value, & $propAttr ) { if( ! DateTimeZoneFactory::isUTCtimeZone( $propAttr[Vcalendar::TZID] )) { return; } unset( $propAttr[Vcalendar::TZID] ); $values = explode( Util::$COMMA, $value ); foreach( array_keys( $values ) as $x ) { if( ParameterFactory::isParamsValueSet( [ Util::$LCparams => $propAttr ], Vcalendar::PERIOD )) { // RDATE $thePeriods = explode( Util::$SLASH, $values[$x] ); $thePeriods[0] .= Vcalendar::Z; if( ! DateIntervalFactory::isStringAndDuration( $thePeriods[1] )) { $thePeriods[1] .= Vcalendar::Z; } $values[$x] = implode( Util::$SLASH, $thePeriods ); } else { $values[$x] .= Vcalendar::Z; } } $value = implode( Util::$COMMA, $values ); } /** * Fix StringFactory::splitContent UTC* bug for MS list UTC-related timezones * * Note, here $propAttr[Vcalendar::TZID] exists * @param string $row2 * @param string $value * @param array $propAttr */ private static function fixUTCx( $row2, & $value, & $propAttr ) { static $UTZx = [ 'UTC-02', 'UTC-11', 'UTC+12' ]; foreach( $UTZx as $theUTC ) { if( false === strpos( $row2, $theUTC )) { continue; } if( false !== strpos( $propAttr[Vcalendar::TZID], Util::$COLON )) { $propAttr[Vcalendar::TZID] = StringFactory::beforeLast( Util::$COLON, $propAttr[Vcalendar::TZID] ); } if( false !== strpos( $value, Util::$COLON )) { $value = StringFactory::afterLast( Util::$COLON, $row2 ); } break; } // end foreach } /** *********************************************************************** * Getters and setters etc */ /** * @return array */ public function getInputiCal() { return $this->inputiCal; } /** * @return bool */ public function isInputiCalSet() { return ( ! empty( $this->inputiCal )); } /** * @param string|array $inputiCal * @return static * @throws UnexpectedValueException */ public function setInputiCal( $inputiCal ) { /* get rows to parse */ $rows = StringFactory::conformParseInput( $inputiCal ); /* concatenate property values spread over several rows */ $this->inputiCal = StringFactory::concatRows( $rows ); /* Initiate output */ $this->setVtimezoneRows( [] ); return $this; } /** * @return array */ private function getVtimezoneRows() { return $this->vtimezoneRows; } /** * @param string $vtimezoneRow * @return static */ private function setVtimezoneRow( $vtimezoneRow ) { $this->vtimezoneRows[] = $vtimezoneRow; return $this; } /** * @param array $vtimezoneRows * @return static */ private function setVtimezoneRows( array $vtimezoneRows = [] ) { $this->vtimezoneRows = $vtimezoneRows; return $this; } /** * @return string */ public function getOutputiCal() { return $this->outputiCal; } /** * Replace tz in outputiCal * * @param string $tzidOld * @param string $tzidNew * @return static */ private function replaceTzidInOutputiCal( $tzidOld, $tzidNew ) { $this->outputiCal = str_replace( $tzidOld, $tzidNew, $this->outputiCal ); return $this; } /** * Append outputiCal from row * * @param string $row * @return static */ private function setOutputiCalRow( $row ) { $this->outputiCal .= StringFactory::size75( $row ); return $this; } /** * Append outputiCal row, built from propName, value, propAttr * * @param string $propName * @param string $value * @param array $propAttr * @return static */ private function setOutputiCalRowElements( $propName, $value, $propAttr ) { $params = ParameterFactory::createParams( $propAttr ); $this->outputiCal .= StringFactory::createElement( $propName, $params, $value ); return $this; } /** * @param string $otherTz * @return string|bool|array bool false on key not found */ public function getOtherTzPhpRelations( $otherTz = null ) { if( ! empty( $otherTz )) { return $this->hasOtherTzPHPtzMap( $otherTz ) ? $this->otherTzPhpRelations[$otherTz] : false; } return $this->otherTzPhpRelations; } /** * @param string $otherTzKey * @return bool */ public function hasOtherTzPHPtzMap( $otherTzKey ) { return ( isset( $this->otherTzPhpRelations[$otherTzKey] )); } /** * @param string $otherTzKey * @param string $phpTz * @param bool $doTzAssert * @throws InvalidArgumentException * @return static */ public function addOtherTzPhpRelation( $otherTzKey, $phpTz, $doTzAssert = true ) { if( $doTzAssert ) { DateTimeZoneFactory::assertDateTimeZone( $phpTz ); } $this->otherTzPhpRelations[$otherTzKey] = $phpTz; return $this; } /** * @param array $otherTzPhpRelations * @return static */ private function addOtherTzPhpRelations( array $otherTzPhpRelations ) { $this->otherTzPhpRelations = $otherTzPhpRelations; return $this; } /** * @var array iCal component non-UTC date-property collection */ private static $TZIDPROPS = [ Vcalendar::DTSTART, Vcalendar::DTEND, Vcalendar::DUE, Vcalendar::RECURRENCE_ID, Vcalendar::EXDATE, Vcalendar::RDATE ]; /** * @var string */ private static $BEGIN = 'BEGIN'; private static $BEGINVTIMEZONE = 'BEGIN:VTIMEZONE'; private static $BEGINSTANDARD = 'BEGIN:STANDARD'; private static $BEGINDAYLIGHT = 'BEGIN:DAYLIGHT'; private static $END = 'END'; private static $ENDVTIMEZONE = 'END:VTIMEZONE'; private static $ENDSTANDARD = 'END:STANDARD'; private static $ENDDAYLIGHT = 'END:DAYLIGHT'; }