Mercurial > hg > rc1
comparison vendor/sabre/vobject/lib/Recur/EventIterator.php @ 7:430dbd5346f7
vendor sabre as distributed
| author | Charlie Root |
|---|---|
| date | Sat, 13 Jan 2018 09:06:10 -0500 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 6:cec75ba50afc | 7:430dbd5346f7 |
|---|---|
| 1 <?php | |
| 2 | |
| 3 namespace Sabre\VObject\Recur; | |
| 4 | |
| 5 use InvalidArgumentException; | |
| 6 use DateTime; | |
| 7 use DateTimeZone; | |
| 8 use Sabre\VObject\Component; | |
| 9 use Sabre\VObject\Component\VEvent; | |
| 10 | |
| 11 /** | |
| 12 * This class is used to determine new for a recurring event, when the next | |
| 13 * events occur. | |
| 14 * | |
| 15 * This iterator may loop infinitely in the future, therefore it is important | |
| 16 * that if you use this class, you set hard limits for the amount of iterations | |
| 17 * you want to handle. | |
| 18 * | |
| 19 * Note that currently there is not full support for the entire iCalendar | |
| 20 * specification, as it's very complex and contains a lot of permutations | |
| 21 * that's not yet used very often in software. | |
| 22 * | |
| 23 * For the focus has been on features as they actually appear in Calendaring | |
| 24 * software, but this may well get expanded as needed / on demand | |
| 25 * | |
| 26 * The following RRULE properties are supported | |
| 27 * * UNTIL | |
| 28 * * INTERVAL | |
| 29 * * COUNT | |
| 30 * * FREQ=DAILY | |
| 31 * * BYDAY | |
| 32 * * BYHOUR | |
| 33 * * BYMONTH | |
| 34 * * FREQ=WEEKLY | |
| 35 * * BYDAY | |
| 36 * * BYHOUR | |
| 37 * * WKST | |
| 38 * * FREQ=MONTHLY | |
| 39 * * BYMONTHDAY | |
| 40 * * BYDAY | |
| 41 * * BYSETPOS | |
| 42 * * FREQ=YEARLY | |
| 43 * * BYMONTH | |
| 44 * * BYMONTHDAY (only if BYMONTH is also set) | |
| 45 * * BYDAY (only if BYMONTH is also set) | |
| 46 * | |
| 47 * Anything beyond this is 'undefined', which means that it may get ignored, or | |
| 48 * you may get unexpected results. The effect is that in some applications the | |
| 49 * specified recurrence may look incorrect, or is missing. | |
| 50 * | |
| 51 * The recurrence iterator also does not yet support THISANDFUTURE. | |
| 52 * | |
| 53 * @copyright Copyright (C) 2011-2015 fruux GmbH (https://fruux.com/). | |
| 54 * @author Evert Pot (http://evertpot.com/) | |
| 55 * @license http://sabre.io/license/ Modified BSD License | |
| 56 */ | |
| 57 class EventIterator implements \Iterator { | |
| 58 | |
| 59 /** | |
| 60 * Reference timeZone for floating dates and times. | |
| 61 * | |
| 62 * @var DateTimeZone | |
| 63 */ | |
| 64 protected $timeZone; | |
| 65 | |
| 66 /** | |
| 67 * True if we're iterating an all-day event. | |
| 68 * | |
| 69 * @var bool | |
| 70 */ | |
| 71 protected $allDay = false; | |
| 72 | |
| 73 /** | |
| 74 * Creates the iterator | |
| 75 * | |
| 76 * You should pass a VCALENDAR component, as well as the UID of the event | |
| 77 * we're going to traverse. | |
| 78 * | |
| 79 * @param Component $vcal | |
| 80 * @param string|null $uid | |
| 81 * @param DateTimeZone $timeZone Reference timezone for floating dates and | |
| 82 * times. | |
| 83 */ | |
| 84 public function __construct(Component $vcal, $uid = null, DateTimeZone $timeZone = null) { | |
| 85 | |
| 86 if (is_null($this->timeZone)) { | |
| 87 $timeZone = new DateTimeZone('UTC'); | |
| 88 } | |
| 89 $this->timeZone = $timeZone; | |
| 90 | |
| 91 if ($vcal instanceof VEvent) { | |
| 92 // Single instance mode. | |
| 93 $events = array($vcal); | |
| 94 } else { | |
| 95 $uid = (string)$uid; | |
| 96 if (!$uid) { | |
| 97 throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor'); | |
| 98 } | |
| 99 if (!isset($vcal->VEVENT)) { | |
| 100 throw new InvalidArgumentException('No events found in this calendar'); | |
| 101 } | |
| 102 $events = array(); | |
| 103 foreach($vcal->VEVENT as $event) { | |
| 104 if ($event->uid->getValue() === $uid) { | |
| 105 $events[] = $event; | |
| 106 } | |
| 107 } | |
| 108 | |
| 109 } | |
| 110 | |
| 111 foreach($events as $vevent) { | |
| 112 | |
| 113 if (!isset($vevent->{'RECURRENCE-ID'})) { | |
| 114 | |
| 115 $this->masterEvent = $vevent; | |
| 116 | |
| 117 } else { | |
| 118 | |
| 119 $this->exceptions[ | |
| 120 $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp() | |
| 121 ] = true; | |
| 122 $this->overriddenEvents[] = $vevent; | |
| 123 | |
| 124 } | |
| 125 | |
| 126 } | |
| 127 | |
| 128 if (!$this->masterEvent) { | |
| 129 // No base event was found. CalDAV does allow cases where only | |
| 130 // overridden instances are stored. | |
| 131 // | |
| 132 // In this particular case, we're just going to grab the first | |
| 133 // event and use that instead. This may not always give the | |
| 134 // desired result. | |
| 135 if (!count($this->overriddenEvents)) { | |
| 136 throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid); | |
| 137 } | |
| 138 $this->masterEvent = array_shift($this->overriddenEvents); | |
| 139 } | |
| 140 | |
| 141 $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone); | |
| 142 $this->allDay = !$this->masterEvent->DTSTART->hasTime(); | |
| 143 | |
| 144 if (isset($this->masterEvent->EXDATE)) { | |
| 145 | |
| 146 foreach($this->masterEvent->EXDATE as $exDate) { | |
| 147 | |
| 148 foreach($exDate->getDateTimes($this->timeZone) as $dt) { | |
| 149 $this->exceptions[$dt->getTimeStamp()] = true; | |
| 150 } | |
| 151 | |
| 152 } | |
| 153 | |
| 154 } | |
| 155 | |
| 156 if (isset($this->masterEvent->DTEND)) { | |
| 157 $this->eventDuration = | |
| 158 $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() - | |
| 159 $this->startDate->getTimeStamp(); | |
| 160 } elseif (isset($this->masterEvent->DURATION)) { | |
| 161 $duration = $this->masterEvent->DURATION->getDateInterval(); | |
| 162 $end = clone $this->startDate; | |
| 163 $end->add($duration); | |
| 164 $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp(); | |
| 165 } elseif ($this->allDay) { | |
| 166 $this->eventDuration = 3600 * 24; | |
| 167 } else { | |
| 168 $this->eventDuration = 0; | |
| 169 } | |
| 170 | |
| 171 if (isset($this->masterEvent->RDATE)) { | |
| 172 $this->recurIterator = new RDateIterator( | |
| 173 $this->masterEvent->RDATE->getParts(), | |
| 174 $this->startDate | |
| 175 ); | |
| 176 } elseif (isset($this->masterEvent->RRULE)) { | |
| 177 $this->recurIterator = new RRuleIterator( | |
| 178 $this->masterEvent->RRULE->getParts(), | |
| 179 $this->startDate | |
| 180 ); | |
| 181 } else { | |
| 182 $this->recurIterator = new RRuleIterator( | |
| 183 array( | |
| 184 'FREQ' => 'DAILY', | |
| 185 'COUNT' => 1, | |
| 186 ), | |
| 187 $this->startDate | |
| 188 ); | |
| 189 } | |
| 190 | |
| 191 $this->rewind(); | |
| 192 if (!$this->valid()) { | |
| 193 throw new NoInstancesException('This recurrence rule does not generate any valid instances'); | |
| 194 } | |
| 195 | |
| 196 } | |
| 197 | |
| 198 /** | |
| 199 * Returns the date for the current position of the iterator. | |
| 200 * | |
| 201 * @return DateTime | |
| 202 */ | |
| 203 public function current() { | |
| 204 | |
| 205 if ($this->currentDate) { | |
| 206 return clone $this->currentDate; | |
| 207 } | |
| 208 | |
| 209 } | |
| 210 | |
| 211 /** | |
| 212 * This method returns the start date for the current iteration of the | |
| 213 * event. | |
| 214 * | |
| 215 * @return DateTime | |
| 216 */ | |
| 217 public function getDtStart() { | |
| 218 | |
| 219 if ($this->currentDate) { | |
| 220 return clone $this->currentDate; | |
| 221 } | |
| 222 | |
| 223 } | |
| 224 | |
| 225 /** | |
| 226 * This method returns the end date for the current iteration of the | |
| 227 * event. | |
| 228 * | |
| 229 * @return DateTime | |
| 230 */ | |
| 231 public function getDtEnd() { | |
| 232 | |
| 233 if (!$this->valid()) { | |
| 234 return null; | |
| 235 } | |
| 236 $end = clone $this->currentDate; | |
| 237 $end->modify('+' . $this->eventDuration . ' seconds'); | |
| 238 return $end; | |
| 239 | |
| 240 } | |
| 241 | |
| 242 /** | |
| 243 * Returns a VEVENT for the current iterations of the event. | |
| 244 * | |
| 245 * This VEVENT will have a recurrence id, and it's DTSTART and DTEND | |
| 246 * altered. | |
| 247 * | |
| 248 * @return VEvent | |
| 249 */ | |
| 250 public function getEventObject() { | |
| 251 | |
| 252 if ($this->currentOverriddenEvent) { | |
| 253 return $this->currentOverriddenEvent; | |
| 254 } | |
| 255 | |
| 256 $event = clone $this->masterEvent; | |
| 257 | |
| 258 // Ignoring the following block, because PHPUnit's code coverage | |
| 259 // ignores most of these lines, and this messes with our stats. | |
| 260 // | |
| 261 // @codeCoverageIgnoreStart | |
| 262 unset( | |
| 263 $event->RRULE, | |
| 264 $event->EXDATE, | |
| 265 $event->RDATE, | |
| 266 $event->EXRULE, | |
| 267 $event->{'RECURRENCE-ID'} | |
| 268 ); | |
| 269 // @codeCoverageIgnoreEnd | |
| 270 | |
| 271 $event->DTSTART->setDateTime($this->getDtStart()); | |
| 272 if (isset($event->DTEND)) { | |
| 273 $event->DTEND->setDateTime($this->getDtEnd()); | |
| 274 } | |
| 275 // Including a RECURRENCE-ID to the object, unless this is the first | |
| 276 // object. | |
| 277 // | |
| 278 // The inner recurIterator is always one step ahead, this is why we're | |
| 279 // checking for the key being higher than 1. | |
| 280 if ($this->recurIterator->key() > 1) { | |
| 281 $recurid = clone $event->DTSTART; | |
| 282 $recurid->name = 'RECURRENCE-ID'; | |
| 283 $event->add($recurid); | |
| 284 } | |
| 285 return $event; | |
| 286 | |
| 287 } | |
| 288 | |
| 289 /** | |
| 290 * Returns the current position of the iterator. | |
| 291 * | |
| 292 * This is for us simply a 0-based index. | |
| 293 * | |
| 294 * @return int | |
| 295 */ | |
| 296 public function key() { | |
| 297 | |
| 298 // The counter is always 1 ahead. | |
| 299 return $this->counter - 1; | |
| 300 | |
| 301 } | |
| 302 | |
| 303 /** | |
| 304 * This is called after next, to see if the iterator is still at a valid | |
| 305 * position, or if it's at the end. | |
| 306 * | |
| 307 * @return bool | |
| 308 */ | |
| 309 public function valid() { | |
| 310 | |
| 311 return !!$this->currentDate; | |
| 312 | |
| 313 } | |
| 314 | |
| 315 /** | |
| 316 * Sets the iterator back to the starting point. | |
| 317 */ | |
| 318 public function rewind() { | |
| 319 | |
| 320 $this->recurIterator->rewind(); | |
| 321 // re-creating overridden event index. | |
| 322 $index = array(); | |
| 323 foreach($this->overriddenEvents as $key=>$event) { | |
| 324 $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp(); | |
| 325 $index[$stamp] = $key; | |
| 326 } | |
| 327 krsort($index); | |
| 328 $this->counter = 0; | |
| 329 $this->overriddenEventsIndex = $index; | |
| 330 $this->currentOverriddenEvent = null; | |
| 331 | |
| 332 $this->nextDate = null; | |
| 333 $this->currentDate = clone $this->startDate; | |
| 334 | |
| 335 $this->next(); | |
| 336 | |
| 337 } | |
| 338 | |
| 339 /** | |
| 340 * Advances the iterator with one step. | |
| 341 * | |
| 342 * @return void | |
| 343 */ | |
| 344 public function next() { | |
| 345 | |
| 346 $this->currentOverriddenEvent = null; | |
| 347 $this->counter++; | |
| 348 if ($this->nextDate) { | |
| 349 // We had a stored value. | |
| 350 $nextDate = $this->nextDate; | |
| 351 $this->nextDate = null; | |
| 352 } else { | |
| 353 // We need to ask rruleparser for the next date. | |
| 354 // We need to do this until we find a date that's not in the | |
| 355 // exception list. | |
| 356 do { | |
| 357 if (!$this->recurIterator->valid()) { | |
| 358 $nextDate = null; | |
| 359 break; | |
| 360 } | |
| 361 $nextDate = $this->recurIterator->current(); | |
| 362 $this->recurIterator->next(); | |
| 363 } while(isset($this->exceptions[$nextDate->getTimeStamp()])); | |
| 364 | |
| 365 } | |
| 366 | |
| 367 | |
| 368 // $nextDate now contains what rrule thinks is the next one, but an | |
| 369 // overridden event may cut ahead. | |
| 370 if ($this->overriddenEventsIndex) { | |
| 371 | |
| 372 $offset = end($this->overriddenEventsIndex); | |
| 373 $timestamp = key($this->overriddenEventsIndex); | |
| 374 if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) { | |
| 375 // Overridden event comes first. | |
| 376 $this->currentOverriddenEvent = $this->overriddenEvents[$offset]; | |
| 377 | |
| 378 // Putting the rrule next date aside. | |
| 379 $this->nextDate = $nextDate; | |
| 380 $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone); | |
| 381 | |
| 382 // Ensuring that this item will only be used once. | |
| 383 array_pop($this->overriddenEventsIndex); | |
| 384 | |
| 385 // Exit point! | |
| 386 return; | |
| 387 | |
| 388 } | |
| 389 | |
| 390 } | |
| 391 | |
| 392 $this->currentDate = $nextDate; | |
| 393 | |
| 394 } | |
| 395 | |
| 396 /** | |
| 397 * Quickly jump to a date in the future. | |
| 398 * | |
| 399 * @param DateTime $dateTime | |
| 400 */ | |
| 401 public function fastForward(DateTime $dateTime) { | |
| 402 | |
| 403 while($this->valid() && $this->getDtEnd() < $dateTime ) { | |
| 404 $this->next(); | |
| 405 } | |
| 406 | |
| 407 } | |
| 408 | |
| 409 /** | |
| 410 * Returns true if this recurring event never ends. | |
| 411 * | |
| 412 * @return bool | |
| 413 */ | |
| 414 public function isInfinite() { | |
| 415 | |
| 416 return $this->recurIterator->isInfinite(); | |
| 417 | |
| 418 } | |
| 419 | |
| 420 /** | |
| 421 * RRULE parser | |
| 422 * | |
| 423 * @var RRuleIterator | |
| 424 */ | |
| 425 protected $recurIterator; | |
| 426 | |
| 427 /** | |
| 428 * The duration, in seconds, of the master event. | |
| 429 * | |
| 430 * We use this to calculate the DTEND for subsequent events. | |
| 431 */ | |
| 432 protected $eventDuration; | |
| 433 | |
| 434 /** | |
| 435 * A reference to the main (master) event. | |
| 436 * | |
| 437 * @var VEVENT | |
| 438 */ | |
| 439 protected $masterEvent; | |
| 440 | |
| 441 /** | |
| 442 * List of overridden events. | |
| 443 * | |
| 444 * @var array | |
| 445 */ | |
| 446 protected $overriddenEvents = array(); | |
| 447 | |
| 448 /** | |
| 449 * Overridden event index. | |
| 450 * | |
| 451 * Key is timestamp, value is the index of the item in the $overriddenEvent | |
| 452 * property. | |
| 453 * | |
| 454 * @var array | |
| 455 */ | |
| 456 protected $overriddenEventsIndex; | |
| 457 | |
| 458 /** | |
| 459 * A list of recurrence-id's that are either part of EXDATE, or are | |
| 460 * overridden. | |
| 461 * | |
| 462 * @var array | |
| 463 */ | |
| 464 protected $exceptions = array(); | |
| 465 | |
| 466 /** | |
| 467 * Internal event counter | |
| 468 * | |
| 469 * @var int | |
| 470 */ | |
| 471 protected $counter; | |
| 472 | |
| 473 /** | |
| 474 * The very start of the iteration process. | |
| 475 * | |
| 476 * @var DateTime | |
| 477 */ | |
| 478 protected $startDate; | |
| 479 | |
| 480 /** | |
| 481 * Where we are currently in the iteration process | |
| 482 * | |
| 483 * @var DateTime | |
| 484 */ | |
| 485 protected $currentDate; | |
| 486 | |
| 487 /** | |
| 488 * The next date from the rrule parser. | |
| 489 * | |
| 490 * Sometimes we need to temporary store the next date, because an | |
| 491 * overridden event came before. | |
| 492 * | |
| 493 * @var DateTime | |
| 494 */ | |
| 495 protected $nextDate; | |
| 496 | |
| 497 } |
