]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/VObject/RecurrenceIterator.php
Merge branch 'master' of git://github.com/friendica/friendica-addons
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / VObject / RecurrenceIterator.php
1 <?php
2
3 /**
4  * This class is used to determine new for a recurring event, when the next
5  * events occur.
6  *
7  * This iterator may loop infinitely in the future, therefore it is important
8  * that if you use this class, you set hard limits for the amount of iterations
9  * you want to handle.
10  *
11  * Note that currently there is not full support for the entire iCalendar
12  * specification, as it's very complex and contains a lot of permutations
13  * that's not yet used very often in software.
14  *
15  * For the focus has been on features as they actually appear in Calendaring
16  * software, but this may well get expanded as needed / on demand
17  *
18  * The following RRULE properties are supported
19  *   * UNTIL
20  *   * INTERVAL
21  *   * COUNT
22  *   * FREQ=DAILY
23  *     * BYDAY
24  *   * FREQ=WEEKLY
25  *     * BYDAY
26  *     * WKST
27  *   * FREQ=MONTHLY
28  *     * BYMONTHDAY
29  *     * BYDAY
30  *     * BYSETPOS
31  *   * FREQ=YEARLY
32  *     * BYMONTH
33  *     * BYMONTHDAY (only if BYMONTH is also set)
34  *     * BYDAY (only if BYMONTH is also set)
35  *
36  * Anything beyond this is 'undefined', which means that it may get ignored, or
37  * you may get unexpected results. The effect is that in some applications the
38  * specified recurrence may look incorrect, or is missing.
39  *
40  * @package Sabre
41  * @subpackage VObject
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 Sabre_VObject_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 Sabre_VObject_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 Sabre_VObject_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 Sabre_VObject_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 Sabre_VObject_Component $vcal
304      * @param string|null $uid
305      */
306     public function __construct(Sabre_VObject_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(Sabre_VObject_DateTimeParser::parse($this->baseEvent->DURATION->value));
340             }
341         }
342         $this->currentDate = clone $this->startDate;
343
344         $rrule = (string)$this->baseEvent->RRULE;
345
346         $parts = explode(';', $rrule);
347
348         // If no rrule was specified, we create a default setting
349         if (!$rrule) {
350             $this->frequency = 'daily';
351             $this->count = 1;
352         } else foreach($parts as $part) {
353
354             list($key, $value) = explode('=', $part, 2);
355
356             switch(strtoupper($key)) {
357
358                 case 'FREQ' :
359                     if (!in_array(
360                         strtolower($value),
361                         array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
362                     )) {
363                         throw new InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
364
365                     }
366                     $this->frequency = strtolower($value);
367                     break;
368
369                 case 'UNTIL' :
370                     $this->until = Sabre_VObject_DateTimeParser::parse($value);
371                     break;
372
373                 case 'COUNT' :
374                     $this->count = (int)$value;
375                     break;
376
377                 case 'INTERVAL' :
378                     $this->interval = (int)$value;
379                     break;
380
381                 case 'BYSECOND' :
382                     $this->bySecond = explode(',', $value);
383                     break;
384
385                 case 'BYMINUTE' :
386                     $this->byMinute = explode(',', $value);
387                     break;
388
389                 case 'BYHOUR' :
390                     $this->byHour = explode(',', $value);
391                     break;
392
393                 case 'BYDAY' :
394                     $this->byDay = explode(',', strtoupper($value));
395                     break;
396
397                 case 'BYMONTHDAY' :
398                     $this->byMonthDay = explode(',', $value);
399                     break;
400
401                 case 'BYYEARDAY' :
402                     $this->byYearDay = explode(',', $value);
403                     break;
404
405                 case 'BYWEEKNO' :
406                     $this->byWeekNo = explode(',', $value);
407                     break;
408
409                 case 'BYMONTH' :
410                     $this->byMonth = explode(',', $value);
411                     break;
412
413                 case 'BYSETPOS' :
414                     $this->bySetPos = explode(',', $value);
415                     break;
416
417                 case 'WKST' :
418                     $this->weekStart = strtoupper($value);
419                     break;
420
421             }
422
423         }
424
425         // Parsing exception dates
426         if (isset($this->baseEvent->EXDATE)) {
427             foreach($this->baseEvent->EXDATE as $exDate) {
428
429                 foreach(explode(',', (string)$exDate) as $exceptionDate) {
430
431                     $this->exceptionDates[] =
432                         Sabre_VObject_DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
433
434                 }
435
436             }
437
438         }
439
440     }
441
442     /**
443      * Returns the current item in the list
444      *
445      * @return DateTime
446      */
447     public function current() {
448
449         if (!$this->valid()) return null;
450         return clone $this->currentDate;
451
452     }
453
454     /**
455      * This method returns the startdate for the current iteration of the
456      * event.
457      *
458      * @return DateTime
459      */
460     public function getDtStart() {
461
462         if (!$this->valid()) return null;
463         return clone $this->currentDate;
464
465     }
466
467     /**
468      * This method returns the enddate for the current iteration of the
469      * event.
470      *
471      * @return DateTime
472      */
473     public function getDtEnd() {
474
475         if (!$this->valid()) return null;
476         $dtEnd = clone $this->currentDate;
477         $dtEnd->add( $this->startDate->diff( $this->endDate ) );
478         return clone $dtEnd;
479
480     }
481
482     /**
483      * Returns a VEVENT object with the updated start and end date.
484      *
485      * Any recurrence information is removed, and this function may return an
486      * 'overridden' event instead.
487      *
488      * This method always returns a cloned instance.
489      *
490      * @return Sabre_VObject_Component_VEvent
491      */
492     public function getEventObject() {
493
494         if ($this->currentOverriddenEvent) {
495             return clone $this->currentOverriddenEvent;
496         }
497         $event = clone $this->baseEvent;
498         unset($event->RRULE);
499         unset($event->EXDATE);
500         unset($event->RDATE);
501         unset($event->EXRULE);
502
503         $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
504         if (isset($event->DTEND)) {
505             $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
506         }
507         if ($this->counter > 0) {
508             $event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
509         }
510
511         return $event;
512
513     }
514
515     /**
516      * Returns the current item number
517      *
518      * @return int
519      */
520     public function key() {
521
522         return $this->counter;
523
524     }
525
526     /**
527      * Whether or not there is a 'next item'
528      *
529      * @return bool
530      */
531     public function valid() {
532
533         if (!is_null($this->count)) {
534             return $this->counter < $this->count;
535         }
536         if (!is_null($this->until)) {
537             return $this->currentDate <= $this->until;
538         }
539         return true;
540
541     }
542
543     /**
544      * Resets the iterator
545      *
546      * @return void
547      */
548     public function rewind() {
549
550         $this->currentDate = clone $this->startDate;
551         $this->counter = 0;
552
553     }
554
555     /**
556      * This method allows you to quickly go to the next occurrence after the
557      * specified date.
558      *
559      * Note that this checks the current 'endDate', not the 'stardDate'. This
560      * means that if you forward to January 1st, the iterator will stop at the
561      * first event that ends *after* January 1st.
562      *
563      * @param DateTime $dt
564      * @return void
565      */
566     public function fastForward(DateTime $dt) {
567
568         while($this->valid() && $this->getDTEnd() < $dt) {
569             $this->next();
570         }
571
572     }
573
574     /**
575      * Returns true if this recurring event never ends.
576      *
577      * @return bool
578      */
579     public function isInfinite() {
580
581         return !$this->count && !$this->until;
582
583     }
584
585     /**
586      * Goes on to the next iteration
587      *
588      * @return void
589      */
590     public function next() {
591
592         /*
593         if (!is_null($this->count) && $this->counter >= $this->count) {
594             $this->currentDate = null;
595         }*/
596
597
598         $previousStamp = $this->currentDate->getTimeStamp();
599
600         while(true) {
601
602             $this->currentOverriddenEvent = null;
603
604             // If we have a next date 'stored', we use that
605             if ($this->nextDate) {
606                 $this->currentDate = $this->nextDate;
607                 $currentStamp = $this->currentDate->getTimeStamp();
608                 $this->nextDate = null;
609             } else {
610
611                 // Otherwise, we calculate it
612                 switch($this->frequency) {
613
614                     case 'daily' :
615                         $this->nextDaily();
616                         break;
617
618                     case 'weekly' :
619                         $this->nextWeekly();
620                         break;
621
622                     case 'monthly' :
623                         $this->nextMonthly();
624                         break;
625
626                     case 'yearly' :
627                         $this->nextYearly();
628                         break;
629
630                 }
631                 $currentStamp = $this->currentDate->getTimeStamp();
632
633                 // Checking exception dates
634                 foreach($this->exceptionDates as $exceptionDate) {
635                     if ($this->currentDate == $exceptionDate) {
636                         $this->counter++;
637                         continue 2;
638                     }
639                 }
640                 foreach($this->overriddenDates as $overriddenDate) {
641                     if ($this->currentDate == $overriddenDate) {
642                         continue 2;
643                     }
644                 }
645
646             }
647
648             // Checking overridden events
649             foreach($this->overriddenEvents as $index=>$event) {
650                 if ($index > $previousStamp && $index <= $currentStamp) {
651
652                     // We're moving the 'next date' aside, for later use.
653                     $this->nextDate = clone $this->currentDate;
654
655                     $this->currentDate = $event->DTSTART->getDateTime();
656                     $this->currentOverriddenEvent = $event;
657
658                     break;
659                 }
660             }
661
662             break;
663
664         }
665
666         /*
667         if (!is_null($this->until)) {
668             if($this->currentDate > $this->until) {
669                 $this->currentDate = null;
670             }
671         }*/
672
673         $this->counter++;
674
675     }
676
677     /**
678      * Does the processing for advancing the iterator for daily frequency.
679      *
680      * @return void
681      */
682     protected function nextDaily() {
683
684         if (!$this->byDay) {
685             $this->currentDate->modify('+' . $this->interval . ' days');
686             return;
687         }
688
689         $recurrenceDays = array();
690         foreach($this->byDay as $byDay) {
691
692             // The day may be preceeded with a positive (+n) or
693             // negative (-n) integer. However, this does not make
694             // sense in 'weekly' so we ignore it here.
695             $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
696
697         }
698
699         do {
700
701             $this->currentDate->modify('+' . $this->interval . ' days');
702
703             // Current day of the week
704             $currentDay = $this->currentDate->format('w');
705
706         } while (!in_array($currentDay, $recurrenceDays));
707
708     }
709
710     /**
711      * Does the processing for advancing the iterator for weekly frequency.
712      *
713      * @return void
714      */
715     protected function nextWeekly() {
716
717         if (!$this->byDay) {
718             $this->currentDate->modify('+' . $this->interval . ' weeks');
719             return;
720         }
721
722         $recurrenceDays = array();
723         foreach($this->byDay as $byDay) {
724
725             // The day may be preceeded with a positive (+n) or
726             // negative (-n) integer. However, this does not make
727             // sense in 'weekly' so we ignore it here.
728             $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
729
730         }
731
732         // Current day of the week
733         $currentDay = $this->currentDate->format('w');
734
735         // First day of the week:
736         $firstDay = $this->dayMap[$this->weekStart];
737
738         $time = array(
739             $this->currentDate->format('H'),
740             $this->currentDate->format('i'),
741             $this->currentDate->format('s')
742         );
743
744         // Increasing the 'current day' until we find our next
745         // occurrence.
746         while(true) {
747
748             $currentDay++;
749
750             if ($currentDay>6) {
751                 $currentDay = 0;
752             }
753
754             // We need to roll over to the next week
755             if ($currentDay === $firstDay) {
756                 $this->currentDate->modify('+' . $this->interval . ' weeks');
757
758                 // We need to go to the first day of this week, but only if we
759                 // are not already on this first day of this week.
760                 if($this->currentDate->format('w') != $firstDay) {
761                     $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
762                     $this->currentDate->setTime($time[0],$time[1],$time[2]);
763                 }
764             }
765
766             // We have a match
767             if (in_array($currentDay ,$recurrenceDays)) {
768                 $this->currentDate->modify($this->dayNames[$currentDay]);
769                 $this->currentDate->setTime($time[0],$time[1],$time[2]);
770                 break;
771             }
772
773         }
774
775     }
776
777     /**
778      * Does the processing for advancing the iterator for monthly frequency.
779      *
780      * @return void
781      */
782     protected function nextMonthly() {
783
784         $currentDayOfMonth = $this->currentDate->format('j');
785         if (!$this->byMonthDay && !$this->byDay) {
786
787             // If the current day is higher than the 28th, rollover can
788             // occur to the next month. We Must skip these invalid
789             // entries.
790             if ($currentDayOfMonth < 29) {
791                 $this->currentDate->modify('+' . $this->interval . ' months');
792             } else {
793                 $increase = 0;
794                 do {
795                     $increase++;
796                     $tempDate = clone $this->currentDate;
797                     $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
798                 } while ($tempDate->format('j') != $currentDayOfMonth);
799                 $this->currentDate = $tempDate;
800             }
801             return;
802         }
803
804         while(true) {
805
806             $occurrences = $this->getMonthlyOccurrences();
807
808             foreach($occurrences as $occurrence) {
809
810                 // The first occurrence thats higher than the current
811                 // day of the month wins.
812                 if ($occurrence > $currentDayOfMonth) {
813                     break 2;
814                 }
815
816             }
817
818             // If we made it all the way here, it means there were no
819             // valid occurrences, and we need to advance to the next
820             // month.
821             $this->currentDate->modify('first day of this month');
822             $this->currentDate->modify('+ ' . $this->interval . ' months');
823
824             // This goes to 0 because we need to start counting at hte
825             // beginning.
826             $currentDayOfMonth = 0;
827
828         }
829
830         $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
831
832     }
833
834     /**
835      * Does the processing for advancing the iterator for yearly frequency.
836      *
837      * @return void
838      */
839     protected function nextYearly() {
840
841         if (!$this->byMonth) {
842             $this->currentDate->modify('+' . $this->interval . ' years');
843             return;
844         }
845
846         $currentMonth = $this->currentDate->format('n');
847         $currentYear = $this->currentDate->format('Y');
848         $currentDayOfMonth = $this->currentDate->format('j');
849
850         $advancedToNewMonth = false;
851
852         // If we got a byDay or getMonthDay filter, we must first expand
853         // further.
854         if ($this->byDay || $this->byMonthDay) {
855
856             while(true) {
857
858                 $occurrences = $this->getMonthlyOccurrences();
859
860                 foreach($occurrences as $occurrence) {
861
862                     // The first occurrence that's higher than the current
863                     // day of the month wins.
864                     // If we advanced to the next month or year, the first
865                     // occurrence is always correct.
866                     if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
867                         break 2;
868                     }
869
870                 }
871
872                 // If we made it here, it means we need to advance to
873                 // the next month or year.
874                 $currentDayOfMonth = 1;
875                 $advancedToNewMonth = true;
876                 do {
877
878                     $currentMonth++;
879                     if ($currentMonth>12) {
880                         $currentYear+=$this->interval;
881                         $currentMonth = 1;
882                     }
883                 } while (!in_array($currentMonth, $this->byMonth));
884
885                 $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
886
887             }
888
889             // If we made it here, it means we got a valid occurrence
890             $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
891             return;
892
893         } else {
894
895             // no byDay or byMonthDay, so we can just loop through the
896             // months.
897             do {
898
899                 $currentMonth++;
900                 if ($currentMonth>12) {
901                     $currentYear+=$this->interval;
902                     $currentMonth = 1;
903                 }
904             } while (!in_array($currentMonth, $this->byMonth));
905             $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
906             return;
907
908         }
909
910     }
911
912     /**
913      * Returns all the occurrences for a monthly frequency with a 'byDay' or
914      * 'byMonthDay' expansion for the current month.
915      *
916      * The returned list is an array of integers with the day of month (1-31).
917      *
918      * @return array
919      */
920     protected function getMonthlyOccurrences() {
921
922         $startDate = clone $this->currentDate;
923
924         $byDayResults = array();
925
926         // Our strategy is to simply go through the byDays, advance the date to
927         // that point and add it to the results.
928         if ($this->byDay) foreach($this->byDay as $day) {
929
930             $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
931
932             // Dayname will be something like 'wednesday'. Now we need to find
933             // all wednesdays in this month.
934             $dayHits = array();
935
936             $checkDate = clone $startDate;
937             $checkDate->modify('first day of this month');
938             $checkDate->modify($dayName);
939
940             do {
941                 $dayHits[] = $checkDate->format('j');
942                 $checkDate->modify('next ' . $dayName);
943             } while ($checkDate->format('n') === $startDate->format('n'));
944
945             // So now we have 'all wednesdays' for month. It is however
946             // possible that the user only really wanted the 1st, 2nd or last
947             // wednesday.
948             if (strlen($day)>2) {
949                 $offset = (int)substr($day,0,-2);
950
951                 if ($offset>0) {
952                     // It is possible that the day does not exist, such as a
953                     // 5th or 6th wednesday of the month.
954                     if (isset($dayHits[$offset-1])) {
955                         $byDayResults[] = $dayHits[$offset-1];
956                     }
957                 } else {
958
959                     // if it was negative we count from the end of the array
960                     $byDayResults[] = $dayHits[count($dayHits) + $offset];
961                 }
962             } else {
963                 // There was no counter (first, second, last wednesdays), so we
964                 // just need to add the all to the list).
965                 $byDayResults = array_merge($byDayResults, $dayHits);
966
967             }
968
969         }
970
971         $byMonthDayResults = array();
972         if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
973
974             // Removing values that are out of range for this month
975             if ($monthDay > $startDate->format('t') ||
976                 $monthDay < 0-$startDate->format('t')) {
977                     continue;
978             }
979             if ($monthDay>0) {
980                 $byMonthDayResults[] = $monthDay;
981             } else {
982                 // Negative values
983                 $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
984             }
985         }
986
987         // If there was just byDay or just byMonthDay, they just specify our
988         // (almost) final list. If both were provided, then byDay limits the
989         // list.
990         if ($this->byMonthDay && $this->byDay) {
991             $result = array_intersect($byMonthDayResults, $byDayResults);
992         } elseif ($this->byMonthDay) {
993             $result = $byMonthDayResults;
994         } else {
995             $result = $byDayResults;
996         }
997         $result = array_unique($result);
998         sort($result, SORT_NUMERIC);
999
1000         // The last thing that needs checking is the BYSETPOS. If it's set, it
1001         // means only certain items in the set survive the filter.
1002         if (!$this->bySetPos) {
1003             return $result;
1004         }
1005
1006         $filteredResult = array();
1007         foreach($this->bySetPos as $setPos) {
1008
1009             if ($setPos<0) {
1010                 $setPos = count($result)-($setPos+1);
1011             }
1012             if (isset($result[$setPos-1])) {
1013                 $filteredResult[] = $result[$setPos-1];
1014             }
1015         }
1016
1017         sort($filteredResult, SORT_NUMERIC);
1018         return $filteredResult;
1019
1020     }
1021
1022
1023 }
1024