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 } |