. * * This file is a part of iCalcreator. */ /** * Additional functions to use with Vtimezone components * * Before calling the functions, set time zone 'GMT' ('date_default_timezone_set')! * * @author Yitzchok Lavi * adjusted for iCalcreator Kjell-Inge Gustafsson, kigkonsult * @version 1.0.2 - 2011-02-24 */ namespace Kigkonsult\Icalcreator; use Kigkonsult\Icalcreator\Util\Util; use function array_merge; use function count; use function explode; use function getdate; use function is_array; use function mktime; use function sort; use function sprintf; use function strtotime; use function substr; use function time; /** * Returns array with the offset information * * From UTC for a (UTC) datetime/timestamp in the * timezone, according to the VTIMEZONE information in the input array. * * @param array $timezonesarray output from function getTimezonesAsDateArrays (below) * @param string $tzid time zone identifier * @param mixed $timestamp timestamp or a UTC datetime (in array format) * @return array time zone data with keys for $OFFSETHIS, $OFFSETSEC and $TZNAME */ function getTzOffsetForDate( $timezonesarray, $tzid, $timestamp ) { static $OFFSETHIS = 'offsetHis'; static $OFFSETSEC = 'offsetSec'; static $TZBEFORE = 'tzbefore'; static $TZAFTER = 'tzafter'; static $TZNAME = 'tzname'; if( is_array( $timestamp )) { $timestamp = \gmmktime( $timestamp[Util::$LCHOUR], $timestamp[Util::$LCMIN], $timestamp[Util::$LCSEC], $timestamp[Util::$LCMONTH], $timestamp[Util::$LCDAY], $timestamp[Util::$LCYEAR] ); } $tzoffset = []; // something to return if all goes wrong (such as if $tzid doesn't find us an array of dates) $tzoffset[$OFFSETHIS] = '+0000'; $tzoffset[$OFFSETSEC] = 0; $tzoffset[$TZNAME] = '?'; if( ! isset( $timezonesarray[$tzid] )) { return $tzoffset; } $tzdatearray = $timezonesarray[$tzid]; if( is_array( $tzdatearray )) { sort( $tzdatearray ); // just in case if( $timestamp < $tzdatearray[0][Util::$LCTIMESTAMP] ) { // our date is before the first change $tzoffset[$OFFSETHIS] = $tzdatearray[0][$TZBEFORE][$OFFSETHIS]; $tzoffset[$OFFSETSEC] = $tzdatearray[0][$TZBEFORE][$OFFSETSEC]; $tzoffset[$TZNAME] = $tzdatearray[0][$TZBEFORE][$OFFSETHIS]; // we don't know the tzname in this case } elseif( $timestamp >= $tzdatearray[count( $tzdatearray ) - 1][Util::$LCTIMESTAMP] ) { // our date is after the last change (we do this so our scan can stop at the last record but one) $tzoffset[$OFFSETHIS] = $tzdatearray[count( $tzdatearray ) - 1][$TZAFTER][$OFFSETHIS]; $tzoffset[$OFFSETSEC] = $tzdatearray[count( $tzdatearray ) - 1][$TZAFTER][$OFFSETSEC]; $tzoffset[$TZNAME] = $tzdatearray[count( $tzdatearray ) - 1][$TZAFTER][$TZNAME]; } else { // our date somewhere in between // loop through the list of dates and stop at the one where the timestamp is before our date and the next one is after it // we don't include the last date in our loop as there isn't one after it to check for( $i = 0; $i <= \count( $tzdatearray ) - 2; $i++ ) { if(( $timestamp >= $tzdatearray[$i][Util::$LCTIMESTAMP] ) && ( $timestamp < $tzdatearray[$i + 1][Util::$LCTIMESTAMP] )) { $tzoffset[$OFFSETHIS] = $tzdatearray[$i][$TZAFTER][$OFFSETHIS]; $tzoffset[$OFFSETSEC] = $tzdatearray[$i][$TZAFTER][$OFFSETSEC]; $tzoffset[$TZNAME] = $tzdatearray[$i][$TZAFTER][$TZNAME]; break; } } } } return $tzoffset; } /** * Return an array containing all the timezone data in the Vcalendar object * * @param Vcalendar $Vcalendar iCalcreator calendar instance * @return array time zone transition timestamp, * array before(offsetHis, offsetSec), * array after(offsetHis, offsetSec, tzname) * based on the timezone data in the Vcalendar object */ function getTimezonesAsDateArrays( $Vcalendar ) { static $TZID = 'tzid'; $timezonedata = []; while( $vtz = $Vcalendar->getComponent( Vcalendar::VTIMEZONE )) { $tzid = $vtz->getProperty( $TZID ); $alltzdates = []; while( $vtzc = $vtz->getComponent( Vcalendar::STANDARD )) { $newtzdates = expandTimezoneDates( $vtzc ); $alltzdates = array_merge( $alltzdates, $newtzdates ); } while( $vtzc = $vtz->getComponent( Vcalendar::DAYLIGHT )) { $newtzdates = expandTimezoneDates( $vtzc ); $alltzdates = array_merge( $alltzdates, $newtzdates ); } sort( $alltzdates ); $timezonedata[$tzid] = $alltzdates; } return $timezonedata; } /** * Returns an array containing time zone data from Vtimezone standard/daylight instances * * @param object $vtzc an iCalcreator calendar standard/daylight instance * @return array time zone data; * array before(offsetHis, offsetSec), * array after(offsetHis, offsetSec, tzname) * @todo fix quickfix... */ function expandTimezoneDates( $vtzc ) { static $OFFSETHIS = 'offsetHis'; static $OFFSETSEC = 'offsetSec'; static $TZBEFORE = 'tzbefore'; static $TZAFTER = 'tzafter'; static $TZNAME = 'tzname'; static $YEARLY = 'YEARLY'; static $FMTDATE = '%04d%02d%02dT%02d%02d%02d'; static $DAYNAMES = [ 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', ]; static $MON = 'mon'; static $MDAY = 'mday'; static $HOURS = 'hours'; static $MINUTES = 'minutes'; static $SECONDS = 'seconds'; static $MINUS1WEEK = '-1 week'; static $PLUS1MONTH = '+1 month'; static $SP1WEEK = ' week'; static $SP1YEAR = ' year'; static $PLUS10YEAR = '+10 year'; $tzdates = []; // prepare time zone "description" to attach to each change $tzbefore = []; $tzbefore[$OFFSETHIS] = $vtzc->getProperty( Util::$TZOFFSETFROM ); $tzbefore[$OFFSETSEC] = Util::tz2offset( $tzbefore[$OFFSETHIS] ); if(( Util::$MINUS != substr((string) $tzbefore[$OFFSETSEC], 0, 1 )) && ( Util::$PLUS != substr((string) $tzbefore[$OFFSETSEC], 0, 1 ))) { $tzbefore[$OFFSETSEC] = Util::$PLUS . $tzbefore[$OFFSETSEC]; } $tzafter = []; $tzafter[$OFFSETHIS] = $vtzc->getProperty( Util::$TZOFFSETTO ); $tzafter[$OFFSETSEC] = Util::tz2offset( $tzafter[$OFFSETHIS] ); if(( Util::$MINUS != substr((string) $tzafter[$OFFSETSEC], 0, 1 )) && ( Util::$PLUS != substr((string) $tzafter[$OFFSETSEC], 0, 1 ))) { $tzafter[$OFFSETSEC] = Util::$PLUS . $tzafter[$OFFSETSEC]; } if( false === ( $tzafter[$TZNAME] = $vtzc->getProperty( Util::$TZNAME ))) { $tzafter[$TZNAME] = $tzafter[$OFFSETHIS]; } // find out where to start from $dtstart = $vtzc->getProperty( Util::$DTSTART ); $dtstarttimestamp = mktime( $dtstart[Util::$LCHOUR], $dtstart[Util::$LCMIN], $dtstart[Util::$LCSEC], $dtstart[Util::$LCMONTH], $dtstart[Util::$LCDAY], $dtstart[Util::$LCYEAR] ); if( ! isset( $dtstart[Util::$UNPARSEDTEXT] )) // ?? { $dtstart[Util::$UNPARSEDTEXT] = sprintf( $FMTDATE, $dtstart[Util::$LCYEAR], $dtstart[Util::$LCMONTH], $dtstart[Util::$LCDAY], $dtstart[Util::$LCHOUR], $dtstart[Util::$LCMIN], $dtstart[Util::$LCSEC] ); } if( $dtstarttimestamp == 0 ) { // it seems that the dtstart string may not have parsed correctly // let's set a timestamp starting from 1902, using the time part of the original string // so that the time will change at the right time of day // at worst we'll get midnight again $origdtstartsplit = explode( 'T', $dtstart[Util::$UNPARSEDTEXT] ); $dtstarttimestamp = strtotime( '19020101', 0 ); $dtstarttimestamp = strtotime( $origdtstartsplit[1], $dtstarttimestamp ); } // the date (in dtstart and opt RDATE/RRULE) is ALWAYS LOCAL (not utc!!), adjust from 'utc' to 'local' timestamp $diff = -1 * $tzbefore[$OFFSETSEC]; $dtstarttimestamp += $diff; // add this (start) change to the array of changes $tzdates[] = [ Util::$LCTIMESTAMP => $dtstarttimestamp, $TZBEFORE => $tzbefore, $TZAFTER => $tzafter, ]; $datearray = getdate( $dtstarttimestamp ); // save original array to use time parts, because strtotime (used below) apparently loses the time $changetime = $datearray; // generate dates according to an RRULE line $rrule = $vtzc->getProperty( Util::$RRULE ); if( is_array( $rrule )) { if( $rrule[Util::$FREQ] == $YEARLY ) { // calculate transition dates starting from DTSTART $offsetchangetimestamp = $dtstarttimestamp; // calculate transition dates until 10 years in the future $stoptimestamp = strtotime( $PLUS10YEAR, time()); // if UNTIL is set, calculate until then (however far ahead) if( isset( $rrule[Util::$UNTIL] ) && ( $rrule[Util::$UNTIL] != Util::$SP0 )) { $stoptimestamp = mktime( $rrule[Util::$UNTIL][Util::$LCHOUR], $rrule[Util::$UNTIL][Util::$LCMIN], $rrule[Util::$UNTIL][Util::$LCSEC], $rrule[Util::$UNTIL][Util::$LCMONTH], $rrule[Util::$UNTIL][Util::$LCDAY], $rrule[Util::$UNTIL][Util::$LCYEAR] ); } $count = 0; $stopcount = isset( $rrule[Util::$COUNT] ) ? $rrule[Util::$COUNT] : 0; // repeat so long as we're between DTSTART and UNTIL, or we haven't prepared COUNT dates while( $offsetchangetimestamp < $stoptimestamp && ( $stopcount == 0 || $count < $stopcount )) { // break up the timestamp into its parts $datearray = getdate( $offsetchangetimestamp ); if( isset( $rrule[Util::$BYMONTH] ) && ( $rrule[Util::$BYMONTH] != 0 )) { // set the month $datearray[$MON] = $rrule[Util::$BYMONTH]; } if( isset( $rrule[Util::$BYMONTHDAY] )) { // start quickfix... // set first found/specific day of month $datearray[$MDAY] = ( is_array( $rrule[Util::$BYMONTHDAY] )) ? reset( $rrule[Util::$BYMONTHDAY] ) : $rrule[Util::$BYMONTHDAY]; // end quickfix } elseif( isset( $rrule[Util::$BYDAY] ) && is_array( $rrule[Util::$BYDAY] )) { // update: 'isset...' // find the Xth WKDAY in the month // the starting point for this process is the first of the month set above $datearray[$MDAY] = 1; // turn $datearray as it is now back into a timestamp $offsetchangetimestamp = mktime( $datearray[$HOURS], $datearray[$MINUTES], $datearray[$SECONDS], $datearray[$MON], $datearray[$MDAY], $datearray[Util::$LCYEAR] ); if( $rrule[Util::$BYDAY][0] > 0 ) { // to find Xth WKDAY in month, we find last WKDAY in month before // we do that by finding first WKDAY in this month and going back one week // then we add X weeks (below) $offsetchangetimestamp = strtotime( $DAYNAMES[$rrule[Util::$BYDAY][Util::$DAY]], $offsetchangetimestamp ); $offsetchangetimestamp = strtotime( $MINUS1WEEK, $offsetchangetimestamp ); } else { // to find Xth WKDAY before the end of the month, we find the first WKDAY in the following month // we do that by going forward one month and going to WKDAY there // then we subtract X weeks (below) $offsetchangetimestamp = strtotime( $PLUS1MONTH, $offsetchangetimestamp ); $offsetchangetimestamp = strtotime( $DAYNAMES[$rrule[Util::$BYDAY][Util::$DAY]], $offsetchangetimestamp ); } // now move forward or back the appropriate number of weeks, into the month we want $offsetchangetimestamp = strtotime( $rrule[Util::$BYDAY][0] . $SP1WEEK, $offsetchangetimestamp ); $datearray = getdate( $offsetchangetimestamp ); } // convert the date parts back into a timestamp, setting the time parts according to the // original time data which we stored $offsetchangetimestamp = mktime( $changetime[$HOURS], $changetime[$MINUTES], $changetime[$SECONDS] + $diff, $datearray[$MON], $datearray[$MDAY], $datearray[Util::$LCYEAR] ); // add this change to the array of changes $tzdates[] = [ Util::$LCTIMESTAMP => $offsetchangetimestamp, $TZBEFORE => $tzbefore, $TZAFTER => $tzafter, ]; // update \counters (timestamp and \count) $offsetchangetimestamp = strtotime( Util::$PLUS . (( isset( $rrule[Util::$INTERVAL] ) && ( $rrule[Util::$INTERVAL] != 0 )) ? $rrule[Util::$INTERVAL] : 1 ) . $SP1YEAR, $offsetchangetimestamp ); $count += 1; } } } // generate dates according to RDATE lines while( $rdates = $vtzc->getProperty( Util::$RDATE )) { if( is_array( $rdates )) { foreach( $rdates as $rdate ) { // convert the explicit change date to a timestamp $offsetchangetimestamp = \mktime( $rdate[Util::$LCHOUR], $rdate[Util::$LCMIN], $rdate[Util::$LCSEC] + $diff, $rdate[Util::$LCMONTH], $rdate[Util::$LCDAY], $rdate[Util::$LCYEAR] ); // add this change to the array of changes $tzdates[] = [ Util::$LCTIMESTAMP => $offsetchangetimestamp, $TZBEFORE => $tzbefore, $TZAFTER => $tzafter, ]; } } } return $tzdates; }