DAViCal
Loading...
Searching...
No Matches
RRule.php
1<?php
17function olson_from_vtimezone( vComponent $vtz ) {
18 $tzid = $vtz->GetProperty('TZID');
19 if ( empty($tzid) ) $tzid = $vtz->GetProperty('TZID');
20 if ( !empty($tzid) ) {
21 $result = olson_from_tzstring($tzid);
22 if ( !empty($result) ) return $result;
23 }
24
28 return null;
29}
30
31// define( 'DEBUG_RRULE', true);
32define( 'DEBUG_RRULE', false );
33
37class RepeatRuleTimeZone extends DateTimeZone {
38 private $tz_defined;
39
40 public function __construct($in_dtz = null) {
41 $this->tz_defined = false;
42 if ( !isset($in_dtz) ) return;
43
44 $olson = olson_from_tzstring($in_dtz);
45 if ( isset($olson) ) {
46 try {
47 parent::__construct($olson);
48 $this->tz_defined = $olson;
49 }
50 catch (Exception $e) {
51 dbg_error_log( 'ERROR', 'Could not handle timezone "%s" (%s) - will use floating time', $in_dtz, $olson );
52 parent::__construct('UTC');
53 $this->tz_defined = false;
54 }
55 }
56 else {
57 dbg_error_log( 'ERROR', 'Could not recognize timezone "%s" - will use floating time', $in_dtz );
58 parent::__construct('UTC');
59 $this->tz_defined = false;
60 }
61 }
62
63 function tzid() {
64 if ( $this->tz_defined === false ) return false;
65 $tzid = $this->getName();
66 if ( $tzid != 'UTC' ) return $tzid;
67 return $this->tz_defined;
68 }
69}
70
78 private $epoch_seconds = null;
79 private $days = 0;
80 private $secs = 0;
81 private $as_text = '';
82
87 function __construct( $in_duration ) {
88 if ( is_integer($in_duration) ) {
89 $this->epoch_seconds = $in_duration;
90 $this->as_text = '';
91 }
92 else if ( gettype($in_duration) == 'string' ) {
93// preg_match('{^-?P(\dW)|((\dD)?(T(\dH)?(\dM)?(\dS)?)?)$}i', $in_duration, $matches)
94 $this->as_text = $in_duration;
95 $this->epoch_seconds = null;
96 }
97 else {
98// fatal('Passed duration is neither numeric nor string!');
99 }
100 }
101
107 function equals( $other ) {
108 if ( $this == $other ) return true;
109 if ( $this->asSeconds() == $other->asSeconds() ) return true;
110 return false;
111 }
112
116 function asSeconds() {
117 if ( !isset($this->epoch_seconds) ) {
118 if ( preg_match('{^(-?)P(?:(\d+W)|(?:(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S?)?)?))$}i', $this->as_text, $matches) ) {
119 // @printf("%s - %s - %s - %s - %s - %s\n", $matches[1], $matches[2], $matches[3], $matches[4], $matches[5], $matches[6]);
120 $this->secs = 0;
121 if ( !empty($matches[2]) ) {
122 $this->days = (intval($matches[2]) * 7);
123 }
124 else {
125 if ( !empty($matches[3]) ) $this->days = intval($matches[3]);
126 if ( !empty($matches[4]) ) $this->secs += intval($matches[4]) * 3600;
127 if ( !empty($matches[5]) ) $this->secs += intval($matches[5]) * 60;
128 if ( !empty($matches[6]) ) $this->secs += intval($matches[6]);
129 }
130 if ( $matches[1] == '-' ) {
131 $this->days *= -1;
132 $this->secs *= -1;
133 }
134 $this->epoch_seconds = ($this->days * 86400) + $this->secs;
135 // printf("Duration: %d days & %d seconds (%d epoch seconds)\n", $this->days, $this->secs, $this->epoch_seconds);
136 }
137 else {
138 throw new Exception('Invalid epoch: "'+$this->as_text+"'");
139 }
140 }
141 return $this->epoch_seconds;
142 }
143
144
149 function __toString() {
150 if ( empty($this->as_text) ) {
151 $this->as_text = ($this->epoch_seconds < 0 ? '-P' : 'P');
152 $in_duration = abs($this->epoch_seconds);
153 if ( $in_duration == 0 ) {
154 $this->as_text .= '0D';
155 } elseif ( $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
210class 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 ) dbg_error_log( 'RRULE', "Date%s property%s: %s%s", ($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 ) dbg_error_log( 'RRULE', "Date%s property%s: %s%s", ($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 ) dbg_error_log( 'RRULE', "Floating Date value: %s", $date );
280 }
281 elseif ( $dtz === null || $dtz == '' ) {
282 $dtz = self::$UTCzone;
283 if ( preg_match('/(\d{8}(T\d{6})?) ?(.*)$/', $date, $matches) ) {
284 $date = $matches[1];
285 if ( $matches[3] == 'Z' ) {
286 $this->tzid = 'UTC';
287 } else {
288 $dtz = new RepeatRuleTimeZone($matches[3]);
289 $this->tzid = $dtz->tzid();
290 }
291 }
292 $this->is_date = (strlen($date) == 8 );
293 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Date%s value with timezone 1: %s in %s", ($this->is_date?"":"Time"), $date, $this->tzid );
294 }
295 elseif ( is_string($dtz) ) {
296 $dtz = new RepeatRuleTimeZone($dtz);
297 $this->tzid = $dtz->tzid();
298 $type = gettype($date);
299 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Date%s $type with timezone 2: %s in %s", ($this->is_date?"":"Time"), $date, $this->tzid );
300 }
301 else {
302 $this->tzid = $dtz->getName();
303 $type = gettype($date);
304 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Date%s $type with timezone 3: %s in %s", ($this->is_date?"":"Time"), $date, $this->tzid );
305 }
306
307 parent::__construct($date, $dtz);
308 if ( isset($is_date) ) $this->is_date = $is_date;
309
310 return $this;
311 }
312
313 public static function withFallbackTzid( $date, $fallback_tzid ) {
314 // Floating times or dates (either VALUE=DATE or with no TZID) can default to the collection's tzid, if one is set
315
316 if ($date->GetParameterValue('VALUE') == 'DATE' && isset($fallback_tzid)) {
317 return new RepeatRuleDateTime($date->Value()."T000000", new RepeatRuleTimeZone($fallback_tzid));
318 } else if ($date->GetParameterValue('TZID') === null && isset($fallback_tzid)) {
319 return new RepeatRuleDateTime($date->Value(), new RepeatRuleTimeZone($fallback_tzid));
320 } else {
321 return new RepeatRuleDateTime($date);
322 }
323 }
324
325
326 public function __toString() {
327 return (string)parent::format(self::$Format) . ' ' . parent::getTimeZone()->getName();
328 }
329
330
331 public function AsDate() {
332 return $this->format('Ymd');
333 }
334
335
336 public function setAsFloat() {
337 unset($this->tzid);
338 }
339
340
341 public function isFloating() {
342 return !isset($this->tzid);
343 }
344
345 public function isDate() {
346 return $this->is_date;
347 }
348
349
350 public function setAsDate() {
351 $this->is_date = true;
352 }
353
354
355 #[\ReturnTypeWillChange]
356 public function modify( $interval ) {
357// print ">>$interval<<\n";
358 if ( preg_match('{^(-)?P(([0-9-]+)W)?(([0-9-]+)D)?T?(([0-9-]+)H)?(([0-9-]+)M)?(([0-9-]+)S)?$}', $interval, $matches) ) {
359 $minus = (isset($matches[1])?$matches[1]:'');
360 $interval = '';
361 if ( isset($matches[2]) && $matches[2] != '' ) $interval .= $minus . $matches[3] . ' weeks ';
362 if ( isset($matches[4]) && $matches[4] != '' ) $interval .= $minus . $matches[5] . ' days ';
363 if ( isset($matches[6]) && $matches[6] != '' ) $interval .= $minus . $matches[7] . ' hours ';
364 if ( isset($matches[8]) && $matches[8] != '' ) $interval .= $minus . $matches[9] . ' minutes ';
365 if (isset($matches[10]) &&$matches[10] != '' ) $interval .= $minus . $matches[11] . ' seconds ';
366 }
367 if ( DEBUG_RRULE) dbg_error_log( 'RRULE', "Modify '%s' by: >>%s<<\n", $this->__toString(), $interval );
368// print_r($this);
369 if ( !isset($interval) || $interval == '' ) $interval = '1 day';
370 parent::modify($interval);
371 if (DEBUG_RRULE) dbg_error_log( 'RRULE', "Modified to '%s'", $this->__toString() );
372 return $this->__toString();
373 }
374
375
383 public function UTC($fmt = 'Ymd\THis\Z' ) {
384 $gmt = clone($this);
385 if ( $this->tzid != 'UTC' ) {
386 if ( isset($this->tzid)) {
387 $dtz = parent::getTimezone();
388 }
389 else {
390 $dtz = new DateTimeZone(date_default_timezone_get());
391 }
392 $offset = 0 - $dtz->getOffset($gmt);
393 $gmt->modify( $offset . ' seconds' );
394 }
395 return $gmt->format($fmt);
396 }
397
398
410 public function FloatOrUTC($return_floating_times = false) {
411 $gmt = clone($this);
412 if ( !$return_floating_times && isset($this->tzid) && $this->tzid != 'UTC' ) {
413 $dtz = parent::getTimezone();
414 $offset = 0 - $dtz->getOffset($gmt);
415 $gmt->modify( $offset . ' seconds' );
416 }
417 if ( $this->is_date ) return $gmt->format('Ymd');
418 if ( $return_floating_times ) return $gmt->format('Ymd\THis');
419 return $gmt->format('Ymd\THis') . (!$return_floating_times && isset($this->tzid) ? 'Z' : '');
420 }
421
422
426 public function RFC5545($return_floating_times = false) {
427 $result = '';
428 if ( isset($this->tzid) && $this->tzid != 'UTC' ) {
429 $result = ';TZID='.$this->tzid;
430 }
431 if ( $this->is_date ) {
432 $result .= ';VALUE=DATE:' . $this->format('Ymd');
433 }
434 else {
435 $result .= ':' . $this->format('Ymd\THis');
436 if ( !$return_floating_times && isset($this->tzid) && $this->tzid == 'UTC' ) {
437 $result .= 'Z';
438 }
439 }
440 return $result;
441 }
442
443
444 #[\ReturnTypeWillChange]
445 public function setTimeZone( $tz ) {
446 if ( is_string($tz) ) {
447 $tz = new RepeatRuleTimeZone($tz);
448 $this->tzid = $tz->tzid();
449 }
450 parent::setTimeZone( $tz );
451 return $this;
452 }
453
454
455 #[\ReturnTypeWillChange]
456 public function getTimeZone() {
457 return $this->tzid;
458 }
459
460
466 public static function hasLeapDay($year) {
467 if ( ($year % 4) == 0 && (($year % 100) != 0 || ($year % 400) == 0) ) return 1;
468 return 0;
469 }
470
477 public static function daysInMonth( $year, $month ) {
478 if ($month == 4 || $month == 6 || $month == 9 || $month == 11) return 30;
479 else if ($month != 2) return 31;
480 return 28 + RepeatRuleDateTime::hasLeapDay($year);
481 }
482
483
484 #[\ReturnTypeWillChange]
485 function setDate( $year=null, $month=null, $day=null ) {
486 if ( !isset($year) ) $year = parent::format('Y');
487 if ( !isset($month) ) $month = parent::format('m');
488 if ( !isset($day) ) $day = parent::format('d');
489 if ( $day < 0 ) {
490 $day += RepeatRuleDateTime::daysInMonth($year, $month) + 1;
491 }
492 parent::setDate( $year , $month , $day );
493 return $this;
494 }
495
496 function setYearDay( $yearday ) {
497 if ( $yearday > 0 ) {
498 $current_yearday = parent::format('z') + 1;
499 }
500 else {
501 $current_yearday = (parent::format('z') - (365 + parent::format('L')));
502 }
503 $diff = $yearday - $current_yearday;
504 if ( $diff < 0 ) $this->modify('-P'.-$diff.'D');
505 else if ( $diff > 0 ) $this->modify('P'.$diff.'D');
506// printf( "Current: %d, Looking for: %d, Diff: %d, What we got: %s (%d,%d)\n", $current_yearday, $yearday, $diff,
507// parent::format('Y-m-d'), (parent::format('z')+1), ((parent::format('z') - (365 + parent::format('L')))) );
508 return $this;
509 }
510
511 function year() {
512 return parent::format('Y');
513 }
514
515 function month() {
516 return parent::format('m');
517 }
518
519 function day() {
520 return parent::format('d');
521 }
522
523 function hour() {
524 return parent::format('H');
525 }
526
527 function minute() {
528 return parent::format('i');
529 }
530
531 function second() {
532 return parent::format('s');
533 }
534
535 function epoch() {
536 return parent::format('U');
537 }
538}
539
540
548 public $from;
549 public $until;
550
560 function __construct( $date1, $date2 ) {
561 if ( $date1 != null && $date2 != null && $date1 > $date2 ) {
562 $this->from = $date2;
563 $this->until = $date1;
564 }
565 else {
566 $this->from = $date1;
567 $this->until = $date2;
568 }
569 }
570
576 function overlaps( RepeatRuleDateRange $other ) {
577 if ( ($this->until == null && $this->from == null) || ($other->until == null && $other->from == null ) ) return true;
578 if ( $this->until == null && $other->until == null ) return true;
579 if ( $this->from == null && $other->from == null ) return true;
580
581 if ( $this->until == null ) return ($other->until > $this->from);
582 if ( $this->from == null ) return ($other->from < $this->until);
583 if ( $other->until == null ) return ($this->until > $other->from);
584 if ( $other->from == null ) return ($this->from < $other->until);
585
586 return !( $this->until < $other->from || $this->from > $other->until );
587 }
588
595 function getDuration() {
596 if ( !isset($this->from) ) return null;
597 if ( $this->from->isDate() && !isset($this->until) )
598 $duration = 'P1D';
599 else if ( !isset($this->until) )
600 $duration = 'P0D';
601 else
602 $duration = ( $this->until->epoch() - $this->from->epoch() );
603 return new Rfc5545Duration( $duration );
604 }
605}
606
607
616
617 private $base;
618 private $until;
619 private $freq;
620 private $count;
621 private $interval;
622 private $bysecond;
623 private $byminute;
624 private $byhour;
625 private $bymonthday;
626 private $byyearday;
627 private $byweekno;
628 private $byday;
629 private $bymonth;
630 private $bysetpos;
631 private $wkst;
632
633 private $instances;
634 private $position;
635 private $finished;
636 private $current_base;
637 private $current_set;
638 private $original_rule;
639 private $frequency_string;
640
641 public function __construct( $basedate, $rrule, $is_date=null, $return_floating_times=false ) {
642 if ( $return_floating_times ) $basedate->setAsFloat();
643 $this->base = (is_object($basedate) ? $basedate : new RepeatRuleDateTime($basedate) );
644 $this->original_rule = $rrule;
645
646 if ( DEBUG_RRULE ) {
647 dbg_error_log( 'RRULE', "Constructing RRULE based on: '%s', rrule: '%s' (float: %s)", $basedate, $rrule, ($return_floating_times ? "yes" : "no") );
648 }
649
650 if ( preg_match('{FREQ=([A-Z]+)(;|$)}', $rrule, $m) ) $this->freq = $m[1];
651
652 if ( preg_match('{UNTIL=([0-9TZ]+)(;|$)}', $rrule, $m) )
653 $this->until = new RepeatRuleDateTime($m[1],$this->base->getTimeZone(),$is_date);
654 if ( preg_match('{COUNT=([0-9]+)(;|$)}', $rrule, $m) ) $this->count = $m[1];
655 if ( preg_match('{INTERVAL=([0-9]+)(;|$)}', $rrule, $m) ) $this->interval = $m[1];
656
657 if ( preg_match('{WKST=(MO|TU|WE|TH|FR|SA|SU)(;|$)}', $rrule, $m) ) $this->wkst = $m[1];
658
659 if ( preg_match('{BYDAY=(([+-]?[0-9]{0,2}(MO|TU|WE|TH|FR|SA|SU),?)+)(;|$)}', $rrule, $m) )
660 $this->byday = explode(',',$m[1]);
661
662 if ( preg_match('{BYYEARDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byyearday = explode(',',$m[1]);
663 if ( preg_match('{BYWEEKNO=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byweekno = explode(',',$m[1]);
664 if ( preg_match('{BYMONTHDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->bymonthday = explode(',',$m[1]);
665 if ( preg_match('{BYMONTH=(([+-]?[0-1]?[0-9],?)+)(;|$)}', $rrule, $m) ) $this->bymonth = explode(',',$m[1]);
666 if ( preg_match('{BYSETPOS=(([+-]?[0-9]{1,3},?)+)(;|$)}', $rrule, $m) ) $this->bysetpos = explode(',',$m[1]);
667
668 if ( preg_match('{BYSECOND=([0-9,]+)(;|$)}', $rrule, $m) ) $this->bysecond = explode(',',$m[1]);
669 if ( preg_match('{BYMINUTE=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byminute = explode(',',$m[1]);
670 if ( preg_match('{BYHOUR=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byhour = explode(',',$m[1]);
671
672 if ( !isset($this->interval) ) $this->interval = 1;
673
674 $freq_name = null;
675 switch( $this->freq ) {
676 case 'SECONDLY': $freq_name = 'second'; break;
677 case 'MINUTELY': $freq_name = 'minute'; break;
678 case 'HOURLY': $freq_name = 'hour'; break;
679 case 'DAILY': $freq_name = 'day'; break;
680 case 'WEEKLY': $freq_name = 'week'; break;
681 case 'MONTHLY': $freq_name = 'month'; break;
682 case 'YEARLY': $freq_name = 'year'; break;
683 default:
685 }
686 $this->frequency_string = sprintf('+%d %s', $this->interval, $freq_name );
687 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Frequency modify string is: '%s', base is: '%s', TZ: %s", $this->frequency_string, $this->base->format('c'), $this->base->getTimeZone() );
688 $this->Start($return_floating_times);
689 }
690
691
696 public function hasLimitedOccurrences() {
697 return ( isset($this->count) || isset($this->until) );
698 }
699
700
701 public function set_timezone( $tzstring ) {
702 $this->base->setTimezone(new DateTimeZone($tzstring));
703 }
704
705
706 public function Start($return_floating_times=false) {
707 $this->instances = array();
708 $this->GetMoreInstances($return_floating_times);
709 $this->rewind();
710 $this->finished = false;
711 }
712
713
714 public function rewind() {
715 $this->position = -1;
716 }
717
718
724 public function next($return_floating_times=false) {
725 $this->position++;
726 return $this->current($return_floating_times);
727 }
728
729
730 public function current($return_floating_times=false) {
731 if ( !$this->valid() ) {
732 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', 'current: not valid at top, return null' );
733 return null;
734 }
735
736 if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances($return_floating_times);
737
738 if ( !isset($this->instances[$this->position]) ) {
739 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "current: \$this->instances[%s] isn't set, return null", $this->position );
740 return null;
741 }
742
743 if ( !$this->valid() ) {
744 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', 'current: not valid after GetMoreInstances, return null' );
745 return null;
746 }
747
748 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Returning date from position %d: %s (%s)", $this->position,
749 $this->instances[$this->position]->format('c'), $this->instances[$this->position]->FloatOrUTC($return_floating_times) );
750
751 return $this->instances[$this->position];
752 }
753
754
755 public function key($return_floating_times=false) {
756 if ( !$this->valid() ) return null;
757 if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances($return_floating_times);
758 if ( !isset($this->keys[$this->position]) ) {
759 $this->keys[$this->position] = $this->instances[$this->position];
760 }
761 return $this->keys[$this->position];
762 }
763
764
765 public function valid() {
766 if ( DEBUG_RRULE && isset($this->instances[$this->position])) {
767 $current = $this->instances[$this->position];
768 dbg_error_log( 'RRULE', "TimeZone: " . $current->getTimeZone());
769 dbg_error_log( 'RRULE', "Date: " . $current->format('r'));
770 dbg_log_array( 'RRULE', "Errors:", $current->getLastErrors());
771 }
772 if ( isset($this->instances[$this->position]) || !$this->finished ) return true;
773 return false;
774 }
775
784 private static function rrule_expand_limit( $freq ) {
785 switch( $freq ) {
786 case 'YEARLY':
787 return array( 'bymonth' => 'expand', 'byweekno' => 'expand', 'byyearday' => 'expand', 'bymonthday' => 'expand',
788 'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
789 case 'MONTHLY':
790 return array( 'bymonth' => 'limit', 'bymonthday' => 'expand',
791 'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
792 case 'WEEKLY':
793 return array( 'bymonth' => 'limit',
794 'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
795 case 'DAILY':
796 return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
797 'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
798 case 'HOURLY':
799 return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
800 'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'expand', 'bysecond' => 'expand' );
801 case 'MINUTELY':
802 return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
803 'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'expand' );
804 case 'SECONDLY':
805 return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
806 'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'limit' );
807 }
808 dbg_error_log('ERROR','Invalid frequency code "%s" - pretending it is "DAILY"', $freq);
809 return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
810 'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
811 }
812
813 private function GetMoreInstances($return_floating_times=false) {
814 global $c;
815 if ( $this->finished ) return;
816 $got_more = false;
817 $loops = 0;
818 if ( $return_floating_times ) $this->base->setAsFloat();
819 while( !$this->finished && !$got_more) {
820 if ($loops++ > $c->rrule_loop_limit ) {
821 dbg_error_log ('ERROR', "RRULE, loop limit has been hit in GetMoreInstances, you probably want to increase \$c->rrule_loop_limit (currently %d)", $c->rrule_loop_limit);
822 break;
823 }
824
825 if ( !isset($this->current_base) ) {
826 $this->current_base = clone($this->base);
827 }
828 else {
829 $this->current_base->modify( $this->frequency_string );
830 }
831 if ( $return_floating_times ) $this->current_base->setAsFloat();
832 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Getting more instances from: '%s' - %d, TZ: %s, Loop: %s", $this->current_base->format('c'), count($this->instances), $this->current_base->getTimeZone(), $loops );
833 $this->current_set = array( clone($this->current_base) );
834 foreach( self::rrule_expand_limit($this->freq) AS $bytype => $action ) {
835 if ( isset($this->{$bytype}) ) {
836 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Going to find more instances by running %s_%s()", $action, $bytype );
837 $this->{$action.'_'.$bytype}();
838 if ( !isset($this->current_set[0]) ) break;
839 }
840 }
841
842 sort($this->current_set);
843 if ( isset($this->bysetpos) ) $this->limit_bysetpos();
844
845 $position = count($this->instances) - 1;
846 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Inserting %d from current_set into position %d", count($this->current_set), $position + 1 );
847
848 foreach( $this->current_set AS $k => $instance ) {
849 if ( $instance < $this->base ) continue;
850 if ( isset($this->until) && $instance > $this->until ) {
851 $this->finished = true;
852 return;
853 }
854 if ( !isset($this->instances[$position]) || $instance != $this->instances[$position] ) {
855 $got_more = true;
856 $position++;
857 if ( isset($this->count) && $position >= $this->count ) {
858 $this->finished = true;
859 return;
860 }
861 $this->instances[$position] = $instance;
862 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Added date %s into position %d in current set", $instance->format('c'), $position );
863 }
864 }
865 }
866 }
867
868
869 public static function rrule_day_number( $day ) {
870 switch( $day ) {
871 case 'SU': return 0;
872 case 'MO': return 1;
873 case 'TU': return 2;
874 case 'WE': return 3;
875 case 'TH': return 4;
876 case 'FR': return 5;
877 case 'SA': return 6;
878 }
879 return false;
880 }
881
882
883 static public function date_mask( $date, $y, $mo, $d, $h, $mi, $s ) {
884 $date_parts = explode(',',$date->format('Y,m,d,H,i,s'));
885
886 if ( isset($y) || isset($mo) || isset($d) ) {
887 if ( isset($y) ) $date_parts[0] = $y;
888 if ( isset($mo) ) $date_parts[1] = $mo;
889 if ( isset($d) ) $date_parts[2] = $d;
890 $date->setDate( $date_parts[0], $date_parts[1], $date_parts[2] );
891 }
892 if ( isset($h) || isset($mi) || isset($s) ) {
893 if ( isset($h) ) $date_parts[3] = $h;
894 if ( isset($mi) ) $date_parts[4] = $mi;
895 if ( isset($s) ) $date_parts[5] = $s;
896 $date->setTime( $date_parts[3], $date_parts[4], $date_parts[5] );
897 }
898 return $date;
899 }
900
901
902 private function expand_bymonth() {
903 $instances = $this->current_set;
904 $this->current_set = array();
905 foreach( $instances AS $k => $instance ) {
906 foreach( $this->bymonth AS $k => $month ) {
907 $expanded = $this->date_mask( clone($instance), null, $month, null, null, null, null);
908 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYMONTH $month into date %s", $expanded->format('c') );
909 $this->current_set[] = $expanded;
910 }
911 }
912 }
913
914 private function expand_bymonthday() {
915 $instances = $this->current_set;
916 $this->current_set = array();
917 foreach( $instances AS $k => $instance ) {
918 foreach( $this->bymonthday AS $k => $monthday ) {
919 $expanded = $this->date_mask( clone($instance), null, null, $monthday, null, null, null);
920 if ($monthday == -1 || $expanded->format('d') == $monthday) {
921 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYMONTHDAY $monthday into date %s from %s", $expanded->format('c'), $instance->format('c') );
922 $this->current_set[] = $expanded;
923 } else {
924 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYMONTHDAY $monthday into date %s from %s, which is not the same day of month, skipping.", $expanded->format('c'), $instance->format('c') );
925 }
926 }
927 }
928 }
929
930 private function expand_byyearday() {
931 $instances = $this->current_set;
932 $this->current_set = array();
933 $days_set = array();
934 foreach( $instances AS $k => $instance ) {
935 foreach( $this->byyearday AS $k => $yearday ) {
936 $on_yearday = clone($instance);
937 $on_yearday->setYearDay($yearday);
938 if ( isset($days_set[$on_yearday->UTC()]) ) continue;
939 $this->current_set[] = $on_yearday;
940 $days_set[$on_yearday->UTC()] = true;
941 }
942 }
943 }
944
945 private function expand_byday_in_week( $day_in_week ) {
946
952 $dow_of_instance = $day_in_week->format('w'); // 0 == Sunday
953 foreach( $this->byday AS $k => $weekday ) {
954 $dow = self::rrule_day_number($weekday);
955 $offset = $dow - $dow_of_instance;
956 if ( $offset < 0 ) $offset += 7;
957 $expanded = clone($day_in_week);
958 $expanded->modify( sprintf('+%d day', $offset) );
959 $this->current_set[] = $expanded;
960 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYDAY(W) $weekday into date %s", $expanded->format('c') );
961 }
962 }
963
964
965 private function expand_byday_in_month( $day_in_month ) {
966
967 $first_of_month = $this->date_mask( clone($day_in_month), null, null, 1, null, null, null);
968 $dow_of_first = $first_of_month->format('w'); // 0 == Sunday
969 $days_in_month = cal_days_in_month(CAL_GREGORIAN, $first_of_month->format('m'), $first_of_month->format('Y'));
970 foreach( $this->byday AS $k => $weekday ) {
971 if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
972 $dow = self::rrule_day_number($matches[3]);
973 $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.
974 $whichweek = intval($matches[2]);
975 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanding BYDAY(M) $weekday in month of %s", $first_of_month->format('c') );
976 if ( $whichweek > 0 ) {
977 $whichweek--;
978 $monthday = $first_dom;
979 if ( $matches[1] == '-' ) {
980 $monthday += 35;
981 while( $monthday > $days_in_month ) $monthday -= 7;
982 $monthday -= (7 * $whichweek);
983 }
984 else {
985 $monthday += (7 * $whichweek);
986 }
987 if ( $monthday > 0 && $monthday <= $days_in_month ) {
988 $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
989 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYDAY(M) $weekday now $monthday into date %s", $expanded->format('c') );
990 $this->current_set[] = $expanded;
991 }
992 }
993 else {
994 for( $monthday = $first_dom; $monthday <= $days_in_month; $monthday += 7 ) {
995 $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
996 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYDAY(M) $weekday now $monthday into date %s", $expanded->format('c') );
997 $this->current_set[] = $expanded;
998 }
999 }
1000 }
1001 }
1002 }
1003
1004
1005 private function expand_byday_in_year( $day_in_year ) {
1006
1007 $first_of_year = $this->date_mask( clone($day_in_year), null, 1, 1, null, null, null);
1008 $dow_of_first = $first_of_year->format('w'); // 0 == Sunday
1009 $days_in_year = 337 + cal_days_in_month(CAL_GREGORIAN, 2, $first_of_year->format('Y'));
1010 foreach( $this->byday AS $k => $weekday ) {
1011 if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
1012 $expanded = clone($first_of_year);
1013 $dow = self::rrule_day_number($matches[3]);
1014 $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.
1015 $whichweek = intval($matches[2]);
1016 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanding BYDAY(Y) $weekday from date %s", $instance->format('c') );
1017 if ( $whichweek > 0 ) {
1018 $whichweek--;
1019 $yearday = $first_doy;
1020 if ( $matches[1] == '-' ) {
1021 $yearday += 371;
1022 while( $yearday > $days_in_year ) $yearday -= 7;
1023 $yearday -= (7 * $whichweek);
1024 }
1025 else {
1026 $yearday += (7 * $whichweek);
1027 }
1028 if ( $yearday > 0 && $yearday <= $days_in_year ) {
1029 $expanded->modify(sprintf('+%d day', $yearday - 1));
1030 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYDAY(Y) $weekday now $yearday into date %s", $expanded->format('c') );
1031 $this->current_set[] = $expanded;
1032 }
1033 }
1034 else {
1035 $expanded->modify(sprintf('+%d day', $first_doy - 1));
1036 for( $yearday = $first_doy; $yearday <= $days_in_year; $yearday += 7 ) {
1037 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Expanded BYDAY(Y) $weekday now $yearday into date %s", $expanded->format('c') );
1038 $this->current_set[] = clone($expanded);
1039 $expanded->modify('+1 week');
1040 }
1041 }
1042 }
1043 }
1044 }
1045
1046
1047 private function expand_byday() {
1048 if ( !isset($this->current_set[0]) ) return;
1049 if ( $this->freq == 'MONTHLY' || $this->freq == 'YEARLY' ) {
1050 if ( isset($this->bymonthday) || isset($this->byyearday) ) {
1051 $this->limit_byday();
1052 return;
1053 }
1054 }
1055 $instances = $this->current_set;
1056 $this->current_set = array();
1057 foreach( $instances AS $k => $instance ) {
1058 if ( $this->freq == 'MONTHLY' ) {
1059 $this->expand_byday_in_month($instance);
1060 }
1061 else if ( $this->freq == 'WEEKLY' ) {
1062 $this->expand_byday_in_week($instance);
1063 }
1064 else { // YEARLY
1065 if ( isset($this->bymonth) ) {
1066 $this->expand_byday_in_month($instance);
1067 }
1068 else if ( isset($this->byweekno) ) {
1069 $this->expand_byday_in_week($instance);
1070 }
1071 else {
1072 $this->expand_byday_in_year($instance);
1073 }
1074 }
1075
1076 }
1077 }
1078
1079 private function expand_byhour() {
1080 $instances = $this->current_set;
1081 $this->current_set = array();
1082 foreach( $instances AS $k => $instance ) {
1083 foreach( $this->byhour AS $k => $hour ) {
1084 $this->current_set[] = $this->date_mask( clone($instance), null, null, null, $hour, null, null);
1085 }
1086 }
1087 }
1088
1089 private function expand_byminute() {
1090 $instances = $this->current_set;
1091 $this->current_set = array();
1092 foreach( $instances AS $k => $instance ) {
1093 foreach( $this->byminute AS $k => $minute ) {
1094 $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, $minute, null);
1095 }
1096 }
1097 }
1098
1099 private function expand_bysecond() {
1100 $instances = $this->current_set;
1101 $this->current_set = array();
1102 foreach( $instances AS $k => $instance ) {
1103 foreach( $this->bysecond AS $k => $second ) {
1104 $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, null, $second);
1105 }
1106 }
1107 }
1108
1109
1110 private function limit_generally( $fmt_char, $element_name ) {
1111 $instances = $this->current_set;
1112 $this->current_set = array();
1113 foreach( $instances AS $k => $instance ) {
1114 foreach( $this->{$element_name} AS $k => $element_value ) {
1115 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' ? %s", $instance->format('c'), $instance->format($fmt_char), $element_value, ($instance->format($fmt_char) == $element_value ? 'Yes' : 'No') );
1116 if ( $instance->format($fmt_char) == $element_value ) $this->current_set[] = $instance;
1117 }
1118 }
1119 }
1120
1121 private function limit_byday() {
1122 $fmt_char = 'w';
1123 $instances = $this->current_set;
1124 $this->current_set = array();
1125 foreach( $this->byday AS $k => $weekday ) {
1126 $dow = self::rrule_day_number($weekday);
1127 foreach( $instances AS $k => $instance ) {
1128 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' (%d) ? %s", $instance->format('c'), $instance->format($fmt_char), $weekday, $dow, ($instance->format($fmt_char) == $dow ? 'Yes' : 'No') );
1129 if ( $instance->format($fmt_char) == $dow ) $this->current_set[] = $instance;
1130 }
1131 }
1132 }
1133
1134 private function limit_bymonth() { $this->limit_generally( 'm', 'bymonth' ); }
1135 private function limit_byyearday() { $this->limit_generally( 'z', 'byyearday' ); }
1136 private function limit_bymonthday() { $this->limit_generally( 'd', 'bymonthday' ); }
1137 private function limit_byhour() { $this->limit_generally( 'H', 'byhour' ); }
1138 private function limit_byminute() { $this->limit_generally( 'i', 'byminute' ); }
1139 private function limit_bysecond() { $this->limit_generally( 's', 'bysecond' ); }
1140
1141
1142 private function limit_bysetpos( ) {
1143 $instances = $this->current_set;
1144 $count = count($instances);
1145 $this->current_set = array();
1146 foreach( $this->bysetpos AS $k => $element_value ) {
1147 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Limiting bysetpos %s of %d instances", $element_value, $count );
1148 if ( $element_value > 0 ) {
1149 $this->current_set[] = $instances[$element_value - 1];
1150 }
1151 else if ( $element_value < 0 ) {
1152 $this->current_set[] = $instances[$count + $element_value];
1153 }
1154 }
1155 }
1156
1157
1158}
1159
1160
1161
1162require_once("vComponent.php");
1163
1173function rdate_expand( $dtstart, $property, $component, $range_end = null, $is_date=null, $return_floating_times=false ) {
1174 $properties = $component->GetProperties($property);
1175 $expansion = array();
1176 foreach( $properties AS $p ) {
1177 $timezone = $p->GetParameterValue('TZID');
1178 $rdate = $p->Value();
1179 $rdates = explode( ',', $rdate );
1180 foreach( $rdates AS $k => $v ) {
1181 $rdate = new RepeatRuleDateTime( $v, $timezone, $is_date);
1182 if ( $return_floating_times ) $rdate->setAsFloat();
1183 $expansion[$rdate->FloatOrUTC($return_floating_times)] = $component;
1184 if ( $rdate > $range_end ) break;
1185 }
1186 }
1187 return $expansion;
1188}
1189
1190
1201function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false, $fallback_tzid=null ) {
1202 global $c;
1203 $expansion = array();
1204
1205 $recur = $component->GetProperty($property);
1206 if ( !isset($recur) ) return $expansion;
1207 $recur = $recur->Value();
1208
1209 $this_start = $component->GetProperty('DTSTART');
1210 if ( isset($this_start) ) {
1211 $this_start = RepeatRuleDateTime::withFallbackTzid($this_start, $fallback_tzid);
1212 }
1213 else {
1214 $this_start = clone($dtstart);
1215 }
1216 if ( $return_floating_times ) $this_start->setAsFloat();
1217
1218// if ( DEBUG_RRULE ) print_r( $this_start );
1219 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "%s (floating: %s)", $recur, ($return_floating_times?"yes":"no") );
1220 $rule = new RepeatRule( $this_start, $recur, $is_date, $return_floating_times );
1221 $i = 0;
1222
1223 if ( !isset($c->rrule_expansion_limit) ) $c->rrule_expansion_limit = 5000;
1224 while( $date = $rule->next($return_floating_times) ) {
1225// if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "[%3d] %s", $i, $date->UTC() );
1226 $expansion[$date->FloatOrUTC($return_floating_times)] = $component;
1227 if ( $date > $range_end ) break;
1228 if ( $i++ >= $c->rrule_expansion_limit ) {
1229 dbg_error_log( 'ERROR', "Hit rrule expansion limit of ".$c->rrule_expansion_limit." on %s %s - increase rrule_expansion_limit in config to avoid events missing from freebusy", $component->GetType(), $component->GetProperty('UID'));
1230 }
1231 }
1232// if ( DEBUG_RRULE ) dbg_log_array( 'RRULE', 'expansion', $expansion );
1233 return $expansion;
1234}
1235
1236
1248function expand_event_instances( vComponent $vResource, $range_start = null, $range_end = null, $return_floating_times=false, $fallback_tzid=null ) {
1249 global $c;
1250 $components = $vResource->GetComponents();
1251
1252 $clear_instance_props = array(
1253 'DTSTART' => true,
1254 'DUE' => true,
1255 'DTEND' => true
1256 );
1257 if ( empty( $c->expanded_instances_include_rrule ) ) {
1258 $clear_instance_props += array(
1259 'RRULE' => true,
1260 'RDATE' => true,
1261 'EXDATE' => true
1262 );
1263 }
1264
1265 if ( empty($range_start) ) { $range_start = new RepeatRuleDateTime(); $range_start->modify('-6 weeks'); }
1266 if ( empty($range_end) ) {
1267 $range_end = clone($range_start);
1268 $range_end->modify('+6 months');
1269 }
1270
1271 dbg_error_log('RRULE', 'Expand event instances, start: %s, end: %s', $range_start, $range_end);
1272
1273 $instances = array();
1274 $expand = false;
1275 $dtstart = null;
1276 $is_date = false;
1277 $has_repeats = false;
1278 $dtstart_type = 'DTSTART';
1279
1280 $components_prefix = [];
1281 $components_base_events = [];
1282 $components_override_events = [];
1283
1284 foreach ($components AS $k => $comp) {
1285 if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) {
1286 // Other types of component (such as VTIMEZONE) go first
1287 $components_prefix[] = $comp;
1288 } else if ($comp->GetProperty('RECURRENCE-ID') === null) {
1289 // This is the base event, we need to handle it first
1290 $components_base_events[] = $comp;
1291 } else {
1292 // This is an override of an event instance, handle it last
1293 $components_override_events[] = $comp;
1294 }
1295 }
1296
1297 $components = array_merge($components_prefix, $components_base_events, $components_override_events);
1298
1299 foreach( $components AS $k => $comp ) {
1300 if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) {
1301 continue;
1302 }
1303 if ( !isset($dtstart) ) {
1304 $dtstart_prop = $comp->GetProperty($dtstart_type);
1305 if ( !isset($dtstart_prop) && $comp->GetType() != 'VTODO' ) {
1306 $dtstart_type = 'DUE';
1307 $dtstart_prop = $comp->GetProperty($dtstart_type);
1308 }
1309 if ( !isset($dtstart_prop) ) continue;
1310 $dtstart = new RepeatRuleDateTime( $dtstart_prop );
1311 if ( $return_floating_times ) $dtstart->setAsFloat();
1312 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Component is: %s (floating: %s)", $comp->GetType(), ($return_floating_times?"yes":"no") );
1313 $is_date = $dtstart->isDate();
1314 $instances[$dtstart->FloatOrUTC($return_floating_times)] = $comp;
1315 $rrule = $comp->GetProperty('RRULE');
1316 $has_repeats = isset($rrule);
1317 }
1318 $p = $comp->GetProperty('RECURRENCE-ID');
1319 if ( isset($p) && $p->Value() != '' ) {
1320 $range = $p->GetParameterValue('RANGE');
1321 $recur_utc = new RepeatRuleDateTime($p);
1322 if ( $is_date ) $recur_utc->setAsDate();
1323 $recur_utc = $recur_utc->FloatOrUTC($return_floating_times);
1324 if ( isset($range) && $range == 'THISANDFUTURE' ) {
1325 foreach( $instances AS $k => $v ) {
1326 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Removing overridden instance at: $k" );
1327 if ( $k >= $recur_utc ) unset($instances[$k]);
1328 }
1329 }
1330 else {
1331 unset($instances[$recur_utc]);
1332 // This is a single instance of a recurring event, it can not in itself produce extra instances due to RRULE etc
1333 continue;
1334 }
1335 }
1336 else if ( DEBUG_RRULE ) {
1337 $p = $comp->GetProperty('SUMMARY');
1338 $summary = ( isset($p) ? $p->Value() : 'not set');
1339 $p = $comp->GetProperty('UID');
1340 $uid = ( isset($p) ? $p->Value() : 'not set');
1341 dbg_error_log( 'RRULE', "Processing event '%s' with UID '%s' starting on %s",
1342 $summary, $uid, $dtstart->FloatOrUTC($return_floating_times) );
1343 dbg_error_log( 'RRULE', "Instances at start");
1344 foreach( $instances AS $k => $v ) {
1345 dbg_error_log( 'RRULE', ' : '.$k);
1346 }
1347 }
1348 $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end, null, $return_floating_times, $fallback_tzid);
1349 if ( DEBUG_RRULE ) {
1350 dbg_error_log( 'RRULE', "After rrule_expand");
1351 foreach( $instances AS $k => $v ) {
1352 dbg_error_log ('RRULE', ' : '.$k);
1353 }
1354 }
1355 $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end, null, $return_floating_times);
1356 if ( DEBUG_RRULE ) {
1357 dbg_error_log( 'RRULE', "After rdate_expand");
1358 foreach( $instances AS $k => $v ) {
1359 dbg_error_log ('RRULE', ' : '.$k);
1360 }
1361 }
1362 foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end, null, $return_floating_times) AS $k => $v ) {
1363 unset($instances[$k]);
1364 }
1365 if ( DEBUG_RRULE ) {
1366 dbg_error_log( 'RRULE', "After exdate_expand");
1367 foreach( $instances AS $k => $v ) {
1368 dbg_error_log( 'RRULE', ' : '.$k);
1369 }
1370 }
1371 }
1372
1373 $last_duration = null;
1374 $early_start = null;
1375 $new_components = array();
1376 $start_utc = $range_start->FloatOrUTC($return_floating_times);
1377 $end_utc = $range_end->FloatOrUTC($return_floating_times);
1378 foreach( $instances AS $utc => $comp ) {
1379 if ( $utc > $end_utc ) {
1380 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "We're done: $utc is out of the range.");
1381 break;
1382 }
1383
1384 $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
1385 $duration = $comp->GetProperty('DURATION');
1386 if ( !isset($duration) || $duration->Value() == '' ) {
1387 $instance_start = $comp->GetProperty($dtstart_type);
1388 $dtsrt = new RepeatRuleDateTime( $instance_start );
1389 if ( $return_floating_times ) $dtsrt->setAsFloat();
1390 $instance_end = $comp->GetProperty($end_type);
1391 if ( isset($instance_end) ) {
1392 $dtend = new RepeatRuleDateTime( $instance_end );
1393 $duration = Rfc5545Duration::fromTwoDates($dtsrt, $dtend);
1394 }
1395 else {
1396 if ( $instance_start->GetParameterValue('VALUE') == 'DATE' ) {
1397 $duration = new Rfc5545Duration('P1D');
1398 }
1399 else {
1400 $duration = new Rfc5545Duration(0);
1401 }
1402 }
1403 }
1404 else {
1405 $duration = new Rfc5545Duration($duration->Value());
1406 }
1407
1408 if ( $utc < $start_utc ) {
1409 if ( isset($early_start) && isset($last_duration) && $duration->equals($last_duration) ) {
1410 if ( $utc < $early_start ) {
1411 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Next please: $utc is before $early_start and before $start_utc.");
1412 continue;
1413 }
1414 }
1415 else {
1417 $latest_start = clone($range_start);
1418 $latest_start->modify('-'.$duration);
1419 $early_start = $latest_start->FloatOrUTC($return_floating_times);
1420 $last_duration = $duration;
1421 if ( $utc < $early_start ) {
1422 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Another please: $utc is before $early_start and before $start_utc.");
1423 continue;
1424 }
1425 }
1426 }
1427 $component = clone($comp);
1428 $component->ClearProperties( $clear_instance_props );
1429 $component->AddProperty($dtstart_type, $utc, ($is_date ? array('VALUE' => 'DATE') : null) );
1430 $component->AddProperty('DURATION', $duration );
1431 if ( $has_repeats && $dtstart->FloatOrUTC($return_floating_times) != $utc )
1432 $component->AddProperty('RECURRENCE-ID', $utc, ($is_date ? array('VALUE' => 'DATE') : null) );
1433 $new_components[$utc] = $component;
1434 }
1435
1436 // Add overriden instances
1437 foreach( $components AS $k => $comp ) {
1438 $p = $comp->GetProperty('RECURRENCE-ID');
1439 if ( isset($p) && $p->Value() != '') {
1440 $recurrence_id = $p->Value();
1441
1442
1443 $dtstart_prop = $comp->GetProperty('DTSTART');
1444 if ( !isset($dtstart_prop) && $comp->GetType() != 'VTODO' ) {
1445 $dtstart_prop = $comp->GetProperty('DUE');
1446 }
1447
1448 if ( !isset($new_components[$recurrence_id]) && !isset($dtstart_prop) ) continue; // No start: no expansion. Note that we consider 'DUE' to be a start if DTSTART is missing
1449 $dtstart_rrdt = new RepeatRuleDateTime( $dtstart_prop );
1450 $is_date = $dtstart_rrdt->isDate();
1451 if ( $return_floating_times ) $dtstart_rrdt->setAsFloat();
1452 $dtstart = $dtstart_rrdt->FloatOrUTC($return_floating_times);
1453 if ( !isset($new_components[$recurrence_id]) && $dtstart > $end_utc ) continue; // Start after end of range, skip it
1454
1455 $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
1456 $duration = $comp->GetProperty('DURATION');
1457
1458 if ( !isset($duration) || $duration->Value() == '' ) {
1459 $instance_end = $comp->GetProperty($end_type);
1460 if ( isset($instance_end) ) {
1461 $dtend_rrdt = new RepeatRuleDateTime( $instance_end );
1462 if ( $return_floating_times ) $dtend_rrdt->setAsFloat();
1463 $dtend = $dtend_rrdt->FloatOrUTC($return_floating_times);
1464
1465 $comp->AddProperty('DURATION', Rfc5545Duration::fromTwoDates($dtstart_rrdt, $dtend_rrdt) );
1466 }
1467 else {
1468 $dtend = $dtstart + ($is_date ? $dtstart + 86400 : 0 );
1469 }
1470 }
1471 else {
1472 $duration = new Rfc5545Duration($duration->Value());
1473 $dtend = $dtstart + $duration->asSeconds();
1474 }
1475
1476 if ( !isset($new_components[$recurrence_id]) && $dtend < $start_utc ) continue; // End before start of range: skip that too.
1477
1478 if ( DEBUG_RRULE ) dbg_error_log( 'RRULE', "Replacing overridden instance at %s", $recurrence_id);
1479 $new_components[$recurrence_id] = $comp;
1480 }
1481 }
1482
1483 $vResource->SetComponents($new_components);
1484
1485 return $vResource;
1486}
1487
1488
1496function getComponentRange(vComponent $comp, $fallback_tzid = null) {
1497 $dtstart_prop = $comp->GetProperty('DTSTART');
1498 $duration_prop = $comp->GetProperty('DURATION');
1499 if ( isset($duration_prop) ) {
1500 if ( !isset($dtstart_prop) ) throw new Exception('Invalid '.$comp->GetType().' containing DURATION without DTSTART', 0);
1501 $dtstart = RepeatRuleDateTime::withFallbackTzid($dtstart_prop, $fallback_tzid);
1502 $dtend = clone($dtstart);
1503 $dtend->modify(new Rfc5545Duration($duration_prop->Value()));
1504 }
1505 else {
1506 $completed_prop = null;
1507 switch ( $comp->GetType() ) {
1508 case 'VEVENT':
1509 if ( !isset($dtstart_prop) ) throw new Exception('Invalid VEVENT without DTSTART', 0);
1510 $dtend_prop = $comp->GetProperty('DTEND');
1511 break;
1512 case 'VTODO':
1513 $completed_prop = $comp->GetProperty('COMPLETED');
1514 $dtend_prop = $comp->GetProperty('DUE');
1515 break;
1516 case 'VJOURNAL':
1517 if ( !isset($dtstart_prop) )
1518 $dtstart_prop = $comp->GetProperty('DTSTAMP');
1519 $dtend_prop = $dtstart_prop;
1520 break;
1521 default:
1522 throw new Exception('getComponentRange cannot handle "'.$comp->GetType().'" components', 0);
1523 }
1524
1525 if ( isset($dtstart_prop) )
1526 $dtstart = RepeatRuleDateTime::withFallbackTzid($dtstart_prop, $fallback_tzid);
1527 else
1528 $dtstart = null;
1529
1530 if ( isset($dtend_prop) )
1531 $dtend = RepeatRuleDateTime::withFallbackTzid($dtend_prop, $fallback_tzid);
1532 else
1533 $dtend = null;
1534
1535 if ( isset($completed_prop) ) {
1536 $completed = RepeatRuleDateTime::withFallbackTzid($completed_prop, $fallback_tzid);
1537 if ( !isset($dtstart) || (isset($dtstart) && $completed < $dtstart) ) $dtstart = $completed;
1538 if ( !isset($dtend) || (isset($dtend) && $completed > $dtend) ) $dtend = $completed;
1539 }
1540 }
1541 return new RepeatRuleDateRange($dtstart, $dtend);
1542}
1543
1553function getVCalendarRange( $vResource, $fallback_tzid = null ) {
1554 $components = $vResource->GetComponents();
1555
1556 $dtstart = null;
1557 $duration = null;
1558 $earliest_start = null;
1559 $latest_end = null;
1560 $has_repeats = false;
1561 foreach( $components AS $k => $comp ) {
1562 if ( $comp->GetType() == 'VTIMEZONE' ) continue;
1563 $range = getComponentRange($comp, $fallback_tzid);
1564 $dtstart = $range->from;
1565 if ( !isset($dtstart) ) continue;
1566 $duration = $range->getDuration();
1567
1568 $rrule = $comp->GetProperty('RRULE');
1569 $limited_occurrences = true;
1570 if ( isset($rrule) ) {
1571 $rule = new RepeatRule($dtstart, $rrule);
1572 $limited_occurrences = $rule->hasLimitedOccurrences();
1573 }
1574
1575 if ( $limited_occurrences ) {
1576 $instances = array();
1577 $instances[$dtstart->FloatOrUTC()] = $dtstart;
1578 if ( !isset($range_end) ) {
1579 $range_end = new RepeatRuleDateTime();
1580 $range_end->modify('+150 years');
1581 }
1582 $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end, null, false, $fallback_tzid);
1583 $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end);
1584 foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end) AS $k => $v ) {
1585 unset($instances[$k]);
1586 }
1587 if ( count($instances) < 1 ) {
1588 if ( empty($earliest_start) || $dtstart < $earliest_start ) $earliest_start = $dtstart;
1589 $latest_end = null;
1590 break;
1591 }
1592 $instances = array_keys($instances);
1593 asort($instances);
1594 $first = new RepeatRuleDateTime($instances[0]);
1595 $last = new RepeatRuleDateTime($instances[count($instances)-1]);
1596 $last->modify($duration);
1597 if ( empty($earliest_start) || $first < $earliest_start ) $earliest_start = $first;
1598 if ( empty($latest_end) || $last > $latest_end ) $latest_end = $last;
1599 }
1600 else {
1601 if ( empty($earliest_start) || $dtstart < $earliest_start ) $earliest_start = $dtstart;
1602 $latest_end = null;
1603 break;
1604 }
1605 }
1606
1607 return new RepeatRuleDateRange($earliest_start, $latest_end );
1608}
__construct( $date1, $date2)
Definition RRule.php:560
overlaps(RepeatRuleDateRange $other)
Definition RRule.php:576
UTC($fmt='Ymd\THis\Z')
Definition RRule.php:383
RFC5545($return_floating_times=false)
Definition RRule.php:426
static daysInMonth( $year, $month)
Definition RRule.php:477
static hasLeapDay($year)
Definition RRule.php:466
FloatOrUTC($return_floating_times=false)
Definition RRule.php:410
hasLimitedOccurrences()
Definition RRule.php:696
expand_byday()
Definition RRule.php:1047
static rrule_expand_limit( $freq)
Definition RRule.php:784
next($return_floating_times=false)
Definition RRule.php:724
expand_byday_in_week( $day_in_week)
Definition RRule.php:945
__construct( $basedate, $rrule, $is_date=null, $return_floating_times=false)
Definition RRule.php:641
__construct( $in_duration)
Definition RRule.php:87
equals( $other)
Definition RRule.php:107
static fromTwoDates( $d1, $d2)
Definition RRule.php:198