]> git.mxchange.org Git - friendica-addons.git/blob - dav/sabre-vobject/lib/Sabre/VObject/RecurrenceIterator.php
Second part of refactoring; should be runnable again, yet not thoroughly tested
[friendica-addons.git] / dav / sabre-vobject / lib / Sabre / VObject / RecurrenceIterator.php
1 <?php
2
3 namespace Sabre\VObject;
4
5 /**
6  * This class is used to determine new for a recurring event, when the next
7  * events occur.
8  *
9  * This iterator may loop infinitely in the future, therefore it is important
10  * that if you use this class, you set hard limits for the amount of iterations
11  * you want to handle.
12  *
13  * Note that currently there is not full support for the entire iCalendar
14  * specification, as it's very complex and contains a lot of permutations
15  * that's not yet used very often in software.
16  *
17  * For the focus has been on features as they actually appear in Calendaring
18  * software, but this may well get expanded as needed / on demand
19  *
20  * The following RRULE properties are supported
21  *   * UNTIL
22  *   * INTERVAL
23  *   * COUNT
24  *   * FREQ=DAILY
25  *     * BYDAY
26  *   * FREQ=WEEKLY
27  *     * BYDAY
28  *     * WKST
29  *   * FREQ=MONTHLY
30  *     * BYMONTHDAY
31  *     * BYDAY
32  *     * BYSETPOS
33  *   * FREQ=YEARLY
34  *     * BYMONTH
35  *     * BYMONTHDAY (only if BYMONTH is also set)
36  *     * BYDAY (only if BYMONTH is also set)
37  *
38  * Anything beyond this is 'undefined', which means that it may get ignored, or
39  * you may get unexpected results. The effect is that in some applications the
40  * specified recurrence may look incorrect, or is missing.
41  *
42  * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
43  * @author Evert Pot (http://www.rooftopsolutions.nl/)
44  * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
45  */
46 class RecurrenceIterator implements \Iterator {
47
48     /**
49      * The initial event date
50      *
51      * @var DateTime
52      */
53     public $startDate;
54
55     /**
56      * The end-date of the initial event
57      *
58      * @var DateTime
59      */
60     public $endDate;
61
62     /**
63      * The 'current' recurrence.
64      *
65      * This will be increased for every iteration.
66      *
67      * @var DateTime
68      */
69     public $currentDate;
70
71
72     /**
73      * List of dates that are excluded from the rules.
74      *
75      * This list contains the items that have been overriden by the EXDATE
76      * property.
77      *
78      * @var array
79      */
80     public $exceptionDates = array();
81
82     /**
83      * Base event
84      *
85      * @var Component\VEvent
86      */
87     public $baseEvent;
88
89     /**
90      * List of dates that are overridden by other events.
91      * Similar to $overriddenEvents, but this just contains the original dates.
92      *
93      * @var array
94      */
95     public $overriddenDates = array();
96
97     /**
98      * list of events that are 'overridden'.
99      *
100      * This is an array of Component\VEvent objects.
101      *
102      * @var array
103      */
104     public $overriddenEvents = array();
105
106
107     /**
108      * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
109      * yearly.
110      *
111      * @var string
112      */
113     public $frequency;
114
115     /**
116      * The last instance of this recurrence, inclusively
117      *
118      * @var DateTime|null
119      */
120     public $until;
121
122     /**
123      * The number of recurrences, or 'null' if infinitely recurring.
124      *
125      * @var int
126      */
127     public $count;
128
129     /**
130      * The interval.
131      *
132      * If for example frequency is set to daily, interval = 2 would mean every
133      * 2 days.
134      *
135      * @var int
136      */
137     public $interval = 1;
138
139     /**
140      * Which seconds to recur.
141      *
142      * This is an array of integers (between 0 and 60)
143      *
144      * @var array
145      */
146     public $bySecond;
147
148     /**
149      * Which minutes to recur
150      *
151      * This is an array of integers (between 0 and 59)
152      *
153      * @var array
154      */
155     public $byMinute;
156
157     /**
158      * Which hours to recur
159      *
160      * This is an array of integers (between 0 and 23)
161      *
162      * @var array
163      */
164     public $byHour;
165
166     /**
167      * Which weekdays to recur.
168      *
169      * This is an array of weekdays
170      *
171      * This may also be preceeded by a positive or negative integer. If present,
172      * this indicates the nth occurrence of a specific day within the monthly or
173      * yearly rrule. For instance, -2TU indicates the second-last tuesday of
174      * the month, or year.
175      *
176      * @var array
177      */
178     public $byDay;
179
180     /**
181      * Which days of the month to recur
182      *
183      * This is an array of days of the months (1-31). The value can also be
184      * negative. -5 for instance means the 5th last day of the month.
185      *
186      * @var array
187      */
188     public $byMonthDay;
189
190     /**
191      * Which days of the year to recur.
192      *
193      * This is an array with days of the year (1 to 366). The values can also
194      * be negative. For instance, -1 will always represent the last day of the
195      * year. (December 31st).
196      *
197      * @var array
198      */
199     public $byYearDay;
200
201     /**
202      * Which week numbers to recur.
203      *
204      * This is an array of integers from 1 to 53. The values can also be
205      * negative. -1 will always refer to the last week of the year.
206      *
207      * @var array
208      */
209     public $byWeekNo;
210
211     /**
212      * Which months to recur
213      *
214      * This is an array of integers from 1 to 12.
215      *
216      * @var array
217      */
218     public $byMonth;
219
220     /**
221      * Which items in an existing st to recur.
222      *
223      * These numbers work together with an existing by* rule. It specifies
224      * exactly which items of the existing by-rule to filter.
225      *
226      * Valid values are 1 to 366 and -1 to -366. As an example, this can be
227      * used to recur the last workday of the month.
228      *
229      * This would be done by setting frequency to 'monthly', byDay to
230      * 'MO,TU,WE,TH,FR' and bySetPos to -1.
231      *
232      * @var array
233      */
234     public $bySetPos;
235
236     /**
237      * When a week starts
238      *
239      * @var string
240      */
241     public $weekStart = 'MO';
242
243     /**
244      * The current item in the list
245      *
246      * @var int
247      */
248     public $counter = 0;
249
250     /**
251      * Simple mapping from iCalendar day names to day numbers
252      *
253      * @var array
254      */
255     private $dayMap = array(
256         'SU' => 0,
257         'MO' => 1,
258         'TU' => 2,
259         'WE' => 3,
260         'TH' => 4,
261         'FR' => 5,
262         'SA' => 6,
263     );
264
265     /**
266      * Mappings between the day number and english day name.
267      *
268      * @var array
269      */
270     private $dayNames = array(
271         0 => 'Sunday',
272         1 => 'Monday',
273         2 => 'Tuesday',
274         3 => 'Wednesday',
275         4 => 'Thursday',
276         5 => 'Friday',
277         6 => 'Saturday',
278     );
279
280     /**
281      * If the current iteration of the event is an overriden event, this
282      * property will hold the VObject
283      *
284      * @var Component
285      */
286     private $currentOverriddenEvent;
287
288     /**
289      * This property may contain the date of the next not-overridden event.
290      * This date is calculated sometimes a bit early, before overridden events
291      * are evaluated.
292      *
293      * @var DateTime
294      */
295     private $nextDate;
296
297     /**
298      * Creates the iterator
299      *
300      * You should pass a VCALENDAR component, as well as the UID of the event
301      * we're going to traverse.
302      *
303      * @param Component $vcal
304      * @param string|null $uid
305      */
306     public function __construct(Component $vcal, $uid=null) {
307
308         if (is_null($uid)) {
309             if ($vcal->name === 'VCALENDAR') {
310                 throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
311             }
312             $components = array($vcal);
313             $uid = (string)$vcal->uid;
314         } else {
315             $components = $vcal->select('VEVENT');
316         }
317         foreach($components as $component) {
318             if ((string)$component->uid == $uid) {
319                 if (isset($component->{'RECURRENCE-ID'})) {
320                     $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component;
321                     $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime();
322                 } else {
323                     $this->baseEvent = $component;
324                 }
325             }
326         }
327         if (!$this->baseEvent) {
328             throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid);
329         }
330
331         $this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
332
333         $this->endDate = null;
334         if (isset($this->baseEvent->DTEND)) {
335             $this->endDate = clone $this->baseEvent->DTEND->getDateTime();
336         } else {
337             $this->endDate = clone $this->startDate;
338             if (isset($this->baseEvent->DURATION)) {
339                 $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value));
340             } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) {
341                 $this->endDate->modify('+1 day');
342             }
343         }
344         $this->currentDate = clone $this->startDate;
345
346         $rrule = (string)$this->baseEvent->RRULE;
347
348         $parts = explode(';', $rrule);
349
350         // If no rrule was specified, we create a default setting
351         if (!$rrule) {
352             $this->frequency = 'daily';
353             $this->count = 1;
354         } else foreach($parts as $part) {
355
356             list($key, $value) = explode('=', $part, 2);
357
358             switch(strtoupper($key)) {
359
360                 case 'FREQ' :
361                     if (!in_array(
362                         strtolower($value),
363                         array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
364                     )) {
365                         throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
366
367                     }
368                     $this->frequency = strtolower($value);
369                     break;
370
371                 case 'UNTIL' :
372                     $this->until = DateTimeParser::parse($value);
373                     break;
374
375                 case 'COUNT' :
376                     $this->count = (int)$value;
377                     break;
378
379                 case 'INTERVAL' :
380                     $this->interval = (int)$value;
381                     break;
382
383                 case 'BYSECOND' :
384                     $this->bySecond = explode(',', $value);
385                     break;
386
387                 case 'BYMINUTE' :
388                     $this->byMinute = explode(',', $value);
389                     break;
390
391                 case 'BYHOUR' :
392                     $this->byHour = explode(',', $value);
393                     break;
394
395                 case 'BYDAY' :
396                     $this->byDay = explode(',', strtoupper($value));
397                     break;
398
399                 case 'BYMONTHDAY' :
400                     $this->byMonthDay = explode(',', $value);
401                     break;
402
403                 case 'BYYEARDAY' :
404                     $this->byYearDay = explode(',', $value);
405                     break;
406
407                 case 'BYWEEKNO' :
408                     $this->byWeekNo = explode(',', $value);
409                     break;
410
411                 case 'BYMONTH' :
412                     $this->byMonth = explode(',', $value);
413                     break;
414
415                 case 'BYSETPOS' :
416                     $this->bySetPos = explode(',', $value);
417                     break;
418
419                 case 'WKST' :
420                     $this->weekStart = strtoupper($value);
421                     break;
422
423             }
424
425         }
426
427         // Parsing exception dates
428         if (isset($this->baseEvent->EXDATE)) {
429             foreach($this->baseEvent->EXDATE as $exDate) {
430
431                 foreach(explode(',', (string)$exDate) as $exceptionDate) {
432
433                     $this->exceptionDates[] =
434                         DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
435
436                 }
437
438             }
439
440         }
441
442     }
443
444     /**
445      * Returns the current item in the list
446      *
447      * @return DateTime
448      */
449     public function current() {
450
451         if (!$this->valid()) return null;
452         return clone $this->currentDate;
453
454     }
455
456     /**
457      * This method returns the startdate for the current iteration of the
458      * event.
459      *
460      * @return DateTime
461      */
462     public function getDtStart() {
463
464         if (!$this->valid()) return null;
465         return clone $this->currentDate;
466
467     }
468
469     /**
470      * This method returns the enddate for the current iteration of the
471      * event.
472      *
473      * @return DateTime
474      */
475     public function getDtEnd() {
476
477         if (!$this->valid()) return null;
478         $dtEnd = clone $this->currentDate;
479         $dtEnd->add( $this->startDate->diff( $this->endDate ) );
480         return clone $dtEnd;
481
482     }
483
484     /**
485      * Returns a VEVENT object with the updated start and end date.
486      *
487      * Any recurrence information is removed, and this function may return an
488      * 'overridden' event instead.
489      *
490      * This method always returns a cloned instance.
491      *
492      * @return Component\VEvent
493      */
494     public function getEventObject() {
495
496         if ($this->currentOverriddenEvent) {
497             return clone $this->currentOverriddenEvent;
498         }
499         $event = clone $this->baseEvent;
500         unset($event->RRULE);
501         unset($event->EXDATE);
502         unset($event->RDATE);
503         unset($event->EXRULE);
504
505         $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
506         if (isset($event->DTEND)) {
507             $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
508         }
509         if ($this->counter > 0) {
510             $event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
511         }
512
513         return $event;
514
515     }
516
517     /**
518      * Returns the current item number
519      *
520      * @return int
521      */
522     public function key() {
523
524         return $this->counter;
525
526     }
527
528     /**
529      * Whether or not there is a 'next item'
530      *
531      * @return bool
532      */
533     public function valid() {
534
535         if (!is_null($this->count)) {
536             return $this->counter < $this->count;
537         }
538         if (!is_null($this->until)) {
539             return $this->currentDate <= $this->until;
540         }
541         return true;
542
543     }
544
545     /**
546      * Resets the iterator
547      *
548      * @return void
549      */
550     public function rewind() {
551
552         $this->currentDate = clone $this->startDate;
553         $this->counter = 0;
554
555     }
556
557     /**
558      * This method allows you to quickly go to the next occurrence after the
559      * specified date.
560      *
561      * Note that this checks the current 'endDate', not the 'stardDate'. This
562      * means that if you forward to January 1st, the iterator will stop at the
563      * first event that ends *after* January 1st.
564      *
565      * @param DateTime $dt
566      * @return void
567      */
568     public function fastForward(\DateTime $dt) {
569
570         while($this->valid() && $this->getDTEnd() <= $dt) {
571             $this->next();
572         }
573
574     }
575
576     /**
577      * Returns true if this recurring event never ends.
578      *
579      * @return bool
580      */
581     public function isInfinite() {
582
583         return !$this->count && !$this->until;
584
585     }
586
587     /**
588      * Goes on to the next iteration
589      *
590      * @return void
591      */
592     public function next() {
593
594         /*
595         if (!is_null($this->count) && $this->counter >= $this->count) {
596             $this->currentDate = null;
597         }*/
598
599
600         $previousStamp = $this->currentDate->getTimeStamp();
601
602         while(true) {
603
604             $this->currentOverriddenEvent = null;
605
606             // If we have a next date 'stored', we use that
607             if ($this->nextDate) {
608                 $this->currentDate = $this->nextDate;
609                 $currentStamp = $this->currentDate->getTimeStamp();
610                 $this->nextDate = null;
611             } else {
612
613                 // Otherwise, we calculate it
614                 switch($this->frequency) {
615
616                     case 'daily' :
617                         $this->nextDaily();
618                         break;
619
620                     case 'weekly' :
621                         $this->nextWeekly();
622                         break;
623
624                     case 'monthly' :
625                         $this->nextMonthly();
626                         break;
627
628                     case 'yearly' :
629                         $this->nextYearly();
630                         break;
631
632                 }
633                 $currentStamp = $this->currentDate->getTimeStamp();
634
635                 // Checking exception dates
636                 foreach($this->exceptionDates as $exceptionDate) {
637                     if ($this->currentDate == $exceptionDate) {
638                         $this->counter++;
639                         continue 2;
640                     }
641                 }
642                 foreach($this->overriddenDates as $overriddenDate) {
643                     if ($this->currentDate == $overriddenDate) {
644                         continue 2;
645                     }
646                 }
647
648             }
649
650             // Checking overridden events
651             foreach($this->overriddenEvents as $index=>$event) {
652                 if ($index > $previousStamp && $index <= $currentStamp) {
653
654                     // We're moving the 'next date' aside, for later use.
655                     $this->nextDate = clone $this->currentDate;
656
657                     $this->currentDate = $event->DTSTART->getDateTime();
658                     $this->currentOverriddenEvent = $event;
659
660                     break;
661                 }
662             }
663
664             break;
665
666         }
667
668         /*
669         if (!is_null($this->until)) {
670             if($this->currentDate > $this->until) {
671                 $this->currentDate = null;
672             }
673         }*/
674
675         $this->counter++;
676
677     }
678
679     /**
680      * Does the processing for advancing the iterator for daily frequency.
681      *
682      * @return void
683      */
684     protected function nextDaily() {
685
686         if (!$this->byDay) {
687             $this->currentDate->modify('+' . $this->interval . ' days');
688             return;
689         }
690
691         $recurrenceDays = array();
692         foreach($this->byDay as $byDay) {
693
694             // The day may be preceeded with a positive (+n) or
695             // negative (-n) integer. However, this does not make
696             // sense in 'weekly' so we ignore it here.
697             $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
698
699         }
700
701         do {
702
703             $this->currentDate->modify('+' . $this->interval . ' days');
704
705             // Current day of the week
706             $currentDay = $this->currentDate->format('w');
707
708         } while (!in_array($currentDay, $recurrenceDays));
709
710     }
711
712     /**
713      * Does the processing for advancing the iterator for weekly frequency.
714      *
715      * @return void
716      */
717     protected function nextWeekly() {
718
719         if (!$this->byDay) {
720             $this->currentDate->modify('+' . $this->interval . ' weeks');
721             return;
722         }
723
724         $recurrenceDays = array();
725         foreach($this->byDay as $byDay) {
726
727             // The day may be preceeded with a positive (+n) or
728             // negative (-n) integer. However, this does not make
729             // sense in 'weekly' so we ignore it here.
730             $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
731
732         }
733
734         // Current day of the week
735         $currentDay = $this->currentDate->format('w');
736
737         // First day of the week:
738         $firstDay = $this->dayMap[$this->weekStart];
739
740         $time = array(
741             $this->currentDate->format('H'),
742             $this->currentDate->format('i'),
743             $this->currentDate->format('s')
744         );
745
746         // Increasing the 'current day' until we find our next
747         // occurrence.
748         while(true) {
749
750             $currentDay++;
751
752             if ($currentDay>6) {
753                 $currentDay = 0;
754             }
755
756             // We need to roll over to the next week
757             if ($currentDay === $firstDay) {
758                 $this->currentDate->modify('+' . $this->interval . ' weeks');
759
760                 // We need to go to the first day of this week, but only if we
761                 // are not already on this first day of this week.
762                 if($this->currentDate->format('w') != $firstDay) {
763                     $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
764                     $this->currentDate->setTime($time[0],$time[1],$time[2]);
765                 }
766             }
767
768             // We have a match
769             if (in_array($currentDay ,$recurrenceDays)) {
770                 $this->currentDate->modify($this->dayNames[$currentDay]);
771                 $this->currentDate->setTime($time[0],$time[1],$time[2]);
772                 break;
773             }
774
775         }
776
777     }
778
779     /**
780      * Does the processing for advancing the iterator for monthly frequency.
781      *
782      * @return void
783      */
784     protected function nextMonthly() {
785
786         $currentDayOfMonth = $this->currentDate->format('j');
787         if (!$this->byMonthDay && !$this->byDay) {
788
789             // If the current day is higher than the 28th, rollover can
790             // occur to the next month. We Must skip these invalid
791             // entries.
792             if ($currentDayOfMonth < 29) {
793                 $this->currentDate->modify('+' . $this->interval . ' months');
794             } else {
795                 $increase = 0;
796                 do {
797                     $increase++;
798                     $tempDate = clone $this->currentDate;
799                     $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
800                 } while ($tempDate->format('j') != $currentDayOfMonth);
801                 $this->currentDate = $tempDate;
802             }
803             return;
804         }
805
806         while(true) {
807
808             $occurrences = $this->getMonthlyOccurrences();
809
810             foreach($occurrences as $occurrence) {
811
812                 // The first occurrence thats higher than the current
813                 // day of the month wins.
814                 if ($occurrence > $currentDayOfMonth) {
815                     break 2;
816                 }
817
818             }
819
820             // If we made it all the way here, it means there were no
821             // valid occurrences, and we need to advance to the next
822             // month.
823             $this->currentDate->modify('first day of this month');
824             $this->currentDate->modify('+ ' . $this->interval . ' months');
825
826             // This goes to 0 because we need to start counting at hte
827             // beginning.
828             $currentDayOfMonth = 0;
829
830         }
831
832         $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
833
834     }
835
836     /**
837      * Does the processing for advancing the iterator for yearly frequency.
838      *
839      * @return void
840      */
841     protected function nextYearly() {
842
843         $currentMonth = $this->currentDate->format('n');
844         $currentYear = $this->currentDate->format('Y');
845         $currentDayOfMonth = $this->currentDate->format('j');
846
847         // No sub-rules, so we just advance by year
848         if (!$this->byMonth) {
849
850             // Unless it was a leap day!
851             if ($currentMonth==2 && $currentDayOfMonth==29) {
852
853                 $counter = 0;
854                 do {
855                     $counter++;
856                     // Here we increase the year count by the interval, until
857                     // we hit a date that's also in a leap year.
858                     //
859                     // We could just find the next interval that's dividable by
860                     // 4, but that would ignore the rule that there's no leap
861                     // year every year that's dividable by a 100, but not by
862                     // 400. (1800, 1900, 2100). So we just rely on the datetime
863                     // functions instead.
864                     $nextDate = clone $this->currentDate;
865                     $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
866                 } while ($nextDate->format('n')!=2);
867                 $this->currentDate = $nextDate;
868
869                 return;
870
871             }
872
873             // The easiest form
874             $this->currentDate->modify('+' . $this->interval . ' years');
875             return;
876
877         }
878
879         $currentMonth = $this->currentDate->format('n');
880         $currentYear = $this->currentDate->format('Y');
881         $currentDayOfMonth = $this->currentDate->format('j');
882
883         $advancedToNewMonth = false;
884
885         // If we got a byDay or getMonthDay filter, we must first expand
886         // further.
887         if ($this->byDay || $this->byMonthDay) {
888
889             while(true) {
890
891                 $occurrences = $this->getMonthlyOccurrences();
892
893                 foreach($occurrences as $occurrence) {
894
895                     // The first occurrence that's higher than the current
896                     // day of the month wins.
897                     // If we advanced to the next month or year, the first
898                     // occurrence is always correct.
899                     if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
900                         break 2;
901                     }
902
903                 }
904
905                 // If we made it here, it means we need to advance to
906                 // the next month or year.
907                 $currentDayOfMonth = 1;
908                 $advancedToNewMonth = true;
909                 do {
910
911                     $currentMonth++;
912                     if ($currentMonth>12) {
913                         $currentYear+=$this->interval;
914                         $currentMonth = 1;
915                     }
916                 } while (!in_array($currentMonth, $this->byMonth));
917
918                 $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
919
920             }
921
922             // If we made it here, it means we got a valid occurrence
923             $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
924             return;
925
926         } else {
927
928             // These are the 'byMonth' rules, if there are no byDay or
929             // byMonthDay sub-rules.
930             do {
931
932                 $currentMonth++;
933                 if ($currentMonth>12) {
934                     $currentYear+=$this->interval;
935                     $currentMonth = 1;
936                 }
937             } while (!in_array($currentMonth, $this->byMonth));
938             $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
939
940             return;
941
942         }
943
944     }
945
946     /**
947      * Returns all the occurrences for a monthly frequency with a 'byDay' or
948      * 'byMonthDay' expansion for the current month.
949      *
950      * The returned list is an array of integers with the day of month (1-31).
951      *
952      * @return array
953      */
954     protected function getMonthlyOccurrences() {
955
956         $startDate = clone $this->currentDate;
957
958         $byDayResults = array();
959
960         // Our strategy is to simply go through the byDays, advance the date to
961         // that point and add it to the results.
962         if ($this->byDay) foreach($this->byDay as $day) {
963
964             $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
965
966             // Dayname will be something like 'wednesday'. Now we need to find
967             // all wednesdays in this month.
968             $dayHits = array();
969
970             $checkDate = clone $startDate;
971             $checkDate->modify('first day of this month');
972             $checkDate->modify($dayName);
973
974             do {
975                 $dayHits[] = $checkDate->format('j');
976                 $checkDate->modify('next ' . $dayName);
977             } while ($checkDate->format('n') === $startDate->format('n'));
978
979             // So now we have 'all wednesdays' for month. It is however
980             // possible that the user only really wanted the 1st, 2nd or last
981             // wednesday.
982             if (strlen($day)>2) {
983                 $offset = (int)substr($day,0,-2);
984
985                 if ($offset>0) {
986                     // It is possible that the day does not exist, such as a
987                     // 5th or 6th wednesday of the month.
988                     if (isset($dayHits[$offset-1])) {
989                         $byDayResults[] = $dayHits[$offset-1];
990                     }
991                 } else {
992
993                     // if it was negative we count from the end of the array
994                     $byDayResults[] = $dayHits[count($dayHits) + $offset];
995                 }
996             } else {
997                 // There was no counter (first, second, last wednesdays), so we
998                 // just need to add the all to the list).
999                 $byDayResults = array_merge($byDayResults, $dayHits);
1000
1001             }
1002
1003         }
1004
1005         $byMonthDayResults = array();
1006         if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
1007
1008             // Removing values that are out of range for this month
1009             if ($monthDay > $startDate->format('t') ||
1010                 $monthDay < 0-$startDate->format('t')) {
1011                     continue;
1012             }
1013             if ($monthDay>0) {
1014                 $byMonthDayResults[] = $monthDay;
1015             } else {
1016                 // Negative values
1017                 $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
1018             }
1019         }
1020
1021         // If there was just byDay or just byMonthDay, they just specify our
1022         // (almost) final list. If both were provided, then byDay limits the
1023         // list.
1024         if ($this->byMonthDay && $this->byDay) {
1025             $result = array_intersect($byMonthDayResults, $byDayResults);
1026         } elseif ($this->byMonthDay) {
1027             $result = $byMonthDayResults;
1028         } else {
1029             $result = $byDayResults;
1030         }
1031         $result = array_unique($result);
1032         sort($result, SORT_NUMERIC);
1033
1034         // The last thing that needs checking is the BYSETPOS. If it's set, it
1035         // means only certain items in the set survive the filter.
1036         if (!$this->bySetPos) {
1037             return $result;
1038         }
1039
1040         $filteredResult = array();
1041         foreach($this->bySetPos as $setPos) {
1042
1043             if ($setPos<0) {
1044                 $setPos = count($result)-($setPos+1);
1045             }
1046             if (isset($result[$setPos-1])) {
1047                 $filteredResult[] = $result[$setPos-1];
1048             }
1049         }
1050
1051         sort($filteredResult, SORT_NUMERIC);
1052         return $filteredResult;
1053
1054     }
1055
1056
1057 }
1058