2b0f315a95353faa70245177e9e2370f968c1b5a
[moodle.git] / calendar / tests / rrule_manager_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Defines test class to test manage rrule during ical imports.
19  *
20  * @package core_calendar
21  * @category test
22  * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
23  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
29 require_once($CFG->dirroot . '/calendar/lib.php');
31 use core_calendar\rrule_manager;
33 /**
34  * Defines test class to test manage rrule during ical imports.
35  *
36  * @package core_calendar
37  * @category test
38  * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
39  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class core_calendar_rrule_manager_testcase extends advanced_testcase {
43     /** @var calendar_event a dummy event */
44     protected $event;
46     /**
47      * Set up method.
48      */
49     protected function setUp() {
50         global $DB;
51         $this->resetAfterTest();
53         // Set our timezone based on the timezone in the RFC's samples (US/Eastern).
54         $tz = 'US/Eastern';
55         $this->setTimezone($tz);
56         $timezone = new DateTimeZone($tz);
57         // Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
58         $time = DateTime::createFromFormat('Ymd\THis', '19970902T090000', $timezone);
59         $timestart = $time->getTimestamp();
61         $user = $this->getDataGenerator()->create_user();
62         $sub = new stdClass();
63         $sub->url = '';
64         $sub->courseid = 0;
65         $sub->groupid = 0;
66         $sub->userid = $user->id;
67         $sub->pollinterval = 0;
68         $subid = $DB->insert_record('event_subscriptions', $sub, true);
70         $event = new stdClass();
71         $event->name = 'Event name';
72         $event->description = '';
73         $event->timestart = $timestart;
74         $event->timeduration = 3600;
75         $event->uuid = 'uuid';
76         $event->subscriptionid = $subid;
77         $event->userid = $user->id;
78         $event->groupid = 0;
79         $event->courseid = 0;
80         $event->eventtype = 'user';
81         $eventobj = calendar_event::create($event, false);
82         $DB->set_field('event', 'repeatid', $eventobj->id, array('id' => $eventobj->id));
83         $eventobj->repeatid = $eventobj->id;
84         $this->event = $eventobj;
85     }
87     /**
88      * Test parse_rrule() method.
89      */
90     public function test_parse_rrule() {
91         $rules = [
92             'FREQ=YEARLY',
93             'COUNT=3',
94             'INTERVAL=4',
95             'BYSECOND=20,40',
96             'BYMINUTE=2,30',
97             'BYHOUR=3,4',
98             'BYDAY=MO,TH',
99             'BYMONTHDAY=20,30',
100             'BYYEARDAY=300,-20',
101             'BYWEEKNO=22,33',
102             'BYMONTH=3,4'
103         ];
104         $rrule = implode(';', $rules);
105         $mang = new rrule_manager($rrule);
106         $mang->parse_rrule();
108         $bydayrules = [
109             (object)[
110                 'day' => 'MO',
111                 'value' => 0
112             ],
113             (object)[
114                 'day' => 'TH',
115                 'value' => 0
116             ],
117         ];
119         $props = [
120             'freq' => rrule_manager::FREQ_YEARLY,
121             'count' => 3,
122             'interval' => 4,
123             'bysecond' => [20, 40],
124             'byminute' => [2, 30],
125             'byhour' => [3, 4],
126             'byday' => $bydayrules,
127             'bymonthday' => [20, 30],
128             'byyearday' => [300, -20],
129             'byweekno' => [22, 33],
130             'bymonth' => [3, 4],
131         ];
133         $reflectionclass = new ReflectionClass($mang);
134         foreach ($props as $prop => $expectedval) {
135             $rcprop = $reflectionclass->getProperty($prop);
136             $rcprop->setAccessible(true);
137             $this->assertEquals($expectedval, $rcprop->getValue($mang));
138         }
139     }
141     /**
142      * Test exception is thrown for invalid property.
143      */
144     public function test_parse_rrule_validation() {
145         $rrule = "RANDOM=PROPERTY;";
146         $mang = new rrule_manager($rrule);
147         $this->expectException('moodle_exception');
148         $mang->parse_rrule();
149     }
151     /**
152      * Test exception is thrown for invalid frequency.
153      */
154     public function test_freq_validation() {
155         $rrule = "FREQ=RANDOMLY;";
156         $mang = new rrule_manager($rrule);
157         $this->expectException('moodle_exception');
158         $mang->parse_rrule();
159     }
161     /**
162      * Test parsing of rules with both COUNT and UNTIL parameters.
163      */
164     public function test_until_count_validation() {
165         $until = $this->event->timestart + DAYSECS * 4;
166         $until = date('Y-m-d', $until);
167         $rrule = "FREQ=DAILY;COUNT=2;UNTIL=$until";
168         $mang = new rrule_manager($rrule);
169         $this->expectException('moodle_exception');
170         $mang->parse_rrule();
171     }
173     /**
174      * Test parsing of INTERVAL rule.
175      */
176     public function test_interval_validation() {
177         $rrule = "INTERVAL=0";
178         $mang = new rrule_manager($rrule);
179         $this->expectException('moodle_exception');
180         $mang->parse_rrule();
181     }
183     /**
184      * Test parsing of BYSECOND rule.
185      */
186     public function test_bysecond_validation() {
187         $rrule = "BYSECOND=30,45,60";
188         $mang = new rrule_manager($rrule);
189         $this->expectException('moodle_exception');
190         $mang->parse_rrule();
191     }
193     /**
194      * Test parsing of BYMINUTE rule.
195      */
196     public function test_byminute_validation() {
197         $rrule = "BYMINUTE=30,45,60";
198         $mang = new rrule_manager($rrule);
199         $this->expectException('moodle_exception');
200         $mang->parse_rrule();
201     }
203     /**
204      * Test parsing of BYMINUTE rule.
205      */
206     public function test_byhour_validation() {
207         $rrule = "BYHOUR=23,45";
208         $mang = new rrule_manager($rrule);
209         $this->expectException('moodle_exception');
210         $mang->parse_rrule();
211     }
213     /**
214      * Test parsing of BYDAY rule.
215      */
216     public function test_byday_validation() {
217         $rrule = "BYDAY=MO,2SE";
218         $mang = new rrule_manager($rrule);
219         $this->expectException('moodle_exception');
220         $mang->parse_rrule();
221     }
223     /**
224      * Test parsing of BYDAY rule with prefixes.
225      */
226     public function test_byday_with_prefix_validation() {
227         // This is acceptable.
228         $rrule = "FREQ=MONTHLY;BYDAY=-1MO,2SA";
229         $mang = new rrule_manager($rrule);
230         $mang->parse_rrule();
232         // This is also acceptable.
233         $rrule = "FREQ=YEARLY;BYDAY=MO,2SA";
234         $mang = new rrule_manager($rrule);
235         $mang->parse_rrule();
237         // This is invalid.
238         $rrule = "FREQ=WEEKLY;BYDAY=MO,2SA";
239         $mang = new rrule_manager($rrule);
240         $this->expectException('moodle_exception');
241         $mang->parse_rrule();
242     }
244     /**
245      * Test parsing of BYMONTHDAY rule.
246      */
247     public function test_bymonthday_upper_bound_validation() {
248         $rrule = "BYMONTHDAY=1,32";
249         $mang = new rrule_manager($rrule);
250         $this->expectException('moodle_exception');
251         $mang->parse_rrule();
252     }
254     /**
255      * Test parsing of BYMONTHDAY rule.
256      */
257     public function test_bymonthday_0_validation() {
258         $rrule = "BYMONTHDAY=1,0";
259         $mang = new rrule_manager($rrule);
260         $this->expectException('moodle_exception');
261         $mang->parse_rrule();
262     }
264     /**
265      * Test parsing of BYMONTHDAY rule.
266      */
267     public function test_bymonthday_lower_bound_validation() {
268         $rrule = "BYMONTHDAY=1,-31,-32";
269         $mang = new rrule_manager($rrule);
270         $this->expectException('moodle_exception');
271         $mang->parse_rrule();
272     }
274     /**
275      * Test parsing of BYYEARDAY rule.
276      */
277     public function test_byyearday_upper_bound_validation() {
278         $rrule = "BYYEARDAY=1,366,367";
279         $mang = new rrule_manager($rrule);
280         $this->expectException('moodle_exception');
281         $mang->parse_rrule();
282     }
284     /**
285      * Test parsing of BYYEARDAY rule.
286      */
287     public function test_byyearday_0_validation() {
288         $rrule = "BYYEARDAY=0";
289         $mang = new rrule_manager($rrule);
290         $this->expectException('moodle_exception');
291         $mang->parse_rrule();
292     }
294     /**
295      * Test parsing of BYYEARDAY rule.
296      */
297     public function test_byyearday_lower_bound_validation() {
298         $rrule = "BYYEARDAY=-1,-366,-367";
299         $mang = new rrule_manager($rrule);
300         $this->expectException('moodle_exception');
301         $mang->parse_rrule();
302     }
304     /**
305      * Test parsing of BYWEEKNO rule.
306      */
307     public function test_non_yearly_freq_with_byweekno() {
308         $rrule = "BYWEEKNO=1,53";
309         $mang = new rrule_manager($rrule);
310         $this->expectException('moodle_exception');
311         $mang->parse_rrule();
312     }
314     /**
315      * Test parsing of BYWEEKNO rule.
316      */
317     public function test_byweekno_upper_bound_validation() {
318         $rrule = "FREQ=YEARLY;BYWEEKNO=1,53,54";
319         $mang = new rrule_manager($rrule);
320         $this->expectException('moodle_exception');
321         $mang->parse_rrule();
322     }
324     /**
325      * Test parsing of BYWEEKNO rule.
326      */
327     public function test_byweekno_0_validation() {
328         $rrule = "FREQ=YEARLY;BYWEEKNO=0";
329         $mang = new rrule_manager($rrule);
330         $this->expectException('moodle_exception');
331         $mang->parse_rrule();
332     }
334     /**
335      * Test parsing of BYWEEKNO rule.
336      */
337     public function test_byweekno_lower_bound_validation() {
338         $rrule = "FREQ=YEARLY;BYWEEKNO=-1,-53,-54";
339         $mang = new rrule_manager($rrule);
340         $this->expectException('moodle_exception');
341         $mang->parse_rrule();
342     }
344     /**
345      * Test parsing of BYMONTH rule.
346      */
347     public function test_bymonth_upper_bound_validation() {
348         $rrule = "BYMONTH=1,12,13";
349         $mang = new rrule_manager($rrule);
350         $this->expectException('moodle_exception');
351         $mang->parse_rrule();
352     }
354     /**
355      * Test parsing of BYMONTH rule.
356      */
357     public function test_bymonth_lower_bound_validation() {
358         $rrule = "BYMONTH=0";
359         $mang = new rrule_manager($rrule);
360         $this->expectException('moodle_exception');
361         $mang->parse_rrule();
362     }
364     /**
365      * Test parsing of BYSETPOS rule.
366      */
367     public function test_bysetpos_without_other_byrules() {
368         $rrule = "BYSETPOS=1,366";
369         $mang = new rrule_manager($rrule);
370         $this->expectException('moodle_exception');
371         $mang->parse_rrule();
372     }
374     /**
375      * Test parsing of BYSETPOS rule.
376      */
377     public function test_bysetpos_upper_bound_validation() {
378         $rrule = "BYSETPOS=1,366,367";
379         $mang = new rrule_manager($rrule);
380         $this->expectException('moodle_exception');
381         $mang->parse_rrule();
382     }
384     /**
385      * Test parsing of BYSETPOS rule.
386      */
387     public function test_bysetpos_0_validation() {
388         $rrule = "BYSETPOS=0";
389         $mang = new rrule_manager($rrule);
390         $this->expectException('moodle_exception');
391         $mang->parse_rrule();
392     }
394     /**
395      * Test parsing of BYSETPOS rule.
396      */
397     public function test_bysetpos_lower_bound_validation() {
398         $rrule = "BYSETPOS=-1,-366,-367";
399         $mang = new rrule_manager($rrule);
400         $this->expectException('moodle_exception');
401         $mang->parse_rrule();
402     }
404     /**
405      * Test recurrence rules for daily frequency.
406      */
407     public function test_daily_events() {
408         global $DB;
410         $rrule = 'FREQ=DAILY;COUNT=3'; // This should generate 2 child events + 1 parent.
411         $mang = new rrule_manager($rrule);
412         $mang->parse_rrule();
413         $mang->create_events($this->event);
414         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
415         $this->assertEquals(3, $count);
416         $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
417                 'timestart' => ($this->event->timestart + DAYSECS)));
418         $this->assertTrue($result);
419         $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
420                 'timestart' => ($this->event->timestart + 2 * DAYSECS)));
421         $this->assertTrue($result);
423         $until = $this->event->timestart + DAYSECS * 2;
424         $until = date('Y-m-d', $until);
425         $rrule = "FREQ=DAILY;UNTIL=$until"; // This should generate 1 child event + 1 parent,since by then until bound would be hit.
426         $mang = new rrule_manager($rrule);
427         $mang->parse_rrule();
428         $mang->create_events($this->event);
429         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
430         $this->assertEquals(2, $count);
431         $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
432                 'timestart' => ($this->event->timestart + DAYSECS)));
433         $this->assertTrue($result);
435         $rrule = 'FREQ=DAILY;COUNT=3;INTERVAL=3'; // This should generate 2 child events + 1 parent, every 3rd day.
436         $mang = new rrule_manager($rrule);
437         $mang->parse_rrule();
438         $mang->create_events($this->event);
439         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
440         $this->assertEquals(3, $count);
441         $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
442                 'timestart' => ($this->event->timestart + 3 * DAYSECS)));
443         $this->assertTrue($result);
444         $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
445                 'timestart' => ($this->event->timestart + 6 * DAYSECS)));
446         $this->assertTrue($result);
447     }
449     /**
450      * Every 300 days, forever.
451      */
452     public function test_every_300_days_forever() {
453         global $DB;
454         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
456         $interval = new DateInterval('P300D');
457         $untildate = new DateTime();
458         $untildate->add(new DateInterval('P10Y'));
459         $until = $untildate->getTimestamp();
461         // Forever event. This should generate events for time() + 10 year period, every 300 days.
462         $rrule = 'FREQ=DAILY;INTERVAL=300';
463         $mang = new rrule_manager($rrule);
464         $mang->parse_rrule();
465         $mang->create_events($this->event);
466         $records = $DB->get_records('event', array('repeatid' => $this->event->id), 'timestart ASC');
468         $expecteddate = clone($startdatetime);
469         foreach ($records as $record) {
470             $this->assertLessThanOrEqual($until, $record->timestart);
471             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
472             // Go to next iteration.
473             $expecteddate->add($interval);
474         }
475     }
477     /**
478      * Test recurrence rules for weekly frequency.
479      */
480     public function test_weekly_events() {
481         global $DB;
483         $rrule = 'FREQ=WEEKLY;COUNT=1';
484         $mang = new rrule_manager($rrule);
485         $mang->parse_rrule();
486         $mang->create_events($this->event);
487         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
488         $this->assertEquals(1, $count);
489         for ($i = 0; $i < $count; $i++) {
490             $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
491                     'timestart' => ($this->event->timestart + $i * DAYSECS)));
492             $this->assertTrue($result);
493         }
494         // This much seconds after the start of the day.
495         $offset = $this->event->timestart - mktime(0, 0, 0, date("n", $this->event->timestart), date("j", $this->event->timestart),
496                 date("Y", $this->event->timestart));
498         // This should generate 4 weekly Monday events.
499         $until = $this->event->timestart + WEEKSECS * 4;
500         $until = date('Ymd\This\Z', $until);
501         $rrule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=$until";
502         $mang = new rrule_manager($rrule);
503         $mang->parse_rrule();
504         $mang->create_events($this->event);
505         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
506         $this->assertEquals(4, $count);
507         $timestart = $this->event->timestart;
508         for ($i = 0; $i < $count; $i++) {
509             $timestart = strtotime("+$offset seconds next Monday", $timestart);
510             $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $timestart));
511             $this->assertTrue($result);
512         }
514         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
515         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
517         $offsetinterval = $startdatetime->diff($startdate, true);
518         $interval = new DateInterval('P3W');
520         // Every 3 weeks on Monday, Wednesday for 2 times.
521         $rrule = 'FREQ=WEEKLY;INTERVAL=3;BYDAY=MO,WE;COUNT=2';
522         $mang = new rrule_manager($rrule);
523         $mang->parse_rrule();
524         $mang->create_events($this->event);
526         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
527         $this->assertCount(2, $records);
529         $expecteddate = clone($startdate);
530         $expecteddate->modify('1997-09-03');
531         foreach ($records as $record) {
532             $expecteddate->add($offsetinterval);
533             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
535             if (date('D', $record->timestart) === 'Mon') {
536                 // Go to the fifth day of this month.
537                 $expecteddate->modify('next Wednesday');
538             } else {
539                 // Reset to Monday.
540                 $expecteddate->modify('last Monday');
541                 // Go to next period.
542                 $expecteddate->add($interval);
543             }
544         }
546         // Forever event. This should generate events over time() + 10 year period, every 50th Monday.
547         $rrule = 'FREQ=WEEKLY;BYDAY=MO;INTERVAL=50';
549         $mang = new rrule_manager($rrule);
550         $mang->parse_rrule();
551         $mang->create_events($this->event);
553         $untildate = new DateTime();
554         $untildate->add(new DateInterval('P10Y'));
555         $until = $untildate->getTimestamp();
557         $interval = new DateInterval('P50W');
558         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
560         // First instance of this set of recurring events: Monday, 17-08-1998.
561         $expecteddate = clone($startdate);
562         $expecteddate->modify('1998-08-17');
563         $expecteddate->add($offsetinterval);
564         foreach ($records as $record) {
565             $eventdateexpected = $expecteddate->format('Y-m-d H:i:s');
566             $eventdateactual = date('Y-m-d H:i:s', $record->timestart);
567             $this->assertEquals($eventdateexpected, $eventdateactual);
569             $expecteddate->add($interval);
570             $this->assertLessThanOrEqual($until, $record->timestart);
571         }
572     }
574     /**
575      * Test recurrence rules for monthly frequency for RRULE with COUNT and BYMONTHDAY rules set.
576      */
577     public function test_monthly_events_with_count_bymonthday() {
578         global $DB;
580         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
581         $interval = new DateInterval('P1M');
583         $rrule = "FREQ=MONTHLY;COUNT=3;BYMONTHDAY=2"; // This should generate 3 events in total.
584         $mang = new rrule_manager($rrule);
585         $mang->parse_rrule();
586         $mang->create_events($this->event);
587         $records = $DB->get_records('event', array('repeatid' => $this->event->id), 'timestart ASC');
588         $this->assertCount(3, $records);
590         $expecteddate = clone($startdatetime);
591         foreach ($records as $record) {
592             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
593             // Go to next month.
594             $expecteddate->add($interval);
595         }
596     }
598     /**
599      * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
600      */
601     public function test_monthly_events_with_until_bymonthday() {
602         global $DB;
604         // This should generate 10 child event + 1 parent, since by then until bound would be hit.
605         $until = strtotime('+1 day +10 months', $this->event->timestart);
606         $until = date('Ymd\This\Z', $until);
607         $rrule = "FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=$until";
608         $mang = new rrule_manager($rrule);
609         $mang->parse_rrule();
610         $mang->create_events($this->event);
611         $count = $DB->count_records('event', ['repeatid' => $this->event->id]);
612         $this->assertEquals(11, $count);
613         for ($i = 0; $i < 11; $i++) {
614             $time = strtotime("+$i month", $this->event->timestart);
615             $result = $DB->record_exists('event', ['repeatid' => $this->event->id, 'timestart' => $time]);
616             $this->assertTrue($result);
617         }
618     }
620     /**
621      * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
622      */
623     public function test_monthly_events_with_until_bymonthday_multi() {
624         global $DB;
626         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
627         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
628         $offsetinterval = $startdatetime->diff($startdate, true);
629         $interval = new DateInterval('P2M');
630         $untildate = clone($startdatetime);
631         $untildate->add(new DateInterval('P10M10D'));
632         $until = $untildate->format('Ymd\This\Z');
634         // This should generate 11 child event + 1 parent, since by then until bound would be hit.
635         $rrule = "FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=2,5;UNTIL=$until";
637         $mang = new rrule_manager($rrule);
638         $mang->parse_rrule();
639         $mang->create_events($this->event);
641         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
642         $this->assertCount(12, $records);
644         $expecteddate = clone($startdate);
645         $expecteddate->add($offsetinterval);
646         foreach ($records as $record) {
647             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
649             if (date('j', $record->timestart) == 2) {
650                 // Go to the fifth day of this month.
651                 $expecteddate->add(new DateInterval('P3D'));
652             } else {
653                 // Reset date to the first day of the month.
654                 $expecteddate->modify('first day of this month');
655                 // Go to next month period.
656                 $expecteddate->add($interval);
657                 // Go to the second day of the next month period.
658                 $expecteddate->modify('+1 day');
659             }
660         }
661     }
663     /**
664      * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY forever.
665      */
666     public function test_monthly_events_with_bymonthday_forever() {
667         global $DB;
669         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
670         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
672         $offsetinterval = $startdatetime->diff($startdate, true);
673         $interval = new DateInterval('P12M');
675         // Forever event. This should generate events over 10 year period, on 2nd day of every 12th month.
676         $rrule = "FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2";
678         $mang = new rrule_manager($rrule);
679         $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
681         $mang->parse_rrule();
682         $mang->create_events($this->event);
684         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
686         $expecteddate = clone($startdate);
687         $expecteddate->add($offsetinterval);
688         foreach ($records as $record) {
689             $this->assertLessThanOrEqual($until, $record->timestart);
691             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
693             // Reset date to the first day of the month.
694             $expecteddate->modify('first day of this month');
695             // Go to next month period.
696             $expecteddate->add($interval);
697             // Go to the second day of the next month period.
698             $expecteddate->modify('+1 day');
699         }
700     }
702     /**
703      * Test recurrence rules for monthly frequency for RRULE with COUNT and BYDAY rules set.
704      */
705     public function test_monthly_events_with_count_byday() {
706         global $DB;
708         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
709         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
711         $offsetinterval = $startdatetime->diff($startdate, true);
712         $interval = new DateInterval('P1M');
714         $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=1MO'; // This should generate 3 events in total, first monday of the month.
715         $mang = new rrule_manager($rrule);
716         $mang->parse_rrule();
717         $mang->create_events($this->event);
719         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
721         // First occurrence of this set of recurring events: 06-10-1997.
722         $expecteddate = clone($startdate);
723         $expecteddate->modify('1997-10-06');
724         $expecteddate->add($offsetinterval);
725         foreach ($records as $record) {
726             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
728             // Go to next month period.
729             $expecteddate->add($interval);
730             $expecteddate->modify('first Monday of this month');
731             $expecteddate->add($offsetinterval);
732         }
733     }
735     /**
736      * Test recurrence rules for monthly frequency for RRULE with BYDAY and UNTIL rules set.
737      */
738     public function test_monthly_events_with_until_byday() {
739         global $DB;
741         // This much seconds after the start of the day.
742         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
743         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
744         $offsetinterval = $startdatetime->diff($startdate, true);
746         $untildate = clone($startdatetime);
747         $untildate->add(new DateInterval('P10M1D'));
748         $until = $untildate->format('Ymd\This\Z');
750         // This rule should generate 9 events in total from first Monday of October 1997 to first Monday of June 1998.
751         $rrule = "FREQ=MONTHLY;BYDAY=1MO;UNTIL=$until";
752         $mang = new rrule_manager($rrule);
753         $mang->parse_rrule();
754         $mang->create_events($this->event);
756         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
757         $this->assertCount(9, $records);
759         $expecteddate = clone($startdate);
760         $expecteddate->modify('first Monday of October 1997');
761         foreach ($records as $record) {
762             $expecteddate->add($offsetinterval);
764             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
766             // Go to next month.
767             $expecteddate->modify('first day of next month');
768             // Go to the first Monday of the next month.
769             $expecteddate->modify('first Monday of this month');
770         }
771     }
773     /**
774      * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
775      */
776     public function test_monthly_events_with_until_byday_multi() {
777         global $DB;
779         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
780         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
782         $offsetinterval = $startdatetime->diff($startdate, true);
783         $interval = new DateInterval('P2M');
785         $untildate = clone($startdatetime);
786         $untildate->add(new DateInterval('P10M20D'));
787         $until = $untildate->format('Ymd\This\Z');
789         // This should generate 11 events from 17 Sep 1997 to 15 Jul 1998.
790         $rrule = "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,3WE;UNTIL=$until";
791         $mang = new rrule_manager($rrule);
792         $mang->parse_rrule();
793         $mang->create_events($this->event);
795         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
796         $this->assertCount(11, $records);
798         $expecteddate = clone($startdate);
799         $expecteddate->modify('1997-09-17');
800         foreach ($records as $record) {
801             $expecteddate->add($offsetinterval);
802             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
804             if (date('D', $record->timestart) === 'Mon') {
805                 // Go to the fifth day of this month.
806                 $expecteddate->modify('third Wednesday of this month');
807             } else {
808                 // Go to next month period.
809                 $expecteddate->add($interval);
810                 $expecteddate->modify('first Monday of this month');
811             }
812         }
813     }
815     /**
816      * Test recurrence rules for monthly frequency for RRULE with BYDAY forever.
817      */
818     public function test_monthly_events_with_byday_forever() {
819         global $DB;
821         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
822         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
824         $offsetinterval = $startdatetime->diff($startdate, true);
825         $interval = new DateInterval('P12M');
827         // Forever event. This should generate events over 10 year period, on 2nd day of every 12th month.
828         $rrule = "FREQ=MONTHLY;INTERVAL=12;BYDAY=1MO";
830         $mang = new rrule_manager($rrule);
831         $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
833         $mang->parse_rrule();
834         $mang->create_events($this->event);
836         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
838         $expecteddate = new DateTime('first Monday of September 1998');
839         foreach ($records as $record) {
840             $expecteddate->add($offsetinterval);
841             $this->assertLessThanOrEqual($until, $record->timestart);
843             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
845             // Go to next month period.
846             $expecteddate->add($interval);
847             // Reset date to the first Monday of the month.
848             $expecteddate->modify('first Monday of this month');
849         }
850     }
852     /**
853      * Test recurrence rules for yearly frequency.
854      */
855     public function test_yearly_events() {
856         global $DB;
858         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
859         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
861         $offsetinterval = $startdatetime->diff($startdate, true);
862         $interval = new DateInterval('P1Y');
864         $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9"; // This should generate 3 events in total.
865         $mang = new rrule_manager($rrule);
866         $mang->parse_rrule();
867         $mang->create_events($this->event);
869         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
870         $this->assertCount(3, $records);
872         $expecteddate = clone($startdatetime);
873         foreach ($records as $record) {
874             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
876             // Go to next period.
877             $expecteddate->add($interval);
878         }
880         // Create a yearly event, until the time limit is hit.
881         $until = strtotime('+20 day +10 years', $this->event->timestart);
882         $until = date('Ymd\THis\Z', $until);
883         $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until"; // Forever event.
884         $mang = new rrule_manager($rrule);
885         $mang->parse_rrule();
886         $mang->create_events($this->event);
887         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
888         $this->assertEquals(11, $count);
889         for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
890             $time = strtotime("+$yoffset years", $this->event->timestart)) {
891             $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
892                     'timestart' => ($time)));
893             $this->assertTrue($result);
894         }
896         // This should generate 5 events in total, every second year in the given month of the event.
897         $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5";
898         $mang = new rrule_manager($rrule);
899         $mang->parse_rrule();
900         $mang->create_events($this->event);
901         $count = $DB->count_records('event', array('repeatid' => $this->event->id));
902         $this->assertEquals(5, $count);
903         for ($i = 0, $time = $this->event->timestart; $i < 5; $i++, $yoffset = $i * 2,
904             $time = strtotime("+$yoffset years", $this->event->timestart)) {
905             $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
906                     'timestart' => ($time)));
907             $this->assertTrue($result);
908         }
910         $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2"; // Forever event.
911         $mang = new rrule_manager($rrule);
912         $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
913         $mang->parse_rrule();
914         $mang->create_events($this->event);
915         for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
916             $time = strtotime("+$yoffset years", $this->event->timestart)) {
917             $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
918                     'timestart' => ($time)));
919             $this->assertTrue($result);
920         }
922         $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9;BYDAY=1MO"; // This should generate 3 events in total.
923         $mang = new rrule_manager($rrule);
924         $mang->parse_rrule();
925         $mang->create_events($this->event);
927         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
928         $this->assertCount(3, $records);
930         $expecteddate = clone($startdatetime);
931         $expecteddate->modify('first Monday of September 1998');
932         $expecteddate->add($offsetinterval);
933         foreach ($records as $record) {
934             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
936             // Go to next period.
937             $expecteddate->add($interval);
938             $monthyear = $expecteddate->format('F Y');
939             $expecteddate->modify('first Monday of ' . $monthyear);
940             $expecteddate->add($offsetinterval);
941         }
943         // Create a yearly event on the specified month, until the time limit is hit.
944         $untildate = clone($startdatetime);
945         $untildate->add(new DateInterval('P10Y20D'));
946         $until = $untildate->format('Ymd\THis\Z');
948         $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until;BYDAY=1MO";
949         $mang = new rrule_manager($rrule);
950         $mang->parse_rrule();
951         $mang->create_events($this->event);
953         // 10 yearly records every first Monday of September 1998 to first Monday of September 2007.
954         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
955         $this->assertCount(10, $records);
957         $expecteddate = clone($startdatetime);
958         $expecteddate->modify('first Monday of September 1998');
959         $expecteddate->add($offsetinterval);
960         foreach ($records as $record) {
961             $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart);
962             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
964             // Go to next period.
965             $expecteddate->add($interval);
966             $monthyear = $expecteddate->format('F Y');
967             $expecteddate->modify('first Monday of ' . $monthyear);
968             $expecteddate->add($offsetinterval);
969         }
971         // This should generate 5 events in total, every second year in the month of September.
972         $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5;BYDAY=1MO";
973         $mang = new rrule_manager($rrule);
974         $mang->parse_rrule();
975         $mang->create_events($this->event);
977         // 5 bi-yearly records every first Monday of September 1998 to first Monday of September 2007.
978         $interval = new DateInterval('P2Y');
979         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
980         $this->assertCount(5, $records);
982         $expecteddate = clone($startdatetime);
983         $expecteddate->modify('first Monday of September 1999');
984         $expecteddate->add($offsetinterval);
985         foreach ($records as $record) {
986             $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart);
987             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
989             // Go to next period.
990             $expecteddate->add($interval);
991             $monthyear = $expecteddate->format('F Y');
992             $expecteddate->modify('first Monday of ' . $monthyear);
993             $expecteddate->add($offsetinterval);
994         }
995     }
997     /**
998      * Test for rrule with FREQ=YEARLY with BYMONTH and BYDAY rules set, recurring forever.
999      */
1000     public function test_yearly_bymonth_byday_forever() {
1001         global $DB;
1003         // Every 2 years on the first Monday of September.
1004         $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;BYDAY=1MO";
1005         $mang = new rrule_manager($rrule);
1006         $mang->parse_rrule();
1007         $mang->create_events($this->event);
1009         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1011         $untildate = new DateTime();
1012         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1013         $untiltimestamp = $untildate->getTimestamp();
1015         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1016         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1018         $offsetinterval = $startdatetime->diff($startdate, true);
1019         $interval = new DateInterval('P2Y');
1021         // First occurrence of this set of events is on the first Monday of September 1999.
1022         $expecteddate = clone($startdatetime);
1023         $expecteddate->modify('first Monday of September 1999');
1024         $expecteddate->add($offsetinterval);
1025         foreach ($records as $record) {
1026             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1027             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1029             // Go to next period.
1030             $expecteddate->add($interval);
1031             $monthyear = $expecteddate->format('F Y');
1032             $expecteddate->modify('first Monday of ' . $monthyear);
1033             $expecteddate->add($offsetinterval);
1034         }
1035     }
1037     /**
1038      * Test for rrule with FREQ=YEARLY recurring forever.
1039      */
1040     public function test_yearly_forever() {
1041         global $DB;
1043         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1045         $interval = new DateInterval('P2Y');
1047         $rrule = 'FREQ=YEARLY;INTERVAL=2'; // Forever event.
1048         $mang = new rrule_manager($rrule);
1049         $mang->parse_rrule();
1050         $mang->create_events($this->event);
1052         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1054         $untildate = new DateTime();
1055         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1056         $untiltimestamp = $untildate->getTimestamp();
1058         $expecteddate = clone($startdatetime);
1059         foreach ($records as $record) {
1060             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1061             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1063             // Go to next period.
1064             $expecteddate->add($interval);
1065         }
1066     }
1068     /******************************************************************************************************************************/
1069     /* Tests based on the examples from the RFC.                                                                                  */
1070     /******************************************************************************************************************************/
1072     /**
1073      * Daily for 10 occurrences:
1074      *
1075      * DTSTART;TZID=US-Eastern:19970902T090000
1076      * RRULE:FREQ=DAILY;COUNT=10
1077      *   ==> (1997 9:00 AM EDT)September 2-11
1078      */
1079     public function test_daily_count() {
1080         global $DB;
1082         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1083         $interval = new DateInterval('P1D');
1085         $rrule = 'FREQ=DAILY;COUNT=10';
1086         $mang = new rrule_manager($rrule);
1087         $mang->parse_rrule();
1088         $mang->create_events($this->event);
1090         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1091         $this->assertCount(10, $records);
1093         $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1094         foreach ($records as $record) {
1095             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1097             // Go to next period.
1098             $expecteddate->add($interval);
1099         }
1100     }
1102     /**
1103      * Daily until December 24, 1997:
1104      *
1105      * DTSTART;TZID=US-Eastern:19970902T090000
1106      * RRULE:FREQ=DAILY;UNTIL=19971224T000000Z
1107      *   ==> (1997 9:00 AM EDT)September 2-30;October 1-25
1108      *       (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23
1109      */
1110     public function test_daily_until() {
1111         global $DB;
1113         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1114         $interval = new DateInterval('P1D');
1116         $untildate = new DateTime('19971224T000000Z');
1117         $untiltimestamp = $untildate->getTimestamp();
1119         $rrule = 'FREQ=DAILY;UNTIL=19971224T000000Z';
1120         $mang = new rrule_manager($rrule);
1121         $mang->parse_rrule();
1122         $mang->create_events($this->event);
1124         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1125         // 113 daily events from 02-09-1997 to 23-12-1997.
1126         $this->assertCount(113, $records);
1128         $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1129         foreach ($records as $record) {
1130             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1131             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1132             // Go to next period.
1133             $expecteddate->add($interval);
1134         }
1135     }
1137     /**
1138      * Every other day - forever:
1139      *
1140      * DTSTART;TZID=US-Eastern:19970902T090000
1141      * RRULE:FREQ=DAILY;INTERVAL=2
1142      *   ==> (1997 9:00 AM EDT)September2,4,6,8...24,26,28,30;October 2,4,6...20,22,24
1143      *       (1997 9:00 AM EST)October 26,28,30;November 1,3,5,7...25,27,29;Dec 1,3,...
1144      */
1145     public function test_every_other_day_forever() {
1146         global $DB;
1148         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1149         $interval = new DateInterval('P2D');
1151         $rrule = 'FREQ=DAILY;INTERVAL=2';
1152         $mang = new rrule_manager($rrule);
1153         $mang->parse_rrule();
1154         $mang->create_events($this->event);
1156         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1158         $untildate = new DateTime();
1159         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1160         $untiltimestamp = $untildate->getTimestamp();
1162         $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1163         foreach ($records as $record) {
1164             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1166             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1167             // Go to next period.
1168             $expecteddate->add($interval);
1169         }
1170     }
1172     /**
1173      * Every 10 days, 5 occurrences:
1174      *
1175      * DTSTART;TZID=US-Eastern:19970902T090000
1176      * RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5
1177      *   ==> (1997 9:00 AM EDT)September 2,12,22;October 2,12
1178      */
1179     public function test_every_10_days_5_count() {
1180         global $DB;
1182         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1183         $interval = new DateInterval('P10D');
1185         $rrule = 'FREQ=DAILY;INTERVAL=10;COUNT=5';
1186         $mang = new rrule_manager($rrule);
1187         $mang->parse_rrule();
1188         $mang->create_events($this->event);
1190         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1191         $this->assertCount(5, $records);
1193         $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1194         foreach ($records as $record) {
1195             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1196             // Go to next period.
1197             $expecteddate->add($interval);
1198         }
1199     }
1201     /**
1202      * Everyday in January, for 3 years:
1203      *
1204      * DTSTART;TZID=US-Eastern:19980101T090000
1205      * RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
1206      *   ==> (1998 9:00 AM EDT)January 1-31
1207      *       (1999 9:00 AM EDT)January 1-31
1208      *       (2000 9:00 AM EDT)January 1-31
1209      */
1210     public function test_everyday_in_jan_for_3_years_yearly() {
1211         global $DB;
1213         // Change our event's date to 01-01-1998, based on the example from the RFC.
1214         $this->change_event_startdate('19980101T090000', 'US/Eastern');
1216         $rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
1217         $mang = new rrule_manager($rrule);
1218         $mang->parse_rrule();
1219         $mang->create_events($this->event);
1221         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1222         // 92 events from 01-01-1998 to 03-01-2000.
1223         $this->assertCount(92, $records);
1225         $untildate = new DateTime('20000131T090000Z');
1226         $untiltimestamp = $untildate->getTimestamp();
1227         foreach ($records as $record) {
1228             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1230             // Assert that the event's date is in January.
1231             $this->assertEquals('January', date('F', $record->timestart));
1232         }
1233     }
1235     /**
1236      * Everyday in January, for 3 years:
1237      *
1238      * DTSTART;TZID=US-Eastern:19980101T090000
1239      * RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1
1240      *   ==> (1998 9:00 AM EDT)January 1-31
1241      *       (1999 9:00 AM EDT)January 1-31
1242      *       (2000 9:00 AM EDT)January 1-31
1243      */
1244     public function test_everyday_in_jan_for_3_years_daily() {
1245         global $DB;
1247         // Change our event's date to 01-01-1998, based on the example from the RFC.
1248         $this->change_event_startdate('19980101T090000', 'US/Eastern');
1250         $rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
1251         $mang = new rrule_manager($rrule);
1252         $mang->parse_rrule();
1253         $mang->create_events($this->event);
1255         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1256         // 92 events from 01-01-1998 to 03-01-2000.
1257         $this->assertCount(92, $records);
1259         $untildate = new DateTime('20000131T090000Z');
1260         $untiltimestamp = $untildate->getTimestamp();
1261         foreach ($records as $record) {
1262             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1264             // Assert that the event's date is in January.
1265             $this->assertEquals('January', date('F', $record->timestart));
1266         }
1267     }
1269     /**
1270      * Weekly for 10 occurrences
1271      *
1272      * DTSTART;TZID=US-Eastern:19970902T090000
1273      * RRULE:FREQ=WEEKLY;COUNT=10
1274      *   ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21
1275      *       (1997 9:00 AM EST)October 28;November 4
1276      */
1277     public function test_weekly_10_count() {
1278         global $DB;
1280         $interval = new DateInterval('P1W');
1282         $rrule = 'FREQ=WEEKLY;COUNT=10';
1283         $mang = new rrule_manager($rrule);
1284         $mang->parse_rrule();
1285         $mang->create_events($this->event);
1287         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1288         $this->assertCount(10, $records);
1290         $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1291         foreach ($records as $record) {
1292             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1293             // Go to next period.
1294             $expecteddate->add($interval);
1295         }
1296     }
1298     /**
1299      * Weekly until December 24, 1997.
1300      *
1301      * DTSTART;TZID=US-Eastern:19970902T090000
1302      * RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z
1303      *   ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21,28
1304      *       (1997 9:00 AM EST)November 4,11,18,25;December 2,9,16,23
1305      */
1306     public function test_weekly_until_24_dec_1997() {
1307         global $DB;
1309         $interval = new DateInterval('P1W');
1311         $rrule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z';
1312         $mang = new rrule_manager($rrule);
1313         $mang->parse_rrule();
1314         $mang->create_events($this->event);
1316         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1317         // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1318         $this->assertCount(17, $records);
1320         $untildate = new DateTime('19971224T000000Z');
1321         $untiltimestamp = $untildate->getTimestamp();
1322         $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1323         foreach ($records as $record) {
1324             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1325             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1326             // Go to next period.
1327             $expecteddate->add($interval);
1328         }
1329     }
1331     /**
1332      * Every other week - forever:
1333      *
1334      * DTSTART;TZID=US-Eastern:19970902T090000
1335      * RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU
1336      *   ==> (1997 9:00 AM EDT)September 2,16,30;October 14
1337      *       (1997 9:00 AM EST)October 28;November 11,25;December 9,23
1338      *       (1998 9:00 AM EST)January 6,20;February
1339      *        ...
1340      */
1341     public function test_every_other_week_forever() {
1342         global $DB;
1344         $interval = new DateInterval('P2W');
1346         $rrule = 'FREQ=WEEKLY;INTERVAL=2;WKST=SU';
1347         $mang = new rrule_manager($rrule);
1348         $mang->parse_rrule();
1349         $mang->create_events($this->event);
1351         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1353         $untildate = new DateTime();
1354         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1355         $untiltimestamp = $untildate->getTimestamp();
1357         $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1358         foreach ($records as $record) {
1359             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1361             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1362             // Go to next period.
1363             $expecteddate->add($interval);
1364         }
1365     }
1367     /**
1368      * Weekly on Tuesday and Thursday for 5 weeks:
1369      *
1370      * DTSTART;TZID=US-Eastern:19970902T090000
1371      * RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH
1372      *   ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
1373      */
1374     public function test_weekly_on_tue_thu_for_5_weeks_by_until() {
1375         global $DB;
1377         $rrule = 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH';
1378         $mang = new rrule_manager($rrule);
1379         $mang->parse_rrule();
1380         $mang->create_events($this->event);
1382         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1383         // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1384         $this->assertCount(10, $records);
1386         $untildate = new DateTime('19971007T000000Z');
1387         $untiltimestamp = $untildate->getTimestamp();
1388         $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1389         $startdate = new DateTime($expecteddate->format('Y-m-d'));
1390         $offset = $expecteddate->diff($startdate, true);
1391         foreach ($records as $record) {
1392             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1394             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1395             // Go to next period.
1396             if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) {
1397                 $expecteddate->modify('next Thursday');
1398             } else {
1399                 $expecteddate->modify('next Tuesday');
1400             }
1401             $expecteddate->add($offset);
1402         }
1403     }
1405     /**
1406      * Weekly on Tuesday and Thursday for 5 weeks:
1407      *
1408      * DTSTART;TZID=US-Eastern:19970902T090000
1409      * RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH
1410      *   ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
1411      */
1412     public function test_weekly_on_tue_thu_for_5_weeks_by_count() {
1413         global $DB;
1415         $rrule = 'FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH';
1416         $mang = new rrule_manager($rrule);
1417         $mang->parse_rrule();
1418         $mang->create_events($this->event);
1420         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1421         // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1422         $this->assertCount(10, $records);
1424         $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1425         $startdate = new DateTime($expecteddate->format('Y-m-d'));
1426         $offset = $expecteddate->diff($startdate, true);
1427         foreach ($records as $record) {
1428             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1429             // Go to next period.
1430             if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) {
1431                 $expecteddate->modify('next Thursday');
1432             } else {
1433                 $expecteddate->modify('next Tuesday');
1434             }
1435             $expecteddate->add($offset);
1436         }
1437     }
1439     /**
1440      * Every other week on Monday, Wednesday and Friday until December 24, 1997, but starting on Tuesday, September 2, 1997:
1441      *
1442      * DTSTART;TZID=US-Eastern:19970902T090000
1443      * RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR
1444      *   ==> (1997 9:00 AM EDT)September 3,5,15,17,19,29;October 1,3,13,15,17
1445      *       (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;December 8,10,12,22
1446      */
1447     public function test_every_other_week_until_24_dec_1997_byday() {
1448         global $DB;
1450         $rrule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR';
1451         $mang = new rrule_manager($rrule);
1452         $mang->parse_rrule();
1453         $mang->create_events($this->event);
1455         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1456         // 24 iterations every M-W-F from 03-09-1997 13:00 UTC to 22-12-1997 13:00 UTC.
1457         $this->assertCount(24, $records);
1459         $untildate = new DateTime('19971224T000000Z');
1460         $untiltimestamp = $untildate->getTimestamp();
1462         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1463         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1465         $offsetinterval = $startdatetime->diff($startdate, true);
1467         // First occurrence of this set of events is on 3 September 1999.
1468         $expecteddate = clone($startdatetime);
1469         $expecteddate->modify('next Wednesday');
1470         $expecteddate->add($offsetinterval);
1471         foreach ($records as $record) {
1472             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1473             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1475             // Go to next period.
1476             switch ($expecteddate->format('l')) {
1477                 case rrule_manager::DAY_MONDAY:
1478                     $expecteddate->modify('next Wednesday');
1479                     break;
1480                 case rrule_manager::DAY_WEDNESDAY:
1481                     $expecteddate->modify('next Friday');
1482                     break;
1483                 default:
1484                     $expecteddate->modify('next Monday');
1485                     // Increment expected date by 1 week if the next day is Monday.
1486                     $expecteddate->add(new DateInterval('P1W'));
1487                     break;
1488             }
1489             $expecteddate->add($offsetinterval);
1490         }
1491     }
1493     /**
1494      * Every other week on Tuesday and Thursday, for 8 occurrences:
1495      *
1496      * DTSTART;TZID=US-Eastern:19970902T090000
1497      * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH
1498      *   ==> (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16
1499      */
1500     public function test_every_other_week_byday_8_count() {
1501         global $DB;
1503         $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH';
1504         $mang = new rrule_manager($rrule);
1505         $mang->parse_rrule();
1506         $mang->create_events($this->event);
1508         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1509         // Should correspond to COUNT rule.
1510         $this->assertCount(8, $records);
1512         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1513         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1515         $offsetinterval = $startdatetime->diff($startdate, true);
1517         // First occurrence of this set of events is on 2 September 1999.
1518         $expecteddate = clone($startdatetime);
1519         foreach ($records as $record) {
1520             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1522             // Go to next period.
1523             switch ($expecteddate->format('l')) {
1524                 case rrule_manager::DAY_TUESDAY:
1525                     $expecteddate->modify('next Thursday');
1526                     break;
1527                 default:
1528                     $expecteddate->modify('next Tuesday');
1529                     // Increment expected date by 1 week if the next day is Tuesday.
1530                     $expecteddate->add(new DateInterval('P1W'));
1531                     break;
1532             }
1533             $expecteddate->add($offsetinterval);
1534         }
1535     }
1537     /**
1538      * Monthly on the 1st Friday for ten occurrences:
1539      *
1540      * DTSTART;TZID=US-Eastern:19970905T090000
1541      * RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR
1542      *   ==> (1997 9:00 AM EDT)September 5;October 3
1543      *       (1997 9:00 AM EST)November 7;Dec 5
1544      *       (1998 9:00 AM EST)January 2;February 6;March 6;April 3
1545      *       (1998 9:00 AM EDT)May 1;June 5
1546      */
1547     public function test_monthly_every_first_friday_10_count() {
1548         global $DB;
1550         // Change our event's date to 05-09-1997, based on the example from the RFC.
1551         $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
1552         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1553         $offsetinterval = $startdatetime->diff($startdate, true);
1555         $rrule = 'FREQ=MONTHLY;COUNT=10;BYDAY=1FR';
1556         $mang = new rrule_manager($rrule);
1557         $mang->parse_rrule();
1558         $mang->create_events($this->event);
1560         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1561         // Should correspond to COUNT rule.
1562         $this->assertCount(10, $records);
1564         foreach ($records as $record) {
1565             // Get the first Friday of the record's month.
1566             $recordmonthyear = date('F Y', $record->timestart);
1567             $expecteddate = new DateTime('first Friday of ' . $recordmonthyear);
1568             // Add the time of the event.
1569             $expecteddate->add($offsetinterval);
1571             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1572         }
1573     }
1575     /**
1576      * Monthly on the 1st Friday until December 24, 1997:
1577      *
1578      * DTSTART;TZID=US-Eastern:19970905T090000
1579      * RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR
1580      *   ==> (1997 9:00 AM EDT)September 5;October 3
1581      *       (1997 9:00 AM EST)November 7;December 5
1582      */
1583     public function test_monthly_every_first_friday_until() {
1584         global $DB;
1586         // Change our event's date to 05-09-1997, based on the example from the RFC.
1587         $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
1588         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1589         $offsetinterval = $startdatetime->diff($startdate, true);
1591         $rrule = 'FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR';
1592         $mang = new rrule_manager($rrule);
1593         $mang->parse_rrule();
1594         $mang->create_events($this->event);
1596         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1597         // Should have 4 events, every first friday of September 1997 to December 1997.
1598         $this->assertCount(4, $records);
1600         foreach ($records as $record) {
1601             // Get the first Friday of the record's month.
1602             $recordmonthyear = date('F Y', $record->timestart);
1603             $expecteddate = new DateTime('first Friday of ' . $recordmonthyear);
1604             // Add the time of the event.
1605             $expecteddate->add($offsetinterval);
1607             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1608         }
1609     }
1611     /**
1612      * Every other month on the 1st and last Sunday of the month for 10 occurrences:
1613      *
1614      * DTSTART;TZID=US-Eastern:19970907T090000
1615      * RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
1616      *   ==> (1997 9:00 AM EDT)September 7,28
1617      *       (1997 9:00 AM EST)November 2,30
1618      *       (1998 9:00 AM EST)January 4,25;March 1,29
1619      *       (1998 9:00 AM EDT)May 3,31
1620      */
1621     public function test_every_other_month_1st_and_last_sunday_10_count() {
1622         global $DB;
1624         // Change our event's date to 05-09-1997, based on the example from the RFC.
1625         $startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
1626         $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1627         $offsetinterval = $startdatetime->diff($startdate, true);
1629         $rrule = 'FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU';
1630         $mang = new rrule_manager($rrule);
1631         $mang->parse_rrule();
1632         $mang->create_events($this->event);
1634         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1635         // Should have 10 records based on COUNT rule.
1636         $this->assertCount(10, $records);
1638         // First occurrence is 07-09-1997 which is the first Sunday.
1639         $ordinal = 'first';
1640         foreach ($records as $record) {
1641             // Get date of the month's first/last Sunday.
1642             $recordmonthyear = date('F Y', $record->timestart);
1643             $expecteddate = new DateTime($ordinal . ' Sunday of ' . $recordmonthyear);
1644             $expecteddate->add($offsetinterval);
1646             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1647             if ($ordinal === 'first') {
1648                 $ordinal = 'last';
1649             } else {
1650                 $ordinal = 'first';
1651             }
1652         }
1653     }
1655     /**
1656      * Monthly on the second to last Monday of the month for 6 months:
1657      *
1658      * DTSTART;TZID=US-Eastern:19970922T090000
1659      * RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO
1660      *   ==> (1997 9:00 AM EDT)September 22;October 20
1661      *       (1997 9:00 AM EST)November 17;December 22
1662      *       (1998 9:00 AM EST)January 19;February 16
1663      */
1664     public function test_monthly_last_monday_for_6_months() {
1665         global $DB;
1667         // Change our event's date to 05-09-1997, based on the example from the RFC.
1668         $startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
1669         $startdate = new DateTime($startdatetime->format('Y-m-d'));
1670         $offsetinterval = $startdatetime->diff($startdate, true);
1672         $rrule = 'FREQ=MONTHLY;COUNT=6;BYDAY=-2MO';
1673         $mang = new rrule_manager($rrule);
1674         $mang->parse_rrule();
1675         $mang->create_events($this->event);
1677         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1678         // Should have 6 records based on COUNT rule.
1679         $this->assertCount(6, $records);
1681         foreach ($records as $record) {
1682             // Get date of the month's last Monday.
1683             $recordmonthyear = date('F Y', $record->timestart);
1684             $expecteddate = new DateTime('last Monday of ' . $recordmonthyear);
1685             // Modify to get the second to the last Monday.
1686             $expecteddate->modify('last Monday');
1687             // Add offset.
1688             $expecteddate->add($offsetinterval);
1690             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1691         }
1692     }
1694     /**
1695      * Monthly on the third to the last day of the month, forever:
1696      *
1697      * DTSTART;TZID=US-Eastern:19970928T090000
1698      * RRULE:FREQ=MONTHLY;BYMONTHDAY=-3
1699      *   ==> (1997 9:00 AM EDT)September 28
1700      *       (1997 9:00 AM EST)October 29;November 28;December 29
1701      *       (1998 9:00 AM EST)January 29;February 26
1702      *       ...
1703      */
1704     public function test_third_to_the_last_day_of_the_month_forever() {
1705         global $DB;
1707         // Change our event's date to 05-09-1997, based on the example from the RFC.
1708         $this->change_event_startdate('19970928T090000', 'US/Eastern');
1710         $rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
1711         $mang = new rrule_manager($rrule);
1712         $mang->parse_rrule();
1713         $mang->create_events($this->event);
1715         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1717         $untildate = new DateTime();
1718         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1719         $untiltimestamp = $untildate->getTimestamp();
1721         $subinterval = new DateInterval('P2D');
1722         foreach ($records as $record) {
1723             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1725             // Get date of the third to the last day of the month.
1726             $recordmonthyear = date('F Y', $record->timestart);
1727             $expecteddate = new DateTime('last day of ' . $recordmonthyear);
1728             // Set time to 9am.
1729             $expecteddate->setTime(9, 0);
1730             // Modify to get the third to the last day of the month.
1731             $expecteddate->sub($subinterval);
1733             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1734         }
1735     }
1737     /**
1738      * Monthly on the 2nd and 15th of the month for 10 occurrences:
1739      *
1740      * DTSTART;TZID=US-Eastern:19970902T090000
1741      * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15
1742      *   ==> (1997 9:00 AM EDT)September 2,15;October 2,15
1743      *       (1997 9:00 AM EST)November 2,15;December 2,15
1744      *       (1998 9:00 AM EST)January 2,15
1745      */
1746     public function test_every_2nd_and_15th_of_the_month_10_count() {
1747         global $DB;
1749         $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1750         $startdate = new DateTime($startdatetime->format('Y-m-d'));
1751         $offsetinterval = $startdatetime->diff($startdate, true);
1753         $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15';
1754         $mang = new rrule_manager($rrule);
1755         $mang->parse_rrule();
1756         $mang->create_events($this->event);
1758         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1759         // Should have 10 records based on COUNT rule.
1760         $this->assertCount(10, $records);
1762         $day = '02';
1763         foreach ($records as $record) {
1764             // Get the first Friday of the record's month.
1765             $recordmonthyear = date('Y-m', $record->timestart);
1767             // Get date of the month's last Monday.
1768             $expecteddate = new DateTime("$recordmonthyear-$day");
1769             // Add offset.
1770             $expecteddate->add($offsetinterval);
1771             if ($day === '02') {
1772                 $day = '15';
1773             } else {
1774                 $day = '02';
1775             }
1777             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1778         }
1779     }
1781     /**
1782      * Monthly on the first and last day of the month for 10 occurrences:
1783      *
1784      * DTSTART;TZID=US-Eastern:19970930T090000
1785      * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1
1786      *   ==> (1997 9:00 AM EDT)September 30;October 1
1787      *       (1997 9:00 AM EST)October 31;November 1,30;December 1,31
1788      *       (1998 9:00 AM EST)January 1,31;February 1
1789      */
1790     public function test_every_first_and_last_day_of_the_month_10_count() {
1791         global $DB;
1793         $startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
1794         $startdate = new DateTime($startdatetime->format('Y-m-d'));
1795         $offsetinterval = $startdatetime->diff($startdate, true);
1797         $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1';
1798         $mang = new rrule_manager($rrule);
1799         $mang->parse_rrule();
1800         $mang->create_events($this->event);
1802         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1803         // Should have 10 records based on COUNT rule.
1804         $this->assertCount(10, $records);
1806         // First occurrence is 30-Sep-1997.
1807         $day = 'last';
1808         foreach ($records as $record) {
1809             // Get the first Friday of the record's month.
1810             $recordmonthyear = date('F Y', $record->timestart);
1812             // Get date of the month's last Monday.
1813             $expecteddate = new DateTime("$day day of $recordmonthyear");
1814             // Add offset.
1815             $expecteddate->add($offsetinterval);
1817             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1819             if ($day === 'first') {
1820                 $day = 'last';
1821             } else {
1822                 $day = 'first';
1823             }
1824         }
1825     }
1827     /**
1828      * Every 18 months on the 10th thru 15th of the month for 10 occurrences:
1829      *
1830      * DTSTART;TZID=US-Eastern:19970910T090000
1831      * RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15
1832      *   ==> (1997 9:00 AM EDT)September 10,11,12,13,14,15
1833      *       (1999 9:00 AM EST)March 10,11,12,13
1834      */
1835     public function test_every_18_months_days_10_to_15_10_count() {
1836         global $DB;
1838         $startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
1840         $rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
1841         $mang = new rrule_manager($rrule);
1842         $mang->parse_rrule();
1843         $mang->create_events($this->event);
1845         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1846         // Should have 10 records based on COUNT rule.
1847         $this->assertCount(10, $records);
1849         // First occurrence is 10-Sep-1997.
1850         $expecteddate = clone($startdatetime);
1851         $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
1852         foreach ($records as $record) {
1853             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1855             // Get next expected date.
1856             if ($expecteddate->format('d') == 15) {
1857                 // If 15th, increment by 18 months.
1858                 $expecteddate->add(new DateInterval('P18M'));
1859                 // Then go back to the 10th.
1860                 $expecteddate->sub(new DateInterval('P5D'));
1861             } else {
1862                 // Otherwise, increment by 1 day.
1863                 $expecteddate->add(new DateInterval('P1D'));
1864             }
1865         }
1866     }
1868     /**
1869      * Every Tuesday, every other month:
1870      *
1871      * DTSTART;TZID=US-Eastern:19970902T090000
1872      * RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU
1873      *   ==> (1997 9:00 AM EDT)September 2,9,16,23,30
1874      *       (1997 9:00 AM EST)November 4,11,18,25
1875      *       (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31
1876      *       ...
1877      */
1878     public function test_every_tuesday_every_other_month_forever() {
1879         global $DB;
1881         $rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
1882         $mang = new rrule_manager($rrule);
1883         $mang->parse_rrule();
1884         $mang->create_events($this->event);
1886         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1888         $untildate = new DateTime();
1889         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1890         $untiltimestamp = $untildate->getTimestamp();
1892         $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1893         $nextmonth = new DateTime($expecteddate->format('Y-m-d'));
1894         $offset = $expecteddate->diff($nextmonth, true);
1895         $nextmonth->modify('first day of next month');
1896         foreach ($records as $record) {
1897             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1899             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1901             // Get next expected date.
1902             $expecteddate->modify('next Tuesday');
1903             if ($expecteddate->getTimestamp() >= $nextmonth->getTimestamp()) {
1904                 // Go to the end of the month.
1905                 $expecteddate->modify('last day of this month');
1906                 // Find the next Tuesday.
1907                 $expecteddate->modify('next Tuesday');
1909                 // Increment next month by 2 months.
1910                 $nextmonth->add(new DateInterval('P2M'));
1911             }
1912             $expecteddate->add($offset);
1913         }
1914     }
1916     /**
1917      * Yearly in June and July for 10 occurrences:
1918      *
1919      * DTSTART;TZID=US-Eastern:19970610T090000
1920      * RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7
1921      *   ==> (1997 9:00 AM EDT)June 10;July 10
1922      *       (1998 9:00 AM EDT)June 10;July 10
1923      *       (1999 9:00 AM EDT)June 10;July 10
1924      *       (2000 9:00 AM EDT)June 10;July 10
1925      *       (2001 9:00 AM EDT)June 10;July 10
1926      * Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components are specified, the day is gotten from DTSTART.
1927      */
1928     public function test_yearly_in_june_july_10_count() {
1929         global $DB;
1931         $startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
1933         $rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
1934         $mang = new rrule_manager($rrule);
1935         $mang->parse_rrule();
1936         $mang->create_events($this->event);
1938         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1939         // Should have 10 records based on COUNT rule.
1940         $this->assertCount(10, $records);
1942         $expecteddate = $startdatetime;
1943         $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
1944         $monthinterval = new DateInterval('P1M');
1945         $yearinterval = new DateInterval('P1Y');
1946         foreach ($records as $record) {
1947             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1949             // Get next expected date.
1950             if ($expecteddate->format('m') == 6) {
1951                 // Go to the month of July.
1952                 $expecteddate->add($monthinterval);
1953             } else {
1954                 // Go to the month of June next year.
1955                 $expecteddate->sub($monthinterval);
1956                 $expecteddate->add($yearinterval);
1957             }
1958         }
1959     }
1961     /**
1962      * Every other year on January, February, and March for 10 occurrences:
1963      *
1964      * DTSTART;TZID=US-Eastern:19970310T090000
1965      * RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3
1966      *   ==> (1997 9:00 AM EST)March 10
1967      *       (1999 9:00 AM EST)January 10;February 10;March 10
1968      *       (2001 9:00 AM EST)January 10;February 10;March 10
1969      *       (2003 9:00 AM EST)January 10;February 10;March 10
1970      */
1971     public function test_every_other_year_in_june_july_10_count() {
1972         global $DB;
1974         $startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
1976         $rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
1977         $mang = new rrule_manager($rrule);
1978         $mang->parse_rrule();
1979         $mang->create_events($this->event);
1981         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1982         // Should have 10 records based on COUNT rule.
1983         $this->assertCount(10, $records);
1985         $expecteddate = $startdatetime;
1986         $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
1987         $monthinterval = new DateInterval('P1M');
1988         $yearinterval = new DateInterval('P2Y');
1989         foreach ($records as $record) {
1990             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1992             // Get next expected date.
1993             if ($expecteddate->format('m') != 3) {
1994                 // Go to the next month.
1995                 $expecteddate->add($monthinterval);
1996             } else {
1997                 // Go to the month of January next year.
1998                 $expecteddate->sub($monthinterval);
1999                 $expecteddate->sub($monthinterval);
2000                 $expecteddate->add($yearinterval);
2001             }
2002         }
2003     }
2005     /**
2006      * Every 3rd year on the 1st, 100th and 200th day for 10 occurrences:
2007      *
2008      * DTSTART;TZID=US-Eastern:19970101T090000
2009      * RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200
2010      *   ==> (1997 9:00 AM EST)January 1
2011      *       (1997 9:00 AM EDT)April 10;July 19
2012      *       (2000 9:00 AM EST)January 1
2013      *       (2000 9:00 AM EDT)April 9;July 18
2014      *       (2003 9:00 AM EST)January 1
2015      *       (2003 9:00 AM EDT)April 10;July 19
2016      *       (2006 9:00 AM EST)January 1
2017      */
2018     public function test_every_3_years_1st_100th_200th_days_10_count() {
2019         global $DB;
2021         $startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
2023         $rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
2024         $mang = new rrule_manager($rrule);
2025         $mang->parse_rrule();
2026         $mang->create_events($this->event);
2028         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2029         // Should have 10 records based on COUNT rule.
2030         $this->assertCount(10, $records);
2032         $expecteddate = $startdatetime;
2033         $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2034         $hundredthdayinterval = new DateInterval('P99D');
2035         $twohundredthdayinterval = new DateInterval('P100D');
2036         $yearinterval = new DateInterval('P3Y');
2038         foreach ($records as $record) {
2039             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2041             // Get next expected date.
2042             if ($expecteddate->format('z') == 0) { // January 1.
2043                 $expecteddate->add($hundredthdayinterval);
2044             } else if ($expecteddate->format('z') == 99) { // 100th day of the year.
2045                 $expecteddate->add($twohundredthdayinterval);
2046             } else { // 200th day of the year.
2047                 $expecteddate->add($yearinterval);
2048                 $expecteddate->modify('January 1');
2049             }
2050         }
2051     }
2053     /**
2054      * Every 20th Monday of the year, forever:
2055      *
2056      * DTSTART;TZID=US-Eastern:19970519T090000
2057      * RRULE:FREQ=YEARLY;BYDAY=20MO
2058      *   ==> (1997 9:00 AM EDT)May 19
2059      *       (1998 9:00 AM EDT)May 18
2060      *       (1999 9:00 AM EDT)May 17
2061      *       ...
2062      */
2063     public function test_yearly_every_20th_monday_forever() {
2064         global $DB;
2066         // Change our event's date to 19-05-1997, based on the example from the RFC.
2067         $startdatetime = $this->change_event_startdate('19970519T090000', 'US/Eastern');
2069         $startdate = new DateTime($startdatetime->format('Y-m-d'));
2071         $offset = $startdatetime->diff($startdate, true);
2073         $interval = new DateInterval('P1Y');
2075         $rrule = 'FREQ=YEARLY;BYDAY=20MO';
2076         $mang = new rrule_manager($rrule);
2077         $mang->parse_rrule();
2078         $mang->create_events($this->event);
2080         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2082         $untildate = new DateTime();
2083         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2084         $untiltimestamp = $untildate->getTimestamp();
2086         $expecteddate = $startdatetime;
2087         $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2088         foreach ($records as $record) {
2089             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2090             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2092             // Go to next period.
2093             $expecteddate->modify('January 1');
2094             $expecteddate->add($interval);
2095             $expecteddate->modify("+20 Monday");
2096             $expecteddate->add($offset);
2097         }
2098     }
2100     /**
2101      * Monday of week number 20 (where the default start of the week is Monday), forever:
2102      *
2103      * DTSTART;TZID=US-Eastern:19970512T090000
2104      * RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO
2105      * ==> (1997 9:00 AM EDT)May 12
2106      *     (1998 9:00 AM EDT)May 11
2107      *     (1999 9:00 AM EDT)May 17
2108      *     ...
2109      */
2110     public function test_yearly_byweekno_forever() {
2111         global $DB;
2113         // Change our event's date to 12-05-1997, based on the example from the RFC.
2114         $startdatetime = $this->change_event_startdate('19970512T090000', 'US/Eastern');
2116         $startdate = clone($startdatetime);
2117         $startdate->modify($startdate->format('Y-m-d'));
2119         $offset = $startdatetime->diff($startdate, true);
2121         $interval = new DateInterval('P1Y');
2123         $rrule = 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO';
2124         $mang = new rrule_manager($rrule);
2125         $mang->parse_rrule();
2126         $mang->create_events($this->event);
2128         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2130         $untildate = new DateTime();
2131         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2132         $untiltimestamp = $untildate->getTimestamp();
2134         $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
2135         foreach ($records as $record) {
2136             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2137             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2139             // Go to next period.
2140             $expecteddate->add($interval);
2141             $expecteddate->setISODate($expecteddate->format('Y'), 20);
2142             $expecteddate->add($offset);
2143         }
2144     }
2146     /**
2147      * Every Thursday in March, forever:
2148      *
2149      * DTSTART;TZID=US-Eastern:19970313T090000
2150      * RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH
2151      *   ==> (1997 9:00 AM EST)March 13,20,27
2152      *       (1998 9:00 AM EST)March 5,12,19,26
2153      *       (1999 9:00 AM EST)March 4,11,18,25
2154      *       ...
2155      */
2156     public function test_every_thursday_in_march_forever() {
2157         global $DB;
2159         // Change our event's date to 12-05-1997, based on the example from the RFC.
2160         $startdatetime = $this->change_event_startdate('19970313T090000', 'US/Eastern');
2162         $interval = new DateInterval('P1Y');
2164         $rrule = 'FREQ=YEARLY;BYMONTH=3;BYDAY=TH';
2165         $mang = new rrule_manager($rrule);
2166         $mang->parse_rrule();
2167         $mang->create_events($this->event);
2169         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2171         $untildate = new DateTime();
2172         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2173         $untiltimestamp = $untildate->getTimestamp();
2175         $expecteddate = $startdatetime;
2176         $startdate = new DateTime($startdatetime->format('Y-m-d'));
2177         $offsetinterval = $startdatetime->diff($startdate, true);
2178         $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2179         $april1st = new DateTime('1997-04-01');
2180         foreach ($records as $record) {
2181             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2182             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2184             // Go to next period.
2185             $expecteddate->modify('next Thursday');
2186             if ($expecteddate->getTimestamp() >= $april1st->getTimestamp()) {
2187                 // Reset to 1st of March.
2188                 $expecteddate->modify('first day of March');
2189                 // Go to next year.
2190                 $expecteddate->add($interval);
2191                 if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) {
2192                     $expecteddate->modify('next Thursday');
2193                 }
2194                 // Increment to next year's April 1st.
2195                 $april1st->add($interval);
2196             }
2197             $expecteddate->add($offsetinterval);
2198         }
2199     }
2201     /**
2202      * Every Thursday, but only during June, July, and August, forever:
2203      *
2204      * DTSTART;TZID=US-Eastern:19970605T090000
2205      * RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8
2206      *   ==> (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31;August 7,14,21,28
2207      *       (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30;August 6,13,20,27
2208      *       (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29;August 5,12,19,26
2209      *       ...
2210      */
2211     public function test_every_thursday_june_july_august_forever() {
2212         global $DB;
2214         // Change our event's date to 05-06-1997, based on the example from the RFC.
2215         $startdatetime = $this->change_event_startdate('19970605T090000', 'US/Eastern');
2217         $startdate = new DateTime($startdatetime->format('Y-m-d'));
2219         $offset = $startdatetime->diff($startdate, true);
2221         $interval = new DateInterval('P1Y');
2223         $rrule = 'FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8';
2224         $mang = new rrule_manager($rrule);
2225         $mang->parse_rrule();
2226         $mang->create_events($this->event);
2228         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2230         $untildate = new DateTime();
2231         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2232         $untiltimestamp = $untildate->getTimestamp();
2234         $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
2235         $september1st = new DateTime('1997-09-01');
2236         foreach ($records as $record) {
2237             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2238             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2240             // Go to next period.
2241             $expecteddate->modify('next Thursday');
2242             if ($expecteddate->getTimestamp() >= $september1st->getTimestamp()) {
2243                 $expecteddate->add($interval);
2244                 $expecteddate->modify('June 1');
2245                 if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) {
2246                     $expecteddate->modify('next Thursday');
2247                 }
2248                 $september1st->add($interval);
2249             }
2250             $expecteddate->add($offset);
2251         }
2252     }
2254     /**
2255      * Every Friday the 13th, forever:
2256      *
2257      * DTSTART;TZID=US-Eastern:19970902T090000
2258      * EXDATE;TZID=US-Eastern:19970902T090000
2259      * RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
2260      *   ==> (1998 9:00 AM EST)February 13;March 13;November 13
2261      *       (1999 9:00 AM EDT)August 13
2262      *       (2000 9:00 AM EDT)October 13
2263      */
2264     public function test_friday_the_thirteenth_forever() {
2265         global $DB;
2267         $rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
2268         $mang = new rrule_manager($rrule);
2269         $mang->parse_rrule();
2270         $mang->create_events($this->event);
2272         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2274         $untildate = new DateTime();
2275         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2276         $untiltimestamp = $untildate->getTimestamp();
2278         foreach ($records as $record) {
2279             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2280             // Assert that the day of the month and the day correspond to Friday the 13th.
2281             $this->assertEquals('Friday 13', date('l d', $record->timestart));
2282         }
2283     }
2285     /**
2286      * The first Saturday that follows the first Sunday of the month, forever:
2287      *
2288      * DTSTART;TZID=US-Eastern:19970913T090000
2289      * RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13
2290      *   ==> (1997 9:00 AM EDT)September 13;October 11
2291      *       (1997 9:00 AM EST)November 8;December 13
2292      *       (1998 9:00 AM EST)January 10;February 7;March 7
2293      *       (1998 9:00 AM EDT)April 11;May 9;June 13...
2294      */
2295     public function test_first_saturday_following_first_sunday_forever() {
2296         global $DB;
2298         $startdatetime = $this->change_event_startdate('19970913T090000', 'US/Eastern');
2299         $startdate = new DateTime($startdatetime->format('Y-m-d'));
2300         $offset = $startdatetime->diff($startdate, true);
2302         $rrule = 'FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13';
2303         $mang = new rrule_manager($rrule);
2304         $mang->parse_rrule();
2305         $mang->create_events($this->event);
2307         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2309         $untildate = new DateTime();
2310         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2311         $untiltimestamp = $untildate->getTimestamp();
2312         $bymonthdays = [7, 8, 9, 10, 11, 12, 13];
2313         foreach ($records as $record) {
2314             $recordmonthyear = date('F Y', $record->timestart);
2315             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2317             // Get first Saturday after the first Sunday of the month.
2318             $expecteddate = new DateTime('first Sunday of ' . $recordmonthyear);
2319             $expecteddate->modify('next Saturday');
2320             $expecteddate->add($offset);
2322             // Assert the record's date corresponds to the first Saturday of the month.
2323             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2325             // Assert that the record is either the 7th, 8th, 9th, ... 13th day of the month.
2326             $this->assertContains(date('j', $record->timestart), $bymonthdays);
2327         }
2328     }
2330     /**
2331      * Every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day):
2332      *
2333      * DTSTART;TZID=US-Eastern:19961105T090000
2334      * RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
2335      *   ==> (1996 9:00 AM EST)November 5
2336      *       (2000 9:00 AM EST)November 7
2337      *       (2004 9:00 AM EST)November 2
2338      *       ...
2339      */
2340     public function test_every_us_presidential_election_forever() {
2341         global $DB;
2343         $startdatetime = $this->change_event_startdate('19961105T090000', 'US/Eastern');
2344         $startdate = new DateTime($startdatetime->format('Y-m-d'));
2345         $offset = $startdatetime->diff($startdate, true);
2347         $rrule = 'FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8';
2348         $mang = new rrule_manager($rrule);
2349         $mang->parse_rrule();
2350         $mang->create_events($this->event);
2352         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2354         $untildate = new DateTime();
2355         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2356         $untiltimestamp = $untildate->getTimestamp();
2357         $bymonthdays = [2, 3, 4, 5, 6, 7, 8];
2358         foreach ($records as $record) {
2359             $recordmonthyear = date('F Y', $record->timestart);
2360             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2362             // Get first Saturday after the first Sunday of the month.
2363             $expecteddate = new DateTime('first Monday of ' . $recordmonthyear);
2364             $expecteddate->modify('next Tuesday');
2365             $expecteddate->add($offset);
2367             // Assert the record's date corresponds to the first Saturday of the month.
2368             $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2370             // Assert that the record is either the 2nd, 3rd, 4th ... 8th day of the month.
2371             $this->assertContains(date('j', $record->timestart), $bymonthdays);
2372         }
2373     }
2375     /**
2376      * The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months:
2377      *
2378      * DTSTART;TZID=US-Eastern:19970904T090000
2379      * RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3
2380      *   ==> (1997 9:00 AM EDT)September 4;October 7
2381      *       (1997 9:00 AM EST)November 6
2382      */
2383     public function test_monthly_bysetpos_3_count() {
2384         global $DB;
2386         $this->change_event_startdate('19970904T090000', 'US/Eastern');
2388         $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
2389         $mang = new rrule_manager($rrule);
2390         $mang->parse_rrule();
2391         $mang->create_events($this->event);
2393         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2394         $this->assertCount(3, $records);
2396         $expecteddates = [
2397             (new DateTime('1997-09-04 09:00:00 EDT'))->getTimestamp(),
2398             (new DateTime('1997-10-07 09:00:00 EDT'))->getTimestamp(),
2399             (new DateTime('1997-11-06 09:00:00 EST'))->getTimestamp()
2400         ];
2401         foreach ($records as $record) {
2402             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2403         }
2404     }
2406     /**
2407      * The 2nd to last weekday of the month:
2408      *
2409      * DTSTART;TZID=US-Eastern:19970929T090000
2410      * RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2
2411      *   ==> (1997 9:00 AM EDT)September 29
2412      *       (1997 9:00 AM EST)October 30;November 27;December 30
2413      *       (1998 9:00 AM EST)January 29;February 26;March 30
2414      *       ...
2415      */
2416     public function test_second_to_the_last_weekday_of_the_month_forever() {
2417         global $DB;
2419         $this->change_event_startdate('19970929T090000', 'US/Eastern');
2421         $rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2';
2422         $mang = new rrule_manager($rrule);
2423         $mang->parse_rrule();
2424         $mang->create_events($this->event);
2426         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2428         $expecteddates = [
2429             (new DateTime('1997-09-29 09:00:00 EDT'))->getTimestamp(),
2430             (new DateTime('1997-10-30 09:00:00 EST'))->getTimestamp(),
2431             (new DateTime('1997-11-27 09:00:00 EST'))->getTimestamp(),
2432             (new DateTime('1997-12-30 09:00:00 EST'))->getTimestamp(),
2433             (new DateTime('1998-01-29 09:00:00 EST'))->getTimestamp(),
2434             (new DateTime('1998-02-26 09:00:00 EST'))->getTimestamp(),
2435             (new DateTime('1998-03-30 09:00:00 EST'))->getTimestamp(),
2436         ];
2438         $untildate = new DateTime();
2439         $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2440         $untiltimestamp = $untildate->getTimestamp();
2442         $i = 0;
2443         foreach ($records as $record) {
2444             $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2446             // Confirm that the first 7 records correspond to the expected dates listed above.
2447             if ($i < 7) {
2448                 $this->assertEquals($expecteddates[$i], $record->timestart);
2449                 $i++;
2450             }
2451         }
2452     }
2454     /**
2455      * Every 3 hours from 9:00 AM to 5:00 PM on a specific day:
2456      *
2457      * DTSTART;TZID=US-Eastern:19970902T090000
2458      * RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z
2459      *   ==> (September 2, 1997 EDT)09:00,12:00,15:00
2460      */
2461     public function test_every_3hours_9am_to_5pm() {
2462         global $DB;
2464         $rrule = 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z';
2465         $mang = new rrule_manager($rrule);
2466         $mang->parse_rrule();
2467         $mang->create_events($this->event);
2469         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2470         $this->assertCount(3, $records);
2472         $expecteddates = [
2473             (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2474             (new DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
2475             (new DateTime('1997-09-02 15:00:00 EDT'))->getTimestamp(),
2476         ];
2477         foreach ($records as $record) {
2478             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2479         }
2480     }
2482     /**
2483      * Every 15 minutes for 6 occurrences:
2484      *
2485      * DTSTART;TZID=US-Eastern:19970902T090000
2486      * RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6
2487      *   ==> (September 2, 1997 EDT)09:00,09:15,09:30,09:45,10:00,10:15
2488      */
2489     public function test_every_15minutes_6_count() {
2490         global $DB;
2492         $rrule = 'FREQ=MINUTELY;INTERVAL=15;COUNT=6';
2493         $mang = new rrule_manager($rrule);
2494         $mang->parse_rrule();
2495         $mang->create_events($this->event);
2497         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2498         $this->assertCount(6, $records);
2500         $expecteddates = [
2501             (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2502             (new DateTime('1997-09-02 09:15:00 EDT'))->getTimestamp(),
2503             (new DateTime('1997-09-02 09:30:00 EDT'))->getTimestamp(),
2504             (new DateTime('1997-09-02 09:45:00 EDT'))->getTimestamp(),
2505             (new DateTime('1997-09-02 10:00:00 EDT'))->getTimestamp(),
2506             (new DateTime('1997-09-02 10:15:00 EDT'))->getTimestamp(),
2507         ];
2508         foreach ($records as $record) {
2509             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2510         }
2511     }
2513     /**
2514      * Every hour and a half for 4 occurrences:
2515      *
2516      * DTSTART;TZID=US-Eastern:19970902T090000
2517      * RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4
2518      *   ==> (September 2, 1997 EDT)09:00,10:30;12:00;13:30
2519      */
2520     public function test_every_90minutes_4_count() {
2521         global $DB;
2523         $rrule = 'FREQ=MINUTELY;INTERVAL=90;COUNT=4';
2524         $mang = new rrule_manager($rrule);
2525         $mang->parse_rrule();
2526         $mang->create_events($this->event);
2528         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2529         $this->assertCount(4, $records);
2531         $expecteddates = [
2532             (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2533             (new DateTime('1997-09-02 10:30:00 EDT'))->getTimestamp(),
2534             (new DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
2535             (new DateTime('1997-09-02 13:30:00 EDT'))->getTimestamp(),
2536         ];
2537         foreach ($records as $record) {
2538             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2539         }
2540     }
2542     /**
2543      * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
2544      *
2545      * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
2546      * so just limit the count to 50).
2547      *
2548      * DTSTART;TZID=US-Eastern:19970902T090000
2549      * RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50
2550      *   ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2551      *       (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2552      *       ...
2553      */
2554     public function test_every_20minutes_daily_byhour_byminute_50_count() {
2555         global $DB;
2557         $rrule = 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50';
2558         $mang = new rrule_manager($rrule);
2559         $mang->parse_rrule();
2560         $mang->create_events($this->event);
2562         $byminuteinterval = new DateInterval('PT20M');
2563         $bydayinterval = new DateInterval('P1D');
2564         $date = new DateTime('1997-09-02 09:00:00 EDT');
2565         $expecteddates = [];
2566         $count = 50;
2567         for ($i = 0; $i < $count; $i++) {
2568             $expecteddates[] = $date->getTimestamp();
2569             $date->add($byminuteinterval);
2570             if ($date->format('H') > 16) {
2571                 // Go to next day.
2572                 $date->add($bydayinterval);
2573                 // Reset time to 9am.
2574                 $date->setTime(9, 0);
2575             }
2576         }
2578         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2579         $this->assertCount($count, $records);
2581         foreach ($records as $record) {
2582             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2583         }
2584     }
2586     /**
2587      * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
2588      *
2589      * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
2590      * so just limit the count to 50).
2591      *
2592      * DTSTART;TZID=US-Eastern:19970902T090000
2593      * RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50
2594      *   ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2595      *       (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2596      *       ...
2597      */
2598     public function test_every_20minutes_minutely_byhour_50_count() {
2599         global $DB;
2601         $rrule = 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50';
2602         $mang = new rrule_manager($rrule);
2603         $mang->parse_rrule();
2604         $mang->create_events($this->event);
2606         $byminuteinterval = new DateInterval('PT20M');
2607         $bydayinterval = new DateInterval('P1D');
2608         $date = new DateTime('1997-09-02 09:00:00');
2609         $expecteddates = [];
2610         $count = 50;
2611         for ($i = 0; $i < $count; $i++) {
2612             $expecteddates[] = $date->getTimestamp();
2613             $date->add($byminuteinterval);
2614             if ($date->format('H') > 16) {
2615                 // Go to next day.
2616                 $date->add($bydayinterval);
2617                 // Reset time to 9am.
2618                 $date->setTime(9, 0);
2619             }
2620         }
2622         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2623         $this->assertCount($count, $records);
2625         foreach ($records as $record) {
2626             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2627         }
2628     }
2630     /**
2631      * An example where the days generated makes a difference because of WKST:
2632      *
2633      * DTSTART;TZID=US-Eastern:19970805T090000
2634      * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO
2635      *   ==> (1997 EDT)Aug 5,10,19,24
2636      */
2637     public function test_weekly_byday_with_wkst_mo() {
2638         global $DB;
2640         $this->change_event_startdate('19970805T090000', 'US/Eastern');
2642         $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
2643         $mang = new rrule_manager($rrule);
2644         $mang->parse_rrule();
2645         $mang->create_events($this->event);
2647         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2648         $this->assertCount(4, $records);
2650         $expecteddates = [
2651             (new DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
2652             (new DateTime('1997-08-10 09:00:00 EDT'))->getTimestamp(),
2653             (new DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
2654             (new DateTime('1997-08-24 09:00:00 EDT'))->getTimestamp(),
2655         ];
2656         foreach ($records as $record) {
2657             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2658         }
2659     }
2661     /**
2662      * An example where the days generated makes a difference because of WKST:
2663      * Changing only WKST from MO to SU, yields different results...
2664      *
2665      * DTSTART;TZID=US-Eastern:19970805T090000
2666      * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU
2667      *   ==> (1997 EDT)August 5,17,19,31
2668      */
2669     public function test_weekly_byday_with_wkst_su() {
2670         global $DB;
2672         $this->change_event_startdate('19970805T090000', 'US/Eastern');
2674         $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
2675         $mang = new rrule_manager($rrule);
2676         $mang->parse_rrule();
2677         $mang->create_events($this->event);
2679         $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2680         $this->assertCount(4, $records);
2682         $expecteddates = [
2683             (new DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
2684             (new DateTime('1997-08-17 09:00:00 EDT'))->getTimestamp(),
2685             (new DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
2686             (new DateTime('1997-08-31 09:00:00 EDT'))->getTimestamp(),
2687         ];
2689         foreach ($records as $record) {
2690             $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2691         }
2692     }
2694     /**
2695      * Change the event's timestart (DTSTART) based on the test's needs.
2696      *
2697      * @param string $datestr The date string. In YYYYmmddThhiiss format. e.g. 19990902T090000.
2698      * @param string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
2699      * @return bool|DateTime
2700      */
2701     protected function change_event_startdate($datestr, $timezonestr) {
2702         $timezone = new DateTimeZone($timezonestr);
2703         $newdatetime = DateTime::createFromFormat('Ymd\THis', $datestr, $timezone);
2705         // Update the start date of the parent event.
2706         $calevent = calendar_event::load($this->event->id);
2707         $updatedata = (object)[
2708             'timestart' => $newdatetime->getTimestamp(),
2709             'repeatid' => $this->event->id
2710         ];
2711         $calevent->update($updatedata, false);
2712         $this->event->timestart = $calevent->timestart;
2714         return $newdatetime;
2715     }