DAViCal
RRule.php
1 <?php
12 if ( !class_exists('DateTime') ) return;
13 
19 function olson_from_vtimezone( vComponent $vtz ) {
20  $tzid = $vtz->GetProperty('TZID');
21  if ( empty($tzid) ) $tzid = $vtz->GetProperty('TZID');
22  if ( !empty($tzid) ) {
23  $result = olson_from_tzstring($tzid);
24  if ( !empty($result) ) return $result;
25  }
26 
30  return null;
31 }
32 
33 // define( 'DEBUG_RRULE', true);
34 define( 'DEBUG_RRULE', false );
35 
39 class RepeatRuleTimeZone extends DateTimeZone {
40  private $tz_defined;
41 
42  public function __construct($in_dtz = null) {
43  $this->tz_defined = false;
44  if ( !isset($in_dtz) ) return;
45 
46  $olson = olson_from_tzstring($in_dtz);
47  if ( isset($olson) ) {
48  try {
49  parent::__construct($olson);
50  $this->tz_defined = $olson;
51  }
52  catch (Exception $e) {
53  dbg_error_log( 'ERROR', 'Could not handle timezone "%s" (%s) - will use floating time', $in_dtz, $olson );
54  parent::__construct('UTC');
55  $this->tz_defined = false;
56  }
57  }
58  else {
59  dbg_error_log( 'ERROR', 'Could not recognize timezone "%s" - will use floating time', $in_dtz );
60  parent::__construct('UTC');
61  $this->tz_defined = false;
62  }
63  }
64 
65  function tzid() {
66  if ( $this->tz_defined === false ) return false;
67  $tzid = $this->getName();
68  if ( $tzid != 'UTC' ) return $tzid;
69  return $this->tz_defined;
70  }
71 }
72 
80  private $epoch_seconds = null;
81  private $days = 0;
82  private $secs = 0;
83  private $as_text = '';
84 
89  function __construct( $in_duration ) {
90  if ( is_integer($in_duration) ) {
91  $this->epoch_seconds = $in_duration;
92  $this->as_text = '';
93  }
94  else if ( gettype($in_duration) == 'string' ) {
95 // preg_match('{^-?P(\dW)|((\dD)?(T(\dH)?(\dM)?(\dS)?)?)$}i', $in_duration, $matches)
96  $this->as_text = $in_duration;
97  $this->epoch_seconds = null;
98  }
99  else {
100 // fatal('Passed duration is neither numeric nor string!');
101  }
102  }
103 
109  function equals( $other ) {
110  if ( $this == $other ) return true;
111  if ( $this->asSeconds() == $other->asSeconds() ) return true;
112  return false;
113  }
114 
118  function asSeconds() {
119  if ( !isset($this->epoch_seconds) ) {
120  if ( preg_match('{^(-?)P(?:(\d+W)|(?:(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S?)?)?))$}i', $this->as_text, $matches) ) {
121  // @printf("%s - %s - %s - %s - %s - %s\n", $matches[1], $matches[2], $matches[3], $matches[4], $matches[5], $matches[6]);
122  $this->secs = 0;
123  if ( !empty($matches[2]) ) {
124  $this->days = (intval($matches[2]) * 7);
125  }
126  else {
127  if ( !empty($matches[3]) ) $this->days = intval($matches[3]);
128  if ( !empty($matches[4]) ) $this->secs += intval($matches[4]) * 3600;
129  if ( !empty($matches[5]) ) $this->secs += intval($matches[5]) * 60;
130  if ( !empty($matches[6]) ) $this->secs += intval($matches[6]);
131  }
132  if ( $matches[1] == '-' ) {
133  $this->days *= -1;
134  $this->secs *= -1;
135  }
136  $this->epoch_seconds = ($this->days * 86400) + $this->secs;
137  // printf("Duration: %d days & %d seconds (%d epoch seconds)\n", $this->days, $this->secs, $this->epoch_seconds);
138  }
139  else {
140  throw new Exception('Invalid epoch: "'+$this->as_text+"'");
141  }
142  }
143  return $this->epoch_seconds;
144  }
145 
146 
151  function __toString() {
152  if ( empty($this->as_text) ) {
153  $this->as_text = ($this->epoch_seconds < 0 ? '-P' : 'P');
154  $in_duration = abs($this->epoch_seconds);
155  if ( $in_duration >= 86400 ) {
156  $this->days = floor($in_duration / 86400);
157  $in_duration -= $this->days * 86400;
158  if ( $in_duration == 0 && ($this->days / 7) == floor($this->days / 7) ) {
159  $this->as_text .= ($this->days/7).'W';
160  return $this->as_text;
161  }
162  $this->as_text .= $this->days.'D';
163  }
164  if ( $in_duration > 0 ) {
165  $secs = $in_duration;
166  $this->as_text .= 'T';
167  $hours = floor($in_duration / 3600);
168  if ( $hours > 0 ) $this->as_text .= $hours . 'H';
169  $minutes = floor(($in_duration % 3600) / 60);
170  if ( $minutes > 0 ) $this->as_text .= $minutes . 'M';
171  $seconds = $in_duration % 60;
172  if ( $seconds > 0 ) $this->as_text .= $seconds . 'S';
173  }
174  }
175  return $this->as_text;
176  }
177 
178 
198  static function fromTwoDates( $d1, $d2 ) {
199  $diff = $d2->epoch() - $d1->epoch();
200  return new Rfc5545Duration($diff);
201  }
202 }
203 
210 class RepeatRuleDateTime extends DateTime {
211  // public static $Format = 'Y-m-d H:i:s';
212  public static $Format = 'c';
213  private static $UTCzone;
214  private $tzid;
215  private $is_date;
216 
217  public function __construct($date = null, $dtz = null, $is_date = null ) {
218  if ( !isset(self::$UTCzone) ) self::$UTCzone = new RepeatRuleTimeZone('UTC');
219  $this->is_date = false;
220  if ( isset($is_date) ) $this->is_date = $is_date;
221  if ( !isset($date) ) {
222  $date = date('Ymd\THis');
223  // Floating
224  $dtz = self::$UTCzone;
225  }
226  $this->tzid = null;
227 
228  if ( is_object($date) && method_exists($date,'GetParameterValue') ) {
229  $tzid = $date->GetParameterValue('TZID');
230  $actual_date = $date->Value();
231  if ( isset($tzid) ) {
232  $dtz = new RepeatRuleTimeZone($tzid);
233  $this->tzid = $dtz->tzid();
234  }
235  else {
236  $dtz = self::$UTCzone;
237  if ( substr($actual_date,-1) == 'Z' ) {
238  $this->tzid = 'UTC';
239  $actual_date = substr($actual_date, 0, strlen($actual_date) - 1);
240  }
241  }
242  if ( strlen($actual_date) == 8 ) {
243  // We allow dates without VALUE=DATE parameter, but we don't create them like that
244  $this->is_date = true;
245  }
246 // $value_type = $date->GetParameterValue('VALUE');
247 // if ( isset($value_type) && $value_type == 'DATE' ) $this->is_date = true;
248  $date = $actual_date;
249  if ( DEBUG_RRULE ) printf( "Date%s property%s: %s%s\n", ($this->is_date ? "" : "Time"),
250  (isset($this->tzid) ? ' with timezone' : ''), $date,
251  (isset($this->tzid) ? ' in '.$this->tzid : '') );
252  }
253  elseif (preg_match('/;TZID= ([^:;]+) (?: ;.* )? : ( \d{8} (?:T\d{6})? ) (Z)?/x', $date, $matches) ) {
254  $date = $matches[2];
255  $this->is_date = (strlen($date) == 8);
256  if ( isset($matches[3]) && $matches[3] == 'Z' ) {
257  $dtz = self::$UTCzone;
258  $this->tzid = 'UTC';
259  }
260  else if ( isset($matches[1]) && $matches[1] != '' ) {
261  $dtz = new RepeatRuleTimeZone($matches[1]);
262  $this->tzid = $dtz->tzid();
263  }
264  else {
265  $dtz = self::$UTCzone;
266  $this->tzid = null;
267  }
268  if ( DEBUG_RRULE ) printf( "Date%s property%s: %s%s\n", ($this->is_date ? "" : "Time"),
269  (isset($this->tzid) ? ' with timezone' : ''), $date,
270  (isset($this->tzid) ? ' in '.$this->tzid : '') );
271  }
272  elseif ( ( $dtz === null || $dtz == '' )
273  && preg_match('{;VALUE=DATE (?:;[^:]+) : ((?:[12]\d{3}) (?:0[1-9]|1[012]) (?:0[1-9]|[12]\d|3[01]Z?) )$}x', $date, $matches) ) {
274  $this->is_date = true;
275  $date = $matches[1];
276  // Floating
277  $dtz = self::$UTCzone;
278  $this->tzid = null;
279  if ( DEBUG_RRULE ) printf( "Floating Date value: %s\n", $date );
280  }
281  elseif ( $dtz === null || $dtz == '' ) {
282  $dtz = self::$UTCzone;
283  if ( preg_match('/(\d{8}(T\d{6})?)(Z?)/', $date, $matches) ) {
284  $date = $matches[1];
285  $this->tzid = ( $matches[3] == 'Z' ? 'UTC' : null );
286  }
287  $this->is_date = (strlen($date) == 8 );
288  if ( DEBUG_RRULE ) printf( "Date%s value with timezone: %s in %s\n", ($this->is_date?"":"Time"), $date, $this->tzid );
289  }
290  elseif ( is_string($dtz) ) {
291  $dtz = new RepeatRuleTimeZone($dtz);
292  $this->tzid = $dtz->tzid();
293  $type = gettype($date);
294  if ( DEBUG_RRULE ) printf( "Date%s $type with timezone: %s in %s\n", ($this->is_date?"":"Time"), $date, $this->tzid );
295  }
296  else {
297  $this->tzid = $dtz->getName();
298  $type = gettype($date);
299  if ( DEBUG_RRULE ) printf( "Date%s $type with timezone: %s in %s\n", ($this->is_date?"":"Time"), $date, $this->tzid );
300  }
301 
302  parent::__construct($date, $dtz);
303  if ( isset($is_date) ) $this->is_date = $is_date;
304 
305  return $this;
306  }
307 
308 
309  public function __toString() {
310  return (string)parent::format(self::$Format) . ' ' . parent::getTimeZone()->getName();
311  }
312 
313 
314  public function AsDate() {
315  return $this->format('Ymd');
316  }
317 
318 
319  public function setAsFloat() {
320  unset($this->tzid);
321  }
322 
323 
324  public function isFloating() {
325  return !isset($this->tzid);
326  }
327 
328  public function isDate() {
329  return $this->is_date;
330  }
331 
332 
333  public function setAsDate() {
334  $this->is_date = true;
335  }
336 
337 
338  public function modify( $interval ) {
339 // print ">>$interval<<\n";
340  if ( preg_match('{^(-)?P(([0-9-]+)W)?(([0-9-]+)D)?T?(([0-9-]+)H)?(([0-9-]+)M)?(([0-9-]+)S)?$}', $interval, $matches) ) {
341  $minus = (isset($matches[1])?$matches[1]:'');
342  $interval = '';
343  if ( isset($matches[2]) && $matches[2] != '' ) $interval .= $minus . $matches[3] . ' weeks ';
344  if ( isset($matches[4]) && $matches[4] != '' ) $interval .= $minus . $matches[5] . ' days ';
345  if ( isset($matches[6]) && $matches[6] != '' ) $interval .= $minus . $matches[7] . ' hours ';
346  if ( isset($matches[8]) && $matches[8] != '' ) $interval .= $minus . $matches[9] . ' minutes ';
347  if (isset($matches[10]) &&$matches[10] != '' ) $interval .= $minus . $matches[11] . ' seconds ';
348  }
349 // printf( "Modify '%s' by: >>%s<<\n", $this->__toString(), $interval );
350 // print_r($this);
351  if ( !isset($interval) || $interval == '' ) $interval = '1 day';
352  if ( parent::format('d') > 28 && strstr($interval,'month') !== false ) {
353  $this->setDate(null,null,28);
354  }
355  parent::modify($interval);
356  return $this->__toString();
357  }
358 
359 
367  public function UTC($fmt = 'Ymd\THis\Z' ) {
368  $gmt = clone($this);
369  if ( $this->tzid != 'UTC' ) {
370  if ( isset($this->tzid)) {
371  $dtz = parent::getTimezone();
372  }
373  else {
374  $dtz = new DateTimeZone(date_default_timezone_get());
375  }
376  $offset = 0 - $dtz->getOffset($gmt);
377  $gmt->modify( $offset . ' seconds' );
378  }
379  return $gmt->format($fmt);
380  }
381 
382 
394  public function FloatOrUTC($return_floating_times = false) {
395  $gmt = clone($this);
396  if ( !$return_floating_times && isset($this->tzid) && $this->tzid != 'UTC' ) {
397  $dtz = parent::getTimezone();
398  $offset = 0 - $dtz->getOffset($gmt);
399  $gmt->modify( $offset . ' seconds' );
400  }
401  if ( $this->is_date ) return $gmt->format('Ymd');
402  if ( $return_floating_times ) return $gmt->format('Ymd\THis');
403  return $gmt->format('Ymd\THis') . (!$return_floating_times && isset($this->tzid) ? 'Z' : '');
404  }
405 
406 
410  public function RFC5545($return_floating_times = false) {
411  $result = '';
412  if ( isset($this->tzid) && $this->tzid != 'UTC' ) {
413  $result = ';TZID='.$this->tzid;
414  }
415  if ( $this->is_date ) {
416  $result .= ';VALUE=DATE:' . $this->format('Ymd');
417  }
418  else {
419  $result .= ':' . $this->format('Ymd\THis');
420  if ( !$return_floating_times && isset($this->tzid) && $this->tzid == 'UTC' ) {
421  $result .= 'Z';
422  }
423  }
424  return $result;
425  }
426 
427 
428  public function setTimeZone( $tz ) {
429  if ( is_string($tz) ) {
430  $tz = new RepeatRuleTimeZone($tz);
431  $this->tzid = $tz->tzid();
432  }
433  parent::setTimeZone( $tz );
434  return $this;
435  }
436 
437 
438  public function getTimeZone() {
439  return $this->tzid;
440  }
441 
442 
448  public static function hasLeapDay($year) {
449  if ( ($year % 4) == 0 && (($year % 100) != 0 || ($year % 400) == 0) ) return 1;
450  return 0;
451  }
452 
459  public static function daysInMonth( $year, $month ) {
460  if ($month == 4 || $month == 6 || $month == 9 || $month == 11) return 30;
461  else if ($month != 2) return 31;
462  return 28 + RepeatRuleDateTime::hasLeapDay($year);
463  }
464 
465 
466  function setDate( $year=null, $month=null, $day=null ) {
467  if ( !isset($year) ) $year = parent::format('Y');
468  if ( !isset($month) ) $month = parent::format('m');
469  if ( !isset($day) ) $day = parent::format('d');
470  if ( $day < 0 ) {
471  $day += RepeatRuleDateTime::daysInMonth($year, $month) + 1;
472  }
473  parent::setDate( $year , $month , $day );
474  return $this;
475  }
476 
477  function setYearDay( $yearday ) {
478  if ( $yearday > 0 ) {
479  $current_yearday = parent::format('z') + 1;
480  }
481  else {
482  $current_yearday = (parent::format('z') - (365 + parent::format('L')));
483  }
484  $diff = $yearday - $current_yearday;
485  if ( $diff < 0 ) $this->modify('-P'.-$diff.'D');
486  else if ( $diff > 0 ) $this->modify('P'.$diff.'D');
487 // printf( "Current: %d, Looking for: %d, Diff: %d, What we got: %s (%d,%d)\n", $current_yearday, $yearday, $diff,
488 // parent::format('Y-m-d'), (parent::format('z')+1), ((parent::format('z') - (365 + parent::format('L')))) );
489  return $this;
490  }
491 
492  function year() {
493  return parent::format('Y');
494  }
495 
496  function month() {
497  return parent::format('m');
498  }
499 
500  function day() {
501  return parent::format('d');
502  }
503 
504  function hour() {
505  return parent::format('H');
506  }
507 
508  function minute() {
509  return parent::format('i');
510  }
511 
512  function second() {
513  return parent::format('s');
514  }
515 
516  function epoch() {
517  return parent::format('U');
518  }
519 }
520 
521 
529  public $from;
530  public $until;
531 
541  function __construct( $date1, $date2 ) {
542  if ( $date1 != null && $date2 != null && $date1 > $date2 ) {
543  $this->from = $date2;
544  $this->until = $date1;
545  }
546  else {
547  $this->from = $date1;
548  $this->until = $date2;
549  }
550  }
551 
557  function overlaps( RepeatRuleDateRange $other ) {
558  if ( ($this->until == null && $this->from == null) || ($other->until == null && $other->from == null ) ) return true;
559  if ( $this->until == null && $other->until == null ) return true;
560  if ( $this->from == null && $other->from == null ) return true;
561 
562  if ( $this->until == null ) return ($other->until > $this->from);
563  if ( $this->from == null ) return ($other->from < $this->until);
564  if ( $other->until == null ) return ($this->until > $other->from);
565  if ( $other->from == null ) return ($thi->from < $other->until);
566 
567  return !( $this->until < $other->from || $this->from > $other->until );
568  }
569 
576  function getDuration() {
577  if ( !isset($this->from) ) return null;
578  if ( $this->from->isDate() && !isset($this->until) )
579  $duration = 'P1D';
580  else if ( !isset($this->until) )
581  $duration = 'P0D';
582  else
583  $duration = ( $this->until->epoch() - $this->from->epoch() );
584  return new Rfc5545Duration( $duration );
585  }
586 }
587 
588 
596 class RepeatRule {
597 
598  private $base;
599  private $until;
600  private $freq;
601  private $count;
602  private $interval;
603  private $bysecond;
604  private $byminute;
605  private $byhour;
606  private $bymonthday;
607  private $byyearday;
608  private $byweekno;
609  private $byday;
610  private $bymonth;
611  private $bysetpos;
612  private $wkst;
613 
614  private $instances;
615  private $position;
616  private $finished;
617  private $current_base;
618  private $original_rule;
619 
620 
621  public function __construct( $basedate, $rrule, $is_date=null, $return_floating_times=false ) {
622  if ( $return_floating_times ) $basedate->setAsFloat();
623  $this->base = (is_object($basedate) ? $basedate : new RepeatRuleDateTime($basedate) );
624  $this->original_rule = $rrule;
625 
626  if ( DEBUG_RRULE ) {
627  printf( "Constructing RRULE based on: '%s', rrule: '%s' (we float: %s)\n", $basedate, $rrule, ($return_floating_times?"yes":"no") );
628  }
629 
630  if ( preg_match('{FREQ=([A-Z]+)(;|$)}', $rrule, $m) ) $this->freq = $m[1];
631 
632  if ( preg_match('{UNTIL=([0-9TZ]+)(;|$)}', $rrule, $m) )
633  $this->until = new RepeatRuleDateTime($m[1],$this->base->getTimeZone(),$is_date);
634  if ( preg_match('{COUNT=([0-9]+)(;|$)}', $rrule, $m) ) $this->count = $m[1];
635  if ( preg_match('{INTERVAL=([0-9]+)(;|$)}', $rrule, $m) ) $this->interval = $m[1];
636 
637  if ( preg_match('{WKST=(MO|TU|WE|TH|FR|SA|SU)(;|$)}', $rrule, $m) ) $this->wkst = $m[1];
638 
639  if ( preg_match('{BYDAY=(([+-]?[0-9]{0,2}(MO|TU|WE|TH|FR|SA|SU),?)+)(;|$)}', $rrule, $m) )
640  $this->byday = explode(',',$m[1]);
641 
642  if ( preg_match('{BYYEARDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byyearday = explode(',',$m[1]);
643  if ( preg_match('{BYWEEKNO=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byweekno = explode(',',$m[1]);
644  if ( preg_match('{BYMONTHDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->bymonthday = explode(',',$m[1]);
645  if ( preg_match('{BYMONTH=(([+-]?[0-1]?[0-9],?)+)(;|$)}', $rrule, $m) ) $this->bymonth = explode(',',$m[1]);
646  if ( preg_match('{BYSETPOS=(([+-]?[0-9]{1,3},?)+)(;|$)}', $rrule, $m) ) $this->bysetpos = explode(',',$m[1]);
647 
648  if ( preg_match('{BYSECOND=([0-9,]+)(;|$)}', $rrule, $m) ) $this->bysecond = explode(',',$m[1]);
649  if ( preg_match('{BYMINUTE=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byminute = explode(',',$m[1]);
650  if ( preg_match('{BYHOUR=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byhour = explode(',',$m[1]);
651 
652  if ( !isset($this->interval) ) $this->interval = 1;
653  switch( $this->freq ) {
654  case 'SECONDLY': $this->freq_name = 'second'; break;
655  case 'MINUTELY': $this->freq_name = 'minute'; break;
656  case 'HOURLY': $this->freq_name = 'hour'; break;
657  case 'DAILY': $this->freq_name = 'day'; break;
658  case 'WEEKLY': $this->freq_name = 'week'; break;
659  case 'MONTHLY': $this->freq_name = 'month'; break;
660  case 'YEARLY': $this->freq_name = 'year'; break;
661  default:
663  }
664  $this->frequency_string = sprintf('+%d %s', $this->interval, $this->freq_name );
665  if ( DEBUG_RRULE ) printf( "Frequency modify string is: '%s', base is: '%s'\n", $this->frequency_string, $this->base->format('c') );
666  $this->Start($return_floating_times);
667  }
668 
669 
674  public function hasLimitedOccurrences() {
675  return ( isset($this->count) || isset($this->until) );
676  }
677 
678 
679  public function set_timezone( $tzstring ) {
680  $this->base->setTimezone(new DateTimeZone($tzstring));
681  }
682 
683 
684  public function Start($return_floating_times=false) {
685  $this->instances = array();
686  $this->GetMoreInstances($return_floating_times);
687  $this->rewind();
688  $this->finished = false;
689  }
690 
691 
692  public function rewind() {
693  $this->position = -1;
694  }
695 
696 
702  public function next($return_floating_times=false) {
703  $this->position++;
704  return $this->current($return_floating_times);
705  }
706 
707 
708  public function current($return_floating_times=false) {
709  if ( !$this->valid() ) return null;
710  if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances($return_floating_times);
711  if ( !$this->valid() ) return null;
712  if ( DEBUG_RRULE ) printf( "Returning date from position %d: %s (%s)\n", $this->position,
713  $this->instances[$this->position]->format('c'), $this->instances[$this->position]->FloatOrUTC($return_floating_times) );
714  return $this->instances[$this->position];
715  }
716 
717 
718  public function key($return_floating_times=false) {
719  if ( !$this->valid() ) return null;
720  if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances($return_floating_times);
721  if ( !isset($this->keys[$this->position]) ) {
722  $this->keys[$this->position] = $this->instances[$this->position];
723  }
724  return $this->keys[$this->position];
725  }
726 
727 
728  public function valid() {
729  if ( isset($this->instances[$this->position]) || !$this->finished ) return true;
730  return false;
731  }
732 
741  private static function rrule_expand_limit( $freq ) {
742  switch( $freq ) {
743  case 'YEARLY':
744  return array( 'bymonth' => 'expand', 'byweekno' => 'expand', 'byyearday' => 'expand', 'bymonthday' => 'expand',
745  'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
746  case 'MONTHLY':
747  return array( 'bymonth' => 'limit', 'bymonthday' => 'expand',
748  'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
749  case 'WEEKLY':
750  return array( 'bymonth' => 'limit',
751  'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
752  case 'DAILY':
753  return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
754  'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
755  case 'HOURLY':
756  return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
757  'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'expand', 'bysecond' => 'expand' );
758  case 'MINUTELY':
759  return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
760  'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'expand' );
761  case 'SECONDLY':
762  return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
763  'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'limit' );
764  }
765  dbg_error_log('ERROR','Invalid frequency code "%s" - pretending it is "DAILY"', $freq);
766  return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
767  'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
768  }
769 
770  private function GetMoreInstances($return_floating_times=false) {
771  if ( $this->finished ) return;
772  $got_more = false;
773  $loop_limit = 10;
774  $loops = 0;
775  if ( $return_floating_times ) $this->base->setAsFloat();
776  while( !$this->finished && !$got_more && $loops++ < $loop_limit ) {
777  if ( !isset($this->current_base) ) {
778  $this->current_base = clone($this->base);
779  }
780  else {
781  $this->current_base->modify( $this->frequency_string );
782  }
783  if ( $return_floating_times ) $this->current_base->setAsFloat();
784  if ( DEBUG_RRULE ) printf( "Getting more instances from: '%s' - %d\n", $this->current_base->format('c'), count($this->instances) );
785  $this->current_set = array( clone($this->current_base) );
786  foreach( self::rrule_expand_limit($this->freq) AS $bytype => $action ) {
787  if ( isset($this->{$bytype}) ) {
788  $this->{$action.'_'.$bytype}();
789  if ( !isset($this->current_set[0]) ) break;
790  }
791  }
792 
793  sort($this->current_set);
794  if ( isset($this->bysetpos) ) $this->limit_bysetpos();
795 
796  $position = count($this->instances) - 1;
797  if ( DEBUG_RRULE ) printf( "Inserting %d from current_set into position %d\n", count($this->current_set), $position + 1 );
798  foreach( $this->current_set AS $k => $instance ) {
799  if ( $instance < $this->base ) continue;
800  if ( isset($this->until) && $instance > $this->until ) {
801  $this->finished = true;
802  return;
803  }
804  if ( !isset($this->instances[$position]) || $instance != $this->instances[$position] ) {
805  $got_more = true;
806  $position++;
807  $this->instances[$position] = $instance;
808  if ( DEBUG_RRULE ) printf( "Added date %s into position %d in current set\n", $instance->format('c'), $position );
809  if ( isset($this->count) && ($position + 1) >= $this->count ) {
810  $this->finished = true;
811  return;
812  }
813  }
814  }
815  }
816  }
817 
818 
819  public static function rrule_day_number( $day ) {
820  switch( $day ) {
821  case 'SU': return 0;
822  case 'MO': return 1;
823  case 'TU': return 2;
824  case 'WE': return 3;
825  case 'TH': return 4;
826  case 'FR': return 5;
827  case 'SA': return 6;
828  }
829  return false;
830  }
831 
832 
833  static public function date_mask( $date, $y, $mo, $d, $h, $mi, $s ) {
834  $date_parts = explode(',',$date->format('Y,m,d,H,i,s'));
835 
836  if ( isset($y) || isset($mo) || isset($d) ) {
837  if ( isset($y) ) $date_parts[0] = $y;
838  if ( isset($mo) ) $date_parts[1] = $mo;
839  if ( isset($d) ) $date_parts[2] = $d;
840  $date->setDate( $date_parts[0], $date_parts[1], $date_parts[2] );
841  }
842  if ( isset($h) || isset($mi) || isset($s) ) {
843  if ( isset($h) ) $date_parts[3] = $h;
844  if ( isset($mi) ) $date_parts[4] = $mi;
845  if ( isset($s) ) $date_parts[5] = $s;
846  $date->setTime( $date_parts[3], $date_parts[4], $date_parts[5] );
847  }
848  return $date;
849  }
850 
851 
852  private function expand_bymonth() {
853  $instances = $this->current_set;
854  $this->current_set = array();
855  foreach( $instances AS $k => $instance ) {
856  foreach( $this->bymonth AS $k => $month ) {
857  $expanded = $this->date_mask( clone($instance), null, $month, null, null, null, null);
858  if ( DEBUG_RRULE ) printf( "Expanded BYMONTH $month into date %s\n", $expanded->format('c') );
859  $this->current_set[] = $expanded;
860  }
861  }
862  }
863 
864  private function expand_bymonthday() {
865  $instances = $this->current_set;
866  $this->current_set = array();
867  foreach( $instances AS $k => $instance ) {
868  foreach( $this->bymonthday AS $k => $monthday ) {
869  $expanded = $this->date_mask( clone($instance), null, null, $monthday, null, null, null);
870  if ( DEBUG_RRULE ) printf( "Expanded BYMONTHDAY $monthday into date %s from %s\n", $expanded->format('c'), $instance->format('c') );
871  $this->current_set[] = $expanded;
872  }
873  }
874  }
875 
876  private function expand_byyearday() {
877  $instances = $this->current_set;
878  $this->current_set = array();
879  $days_set = array();
880  foreach( $instances AS $k => $instance ) {
881  foreach( $this->byyearday AS $k => $yearday ) {
882  $on_yearday = clone($instance);
883  $on_yearday->setYearDay($yearday);
884  if ( isset($days_set[$on_yearday->UTC()]) ) continue;
885  $this->current_set[] = $on_yearday;
886  $days_set[$on_yearday->UTC()] = true;
887  }
888  }
889  }
890 
891  private function expand_byday_in_week( $day_in_week ) {
892 
898  $dow_of_instance = $day_in_week->format('w'); // 0 == Sunday
899  foreach( $this->byday AS $k => $weekday ) {
900  $dow = self::rrule_day_number($weekday);
901  $offset = $dow - $dow_of_instance;
902  if ( $offset < 0 ) $offset += 7;
903  $expanded = clone($day_in_week);
904  $expanded->modify( sprintf('+%d day', $offset) );
905  $this->current_set[] = $expanded;
906  if ( DEBUG_RRULE ) printf( "Expanded BYDAY(W) $weekday into date %s\n", $expanded->format('c') );
907  }
908  }
909 
910 
911  private function expand_byday_in_month( $day_in_month ) {
912 
913  $first_of_month = $this->date_mask( clone($day_in_month), null, null, 1, null, null, null);
914  $dow_of_first = $first_of_month->format('w'); // 0 == Sunday
915  $days_in_month = cal_days_in_month(CAL_GREGORIAN, $first_of_month->format('m'), $first_of_month->format('Y'));
916  foreach( $this->byday AS $k => $weekday ) {
917  if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
918  $dow = self::rrule_day_number($matches[3]);
919  $first_dom = 1 + $dow - $dow_of_first; if ( $first_dom < 1 ) $first_dom +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc.
920  $whichweek = intval($matches[2]);
921  if ( DEBUG_RRULE ) printf( "Expanding BYDAY(M) $weekday in month of %s\n", $first_of_month->format('c') );
922  if ( $whichweek > 0 ) {
923  $whichweek--;
924  $monthday = $first_dom;
925  if ( $matches[1] == '-' ) {
926  $monthday += 35;
927  while( $monthday > $days_in_month ) $monthday -= 7;
928  $monthday -= (7 * $whichweek);
929  }
930  else {
931  $monthday += (7 * $whichweek);
932  }
933  if ( $monthday > 0 && $monthday <= $days_in_month ) {
934  $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
935  if ( DEBUG_RRULE ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') );
936  $this->current_set[] = $expanded;
937  }
938  }
939  else {
940  for( $monthday = $first_dom; $monthday <= $days_in_month; $monthday += 7 ) {
941  $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
942  if ( DEBUG_RRULE ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') );
943  $this->current_set[] = $expanded;
944  }
945  }
946  }
947  }
948  }
949 
950 
951  private function expand_byday_in_year( $day_in_year ) {
952 
953  $first_of_year = $this->date_mask( clone($day_in_year), null, 1, 1, null, null, null);
954  $dow_of_first = $first_of_year->format('w'); // 0 == Sunday
955  $days_in_year = 337 + cal_days_in_month(CAL_GREGORIAN, 2, $first_of_year->format('Y'));
956  foreach( $this->byday AS $k => $weekday ) {
957  if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
958  $expanded = clone($first_of_year);
959  $dow = self::rrule_day_number($matches[3]);
960  $first_doy = 1 + $dow - $dow_of_first; if ( $first_doy < 1 ) $first_doy +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc.
961  $whichweek = intval($matches[2]);
962  if ( DEBUG_RRULE ) printf( "Expanding BYDAY(Y) $weekday from date %s\n", $instance->format('c') );
963  if ( $whichweek > 0 ) {
964  $whichweek--;
965  $yearday = $first_doy;
966  if ( $matches[1] == '-' ) {
967  $yearday += 371;
968  while( $yearday > $days_in_year ) $yearday -= 7;
969  $yearday -= (7 * $whichweek);
970  }
971  else {
972  $yearday += (7 * $whichweek);
973  }
974  if ( $yearday > 0 && $yearday <= $days_in_year ) {
975  $expanded->modify(sprintf('+%d day', $yearday - 1));
976  if ( DEBUG_RRULE ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') );
977  $this->current_set[] = $expanded;
978  }
979  }
980  else {
981  $expanded->modify(sprintf('+%d day', $first_doy - 1));
982  for( $yearday = $first_doy; $yearday <= $days_in_year; $yearday += 7 ) {
983  if ( DEBUG_RRULE ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') );
984  $this->current_set[] = clone($expanded);
985  $expanded->modify('+1 week');
986  }
987  }
988  }
989  }
990  }
991 
992 
993  private function expand_byday() {
994  if ( !isset($this->current_set[0]) ) return;
995  if ( $this->freq == 'MONTHLY' || $this->freq == 'YEARLY' ) {
996  if ( isset($this->bymonthday) || isset($this->byyearday) ) {
997  $this->limit_byday();
998  return;
999  }
1000  }
1001  $instances = $this->current_set;
1002  $this->current_set = array();
1003  foreach( $instances AS $k => $instance ) {
1004  if ( $this->freq == 'MONTHLY' ) {
1005  $this->expand_byday_in_month($instance);
1006  }
1007  else if ( $this->freq == 'WEEKLY' ) {
1008  $this->expand_byday_in_week($instance);
1009  }
1010  else { // YEARLY
1011  if ( isset($this->bymonth) ) {
1012  $this->expand_byday_in_month($instance);
1013  }
1014  else if ( isset($this->byweekno) ) {
1015  $this->expand_byday_in_week($instance);
1016  }
1017  else {
1018  $this->expand_byday_in_year($instance);
1019  }
1020  }
1021 
1022  }
1023  }
1024 
1025  private function expand_byhour() {
1026  $instances = $this->current_set;
1027  $this->current_set = array();
1028  foreach( $instances AS $k => $instance ) {
1029  foreach( $this->bymonth AS $k => $month ) {
1030  $this->current_set[] = $this->date_mask( clone($instance), null, null, null, $hour, null, null);
1031  }
1032  }
1033  }
1034 
1035  private function expand_byminute() {
1036  $instances = $this->current_set;
1037  $this->current_set = array();
1038  foreach( $instances AS $k => $instance ) {
1039  foreach( $this->bymonth AS $k => $month ) {
1040  $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, $minute, null);
1041  }
1042  }
1043  }
1044 
1045  private function expand_bysecond() {
1046  $instances = $this->current_set;
1047  $this->current_set = array();
1048  foreach( $instances AS $k => $instance ) {
1049  foreach( $this->bymonth AS $k => $second ) {
1050  $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, null, $second);
1051  }
1052  }
1053  }
1054 
1055 
1056  private function limit_generally( $fmt_char, $element_name ) {
1057  $instances = $this->current_set;
1058  $this->current_set = array();
1059  foreach( $instances AS $k => $instance ) {
1060  foreach( $this->{$element_name} AS $k => $element_value ) {
1061  if ( DEBUG_RRULE ) printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' ? %s\n", $instance->format('c'), $instance->format($fmt_char), $element_value, ($instance->format($fmt_char) == $element_value ? 'Yes' : 'No') );
1062  if ( $instance->format($fmt_char) == $element_value ) $this->current_set[] = $instance;
1063  }
1064  }
1065  }
1066 
1067  private function limit_byday() {
1068  $fmt_char = 'w';
1069  $instances = $this->current_set;
1070  $this->current_set = array();
1071  foreach( $this->byday AS $k => $weekday ) {
1072  $dow = self::rrule_day_number($weekday);
1073  foreach( $instances AS $k => $instance ) {
1074  if ( DEBUG_RRULE ) printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' (%d) ? %s\n", $instance->format('c'), $instance->format($fmt_char), $weekday, $dow, ($instance->format($fmt_char) == $dow ? 'Yes' : 'No') );
1075  if ( $instance->format($fmt_char) == $dow ) $this->current_set[] = $instance;
1076  }
1077  }
1078  }
1079 
1080  private function limit_bymonth() { $this->limit_generally( 'm', 'bymonth' ); }
1081  private function limit_byyearday() { $this->limit_generally( 'z', 'byyearday' ); }
1082  private function limit_bymonthday() { $this->limit_generally( 'd', 'bymonthday' ); }
1083  private function limit_byhour() { $this->limit_generally( 'H', 'byhour' ); }
1084  private function limit_byminute() { $this->limit_generally( 'i', 'byminute' ); }
1085  private function limit_bysecond() { $this->limit_generally( 's', 'bysecond' ); }
1086 
1087 
1088  private function limit_bysetpos( ) {
1089  $instances = $this->current_set;
1090  $count = count($instances);
1091  $this->current_set = array();
1092  foreach( $this->bysetpos AS $k => $element_value ) {
1093  if ( DEBUG_RRULE ) printf( "Limiting bysetpos %s of %d instances\n", $element_value, $count );
1094  if ( $element_value > 0 ) {
1095  $this->current_set[] = $instances[$element_value - 1];
1096  }
1097  else if ( $element_value < 0 ) {
1098  $this->current_set[] = $instances[$count + $element_value];
1099  }
1100  }
1101  }
1102 
1103 
1104 }
1105 
1106 
1107 
1108 require_once("vComponent.php");
1109 
1119 function rdate_expand( $dtstart, $property, $component, $range_end = null, $is_date=null, $return_floating_times=false ) {
1120  $properties = $component->GetProperties($property);
1121  $expansion = array();
1122  foreach( $properties AS $p ) {
1123  $timezone = $p->GetParameterValue('TZID');
1124  $rdate = $p->Value();
1125  $rdates = explode( ',', $rdate );
1126  foreach( $rdates AS $k => $v ) {
1127  $rdate = new RepeatRuleDateTime( $v, $timezone, $is_date);
1128  if ( $return_floating_times ) $rdate->setAsFloat();
1129  $expansion[$rdate->FloatOrUTC($return_floating_times)] = $component;
1130  if ( $rdate > $range_end ) break;
1131  }
1132  }
1133  return $expansion;
1134 }
1135 
1136 
1147 function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false ) {
1148  $expansion = array();
1149 
1150  $recur = $component->GetProperty($property);
1151  if ( !isset($recur) ) return $expansion;
1152  $recur = $recur->Value();
1153 
1154  $this_start = $component->GetProperty('DTSTART');
1155  if ( isset($this_start) ) {
1156  $this_start = new RepeatRuleDateTime($this_start);
1157  }
1158  else {
1159  $this_start = clone($dtstart);
1160  }
1161  if ( $return_floating_times ) $this_start->setAsFloat();
1162 
1163 // if ( DEBUG_RRULE ) print_r( $this_start );
1164  if ( DEBUG_RRULE ) printf( "RRULE: %s (floating: %s)\n", $recur, ($return_floating_times?"yes":"no") );
1165  $rule = new RepeatRule( $this_start, $recur, $is_date, $return_floating_times );
1166  $i = 0;
1167  $result_limit = 1000;
1168  while( $date = $rule->next($return_floating_times) ) {
1169 // if ( DEBUG_RRULE ) printf( "[%3d] %s\n", $i, $date->UTC() );
1170  $expansion[$date->FloatOrUTC($return_floating_times)] = $component;
1171  if ( $i++ >= $result_limit || $date > $range_end ) break;
1172  }
1173 // if ( DEBUG_RRULE ) print_r( $expansion );
1174  return $expansion;
1175 }
1176 
1177 
1189 function expand_event_instances( vComponent $vResource, $range_start = null, $range_end = null, $return_floating_times=false ) {
1190  global $c;
1191  $components = $vResource->GetComponents();
1192 
1193  $clear_instance_props = array(
1194  'DTSTART' => true,
1195  'DUE' => true,
1196  'DTEND' => true
1197  );
1198  if ( empty( $c->expanded_instances_include_rrule ) ) {
1199  $clear_instance_props += array(
1200  'RRULE' => true,
1201  'RDATE' => true,
1202  'EXDATE' => true
1203  );
1204  }
1205 
1206  if ( empty($range_start) ) { $range_start = new RepeatRuleDateTime(); $range_start->modify('-6 weeks'); }
1207  if ( empty($range_end) ) {
1208  $range_end = clone($range_start);
1209  $range_end->modify('+6 months');
1210  }
1211 
1212  $instances = array();
1213  $expand = false;
1214  $dtstart = null;
1215  $is_date = false;
1216  $has_repeats = false;
1217  $dtstart_type = 'DTSTART';
1218  foreach( $components AS $k => $comp ) {
1219  if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) {
1220  continue;
1221  }
1222  if ( !isset($dtstart) ) {
1223  $dtstart_prop = $comp->GetProperty($dtstart_type);
1224  if ( !isset($dtstart_prop) && $comp->GetType() != 'VTODO' ) {
1225  $dtstart_type = 'DUE';
1226  $dtstart_prop = $comp->GetProperty($dtstart_type);
1227  }
1228  if ( !isset($dtstart_prop) ) continue;
1229  $dtstart = new RepeatRuleDateTime( $dtstart_prop );
1230  if ( $return_floating_times ) $dtstart->setAsFloat();
1231  if ( DEBUG_RRULE ) printf( "Component is: %s (floating: %s)\n", $comp->GetType(), ($return_floating_times?"yes":"no") );
1232  $is_date = $dtstart->isDate();
1233  $instances[$dtstart->FloatOrUTC($return_floating_times)] = $comp;
1234  $rrule = $comp->GetProperty('RRULE');
1235  $has_repeats = isset($rrule);
1236  }
1237  $p = $comp->GetProperty('RECURRENCE-ID');
1238  if ( isset($p) && $p->Value() != '' ) {
1239  $range = $p->GetParameterValue('RANGE');
1240  $recur_utc = new RepeatRuleDateTime($p);
1241  if ( $is_date ) $recur_utc->setAsDate();
1242  $recur_utc = $recur_utc->FloatOrUTC($return_floating_times);
1243  if ( isset($range) && $range == 'THISANDFUTURE' ) {
1244  foreach( $instances AS $k => $v ) {
1245  if ( DEBUG_RRULE ) printf( "Removing overridden instance at: $k\n" );
1246  if ( $k >= $recur_utc ) unset($instances[$k]);
1247  }
1248  }
1249  else {
1250  unset($instances[$recur_utc]);
1251  }
1252  }
1253  else if ( DEBUG_RRULE ) {
1254  $p = $comp->GetProperty('SUMMARY');
1255  $summary = ( isset($p) ? $p->Value() : 'not set');
1256  $p = $comp->GetProperty('UID');
1257  $uid = ( isset($p) ? $p->Value() : 'not set');
1258  printf( "Processing event '%s' with UID '%s' starting on %s\n",
1259  $summary, $uid, $dtstart->FloatOrUTC($return_floating_times) );
1260  print( "Instances at start");
1261  foreach( $instances AS $k => $v ) {
1262  print ' : '.$k;
1263  }
1264  print "\n";
1265  }
1266  $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end, null, $return_floating_times);
1267  if ( DEBUG_RRULE ) {
1268  print( "After rrule_expand");
1269  foreach( $instances AS $k => $v ) {
1270  print ' : '.$k;
1271  }
1272  print "\n";
1273  }
1274  $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end, null, $return_floating_times);
1275  if ( DEBUG_RRULE ) {
1276  print( "After rdate_expand");
1277  foreach( $instances AS $k => $v ) {
1278  print ' : '.$k;
1279  }
1280  print "\n";
1281  }
1282  foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end, null, $return_floating_times) AS $k => $v ) {
1283  unset($instances[$k]);
1284  }
1285  if ( DEBUG_RRULE ) {
1286  print( "After exdate_expand");
1287  foreach( $instances AS $k => $v ) {
1288  print ' : '.$k;
1289  }
1290  print "\n";
1291  }
1292  }
1293 
1294  $last_duration = null;
1295  $early_start = null;
1296  $new_components = array();
1297  $start_utc = $range_start->FloatOrUTC($return_floating_times);
1298  $end_utc = $range_end->FloatOrUTC($return_floating_times);
1299  foreach( $instances AS $utc => $comp ) {
1300  if ( $utc > $end_utc ) {
1301  if ( DEBUG_RRULE ) printf( "We're done: $utc is out of the range.\n");
1302  break;
1303  }
1304 
1305  $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
1306  $duration = $comp->GetProperty('DURATION');
1307  if ( !isset($duration) || $duration->Value() == '' ) {
1308  $instance_start = $comp->GetProperty($dtstart_type);
1309  $dtsrt = new RepeatRuleDateTime( $instance_start );
1310  if ( $return_floating_times ) $dtsrt->setAsFloat();
1311  $instance_end = $comp->GetProperty($end_type);
1312  if ( isset($instance_end) ) {
1313  $dtend = new RepeatRuleDateTime( $instance_end );
1314  $duration = Rfc5545Duration::fromTwoDates($dtsrt, $dtend);
1315  }
1316  else {
1317  if ( $instance_start->GetParameterValue('VALUE') == 'DATE' ) {
1318  $duration = new Rfc5545Duration('P1D');
1319  }
1320  else {
1321  $duration = new Rfc5545Duration(0);
1322  }
1323  }
1324  }
1325  else {
1326  $duration = new Rfc5545Duration($duration->Value());
1327  }
1328 
1329  if ( $utc < $start_utc ) {
1330  if ( isset($early_start) && isset($last_duration) && $duration->equals($last_duration) ) {
1331  if ( $utc < $early_start ) {
1332  if ( DEBUG_RRULE ) printf( "Next please: $utc is before $early_start and before $start_utc.\n");
1333  continue;
1334  }
1335  }
1336  else {
1338  $latest_start = clone($range_start);
1339  $latest_start->modify('-'.$duration);
1340  $early_start = $latest_start->FloatOrUTC($return_floating_times);
1341  $last_duration = $duration;
1342  if ( $utc < $early_start ) {
1343  if ( DEBUG_RRULE ) printf( "Another please: $utc is before $early_start and before $start_utc.\n");
1344  continue;
1345  }
1346  }
1347  }
1348  $component = clone($comp);
1349  $component->ClearProperties( $clear_instance_props );
1350  $component->AddProperty($dtstart_type, $utc, ($is_date ? array('VALUE' => 'DATE') : null) );
1351  $component->AddProperty('DURATION', $duration );
1352  if ( $has_repeats && $dtstart->FloatOrUTC($return_floating_times) != $utc )
1353  $component->AddProperty('RECURRENCE-ID', $utc, ($is_date ? array('VALUE' => 'DATE') : null) );
1354  $new_components[$utc] = $component;
1355  }
1356 
1357  // Add overriden instances
1358  foreach( $components AS $k => $comp ) {
1359  $p = $comp->GetProperty('RECURRENCE-ID');
1360  if ( isset($p) && $p->Value() != '') {
1361  $recurrence_id = $p->Value();
1362  if ( !isset($new_components[$recurrence_id]) ) {
1363  // The component we're replacing is outside the range. Unless the replacement
1364  // is *in* the range we will move along to the next one.
1365  $dtstart_prop = $comp->GetProperty($dtstart_type);
1366  if ( !isset($dtstart_prop) ) continue; // No start: no expansion. Note that we consider 'DUE' to be a start if DTSTART is missing
1367  $dtstart = new RepeatRuleDateTime( $dtstart_prop );
1368  $is_date = $dtstart->isDate();
1369  if ( $return_floating_times ) $dtstart->setAsFloat();
1370  $dtstart = $dtstart->FloatOrUTC($return_floating_times);
1371  if ( $dtstart > $end_utc ) continue; // Start after end of range, skip it
1372 
1373  $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
1374  $duration = $comp->GetProperty('DURATION');
1375  if ( !isset($duration) || $duration->Value() == '' ) {
1376  $instance_end = $comp->GetProperty($end_type);
1377  if ( isset($instance_end) ) {
1378  $dtend = new RepeatRuleDateTime( $instance_end );
1379  if ( $return_floating_times ) $dtend->setAsFloat();
1380  $dtend = $dtend->FloatOrUTC($return_floating_times);
1381  }
1382  else {
1383  $dtend = $dtstart + ($is_date ? $dtstart + 86400 : 0 );
1384  }
1385  }
1386  else {
1387  $duration = new Rfc5545Duration($duration->Value());
1388  $dtend = $dtstart + $duration->asSeconds();
1389  }
1390  if ( $dtend < $start_utc ) continue; // End before start of range: skip that too.
1391  }
1392  if ( DEBUG_RRULE ) printf( "Replacing overridden instance at %s\n", $recurrence_id);
1393  $new_components[$recurrence_id] = $comp;
1394  }
1395  }
1396 
1397  $vResource->SetComponents($new_components);
1398 
1399  return $vResource;
1400 }
1401 
1402 
1409 function getComponentRange(vComponent $comp) {
1410  $dtstart_prop = $comp->GetProperty('DTSTART');
1411  $duration_prop = $comp->GetProperty('DURATION');
1412  if ( isset($duration_prop) ) {
1413  if ( !isset($dtstart_prop) ) throw new Exception('Invalid '.$comp->GetType().' containing DURATION without DTSTART', 0);
1414  $dtstart = new RepeatRuleDateTime($dtstart_prop);
1415  $dtend = clone($dtstart);
1416  $dtend->modify(new Rfc5545Duration($duration_prop->Value()));
1417  }
1418  else {
1419  $completed_prop = null;
1420  switch ( $comp->GetType() ) {
1421  case 'VEVENT':
1422  if ( !isset($dtstart_prop) ) throw new Exception('Invalid VEVENT without DTSTART', 0);
1423  $dtend_prop = $comp->GetProperty('DTEND');
1424  break;
1425  case 'VTODO':
1426  $completed_prop = $comp->GetProperty('COMPLETED');
1427  $dtend_prop = $comp->GetProperty('DUE');
1428  break;
1429  case 'VJOURNAL':
1430  if ( !isset($dtstart_prop) )
1431  $dtstart_prop = $comp->GetProperty('DTSTAMP');
1432  $dtend_prop = $dtstart_prop;
1433  default:
1434  throw new Exception('getComponentRange cannot handle "'.$comp->GetType().'" components', 0);
1435  }
1436 
1437  if ( isset($dtstart_prop) )
1438  $dtstart = new RepeatRuleDateTime($dtstart_prop);
1439  else
1440  $dtstart = null;
1441 
1442  if ( isset($dtend_prop) )
1443  $dtend = new RepeatRuleDateTime($dtend_prop);
1444  else
1445  $dtend = null;
1446 
1447  if ( isset($completed_prop) ) {
1448  $completed = new RepeatRuleDateTime($completed_prop);
1449  if ( !isset($dtstart) || (isset($dtstart) && $completed < $dtstart) ) $dtstart = $completed;
1450  if ( !isset($dtend) || (isset($dtend) && $completed > $dtend) ) $dtend = $completed;
1451  }
1452  }
1453  return new RepeatRuleDateRange($dtstart, $dtend);
1454 }
1455 
1464 function getVCalendarRange( $vResource ) {
1465  $components = $vResource->GetComponents();
1466 
1467  $dtstart = null;
1468  $duration = null;
1469  $earliest_start = null;
1470  $latest_end = null;
1471  $has_repeats = false;
1472  foreach( $components AS $k => $comp ) {
1473  if ( $comp->GetType() == 'VTIMEZONE' ) continue;
1474  $range = getComponentRange($comp);
1475  $dtstart = $range->from;
1476  if ( !isset($dtstart) ) continue;
1477  $duration = $range->getDuration();
1478 
1479  $rrule = $comp->GetProperty('RRULE');
1480  $limited_occurrences = true;
1481  if ( isset($rrule) ) {
1482  $rule = new RepeatRule($dtstart, $rrule);
1483  $limited_occurrences = $rule->hasLimitedOccurrences();
1484  }
1485 
1486  if ( $limited_occurrences ) {
1487  $instances = array();
1488  $instances[$dtstart->FloatOrUTC()] = $dtstart;
1489  if ( !isset($range_end) ) {
1490  $range_end = new RepeatRuleDateTime();
1491  $range_end->modify('+150 years');
1492  }
1493  $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end);
1494  $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end);
1495  foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end) AS $k => $v ) {
1496  unset($instances[$k]);
1497  }
1498  if ( count($instances) < 1 ) {
1499  if ( empty($earliest_start) || $dtstart < $earliest_start ) $earliest_start = $dtstart;
1500  $latest_end = null;
1501  break;
1502  }
1503  $instances = array_keys($instances);
1504  asort($instances);
1505  $first = new RepeatRuleDateTime($instances[0]);
1506  $last = new RepeatRuleDateTime($instances[count($instances)-1]);
1507  $last->modify($duration);
1508  if ( empty($earliest_start) || $first < $earliest_start ) $earliest_start = $first;
1509  if ( empty($latest_end) || $last > $latest_end ) $latest_end = $last;
1510  }
1511  else {
1512  if ( empty($earliest_start) || $dtstart < $earliest_start ) $earliest_start = $dtstart;
1513  $latest_end = null;
1514  break;
1515  }
1516  }
1517 
1518  return new RepeatRuleDateRange($earliest_start, $latest_end );
1519 }
expand_byday_in_week( $day_in_week)
Definition: RRule.php:891
FloatOrUTC($return_floating_times=false)
Definition: RRule.php:394
hasLimitedOccurrences()
Definition: RRule.php:674
static hasLeapDay($year)
Definition: RRule.php:448
RFC5545($return_floating_times=false)
Definition: RRule.php:410
static fromTwoDates( $d1, $d2)
Definition: RRule.php:198
expand_byday()
Definition: RRule.php:993
equals( $other)
Definition: RRule.php:109
__construct( $basedate, $rrule, $is_date=null, $return_floating_times=false)
Definition: RRule.php:621
__construct( $date1, $date2)
Definition: RRule.php:541
__construct( $in_duration)
Definition: RRule.php:89
UTC($fmt='Ymd\THis\Z')
Definition: RRule.php:367
next($return_floating_times=false)
Definition: RRule.php:702
static daysInMonth( $year, $month)
Definition: RRule.php:459
static rrule_expand_limit( $freq)
Definition: RRule.php:741
overlaps(RepeatRuleDateRange $other)
Definition: RRule.php:557