* @copyright Copyright (C) 2006 - 2017 by Dan Cogliano * @license GNU GPLv3 * @link http://icalendar.org/php-library.html */ // No direct access defined('_ZAPCAL') or die( 'Restricted access' ); /** * Zap Calendar Recurring Date Helper Class * * Class to expand recurring rule to a list of dates */ class ZCRecurringDate { /** * rules string * * @var string */ var $rules = ""; /** * start date in Unix Timestamp format (local timezone) * * @var integer */ var $startdate = null; /** * repeating frequency type (i.e. "y" for yearly, "m" for monthly) * * @var string */ var $freq = null; /** * timezone of event (using PHP timezones) * * @var string */ var $tzid = null; /** * repeat mode ('c': count, 'u': until) * * @var string */ var $repeatmode=null; /** * repeat until date (in UTC Unix Timestamp format) * * @var integer */ var $until=null; /** * repeat count when repeat mode is 'c' * * @var integer */ var $count=0; /** * array of repeat by seconds values * * @var array */ var $bysecond=array(); /** * array of repeat by minutes values * * @var array */ var $byminute=array(); /** * array of repeat by hour values * * @var array */ var $byhour=array(); /** * array of repeat by day values * * @var array */ var $byday=array(); /** * array of repeat by month day values * * @var array */ var $bymonthday=array(); /** * array of repeat by month values * * @var array */ var $bymonth=array(); /** * array of repeat by year values * * @var array */ var $byyear=array(); /** * array of repeat by setpos values * * @var array */ var $bysetpos=array(); /** * inteval of repeating event (i.e. every 2 weeks, every 6 months) * * @var integer */ var $interval = 1; /** * debug level (for testing only) * * @var integer */ var $debug = 0; /** * error string (future use) * * @var string */ var $error; /** * array of exception dates in Unix Timestamp format (UTC dates) * * @var array */ var $exdates=array(); /** * Expand recurring rule to a list of dates * * @param string $rules iCalendar rules string * @param integer $startdate start date in Unix Timestamp format * @param array $exdates array of exception dates * @param string $tzid timezone of event (using PHP timezones) */ function __construct($rules, $startdate, $exdates = array(),$tzid = "UTC"){ if(strlen($rules) > 0){ //move exdates to event timezone for comparing with event date for($i = 0; $i < count($exdates); $i++) { $exdates[$i] = ZDateHelper::toUnixDateTime(ZDateHelper::toLocalDateTime(ZDateHelper::toSQLDateTime($exdates[$i]),$tzid)); } $rules=str_replace("\'","",$rules); $this->rules = $rules; if($startdate == null){ // if not specified, use start date of beginning of last year $tdate=getdate(); $startdate=mktime(0,0,0,1,1,$tdate["year"] - 1); } $this->startdate = $startdate; $this->tzid = $tzid; $this->exdates = $exdates; $rules=explode(";", $rules); $ruletype = ""; foreach($rules as $rule){ $item=explode("=",$rule); //echo $item[0] . "=" . $item[1] . "
\n"; switch($item[0]){ case "FREQ": switch($item[1]){ case "YEARLY": $this->freq="y"; break; case "MONTHLY": $this->freq="m"; break; case "WEEKLY": $this->freq="w"; break; case "DAILY": $this->freq="d"; break; case "HOURLY": $this->freq="h"; break; case "MINUTELY": $this->freq="i"; break; case "SECONDLY": $this->freq="s"; break; } break; case "INTERVAL": $this->interval = $item[1]; break; case "BYSECOND": $this->bysecond = explode(",",$item[1]); $ruletype = $item[0]; break; case "BYMINUTE": $this->byminute = explode(",",$item[1]); $ruletype = $item[0]; break; case "BYHOUR": $this->byhour = explode(",",$item[1]); $ruletype = $item[0]; break; case "BYDAY": $this->byday = explode(",",$item[1]); $ruletype = $item[0]; break; case "BYMONTHDAY": $this->bymonthday = explode(",",$item[1]); $ruletype = $item[0]; break; case "BYMONTH": $this->bymonth = explode(",",$item[1]); $ruletype = $item[0]; break; case "BYYEAR": $this->byyear = explode(",",$item[1]); $ruletype = $item[0]; break; case "COUNT": $this->count = intval($item[1]); $this->repeatmode = "c"; break; case "BYSETPOS": $this->bysetpos = explode(",",$item[1]); break; case "UNTIL": $this->until = ZDateHelper::fromiCaltoUnixDateTime($item[1]); $this->repeatmode = "u"; break; } } if(count($this->bysetpos) > 0){ switch($ruletype){ case "BYYEAR": $this->byyear = $this->bySetPos($this->byyear,$this->bysetpos); break; case "BYMONTH": $this->bymonth = $this->bySetPos($this->bymonth,$this->bysetpos); break; case "BYMONTHDAY": $this->bymonthday = $this->bySetPos($this->bymonthday,$this->bysetpos); break; case "BYDAY": $this->byday = $this->bySetPos($this->byday,$this->bysetpos); break; case "BYHOUR": $this->byhour = $this->bySetPos($this->byhour,$this->bysetpos); break; case "BYMINUTE": $this->byminute = $this->bySetPos($this->byminute,$this->bysetpos); break; case "BYSECOND": $this->bysecond = $this->bySetPos($this->bysecond,$this->bysetpos); break; } } } } /** * bysetpos rule support * * @param array $bytype * @param array $bysetpos * * @return array */ function bySetPos($bytype, $bysetpos){ $result = array(); for($i=0; $i < count($bysetpos); $i++){ for($j=0; $j < count($bytype); $j++){ $result[] = $bysetpos[$i] . $bytype[$j]; } } return $result; } /** * save error * * @param string $msg */ function setError($msg){ $this->error = $msg; } /** * get error message * * @return string error message */ function getError(){ return $this->error; } /** * set debug level (0: none, 1: minimal, 2: more output) * * @param integer $level * */ function setDebug($level) { $this->debug = $level; } /** * display debug message * * @param integer $level * @param string $msg */ function debug($level, $msg){ if($this->debug >= $level) echo $msg . "
\n"; } /** * Get repeating dates by year * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function byYear($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"byYear(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $count = 0; if(count($this->byyear) > 0){ foreach($this->byyear as $year){ $t = getdate($startdate); $wdate = mktime($t[hours],$t[minutes],$t[seconds],$t[month],$t[mday],$year); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->byMonth($wdate, $enddate, $rdates, $tzid); if($count == 0) { $rdates[] = $wdate; $count++; } } } } else if(!$this->maxDates($rdates)) $count = $this->byMonth($startdate, $enddate, $rdates, $tzid); self::debug(1,"byYear() returned " . $count ); return $count; } /** * Get repeating dates by month * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function byMonth($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"byMonth(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $count = 0; if(count($this->bymonth) > 0){ foreach($this->bymonth as $month){ $t = getdate($startdate); $wdate = mktime($t["hours"],$t["minutes"],$t["seconds"],$month,$t["mday"],$t["year"]); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->byMonthDay($wdate, $enddate, $rdates, $tzid); if($count == 0) { $rdates[] = $wdate; $count++; } } } } else if(!$this->maxDates($rdates)) $count = $this->byMonthDay($startdate, $enddate, $rdates, $tzid); self::debug(1,"byMonth() returned " . $count ); return $count; } /** * Get repeating dates by month day * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function byMonthDay($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"byMonthDay(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $count = 0; self::debug(1,"start date: " . ZDateHelper::toSqlDateTime($startdate)); if(count($this->bymonthday) > 0){ foreach($this->bymonthday as $day){ $day = intval($day); $t = getdate($startdate); $wdate = mktime($t['hours'],$t['minutes'],$t['seconds'],$t['mon'],$day,$t['year']); self::debug(2,"mktime(" . $t['hours'] . ", " . $t['minutes'] . ", " . $t['mon'] . ", " . $day . ", " . $t['year'] . ") returned $wdate"); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->byDay($wdate, $enddate, $rdates, $tzid); if($count == 0) { $rdates[] = $wdate; $count++; } } } } else if(!$this->maxDates($rdates)) { self::debug(1,"start date: " . ZDateHelper::toSqlDateTime($startdate)); $count = $this->byDay($startdate, $enddate, $rdates, $tzid); } self::debug(1,"byMonthDay() returned " . $count ); return $count; } /** * Get repeating dates by day * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function byDay($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"byDay(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $days = array( "SU" => 0, "MO" => 1, "TU" => 2, "WE" => 3, "TH" => 4, "FR" => 5, "SA" => 6); $idays = array( 0 => "SU", 1 => "MO", 2 => "TU", 3 => "WE", 4 => "TH", 5 => "FR", 6 => "SA"); $count = 0; if(count($this->byday) > 0){ if(empty($this->byday[0])) { $this->byday[0] = $idays[date("w",$startdate)]; } foreach($this->byday as $tday){ $t = getdate($startdate); $day = substr($tday,strlen($tday) - 2); if(strlen($day) < 2) { // missing start day, use current date for DOW $day = $idays[date("w",$startdate)]; } if(strlen($tday) > 2) { $imin = 1; $imax = 5; // max # of occurances in a month if(strlen($tday) > 2) $imin = $imax = substr($tday,0,strlen($tday) - 2); self::debug(2,"imin: $imin, imax: $imax, tday: $tday, day: $day, daynum: {$days[$day]}"); for($i = $imin; $i <= $imax; $i++){ $wdate = ZDateHelper::getDateFromDay($startdate,$i-1,$days[$day],$tzid); self::debug(2,"getDateFromDay(" . ZDateHelper::toSqlDateTime($startdate) . ",$i,{$days[$day]}) returned " . ZDateHelper::toSqlDateTime($wdate)); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->byHour($wdate, $enddate, $rdates); if($count == 0){ $rdates[] = $wdate; $count++; //break; } } } } else { // day of week version $startdate_dow = date("w",$startdate); $datedelta = $days[$day] - $startdate_dow; self::debug(2, "start_dow: $startdate_dow, datedelta: $datedelta"); if($datedelta >= 0) { $wdate = ZDateHelper::addDate($startdate,0,0,0,0,$datedelta,0,$this->tzid); self::debug(2, "wdate: " . ZDateHelper::toSqlDateTime($wdate)); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->byHour($wdate, $enddate, $rdates); if($count == 0){ $rdates[] = $wdate; $count++; self::debug(2,"adding date " . ZDateHelper::toSqlDateTime($wdate) ); } } } } } } else if(!$this->maxDates($rdates)) $count = $this->byHour($startdate, $enddate, $rdates); self::debug(1,"byDay() returned " . $count ); return $count; } /** * Get repeating dates by hour * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function byHour($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"byHour(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $count = 0; if(count($this->byhour) > 0){ foreach($this->byhour as $hour){ $t = getdate($startdate); $wdate = mktime($hour,$t["minutes"],$t["seconds"],$t["mon"],$t["mday"],$t["year"]); self::debug(2,"checking date/time " . ZDateHelper::toSqlDateTime($wdate)); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->byMinute($wdate, $enddate, $rdates); if($count == 0) { $rdates[] = $wdate; $count++; } } } } else if(!$this->maxDates($rdates)) $count = $this->byMinute($startdate, $enddate, $rdates); self::debug(1,"byHour() returned " . $count ); return $count; } /** * Get repeating dates by minute * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function byMinute($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"byMinute(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $count = 0; if(count($this->byminute) > 0){ foreach($this->byminute as $minute){ $t = getdate($startdate); $wdate = mktime($t["hours"],$minute,$t["seconds"],$t["mon"],$t["mday"],$t["year"]); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $count = $this->bySecond($wdate, $enddate, $rdates); if($count == 0) { $rdates[] = $wdate; $count++; } } } } else if(!$this->maxDates($rdates)) $count = $this->bySecond($startdate, $enddate, $rdates); self::debug(1,"byMinute() returned " . $count ); return $count; } /** * Get repeating dates by second * * @param integer $startdate start date of repeating events, in Unix timestamp format * @param integer $enddate end date of repeating events, in Unix timestamp format * @param array $rdates array to contain expanded repeating dates * @param string $tzid timezone of event (using PHP timezones) * * @return integer count of dates */ private function bySecond($startdate, $enddate, &$rdates, $tzid="UTC"){ self::debug(1,"bySecond(" . ZDateHelper::toSqlDateTime($startdate) . "," . ZDateHelper::toSqlDateTime($enddate) . "," . count($rdates) . " dates)"); $count = 0; if(count($this->bysecond) > 0){ foreach($this->bysecond as $second){ $t = getdate($startdate); $wdate = mktime($t["hours"],$t["minutes"],$second,$t["mon"],$t["mday"],$t["year"]); if($startdate <= $wdate && $wdate < $enddate && !$this->maxDates($rdates)){ $rdates[] = $wdate; $count++; } } } self::debug(1,"bySecond() returned " . $count ); return $count; } /** * Determine if the loop has reached the end date * * @param array $rdates array of repeating dates * * @return boolean */ private function maxDates($rdates){ if($this->repeatmode == "c" && count($rdates) >= $this->count) return true; // exceeded count else if(count($rdates) > 0 && $this->repeatmode == "u" && $rdates[count($rdates) - 1] > $this->until){ return true; //past date } return false; } /** * Get array of dates from recurring rule * * @param $maxdate integer maximum date to appear in repeating dates in Unix timestamp format * * @return array */ public function getDates($maxdate = null){ //$this->debug = 2; self::debug(1,"getDates()"); $nextdate = $enddate = $this->startdate; $rdates = array(); $done = false; $eventcount = 0; $loopcount = 0; self::debug(2,"freq: " . $this->freq . ", interval: " . $this->interval); while(!$done){ self::debug(1,"*** Frequency ({$this->freq}) loop pass $loopcount ***"); switch($this->freq){ case "y": if($eventcount > 0) { $nextdate = ZDateHelper::addDate($nextdate,0,0,0,0,0,$this->interval,$this->tzid); self::debug(2,"addDate() returned " . ZDateHelper::toSqlDateTime($nextdate)); if(!empty($this->byday)){ $t = getdate($nextdate); $nextdate = gmmktime($t["hours"],$t["minutes"],$t["seconds"],$t["mon"],1,$t["year"]); } self::debug(2,"nextdate set to $nextdate (". ZDateHelper::toSQLDateTime($nextdate) . ")"); } $enddate=ZDateHelper::addDate($nextdate,0,0,0,0,0,1); break; case "m": if($eventcount > 0) { $nextdate = ZDateHelper::addDate($nextdate,0,0,0,$this->interval,0,0,$this->tzid); self::debug(2,"addDate() returned " . ZDateHelper::toSqlDateTime($nextdate)); } if(count($this->byday) > 0) { $t = getdate($nextdate); if($t["mday"] > 28) { //check for short months when using month by day, make sure we do not overshoot the counter and skip a month $nextdate = ZDateHelper::addDate($nextdate,0,0,0,$this->interval,0,0,$this->tzid); $t2 = getdate($nextdate); if($t2["mday"] < $t["mday"]) { // oops, skipped a month, backup to previous month $nextdate = ZDateHelper::addDate($nextdate,0,0,0,0,$t2["mday"] - $t["mday"],0,$this->tzid); } } $t = getdate($nextdate); $nextdate = mktime($t["hours"],$t["minutes"],$t["seconds"],$t["mon"],1,$t["year"]); } self::debug(2,"nextdate set to $nextdate (". ZDateHelper::toSQLDateTime($nextdate) . ")"); $enddate=ZDateHelper::addDate($nextdate,0,0,0,$this->interval,0,0); break; case "w": if($eventcount == 0) $nextdate=$nextdate; else { $nextdate = ZDateHelper::addDate($nextdate,0,0,0,0,$this->interval*7,0,$this->tzid); if(count($this->byday) > 0){ $dow = date("w", $nextdate); // move to beginning of week (Sunday) $bow = 0; $diff = $bow - $dow; if($diff > 0) $diff = $diff - 7; $nextdate = ZDateHelper::addDate($nextdate,0,0,0,0,$diff,0); } self::debug(2,"nextdate set to $nextdate (". ZDateHelper::toSQLDateTime($nextdate) . ")"); } $enddate=ZDateHelper::addDate($nextdate,0,0,0,0,$this->interval*7,0); break; case "d": $nextdate=($eventcount==0?$nextdate: ZDateHelper::addDate($nextdate,0,0,0,0,$this->interval,0,$this->tzid)); $enddate=ZDateHelper::addDate($nextdate,0,0,0,0,1,0); break; } $count = $this->byYear($nextdate,$enddate,$rdates,$this->tzid); $eventcount += $count; if($maxdate > 0 && $maxdate < $nextdate) { array_pop($rdates); $done = true; } else if($count == 0 && !$this->maxDates($rdates)){ $rdates[] = $nextdate; $eventcount++; } if($this->maxDates($rdates)) $done = true; $year = date("Y", $nextdate); if($year > _ZAPCAL_MAXYEAR) { $done = true; } $loopcount++; if($loopcount > _ZAPCAL_MAXYEAR){ $done = true; throw new Exception("Infinite loop detected in getDates()"); } } if($this->repeatmode == "u" && $rdates[count($rdates) - 1] > $this->until){ // erase last item array_pop($rdates); } $count1 = count($rdates); $rdates = array_unique($rdates); $count2 = count($rdates); $dups = $count1 - $count2; $excount = 0; foreach($this->exdates as $exdate) { if($pos = array_search($exdate,$rdates)) { array_splice($rdates,$pos,1); $excount++; } } self::debug(1,"getDates() returned " . count($rdates) . " dates, removing $dups duplicates, $excount exceptions"); if($this->debug >= 2) { self::debug(2,"Recurring Dates:"); foreach($rdates as $rdate) { $d = getdate($rdate); self::debug(2,ZDateHelper::toSQLDateTime($rdate) . " " . $d["wday"] ); } self::debug(2,"Exception Dates:"); foreach($this->exdates as $exdate) { self::debug(2, ZDateHelper::toSQLDateTime($exdate)); } //exit; } return $rdates; } }