1<?php
2
3namespace Sabre\VObject\Recur;
4
5use DateTime;
6use DateTimeZone;
7
8class RRuleIteratorTest extends \PHPUnit_Framework_TestCase {
9
10    function testHourly() {
11
12        $this->parse(
13            'FREQ=HOURLY;INTERVAL=3;COUNT=12',
14            '2011-10-07 12:00:00',
15            array(
16                '2011-10-07 12:00:00',
17                '2011-10-07 15:00:00',
18                '2011-10-07 18:00:00',
19                '2011-10-07 21:00:00',
20                '2011-10-08 00:00:00',
21                '2011-10-08 03:00:00',
22                '2011-10-08 06:00:00',
23                '2011-10-08 09:00:00',
24                '2011-10-08 12:00:00',
25                '2011-10-08 15:00:00',
26                '2011-10-08 18:00:00',
27                '2011-10-08 21:00:00',
28            )
29        );
30
31    }
32
33    function testDaily() {
34
35        $this->parse(
36            'FREQ=DAILY;INTERVAL=3;UNTIL=20111025T000000Z',
37            '2011-10-07',
38            array(
39                '2011-10-07 00:00:00',
40                '2011-10-10 00:00:00',
41                '2011-10-13 00:00:00',
42                '2011-10-16 00:00:00',
43                '2011-10-19 00:00:00',
44                '2011-10-22 00:00:00',
45                '2011-10-25 00:00:00',
46            )
47        );
48
49    }
50
51    function testDailyByDayByHour() {
52
53        $this->parse(
54            'FREQ=DAILY;BYDAY=SA,SU;BYHOUR=6,7',
55            '2011-10-08 06:00:00',
56            array(
57                '2011-10-08 06:00:00',
58                '2011-10-08 07:00:00',
59                '2011-10-09 06:00:00',
60                '2011-10-09 07:00:00',
61                '2011-10-15 06:00:00',
62                '2011-10-15 07:00:00',
63                '2011-10-16 06:00:00',
64                '2011-10-16 07:00:00',
65                '2011-10-22 06:00:00',
66                '2011-10-22 07:00:00',
67                '2011-10-23 06:00:00',
68                '2011-10-23 07:00:00',
69            )
70        );
71
72    }
73
74    function testDailyByHour() {
75
76        $this->parse(
77            'FREQ=DAILY;INTERVAL=2;BYHOUR=10,11,12,13,14,15',
78            '2012-10-11 12:00:00',
79            array(
80                '2012-10-11 12:00:00',
81                '2012-10-11 13:00:00',
82                '2012-10-11 14:00:00',
83                '2012-10-11 15:00:00',
84                '2012-10-13 10:00:00',
85                '2012-10-13 11:00:00',
86                '2012-10-13 12:00:00',
87                '2012-10-13 13:00:00',
88                '2012-10-13 14:00:00',
89                '2012-10-13 15:00:00',
90                '2012-10-15 10:00:00',
91                '2012-10-15 11:00:00',
92            )
93        );
94
95    }
96
97    function testDailyByDay() {
98
99        $this->parse(
100            'FREQ=DAILY;INTERVAL=2;BYDAY=TU,WE,FR',
101            '2011-10-07 12:00:00',
102            array(
103                '2011-10-07 12:00:00',
104                '2011-10-11 12:00:00',
105                '2011-10-19 12:00:00',
106                '2011-10-21 12:00:00',
107                '2011-10-25 12:00:00',
108                '2011-11-02 12:00:00',
109                '2011-11-04 12:00:00',
110                '2011-11-08 12:00:00',
111                '2011-11-16 12:00:00',
112                '2011-11-18 12:00:00',
113                '2011-11-22 12:00:00',
114                '2011-11-30 12:00:00',
115            )
116        );
117
118    }
119
120    function testDailyCount() {
121
122        $this->parse(
123            'FREQ=DAILY;COUNT=5',
124            '2014-08-01 18:03:00',
125            array(
126                '2014-08-01 18:03:00',
127                '2014-08-02 18:03:00',
128                '2014-08-03 18:03:00',
129                '2014-08-04 18:03:00',
130                '2014-08-05 18:03:00',
131            )
132        );
133
134    }
135
136    function testDailyByMonth() {
137
138        $this->parse(
139            'FREQ=DAILY;BYMONTH=9,10;BYDAY=SU',
140            '2007-10-04 16:00:00',
141            array(
142                "2013-09-29 16:00:00",
143                "2013-10-06 16:00:00",
144                "2013-10-13 16:00:00",
145                "2013-10-20 16:00:00",
146                "2013-10-27 16:00:00",
147                "2014-09-07 16:00:00"
148            ),
149            '2013-09-28'
150        );
151
152    }
153
154    function testWeekly() {
155
156        $this->parse(
157            'FREQ=WEEKLY;INTERVAL=2;COUNT=10',
158            '2011-10-07 00:00:00',
159            array(
160                '2011-10-07 00:00:00',
161                '2011-10-21 00:00:00',
162                '2011-11-04 00:00:00',
163                '2011-11-18 00:00:00',
164                '2011-12-02 00:00:00',
165                '2011-12-16 00:00:00',
166                '2011-12-30 00:00:00',
167                '2012-01-13 00:00:00',
168                '2012-01-27 00:00:00',
169                '2012-02-10 00:00:00',
170            )
171        );
172
173    }
174
175    function testWeeklyByDay() {
176
177        $this->parse(
178            'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=MO;WKST=SA',
179            '2014-08-01 00:00:00',
180            array(
181                '2014-08-01 00:00:00',
182                '2014-08-04 00:00:00',
183                '2014-08-11 00:00:00',
184                '2014-08-18 00:00:00',
185            )
186        );
187
188    }
189
190    function testWeeklyByDay2() {
191
192        $this->parse(
193            'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU',
194            '2011-10-07 00:00:00',
195            array(
196                '2011-10-07 00:00:00',
197                '2011-10-18 00:00:00',
198                '2011-10-19 00:00:00',
199                '2011-10-21 00:00:00',
200                '2011-11-01 00:00:00',
201                '2011-11-02 00:00:00',
202                '2011-11-04 00:00:00',
203                '2011-11-15 00:00:00',
204                '2011-11-16 00:00:00',
205                '2011-11-18 00:00:00',
206                '2011-11-29 00:00:00',
207                '2011-11-30 00:00:00',
208            )
209        );
210
211    }
212
213    function testWeeklyByDayByHour() {
214
215        $this->parse(
216            'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=MO;BYHOUR=8,9,10',
217            '2011-10-07 08:00:00',
218            array(
219                '2011-10-07 08:00:00',
220                '2011-10-07 09:00:00',
221                '2011-10-07 10:00:00',
222                '2011-10-18 08:00:00',
223                '2011-10-18 09:00:00',
224                '2011-10-18 10:00:00',
225                '2011-10-19 08:00:00',
226                '2011-10-19 09:00:00',
227                '2011-10-19 10:00:00',
228                '2011-10-21 08:00:00',
229                '2011-10-21 09:00:00',
230                '2011-10-21 10:00:00',
231                '2011-11-01 08:00:00',
232                '2011-11-01 09:00:00',
233                '2011-11-01 10:00:00',
234            )
235        );
236
237    }
238
239    function testWeeklyByDaySpecificHour() {
240
241        $this->parse(
242            'FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,WE,FR;WKST=SU',
243            '2011-10-07 18:00:00',
244            array(
245                '2011-10-07 18:00:00',
246                '2011-10-18 18:00:00',
247                '2011-10-19 18:00:00',
248                '2011-10-21 18:00:00',
249                '2011-11-01 18:00:00',
250                '2011-11-02 18:00:00',
251                '2011-11-04 18:00:00',
252                '2011-11-15 18:00:00',
253                '2011-11-16 18:00:00',
254                '2011-11-18 18:00:00',
255                '2011-11-29 18:00:00',
256                '2011-11-30 18:00:00',
257            )
258        );
259
260    }
261
262    function testMonthly() {
263
264        $this->parse(
265            'FREQ=MONTHLY;INTERVAL=3;COUNT=5',
266            '2011-12-05 00:00:00',
267            array(
268                 '2011-12-05 00:00:00',
269                 '2012-03-05 00:00:00',
270                 '2012-06-05 00:00:00',
271                 '2012-09-05 00:00:00',
272                 '2012-12-05 00:00:00',
273            )
274        );
275
276    }
277
278    function testMonlthyEndOfMonth() {
279
280        $this->parse(
281            'FREQ=MONTHLY;INTERVAL=2;COUNT=12',
282            '2011-12-31 00:00:00',
283            array(
284                '2011-12-31 00:00:00',
285                '2012-08-31 00:00:00',
286                '2012-10-31 00:00:00',
287                '2012-12-31 00:00:00',
288                '2013-08-31 00:00:00',
289                '2013-10-31 00:00:00',
290                '2013-12-31 00:00:00',
291                '2014-08-31 00:00:00',
292                '2014-10-31 00:00:00',
293                '2014-12-31 00:00:00',
294                '2015-08-31 00:00:00',
295                '2015-10-31 00:00:00',
296            )
297        );
298
299    }
300
301    function testMonthlyByMonthDay() {
302
303        $this->parse(
304            'FREQ=MONTHLY;INTERVAL=5;COUNT=9;BYMONTHDAY=1,31,-7',
305            '2011-01-01 00:00:00',
306            array(
307                '2011-01-01 00:00:00',
308                '2011-01-25 00:00:00',
309                '2011-01-31 00:00:00',
310                '2011-06-01 00:00:00',
311                '2011-06-24 00:00:00',
312                '2011-11-01 00:00:00',
313                '2011-11-24 00:00:00',
314                '2012-04-01 00:00:00',
315                '2012-04-24 00:00:00',
316            )
317        );
318
319    }
320
321    function testMonthlyByDay() {
322
323        $this->parse(
324            'FREQ=MONTHLY;INTERVAL=2;COUNT=16;BYDAY=MO,-2TU,+1WE,3TH',
325            '2011-01-03 00:00:00',
326            array(
327                '2011-01-03 00:00:00',
328                '2011-01-05 00:00:00',
329                '2011-01-10 00:00:00',
330                '2011-01-17 00:00:00',
331                '2011-01-18 00:00:00',
332                '2011-01-20 00:00:00',
333                '2011-01-24 00:00:00',
334                '2011-01-31 00:00:00',
335                '2011-03-02 00:00:00',
336                '2011-03-07 00:00:00',
337                '2011-03-14 00:00:00',
338                '2011-03-17 00:00:00',
339                '2011-03-21 00:00:00',
340                '2011-03-22 00:00:00',
341                '2011-03-28 00:00:00',
342                '2011-05-02 00:00:00',
343            )
344        );
345
346    }
347
348    function testMonthlyByDayByMonthDay() {
349
350        $this->parse(
351            'FREQ=MONTHLY;COUNT=10;BYDAY=MO;BYMONTHDAY=1',
352            '2011-08-01 00:00:00',
353            array(
354                '2011-08-01 00:00:00',
355                '2012-10-01 00:00:00',
356                '2013-04-01 00:00:00',
357                '2013-07-01 00:00:00',
358                '2014-09-01 00:00:00',
359                '2014-12-01 00:00:00',
360                '2015-06-01 00:00:00',
361                '2016-02-01 00:00:00',
362                '2016-08-01 00:00:00',
363                '2017-05-01 00:00:00',
364            )
365        );
366
367    }
368
369    function testMonthlyByDayBySetPos() {
370
371        $this->parse(
372            'FREQ=MONTHLY;COUNT=10;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1,-1',
373            '2011-01-03 00:00:00',
374            array(
375                '2011-01-03 00:00:00',
376                '2011-01-31 00:00:00',
377                '2011-02-01 00:00:00',
378                '2011-02-28 00:00:00',
379                '2011-03-01 00:00:00',
380                '2011-03-31 00:00:00',
381                '2011-04-01 00:00:00',
382                '2011-04-29 00:00:00',
383                '2011-05-02 00:00:00',
384                '2011-05-31 00:00:00',
385            )
386        );
387
388    }
389
390    function testYearly() {
391
392        $this->parse(
393            'FREQ=YEARLY;COUNT=10;INTERVAL=3',
394            '2011-01-01 00:00:00',
395            array(
396                '2011-01-01 00:00:00',
397                '2014-01-01 00:00:00',
398                '2017-01-01 00:00:00',
399                '2020-01-01 00:00:00',
400                '2023-01-01 00:00:00',
401                '2026-01-01 00:00:00',
402                '2029-01-01 00:00:00',
403                '2032-01-01 00:00:00',
404                '2035-01-01 00:00:00',
405                '2038-01-01 00:00:00',
406            )
407        );
408    }
409
410    function testYearlyLeapYear() {
411
412        $this->parse(
413            'FREQ=YEARLY;COUNT=3',
414            '2012-02-29 00:00:00',
415            array(
416                '2012-02-29 00:00:00',
417                '2016-02-29 00:00:00',
418                '2020-02-29 00:00:00',
419            )
420        );
421    }
422
423    function testYearlyByMonth() {
424
425        $this->parse(
426            'FREQ=YEARLY;COUNT=8;INTERVAL=4;BYMONTH=4,10',
427            '2011-04-07 00:00:00',
428            array(
429                '2011-04-07 00:00:00',
430                '2011-10-07 00:00:00',
431                '2015-04-07 00:00:00',
432                '2015-10-07 00:00:00',
433                '2019-04-07 00:00:00',
434                '2019-10-07 00:00:00',
435                '2023-04-07 00:00:00',
436                '2023-10-07 00:00:00',
437            )
438        );
439
440    }
441
442    function testYearlyByMonthByDay() {
443
444        $this->parse(
445            'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU',
446            '2011-04-04 00:00:00',
447            array(
448                '2011-04-04 00:00:00',
449                '2011-04-24 00:00:00',
450                '2011-10-03 00:00:00',
451                '2011-10-30 00:00:00',
452                '2016-04-04 00:00:00',
453                '2016-04-24 00:00:00',
454                '2016-10-03 00:00:00',
455                '2016-10-30 00:00:00',
456            )
457        );
458
459    }
460
461    function testFastForward() {
462
463        // The idea is that we're fast-forwarding too far in the future, so
464        // there will be no results left.
465        $this->parse(
466            'FREQ=YEARLY;COUNT=8;INTERVAL=5;BYMONTH=4,10;BYDAY=1MO,-1SU',
467            '2011-04-04 00:00:00',
468            array(),
469            '2020-05-05 00:00:00'
470        );
471
472    }
473
474    /**
475     * The bug that was in the
476     * system before would fail on the 5th tuesday of the month, if the 5th
477     * tuesday did not exist.
478     *
479     * A pretty slow test. Had to be marked as 'medium' for phpunit to not die
480     * after 1 second. Would be good to optimize later.
481     *
482     * @medium
483     */
484    function testFifthTuesdayProblem() {
485
486        $this->parse(
487            'FREQ=MONTHLY;INTERVAL=1;UNTIL=20071030T035959Z;BYDAY=5TU',
488            '2007-10-04 14:46:42',
489            array(
490                "2007-10-04 14:46:42",
491            )
492        );
493
494    }
495
496    /**
497     * This bug came from a Fruux customer. This would result in a never-ending
498     * request.
499     */
500    function testFastFowardTooFar() {
501
502        $this->parse(
503            'FREQ=WEEKLY;BYDAY=MO;UNTIL=20090704T205959Z;INTERVAL=1',
504            '2009-04-20 18:00:00',
505            array(
506                '2009-04-20 18:00:00',
507                '2009-04-27 18:00:00',
508                '2009-05-04 18:00:00',
509                '2009-05-11 18:00:00',
510                '2009-05-18 18:00:00',
511                '2009-05-25 18:00:00',
512                '2009-06-01 18:00:00',
513                '2009-06-08 18:00:00',
514                '2009-06-15 18:00:00',
515                '2009-06-22 18:00:00',
516                '2009-06-29 18:00:00',
517            )
518        );
519
520    }
521
522    /**
523     * This also at one point caused an infinite loop. We're keeping the test.
524     */
525    function testYearlyByMonthLoop() {
526
527        $this->parse(
528            'FREQ=YEARLY;INTERVAL=1;UNTIL=20120203T225959Z;BYMONTH=2;BYSETPOS=1;BYDAY=SU,MO,TU,WE,TH,FR,SA',
529            '2012-01-01 15:45:00',
530            array(
531                '2012-02-01 15:45:00',
532            ),
533            '2012-01-29 23:00:00'
534        );
535
536
537    }
538
539    /**
540     * Something, somewhere produced an ics with an interval set to 0. Because
541     * this means we increase the current day (or week, month) by 0, this also
542     * results in an infinite loop.
543     *
544     * @expectedException InvalidArgumentException
545     */
546    function testZeroInterval() {
547
548        $this->parse(
549            'FREQ=YEARLY;INTERVAL=0',
550            '2012-08-24 14:57:00',
551            array(),
552            '2013-01-01 23:00:00'
553        );
554
555    }
556
557    /**
558     * @expectedException InvalidArgumentException
559     */
560    function testInvalidFreq() {
561
562        $this->parse(
563            'FREQ=SMONTHLY;INTERVAL=3;UNTIL=20111025T000000Z',
564            '2011-10-07',
565            array()
566        );
567
568    }
569
570    /**
571     * @expectedException InvalidArgumentException
572     */
573    function testByDayBadOffset() {
574
575        $this->parse(
576            'FREQ=WEEKLY;INTERVAL=1;COUNT=4;BYDAY=0MO;WKST=SA',
577            '2014-08-01 00:00:00',
578            array()
579        );
580
581    }
582
583    function testUntilBeginHAsTimezone() {
584
585        $this->parse(
586            'FREQ=WEEKLY;UNTIL=20131118T183000',
587            '2013-09-23 18:30:00',
588            array(
589                '2013-09-23 18:30:00',
590                '2013-09-30 18:30:00',
591                '2013-10-07 18:30:00',
592                '2013-10-14 18:30:00',
593                '2013-10-21 18:30:00',
594                '2013-10-28 18:30:00',
595                '2013-11-04 18:30:00',
596                '2013-11-11 18:30:00',
597                '2013-11-18 18:30:00',
598            ),
599            null,
600            'America/New_York'
601        );
602
603    }
604
605    function testUntilBeforeDtStart() {
606
607        $this->parse(
608            'FREQ=DAILY;UNTIL=20140101T000000Z',
609            '2014-08-02 00:15:00',
610            array(
611                '2014-08-02 00:15:00',
612            )
613        );
614
615    }
616
617    function testIgnoredStuff() {
618
619        $this->parse(
620            'FREQ=DAILY;BYSECOND=1;BYMINUTE=1;BYYEARDAY=1;BYWEEKNO=1;COUNT=2',
621            '2014-08-02 00:15:00',
622            array(
623                '2014-08-02 00:15:00',
624                '2014-08-03 00:15:00',
625            )
626        );
627
628    }
629
630    function testMinusFifthThursday() {
631
632        $this->parse(
633            'FREQ=MONTHLY;BYDAY=-4TH,-5TH;COUNT=4',
634            '2015-01-01 00:15:00',
635            array(
636                '2015-01-01 00:15:00',
637                '2015-01-08 00:15:00',
638                '2015-02-05 00:15:00',
639                '2015-03-05 00:15:00'
640            )
641        );
642
643    }
644
645    /**
646     * @expectedException InvalidArgumentException
647     */
648    function testUnsupportedPart() {
649
650        $this->parse(
651            'FREQ=DAILY;BYWODAN=1',
652            '2014-08-02 00:15:00',
653            array()
654        );
655
656    }
657
658    function testIteratorFunctions() {
659
660        $parser = new RRuleIterator('FREQ=DAILY', new DateTime('2014-08-02 00:00:13'));
661        $parser->next();
662        $this->assertEquals(
663            new DateTime('2014-08-03 00:00:13'),
664            $parser->current()
665        );
666        $this->assertEquals(
667            1,
668            $parser->key()
669        );
670
671        $parser->rewind();
672
673        $this->assertEquals(
674            new DateTime('2014-08-02 00:00:13'),
675            $parser->current()
676        );
677        $this->assertEquals(
678            0,
679            $parser->key()
680        );
681
682    }
683
684    function parse($rule, $start, $expected, $fastForward = null, $tz = 'UTC') {
685
686        $dt = new DateTime($start, new DateTimeZone($tz));
687        $parser = new RRuleIterator($rule, $dt);
688
689        if ($fastForward) {
690            $parser->fastForward(new DateTime($fastForward));
691        }
692
693        $result = array();
694        while($parser->valid()) {
695
696            $item = $parser->current();
697            $result[] = $item->format('Y-m-d H:i:s');
698
699            if ($parser->isInfinite() && count($result) >= count($expected)) {
700                break;
701            }
702            $parser->next();
703
704        }
705
706        $this->assertEquals(
707            $expected,
708            $result
709        );
710
711    }
712
713}
714