ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
RRuleIterator.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Sabre\VObject\Recur;
4 
7 use Iterator;
11 
25 class RRuleIterator implements Iterator {
26 
33  function __construct($rrule, DateTimeInterface $start) {
34 
35  $this->startDate = $start;
36  $this->parseRRule($rrule);
37  $this->currentDate = clone $this->startDate;
38 
39  }
40 
41  /* Implementation of the Iterator interface {{{ */
42 
43  function current() {
44 
45  if (!$this->valid()) return;
46  return clone $this->currentDate;
47 
48  }
49 
55  function key() {
56 
57  return $this->counter;
58 
59  }
60 
68  function valid() {
69 
70  if (!is_null($this->count)) {
71  return $this->counter < $this->count;
72  }
73  return is_null($this->until) || $this->currentDate <= $this->until;
74 
75  }
76 
82  function rewind() {
83 
84  $this->currentDate = clone $this->startDate;
85  $this->counter = 0;
86 
87  }
88 
94  function next() {
95 
96  // Otherwise, we find the next event in the normal RRULE
97  // sequence.
98  switch ($this->frequency) {
99 
100  case 'hourly' :
101  $this->nextHourly();
102  break;
103 
104  case 'daily' :
105  $this->nextDaily();
106  break;
107 
108  case 'weekly' :
109  $this->nextWeekly();
110  break;
111 
112  case 'monthly' :
113  $this->nextMonthly();
114  break;
115 
116  case 'yearly' :
117  $this->nextYearly();
118  break;
119 
120  }
121  $this->counter++;
122 
123  }
124 
125  /* End of Iterator implementation }}} */
126 
132  function isInfinite() {
133 
134  return !$this->count && !$this->until;
135 
136  }
137 
147 
148  while ($this->valid() && $this->currentDate < $dt) {
149  $this->next();
150  }
151 
152  }
153 
161  protected $startDate;
162 
169  protected $currentDate;
170 
177  protected $frequency;
178 
184  protected $count;
185 
194  protected $interval = 1;
195 
201  protected $until;
202 
210  protected $bySecond;
211 
219  protected $byMinute;
220 
228  protected $byHour;
229 
237  protected $counter = 0;
238 
251  protected $byDay;
252 
261  protected $byMonthDay;
262 
272  protected $byYearDay;
273 
282  protected $byWeekNo;
283 
291  protected $byMonth;
292 
307  protected $bySetPos;
308 
314  protected $weekStart = 'MO';
315 
316  /* Functions that advance the iterator {{{ */
317 
323  protected function nextHourly() {
324 
325  $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' hours');
326 
327  }
328 
334  protected function nextDaily() {
335 
336  if (!$this->byHour && !$this->byDay) {
337  $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' days');
338  return;
339  }
340 
341  if (!empty($this->byHour)) {
342  $recurrenceHours = $this->getHours();
343  }
344 
345  if (!empty($this->byDay)) {
346  $recurrenceDays = $this->getDays();
347  }
348 
349  if (!empty($this->byMonth)) {
350  $recurrenceMonths = $this->getMonths();
351  }
352 
353  do {
354  if ($this->byHour) {
355  if ($this->currentDate->format('G') == '23') {
356  // to obey the interval rule
357  $this->currentDate = $this->currentDate->modify('+' . $this->interval - 1 . ' days');
358  }
359 
360  $this->currentDate = $this->currentDate->modify('+1 hours');
361 
362  } else {
363  $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' days');
364 
365  }
366 
367  // Current month of the year
368  $currentMonth = $this->currentDate->format('n');
369 
370  // Current day of the week
371  $currentDay = $this->currentDate->format('w');
372 
373  // Current hour of the day
374  $currentHour = $this->currentDate->format('G');
375 
376  } while (
377  ($this->byDay && !in_array($currentDay, $recurrenceDays)) ||
378  ($this->byHour && !in_array($currentHour, $recurrenceHours)) ||
379  ($this->byMonth && !in_array($currentMonth, $recurrenceMonths))
380  );
381 
382  }
383 
389  protected function nextWeekly() {
390 
391  if (!$this->byHour && !$this->byDay) {
392  $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' weeks');
393  return;
394  }
395 
396  if ($this->byHour) {
397  $recurrenceHours = $this->getHours();
398  }
399 
400  if ($this->byDay) {
401  $recurrenceDays = $this->getDays();
402  }
403 
404  // First day of the week:
405  $firstDay = $this->dayMap[$this->weekStart];
406 
407  do {
408 
409  if ($this->byHour) {
410  $this->currentDate = $this->currentDate->modify('+1 hours');
411  } else {
412  $this->currentDate = $this->currentDate->modify('+1 days');
413  }
414 
415  // Current day of the week
416  $currentDay = (int)$this->currentDate->format('w');
417 
418  // Current hour of the day
419  $currentHour = (int)$this->currentDate->format('G');
420 
421  // We need to roll over to the next week
422  if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
423  $this->currentDate = $this->currentDate->modify('+' . $this->interval - 1 . ' weeks');
424 
425  // We need to go to the first day of this week, but only if we
426  // are not already on this first day of this week.
427  if ($this->currentDate->format('w') != $firstDay) {
428  $this->currentDate = $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
429  }
430  }
431 
432  // We have a match
433  } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
434  }
435 
441  protected function nextMonthly() {
442 
443  $currentDayOfMonth = $this->currentDate->format('j');
444  if (!$this->byMonthDay && !$this->byDay) {
445 
446  // If the current day is higher than the 28th, rollover can
447  // occur to the next month. We Must skip these invalid
448  // entries.
449  if ($currentDayOfMonth < 29) {
450  $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' months');
451  } else {
452  $increase = 0;
453  do {
454  $increase++;
455  $tempDate = clone $this->currentDate;
456  $tempDate = $tempDate->modify('+ ' . ($this->interval * $increase) . ' months');
457  } while ($tempDate->format('j') != $currentDayOfMonth);
458  $this->currentDate = $tempDate;
459  }
460  return;
461  }
462 
463  while (true) {
464 
465  $occurrences = $this->getMonthlyOccurrences();
466 
467  foreach ($occurrences as $occurrence) {
468 
469  // The first occurrence thats higher than the current
470  // day of the month wins.
471  if ($occurrence > $currentDayOfMonth) {
472  break 2;
473  }
474 
475  }
476 
477  // If we made it all the way here, it means there were no
478  // valid occurrences, and we need to advance to the next
479  // month.
480  //
481  // This line does not currently work in hhvm. Temporary workaround
482  // follows:
483  // $this->currentDate->modify('first day of this month');
484  $this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone());
485  // end of workaround
486  $this->currentDate = $this->currentDate->modify('+ ' . $this->interval . ' months');
487 
488  // This goes to 0 because we need to start counting at the
489  // beginning.
490  $currentDayOfMonth = 0;
491 
492  }
493 
494  $this->currentDate = $this->currentDate->setDate(
495  (int)$this->currentDate->format('Y'),
496  (int)$this->currentDate->format('n'),
497  (int)$occurrence
498  );
499 
500  }
501 
507  protected function nextYearly() {
508 
509  $currentMonth = $this->currentDate->format('n');
510  $currentYear = $this->currentDate->format('Y');
511  $currentDayOfMonth = $this->currentDate->format('j');
512 
513  // No sub-rules, so we just advance by year
514  if (empty($this->byMonth)) {
515 
516  // Unless it was a leap day!
517  if ($currentMonth == 2 && $currentDayOfMonth == 29) {
518 
519  $counter = 0;
520  do {
521  $counter++;
522  // Here we increase the year count by the interval, until
523  // we hit a date that's also in a leap year.
524  //
525  // We could just find the next interval that's dividable by
526  // 4, but that would ignore the rule that there's no leap
527  // year every year that's dividable by a 100, but not by
528  // 400. (1800, 1900, 2100). So we just rely on the datetime
529  // functions instead.
530  $nextDate = clone $this->currentDate;
531  $nextDate = $nextDate->modify('+ ' . ($this->interval * $counter) . ' years');
532  } while ($nextDate->format('n') != 2);
533 
534  $this->currentDate = $nextDate;
535 
536  return;
537 
538  }
539 
540  if ($this->byWeekNo !== null) { // byWeekNo is an array with values from -53 to -1, or 1 to 53
541  $dayOffsets = [];
542  if ($this->byDay) {
543  foreach ($this->byDay as $byDay) {
544  $dayOffsets[] = $this->dayMap[$byDay];
545  }
546  } else { // default is Monday
547  $dayOffsets[] = 1;
548  }
549 
550  $currentYear = $this->currentDate->format('Y');
551 
552  while (true) {
553  $checkDates = [];
554 
555  // loop through all WeekNo and Days to check all the combinations
556  foreach ($this->byWeekNo as $byWeekNo) {
557  foreach ($dayOffsets as $dayOffset) {
558  $date = clone $this->currentDate;
559  $date->setISODate($currentYear, $byWeekNo, $dayOffset);
560 
561  if ($date > $this->currentDate) {
562  $checkDates[] = $date;
563  }
564  }
565  }
566 
567  if (count($checkDates) > 0) {
568  $this->currentDate = min($checkDates);
569  return;
570  }
571 
572  // if there is no date found, check the next year
573  $currentYear += $this->interval;
574  }
575  }
576 
577  if ($this->byYearDay !== null) { // byYearDay is an array with values from -366 to -1, or 1 to 366
578  $dayOffsets = [];
579  if ($this->byDay) {
580  foreach ($this->byDay as $byDay) {
581  $dayOffsets[] = $this->dayMap[$byDay];
582  }
583  } else { // default is Monday-Sunday
584  $dayOffsets = [1,2,3,4,5,6,7];
585  }
586 
587  $currentYear = $this->currentDate->format('Y');
588 
589  while (true) {
590  $checkDates = [];
591 
592  // loop through all YearDay and Days to check all the combinations
593  foreach ($this->byYearDay as $byYearDay) {
594  $date = clone $this->currentDate;
595  $date = $date->setDate($currentYear, 1, 1);
596  if ($byYearDay > 0) {
597  $date = $date->add(new \DateInterval('P' . $byYearDay . 'D'));
598  } else {
599  $date = $date->sub(new \DateInterval('P' . abs($byYearDay) . 'D'));
600  }
601 
602  if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) {
603  $checkDates[] = $date;
604  }
605  }
606 
607  if (count($checkDates) > 0) {
608  $this->currentDate = min($checkDates);
609  return;
610  }
611 
612  // if there is no date found, check the next year
613  $currentYear += $this->interval;
614  }
615  }
616 
617  // The easiest form
618  $this->currentDate = $this->currentDate->modify('+' . $this->interval . ' years');
619  return;
620 
621  }
622 
623  $currentMonth = $this->currentDate->format('n');
624  $currentYear = $this->currentDate->format('Y');
625  $currentDayOfMonth = $this->currentDate->format('j');
626 
627  $advancedToNewMonth = false;
628 
629  // If we got a byDay or getMonthDay filter, we must first expand
630  // further.
631  if ($this->byDay || $this->byMonthDay) {
632 
633  while (true) {
634 
635  $occurrences = $this->getMonthlyOccurrences();
636 
637  foreach ($occurrences as $occurrence) {
638 
639  // The first occurrence that's higher than the current
640  // day of the month wins.
641  // If we advanced to the next month or year, the first
642  // occurrence is always correct.
643  if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
644  break 2;
645  }
646 
647  }
648 
649  // If we made it here, it means we need to advance to
650  // the next month or year.
651  $currentDayOfMonth = 1;
652  $advancedToNewMonth = true;
653  do {
654 
655  $currentMonth++;
656  if ($currentMonth > 12) {
657  $currentYear += $this->interval;
658  $currentMonth = 1;
659  }
660  } while (!in_array($currentMonth, $this->byMonth));
661 
662  $this->currentDate = $this->currentDate->setDate(
663  (int)$currentYear,
664  (int)$currentMonth,
665  (int)$currentDayOfMonth
666  );
667 
668  }
669 
670  // If we made it here, it means we got a valid occurrence
671  $this->currentDate = $this->currentDate->setDate(
672  (int)$currentYear,
673  (int)$currentMonth,
674  (int)$occurrence
675  );
676  return;
677 
678  } else {
679 
680  // These are the 'byMonth' rules, if there are no byDay or
681  // byMonthDay sub-rules.
682  do {
683 
684  $currentMonth++;
685  if ($currentMonth > 12) {
686  $currentYear += $this->interval;
687  $currentMonth = 1;
688  }
689  } while (!in_array($currentMonth, $this->byMonth));
690  $this->currentDate = $this->currentDate->setDate(
691  (int)$currentYear,
692  (int)$currentMonth,
693  (int)$currentDayOfMonth
694  );
695 
696  return;
697 
698  }
699 
700  }
701 
702  /* }}} */
703 
712  protected function parseRRule($rrule) {
713 
714  if (is_string($rrule)) {
716  }
717 
718  foreach ($rrule as $key => $value) {
719 
720  $key = strtoupper($key);
721  switch ($key) {
722 
723  case 'FREQ' :
724  $value = strtolower($value);
725  if (!in_array(
726  $value,
727  ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly']
728  )) {
729  throw new InvalidDataException('Unknown value for FREQ=' . strtoupper($value));
730  }
731  $this->frequency = $value;
732  break;
733 
734  case 'UNTIL' :
735  $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone());
736 
737  // In some cases events are generated with an UNTIL=
738  // parameter before the actual start of the event.
739  //
740  // Not sure why this is happening. We assume that the
741  // intention was that the event only recurs once.
742  //
743  // So we are modifying the parameter so our code doesn't
744  // break.
745  if ($this->until < $this->startDate) {
746  $this->until = $this->startDate;
747  }
748  break;
749 
750  case 'INTERVAL' :
751  // No break
752 
753  case 'COUNT' :
754  $val = (int)$value;
755  if ($val < 1) {
756  throw new InvalidDataException(strtoupper($key) . ' in RRULE must be a positive integer!');
757  }
758  $key = strtolower($key);
759  $this->$key = $val;
760  break;
761 
762  case 'BYSECOND' :
763  $this->bySecond = (array)$value;
764  break;
765 
766  case 'BYMINUTE' :
767  $this->byMinute = (array)$value;
768  break;
769 
770  case 'BYHOUR' :
771  $this->byHour = (array)$value;
772  break;
773 
774  case 'BYDAY' :
775  $value = (array)$value;
776  foreach ($value as $part) {
777  if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) {
778  throw new InvalidDataException('Invalid part in BYDAY clause: ' . $part);
779  }
780  }
781  $this->byDay = $value;
782  break;
783 
784  case 'BYMONTHDAY' :
785  $this->byMonthDay = (array)$value;
786  break;
787 
788  case 'BYYEARDAY' :
789  $this->byYearDay = (array)$value;
790  foreach ($this->byYearDay as $byYearDay) {
791  if (!is_numeric($byYearDay) || (int)$byYearDay < -366 || (int)$byYearDay == 0 || (int)$byYearDay > 366) {
792  throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!');
793  }
794  }
795  break;
796 
797  case 'BYWEEKNO' :
798  $this->byWeekNo = (array)$value;
799  foreach ($this->byWeekNo as $byWeekNo) {
800  if (!is_numeric($byWeekNo) || (int)$byWeekNo < -53 || (int)$byWeekNo == 0 || (int)$byWeekNo > 53) {
801  throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!');
802  }
803  }
804  break;
805 
806  case 'BYMONTH' :
807  $this->byMonth = (array)$value;
808  foreach ($this->byMonth as $byMonth) {
809  if (!is_numeric($byMonth) || (int)$byMonth < 1 || (int)$byMonth > 12) {
810  throw new InvalidDataException('BYMONTH in RRULE must have value(s) betweeen 1 and 12!');
811  }
812  }
813  break;
814 
815  case 'BYSETPOS' :
816  $this->bySetPos = (array)$value;
817  break;
818 
819  case 'WKST' :
820  $this->weekStart = strtoupper($value);
821  break;
822 
823  default:
824  throw new InvalidDataException('Not supported: ' . strtoupper($key));
825 
826  }
827 
828  }
829 
830  }
831 
837  protected $dayNames = [
838  0 => 'Sunday',
839  1 => 'Monday',
840  2 => 'Tuesday',
841  3 => 'Wednesday',
842  4 => 'Thursday',
843  5 => 'Friday',
844  6 => 'Saturday',
845  ];
846 
855  protected function getMonthlyOccurrences() {
856 
858 
859  $byDayResults = [];
860 
861  // Our strategy is to simply go through the byDays, advance the date to
862  // that point and add it to the results.
863  if ($this->byDay) foreach ($this->byDay as $day) {
864 
865  $dayName = $this->dayNames[$this->dayMap[substr($day, -2)]];
866 
867 
868  // Dayname will be something like 'wednesday'. Now we need to find
869  // all wednesdays in this month.
870  $dayHits = [];
871 
872  // workaround for missing 'first day of the month' support in hhvm
873  $checkDate = new \DateTime($startDate->format('Y-m-1'));
874  // workaround modify always advancing the date even if the current day is a $dayName in hhvm
875  if ($checkDate->format('l') !== $dayName) {
876  $checkDate = $checkDate->modify($dayName);
877  }
878 
879  do {
880  $dayHits[] = $checkDate->format('j');
881  $checkDate = $checkDate->modify('next ' . $dayName);
882  } while ($checkDate->format('n') === $startDate->format('n'));
883 
884  // So now we have 'all wednesdays' for month. It is however
885  // possible that the user only really wanted the 1st, 2nd or last
886  // wednesday.
887  if (strlen($day) > 2) {
888  $offset = (int)substr($day, 0, -2);
889 
890  if ($offset > 0) {
891  // It is possible that the day does not exist, such as a
892  // 5th or 6th wednesday of the month.
893  if (isset($dayHits[$offset - 1])) {
894  $byDayResults[] = $dayHits[$offset - 1];
895  }
896  } else {
897 
898  // if it was negative we count from the end of the array
899  // might not exist, fx. -5th tuesday
900  if (isset($dayHits[count($dayHits) + $offset])) {
901  $byDayResults[] = $dayHits[count($dayHits) + $offset];
902  }
903  }
904  } else {
905  // There was no counter (first, second, last wednesdays), so we
906  // just need to add the all to the list).
907  $byDayResults = array_merge($byDayResults, $dayHits);
908 
909  }
910 
911  }
912 
913  $byMonthDayResults = [];
914  if ($this->byMonthDay) foreach ($this->byMonthDay as $monthDay) {
915 
916  // Removing values that are out of range for this month
917  if ($monthDay > $startDate->format('t') ||
918  $monthDay < 0 - $startDate->format('t')) {
919  continue;
920  }
921  if ($monthDay > 0) {
922  $byMonthDayResults[] = $monthDay;
923  } else {
924  // Negative values
925  $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
926  }
927  }
928 
929  // If there was just byDay or just byMonthDay, they just specify our
930  // (almost) final list. If both were provided, then byDay limits the
931  // list.
932  if ($this->byMonthDay && $this->byDay) {
933  $result = array_intersect($byMonthDayResults, $byDayResults);
934  } elseif ($this->byMonthDay) {
935  $result = $byMonthDayResults;
936  } else {
937  $result = $byDayResults;
938  }
939  $result = array_unique($result);
940  sort($result, SORT_NUMERIC);
941 
942  // The last thing that needs checking is the BYSETPOS. If it's set, it
943  // means only certain items in the set survive the filter.
944  if (!$this->bySetPos) {
945  return $result;
946  }
947 
948  $filteredResult = [];
949  foreach ($this->bySetPos as $setPos) {
950 
951  if ($setPos < 0) {
952  $setPos = count($result) + ($setPos + 1);
953  }
954  if (isset($result[$setPos - 1])) {
955  $filteredResult[] = $result[$setPos - 1];
956  }
957  }
958 
959  sort($filteredResult, SORT_NUMERIC);
960  return $filteredResult;
961 
962  }
963 
969  protected $dayMap = [
970  'SU' => 0,
971  'MO' => 1,
972  'TU' => 2,
973  'WE' => 3,
974  'TH' => 4,
975  'FR' => 5,
976  'SA' => 6,
977  ];
978 
979  protected function getHours() {
980 
981  $recurrenceHours = [];
982  foreach ($this->byHour as $byHour) {
983  $recurrenceHours[] = $byHour;
984  }
985 
986  return $recurrenceHours;
987  }
988 
989  protected function getDays() {
990 
991  $recurrenceDays = [];
992  foreach ($this->byDay as $byDay) {
993 
994  // The day may be preceeded with a positive (+n) or
995  // negative (-n) integer. However, this does not make
996  // sense in 'weekly' so we ignore it here.
997  $recurrenceDays[] = $this->dayMap[substr($byDay, -2)];
998 
999  }
1000 
1001  return $recurrenceDays;
1002  }
1003 
1004  protected function getMonths() {
1005 
1006  $recurrenceMonths = [];
1007  foreach ($this->byMonth as $byMonth) {
1008  $recurrenceMonths[] = $byMonth;
1009  }
1010 
1011  return $recurrenceMonths;
1012  }
1013 }
getMonthlyOccurrences()
Returns all the occurrences for a monthly frequency with a &#39;byDay&#39; or &#39;byMonthDay&#39; expansion for the ...
isInfinite()
Returns true if this recurring event never ends.
static parse($date, $referenceTz=null)
Parses either a Date or DateTime, or Duration value.
rewind()
Resets the iterator.
nextYearly()
Does the processing for advancing the iterator for yearly frequency.
nextDaily()
Does the processing for advancing the iterator for daily frequency.
$result
nextMonthly()
Does the processing for advancing the iterator for monthly frequency.
nextHourly()
Does the processing for advancing the iterator for hourly frequency.
key()
Returns the current item number.
next()
Goes on to the next iteration.
parseRRule($rrule)
This method receives a string from an RRULE property, and populates this class with all the values...
$start
Definition: bench.php:8
__construct($rrule, DateTimeInterface $start)
Creates the Iterator.
static stringToArray($value)
Parses an RRULE value string, and turns it into a struct-ish array.
Definition: Recur.php:209
fastForward(DateTimeInterface $dt)
This method allows you to quickly go to the next occurrence after the specified date.
valid()
Returns whether the current item is a valid item for the recurrence iterator.
$key
Definition: croninfo.php:18
nextWeekly()
Does the processing for advancing the iterator for weekly frequency.
This exception is thrown whenever an invalid value is found anywhere in a iCalendar or vCard object...