Overview

Packages

  • awl
    • caldav-client-v2
    • RRule
  • davical
    • authentication
      • drivers
    • caldav
    • DAViCalSession
    • DAVTicket
    • external-bind
    • feed
    • HTTPAuthSession
    • iSchedule
    • iSchedule-POST
    • logging
    • metrics
    • Principal
    • propfind
    • PublicSession
    • Request
    • Resource
    • tzservice
  • None
  • PHP

Classes

  • RepeatRule
  • RepeatRuleDateRange
  • RepeatRuleDateTime
  • RepeatRuleTimeZone
  • Rfc5545Duration

Functions

  • expand_event_instances
  • getComponentRange
  • getVCalendarRange
  • olson_from_vtimezone
  • rdate_expand
  • rrule_expand
  • Overview
  • Package
  • Function
  • Tree
  • Deprecated
  • Todo
   1: <?php
   2: /**
   3: * Class for parsing RRule and getting us the dates (v2)
   4: *
   5: * @package   awl
   6: * @subpackage RRule
   7: * @author    Andrew McMillan <andrew@mcmillan.net.nz>
   8: * @copyright Morphoss Ltd
   9: * @license   http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
  10: */
  11: 
  12: if ( !class_exists('DateTime') ) return;
  13: 
  14: /**
  15: * Try and extract something like "Pacific/Auckland" or "America/Indiana/Indianapolis" if possible, given
  16: * the VTIMEZONE component that is passed in.  This is much more complex than olson_from_tzstring since
  17: * we start to examine the rules and work out what actual timezone this might be.
  18: */
  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: 
  27:   /**
  28:    * @todo: We'll do other stuff here, in due course...
  29:    */
  30:   return null;
  31: }
  32: 
  33: // define( 'DEBUG_RRULE', true);
  34: define( 'DEBUG_RRULE', false );
  35: 
  36: /**
  37: * Wrap the DateTimeZone class to allow parsing some iCalendar TZID strangenesses
  38: */
  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: 
  73: /**
  74:  * Provide a useful way of dealing with RFC5545 duration strings of the form
  75:  * ^-?P(\dW)|((\dD)?(T(\dH)?(\dM)?(\dS)?)?)$
  76:  */
  77: class Rfc5545Duration {
  78:   private $epoch_seconds = null;
  79:   private $days = 0;
  80:   private $secs = 0;
  81:   private $as_text = '';
  82: 
  83:   /**
  84:    * Construct a new Rfc5545Duration either from incoming seconds or a text string.
  85:    * @param mixed $in_duration
  86:    */
  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: 
 102:   /**
 103:    * Return true if $this and $other are equal, false otherwise.
 104:    * @param Rfc5545Duration $other
 105:    * @return boolean
 106:    */
 107:   function equals( $other ) {
 108:     if ( $this == $other ) return true;
 109:     if ( $this->asSeconds() == $other->asSeconds() ) return true;
 110:     return false;
 111:   }
 112: 
 113:   /**
 114:    * Returns the duration as epoch seconds.
 115:    */
 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: 
 145:   /**
 146:    * Returns the duration as a text string of the form ^(-?)P(\d+W)|((\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?)$
 147:    * @return string The stringified stuff.
 148:    */
 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 >= 86400 ) {
 154:         $this->days = floor($in_duration / 86400);
 155:         $in_duration -= $this->days * 86400;
 156:         if ( $in_duration == 0 && ($this->days / 7) == floor($this->days / 7) ) {
 157:           $this->as_text .= ($this->days/7).'W';
 158:           return $this->as_text;
 159:         }
 160:         $this->as_text .= $this->days.'D';
 161:       }
 162:       if ( $in_duration > 0 ) {
 163:         $secs = $in_duration;
 164:         $this->as_text .= 'T';
 165:         $hours = floor($in_duration / 3600);
 166:         if ( $hours > 0 ) $this->as_text .= $hours . 'H';
 167:         $minutes = floor(($in_duration % 3600) / 60);
 168:         if ( $minutes > 0 ) $this->as_text .= $minutes . 'M';
 169:         $seconds = $in_duration % 60;
 170:         if ( $seconds > 0 ) $this->as_text .= $seconds . 'S';
 171:       }
 172:     }
 173:     return $this->as_text;
 174:   }
 175: 
 176: 
 177:   /**
 178:    * Factory method to return an Rfc5545Duration object from the difference
 179:    * between two dates.
 180:    *
 181:    * This is flawed, at present: we should really localise both dates and work
 182:    * out the difference in days, then localise the times and work out the difference
 183:    * between the clock times.  On the other hand we're replacing a quick and dirty
 184:    * hack that did it exactly the same way in the past, so we're not making things
 185:    * any *worse* and at least we're making it clear that it could be improved...
 186:    *
 187:    * The problem strikes (as they all do) across DST boundaries.
 188:    *
 189:    * @todo Improve this to calculate the days difference and then the clock time diff
 190:    * and work from there.
 191:    *
 192:    * @param RepeatRuleDateTime $d1
 193:    * @param RepeatRuleDateTime $d2
 194:    * @return Rfc5545Duration
 195:    */
 196:   static function fromTwoDates( $d1, $d2 ) {
 197:     $diff = $d2->epoch() - $d1->epoch();
 198:     return new Rfc5545Duration($diff);
 199:   }
 200: }
 201: 
 202: /**
 203: * Wrap the DateTime class to make it friendlier to passing in random strings from iCalendar
 204: * objects, and especially the random stuff used to identify timezones.  We also add some
 205: * utility methods and stuff too, in order to simplify some of the operations we need to do
 206: * with dates.
 207: */
 208: class RepeatRuleDateTime extends DateTime {
 209:   // public static $Format = 'Y-m-d H:i:s';
 210:   public static $Format = 'c';
 211:   private static $UTCzone;
 212:   private $tzid;
 213:   private $is_date;
 214: 
 215:   public function __construct($date = null, $dtz = null, $is_date = null ) {
 216:     if ( !isset(self::$UTCzone) ) self::$UTCzone = new RepeatRuleTimeZone('UTC');
 217:     $this->is_date = false;
 218:     if ( isset($is_date) ) $this->is_date = $is_date;
 219:     if ( !isset($date) ) {
 220:       $date = date('Ymd\THis');
 221:       // Floating
 222:       $dtz = self::$UTCzone;
 223:     }
 224:     $this->tzid = null;
 225: 
 226:     if ( is_object($date) && method_exists($date,'GetParameterValue') ) {
 227:       $tzid = $date->GetParameterValue('TZID');
 228:       $actual_date = $date->Value();
 229:       if ( isset($tzid) ) {
 230:         $dtz = new RepeatRuleTimeZone($tzid);
 231:         $this->tzid = $dtz->tzid();
 232:       }
 233:       else {
 234:         $dtz = self::$UTCzone;
 235:         if ( substr($actual_date,-1) == 'Z' ) {
 236:           $this->tzid = 'UTC';
 237:           $actual_date = substr($actual_date, 0, strlen($actual_date) - 1);
 238:         }
 239:       }
 240:       if ( strlen($actual_date) == 8 ) {
 241:         // We allow dates without VALUE=DATE parameter, but we don't create them like that
 242:         $this->is_date = true;
 243:       }
 244: //      $value_type = $date->GetParameterValue('VALUE');
 245: //      if ( isset($value_type) && $value_type == 'DATE' ) $this->is_date = true;
 246:       $date = $actual_date;
 247:       if ( DEBUG_RRULE ) printf( "Date%s property%s: %s%s\n", ($this->is_date ? "" : "Time"),
 248:               (isset($this->tzid) ? ' with timezone' : ''), $date,
 249:               (isset($this->tzid) ? ' in '.$this->tzid : '') );
 250:     }
 251:     elseif (preg_match('/;TZID= ([^:;]+) (?: ;.* )? : ( \d{8} (?:T\d{6})? ) (Z)?/x', $date, $matches) ) {
 252:       $date = $matches[2];
 253:       $this->is_date = (strlen($date) == 8);
 254:       if ( isset($matches[3]) && $matches[3] == 'Z' ) {
 255:         $dtz = self::$UTCzone;
 256:         $this->tzid = 'UTC';
 257:       }
 258:       else if ( isset($matches[1]) && $matches[1] != '' ) {
 259:         $dtz = new RepeatRuleTimeZone($matches[1]);
 260:         $this->tzid = $dtz->tzid();
 261:       }
 262:       else {
 263:         $dtz = self::$UTCzone;
 264:         $this->tzid = null;
 265:       }
 266:       if ( DEBUG_RRULE ) printf( "Date%s property%s: %s%s\n", ($this->is_date ? "" : "Time"),
 267:               (isset($this->tzid) ? ' with timezone' : ''), $date,
 268:               (isset($this->tzid) ? ' in '.$this->tzid : '') );
 269:     }
 270:     elseif ( ( $dtz === null || $dtz == '' )
 271:              && preg_match('{;VALUE=DATE (?:;[^:]+) : ((?:[12]\d{3}) (?:0[1-9]|1[012]) (?:0[1-9]|[12]\d|3[01]Z?) )$}x', $date, $matches) ) {
 272:       $this->is_date = true;
 273:       $date = $matches[1];
 274:       // Floating
 275:       $dtz = self::$UTCzone;
 276:       $this->tzid = null;
 277:       if ( DEBUG_RRULE ) printf( "Floating Date value: %s\n", $date );
 278:     }
 279:     elseif ( $dtz === null || $dtz == '' ) {
 280:       $dtz = self::$UTCzone;
 281:       if ( preg_match('/(\d{8}(T\d{6})?)(Z?)/', $date, $matches) ) {
 282:         $date = $matches[1];
 283:         $this->tzid = ( $matches[3] == 'Z' ? 'UTC' : null );
 284:       }
 285:       $this->is_date = (strlen($date) == 8 );
 286:       if ( DEBUG_RRULE ) printf( "Date%s value with timezone: %s in %s\n", ($this->is_date?"":"Time"), $date, $this->tzid );
 287:     }
 288:     elseif ( is_string($dtz) ) {
 289:       $dtz = new RepeatRuleTimeZone($dtz);
 290:       $this->tzid = $dtz->tzid();
 291:       $type = gettype($date);
 292:       if ( DEBUG_RRULE ) printf( "Date%s $type with timezone: %s in %s\n", ($this->is_date?"":"Time"), $date, $this->tzid );
 293:     }
 294:     else {
 295:       $this->tzid = $dtz->getName();
 296:       $type = gettype($date);
 297:       if ( DEBUG_RRULE ) printf( "Date%s $type with timezone: %s in %s\n", ($this->is_date?"":"Time"), $date, $this->tzid );
 298:     }
 299: 
 300:     parent::__construct($date, $dtz);
 301:     if ( isset($is_date) ) $this->is_date = $is_date;
 302: 
 303:     return $this;
 304:   }
 305: 
 306: 
 307:   public function __toString() {
 308:     return (string)parent::format(self::$Format) . ' ' . parent::getTimeZone()->getName();
 309:   }
 310: 
 311: 
 312:   public function AsDate() {
 313:     return $this->format('Ymd');
 314:   }
 315: 
 316: 
 317:   public function setAsFloat() {
 318:     unset($this->tzid);
 319:   }
 320: 
 321: 
 322:   public function isFloating() {
 323:     return !isset($this->tzid);
 324:   }
 325: 
 326:   public function isDate() {
 327:     return $this->is_date;
 328:   }
 329: 
 330: 
 331:   public function setAsDate() {
 332:     $this->is_date = true;
 333:   }
 334: 
 335: 
 336:   public function modify( $interval ) {
 337: //    print ">>$interval<<\n";
 338:     if ( preg_match('{^(-)?P(([0-9-]+)W)?(([0-9-]+)D)?T?(([0-9-]+)H)?(([0-9-]+)M)?(([0-9-]+)S)?$}', $interval, $matches) ) {
 339:       $minus = (isset($matches[1])?$matches[1]:'');
 340:       $interval = '';
 341:       if ( isset($matches[2]) && $matches[2] != '' ) $interval .= $minus . $matches[3] . ' weeks ';
 342:       if ( isset($matches[4]) && $matches[4] != '' ) $interval .= $minus . $matches[5] . ' days ';
 343:       if ( isset($matches[6]) && $matches[6] != '' ) $interval .= $minus . $matches[7] . ' hours ';
 344:       if ( isset($matches[8]) && $matches[8] != '' ) $interval .= $minus . $matches[9] . ' minutes ';
 345:       if (isset($matches[10]) &&$matches[10] != '' ) $interval .= $minus . $matches[11] . ' seconds ';
 346:     }
 347: //    printf( "Modify '%s' by: >>%s<<\n", $this->__toString(), $interval );
 348: //    print_r($this);
 349:     if ( !isset($interval) || $interval == '' ) $interval = '1 day';
 350:     if ( parent::format('d') > 28 && strstr($interval,'month') !== false ) {
 351:       $this->setDate(null,null,28);
 352:     }
 353:     parent::modify($interval);
 354:     return $this->__toString();
 355:   }
 356: 
 357: 
 358:   /**
 359:    * Always returns a time localised to UTC.  Even floating times are converted to UTC
 360:    * using the server's currently configured PHP timezone.  Even dates will include a
 361:    * time, which will be non-zero if they were localised dates.
 362:    *
 363:    * @see RepeatRuleDateTime::FloatOrUTC()
 364:    */
 365:   public function UTC($fmt = 'Ymd\THis\Z' ) {
 366:     $gmt = clone($this);
 367:     if ( $this->tzid != 'UTC' ) {
 368:       if ( isset($this->tzid)) {
 369:         $dtz = parent::getTimezone();
 370:       }
 371:       else {
 372:         $dtz = new DateTimeZone(date_default_timezone_get());
 373:       }
 374:       $offset = 0 - $dtz->getOffset($gmt);
 375:       $gmt->modify( $offset . ' seconds' );
 376:     }
 377:     return $gmt->format($fmt);
 378:   }
 379: 
 380: 
 381:   /**
 382:    * If this is a localised time then this will return the UTC equivalent.  If it is a
 383:    * floating time, then you will just get the floating time.  If it is a date then it
 384:    * will be returned as a date.  Note that if it is a *localised* date then the answer
 385:    * will still be the UTC equivalent but only the date itself will be returned.
 386:    *
 387:    * If return_floating_times is true then all dates will be returned as floating times
 388:    * and UTC will not be returned.
 389:    *
 390:    * @see RepeatRuleDateTime::UTC()
 391:    */
 392:   public function FloatOrUTC($return_floating_times = false) {
 393:     $gmt = clone($this);
 394:     if ( !$return_floating_times && isset($this->tzid) && $this->tzid != 'UTC' ) {
 395:       $dtz = parent::getTimezone();
 396:       $offset = 0 - $dtz->getOffset($gmt);
 397:       $gmt->modify( $offset . ' seconds' );
 398:     }
 399:     if ( $this->is_date ) return $gmt->format('Ymd');
 400:     if ( $return_floating_times ) return $gmt->format('Ymd\THis');
 401:     return $gmt->format('Ymd\THis') . (!$return_floating_times && isset($this->tzid) ? 'Z' : '');
 402:   }
 403: 
 404: 
 405:   /**
 406:    * Returns the string following a property name for an RFC5545 DATE-TIME value.
 407:    */
 408:   public function RFC5545($return_floating_times = false) {
 409:     $result = '';
 410:     if ( isset($this->tzid) && $this->tzid != 'UTC' ) {
 411:       $result = ';TZID='.$this->tzid;
 412:     }
 413:     if ( $this->is_date ) {
 414:       $result .= ';VALUE=DATE:' . $this->format('Ymd');
 415:     }
 416:     else {
 417:       $result .= ':' . $this->format('Ymd\THis');
 418:       if ( !$return_floating_times && isset($this->tzid) && $this->tzid == 'UTC' ) {
 419:         $result .= 'Z';
 420:       }
 421:     }
 422:     return $result;
 423:   }
 424: 
 425: 
 426:   public function setTimeZone( $tz ) {
 427:     if ( is_string($tz) ) {
 428:       $tz = new RepeatRuleTimeZone($tz);
 429:       $this->tzid = $tz->tzid();
 430:     }
 431:     parent::setTimeZone( $tz );
 432:     return $this;
 433:   }
 434: 
 435: 
 436:   public function getTimeZone() {
 437:     return $this->tzid;
 438:   }
 439: 
 440: 
 441:   /**
 442:    * Returns a 1 if this year is a leap year, otherwise a 0
 443:    * @param int $year The year we are quizzical about.
 444:    * @return 1 if this is a leap year, 0 otherwise
 445:    */
 446:   public static function hasLeapDay($year) {
 447:     if ( ($year % 4) == 0 && (($year % 100) != 0 || ($year % 400) == 0) ) return 1;
 448:     return 0;
 449:   }
 450: 
 451:   /**
 452:    * Returns the number of days in a year/month pair
 453:    * @param int $year
 454:    * @param int $month
 455:    * @return int the number of days in the month
 456:    */
 457:   public static function daysInMonth( $year, $month ) {
 458:     if ($month == 4 || $month == 6 || $month == 9 || $month == 11) return 30;
 459:     else if ($month != 2) return 31;
 460:     return 28 + RepeatRuleDateTime::hasLeapDay($year);
 461:   }
 462: 
 463: 
 464:   function setDate( $year=null, $month=null, $day=null ) {
 465:     if ( !isset($year) )  $year  = parent::format('Y');
 466:     if ( !isset($month) ) $month = parent::format('m');
 467:     if ( !isset($day) )   $day   = parent::format('d');
 468:     if ( $day < 0 ) {
 469:       $day += RepeatRuleDateTime::daysInMonth($year, $month) + 1;
 470:     }
 471:     parent::setDate( $year , $month , $day );
 472:     return $this;
 473:   }
 474: 
 475:   function setYearDay( $yearday ) {
 476:     if ( $yearday > 0 ) {
 477:       $current_yearday = parent::format('z') + 1;
 478:     }
 479:     else {
 480:       $current_yearday = (parent::format('z') - (365 + parent::format('L')));
 481:     }
 482:     $diff = $yearday - $current_yearday;
 483:     if ( $diff < 0 ) $this->modify('-P'.-$diff.'D');
 484:     else if ( $diff > 0 ) $this->modify('P'.$diff.'D');
 485: //    printf( "Current: %d, Looking for: %d, Diff: %d, What we got: %s (%d,%d)\n", $current_yearday, $yearday, $diff,
 486: //                 parent::format('Y-m-d'), (parent::format('z')+1), ((parent::format('z') - (365 + parent::format('L')))) );
 487:     return $this;
 488:   }
 489: 
 490:   function year() {
 491:     return parent::format('Y');
 492:   }
 493: 
 494:   function month() {
 495:     return parent::format('m');
 496:   }
 497: 
 498:   function day() {
 499:     return parent::format('d');
 500:   }
 501: 
 502:   function hour() {
 503:     return parent::format('H');
 504:   }
 505: 
 506:   function minute() {
 507:     return parent::format('i');
 508:   }
 509: 
 510:   function second() {
 511:     return parent::format('s');
 512:   }
 513: 
 514:   function epoch() {
 515:     return parent::format('U');
 516:   }
 517: }
 518: 
 519: 
 520: /**
 521:  * This class is used to hold a pair of dates defining a range.  The range may be open-ended by including
 522:  * a null for one end or the other, or both.
 523:  *
 524:  * @author Andrew McMillan <andrew@mcmillan.net.nz>
 525:  */
 526: class RepeatRuleDateRange {
 527:   public $from;
 528:   public $until;
 529: 
 530:   /**
 531:    * Construct a new RepeatRuleDateRange which will be the range between $date1 and $date2. The earliest of the two
 532:    * dates will be used as the start of the period, the latest as the end.  If one of the dates is null then the order
 533:    * of the parameters is significant, with the null treated as -infinity if it is first, or +infinity if it is second.
 534:    * If both parameters are null then the range is from -infinity to +infinity.
 535:    *
 536:    * @param RepeatRuleDateTime $date1
 537:    * @param RepeatRuleDateTime $date2
 538:    */
 539:   function __construct( $date1, $date2 ) {
 540:     if ( $date1 != null && $date2 != null && $date1 > $date2 )  {
 541:       $this->from = $date2;
 542:       $this->until = $date1;
 543:     }
 544:     else {
 545:       $this->from = $date1;
 546:       $this->until = $date2;
 547:     }
 548:   }
 549: 
 550:   /**
 551:    * Assess whether this range overlaps the supplied range.  null values are treated as infinity.
 552:    * @param RepeatRuleDateRange $other
 553:    * @return boolean
 554:    */
 555:   function overlaps( RepeatRuleDateRange $other ) {
 556:     if ( ($this->until == null && $this->from == null) || ($other->until == null && $other->from == null ) ) return true;
 557:     if ( $this->until == null && $other->until == null ) return true;
 558:     if ( $this->from == null && $other->from == null ) return true;
 559: 
 560:     if ( $this->until == null ) return ($other->until > $this->from);
 561:     if ( $this->from == null ) return ($other->from < $this->until);
 562:     if ( $other->until == null ) return ($this->until > $other->from);
 563:     if ( $other->from == null ) return ($thi->from < $other->until);
 564: 
 565:     return !( $this->until < $other->from || $this->from > $other->until );
 566:   }
 567: 
 568:   /**
 569:    * Get an Rfc5545Duration from this date range.  If the from date is null it will be null.
 570:    * If the until date is null the duration will either be 1 day (if the from is a date) or 0 otherwise.
 571:    *
 572:    * @return NULL|Rfc5545Duration
 573:    */
 574:   function getDuration() {
 575:     if ( !isset($this->from) ) return null;
 576:     if ( $this->from->isDate() && !isset($this->until) )
 577:       $duration = 'P1D';
 578:     else if ( !isset($this->until) )
 579:       $duration = 'P0D';
 580:     else
 581:       $duration = ( $this->until->epoch() - $this->from->epoch() );
 582:     return new Rfc5545Duration( $duration );
 583:   }
 584: }
 585: 
 586: 
 587: /**
 588:  * This class is an implementation of RRULE parsing and expansion, as per RFC5545.  It should be reasonably
 589:  * complete, except that it does not handle changing the WKST - there may be a few errors in unusual rules
 590:  * also, but all of the common cases should be handled correctly.
 591:  *
 592:  * @author Andrew McMillan <andrew@mcmillan.net.nz>
 593:  */
 594: class RepeatRule {
 595: 
 596:   private $base;
 597:   private $until;
 598:   private $freq;
 599:   private $count;
 600:   private $interval;
 601:   private $bysecond;
 602:   private $byminute;
 603:   private $byhour;
 604:   private $bymonthday;
 605:   private $byyearday;
 606:   private $byweekno;
 607:   private $byday;
 608:   private $bymonth;
 609:   private $bysetpos;
 610:   private $wkst;
 611: 
 612:   private $instances;
 613:   private $position;
 614:   private $finished;
 615:   private $current_base;
 616:   private $original_rule;
 617: 
 618: 
 619:   public function __construct( $basedate, $rrule, $is_date=null, $return_floating_times=false ) {
 620:     if ( $return_floating_times ) $basedate->setAsFloat();
 621:     $this->base = (is_object($basedate) ? $basedate : new RepeatRuleDateTime($basedate) );
 622:     $this->original_rule = $rrule;
 623: 
 624:     if ( DEBUG_RRULE ) {
 625:       printf( "Constructing RRULE based on: '%s', rrule: '%s' (we float: %s)\n", $basedate, $rrule, ($return_floating_times?"yes":"no") );
 626:     }
 627: 
 628:     if ( preg_match('{FREQ=([A-Z]+)(;|$)}', $rrule, $m) ) $this->freq = $m[1];
 629: 
 630:     if ( preg_match('{UNTIL=([0-9TZ]+)(;|$)}', $rrule, $m) )
 631:       $this->until = new RepeatRuleDateTime($m[1],$this->base->getTimeZone(),$is_date);
 632:     if ( preg_match('{COUNT=([0-9]+)(;|$)}', $rrule, $m) ) $this->count = $m[1];
 633:     if ( preg_match('{INTERVAL=([0-9]+)(;|$)}', $rrule, $m) ) $this->interval = $m[1];
 634: 
 635:     if ( preg_match('{WKST=(MO|TU|WE|TH|FR|SA|SU)(;|$)}', $rrule, $m) ) $this->wkst = $m[1];
 636: 
 637:     if ( preg_match('{BYDAY=(([+-]?[0-9]{0,2}(MO|TU|WE|TH|FR|SA|SU),?)+)(;|$)}', $rrule, $m) )
 638:       $this->byday = explode(',',$m[1]);
 639: 
 640:     if ( preg_match('{BYYEARDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byyearday = explode(',',$m[1]);
 641:     if ( preg_match('{BYWEEKNO=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byweekno = explode(',',$m[1]);
 642:     if ( preg_match('{BYMONTHDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->bymonthday = explode(',',$m[1]);
 643:     if ( preg_match('{BYMONTH=(([+-]?[0-1]?[0-9],?)+)(;|$)}', $rrule, $m) ) $this->bymonth = explode(',',$m[1]);
 644:     if ( preg_match('{BYSETPOS=(([+-]?[0-9]{1,3},?)+)(;|$)}', $rrule, $m) ) $this->bysetpos = explode(',',$m[1]);
 645: 
 646:     if ( preg_match('{BYSECOND=([0-9,]+)(;|$)}', $rrule, $m) ) $this->bysecond = explode(',',$m[1]);
 647:     if ( preg_match('{BYMINUTE=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byminute = explode(',',$m[1]);
 648:     if ( preg_match('{BYHOUR=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byhour = explode(',',$m[1]);
 649: 
 650:     if ( !isset($this->interval) ) $this->interval = 1;
 651:     switch( $this->freq ) {
 652:       case 'SECONDLY': $this->freq_name = 'second'; break;
 653:       case 'MINUTELY': $this->freq_name = 'minute'; break;
 654:       case 'HOURLY':   $this->freq_name = 'hour';   break;
 655:       case 'DAILY':    $this->freq_name = 'day';    break;
 656:       case 'WEEKLY':   $this->freq_name = 'week';   break;
 657:       case 'MONTHLY':  $this->freq_name = 'month';  break;
 658:       case 'YEARLY':   $this->freq_name = 'year';   break;
 659:       default:
 660:         /** need to handle the error, but FREQ is mandatory so unlikely */
 661:     }
 662:     $this->frequency_string = sprintf('+%d %s', $this->interval, $this->freq_name );
 663:     if ( DEBUG_RRULE ) printf( "Frequency modify string is: '%s', base is: '%s'\n", $this->frequency_string, $this->base->format('c') );
 664:     $this->Start($return_floating_times);
 665:   }
 666: 
 667: 
 668:   /**
 669:    * If this repeat rule has an UNTIL= or COUNT= then we can know it will end. Eventually.
 670:    * @return boolean Whether or not one of these properties is present.
 671:    */
 672:   public function hasLimitedOccurrences() {
 673:     return ( isset($this->count) || isset($this->until) );
 674:   }
 675: 
 676: 
 677:   public function set_timezone( $tzstring ) {
 678:     $this->base->setTimezone(new DateTimeZone($tzstring));
 679:   }
 680: 
 681: 
 682:   public function Start($return_floating_times=false) {
 683:     $this->instances = array();
 684:     $this->GetMoreInstances($return_floating_times);
 685:     $this->rewind();
 686:     $this->finished = false;
 687:   }
 688: 
 689: 
 690:   public function rewind() {
 691:     $this->position = -1;
 692:   }
 693: 
 694: 
 695:   /**
 696:    * Return the next date in the repeating series.
 697:    * @param boolean $return_floating_times Whether to return dates as floating times.
 698:    * @return vComponent The next instance.
 699:    */
 700:   public function next($return_floating_times=false) {
 701:     $this->position++;
 702:     return $this->current($return_floating_times);
 703:   }
 704: 
 705: 
 706:   public function current($return_floating_times=false) {
 707:     if ( !$this->valid() ) return null;
 708:     if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances($return_floating_times);
 709:     if ( !$this->valid() ) return null;
 710:     if ( DEBUG_RRULE ) printf( "Returning date from position %d: %s (%s)\n", $this->position,
 711:                           $this->instances[$this->position]->format('c'), $this->instances[$this->position]->FloatOrUTC($return_floating_times) );
 712:     return $this->instances[$this->position];
 713:   }
 714: 
 715: 
 716:   public function key($return_floating_times=false) {
 717:     if ( !$this->valid() ) return null;
 718:     if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances($return_floating_times);
 719:     if ( !isset($this->keys[$this->position]) ) {
 720:       $this->keys[$this->position] = $this->instances[$this->position];
 721:     }
 722:     return $this->keys[$this->position];
 723:   }
 724: 
 725: 
 726:   public function valid() {
 727:     if ( isset($this->instances[$this->position]) || !$this->finished ) return true;
 728:     return false;
 729:   }
 730: 
 731:   /**
 732:    * This function returns an array which lists the order of processing, and whether the processing is
 733:    * to expand or limit based on this component.
 734:    *
 735:    * Note that yearly-byday and monthly-byday have special handling which is coded within the
 736:    * expand_byday() method
 737:    * @param $freq a string indicating the frequency.
 738:    */
 739:   private static function rrule_expand_limit( $freq ) {
 740:     switch( $freq ) {
 741:       case 'YEARLY':
 742:         return array( 'bymonth' => 'expand', 'byweekno' => 'expand', 'byyearday' => 'expand', 'bymonthday' => 'expand',
 743:                         'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
 744:       case 'MONTHLY':
 745:         return array( 'bymonth' => 'limit', 'bymonthday' => 'expand',
 746:                         'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
 747:       case 'WEEKLY':
 748:         return array( 'bymonth' => 'limit',
 749:                         'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
 750:       case 'DAILY':
 751:         return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
 752:                         'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
 753:       case 'HOURLY':
 754:         return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
 755:                         'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'expand', 'bysecond' => 'expand' );
 756:       case 'MINUTELY':
 757:         return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
 758:                         'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'expand' );
 759:       case 'SECONDLY':
 760:         return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
 761:                         'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'limit' );
 762:     }
 763:     dbg_error_log('ERROR','Invalid frequency code "%s" - pretending it is "DAILY"', $freq);
 764:     return array( 'bymonth' => 'limit', 'bymonthday' => 'limit',
 765:                         'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' );
 766:   }
 767: 
 768:   private function GetMoreInstances($return_floating_times=false) {
 769:     if ( $this->finished ) return;
 770:     $got_more = false;
 771:     $loop_limit = 10;
 772:     $loops = 0;
 773:     if ( $return_floating_times ) $this->base->setAsFloat();
 774:     while( !$this->finished && !$got_more && $loops++ < $loop_limit ) {
 775:       if ( !isset($this->current_base) ) {
 776:         $this->current_base = clone($this->base);
 777:       }
 778:       else {
 779:         $this->current_base->modify( $this->frequency_string );
 780:       }
 781:       if ( $return_floating_times ) $this->current_base->setAsFloat();
 782:       if ( DEBUG_RRULE ) printf( "Getting more instances from: '%s' - %d\n", $this->current_base->format('c'), count($this->instances) );
 783:       $this->current_set = array( clone($this->current_base) );
 784:       foreach( self::rrule_expand_limit($this->freq) AS $bytype => $action ) {
 785:         if ( isset($this->{$bytype}) ) {
 786:           $this->{$action.'_'.$bytype}();
 787:           if ( !isset($this->current_set[0]) ) break;
 788:         }
 789:       }
 790: 
 791:       sort($this->current_set);
 792:       if ( isset($this->bysetpos) ) $this->limit_bysetpos();
 793: 
 794:       $position = count($this->instances) - 1;
 795:       if ( DEBUG_RRULE ) printf( "Inserting %d from current_set into position %d\n", count($this->current_set), $position + 1 );
 796:       foreach( $this->current_set AS $k => $instance ) {
 797:         if ( $instance < $this->base ) continue;
 798:         if ( isset($this->until) && $instance > $this->until ) {
 799:           $this->finished = true;
 800:           return;
 801:         }
 802:         if ( !isset($this->instances[$position]) || $instance != $this->instances[$position] ) {
 803:           $got_more = true;
 804:           $position++;
 805:           $this->instances[$position] = $instance;
 806:           if ( DEBUG_RRULE ) printf( "Added date %s into position %d in current set\n", $instance->format('c'), $position );
 807:           if ( isset($this->count) && ($position + 1) >= $this->count ) {
 808:             $this->finished = true;
 809:             return;
 810:           }
 811:         }
 812:       }
 813:     }
 814:   }
 815: 
 816: 
 817:   public static function rrule_day_number( $day ) {
 818:     switch( $day ) {
 819:       case 'SU': return 0;
 820:       case 'MO': return 1;
 821:       case 'TU': return 2;
 822:       case 'WE': return 3;
 823:       case 'TH': return 4;
 824:       case 'FR': return 5;
 825:       case 'SA': return 6;
 826:     }
 827:     return false;
 828:   }
 829: 
 830: 
 831:   static public function date_mask( $date, $y, $mo, $d, $h, $mi, $s ) {
 832:     $date_parts = explode(',',$date->format('Y,m,d,H,i,s'));
 833: 
 834:     if ( isset($y) || isset($mo) || isset($d) ) {
 835:       if ( isset($y) ) $date_parts[0] = $y;
 836:       if ( isset($mo) ) $date_parts[1] = $mo;
 837:       if ( isset($d) ) $date_parts[2] = $d;
 838:       $date->setDate( $date_parts[0], $date_parts[1], $date_parts[2] );
 839:     }
 840:     if ( isset($h) || isset($mi) || isset($s) ) {
 841:       if ( isset($h) ) $date_parts[3] = $h;
 842:       if ( isset($mi) ) $date_parts[4] = $mi;
 843:       if ( isset($s) ) $date_parts[5] = $s;
 844:       $date->setTime( $date_parts[3], $date_parts[4], $date_parts[5] );
 845:     }
 846:     return $date;
 847:   }
 848: 
 849: 
 850:   private function expand_bymonth() {
 851:     $instances = $this->current_set;
 852:     $this->current_set = array();
 853:     foreach( $instances AS $k => $instance ) {
 854:       foreach( $this->bymonth AS $k => $month ) {
 855:         $expanded = $this->date_mask( clone($instance), null, $month, null, null, null, null);
 856:         if ( DEBUG_RRULE ) printf( "Expanded BYMONTH $month into date %s\n", $expanded->format('c') );
 857:         $this->current_set[] = $expanded;
 858:       }
 859:     }
 860:   }
 861: 
 862:   private function expand_bymonthday() {
 863:     $instances = $this->current_set;
 864:     $this->current_set = array();
 865:     foreach( $instances AS $k => $instance ) {
 866:       foreach( $this->bymonthday AS $k => $monthday ) {
 867:         $expanded = $this->date_mask( clone($instance), null, null, $monthday, null, null, null);
 868:         if ( DEBUG_RRULE ) printf( "Expanded BYMONTHDAY $monthday into date %s from %s\n", $expanded->format('c'), $instance->format('c') );
 869:         $this->current_set[] = $expanded;
 870:       }
 871:     }
 872:   }
 873: 
 874:   private function expand_byyearday() {
 875:     $instances = $this->current_set;
 876:     $this->current_set = array();
 877:     $days_set = array();
 878:     foreach( $instances AS $k => $instance ) {
 879:       foreach( $this->byyearday AS $k => $yearday ) {
 880:         $on_yearday = clone($instance);
 881:         $on_yearday->setYearDay($yearday);
 882:         if ( isset($days_set[$on_yearday->UTC()]) ) continue;
 883:         $this->current_set[] = $on_yearday;
 884:         $days_set[$on_yearday->UTC()] = true;
 885:       }
 886:     }
 887:   }
 888: 
 889:   private function expand_byday_in_week( $day_in_week ) {
 890: 
 891:     /**
 892:     * @todo This should really allow for WKST, since if we start a series
 893:     * on (eg.) TH and interval > 1, a MO, TU, FR repeat will not be in the
 894:     * same week with this code.
 895:     */
 896:     $dow_of_instance = $day_in_week->format('w'); // 0 == Sunday
 897:     foreach( $this->byday AS $k => $weekday ) {
 898:       $dow = self::rrule_day_number($weekday);
 899:       $offset = $dow - $dow_of_instance;
 900:       if ( $offset < 0 ) $offset += 7;
 901:       $expanded = clone($day_in_week);
 902:       $expanded->modify( sprintf('+%d day', $offset) );
 903:       $this->current_set[] = $expanded;
 904:       if ( DEBUG_RRULE ) printf( "Expanded BYDAY(W) $weekday into date %s\n", $expanded->format('c') );
 905:     }
 906:   }
 907: 
 908: 
 909:   private function expand_byday_in_month( $day_in_month ) {
 910: 
 911:     $first_of_month = $this->date_mask( clone($day_in_month), null, null, 1, null, null, null);
 912:     $dow_of_first = $first_of_month->format('w'); // 0 == Sunday
 913:     $days_in_month = cal_days_in_month(CAL_GREGORIAN, $first_of_month->format('m'), $first_of_month->format('Y'));
 914:     foreach( $this->byday AS $k => $weekday ) {
 915:       if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
 916:         $dow = self::rrule_day_number($matches[3]);
 917:         $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.
 918:         $whichweek = intval($matches[2]);
 919:         if ( DEBUG_RRULE ) printf( "Expanding BYDAY(M) $weekday in month of %s\n", $first_of_month->format('c') );
 920:         if ( $whichweek > 0 ) {
 921:           $whichweek--;
 922:           $monthday = $first_dom;
 923:           if ( $matches[1] == '-' ) {
 924:             $monthday += 35;
 925:             while( $monthday > $days_in_month ) $monthday -= 7;
 926:             $monthday -= (7 * $whichweek);
 927:           }
 928:           else {
 929:             $monthday += (7 * $whichweek);
 930:           }
 931:           if ( $monthday > 0 && $monthday <= $days_in_month ) {
 932:             $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
 933:             if ( DEBUG_RRULE ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') );
 934:             $this->current_set[] = $expanded;
 935:           }
 936:         }
 937:         else {
 938:           for( $monthday = $first_dom; $monthday <= $days_in_month; $monthday += 7 ) {
 939:             $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null);
 940:             if ( DEBUG_RRULE ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') );
 941:             $this->current_set[] = $expanded;
 942:           }
 943:         }
 944:       }
 945:     }
 946:   }
 947: 
 948: 
 949:   private function expand_byday_in_year( $day_in_year ) {
 950: 
 951:     $first_of_year = $this->date_mask( clone($day_in_year), null, 1, 1, null, null, null);
 952:     $dow_of_first = $first_of_year->format('w'); // 0 == Sunday
 953:     $days_in_year = 337 + cal_days_in_month(CAL_GREGORIAN, 2, $first_of_year->format('Y'));
 954:     foreach( $this->byday AS $k => $weekday ) {
 955:       if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) {
 956:         $expanded = clone($first_of_year);
 957:         $dow = self::rrule_day_number($matches[3]);
 958:         $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.
 959:         $whichweek = intval($matches[2]);
 960:         if ( DEBUG_RRULE ) printf( "Expanding BYDAY(Y) $weekday from date %s\n", $instance->format('c') );
 961:         if ( $whichweek > 0 ) {
 962:           $whichweek--;
 963:           $yearday = $first_doy;
 964:           if ( $matches[1] == '-' ) {
 965:             $yearday += 371;
 966:             while( $yearday > $days_in_year ) $yearday -= 7;
 967:             $yearday -= (7 * $whichweek);
 968:           }
 969:           else {
 970:             $yearday += (7 * $whichweek);
 971:           }
 972:           if ( $yearday > 0 && $yearday <= $days_in_year ) {
 973:             $expanded->modify(sprintf('+%d day', $yearday - 1));
 974:             if ( DEBUG_RRULE ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') );
 975:             $this->current_set[] = $expanded;
 976:           }
 977:         }
 978:         else {
 979:           $expanded->modify(sprintf('+%d day', $first_doy - 1));
 980:           for( $yearday = $first_doy; $yearday <= $days_in_year; $yearday += 7 ) {
 981:             if ( DEBUG_RRULE ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') );
 982:             $this->current_set[] = clone($expanded);
 983:             $expanded->modify('+1 week');
 984:           }
 985:         }
 986:       }
 987:     }
 988:   }
 989: 
 990: 
 991:   private function expand_byday() {
 992:     if ( !isset($this->current_set[0]) ) return;
 993:     if ( $this->freq == 'MONTHLY' || $this->freq == 'YEARLY' ) {
 994:       if ( isset($this->bymonthday) || isset($this->byyearday) ) {
 995:         $this->limit_byday();  /** Per RFC5545 3.3.10 from note 1&2 to table */
 996:         return;
 997:       }
 998:     }
 999:     $instances = $this->current_set;
1000:     $this->current_set = array();
1001:     foreach( $instances AS $k => $instance ) {
1002:       if ( $this->freq == 'MONTHLY' ) {
1003:         $this->expand_byday_in_month($instance);
1004:       }
1005:       else if ( $this->freq == 'WEEKLY' ) {
1006:         $this->expand_byday_in_week($instance);
1007:       }
1008:       else { // YEARLY
1009:         if ( isset($this->bymonth) ) {
1010:           $this->expand_byday_in_month($instance);
1011:         }
1012:         else if ( isset($this->byweekno) ) {
1013:           $this->expand_byday_in_week($instance);
1014:         }
1015:         else {
1016:           $this->expand_byday_in_year($instance);
1017:         }
1018:       }
1019: 
1020:     }
1021:   }
1022: 
1023:   private function expand_byhour() {
1024:     $instances = $this->current_set;
1025:     $this->current_set = array();
1026:     foreach( $instances AS $k => $instance ) {
1027:       foreach( $this->bymonth AS $k => $month ) {
1028:         $this->current_set[] = $this->date_mask( clone($instance), null, null, null, $hour, null, null);
1029:       }
1030:     }
1031:   }
1032: 
1033:   private function expand_byminute() {
1034:     $instances = $this->current_set;
1035:     $this->current_set = array();
1036:     foreach( $instances AS $k => $instance ) {
1037:       foreach( $this->bymonth AS $k => $month ) {
1038:         $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, $minute, null);
1039:       }
1040:     }
1041:   }
1042: 
1043:   private function expand_bysecond() {
1044:     $instances = $this->current_set;
1045:     $this->current_set = array();
1046:     foreach( $instances AS $k => $instance ) {
1047:       foreach( $this->bymonth AS $k => $second ) {
1048:         $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, null, $second);
1049:       }
1050:     }
1051:   }
1052: 
1053: 
1054:   private function limit_generally( $fmt_char, $element_name ) {
1055:     $instances = $this->current_set;
1056:     $this->current_set = array();
1057:     foreach( $instances AS $k => $instance ) {
1058:       foreach( $this->{$element_name} AS $k => $element_value ) {
1059:         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') );
1060:         if ( $instance->format($fmt_char) == $element_value ) $this->current_set[] = $instance;
1061:       }
1062:     }
1063:   }
1064: 
1065:   private function limit_byday() {
1066:     $fmt_char = 'w';
1067:     $instances = $this->current_set;
1068:     $this->current_set = array();
1069:     foreach( $this->byday AS $k => $weekday ) {
1070:       $dow = self::rrule_day_number($weekday);
1071:       foreach( $instances AS $k => $instance ) {
1072:         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') );
1073:         if ( $instance->format($fmt_char) == $dow ) $this->current_set[] = $instance;
1074:       }
1075:     }
1076:   }
1077: 
1078:   private function limit_bymonth()    {   $this->limit_generally( 'm', 'bymonth' );     }
1079:   private function limit_byyearday()  {   $this->limit_generally( 'z', 'byyearday' );   }
1080:   private function limit_bymonthday() {   $this->limit_generally( 'd', 'bymonthday' );  }
1081:   private function limit_byhour()     {   $this->limit_generally( 'H', 'byhour' );      }
1082:   private function limit_byminute()   {   $this->limit_generally( 'i', 'byminute' );    }
1083:   private function limit_bysecond()   {   $this->limit_generally( 's', 'bysecond' );    }
1084: 
1085: 
1086:   private function limit_bysetpos( ) {
1087:     $instances = $this->current_set;
1088:     $count = count($instances);
1089:     $this->current_set = array();
1090:     foreach( $this->bysetpos AS $k => $element_value ) {
1091:       if ( DEBUG_RRULE ) printf( "Limiting bysetpos %s of %d instances\n", $element_value, $count );
1092:       if ( $element_value > 0 ) {
1093:         $this->current_set[] = $instances[$element_value - 1];
1094:       }
1095:       else if ( $element_value < 0 ) {
1096:         $this->current_set[] = $instances[$count + $element_value];
1097:       }
1098:     }
1099:   }
1100: 
1101: 
1102: }
1103: 
1104: 
1105: 
1106: require_once("vComponent.php");
1107: 
1108: /**
1109: * Expand the event instances for an RDATE or EXDATE property
1110: *
1111: * @param string $property RDATE or EXDATE, depending...
1112: * @param array $component A vComponent which is a VEVENT, VTODO or VJOURNAL
1113: * @param array $range_end A date after which we care less about expansion
1114: *
1115: * @return array An array keyed on the UTC dates, referring to the component
1116: */
1117: function rdate_expand( $dtstart, $property, $component, $range_end = null, $is_date=null, $return_floating_times=false ) {
1118:   $properties = $component->GetProperties($property);
1119:   $expansion = array();
1120:   foreach( $properties AS $p ) {
1121:     $timezone = $p->GetParameterValue('TZID');
1122:     $rdate = $p->Value();
1123:     $rdates = explode( ',', $rdate );
1124:     foreach( $rdates AS $k => $v ) {
1125:       $rdate = new RepeatRuleDateTime( $v, $timezone, $is_date);
1126:       if ( $return_floating_times ) $rdate->setAsFloat();
1127:       $expansion[$rdate->FloatOrUTC($return_floating_times)] = $component;
1128:       if ( $rdate > $range_end ) break;
1129:     }
1130:   }
1131:   return $expansion;
1132: }
1133: 
1134: 
1135: /**
1136: * Expand the event instances for an RRULE property
1137: *
1138: * @param object $dtstart A RepeatRuleDateTime which is the master dtstart
1139: * @param string $property RDATE or EXDATE, depending...
1140: * @param array $component A vComponent which is a VEVENT, VTODO or VJOURNAL
1141: * @param array $range_end A date after which we care less about expansion
1142: *
1143: * @return array An array keyed on the UTC dates, referring to the component
1144: */
1145: function rrule_expand( $dtstart, $property, $component, $range_end, $is_date=null, $return_floating_times=false ) {
1146:   $expansion = array();
1147: 
1148:   $recur = $component->GetProperty($property);
1149:   if ( !isset($recur) ) return $expansion;
1150:   $recur = $recur->Value();
1151: 
1152:   $this_start = $component->GetProperty('DTSTART');
1153:   if ( isset($this_start) ) {
1154:     $this_start = new RepeatRuleDateTime($this_start);
1155:   }
1156:   else {
1157:     $this_start = clone($dtstart);
1158:   }
1159:   if ( $return_floating_times ) $this_start->setAsFloat();
1160: 
1161: //  if ( DEBUG_RRULE ) print_r( $this_start );
1162:   if ( DEBUG_RRULE ) printf( "RRULE: %s (floating: %s)\n", $recur, ($return_floating_times?"yes":"no") );
1163:   $rule = new RepeatRule( $this_start, $recur, $is_date, $return_floating_times );
1164:   $i = 0;
1165:   $result_limit = 1000;
1166:   while( $date = $rule->next($return_floating_times) ) {
1167: //    if ( DEBUG_RRULE ) printf( "[%3d] %s\n", $i, $date->UTC() );
1168:     $expansion[$date->FloatOrUTC($return_floating_times)] = $component;
1169:     if ( $i++ >= $result_limit || $date > $range_end ) break;
1170:   }
1171: //  if ( DEBUG_RRULE ) print_r( $expansion );
1172:   return $expansion;
1173: }
1174: 
1175: 
1176: /**
1177: * Expand the event instances for an iCalendar VEVENT (or VTODO)
1178: *
1179: * Note: expansion here does not apply modifications to instances other than modifying start/end/due/duration.
1180: *
1181: * @param object $vResource A vComponent which is a VCALENDAR containing components needing expansion
1182: * @param object $range_start A RepeatRuleDateTime which is the beginning of the range for events, default -6 weeks
1183: * @param object $range_end A RepeatRuleDateTime which is the end of the range for events, default +6 weeks
1184: *
1185: * @return vComponent The original vComponent, with the instances of the internal components expanded.
1186: */
1187: function expand_event_instances( vComponent $vResource, $range_start = null, $range_end = null, $return_floating_times=false ) {
1188:     global $c;
1189:     $components = $vResource->GetComponents();
1190: 
1191:     $clear_instance_props = array(
1192:             'DTSTART' => true,
1193:             'DUE' => true,
1194:             'DTEND' => true
1195:     );
1196:     if ( empty( $c->expanded_instances_include_rrule ) ) {
1197:         $clear_instance_props += array(
1198:                 'RRULE' => true,
1199:                 'RDATE' => true,
1200:                 'EXDATE' => true
1201:         );
1202:     }
1203: 
1204:   if ( empty($range_start) ) { $range_start = new RepeatRuleDateTime(); $range_start->modify('-6 weeks'); }
1205:   if ( empty($range_end) )   {
1206:       $range_end   = clone($range_start);
1207:       $range_end->modify('+6 months');
1208:   }
1209: 
1210:   $instances = array();
1211:   $expand = false;
1212:   $dtstart = null;
1213:   $is_date = false;
1214:   $has_repeats = false;
1215:   $dtstart_type = 'DTSTART';
1216:   foreach( $components AS $k => $comp ) {
1217:     if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) {
1218:       continue;
1219:     }
1220:     if ( !isset($dtstart) ) {
1221:       $dtstart_prop = $comp->GetProperty($dtstart_type);
1222:       if ( !isset($dtstart_prop) && $comp->GetType() != 'VTODO' ) {
1223:         $dtstart_type = 'DUE';
1224:         $dtstart_prop = $comp->GetProperty($dtstart_type);
1225:       }
1226:       if ( !isset($dtstart_prop) ) continue;
1227:       $dtstart = new RepeatRuleDateTime( $dtstart_prop );
1228:       if ( $return_floating_times ) $dtstart->setAsFloat();
1229:       if ( DEBUG_RRULE ) printf( "Component is: %s (floating: %s)\n", $comp->GetType(), ($return_floating_times?"yes":"no") );
1230:       $is_date = $dtstart->isDate();
1231:       $instances[$dtstart->FloatOrUTC($return_floating_times)] = $comp;
1232:       $rrule = $comp->GetProperty('RRULE');
1233:       $has_repeats = isset($rrule);
1234:     }
1235:     $p = $comp->GetProperty('RECURRENCE-ID');
1236:     if ( isset($p) && $p->Value() != '' ) {
1237:       $range = $p->GetParameterValue('RANGE');
1238:       $recur_utc = new RepeatRuleDateTime($p);
1239:       if ( $is_date ) $recur_utc->setAsDate();
1240:       $recur_utc = $recur_utc->FloatOrUTC($return_floating_times);
1241:       if ( isset($range) && $range == 'THISANDFUTURE' ) {
1242:         foreach( $instances AS $k => $v ) {
1243:           if ( DEBUG_RRULE ) printf( "Removing overridden instance at: $k\n" );
1244:           if ( $k >= $recur_utc ) unset($instances[$k]);
1245:         }
1246:       }
1247:       else {
1248:         unset($instances[$recur_utc]);
1249:       }
1250:     }
1251:     else if ( DEBUG_RRULE ) {
1252:       $p =  $comp->GetProperty('SUMMARY');
1253:       $summary = ( isset($p) ? $p->Value() : 'not set');
1254:       $p =  $comp->GetProperty('UID');
1255:       $uid = ( isset($p) ? $p->Value() : 'not set');
1256:       printf( "Processing event '%s' with UID '%s' starting on %s\n",
1257:                  $summary, $uid, $dtstart->FloatOrUTC($return_floating_times) );
1258:       print( "Instances at start");
1259:       foreach( $instances AS $k => $v ) {
1260:         print ' : '.$k;
1261:       }
1262:       print "\n";
1263:     }
1264:     $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end, null, $return_floating_times);
1265:     if ( DEBUG_RRULE ) {
1266:       print( "After rrule_expand");
1267:       foreach( $instances AS $k => $v ) {
1268:         print ' : '.$k;
1269:       }
1270:       print "\n";
1271:     }
1272:     $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end, null, $return_floating_times);
1273:     if ( DEBUG_RRULE ) {
1274:       print( "After rdate_expand");
1275:       foreach( $instances AS $k => $v ) {
1276:         print ' : '.$k;
1277:       }
1278:       print "\n";
1279:     }
1280:     foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end, null, $return_floating_times) AS $k => $v ) {
1281:       unset($instances[$k]);
1282:     }
1283:     if ( DEBUG_RRULE ) {
1284:       print( "After exdate_expand");
1285:       foreach( $instances AS $k => $v ) {
1286:         print ' : '.$k;
1287:       }
1288:       print "\n";
1289:     }
1290:   }
1291: 
1292:   $last_duration = null;
1293:   $early_start = null;
1294:   $new_components = array();
1295:   $start_utc = $range_start->FloatOrUTC($return_floating_times);
1296:   $end_utc = $range_end->FloatOrUTC($return_floating_times);
1297:   foreach( $instances AS $utc => $comp ) {
1298:     if ( $utc > $end_utc ) {
1299:       if ( DEBUG_RRULE ) printf( "We're done: $utc is out of the range.\n");
1300:       break;
1301:     }
1302: 
1303:     $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
1304:     $duration = $comp->GetProperty('DURATION');
1305:     if ( !isset($duration) || $duration->Value() == '' ) {
1306:       $instance_start = $comp->GetProperty($dtstart_type);
1307:       $dtsrt = new RepeatRuleDateTime( $instance_start );
1308:       if ( $return_floating_times ) $dtsrt->setAsFloat();
1309:       $instance_end = $comp->GetProperty($end_type);
1310:       if ( isset($instance_end) ) {
1311:         $dtend = new RepeatRuleDateTime( $instance_end );
1312:         $duration = Rfc5545Duration::fromTwoDates($dtsrt, $dtend);
1313:       }
1314:       else {
1315:         if ( $instance_start->GetParameterValue('VALUE') == 'DATE' ) {
1316:           $duration = new Rfc5545Duration('P1D');
1317:         }
1318:         else {
1319:           $duration = new Rfc5545Duration(0);
1320:         }
1321:       }
1322:     }
1323:     else {
1324:       $duration = new Rfc5545Duration($duration->Value());
1325:     }
1326: 
1327:     if ( $utc < $start_utc ) {
1328:       if ( isset($early_start) && isset($last_duration) && $duration->equals($last_duration) ) {
1329:         if ( $utc < $early_start ) {
1330:           if ( DEBUG_RRULE ) printf( "Next please: $utc is before $early_start and before $start_utc.\n");
1331:           continue;
1332:         }
1333:       }
1334:       else {
1335:         /** Calculate the latest possible start date when this event would overlap our range start */
1336:         $latest_start = clone($range_start);
1337:         $latest_start->modify('-'.$duration);
1338:         $early_start = $latest_start->FloatOrUTC($return_floating_times);
1339:         $last_duration = $duration;
1340:         if ( $utc < $early_start ) {
1341:           if ( DEBUG_RRULE ) printf( "Another please: $utc is before $early_start and before $start_utc.\n");
1342:           continue;
1343:         }
1344:       }
1345:     }
1346:     $component = clone($comp);
1347:     $component->ClearProperties( $clear_instance_props );
1348:     $component->AddProperty($dtstart_type, $utc, ($is_date ? array('VALUE' => 'DATE') : null) );
1349:     $component->AddProperty('DURATION', $duration );
1350:     if ( $has_repeats && $dtstart->FloatOrUTC($return_floating_times) != $utc )
1351:       $component->AddProperty('RECURRENCE-ID', $utc, ($is_date ? array('VALUE' => 'DATE') : null) );
1352:     $new_components[$utc] = $component;
1353:   }
1354: 
1355:   // Add overriden instances
1356:   foreach( $components AS $k => $comp ) {
1357:     $p = $comp->GetProperty('RECURRENCE-ID');
1358:     if ( isset($p) && $p->Value() != '') {
1359:       $recurrence_id = $p->Value();
1360:       if ( !isset($new_components[$recurrence_id]) ) {
1361:         // The component we're replacing is outside the range.  Unless the replacement
1362:         // is *in* the range we will move along to the next one.
1363:         $dtstart_prop = $comp->GetProperty($dtstart_type);
1364:         if ( !isset($dtstart_prop) ) continue;  // No start: no expansion.  Note that we consider 'DUE' to be a start if DTSTART is missing
1365:         $dtstart = new RepeatRuleDateTime( $dtstart_prop );
1366:         $is_date = $dtstart->isDate();
1367:         if ( $return_floating_times ) $dtstart->setAsFloat();
1368:         $dtstart = $dtstart->FloatOrUTC($return_floating_times);
1369:         if ( $dtstart > $end_utc ) continue; // Start after end of range, skip it
1370: 
1371:         $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND');
1372:         $duration = $comp->GetProperty('DURATION');
1373:         if ( !isset($duration) || $duration->Value() == '' ) {
1374:           $instance_end = $comp->GetProperty($end_type);
1375:           if ( isset($instance_end) ) {
1376:             $dtend = new RepeatRuleDateTime( $instance_end );
1377:             if ( $return_floating_times ) $dtend->setAsFloat();
1378:             $dtend = $dtend->FloatOrUTC($return_floating_times);
1379:           }
1380:           else {
1381:             $dtend = $dtstart  + ($is_date ? $dtstart + 86400 : 0 );
1382:           }
1383:         }
1384:         else {
1385:           $duration = new Rfc5545Duration($duration->Value());
1386:           $dtend = $dtstart + $duration->asSeconds();
1387:         }
1388:         if ( $dtend < $start_utc ) continue; // End before start of range: skip that too.
1389:       }
1390:       if ( DEBUG_RRULE ) printf( "Replacing overridden instance at %s\n", $recurrence_id);
1391:       $new_components[$recurrence_id] = $comp;
1392:     }
1393:   }
1394: 
1395:   $vResource->SetComponents($new_components);
1396: 
1397:   return $vResource;
1398: }
1399: 
1400: 
1401: /**
1402:  * Return a date range for this component.
1403:  * @param vComponent $comp
1404:  * @throws Exception (1) When DTSTART is not present but the RFC says MUST and (2) when we get an unsupported component
1405:  * @return RepeatRuleDateRange
1406:  */
1407: function getComponentRange(vComponent $comp) {
1408:   $dtstart_prop = $comp->GetProperty('DTSTART');
1409:   $duration_prop = $comp->GetProperty('DURATION');
1410:   if ( isset($duration_prop) ) {
1411:     if ( !isset($dtstart_prop) ) throw new Exception('Invalid '.$comp->GetType().' containing DURATION without DTSTART', 0);
1412:     $dtstart = new RepeatRuleDateTime($dtstart_prop);
1413:     $dtend = clone($dtstart);
1414:     $dtend->modify(new Rfc5545Duration($duration_prop->Value()));
1415:   }
1416:   else {
1417:     $completed_prop = null;
1418:     switch ( $comp->GetType() ) {
1419:       case 'VEVENT':
1420:         if ( !isset($dtstart_prop) ) throw new Exception('Invalid VEVENT without DTSTART', 0);
1421:         $dtend_prop = $comp->GetProperty('DTEND');
1422:         break;
1423:       case 'VTODO':
1424:         $completed_prop = $comp->GetProperty('COMPLETED');
1425:         $dtend_prop = $comp->GetProperty('DUE');
1426:         break;
1427:       case 'VJOURNAL':
1428:         if ( !isset($dtstart_prop) )
1429:           $dtstart_prop = $comp->GetProperty('DTSTAMP');
1430:         $dtend_prop = $dtstart_prop;
1431:       default:
1432:         throw new Exception('getComponentRange cannot handle "'.$comp->GetType().'" components', 0);
1433:     }
1434: 
1435:     if ( isset($dtstart_prop) )
1436:       $dtstart = new RepeatRuleDateTime($dtstart_prop);
1437:     else
1438:       $dtstart = null;
1439: 
1440:     if ( isset($dtend_prop) )
1441:       $dtend = new RepeatRuleDateTime($dtend_prop);
1442:     else
1443:       $dtend = null;
1444: 
1445:     if ( isset($completed_prop) ) {
1446:       $completed = new RepeatRuleDateTime($completed_prop);
1447:       if ( !isset($dtstart) || (isset($dtstart) && $completed < $dtstart) ) $dtstart = $completed;
1448:       if ( !isset($dtend) || (isset($dtend) && $completed > $dtend) ) $dtend = $completed;
1449:     }
1450:   }
1451:   return new RepeatRuleDateRange($dtstart, $dtend);
1452: }
1453: 
1454: /**
1455: * Return a RepeatRuleDateRange from the earliest start to the latest end of the event.
1456: *
1457: * @todo: This should probably be made part of the VCalendar object when we move the RRule.php into AWL.
1458: *
1459: * @param object $vResource A vComponent which is a VCALENDAR containing components needing expansion
1460: * @return RepeatRuleDateRange Representing the range of time covered by the event.
1461: */
1462: function getVCalendarRange( $vResource ) {
1463:   $components = $vResource->GetComponents();
1464: 
1465:   $dtstart = null;
1466:   $duration = null;
1467:   $earliest_start = null;
1468:   $latest_end = null;
1469:   $has_repeats = false;
1470:   foreach( $components AS $k => $comp ) {
1471:     if ( $comp->GetType() == 'VTIMEZONE' ) continue;
1472:     $range = getComponentRange($comp);
1473:     $dtstart = $range->from;
1474:     if ( !isset($dtstart) ) continue;
1475:     $duration = $range->getDuration();
1476: 
1477:     $rrule = $comp->GetProperty('RRULE');
1478:     $limited_occurrences = true;
1479:     if ( isset($rrule) ) {
1480:       $rule = new RepeatRule($dtstart, $rrule);
1481:       $limited_occurrences  = $rule->hasLimitedOccurrences();
1482:     }
1483: 
1484:     if ( $limited_occurrences ) {
1485:       $instances = array();
1486:       $instances[$dtstart->FloatOrUTC()] = $dtstart;
1487:       if ( !isset($range_end) ) {
1488:         $range_end   = new RepeatRuleDateTime();
1489:         $range_end->modify('+150 years');
1490:       }
1491:       $instances += rrule_expand($dtstart, 'RRULE', $comp, $range_end);
1492:       $instances += rdate_expand($dtstart, 'RDATE', $comp, $range_end);
1493:       foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end) AS $k => $v ) {
1494:         unset($instances[$k]);
1495:       }
1496:       if ( count($instances) < 1 ) {
1497:         if ( empty($earliest_start) || $dtstart < $earliest_start ) $earliest_start = $dtstart;
1498:         $latest_end = null;
1499:         break;
1500:       }
1501:       $instances = array_keys($instances);
1502:       asort($instances);
1503:       $first = new RepeatRuleDateTime($instances[0]);
1504:       $last = new RepeatRuleDateTime($instances[count($instances)-1]);
1505:       $last->modify($duration);
1506:       if ( empty($earliest_start) || $first < $earliest_start )  $earliest_start = $first;
1507:       if ( empty($latest_end) || $last > $latest_end )           $latest_end = $last;
1508:     }
1509:     else {
1510:       if ( empty($earliest_start) || $dtstart < $earliest_start ) $earliest_start = $dtstart;
1511:       $latest_end = null;
1512:       break;
1513:     }
1514:   }
1515: 
1516:   return new RepeatRuleDateRange($earliest_start, $latest_end );
1517: }
1518: 
DAViCal API documentation generated by ApiGen 2.8.0