Skip to content

Commit 05fd96b

Browse files
committed
Backport of sabre-io/vobject#716 -- support RDATE together with RRULE.
1 parent e0a5097 commit 05fd96b

File tree

2 files changed

+89
-42
lines changed

2 files changed

+89
-42
lines changed

sabre/vobject/lib/Recur/EventIterator.php

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Sabre\VObject\Recur;
44

5+
use AppendIterator;
56
use DateTimeImmutable;
67
use DateTimeInterface;
78
use DateTimeZone;
@@ -167,18 +168,29 @@ public function __construct($input, $uid = null, ?DateTimeZone $timeZone = null)
167168
$this->eventDuration = 0;
168169
}
169170

171+
$this->recurIterators = [];
172+
$isRecurring = false;
173+
if (isset($this->masterEvent->RRULE)) {
174+
foreach ($this->masterEvent->RRULE as $rRule) {
175+
$this->recurIterators[] = new RRuleIterator(
176+
$this->masterEvent->RRULE->getParts(),
177+
$this->startDate
178+
);
179+
}
180+
$isRecurring = true;
181+
}
170182
if (isset($this->masterEvent->RDATE)) {
171-
$this->recurIterator = new RDateIterator(
172-
$this->masterEvent->RDATE->getParts(),
173-
$this->startDate
174-
);
175-
} elseif (isset($this->masterEvent->RRULE)) {
176-
$this->recurIterator = new RRuleIterator(
177-
$this->masterEvent->RRULE->getParts(),
178-
$this->startDate
179-
);
180-
} else {
181-
$this->recurIterator = new RRuleIterator(
183+
foreach ($this->masterEvent->RDATE as $rDate) {
184+
$this->recurIterators[] = new RDateIterator(
185+
$rDate->getParts(),
186+
$this->startDate,
187+
omitStart: $isRecurring
188+
);
189+
$isRecurring = true;
190+
}
191+
}
192+
if (!$isRecurring) {
193+
$this->recurIterators[] = new RRuleIterator(
182194
[
183195
'FREQ' => 'DAILY',
184196
'COUNT' => 1,
@@ -317,7 +329,9 @@ public function valid()
317329
#[\ReturnTypeWillChange]
318330
public function rewind()
319331
{
320-
$this->recurIterator->rewind();
332+
foreach ($this->recurIterators as $iterator) {
333+
$iterator->rewind();
334+
}
321335
// re-creating overridden event index.
322336
$index = [];
323337
foreach ($this->overriddenEvents as $key => $event) {
@@ -332,6 +346,15 @@ public function rewind()
332346
$this->nextDate = null;
333347
$this->currentDate = clone $this->startDate;
334348

349+
$this->currentCandidates = [];
350+
foreach ($this->recurIterators as $index => $iterator) {
351+
if (!$iterator->valid()) {
352+
continue;
353+
}
354+
$this->currentCandidates[$index] = $iterator->current()->getTimeStamp();
355+
}
356+
asort($this->currentCandidates);
357+
335358
$this->next();
336359
}
337360

@@ -354,13 +377,30 @@ public function next()
354377
// We need to do this until we find a date that's not in the
355378
// exception list.
356379
do {
357-
if (!$this->recurIterator->valid()) {
380+
if (empty($this->currentCandidates)) {
358381
$nextDate = null;
359382
break;
360383
}
361-
$nextDate = $this->recurIterator->current();
362-
$this->recurIterator->next();
363-
} while (isset($this->exceptions[$nextDate->getTimeStamp()]));
384+
$nextIndex = array_key_first($this->currentCandidates);
385+
$nextDate = $this->recurIterators[$nextIndex]->current();
386+
$nextStamp = $this->currentCandidates[$nextIndex];
387+
388+
// advance all iterators which match the current timestamp
389+
foreach ($this->currentCandidates as $index => $stamp) {
390+
if ($stamp > $nextStamp) {
391+
break;
392+
}
393+
$iterator = $this->recurIterators[$index];
394+
$iterator->next();
395+
if ($iterator->valid()) {
396+
$this->currentCandidates[$index] = $iterator->current()->getTimeStamp();
397+
asort($this->currentCandidates);
398+
} else {
399+
unset($this->currentCandidates[$index]);
400+
// resort not neccessary
401+
}
402+
}
403+
} while (isset($this->exceptions[$nextStamp]));
364404
}
365405

366406
// $nextDate now contains what rrule thinks is the next one, but an
@@ -408,15 +448,27 @@ public function fastForward(DateTimeInterface $dateTime)
408448
*/
409449
public function isInfinite()
410450
{
411-
return $this->recurIterator->isInfinite();
451+
foreach ($this->recurIterators as $iterator) {
452+
if ($iterator->isInfinite()) {
453+
return true;
454+
}
455+
}
456+
return false;
412457
}
413458

414459
/**
415-
* RRULE parser.
460+
* Array of RRULE parsers.
461+
*
462+
* @var array<int, RRuleIterator>
463+
*/
464+
protected $recurIterators;
465+
466+
/**
467+
* Array of current candidate timestamps.
416468
*
417-
* @var RRuleIterator
469+
* @var array<int, int>
418470
*/
419-
protected $recurIterator;
471+
protected $currentCandidates;
420472

421473
/**
422474
* The duration, in seconds, of the master event.

sabre/vobject/lib/Recur/RDateIterator.php

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Sabre\VObject\Recur;
44

5+
use DateTimeImmutable;
56
use DateTimeInterface;
67
use Iterator;
78
use Sabre\VObject\DateTimeParser;
@@ -26,11 +27,14 @@ class RDateIterator implements Iterator
2627
*
2728
* @param string|array $rrule
2829
*/
29-
public function __construct($rrule, DateTimeInterface $start)
30+
public function __construct($rrule, DateTimeInterface $start, bool $omitStart = false)
3031
{
3132
$this->startDate = $start;
3233
$this->parseRDate($rrule);
33-
$this->currentDate = clone $this->startDate;
34+
if (!$omitStart) {
35+
array_unshift($this->dates, DateTimeImmutable::createFromInterface($this->startDate));
36+
}
37+
$this->rewind();
3438
}
3539

3640
/* Implementation of the Iterator interface {{{ */
@@ -39,10 +43,16 @@ public function __construct($rrule, DateTimeInterface $start)
3943
public function current()
4044
{
4145
if (!$this->valid()) {
42-
return;
46+
return null;
4347
}
44-
45-
return clone $this->currentDate;
48+
if (is_string($this->dates[$this->counter])) {
49+
$this->dates[$this->counter] =
50+
DateTimeParser::parse(
51+
$this->dates[$this->counter],
52+
$this->startDate->getTimezone()
53+
);
54+
}
55+
return $this->dates[$this->counter];
4656
}
4757

4858
/**
@@ -65,7 +75,7 @@ public function key()
6575
#[\ReturnTypeWillChange]
6676
public function valid()
6777
{
68-
return $this->counter <= count($this->dates);
78+
return $this->counter < count($this->dates);
6979
}
7080

7181
/**
@@ -76,7 +86,6 @@ public function valid()
7686
#[\ReturnTypeWillChange]
7787
public function rewind()
7888
{
79-
$this->currentDate = clone $this->startDate;
8089
$this->counter = 0;
8190
}
8291

@@ -92,12 +101,6 @@ public function next()
92101
if (!$this->valid()) {
93102
return;
94103
}
95-
96-
$this->currentDate =
97-
DateTimeParser::parse(
98-
$this->dates[$this->counter - 1],
99-
$this->startDate->getTimezone()
100-
);
101104
}
102105

103106
/* End of Iterator implementation }}} */
@@ -118,7 +121,7 @@ public function isInfinite()
118121
*/
119122
public function fastForward(DateTimeInterface $dt)
120123
{
121-
while ($this->valid() && $this->currentDate < $dt) {
124+
while ($this->valid() && $this->current() < $dt) {
122125
$this->next();
123126
}
124127
}
@@ -132,14 +135,6 @@ public function fastForward(DateTimeInterface $dt)
132135
*/
133136
protected $startDate;
134137

135-
/**
136-
* The date of the current iteration. You can get this by calling
137-
* ->current().
138-
*
139-
* @var DateTimeInterface
140-
*/
141-
protected $currentDate;
142-
143138
/**
144139
* The current item in the list.
145140
*

0 commit comments

Comments
 (0)