Merge branch 'MDL-36754-master' of git://github.com/andrewnicols/moodle
[moodle.git] / calendar / tests / rrule_manager_test.php
bloba6e2531e658d13059e86a21cf4c78e1211266ff2
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.
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.
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
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.
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
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.
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;
87 /**
88 * Test parse_rrule() method.
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'
104 $rrule = implode(';', $rules);
105 $mang = new rrule_manager($rrule);
106 $mang->parse_rrule();
108 $bydayrules = [
109 (object)[
110 'day' => 'MO',
111 'value' => 0
113 (object)[
114 'day' => 'TH',
115 'value' => 0
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],
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));
142 * Test exception is thrown for invalid property.
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();
152 * Test exception is thrown for invalid frequency.
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();
162 * Test parsing of rules with both COUNT and UNTIL parameters.
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();
174 * Test parsing of INTERVAL rule.
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();
184 * Test parsing of BYSECOND rule.
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();
194 * Test parsing of BYMINUTE rule.
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();
204 * Test parsing of BYMINUTE rule.
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();
214 * Test parsing of BYDAY rule.
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();
224 * Test parsing of BYDAY rule with prefixes.
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();
245 * Test parsing of BYMONTHDAY rule.
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();
255 * Test parsing of BYMONTHDAY rule.
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();
265 * Test parsing of BYMONTHDAY rule.
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();
275 * Test parsing of BYYEARDAY rule.
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();
285 * Test parsing of BYYEARDAY rule.
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();
295 * Test parsing of BYYEARDAY rule.
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();
305 * Test parsing of BYWEEKNO rule.
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();
315 * Test parsing of BYWEEKNO rule.
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();
325 * Test parsing of BYWEEKNO rule.
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();
335 * Test parsing of BYWEEKNO rule.
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();
345 * Test parsing of BYMONTH rule.
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();
355 * Test parsing of BYMONTH rule.
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();
365 * Test parsing of BYSETPOS rule.
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();
375 * Test parsing of BYSETPOS rule.
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();
385 * Test parsing of BYSETPOS rule.
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();
395 * Test parsing of BYSETPOS rule.
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();
405 * Test recurrence rules for daily frequency.
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);
450 * Every 300 days, forever.
452 public function test_every_300_days_forever() {
453 global $DB;
455 // Change the start date for forever events to 9am of the current date.
456 $this->change_event_startdate(date('Ymd\T090000'));
457 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
459 $interval = new DateInterval('P300D');
460 $untildate = new DateTime();
461 $untildate->add(new DateInterval('P10Y'));
462 $until = $untildate->getTimestamp();
464 // Forever event. This should generate events for time() + 10 year period, every 300 days.
465 $rrule = 'FREQ=DAILY;INTERVAL=300';
466 $mang = new rrule_manager($rrule);
467 $mang->parse_rrule();
468 $mang->create_events($this->event);
469 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
470 $records = $DB->get_records('event', array('repeatid' => $this->event->id), 'timestart ASC', 0, 100);
472 $expecteddate = clone($startdatetime);
473 $first = true;
474 foreach ($records as $record) {
475 $this->assertLessThanOrEqual($until, $record->timestart);
476 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
477 // Go to next iteration.
478 $expecteddate->add($interval);
479 // Check UUID.
480 if ($first) {
481 // The first instance of the event contains the UUID.
482 $this->assertEquals('uuid', $record->uuid);
483 $first = false;
484 } else {
485 // Succeeding instances will not contain the UUID.
486 $this->assertEmpty($record->uuid);
492 * Test recurrence rules for weekly frequency.
494 public function test_weekly_events() {
495 global $DB;
497 $rrule = 'FREQ=WEEKLY;COUNT=1';
498 $mang = new rrule_manager($rrule);
499 $mang->parse_rrule();
500 $mang->create_events($this->event);
501 $count = $DB->count_records('event', array('repeatid' => $this->event->id));
502 $this->assertEquals(1, $count);
503 for ($i = 0; $i < $count; $i++) {
504 $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
505 'timestart' => ($this->event->timestart + $i * DAYSECS)));
506 $this->assertTrue($result);
508 // This much seconds after the start of the day.
509 $offset = $this->event->timestart - mktime(0, 0, 0, date("n", $this->event->timestart), date("j", $this->event->timestart),
510 date("Y", $this->event->timestart));
512 // This should generate 4 weekly Monday events.
513 $until = $this->event->timestart + WEEKSECS * 4;
514 $until = date('Ymd\This\Z', $until);
515 $rrule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=$until";
516 $mang = new rrule_manager($rrule);
517 $mang->parse_rrule();
518 $mang->create_events($this->event);
519 $count = $DB->count_records('event', array('repeatid' => $this->event->id));
520 $this->assertEquals(4, $count);
521 $timestart = $this->event->timestart;
522 for ($i = 0; $i < $count; $i++) {
523 $timestart = strtotime("+$offset seconds next Monday", $timestart);
524 $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $timestart));
525 $this->assertTrue($result);
528 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
529 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
531 $offsetinterval = $startdatetime->diff($startdate, true);
532 $interval = new DateInterval('P3W');
534 // Every 3 weeks on Monday, Wednesday for 2 times.
535 $rrule = 'FREQ=WEEKLY;INTERVAL=3;BYDAY=MO,WE;COUNT=2';
536 $mang = new rrule_manager($rrule);
537 $mang->parse_rrule();
538 $mang->create_events($this->event);
540 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
541 $this->assertCount(2, $records);
543 $expecteddate = clone($startdate);
544 $expecteddate->modify('1997-09-03');
545 foreach ($records as $record) {
546 $expecteddate->add($offsetinterval);
547 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
549 if (date('D', $record->timestart) === 'Mon') {
550 // Go to the fifth day of this month.
551 $expecteddate->modify('next Wednesday');
552 } else {
553 // Reset to Monday.
554 $expecteddate->modify('last Monday');
555 // Go to next period.
556 $expecteddate->add($interval);
562 * Test recurrence rules for weekly frequency for RRULE with BYDAY rule set, recurring forever.
564 public function test_weekly_byday_forever() {
565 global $DB;
567 // Set the next Monday as the starting date of this event.
568 $startdate = new DateTime('next Monday');
569 // Change the start date of the parent event.
570 $startdate = $this->change_event_startdate($startdate->format('Ymd\T090000'));
572 // Forever event. This should generate events over time() + 10 year period, every 50 weeks.
573 $rrule = 'FREQ=WEEKLY;BYDAY=MO;INTERVAL=50';
575 $mang = new rrule_manager($rrule);
576 $mang->parse_rrule();
577 $mang->create_events($this->event);
579 $untildate = new DateTime();
580 $untildate->add(new DateInterval('P10Y'));
581 $until = $untildate->getTimestamp();
583 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
585 $interval = new DateInterval('P50W');
587 // First instance of this set of recurring events.
588 $expecteddate = clone($startdate);
590 // Iterate over each record and increment the expected date accordingly.
591 foreach ($records as $record) {
592 $eventdateexpected = $expecteddate->format('Y-m-d H:i:s');
593 $eventdateactual = date('Y-m-d H:i:s', $record->timestart);
594 $this->assertEquals($eventdateexpected, $eventdateactual);
596 $expecteddate->add($interval);
597 $this->assertLessThanOrEqual($until, $record->timestart);
602 * Test recurrence rules for monthly frequency for RRULE with COUNT and BYMONTHDAY rules set.
604 public function test_monthly_events_with_count_bymonthday() {
605 global $DB;
607 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
608 $interval = new DateInterval('P1M');
610 $rrule = "FREQ=MONTHLY;COUNT=3;BYMONTHDAY=2"; // This should generate 3 events in total.
611 $mang = new rrule_manager($rrule);
612 $mang->parse_rrule();
613 $mang->create_events($this->event);
614 $records = $DB->get_records('event', array('repeatid' => $this->event->id), 'timestart ASC');
615 $this->assertCount(3, $records);
617 $expecteddate = clone($startdatetime);
618 foreach ($records as $record) {
619 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
620 // Go to next month.
621 $expecteddate->add($interval);
626 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
628 public function test_monthly_events_with_until_bymonthday() {
629 global $DB;
631 // This should generate 10 child event + 1 parent, since by then until bound would be hit.
632 $until = strtotime('+1 day +10 months', $this->event->timestart);
633 $until = date('Ymd\This\Z', $until);
634 $rrule = "FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=$until";
635 $mang = new rrule_manager($rrule);
636 $mang->parse_rrule();
637 $mang->create_events($this->event);
638 $count = $DB->count_records('event', ['repeatid' => $this->event->id]);
639 $this->assertEquals(11, $count);
640 for ($i = 0; $i < 11; $i++) {
641 $time = strtotime("+$i month", $this->event->timestart);
642 $result = $DB->record_exists('event', ['repeatid' => $this->event->id, 'timestart' => $time]);
643 $this->assertTrue($result);
648 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
650 public function test_monthly_events_with_until_bymonthday_multi() {
651 global $DB;
653 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
654 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
655 $offsetinterval = $startdatetime->diff($startdate, true);
656 $interval = new DateInterval('P2M');
657 $untildate = clone($startdatetime);
658 $untildate->add(new DateInterval('P10M10D'));
659 $until = $untildate->format('Ymd\This\Z');
661 // This should generate 11 child event + 1 parent, since by then until bound would be hit.
662 $rrule = "FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=2,5;UNTIL=$until";
664 $mang = new rrule_manager($rrule);
665 $mang->parse_rrule();
666 $mang->create_events($this->event);
668 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
669 $this->assertCount(12, $records);
671 $expecteddate = clone($startdate);
672 $expecteddate->add($offsetinterval);
673 foreach ($records as $record) {
674 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
676 if (date('j', $record->timestart) == 2) {
677 // Go to the fifth day of this month.
678 $expecteddate->add(new DateInterval('P3D'));
679 } else {
680 // Reset date to the first day of the month.
681 $expecteddate->modify('first day of this month');
682 // Go to next month period.
683 $expecteddate->add($interval);
684 // Go to the second day of the next month period.
685 $expecteddate->modify('+1 day');
691 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY forever.
693 public function test_monthly_events_with_bymonthday_forever() {
694 global $DB;
696 // Change the start date for forever events to 9am of the 2nd day of the current month and year.
697 $this->change_event_startdate(date('Ym02\T090000'));
698 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
699 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
701 $offsetinterval = $startdatetime->diff($startdate, true);
702 $interval = new DateInterval('P12M');
704 // Forever event. This should generate events over a 10-year period, on 2nd day of the month, every 12 months.
705 $rrule = "FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2";
707 $mang = new rrule_manager($rrule);
708 $untildate = new DateTime();
709 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
710 $until = $untildate->getTimestamp();
712 $mang->parse_rrule();
713 $mang->create_events($this->event);
715 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
717 $expecteddate = clone($startdate);
718 $expecteddate->add($offsetinterval);
719 foreach ($records as $record) {
720 $this->assertLessThanOrEqual($until, $record->timestart);
722 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
724 // Reset date to the first day of the month.
725 $expecteddate->modify('first day of this month');
726 // Go to next month period.
727 $expecteddate->add($interval);
728 // Go to the second day of the next month period.
729 $expecteddate->modify('+1 day');
734 * Test recurrence rules for monthly frequency for RRULE with COUNT and BYDAY rules set.
736 public function test_monthly_events_with_count_byday() {
737 global $DB;
739 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
740 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
742 $offsetinterval = $startdatetime->diff($startdate, true);
743 $interval = new DateInterval('P1M');
745 $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=1MO'; // This should generate 3 events in total, first monday of the month.
746 $mang = new rrule_manager($rrule);
747 $mang->parse_rrule();
748 $mang->create_events($this->event);
750 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
752 // First occurrence of this set of recurring events: 06-10-1997.
753 $expecteddate = clone($startdate);
754 $expecteddate->modify('1997-10-06');
755 $expecteddate->add($offsetinterval);
756 foreach ($records as $record) {
757 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
759 // Go to next month period.
760 $expecteddate->add($interval);
761 $expecteddate->modify('first Monday of this month');
762 $expecteddate->add($offsetinterval);
767 * Test recurrence rules for monthly frequency for RRULE with BYDAY and UNTIL rules set.
769 public function test_monthly_events_with_until_byday() {
770 global $DB;
772 // This much seconds after the start of the day.
773 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
774 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
775 $offsetinterval = $startdatetime->diff($startdate, true);
777 $untildate = clone($startdatetime);
778 $untildate->add(new DateInterval('P10M1D'));
779 $until = $untildate->format('Ymd\This\Z');
781 // This rule should generate 9 events in total from first Monday of October 1997 to first Monday of June 1998.
782 $rrule = "FREQ=MONTHLY;BYDAY=1MO;UNTIL=$until";
783 $mang = new rrule_manager($rrule);
784 $mang->parse_rrule();
785 $mang->create_events($this->event);
787 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
788 $this->assertCount(9, $records);
790 $expecteddate = clone($startdate);
791 $expecteddate->modify('first Monday of October 1997');
792 foreach ($records as $record) {
793 $expecteddate->add($offsetinterval);
795 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
797 // Go to next month.
798 $expecteddate->modify('first day of next month');
799 // Go to the first Monday of the next month.
800 $expecteddate->modify('first Monday of this month');
805 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
807 public function test_monthly_events_with_until_byday_multi() {
808 global $DB;
810 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
811 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
813 $offsetinterval = $startdatetime->diff($startdate, true);
814 $interval = new DateInterval('P2M');
816 $untildate = clone($startdatetime);
817 $untildate->add(new DateInterval('P10M20D'));
818 $until = $untildate->format('Ymd\This\Z');
820 // This should generate 11 events from 17 Sep 1997 to 15 Jul 1998.
821 $rrule = "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,3WE;UNTIL=$until";
822 $mang = new rrule_manager($rrule);
823 $mang->parse_rrule();
824 $mang->create_events($this->event);
826 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
827 $this->assertCount(11, $records);
829 $expecteddate = clone($startdate);
830 $expecteddate->modify('1997-09-17');
831 foreach ($records as $record) {
832 $expecteddate->add($offsetinterval);
833 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
835 if (date('D', $record->timestart) === 'Mon') {
836 // Go to the fifth day of this month.
837 $expecteddate->modify('third Wednesday of this month');
838 } else {
839 // Go to next month period.
840 $expecteddate->add($interval);
841 $expecteddate->modify('first Monday of this month');
847 * Test recurrence rules for monthly frequency for RRULE with BYDAY forever.
849 public function test_monthly_events_with_byday_forever() {
850 global $DB;
852 // Change the start date for forever events to 9am of the 2nd day of the current month and year.
853 $this->change_event_startdate(date('Ym02\T090000'));
855 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
856 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
858 $offsetinterval = $startdatetime->diff($startdate, true);
859 $interval = new DateInterval('P12M');
861 // Forever event. This should generate events over a 10 year period, on 1st Monday of the month every 12 months.
862 $rrule = "FREQ=MONTHLY;INTERVAL=12;BYDAY=1MO";
864 $mang = new rrule_manager($rrule);
865 $untildate = new DateTime();
866 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
867 $until = $untildate->getTimestamp();
869 $mang->parse_rrule();
870 $mang->create_events($this->event);
872 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
873 $expecteddate = new DateTime('first Monday of this month');
874 // Move to the next interval's first Monday if the calculated start date is after this month's first Monday.
875 if ($expecteddate->getTimestamp() < $startdate->getTimestamp()) {
876 $expecteddate->add($interval);
877 $expecteddate->modify('first Monday of this month');
879 foreach ($records as $record) {
880 $expecteddate->add($offsetinterval);
881 $this->assertLessThanOrEqual($until, $record->timestart);
883 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
885 // Go to next month period.
886 $expecteddate->add($interval);
887 // Reset date to the first Monday of the month.
888 $expecteddate->modify('first Monday of this month');
893 * Test recurrence rules for yearly frequency.
895 public function test_yearly_events() {
896 global $DB;
898 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
899 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
901 $offsetinterval = $startdatetime->diff($startdate, true);
902 $interval = new DateInterval('P1Y');
904 $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9"; // This should generate 3 events in total.
905 $mang = new rrule_manager($rrule);
906 $mang->parse_rrule();
907 $mang->create_events($this->event);
909 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
910 $this->assertCount(3, $records);
912 $expecteddate = clone($startdatetime);
913 foreach ($records as $record) {
914 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
916 // Go to next period.
917 $expecteddate->add($interval);
920 // Create a yearly event, until the time limit is hit.
921 $until = strtotime('+20 day +10 years', $this->event->timestart);
922 $until = date('Ymd\THis\Z', $until);
923 $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until";
924 $mang = new rrule_manager($rrule);
925 $mang->parse_rrule();
926 $mang->create_events($this->event);
927 $count = $DB->count_records('event', array('repeatid' => $this->event->id));
928 $this->assertEquals(11, $count);
929 for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
930 $time = strtotime("+$yoffset years", $this->event->timestart)) {
931 $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
932 'timestart' => ($time)));
933 $this->assertTrue($result);
936 // This should generate 5 events in total, every second year in the given month of the event.
937 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5";
938 $mang = new rrule_manager($rrule);
939 $mang->parse_rrule();
940 $mang->create_events($this->event);
941 $count = $DB->count_records('event', array('repeatid' => $this->event->id));
942 $this->assertEquals(5, $count);
943 for ($i = 0, $time = $this->event->timestart; $i < 5; $i++, $yoffset = $i * 2,
944 $time = strtotime("+$yoffset years", $this->event->timestart)) {
945 $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
946 'timestart' => ($time)));
947 $this->assertTrue($result);
950 $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9;BYDAY=1MO"; // This should generate 3 events in total.
951 $mang = new rrule_manager($rrule);
952 $mang->parse_rrule();
953 $mang->create_events($this->event);
955 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
956 $this->assertCount(3, $records);
958 $expecteddate = clone($startdatetime);
959 $expecteddate->modify('first Monday of September 1998');
960 $expecteddate->add($offsetinterval);
961 foreach ($records as $record) {
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);
971 // Create a yearly event on the specified month, until the time limit is hit.
972 $untildate = clone($startdatetime);
973 $untildate->add(new DateInterval('P10Y20D'));
974 $until = $untildate->format('Ymd\THis\Z');
976 $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until;BYDAY=1MO";
977 $mang = new rrule_manager($rrule);
978 $mang->parse_rrule();
979 $mang->create_events($this->event);
981 // 10 yearly records every first Monday of September 1998 to first Monday of September 2007.
982 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
983 $this->assertCount(10, $records);
985 $expecteddate = clone($startdatetime);
986 $expecteddate->modify('first Monday of September 1998');
987 $expecteddate->add($offsetinterval);
988 foreach ($records as $record) {
989 $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart);
990 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
992 // Go to next period.
993 $expecteddate->add($interval);
994 $monthyear = $expecteddate->format('F Y');
995 $expecteddate->modify('first Monday of ' . $monthyear);
996 $expecteddate->add($offsetinterval);
999 // This should generate 5 events in total, every second year in the month of September.
1000 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5;BYDAY=1MO";
1001 $mang = new rrule_manager($rrule);
1002 $mang->parse_rrule();
1003 $mang->create_events($this->event);
1005 // 5 bi-yearly records every first Monday of September 1998 to first Monday of September 2007.
1006 $interval = new DateInterval('P2Y');
1007 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1008 $this->assertCount(5, $records);
1010 $expecteddate = clone($startdatetime);
1011 $expecteddate->modify('first Monday of September 1999');
1012 $expecteddate->add($offsetinterval);
1013 foreach ($records as $record) {
1014 $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart);
1015 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1017 // Go to next period.
1018 $expecteddate->add($interval);
1019 $monthyear = $expecteddate->format('F Y');
1020 $expecteddate->modify('first Monday of ' . $monthyear);
1021 $expecteddate->add($offsetinterval);
1026 * Test for rrule with FREQ=YEARLY and INTERVAL=2 with BYMONTH rule set, recurring forever.
1028 public function test_yearly_september_every_two_years_forever() {
1029 global $DB;
1031 // Change the start date for forever events to 9am on the 2nd day of September of the current year.
1032 $this->change_event_startdate(date('Y0902\T090000'));
1034 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2"; // Forever event.
1035 $mang = new rrule_manager($rrule);
1036 $untildate = new DateTime();
1037 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1038 $untiltimestamp = $untildate->getTimestamp();
1039 $mang->parse_rrule();
1040 $mang->create_events($this->event);
1042 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1044 $interval = new DateInterval('P2Y');
1045 $expecteddate = new DateTime(date('Y0902\T090000'));
1046 foreach ($records as $record) {
1047 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1048 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1050 // Go to the next expected date.
1051 $expecteddate->add($interval);
1056 * Test for rrule with FREQ=YEARLY with BYMONTH and BYDAY rules set, recurring forever.
1058 public function test_yearly_bymonth_byday_forever() {
1059 global $DB;
1061 // Change the start date for forever events to the first day of September of the current year at 9am.
1062 $this->change_event_startdate(date('Y0901\T090000'));
1064 // Every 2 years on the first Monday of September.
1065 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;BYDAY=1MO";
1066 $mang = new rrule_manager($rrule);
1067 $mang->parse_rrule();
1068 $mang->create_events($this->event);
1070 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1072 $untildate = new DateTime();
1073 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1074 $untiltimestamp = $untildate->getTimestamp();
1076 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1077 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1079 $offsetinterval = $startdatetime->diff($startdate, true);
1080 $interval = new DateInterval('P2Y');
1082 // First occurrence of this set of events is on the first Monday of September.
1083 $expecteddate = clone($startdatetime);
1084 $expecteddate->modify('first Monday of September');
1085 $expecteddate->add($offsetinterval);
1086 foreach ($records as $record) {
1087 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1088 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1090 // Go to next period.
1091 $expecteddate->add($interval);
1092 $monthyear = $expecteddate->format('F Y');
1093 $expecteddate->modify('first Monday of ' . $monthyear);
1094 $expecteddate->add($offsetinterval);
1099 * Test for rrule with FREQ=YEARLY recurring forever.
1101 public function test_yearly_forever() {
1102 global $DB;
1104 // Change the start date for forever events to 9am of the current date.
1105 $this->change_event_startdate(date('Ymd\T090000'));
1107 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1109 $interval = new DateInterval('P2Y');
1111 $rrule = 'FREQ=YEARLY;INTERVAL=2'; // Forever event.
1112 $mang = new rrule_manager($rrule);
1113 $mang->parse_rrule();
1114 $mang->create_events($this->event);
1116 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1118 $untildate = new DateTime();
1119 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1120 $untiltimestamp = $untildate->getTimestamp();
1122 $expecteddate = clone($startdatetime);
1123 foreach ($records as $record) {
1124 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1125 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1127 // Go to next period.
1128 $expecteddate->add($interval);
1132 /******************************************************************************************************************************/
1133 /* Tests based on the examples from the RFC. */
1134 /******************************************************************************************************************************/
1137 * Daily for 10 occurrences:
1139 * DTSTART;TZID=US-Eastern:19970902T090000
1140 * RRULE:FREQ=DAILY;COUNT=10
1141 * ==> (1997 9:00 AM EDT)September 2-11
1143 public function test_daily_count() {
1144 global $DB;
1146 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1147 $interval = new DateInterval('P1D');
1149 $rrule = 'FREQ=DAILY;COUNT=10';
1150 $mang = new rrule_manager($rrule);
1151 $mang->parse_rrule();
1152 $mang->create_events($this->event);
1154 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1155 $this->assertCount(10, $records);
1157 $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1158 foreach ($records as $record) {
1159 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1161 // Go to next period.
1162 $expecteddate->add($interval);
1167 * Daily until December 24, 1997:
1169 * DTSTART;TZID=US-Eastern:19970902T090000
1170 * RRULE:FREQ=DAILY;UNTIL=19971224T000000Z
1171 * ==> (1997 9:00 AM EDT)September 2-30;October 1-25
1172 * (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23
1174 public function test_daily_until() {
1175 global $DB;
1177 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1178 $interval = new DateInterval('P1D');
1180 $untildate = new DateTime('19971224T000000Z');
1181 $untiltimestamp = $untildate->getTimestamp();
1183 $rrule = 'FREQ=DAILY;UNTIL=19971224T000000Z';
1184 $mang = new rrule_manager($rrule);
1185 $mang->parse_rrule();
1186 $mang->create_events($this->event);
1188 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1189 // 113 daily events from 02-09-1997 to 23-12-1997.
1190 $this->assertCount(113, $records);
1192 $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1193 foreach ($records as $record) {
1194 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
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);
1202 * Every other day - forever:
1204 * DTSTART;TZID=US-Eastern:[Current date]T090000
1205 * RRULE:FREQ=DAILY;INTERVAL=2
1207 * Sample results (e.g. in the year 1997):
1208 * (1997 9:00 AM EDT)September2,4,6,8...24,26,28,30;October 2,4,6...20,22,24
1209 * (1997 9:00 AM EST)October 26,28,30;November 1,3,5,7...25,27,29;Dec 1,3,...
1211 public function test_every_other_day_forever() {
1212 global $DB;
1214 // Change the start date for forever events to 9am of the current date in US/Eastern time.
1215 $this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
1217 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1218 $interval = new DateInterval('P2D');
1220 $rrule = 'FREQ=DAILY;INTERVAL=2';
1221 $mang = new rrule_manager($rrule);
1222 $mang->parse_rrule();
1223 $mang->create_events($this->event);
1225 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1226 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1228 $untildate = new DateTime();
1229 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1230 $untiltimestamp = $untildate->getTimestamp();
1232 $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1233 foreach ($records as $record) {
1234 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1236 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1237 // Go to next period.
1238 $expecteddate->add($interval);
1243 * Every 10 days, 5 occurrences:
1245 * DTSTART;TZID=US-Eastern:19970902T090000
1246 * RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5
1247 * ==> (1997 9:00 AM EDT)September 2,12,22;October 2,12
1249 public function test_every_10_days_5_count() {
1250 global $DB;
1252 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1253 $interval = new DateInterval('P10D');
1255 $rrule = 'FREQ=DAILY;INTERVAL=10;COUNT=5';
1256 $mang = new rrule_manager($rrule);
1257 $mang->parse_rrule();
1258 $mang->create_events($this->event);
1260 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1261 $this->assertCount(5, $records);
1263 $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1264 foreach ($records as $record) {
1265 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1266 // Go to next period.
1267 $expecteddate->add($interval);
1272 * Everyday in January, for 3 years:
1274 * DTSTART;TZID=US-Eastern:19980101T090000
1275 * RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
1276 * ==> (1998 9:00 AM EDT)January 1-31
1277 * (1999 9:00 AM EDT)January 1-31
1278 * (2000 9:00 AM EDT)January 1-31
1280 public function test_everyday_in_jan_for_3_years_yearly() {
1281 global $DB;
1283 // Change our event's date to 01-01-1998, based on the example from the RFC.
1284 $this->change_event_startdate('19980101T090000', 'US/Eastern');
1286 $rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
1287 $mang = new rrule_manager($rrule);
1288 $mang->parse_rrule();
1289 $mang->create_events($this->event);
1291 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1292 // 92 events from 01-01-1998 to 03-01-2000.
1293 $this->assertCount(92, $records);
1295 $untildate = new DateTime('20000131T090000Z');
1296 $untiltimestamp = $untildate->getTimestamp();
1297 foreach ($records as $record) {
1298 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1300 // Assert that the event's date is in January.
1301 $this->assertEquals('January', date('F', $record->timestart));
1306 * Everyday in January, for 3 years:
1308 * DTSTART;TZID=US-Eastern:19980101T090000
1309 * RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1
1310 * ==> (1998 9:00 AM EDT)January 1-31
1311 * (1999 9:00 AM EDT)January 1-31
1312 * (2000 9:00 AM EDT)January 1-31
1314 public function test_everyday_in_jan_for_3_years_daily() {
1315 global $DB;
1317 // Change our event's date to 01-01-1998, based on the example from the RFC.
1318 $this->change_event_startdate('19980101T090000', 'US/Eastern');
1320 $rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
1321 $mang = new rrule_manager($rrule);
1322 $mang->parse_rrule();
1323 $mang->create_events($this->event);
1325 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1326 // 92 events from 01-01-1998 to 03-01-2000.
1327 $this->assertCount(92, $records);
1329 $untildate = new DateTime('20000131T090000Z');
1330 $untiltimestamp = $untildate->getTimestamp();
1331 foreach ($records as $record) {
1332 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1334 // Assert that the event's date is in January.
1335 $this->assertEquals('January', date('F', $record->timestart));
1340 * Weekly for 10 occurrences
1342 * DTSTART;TZID=US-Eastern:19970902T090000
1343 * RRULE:FREQ=WEEKLY;COUNT=10
1344 * ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21
1345 * (1997 9:00 AM EST)October 28;November 4
1347 public function test_weekly_10_count() {
1348 global $DB;
1350 $interval = new DateInterval('P1W');
1352 $rrule = 'FREQ=WEEKLY;COUNT=10';
1353 $mang = new rrule_manager($rrule);
1354 $mang->parse_rrule();
1355 $mang->create_events($this->event);
1357 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1358 $this->assertCount(10, $records);
1360 $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1361 foreach ($records as $record) {
1362 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1363 // Go to next period.
1364 $expecteddate->add($interval);
1369 * Weekly until December 24, 1997.
1371 * DTSTART;TZID=US-Eastern:19970902T090000
1372 * RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z
1373 * ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21,28
1374 * (1997 9:00 AM EST)November 4,11,18,25;December 2,9,16,23
1376 public function test_weekly_until_24_dec_1997() {
1377 global $DB;
1379 $interval = new DateInterval('P1W');
1381 $rrule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z';
1382 $mang = new rrule_manager($rrule);
1383 $mang->parse_rrule();
1384 $mang->create_events($this->event);
1386 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1387 // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1388 $this->assertCount(17, $records);
1390 $untildate = new DateTime('19971224T000000Z');
1391 $untiltimestamp = $untildate->getTimestamp();
1392 $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1393 foreach ($records as $record) {
1394 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1395 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1396 // Go to next period.
1397 $expecteddate->add($interval);
1402 * Every other week - forever:
1404 * DTSTART;TZID=US-Eastern:[Current date]T090000
1405 * RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU
1407 * Sample results (e.g. in the year 1997):
1408 * (1997 9:00 AM EDT)September 2,16,30;October 14
1409 * (1997 9:00 AM EST)October 28;November 11,25;December 9,23
1410 * (1998 9:00 AM EST)January 6,20;February
1411 * ...
1413 public function test_every_other_week_forever() {
1414 global $DB;
1416 // Change the start date for forever events to 9am of the current date in US/Eastern time.
1417 $this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
1419 $interval = new DateInterval('P2W');
1421 $rrule = 'FREQ=WEEKLY;INTERVAL=2;WKST=SU';
1422 $mang = new rrule_manager($rrule);
1423 $mang->parse_rrule();
1424 $mang->create_events($this->event);
1426 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1427 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1429 $untildate = new DateTime();
1430 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1431 $untiltimestamp = $untildate->getTimestamp();
1433 $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1434 foreach ($records as $record) {
1435 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1437 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1438 // Go to next period.
1439 $expecteddate->add($interval);
1444 * Weekly on Tuesday and Thursday for 5 weeks:
1446 * DTSTART;TZID=US-Eastern:19970902T090000
1447 * RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH
1448 * ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
1450 public function test_weekly_on_tue_thu_for_5_weeks_by_until() {
1451 global $DB;
1453 $rrule = 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH';
1454 $mang = new rrule_manager($rrule);
1455 $mang->parse_rrule();
1456 $mang->create_events($this->event);
1458 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1459 // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1460 $this->assertCount(10, $records);
1462 $untildate = new DateTime('19971007T000000Z');
1463 $untiltimestamp = $untildate->getTimestamp();
1464 $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1465 $startdate = new DateTime($expecteddate->format('Y-m-d'));
1466 $offset = $expecteddate->diff($startdate, true);
1467 foreach ($records as $record) {
1468 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1470 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1471 // Go to next period.
1472 if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) {
1473 $expecteddate->modify('next Thursday');
1474 } else {
1475 $expecteddate->modify('next Tuesday');
1477 $expecteddate->add($offset);
1482 * Weekly on Tuesday and Thursday for 5 weeks:
1484 * DTSTART;TZID=US-Eastern:19970902T090000
1485 * RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH
1486 * ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
1488 public function test_weekly_on_tue_thu_for_5_weeks_by_count() {
1489 global $DB;
1491 $rrule = 'FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH';
1492 $mang = new rrule_manager($rrule);
1493 $mang->parse_rrule();
1494 $mang->create_events($this->event);
1496 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1497 // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1498 $this->assertCount(10, $records);
1500 $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1501 $startdate = new DateTime($expecteddate->format('Y-m-d'));
1502 $offset = $expecteddate->diff($startdate, true);
1503 foreach ($records as $record) {
1504 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1505 // Go to next period.
1506 if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) {
1507 $expecteddate->modify('next Thursday');
1508 } else {
1509 $expecteddate->modify('next Tuesday');
1511 $expecteddate->add($offset);
1516 * Every other week on Monday, Wednesday and Friday until December 24, 1997, but starting on Tuesday, September 2, 1997:
1518 * DTSTART;TZID=US-Eastern:19970902T090000
1519 * RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR
1520 * ==> (1997 9:00 AM EDT)September 3,5,15,17,19,29;October 1,3,13,15,17
1521 * (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;December 8,10,12,22
1523 public function test_every_other_week_until_24_dec_1997_byday() {
1524 global $DB;
1526 $rrule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR';
1527 $mang = new rrule_manager($rrule);
1528 $mang->parse_rrule();
1529 $mang->create_events($this->event);
1531 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1532 // 24 iterations every M-W-F from 03-09-1997 13:00 UTC to 22-12-1997 13:00 UTC.
1533 $this->assertCount(24, $records);
1535 $untildate = new DateTime('19971224T000000Z');
1536 $untiltimestamp = $untildate->getTimestamp();
1538 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1539 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1541 $offsetinterval = $startdatetime->diff($startdate, true);
1543 // First occurrence of this set of events is on 3 September 1999.
1544 $expecteddate = clone($startdatetime);
1545 $expecteddate->modify('next Wednesday');
1546 $expecteddate->add($offsetinterval);
1547 foreach ($records as $record) {
1548 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1549 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1551 // Go to next period.
1552 switch ($expecteddate->format('l')) {
1553 case rrule_manager::DAY_MONDAY:
1554 $expecteddate->modify('next Wednesday');
1555 break;
1556 case rrule_manager::DAY_WEDNESDAY:
1557 $expecteddate->modify('next Friday');
1558 break;
1559 default:
1560 $expecteddate->modify('next Monday');
1561 // Increment expected date by 1 week if the next day is Monday.
1562 $expecteddate->add(new DateInterval('P1W'));
1563 break;
1565 $expecteddate->add($offsetinterval);
1570 * Every other week on Tuesday and Thursday, for 8 occurrences:
1572 * DTSTART;TZID=US-Eastern:19970902T090000
1573 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH
1574 * ==> (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16
1576 public function test_every_other_week_byday_8_count() {
1577 global $DB;
1579 $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH';
1580 $mang = new rrule_manager($rrule);
1581 $mang->parse_rrule();
1582 $mang->create_events($this->event);
1584 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1585 // Should correspond to COUNT rule.
1586 $this->assertCount(8, $records);
1588 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1589 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1591 $offsetinterval = $startdatetime->diff($startdate, true);
1593 // First occurrence of this set of events is on 2 September 1999.
1594 $expecteddate = clone($startdatetime);
1595 foreach ($records as $record) {
1596 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1598 // Go to next period.
1599 switch ($expecteddate->format('l')) {
1600 case rrule_manager::DAY_TUESDAY:
1601 $expecteddate->modify('next Thursday');
1602 break;
1603 default:
1604 $expecteddate->modify('next Tuesday');
1605 // Increment expected date by 1 week if the next day is Tuesday.
1606 $expecteddate->add(new DateInterval('P1W'));
1607 break;
1609 $expecteddate->add($offsetinterval);
1614 * Monthly on the 1st Friday for ten occurrences:
1616 * DTSTART;TZID=US-Eastern:19970905T090000
1617 * RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR
1618 * ==> (1997 9:00 AM EDT)September 5;October 3
1619 * (1997 9:00 AM EST)November 7;Dec 5
1620 * (1998 9:00 AM EST)January 2;February 6;March 6;April 3
1621 * (1998 9:00 AM EDT)May 1;June 5
1623 public function test_monthly_every_first_friday_10_count() {
1624 global $DB;
1626 // Change our event's date to 05-09-1997, based on the example from the RFC.
1627 $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
1628 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1629 $offsetinterval = $startdatetime->diff($startdate, true);
1631 $rrule = 'FREQ=MONTHLY;COUNT=10;BYDAY=1FR';
1632 $mang = new rrule_manager($rrule);
1633 $mang->parse_rrule();
1634 $mang->create_events($this->event);
1636 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1637 // Should correspond to COUNT rule.
1638 $this->assertCount(10, $records);
1640 foreach ($records as $record) {
1641 // Get the first Friday of the record's month.
1642 $recordmonthyear = date('F Y', $record->timestart);
1643 $expecteddate = new DateTime('first Friday of ' . $recordmonthyear);
1644 // Add the time of the event.
1645 $expecteddate->add($offsetinterval);
1647 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1652 * Monthly on the 1st Friday until December 24, 1997:
1654 * DTSTART;TZID=US-Eastern:19970905T090000
1655 * RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR
1656 * ==> (1997 9:00 AM EDT)September 5;October 3
1657 * (1997 9:00 AM EST)November 7;December 5
1659 public function test_monthly_every_first_friday_until() {
1660 global $DB;
1662 // Change our event's date to 05-09-1997, based on the example from the RFC.
1663 $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
1664 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1665 $offsetinterval = $startdatetime->diff($startdate, true);
1667 $rrule = 'FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR';
1668 $mang = new rrule_manager($rrule);
1669 $mang->parse_rrule();
1670 $mang->create_events($this->event);
1672 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1673 // Should have 4 events, every first friday of September 1997 to December 1997.
1674 $this->assertCount(4, $records);
1676 foreach ($records as $record) {
1677 // Get the first Friday of the record's month.
1678 $recordmonthyear = date('F Y', $record->timestart);
1679 $expecteddate = new DateTime('first Friday of ' . $recordmonthyear);
1680 // Add the time of the event.
1681 $expecteddate->add($offsetinterval);
1683 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1688 * Every other month on the 1st and last Sunday of the month for 10 occurrences:
1690 * DTSTART;TZID=US-Eastern:19970907T090000
1691 * RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
1692 * ==> (1997 9:00 AM EDT)September 7,28
1693 * (1997 9:00 AM EST)November 2,30
1694 * (1998 9:00 AM EST)January 4,25;March 1,29
1695 * (1998 9:00 AM EDT)May 3,31
1697 public function test_every_other_month_1st_and_last_sunday_10_count() {
1698 global $DB;
1700 // Change our event's date to 05-09-1997, based on the example from the RFC.
1701 $startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
1702 $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
1703 $offsetinterval = $startdatetime->diff($startdate, true);
1705 $rrule = 'FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU';
1706 $mang = new rrule_manager($rrule);
1707 $mang->parse_rrule();
1708 $mang->create_events($this->event);
1710 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1711 // Should have 10 records based on COUNT rule.
1712 $this->assertCount(10, $records);
1714 // First occurrence is 07-09-1997 which is the first Sunday.
1715 $ordinal = 'first';
1716 foreach ($records as $record) {
1717 // Get date of the month's first/last Sunday.
1718 $recordmonthyear = date('F Y', $record->timestart);
1719 $expecteddate = new DateTime($ordinal . ' Sunday of ' . $recordmonthyear);
1720 $expecteddate->add($offsetinterval);
1722 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1723 if ($ordinal === 'first') {
1724 $ordinal = 'last';
1725 } else {
1726 $ordinal = 'first';
1732 * Monthly on the second to last Monday of the month for 6 months:
1734 * DTSTART;TZID=US-Eastern:19970922T090000
1735 * RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO
1736 * ==> (1997 9:00 AM EDT)September 22;October 20
1737 * (1997 9:00 AM EST)November 17;December 22
1738 * (1998 9:00 AM EST)January 19;February 16
1740 public function test_monthly_last_monday_for_6_months() {
1741 global $DB;
1743 // Change our event's date to 05-09-1997, based on the example from the RFC.
1744 $startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
1745 $startdate = new DateTime($startdatetime->format('Y-m-d'));
1746 $offsetinterval = $startdatetime->diff($startdate, true);
1748 $rrule = 'FREQ=MONTHLY;COUNT=6;BYDAY=-2MO';
1749 $mang = new rrule_manager($rrule);
1750 $mang->parse_rrule();
1751 $mang->create_events($this->event);
1753 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1754 // Should have 6 records based on COUNT rule.
1755 $this->assertCount(6, $records);
1757 foreach ($records as $record) {
1758 // Get date of the month's last Monday.
1759 $recordmonthyear = date('F Y', $record->timestart);
1760 $expecteddate = new DateTime('last Monday of ' . $recordmonthyear);
1761 // Modify to get the second to the last Monday.
1762 $expecteddate->modify('last Monday');
1763 // Add offset.
1764 $expecteddate->add($offsetinterval);
1766 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1771 * Monthly on the third to the last day of the month, forever:
1773 * DTSTART;TZID=US-Eastern:[Current year]0928T090000
1774 * RRULE:FREQ=MONTHLY;BYMONTHDAY=-3
1776 * Sample results (e.g. in the year 1997):
1777 * (1997 9:00 AM EDT)September 28
1778 * (1997 9:00 AM EST)October 29;November 28;December 29
1779 * (1998 9:00 AM EST)January 29;February 26
1780 * ...
1782 public function test_third_to_the_last_day_of_the_month_forever() {
1783 global $DB;
1785 // Change our event's date to 28 September of the current year, based on the example from the RFC.
1786 $this->change_event_startdate(date('Y0928\T090000'), 'US/Eastern');
1788 $rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
1789 $mang = new rrule_manager($rrule);
1790 $mang->parse_rrule();
1791 $mang->create_events($this->event);
1793 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1794 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1796 $untildate = new DateTime();
1797 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1798 $untiltimestamp = $untildate->getTimestamp();
1800 $subinterval = new DateInterval('P2D');
1801 foreach ($records as $record) {
1802 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1804 // Get date of the third to the last day of the month.
1805 $recordmonthyear = date('F Y', $record->timestart);
1806 $expecteddate = new DateTime('last day of ' . $recordmonthyear);
1807 // Set time to 9am.
1808 $expecteddate->setTime(9, 0);
1809 // Modify to get the third to the last day of the month.
1810 $expecteddate->sub($subinterval);
1812 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1817 * Monthly on the 2nd and 15th of the month for 10 occurrences:
1819 * DTSTART;TZID=US-Eastern:19970902T090000
1820 * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15
1821 * ==> (1997 9:00 AM EDT)September 2,15;October 2,15
1822 * (1997 9:00 AM EST)November 2,15;December 2,15
1823 * (1998 9:00 AM EST)January 2,15
1825 public function test_every_2nd_and_15th_of_the_month_10_count() {
1826 global $DB;
1828 $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1829 $startdate = new DateTime($startdatetime->format('Y-m-d'));
1830 $offsetinterval = $startdatetime->diff($startdate, true);
1832 $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15';
1833 $mang = new rrule_manager($rrule);
1834 $mang->parse_rrule();
1835 $mang->create_events($this->event);
1837 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1838 // Should have 10 records based on COUNT rule.
1839 $this->assertCount(10, $records);
1841 $day = '02';
1842 foreach ($records as $record) {
1843 // Get the first Friday of the record's month.
1844 $recordmonthyear = date('Y-m', $record->timestart);
1846 // Get date of the month's last Monday.
1847 $expecteddate = new DateTime("$recordmonthyear-$day");
1848 // Add offset.
1849 $expecteddate->add($offsetinterval);
1850 if ($day === '02') {
1851 $day = '15';
1852 } else {
1853 $day = '02';
1856 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1861 * Monthly on the first and last day of the month for 10 occurrences:
1863 * DTSTART;TZID=US-Eastern:19970930T090000
1864 * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1
1865 * ==> (1997 9:00 AM EDT)September 30;October 1
1866 * (1997 9:00 AM EST)October 31;November 1,30;December 1,31
1867 * (1998 9:00 AM EST)January 1,31;February 1
1869 public function test_every_first_and_last_day_of_the_month_10_count() {
1870 global $DB;
1872 $startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
1873 $startdate = new DateTime($startdatetime->format('Y-m-d'));
1874 $offsetinterval = $startdatetime->diff($startdate, true);
1876 $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1';
1877 $mang = new rrule_manager($rrule);
1878 $mang->parse_rrule();
1879 $mang->create_events($this->event);
1881 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1882 // Should have 10 records based on COUNT rule.
1883 $this->assertCount(10, $records);
1885 // First occurrence is 30-Sep-1997.
1886 $day = 'last';
1887 foreach ($records as $record) {
1888 // Get the first Friday of the record's month.
1889 $recordmonthyear = date('F Y', $record->timestart);
1891 // Get date of the month's last Monday.
1892 $expecteddate = new DateTime("$day day of $recordmonthyear");
1893 // Add offset.
1894 $expecteddate->add($offsetinterval);
1896 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1898 if ($day === 'first') {
1899 $day = 'last';
1900 } else {
1901 $day = 'first';
1907 * Every 18 months on the 10th thru 15th of the month for 10 occurrences:
1909 * DTSTART;TZID=US-Eastern:19970910T090000
1910 * RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15
1911 * ==> (1997 9:00 AM EDT)September 10,11,12,13,14,15
1912 * (1999 9:00 AM EST)March 10,11,12,13
1914 public function test_every_18_months_days_10_to_15_10_count() {
1915 global $DB;
1917 $startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
1919 $rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
1920 $mang = new rrule_manager($rrule);
1921 $mang->parse_rrule();
1922 $mang->create_events($this->event);
1924 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
1925 // Should have 10 records based on COUNT rule.
1926 $this->assertCount(10, $records);
1928 // First occurrence is 10-Sep-1997.
1929 $expecteddate = clone($startdatetime);
1930 $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
1931 foreach ($records as $record) {
1932 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1934 // Get next expected date.
1935 if ($expecteddate->format('d') == 15) {
1936 // If 15th, increment by 18 months.
1937 $expecteddate->add(new DateInterval('P18M'));
1938 // Then go back to the 10th.
1939 $expecteddate->sub(new DateInterval('P5D'));
1940 } else {
1941 // Otherwise, increment by 1 day.
1942 $expecteddate->add(new DateInterval('P1D'));
1948 * Every Tuesday, every other month:
1950 * DTSTART;TZID=US-Eastern:[Next Tuesday]T090000
1951 * RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU
1953 * Sample results (e.g. in the year 1997):
1954 * (1997 9:00 AM EDT)September 2,9,16,23,30
1955 * (1997 9:00 AM EST)November 4,11,18,25
1956 * (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31
1957 * ...
1959 public function test_every_tuesday_every_other_month_forever() {
1960 global $DB;
1962 // Change the start date for forever events to 9am of the Tuesday on or before of the current date in US/Eastern time.
1963 $nexttuesday = new DateTime('next Tuesday');
1964 $this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'US/Eastern');
1966 $rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
1967 $mang = new rrule_manager($rrule);
1968 $mang->parse_rrule();
1969 $mang->create_events($this->event);
1971 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1972 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1974 $untildate = new DateTime();
1975 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
1976 $untiltimestamp = $untildate->getTimestamp();
1978 $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
1979 $nextmonth = new DateTime($expecteddate->format('Y-m-d'));
1980 $offset = $expecteddate->diff($nextmonth, true);
1981 $nextmonth->modify('first day of next month');
1982 foreach ($records as $record) {
1983 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
1985 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
1987 // Get next expected date.
1988 $expecteddate->modify('next Tuesday');
1989 if ($expecteddate->getTimestamp() >= $nextmonth->getTimestamp()) {
1990 // Go to the end of the month.
1991 $expecteddate->modify('last day of this month');
1992 // Find the next Tuesday.
1993 $expecteddate->modify('next Tuesday');
1995 // Increment next month by 2 months.
1996 $nextmonth->add(new DateInterval('P2M'));
1998 $expecteddate->add($offset);
2003 * Yearly in June and July for 10 occurrences:
2005 * DTSTART;TZID=US-Eastern:19970610T090000
2006 * RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7
2007 * ==> (1997 9:00 AM EDT)June 10;July 10
2008 * (1998 9:00 AM EDT)June 10;July 10
2009 * (1999 9:00 AM EDT)June 10;July 10
2010 * (2000 9:00 AM EDT)June 10;July 10
2011 * (2001 9:00 AM EDT)June 10;July 10
2012 * Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components are specified, the day is gotten from DTSTART.
2014 public function test_yearly_in_june_july_10_count() {
2015 global $DB;
2017 $startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
2019 $rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
2020 $mang = new rrule_manager($rrule);
2021 $mang->parse_rrule();
2022 $mang->create_events($this->event);
2024 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2025 // Should have 10 records based on COUNT rule.
2026 $this->assertCount(10, $records);
2028 $expecteddate = $startdatetime;
2029 $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2030 $monthinterval = new DateInterval('P1M');
2031 $yearinterval = new DateInterval('P1Y');
2032 foreach ($records as $record) {
2033 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2035 // Get next expected date.
2036 if ($expecteddate->format('m') == 6) {
2037 // Go to the month of July.
2038 $expecteddate->add($monthinterval);
2039 } else {
2040 // Go to the month of June next year.
2041 $expecteddate->sub($monthinterval);
2042 $expecteddate->add($yearinterval);
2048 * Every other year on January, February, and March for 10 occurrences:
2050 * DTSTART;TZID=US-Eastern:19970310T090000
2051 * RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3
2052 * ==> (1997 9:00 AM EST)March 10
2053 * (1999 9:00 AM EST)January 10;February 10;March 10
2054 * (2001 9:00 AM EST)January 10;February 10;March 10
2055 * (2003 9:00 AM EST)January 10;February 10;March 10
2057 public function test_every_other_year_in_june_july_10_count() {
2058 global $DB;
2060 $startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
2062 $rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
2063 $mang = new rrule_manager($rrule);
2064 $mang->parse_rrule();
2065 $mang->create_events($this->event);
2067 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2068 // Should have 10 records based on COUNT rule.
2069 $this->assertCount(10, $records);
2071 $expecteddate = $startdatetime;
2072 $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2073 $monthinterval = new DateInterval('P1M');
2074 $yearinterval = new DateInterval('P2Y');
2075 foreach ($records as $record) {
2076 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2078 // Get next expected date.
2079 if ($expecteddate->format('m') != 3) {
2080 // Go to the next month.
2081 $expecteddate->add($monthinterval);
2082 } else {
2083 // Go to the month of January next year.
2084 $expecteddate->sub($monthinterval);
2085 $expecteddate->sub($monthinterval);
2086 $expecteddate->add($yearinterval);
2092 * Every 3rd year on the 1st, 100th and 200th day for 10 occurrences:
2094 * DTSTART;TZID=US-Eastern:19970101T090000
2095 * RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200
2096 * ==> (1997 9:00 AM EST)January 1
2097 * (1997 9:00 AM EDT)April 10;July 19
2098 * (2000 9:00 AM EST)January 1
2099 * (2000 9:00 AM EDT)April 9;July 18
2100 * (2003 9:00 AM EST)January 1
2101 * (2003 9:00 AM EDT)April 10;July 19
2102 * (2006 9:00 AM EST)January 1
2104 public function test_every_3_years_1st_100th_200th_days_10_count() {
2105 global $DB;
2107 $startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
2109 $rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
2110 $mang = new rrule_manager($rrule);
2111 $mang->parse_rrule();
2112 $mang->create_events($this->event);
2114 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2115 // Should have 10 records based on COUNT rule.
2116 $this->assertCount(10, $records);
2118 $expecteddate = $startdatetime;
2119 $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2120 $hundredthdayinterval = new DateInterval('P99D');
2121 $twohundredthdayinterval = new DateInterval('P100D');
2122 $yearinterval = new DateInterval('P3Y');
2124 foreach ($records as $record) {
2125 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2127 // Get next expected date.
2128 if ($expecteddate->format('z') == 0) { // January 1.
2129 $expecteddate->add($hundredthdayinterval);
2130 } else if ($expecteddate->format('z') == 99) { // 100th day of the year.
2131 $expecteddate->add($twohundredthdayinterval);
2132 } else { // 200th day of the year.
2133 $expecteddate->add($yearinterval);
2134 $expecteddate->modify('January 1');
2140 * Every 20th Monday of the year, forever:
2142 * DTSTART;TZID=US-Eastern:[20th Monday of the current year]T090000
2143 * RRULE:FREQ=YEARLY;BYDAY=20MO
2145 * Sample results (e.g. in the year 1997):
2146 * (1997 9:00 AM EDT)May 19
2147 * (1998 9:00 AM EDT)May 18
2148 * (1999 9:00 AM EDT)May 17
2149 * ...
2151 public function test_yearly_every_20th_monday_forever() {
2152 global $DB;
2154 // Change our event's date to the 20th Monday of the current year.
2155 $twentiethmonday = new DateTime(date('Y-01-01'));
2156 $twentiethmonday->modify('+20 Monday');
2157 $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T090000'), 'US/Eastern');
2159 $startdate = new DateTime($startdatetime->format('Y-m-d'));
2161 $offset = $startdatetime->diff($startdate, true);
2163 $interval = new DateInterval('P1Y');
2165 $rrule = 'FREQ=YEARLY;BYDAY=20MO';
2166 $mang = new rrule_manager($rrule);
2167 $mang->parse_rrule();
2168 $mang->create_events($this->event);
2170 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2172 $untildate = new DateTime();
2173 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2174 $untiltimestamp = $untildate->getTimestamp();
2176 $expecteddate = $startdatetime;
2177 $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2178 foreach ($records as $record) {
2179 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2180 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2182 // Go to next period.
2183 $expecteddate->modify('January 1');
2184 $expecteddate->add($interval);
2185 $expecteddate->modify("+20 Monday");
2186 $expecteddate->add($offset);
2191 * Monday of week number 20 (where the default start of the week is Monday), forever:
2193 * DTSTART;TZID=US-Eastern:[1st day of the 20th week this year]T090000
2194 * RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO
2196 * Sample results (e.g. in the year 1997):
2197 * (1997 9:00 AM EDT)May 12
2198 * (1998 9:00 AM EDT)May 11
2199 * (1999 9:00 AM EDT)May 17
2200 * ...
2202 public function test_yearly_byweekno_forever() {
2203 global $DB;
2205 // Change our event's date to the start of the 20th week of the current year.
2206 $twentiethweek = new DateTime(date('Y-01-01'));
2207 $twentiethweek->setISODate($twentiethweek->format('Y'), 20);
2208 $startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'US/Eastern');
2210 $startdate = clone($startdatetime);
2211 $startdate->modify($startdate->format('Y-m-d'));
2213 $offset = $startdatetime->diff($startdate, true);
2215 $interval = new DateInterval('P1Y');
2217 $rrule = 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO';
2218 $mang = new rrule_manager($rrule);
2219 $mang->parse_rrule();
2220 $mang->create_events($this->event);
2222 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2224 $untildate = new DateTime();
2225 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2226 $untiltimestamp = $untildate->getTimestamp();
2228 $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
2229 foreach ($records as $record) {
2230 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2231 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2233 // Go to next period.
2234 $expecteddate->add($interval);
2235 $expecteddate->setISODate($expecteddate->format('Y'), 20);
2236 $expecteddate->add($offset);
2241 * Every Thursday in March, forever:
2243 * DTSTART;TZID=US-Eastern:[First thursday of March of the current year]T090000
2244 * RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH
2246 * Sample results (e.g. in the year 1997):
2247 * (1997 9:00 AM EST)March 13,20,27
2248 * (1998 9:00 AM EST)March 5,12,19,26
2249 * (1999 9:00 AM EST)March 4,11,18,25
2250 * ...
2252 public function test_every_thursday_in_march_forever() {
2253 global $DB;
2255 // Change our event's date to the first Thursday of March of the current year at 9am US/Eastern time.
2256 $firstthursdayofmarch = new DateTime('first Thursday of March');
2257 $startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'US/Eastern');
2259 $interval = new DateInterval('P1Y');
2261 $rrule = 'FREQ=YEARLY;BYMONTH=3;BYDAY=TH';
2262 $mang = new rrule_manager($rrule);
2263 $mang->parse_rrule();
2264 $mang->create_events($this->event);
2266 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2267 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2269 $untildate = new DateTime();
2270 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2271 $untiltimestamp = $untildate->getTimestamp();
2273 $expecteddate = $startdatetime;
2274 $startdate = new DateTime($startdatetime->format('Y-m-d'));
2275 $offsetinterval = $startdatetime->diff($startdate, true);
2276 $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
2277 $april1st = new DateTime('April 1');
2278 foreach ($records as $record) {
2279 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2280 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2282 // Go to next period.
2283 $expecteddate->modify('next Thursday');
2284 if ($expecteddate->getTimestamp() >= $april1st->getTimestamp()) {
2285 // Reset to 1st of March.
2286 $expecteddate->modify('first day of March');
2287 // Go to next year.
2288 $expecteddate->add($interval);
2289 if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) {
2290 $expecteddate->modify('next Thursday');
2292 // Increment to next year's April 1st.
2293 $april1st->add($interval);
2295 $expecteddate->add($offsetinterval);
2300 * Every Thursday, but only during June, July, and August, forever:
2302 * DTSTART;TZID=US-Eastern:[First Thursday of June of the current year]T090000
2303 * RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8
2305 * Sample results (e.g. in the year 1997):
2306 * (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31;August 7,14,21,28
2307 * (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30;August 6,13,20,27
2308 * (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29;August 5,12,19,26
2309 * ...
2311 public function test_every_thursday_june_july_august_forever() {
2312 global $DB;
2314 // Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
2315 $firstthursdayofjune = new DateTime('first Thursday of June');
2316 $startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'US/Eastern');
2318 $startdate = new DateTime($startdatetime->format('Y-m-d'));
2320 $offset = $startdatetime->diff($startdate, true);
2322 $interval = new DateInterval('P1Y');
2324 $rrule = 'FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8';
2325 $mang = new rrule_manager($rrule);
2326 $mang->parse_rrule();
2327 $mang->create_events($this->event);
2329 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2330 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2332 $untildate = new DateTime();
2333 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2334 $untiltimestamp = $untildate->getTimestamp();
2336 $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
2337 $september1st = new DateTime('September 1');
2338 foreach ($records as $record) {
2339 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2340 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2342 // Go to next period.
2343 $expecteddate->modify('next Thursday');
2344 if ($expecteddate->getTimestamp() >= $september1st->getTimestamp()) {
2345 $expecteddate->add($interval);
2346 $expecteddate->modify('June 1');
2347 if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) {
2348 $expecteddate->modify('next Thursday');
2350 $september1st->add($interval);
2352 $expecteddate->add($offset);
2357 * Every Friday the 13th, forever:
2359 * DTSTART;TZID=US-Eastern:[Current date]T090000
2360 * RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
2362 * Sample results (e.g. in the year 1997):
2363 * (1998 9:00 AM EST)February 13;March 13;November 13
2364 * (1999 9:00 AM EDT)August 13
2365 * (2000 9:00 AM EDT)October 13
2366 * ...
2368 public function test_friday_the_thirteenth_forever() {
2369 global $DB;
2371 // Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
2372 $this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
2374 $rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
2375 $mang = new rrule_manager($rrule);
2376 $mang->parse_rrule();
2377 $mang->create_events($this->event);
2379 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2380 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2382 $untildate = new DateTime();
2383 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2384 $untiltimestamp = $untildate->getTimestamp();
2386 foreach ($records as $record) {
2387 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2388 // Assert that the day of the month and the day correspond to Friday the 13th.
2389 $this->assertEquals('Friday 13', date('l d', $record->timestart));
2394 * The first Saturday that follows the first Sunday of the month, forever:
2396 * DTSTART;TZID=US-Eastern:[The Saturday after the month's first Sunday]T090000
2397 * RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13
2399 * Sample results (e.g. from 13 September 1997):
2400 * (1997 9:00 AM EDT)September 13;October 11
2401 * (1997 9:00 AM EST)November 8;December 13
2402 * (1998 9:00 AM EST)January 10;February 7;March 7
2403 * (1998 9:00 AM EDT)April 11;May 9;June 13...
2405 public function test_first_saturday_following_first_sunday_forever() {
2406 global $DB;
2408 // Change our event's date to the next Saturday after the first Sunday of the the current month at 9am US/Eastern time.
2409 $firstsaturdayafterfirstsunday = new DateTime('first Sunday of this month');
2410 $firstsaturdayafterfirstsunday->modify('next Saturday');
2411 $startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'US/Eastern');
2412 $startdate = new DateTime($startdatetime->format('Y-m-d'));
2413 $offset = $startdatetime->diff($startdate, true);
2415 $rrule = 'FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13';
2416 $mang = new rrule_manager($rrule);
2417 $mang->parse_rrule();
2418 $mang->create_events($this->event);
2420 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2421 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2423 $untildate = new DateTime();
2424 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2425 $untiltimestamp = $untildate->getTimestamp();
2426 $bymonthdays = [7, 8, 9, 10, 11, 12, 13];
2427 foreach ($records as $record) {
2428 $recordmonthyear = date('F Y', $record->timestart);
2429 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2431 // Get first Saturday after the first Sunday of the month.
2432 $expecteddate = new DateTime('first Sunday of ' . $recordmonthyear);
2433 $expecteddate->modify('next Saturday');
2434 $expecteddate->add($offset);
2436 // Assert the record's date corresponds to the first Saturday of the month.
2437 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2439 // Assert that the record is either the 7th, 8th, 9th, ... 13th day of the month.
2440 $this->assertContains(date('j', $record->timestart), $bymonthdays);
2445 * Every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day):
2447 * DTSTART;TZID=US-Eastern:[Most recent election date]T090000
2448 * RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
2450 * Sample results (e.g. from 05 November 1996):
2451 * (1996 9:00 AM EST)November 5
2452 * (2000 9:00 AM EST)November 7
2453 * (2004 9:00 AM EST)November 2
2454 * ...
2456 public function test_every_us_presidential_election_forever() {
2457 global $DB;
2459 // Calculate the most recent election date, starting from 1996 (e.g. today's 2017 so the most recent election was in 2016).
2460 $currentyear = (int) date('Y');
2461 $electionyear = 1996;
2462 while ($electionyear + 4 < $currentyear) {
2463 $electionyear += 4;
2465 $electiondate = new DateTime('first Monday of November ' . $electionyear);
2466 $electiondate->modify('+1 Tuesday');
2468 // Use the most recent election date as the starting date of our recurring events.
2469 $startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'US/Eastern');
2470 $startdate = new DateTime($startdatetime->format('Y-m-d'));
2471 $offset = $startdatetime->diff($startdate, true);
2473 $rrule = 'FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8';
2474 $mang = new rrule_manager($rrule);
2475 $mang->parse_rrule();
2476 $mang->create_events($this->event);
2478 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2480 $untildate = new DateTime();
2481 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2482 $untiltimestamp = $untildate->getTimestamp();
2483 $bymonthdays = [2, 3, 4, 5, 6, 7, 8];
2484 foreach ($records as $record) {
2485 $recordmonthyear = date('F Y', $record->timestart);
2486 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2488 // Get first Saturday after the first Sunday of the month.
2489 $expecteddate = new DateTime('first Monday of ' . $recordmonthyear);
2490 $expecteddate->modify('next Tuesday');
2491 $expecteddate->add($offset);
2493 // Assert the record's date corresponds to the first Saturday of the month.
2494 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
2496 // Assert that the record is either the 2nd, 3rd, 4th ... 8th day of the month.
2497 $this->assertContains(date('j', $record->timestart), $bymonthdays);
2502 * The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months:
2504 * DTSTART;TZID=US-Eastern:19970904T090000
2505 * RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3
2506 * ==> (1997 9:00 AM EDT)September 4;October 7
2507 * (1997 9:00 AM EST)November 6
2509 public function test_monthly_bysetpos_3_count() {
2510 global $DB;
2512 $this->change_event_startdate('19970904T090000', 'US/Eastern');
2514 $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
2515 $mang = new rrule_manager($rrule);
2516 $mang->parse_rrule();
2517 $mang->create_events($this->event);
2519 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2520 $this->assertCount(3, $records);
2522 $expecteddates = [
2523 (new DateTime('1997-09-04 09:00:00 EDT'))->getTimestamp(),
2524 (new DateTime('1997-10-07 09:00:00 EDT'))->getTimestamp(),
2525 (new DateTime('1997-11-06 09:00:00 EST'))->getTimestamp()
2527 foreach ($records as $record) {
2528 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2533 * The 2nd to last weekday of the month:
2535 * DTSTART;TZID=US-Eastern:19970929T090000
2536 * RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7
2537 * ==> (1997 9:00 AM EDT)September 29
2538 * (1997 9:00 AM EST)October 30;November 27;December 30
2539 * (1998 9:00 AM EST)January 29;February 26;March 30
2540 * ...
2542 * (Original RFC example is set to recur forever. But we just want to verify that the results match the dates listed from
2543 * the RFC example. So just limit the count to 7.)
2545 public function test_second_to_the_last_weekday_of_the_month() {
2546 global $DB;
2548 $this->change_event_startdate('19970929T090000', 'US/Eastern');
2550 $rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7';
2551 $mang = new rrule_manager($rrule);
2552 $mang->parse_rrule();
2553 $mang->create_events($this->event);
2555 // Get the first 7 samples. This should be enough to verify that we have generated the recurring events correctly.
2556 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart', 0, 7);
2558 $expecteddates = [
2559 (new DateTime('1997-09-29 09:00:00 EDT'))->getTimestamp(),
2560 (new DateTime('1997-10-30 09:00:00 EST'))->getTimestamp(),
2561 (new DateTime('1997-11-27 09:00:00 EST'))->getTimestamp(),
2562 (new DateTime('1997-12-30 09:00:00 EST'))->getTimestamp(),
2563 (new DateTime('1998-01-29 09:00:00 EST'))->getTimestamp(),
2564 (new DateTime('1998-02-26 09:00:00 EST'))->getTimestamp(),
2565 (new DateTime('1998-03-30 09:00:00 EST'))->getTimestamp(),
2568 $untildate = new DateTime();
2569 $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
2570 $untiltimestamp = $untildate->getTimestamp();
2572 $i = 0;
2573 foreach ($records as $record) {
2574 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
2576 // Confirm that the first 7 records correspond to the expected dates listed above.
2577 $this->assertEquals($expecteddates[$i], $record->timestart);
2578 $i++;
2583 * Every 3 hours from 9:00 AM to 5:00 PM on a specific day:
2585 * DTSTART;TZID=US-Eastern:19970902T090000
2586 * RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z
2587 * ==> (September 2, 1997 EDT)09:00,12:00,15:00
2589 public function test_every_3hours_9am_to_5pm() {
2590 global $DB;
2592 $rrule = 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z';
2593 $mang = new rrule_manager($rrule);
2594 $mang->parse_rrule();
2595 $mang->create_events($this->event);
2597 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2598 $this->assertCount(3, $records);
2600 $expecteddates = [
2601 (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2602 (new DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
2603 (new DateTime('1997-09-02 15:00:00 EDT'))->getTimestamp(),
2605 foreach ($records as $record) {
2606 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2611 * Every 15 minutes for 6 occurrences:
2613 * DTSTART;TZID=US-Eastern:19970902T090000
2614 * RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6
2615 * ==> (September 2, 1997 EDT)09:00,09:15,09:30,09:45,10:00,10:15
2617 public function test_every_15minutes_6_count() {
2618 global $DB;
2620 $rrule = 'FREQ=MINUTELY;INTERVAL=15;COUNT=6';
2621 $mang = new rrule_manager($rrule);
2622 $mang->parse_rrule();
2623 $mang->create_events($this->event);
2625 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2626 $this->assertCount(6, $records);
2628 $expecteddates = [
2629 (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2630 (new DateTime('1997-09-02 09:15:00 EDT'))->getTimestamp(),
2631 (new DateTime('1997-09-02 09:30:00 EDT'))->getTimestamp(),
2632 (new DateTime('1997-09-02 09:45:00 EDT'))->getTimestamp(),
2633 (new DateTime('1997-09-02 10:00:00 EDT'))->getTimestamp(),
2634 (new DateTime('1997-09-02 10:15:00 EDT'))->getTimestamp(),
2636 foreach ($records as $record) {
2637 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2642 * Every hour and a half for 4 occurrences:
2644 * DTSTART;TZID=US-Eastern:19970902T090000
2645 * RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4
2646 * ==> (September 2, 1997 EDT)09:00,10:30;12:00;13:30
2648 public function test_every_90minutes_4_count() {
2649 global $DB;
2651 $rrule = 'FREQ=MINUTELY;INTERVAL=90;COUNT=4';
2652 $mang = new rrule_manager($rrule);
2653 $mang->parse_rrule();
2654 $mang->create_events($this->event);
2656 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2657 $this->assertCount(4, $records);
2659 $expecteddates = [
2660 (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2661 (new DateTime('1997-09-02 10:30:00 EDT'))->getTimestamp(),
2662 (new DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
2663 (new DateTime('1997-09-02 13:30:00 EDT'))->getTimestamp(),
2665 foreach ($records as $record) {
2666 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2671 * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
2673 * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
2674 * so just limit the count to 50).
2676 * DTSTART;TZID=US-Eastern:19970902T090000
2677 * RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50
2678 * ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2679 * (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2680 * ...
2682 public function test_every_20minutes_daily_byhour_byminute_50_count() {
2683 global $DB;
2685 $rrule = 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50';
2686 $mang = new rrule_manager($rrule);
2687 $mang->parse_rrule();
2688 $mang->create_events($this->event);
2690 $byminuteinterval = new DateInterval('PT20M');
2691 $bydayinterval = new DateInterval('P1D');
2692 $date = new DateTime('1997-09-02 09:00:00 EDT');
2693 $expecteddates = [];
2694 $count = 50;
2695 for ($i = 0; $i < $count; $i++) {
2696 $expecteddates[] = $date->getTimestamp();
2697 $date->add($byminuteinterval);
2698 if ($date->format('H') > 16) {
2699 // Go to next day.
2700 $date->add($bydayinterval);
2701 // Reset time to 9am.
2702 $date->setTime(9, 0);
2706 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2707 $this->assertCount($count, $records);
2709 foreach ($records as $record) {
2710 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2715 * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
2717 * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
2718 * so just limit the count to 50).
2720 * DTSTART;TZID=US-Eastern:19970902T090000
2721 * RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50
2722 * ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2723 * (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2724 * ...
2726 public function test_every_20minutes_minutely_byhour_50_count() {
2727 global $DB;
2729 $rrule = 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50';
2730 $mang = new rrule_manager($rrule);
2731 $mang->parse_rrule();
2732 $mang->create_events($this->event);
2734 $byminuteinterval = new DateInterval('PT20M');
2735 $bydayinterval = new DateInterval('P1D');
2736 $date = new DateTime('1997-09-02 09:00:00');
2737 $expecteddates = [];
2738 $count = 50;
2739 for ($i = 0; $i < $count; $i++) {
2740 $expecteddates[] = $date->getTimestamp();
2741 $date->add($byminuteinterval);
2742 if ($date->format('H') > 16) {
2743 // Go to next day.
2744 $date->add($bydayinterval);
2745 // Reset time to 9am.
2746 $date->setTime(9, 0);
2750 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2751 $this->assertCount($count, $records);
2753 foreach ($records as $record) {
2754 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2759 * An example where the days generated makes a difference because of WKST:
2761 * DTSTART;TZID=US-Eastern:19970805T090000
2762 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO
2763 * ==> (1997 EDT)Aug 5,10,19,24
2765 public function test_weekly_byday_with_wkst_mo() {
2766 global $DB;
2768 $this->change_event_startdate('19970805T090000', 'US/Eastern');
2770 $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
2771 $mang = new rrule_manager($rrule);
2772 $mang->parse_rrule();
2773 $mang->create_events($this->event);
2775 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2776 $this->assertCount(4, $records);
2778 $expecteddates = [
2779 (new DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
2780 (new DateTime('1997-08-10 09:00:00 EDT'))->getTimestamp(),
2781 (new DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
2782 (new DateTime('1997-08-24 09:00:00 EDT'))->getTimestamp(),
2784 foreach ($records as $record) {
2785 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2790 * An example where the days generated makes a difference because of WKST:
2791 * Changing only WKST from MO to SU, yields different results...
2793 * DTSTART;TZID=US-Eastern:19970805T090000
2794 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU
2795 * ==> (1997 EDT)August 5,17,19,31
2797 public function test_weekly_byday_with_wkst_su() {
2798 global $DB;
2800 $this->change_event_startdate('19970805T090000', 'US/Eastern');
2802 $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
2803 $mang = new rrule_manager($rrule);
2804 $mang->parse_rrule();
2805 $mang->create_events($this->event);
2807 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2808 $this->assertCount(4, $records);
2810 $expecteddates = [
2811 (new DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
2812 (new DateTime('1997-08-17 09:00:00 EDT'))->getTimestamp(),
2813 (new DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
2814 (new DateTime('1997-08-31 09:00:00 EDT'))->getTimestamp(),
2817 foreach ($records as $record) {
2818 $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
2823 * Other edge case tests.
2827 * Tests for MONTHLY RRULE with BYMONTHDAY set to 31.
2828 * Should not include February, April, June, September and November.
2830 public function test_monthly_bymonthday_31() {
2831 global $DB;
2833 $rrule = 'FREQ=MONTHLY;BYMONTHDAY=31;COUNT=20';
2834 $mang = new rrule_manager($rrule);
2835 $mang->parse_rrule();
2836 $mang->create_events($this->event);
2838 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2839 $this->assertCount(20, $records);
2841 $non31months = ['February', 'April', 'June', 'September', 'November'];
2843 foreach ($records as $record) {
2844 $month = date('F', $record->timestart);
2845 $this->assertNotContains($month, $non31months);
2850 * Tests for the last day in February. (Leap year test)
2852 public function test_yearly_on_the_last_day_of_february() {
2853 global $DB;
2855 $rrule = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1;COUNT=30';
2856 $mang = new rrule_manager($rrule);
2857 $mang->parse_rrule();
2858 $mang->create_events($this->event);
2860 $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
2861 $this->assertCount(30, $records);
2863 foreach ($records as $record) {
2864 $date = new DateTime(date('Y-m-d H:i:s', $record->timestart));
2865 $year = $date->format('Y');
2866 $day = $date->format('d');
2867 if ($year % 4 == 0) {
2868 $this->assertEquals(29, $day);
2869 } else {
2870 $this->assertEquals(28, $day);
2876 * Change the event's timestart (DTSTART) based on the test's needs.
2878 * @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000.
2879 * @param null|string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
2880 * If not provided, the default timezone will be used.
2881 * @return bool|DateTime
2883 protected function change_event_startdate($datestr, $timezonestr = null) {
2884 // Use default timezone if not provided.
2885 if ($timezonestr === null) {
2886 $newdatetime = DateTime::createFromFormat('Ymd\THis', $datestr);
2887 } else {
2888 $timezone = new DateTimeZone($timezonestr);
2889 $newdatetime = DateTime::createFromFormat('Ymd\THis', $datestr, $timezone);
2892 // Update the start date of the parent event.
2893 $calevent = calendar_event::load($this->event->id);
2894 $updatedata = (object)[
2895 'timestart' => $newdatetime->getTimestamp(),
2896 'repeatid' => $this->event->id
2898 $calevent->update($updatedata, false);
2899 $this->event->timestart = $calevent->timestart;
2901 return $newdatetime;