aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/CalDAV/EventReader.php
blob: 99e5677d432658b1ca3f8ac0c3e63f4930db5b5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\DAV\CalDAV;

use DateTime;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;

use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Reader;

class EventReader {

	protected VEvent $baseEvent;
	protected DateTimeInterface $baseEventStartDate;
	protected DateTimeZone $baseEventStartTimeZone;
	protected DateTimeInterface $baseEventEndDate;
	protected DateTimeZone $baseEventEndTimeZone;
	protected bool $baseEventStartDateFloating = false;
	protected bool $baseEventEndDateFloating = false;
	protected int $baseEventDuration;

	protected ?EventReaderRRule $rruleIterator = null;
	protected ?EventReaderRDate $rdateIterator = null;
	protected ?EventReaderRRule $eruleIterator = null;
	protected ?EventReaderRDate $edateIterator = null;

	protected array $recurrenceModified;
	protected ?DateTimeInterface $recurrenceCurrentDate;

	protected array $dayNamesMap = [
		'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday'
	];
	protected array $monthNamesMap = [
		1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June',
		7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
	];
	protected array $relativePositionNamesMap = [
		1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifty',
		-1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifty Last'
	];

	/**
	 * Initilizes the Event Reader
	 *
	 * There is several ways to set up the iterator.
	 *
	 * 1. You can pass a VCALENDAR component (as object or string) and a UID.
	 * 2. You can pass an array of VEVENTs (all UIDS should match).
	 * 3. You can pass a single VEVENT component (as object or string).
	 *
	 * Only the second method is recommended. The other 1 and 3 will be removed
	 * at some point in the future.
	 *
	 * The $uid parameter is only required for the first method.
	 *
	 * @since 30.0.0
	 *
	 * @param VCalendar|VEvent|Array|String $input
	 * @param string|null     				$uid
	 * @param DateTimeZone|null    				$timeZone reference timezone for floating dates and times
	 */
	public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) {

		// evaluate if the input is a string and convert it to and vobject if required
		if (is_string($input)) {
			$input = Reader::read($input);
		}
		// evaluate if input is a single event vobject and convert it to a collection
		if ($input instanceof VEvent) {
			$events = [$input];
		}
		// evaluate if input is a calendar vobject
		elseif ($input instanceof VCalendar) {
			// Calendar + UID mode.
			if ($uid === null) {
				throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used');
			}
			// extract events from calendar
			$events = $input->getByUID($uid);
			// evaluate if any event where found
			if (count($events) === 0) {
				throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
			}
			// extract calendar timezone
			if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) {
				$calendarTimeZone = new DateTimeZone($input->VTIMEZONE->TZID->getValue());
			}
		}
		// evaluate if input is a collection of event vobjects
		elseif (is_array($input)) {
			$events = $input;
		} else {
			throw new InvalidArgumentException('Invalid input data type');
		}
		// find base event instance and remove it from events collection
		foreach ($events as $key => $vevent) {
			if (!isset($vevent->{'RECURRENCE-ID'})) {
				$this->baseEvent = $vevent;
				unset($events[$key]);
			}
		}
		
		// No base event was found. CalDAV does allow cases where only
		// overridden instances are stored.
		//
		// In this particular case, we're just going to grab the first
		// event and use that instead. This may not always give the
		// desired result.
		if (!isset($this->baseEvent) && count($events) > 0) {
			$this->baseEvent = array_shift($events);
		}

		// determain the event starting time zone
		// we require this to align all other dates times
		// evaluate if timezone paramater was used (treat this as a override)
		if ($timeZone !== null) {
			$this->baseEventStartTimeZone = $timeZone;
		}
		// evaluate if event start date has a timezone parameter
		elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) {
			$this->baseEventStartTimeZone = new DateTimeZone($this->baseEvent->DTSTART->parameters['TZID']->getValue());
		}
		// evaluate if event parent calendar has a time zone
		elseif (isset($calendarTimeZone)) {
			$this->baseEventStartTimeZone = clone $calendarTimeZone;
		}
		// otherwise, as a last resort use the UTC timezone
		else {
			$this->baseEventStartTimeZone = new DateTimeZone('UTC');
		}

		// determain the event end time zone
		// we require this to align all other dates and times
		// evaluate if timezone paramater was used (treat this as a override)
		if ($timeZone !== null) {
			$this->baseEventEndTimeZone = $timeZone;
		}
		// evaluate if event end date has a timezone parameter
		elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) {
			$this->baseEventEndTimeZone = new DateTimeZone($this->baseEvent->DTEND->parameters['TZID']->getValue());
		}
		// evaluate if event parent calendar has a time zone
		elseif (isset($calendarTimeZone)) {
			$this->baseEventEndTimeZone = clone $calendarTimeZone;
		}
		// otherwise, as a last resort use the start date time zone
		else {
			$this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone;
		}
		// extract start date and time
		$this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone);
		$this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating();
		// determine event end date and duration
		// evaluate if end date exists
		// extract end date and calculate duration
		if (isset($this->baseEvent->DTEND)) {
			$this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone);
			$this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating();
			$this->baseEventDuration =
				$this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp() -
				$this->baseEventStartDate->getTimeStamp();
		}
		// evaluate if duration exists
		// extract duration and calculate end date
		elseif (isset($this->baseEvent->DURATION)) {
			$this->baseEventDuration = $this->baseEvent->DURATION->getDateInterval();
			$this->baseEventEndDate = ((clone $this->baseEventStartDate)->add($this->baseEventDuration));
		}
		// evaluate if start date is floating
		// set duration to 24 hours and calculate the end date
		// according to the rfc any event without a end date or duration is a complete day
		elseif ($this->baseEventStartDateFloating == true) {
			$this->baseEventDuration = 86400;
			$this->baseEventEndDate = ((clone $this->baseEventStartDate)->add($this->baseEventDuration));
		}
		// otherwise, set duration to zero this should never happen
		else {
			$this->baseEventDuration = 0;
			$this->baseEventEndDate = $this->baseEventStartDate;
		}
		// evaluate if RRULE exist and construct iterator
		if (isset($this->baseEvent->RRULE)) {
			$this->rruleIterator = new EventReaderRRule(
				$this->baseEvent->RRULE->getParts(),
				$this->baseEventStartDate
			);
		}
		// evaluate if RDATE exist and construct iterator
		if (isset($this->baseEvent->RDATE)) {
			$this->rdateIterator = new EventReaderRDate(
				$this->baseEvent->RDATE->getValue(),
				$this->baseEventStartDate
			);
		}
		// evaluate if EXRULE exist and construct iterator
		if (isset($this->baseEvent->EXRULE)) {
			$this->eruleIterator = new EventReaderRRule(
				$this->baseEvent->EXRULE->getParts(),
				$this->baseEventStartDate
			);
		}
		// evaluate if EXDATE exist and construct iterator
		if (isset($this->baseEvent->EXDATE)) {
			$this->edateIterator = new EventReaderRDate(
				$this->baseEvent->EXDATE->getValue(),
				$this->baseEventStartDate
			);
		}
		// construct collection of modified events with recurrence id as hash
		foreach ($events as $vevent) {
			$this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent;
		}
		
		$this->recurrenceCurrentDate = clone $this->baseEventStartDate;
	}

	/**
	 * retrieve date and time of event start
	 *
	 * @since 30.0.0
	 *
	 * @return DateTime
	 */
	public function startDateTime(): DateTime {
		return DateTime::createFromInterface($this->baseEventStartDate);
	}

	/**
	 * retrieve time zone of event start
	 *
	 * @since 30.0.0
	 *
	 * @return DateTimeZone
	 */
	public function startTimeZone(): DateTimeZone {
		return $this->baseEventStartTimeZone;
	}

	/**
	 * retrieve date and time of event end
	 *
	 * @since 30.0.0
	 *
	 * @return DateTime
	 */
	public function endDateTime(): DateTime {
		return DateTime::createFromInterface($this->baseEventEndDate);
	}

	/**
	 * retrieve time zone of event end
	 *
	 * @since 30.0.0
	 *
	 * @return DateTimeZone
	 */
	public function endTimeZone(): DateTimeZone {
		return $this->baseEventEndTimeZone;
	}

	/**
	 * is this an all day event
	 *
	 * @since 30.0.0
	 *
	 * @return bool
	 */
	public function entireDay(): bool {
		return $this->baseEventStartDateFloating;
	}

	/**
	 * is this a recurring event
	 *
	 * @since 30.0.0
	 *
	 * @return bool
	 */
	public function recurs(): bool {
		return ($this->rruleIterator !== null || $this->rdateIterator !== null);
	}

	/**
	 * event recurrence pattern
	 *
	 * @since 30.0.0
	 *
	 * @return string|null				R - Relative or A - Absolute
	 */
	public function recurringPattern(): string | null {
		if ($this->rruleIterator === null && $this->rdateIterator === null) {
			return null;
		}
		if ($this->rruleIterator?->isRelative()) {
			return 'R';
		}
		return 'A';
	}

	/**
	 * event recurrence precision
	 *
	 * @since 30.0.0
	 *
	 * @return string|null			daily, weekly, monthly, yearly, fixed
	 */
	public function recurringPrecision(): string | null {
		if ($this->rruleIterator !== null) {
			return $this->rruleIterator->precision();
		}
		if ($this->rdateIterator !== null) {
			return 'fixed';
		}
		return null;
	}

	/**
	 * event recurrence interval
	 *
	 * @since 30.0.0
	 *
	 * @return int|null
	 */
	public function recurringInterval(): int | null {
		return $this->rruleIterator?->interval();
	}

	/**
	 * event recurrence conclusion
	 *
	 * returns true if RRULE with UNTIL or COUNT (calculated) is used
	 * returns true RDATE is used
	 * returns false if RRULE or RDATE are absent, or RRRULE is infinite
	 *
	 * @since 30.0.0
	 *
	 * @return bool
	 */
	public function recurringConcludes(): bool {

		// retrieve rrule conclusions
		if ($this->rruleIterator?->concludesOn() !== null ||
			$this->rruleIterator?->concludesAfter() !== null) {
			return true;
		}
		// retrieve rdate conclusions
		if ($this->rdateIterator?->concludesAfter() !== null) {
			return true;
		}

		return false;

	}

	/**
	 * event recurrence conclusion iterations
	 *
	 * returns the COUNT value if RRULE is used
	 * returns the collection count if RDATE is used
	 * returns combined count of RRULE COUNT and RDATE if both are used
	 * returns null if RRULE and RDATE are absent
	 *
	 * @since 30.0.0
	 *
	 * @return int|null
	 */
	public function recurringConcludesAfter(): int | null {
		
		// construct count place holder
		$count = 0;
		// retrieve and add RRULE iterations count
		$count += (int) $this->rruleIterator?->concludesAfter();
		// retrieve and add RDATE iterations count
		$count += (int) $this->rdateIterator?->concludesAfter();
		// return count
		return !empty($count) ? $count : null;

	}

	/**
	 * event recurrence conclusion date
	 *
	 * returns the last date of UNTIL or COUNT (calculated) if RRULE is used
	 * returns the last date in the collection if RDATE is used
	 * returns the highest date if both RRULE and RDATE are used
	 * returns null if RRULE and RDATE are absent or RRULE is infinite
	 *
	 * @since 30.0.0
	 *
	 * @return DateTime|null
	 */
	public function recurringConcludesOn(): DateTime | null {

		if ($this->rruleIterator !== null) {
			// retrieve rrule conclusion date
			$rrule = $this->rruleIterator->concludes();
			// evaluate if rrule conclusion is null
			// if this is null that means the recurrence is infinate
			if ($rrule === null) {
				return null;
			}
		}
		// retrieve rdate conclusion date
		if ($this->rdateIterator !== null) {
			$rdate = $this->rdateIterator->concludes();
		}
		// evaluate if both rrule and rdate have date
		if (isset($rdate) && isset($rrule)) {
			// return the highest date
			return (($rdate > $rrule) ? $rdate : $rrule);
		} elseif (isset($rrule)) {
			return $rrule;
		} elseif (isset($rdate)) {
			return $rdate;
		}

		return null;

	}

	/**
	 * event recurrence days of the week
	 *
	 * returns collection of RRULE BYDAY day(s) ['MO','WE','FR']
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringDaysOfWeek(): array {
		// evaluate if RRULE exists and return day(s) of the week
		return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
	}

	/**
	 * event recurrence days of the week (named)
	 *
	 * returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday']
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringDaysOfWeekNamed(): array {
		// evaluate if RRULE exists and extract day(s) of the week
		$days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
		// convert numberic month to month name
		foreach ($days as $key => $value) {
			$days[$key] = $this->dayNamesMap[$value];
		}
		// return names collection
		return $days;
	}

	/**
	 * event recurrence days of the month
	 *
	 * returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31]
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringDaysOfMonth(): array {
		// evaluate if RRULE exists and return day(s) of the month
		return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : [];
	}

	/**
	 * event recurrence days of the year
	 *
	 * returns collection of RRULE BYYEARDAY day(s) [57, 205, 365]
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringDaysOfYear(): array {
		// evaluate if RRULE exists and return day(s) of the year
		return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : [];
	}

	/**
	 * event recurrence weeks of the month
	 *
	 * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
	 * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringWeeksOfMonth(): array {
		// evaluate if RRULE exists and RRULE is relative return relative position(s)
		return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
	}

	/**
	 * event recurrence weeks of the month (named)
	 *
	 * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
	 * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringWeeksOfMonthNamed(): array {
		// evaluate if RRULE exists and extract relative position(s)
		$positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
		// convert numberic relative position to relative label
		foreach ($positions as $key => $value) {
			$positions[$key] = $this->relativePositionNamesMap[$value];
		}
		// return positions collection
		return $positions;
	}

	/**
	 * event recurrence weeks of the year
	 *
	 * returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52]
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringWeeksOfYear(): array {
		// evaluate if RRULE exists and return weeks(s) of the year
		return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : [];
	}

	/**
	 * event recurrence months of the year
	 *
	 * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringMonthsOfYear(): array {
		// evaluate if RRULE exists and return month(s) of the year
		return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
	}

	/**
	 * event recurrence months of the year (named)
	 *
	 * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringMonthsOfYearNamed(): array {
		// evaluate if RRULE exists and extract month(s) of the year
		$months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
		// convert numberic month to month name
		foreach ($months as $key => $value) {
			$months[$key] = $this->monthNamesMap[$value];
		}
		// return months collection
		return $months;
	}

	/**
	 * event recurrence relative positions
	 *
	 * returns collection of RRULE SETPOS value(s) [1, 5, -3]
	 * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringRelativePosition(): array {
		// evaluate if RRULE exists and return relative position(s)
		return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : [];
	}

	/**
	 * event recurrence relative positions (named)
	 *
	 * returns collection of RRULE SETPOS [1, 3, -1]
	 * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
	 *
	 * @since 30.0.0
	 *
	 * @return array
	 */
	public function recurringRelativePositionNamed(): array {
		// evaluate if RRULE exists and extract relative position(s)
		$positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
		// convert numberic relative position to relative label
		foreach ($positions as $key => $value) {
			$positions[$key] = $this->relativePositionNamesMap[$value];
		}
		// return positions collection
		return $positions;
	}

	/**
	 * event recurrence date
	 *
	 * returns date of currently selected recurrence
	 *
	 * @since 30.0.0
	 *
	 * @return DateTime
	 */
	public function recurrenceDate(): DateTime | null {
		if ($this->recurrenceCurrentDate !== null) {
			return DateTime::createFromInterface($this->recurrenceCurrentDate);
		} else {
			return null;
		}
	}

	/**
	 * event recurrence rewind
	 *
	 * sets the current recurrence to the first recurrence in the collection
	 *
	 * @since 30.0.0
	 *
	 * @return void
	 */
	public function recurrenceRewind(): void {
		// rewind and increment rrule
		if ($this->rruleIterator !== null) {
			$this->rruleIterator->rewind();
		}
		// rewind and increment rdate
		if ($this->rdateIterator !== null) {
			$this->rdateIterator->rewind();
		}
		// rewind and increment exrule
		if ($this->eruleIterator !== null) {
			$this->eruleIterator->rewind();
		}
		// rewind and increment exdate
		if ($this->edateIterator !== null) {
			$this->edateIterator->rewind();
		}
		// set current date to event start date
		$this->recurrenceCurrentDate = clone $this->baseEventStartDate;
	}

	/**
	 * event recurrence advance
	 *
	 * sets the current recurrence to the next recurrence in the collection
	 *
	 * @since 30.0.0
	 *
	 * @return void
	 */
	public function recurrenceAdvance(): void {
		// place holders
		$nextOccurrenceDate = null;
		$nextExceptionDate = null;
		$rruleDate = null;
		$rdateDate = null;
		$eruleDate = null;
		$edateDate = null;
		// evaludate if rrule is set and advance one interation past current date
		if ($this->rruleIterator !== null) {
			// forward rrule to the next future date
			while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) {
				$this->rruleIterator->next();
			}
			$rruleDate = $this->rruleIterator->current();
		}
		// evaludate if rdate is set and advance one interation past current date
		if ($this->rdateIterator !== null) {
			// forward rdate to the next future date
			while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) {
				$this->rdateIterator->next();
			}
			$rdateDate = $this->rdateIterator->current();
		}
		if ($rruleDate !== null && $rdateDate !== null) {
			$nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate;
		} elseif ($rruleDate !== null) {
			$nextOccurrenceDate = $rruleDate;
		} elseif ($rdateDate !== null) {
			$nextOccurrenceDate = $rdateDate;
		}

		// evaludate if exrule is set and advance one interation past current date
		if ($this->eruleIterator !== null) {
			// forward exrule to the next future date
			while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) {
				$this->eruleIterator->next();
			}
			$eruleDate = $this->eruleIterator->current();
		}
		// evaludate if exdate is set and advance one interation past current date
		if ($this->edateIterator !== null) {
			// forward exdate to the next future date
			while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) {
				$this->edateIterator->next();
			}
			$edateDate = $this->edateIterator->current();
		}
		// evaludate if exrule and exdate are set and set nextExDate to the first next date
		if ($eruleDate !== null && $edateDate !== null) {
			$nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate;
		} elseif ($eruleDate !== null) {
			$nextExceptionDate = $eruleDate;
		} elseif ($edateDate !== null) {
			$nextExceptionDate = $edateDate;
		}
		// if the next date is part of exrule or exdate find another date
		if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) {
			$this->recurrenceCurrentDate = $nextOccurrenceDate;
			$this->recurrenceAdvance();
		} else {
			$this->recurrenceCurrentDate = $nextOccurrenceDate;
		}
	}

	/**
	 * event recurrence advance
	 *
	 * sets the current recurrence to the next recurrence in the collection after the specific date
	 *
	 * @since 30.0.0
	 *
	 * @param DateTimeInterface $dt			date and time to advance
	 *
	 * @return void
	 */
	public function recurrenceAdvanceTo(DateTimeInterface $dt): void {
		while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) {
			$this->recurrenceAdvance();
		}
	}

}