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 }