PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.5.23
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.5.23
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Services / Libs / RRule / RSet.php

RSet.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 1.5.23, at app/Services/Libs/RRule/RSet.php

698 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Licensed under the MIT license.
5 *
6 * For the full copyright and license information, please view the LICENSE file.
7 *
8 * @author Rémi Lanvin <remi@cloudconnected.fr>
9 * @link https://github.com/rlanvin/php-rrule
10 */
11
12 namespace FluentBooking\App\Services\Libs\RRule;
13
14 /**
15 * Recurrence set
16 */
17 class RSet implements RRuleInterface
18 {
19 use RRuleTrait;
20
21 /**
22 * @var array List of RDATE (single dates)
23 */
24 protected $rdates = array();
25
26 /**
27 * @var array List of RRULE
28 */
29 protected $rrules = array();
30
31 /**
32 * @var array List of EXDATE (single dates to be excluded)
33 */
34 protected $exdates = array();
35
36 /**
37 * @var array List of EXRULES (single rules to be excluded)
38 */
39 protected $exrules = array();
40
41 // cache variable
42
43 /**
44 * @var int|null Cache for the total number of occurrences
45 */
46 protected $total = null;
47
48 /**
49 * @var int|null Cache for the finite status of the RSet
50 */
51 protected $infinite = null;
52
53 /**
54 * @var array Cache for all the occurrences
55 */
56 protected $cache = array();
57
58 /**
59 * Constructor
60 *
61 * @param string $string a RFC compliant text block
62 */
63 public function __construct($string = null, $default_dtstart = null)
64 {
65 if ($string && is_string($string)) {
66 $string = trim($string);
67 $rrules = array();
68 $exrules = array();
69 $rdates = array();
70 $exdates = array();
71 $dtstart = null;
72
73 // parse
74 $lines = explode("\n", $string);
75 foreach ($lines as $line) {
76 $line = trim($line);
77
78 if (strpos($line,':') === false) {
79 throw new \InvalidArgumentException('Failed to parse RFC string, line is not starting with a property name followed by ":"');
80 }
81
82 list($property_name,$property_value) = explode(':',$line);
83 $tmp = explode(";",$property_name);
84 $property_name = $tmp[0];
85 switch (strtoupper($property_name)) {
86 case 'DTSTART':
87 if ($default_dtstart || $dtstart !== null) {
88 throw new \InvalidArgumentException('Failed to parse RFC string, multiple DTSTART found');
89 }
90 $dtstart = $line;
91 break;
92 case 'RRULE':
93 $rrules[] = $line;
94 break;
95 case 'EXRULE':
96 $exrules[] = $line;
97 break;
98 case 'RDATE':
99 $rdates = array_merge($rdates, RfcParser::parseRDate($line));
100 break;
101 case 'EXDATE':
102 $exdates = array_merge($exdates, RfcParser::parseExDate($line));
103 break;
104 default:
105 throw new \InvalidArgumentException('Failed to parse RFC, unknown property: ' . esc_html($property_name));
106 }
107 }
108 foreach ($rrules as $rrule) {
109 if ($dtstart) {
110 $rrule = $dtstart."\n".$rrule;
111 }
112
113 $this->addRRule(new RRule($rrule, $default_dtstart));
114 }
115
116 foreach ($exrules as $rrule) {
117 if ($dtstart) {
118 $rrule = $dtstart."\n".$rrule;
119 }
120 $this->addExRule(new RRule($rrule, $default_dtstart));
121 }
122
123 foreach ($rdates as $date) {
124 $this->addDate($date);
125 }
126
127 foreach ($exdates as $date) {
128 $this->addExDate($date);
129 }
130 }
131 }
132
133 /**
134 * Add a RRule (or another RSet)
135 *
136 * @param mixed $rrule an instance of RRuleInterface or something that can be transformed into a RRule (string or array)
137 * @return $this
138 */
139 public function addRRule($rrule)
140 {
141 if (is_string($rrule) || is_array($rrule)) {
142 $rrule = new RRule($rrule);
143 }
144 elseif (! $rrule instanceof RRuleInterface) {
145 throw new \InvalidArgumentException('The rule must be a string, an array, or implement RRuleInterface');
146 }
147
148 // cloning because I want to iterate it without being disturbed
149 $this->rrules[] = clone $rrule;
150
151 $this->clearCache();
152
153 return $this;
154 }
155
156 /**
157 * Return the RRULE(s) contained in this set
158 *
159 * @todo check if a deep copy is needed.
160 *
161 * @return array Array of RRule
162 */
163 public function getRRules()
164 {
165 return $this->rrules;
166 }
167
168 /**
169 * Add a RRule with exclusion rules.
170 * In RFC 2445 but deprecated in RFC 5545
171 *
172 * @param mixed $rrule an instance of RRuleInterface or something that can be transformed into a RRule (string or array)
173 * @return $this
174 */
175 public function addExRule($rrule)
176 {
177 if (is_string($rrule) || is_array($rrule)) {
178 $rrule = new RRule($rrule);
179 }
180 elseif (! $rrule instanceof RRuleInterface) {
181 throw new \InvalidArgumentException('The rule must be a string, an array or implement RRuleInterface');
182 }
183
184 // cloning because I want to iterate it without being disturbed
185 $this->exrules[] = clone $rrule;
186
187 $this->clearCache();
188
189 return $this;
190 }
191
192 /**
193 * Return the EXRULE(s) contained in this set
194 *
195 * @todo check if a deep copy is needed.
196 *
197 * @return array Array of RRule
198 */
199 public function getExRules()
200 {
201 return $this->exrules;
202 }
203
204 /**
205 * Add a RDATE (renamed Date for simplicy, since we don't support full RDATE syntax at the moment)
206 *
207 * @param mixed $date a valid date representation or a \DateTime object
208 * @return $this
209 */
210 public function addDate($date)
211 {
212 try {
213 $this->rdates[] = RRule::parseDate($date);
214 sort($this->rdates);
215 } catch (\Exception $e) {
216 throw new \InvalidArgumentException(
217 'Failed to parse RDATE - it must be a valid date, timestamp or \DateTime object'
218 );
219 }
220
221 $this->clearCache();
222
223 return $this;
224 }
225
226 /**
227 * Remove an RDATE
228 *
229 * @param mixed $date a valid date representation or a \DateTime object
230 * @return $this
231 */
232 public function removeDate($date)
233 {
234 try {
235 $date_to_remove = RRule::parseDate($date);
236 $index = array_search($date_to_remove, $this->rdates);
237
238 if ($index !== false) {
239 unset($this->rdates[$index]);
240 $this->rdates = array_values($this->rdates);
241 }
242 } catch (\Exception $e) {
243 throw new \InvalidArgumentException(
244 'Failed to parse RDATE - it must be a valid date, timestamp or \DateTime object'
245 );
246 }
247
248 $this->clearCache();
249
250 return $this;
251 }
252
253 /**
254 * Remove all RDATEs
255 *
256 * @return $this
257 */
258 public function clearDates()
259 {
260 $this->rdates = [];
261 $this->clearCache();
262
263 return $this;
264 }
265
266 /**
267 * Return the RDATE(s) contained in this set
268 *
269 * @todo check if a deep copy is needed.
270 *
271 * @return array Array of \DateTime
272 */
273 public function getDates()
274 {
275 return $this->rdates;
276 }
277
278 /**
279 * Add a EXDATE
280 *
281 * @param mixed $date a valid date representation or a \DateTime object
282 * @return $this
283 */
284 public function addExDate($date)
285 {
286 try {
287 $this->exdates[] = RRule::parseDate($date);
288 sort($this->exdates);
289 } catch (\Exception $e) {
290 throw new \InvalidArgumentException(
291 'Failed to parse EXDATE - it must be a valid date, timestamp or \DateTime object'
292 );
293 }
294
295 $this->clearCache();
296
297 return $this;
298 }
299
300 /**
301 * Remove an EXDATE
302 *
303 * @param mixed $date a valid date representation or a \DateTime object
304 * @return $this
305 */
306 public function removeExDate($date)
307 {
308 try {
309 $date_to_remove = RRule::parseDate($date);
310 $index = array_search($date_to_remove, $this->exdates);
311
312 if ($index !== false) {
313 unset($this->exdates[$index]);
314 $this->exdates = array_values($this->exdates);
315 }
316 } catch (\Exception $e) {
317 throw new \InvalidArgumentException(
318 'Failed to parse EXDATE - it must be a valid date, timestamp or \DateTime object'
319 );
320 }
321
322 $this->clearCache();
323
324 return $this;
325 }
326
327 /**
328 * Removes all EXDATEs
329 *
330 * @return $this
331 */
332 public function clearExDates()
333 {
334 $this->exdates = [];
335 $this->clearCache();
336
337 return $this;
338 }
339
340 /**
341 * Return the EXDATE(s) contained in this set
342 *
343 * @todo check if a deep copy is needed.
344 *
345 * @return array Array of \DateTime
346 */
347 public function getExDates()
348 {
349 return $this->exdates;
350 }
351
352 /**
353 * Clear the cache.
354 * Do NOT use while the class is iterating.
355 * @return $this
356 */
357 public function clearCache()
358 {
359 $this->total = null;
360 $this->infinite = null;
361 $this->cache = array();
362
363 $this->rlist_heap = null;
364 $this->rlist_iterator = null;
365 $this->exlist_heap = null;
366 $this->exlist_iterator = null;
367
368 return $this;
369 }
370
371 ///////////////////////////////////////////////////////////////////////////////
372 // RRule interface
373
374 /**
375 * Return true if the rrule has an end condition, false otherwise
376 *
377 * @return bool
378 */
379 public function isFinite()
380 {
381 return ! $this->isInfinite();
382 }
383
384 /**
385 * Return true if the rrule has no end condition (infite)
386 *
387 * @return bool
388 */
389 public function isInfinite()
390 {
391 if ($this->infinite === null) {
392 $this->infinite = false;
393 foreach ($this->rrules as $rrule) {
394 if ($rrule->isInfinite()) {
395 $this->infinite = true;
396 break;
397 }
398 }
399 }
400 return $this->infinite;
401 }
402
403 /**
404 * Return all the occurrences in an array of \DateTime.
405 *
406 * @param int $limit Limit the resultset to n occurrences (0, null or false = everything)
407 * @return array An array of \DateTime objects
408 */
409 public function getOccurrences($limit = null)
410 {
411 if (!$limit && $this->isInfinite()) {
412 throw new \LogicException('Cannot get all occurrences of an infinite recurrence set.');
413 }
414
415 // cached version already computed
416 $iterator = $this;
417 if ($this->total !== null) {
418 $iterator = $this->cache;
419 }
420
421 $res = array();
422 $n = 0;
423 foreach ($iterator as $occurrence) {
424 $res[] = clone $occurrence; // we have to clone because DateTime is not immutable
425 $n += 1;
426 if ($limit && $n >= $limit) {
427 break;
428 }
429 }
430 return $res;
431 }
432
433 /**
434 * Return true if $date is an occurrence.
435 *
436 * @param mixed $date
437 * @return bool
438 */
439 public function occursAt($date)
440 {
441 $date = RRule::parseDate($date);
442
443 if (in_array($date, $this->cache)) {
444 // in the cache (whether cache is complete or not)
445 return true;
446 }
447 elseif ($this->total !== null) {
448 // cache complete and not in cache
449 return false;
450 }
451
452 // test if it *should* occur (before exclusion)
453 $occurs = false;
454 foreach ($this->rdates as $rdate) {
455 if ($rdate == $date) {
456 $occurs = true;
457 break;
458 }
459 }
460 if (! $occurs) {
461 foreach ($this->rrules as $rrule) {
462 if ($rrule->occursAt($date)) {
463 $occurs = true;
464 break;
465 }
466 }
467 }
468
469 // if it should occur, test if it's excluded
470 if ($occurs) {
471 foreach ($this->exdates as $exdate) {
472 if ($exdate == $date) {
473 return false;
474 }
475 }
476 foreach ($this->exrules as $exrule) {
477 if ($exrule->occursAt($date)) {
478 return false;
479 }
480 }
481 }
482
483 return $occurs;
484 }
485
486 ///////////////////////////////////////////////////////////////////////////////
487 // ArrayAccess interface
488
489 /**
490 * @internal
491 * @return bool
492 */
493 #[\ReturnTypeWillChange]
494 public function offsetExists($offset)
495 {
496 return is_numeric($offset) && $offset >= 0 && ! is_float($offset) && $offset < count($this);
497 }
498
499 /**
500 * @internal
501 * @return mixed
502 */
503 #[\ReturnTypeWillChange]
504 public function offsetGet($offset)
505 {
506 if (! is_numeric($offset) || $offset < 0 || is_float($offset)) {
507 throw new \InvalidArgumentException('Illegal offset type: '. esc_html(gettype($offset)));
508 }
509
510 if (isset($this->cache[$offset])) {
511 // found in cache
512 return clone $this->cache[$offset];
513 }
514 elseif ($this->total !== null) {
515 // cache complete and not found in cache
516 return null;
517 }
518
519 // not in cache and cache not complete, we have to loop to find it
520 $i = 0;
521 foreach ($this as $occurrence) {
522 if ($i == $offset) {
523 return $occurrence;
524 }
525 $i++;
526 if ($i > $offset) {
527 break;
528 }
529 }
530 return null;
531 }
532
533 /**
534 * @internal
535 * @return void
536 */
537 #[\ReturnTypeWillChange]
538 public function offsetSet($offset, $value)
539 {
540 throw new \LogicException('Setting a Date in a RSet is not supported (use addDate)');
541 }
542
543 /**
544 * @internal
545 * @return void
546 */
547 #[\ReturnTypeWillChange]
548 public function offsetUnset($offset)
549 {
550 throw new \LogicException('Unsetting a Date in a RSet is not supported (use addDate)');
551 }
552
553 ///////////////////////////////////////////////////////////////////////////////
554 // Countable interface
555
556 /**
557 * Returns the number of recurrences in this set. It will have go
558 * through the whole recurrence, if this hasn't been done before, which
559 * introduces a performance penality.
560 * @return int
561 */
562 #[\ReturnTypeWillChange]
563 public function count()
564 {
565 if ($this->isInfinite()) {
566 throw new \LogicException('Cannot count an infinite recurrence set.');
567 }
568
569 if ($this->total === null) {
570 foreach ($this as $occurrence) {}
571 }
572
573 return $this->total;
574 }
575
576 ///////////////////////////////////////////////////////////////////////////////
577 // Private methods
578
579 // cache variables
580 protected $rlist_heap = null;
581 protected $rlist_iterator = null;
582 protected $exlist_heap = null;
583 protected $exlist_iterator = null;
584
585 /**
586 * This method will iterate over a bunch of different iterators (rrules and arrays),
587 * keeping the results *in order*, while never attempting to merge or sort
588 * anything in memory. It can combine both finite and infinite rrule.
589 *
590 * What we need to do it to build two heaps: rlist and exlist
591 * Each heap contains multiple iterators (either RRule or ArrayIterator)
592 * At each step of the loop, it calls all of the iterators to generate a new item,
593 * and stores them in the heap, that keeps them in order.
594 *
595 * This is made slightly more complicated because this method is a generator.
596 *
597 * @param $reset (bool) Whether to restart the iteration, or keep going
598 * @return \DateTime|null
599 */
600 #[\ReturnTypeWillChange]
601 public function getIterator()
602 {
603 $previous_occurrence = null;
604 $total = 0;
605
606 foreach ($this->cache as $occurrence) {
607 yield clone $occurrence; // since DateTime is not immutable, avoid any problem
608
609 $total += 1;
610 }
611
612 if ($this->rlist_heap === null) {
613 // rrules + rdate
614 $this->rlist_heap = new \SplMinHeap();
615 $this->rlist_iterator = new \MultipleIterator(\MultipleIterator::MIT_NEED_ANY);
616 $this->rlist_iterator->attachIterator(new \ArrayIterator($this->rdates));
617 foreach ($this->rrules as $rrule) {
618 $this->rlist_iterator->attachIterator($rrule->getIterator());
619 }
620 $this->rlist_iterator->rewind();
621
622 // exrules + exdate
623 $this->exlist_heap = new \SplMinHeap();
624 $this->exlist_iterator = new \MultipleIterator(\MultipleIterator::MIT_NEED_ANY);
625
626 $this->exlist_iterator->attachIterator(new \ArrayIterator($this->exdates));
627 foreach ($this->exrules as $rrule) {
628 $this->exlist_iterator->attachIterator($rrule->getIterator());
629 }
630 $this->exlist_iterator->rewind();
631 }
632
633 while (true) {
634 foreach ($this->rlist_iterator->current() as $date) {
635 if ($date !== null) {
636 $this->rlist_heap->insert($date);
637 }
638 }
639 $this->rlist_iterator->next(); // advance the iterator for the next call
640
641 if ($this->rlist_heap->isEmpty()) {
642 break; // exit the loop to stop the iterator
643 }
644
645 $occurrence = $this->rlist_heap->top();
646 $this->rlist_heap->extract(); // remove the occurrence from the heap
647
648 if ($occurrence == $previous_occurrence) {
649 continue; // skip, was already considered
650 }
651
652 // now we need to check against exlist
653 // we need to iterate exlist as long as it contains dates lower than occurrence
654 // (they will be discarded), and then check if the date is the same
655 // as occurrence (in which case it is discarded)
656 $excluded = false;
657 while (true) {
658 foreach ($this->exlist_iterator->current() as $date) {
659 if ($date !== null) {
660 $this->exlist_heap->insert($date);
661 }
662 }
663 $this->exlist_iterator->next(); // advance the iterator for the next call
664
665 if ($this->exlist_heap->isEmpty()) {
666 break 1; // break this loop only
667 }
668
669 $exdate = $this->exlist_heap->top();
670 if ($exdate < $occurrence) {
671 $this->exlist_heap->extract();
672 continue;
673 }
674 elseif ($exdate == $occurrence) {
675 $excluded = true;
676 break 1;
677 }
678 else {
679 break 1; // exdate is > occurrence, so we'll keep it for later
680 }
681 }
682
683 $previous_occurrence = $occurrence;
684
685 if ($excluded) {
686 continue;
687 }
688
689 $total += 1;
690 $this->cache[] = clone $occurrence;
691 yield clone $occurrence; // = yield
692 }
693
694 $this->total = $total; // save total for count cache
695 return; // stop the iterator
696 }
697 }
698