2 // This file is part of Moodle - http://moodle.org/
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.
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 namespace core_calendar
;
19 defined('MOODLE_INTERNAL') ||
die();
22 require_once($CFG->dirroot
. '/calendar/lib.php');
25 * Defines test class to test manage rrule during ical imports.
27 * @package core_calendar
29 * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
30 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32 class rrule_manager_test
extends \advanced_testcase
{
34 /** @var calendar_event a dummy event */
40 protected function setUp(): void
{
42 $this->resetAfterTest();
44 // Set our timezone based on the timezone in the RFC's samples (US/Eastern).
46 $this->setTimezone($tz);
47 $timezone = new \
DateTimeZone($tz);
48 // Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
49 $time = \DateTime
::createFromFormat('Ymd\THis', '19970902T090000', $timezone);
50 $timestart = $time->getTimestamp();
52 $user = $this->getDataGenerator()->create_user();
53 $sub = new \
stdClass();
57 $sub->userid
= $user->id
;
58 $sub->pollinterval
= 0;
59 $subid = $DB->insert_record('event_subscriptions', $sub, true);
61 $event = new \
stdClass();
62 $event->name
= 'Event name';
63 $event->description
= '';
64 $event->timestart
= $timestart;
65 $event->timeduration
= 3600;
66 $event->uuid
= 'uuid';
67 $event->subscriptionid
= $subid;
68 $event->userid
= $user->id
;
71 $event->eventtype
= 'user';
72 $eventobj = \calendar_event
::create($event, false);
73 $DB->set_field('event', 'repeatid', $eventobj->id
, array('id' => $eventobj->id
));
74 $eventobj->repeatid
= $eventobj->id
;
75 $this->event
= $eventobj;
79 * Test parse_rrule() method.
81 public function test_parse_rrule() {
95 $rrule = implode(';', $rules);
96 $mang = new rrule_manager($rrule);
111 'freq' => rrule_manager
::FREQ_YEARLY
,
114 'bysecond' => [20, 40],
115 'byminute' => [2, 30],
117 'byday' => $bydayrules,
118 'bymonthday' => [20, 30],
119 'byyearday' => [300, -20],
120 'byweekno' => [22, 33],
124 $reflectionclass = new \
ReflectionClass($mang);
125 foreach ($props as $prop => $expectedval) {
126 $rcprop = $reflectionclass->getProperty($prop);
127 $rcprop->setAccessible(true);
128 $this->assertEquals($expectedval, $rcprop->getValue($mang));
133 * Test exception is thrown for invalid property.
135 public function test_parse_rrule_validation() {
136 $rrule = "RANDOM=PROPERTY;";
137 $mang = new rrule_manager($rrule);
138 $this->expectException('moodle_exception');
139 $mang->parse_rrule();
143 * Test exception is thrown for invalid frequency.
145 public function test_freq_validation() {
146 $rrule = "FREQ=RANDOMLY;";
147 $mang = new rrule_manager($rrule);
148 $this->expectException('moodle_exception');
149 $mang->parse_rrule();
153 * Test parsing of rules with both COUNT and UNTIL parameters.
155 public function test_until_count_validation() {
156 $until = $this->event
->timestart + DAYSECS
* 4;
157 $until = date('Y-m-d', $until);
158 $rrule = "FREQ=DAILY;COUNT=2;UNTIL=$until";
159 $mang = new rrule_manager($rrule);
160 $this->expectException('moodle_exception');
161 $mang->parse_rrule();
165 * Test parsing of INTERVAL rule.
167 public function test_interval_validation() {
168 $rrule = "INTERVAL=0";
169 $mang = new rrule_manager($rrule);
170 $this->expectException('moodle_exception');
171 $mang->parse_rrule();
175 * Test parsing of BYSECOND rule.
177 public function test_bysecond_validation() {
178 $rrule = "BYSECOND=30,45,60";
179 $mang = new rrule_manager($rrule);
180 $this->expectException('moodle_exception');
181 $mang->parse_rrule();
185 * Test parsing of BYMINUTE rule.
187 public function test_byminute_validation() {
188 $rrule = "BYMINUTE=30,45,60";
189 $mang = new rrule_manager($rrule);
190 $this->expectException('moodle_exception');
191 $mang->parse_rrule();
195 * Test parsing of BYMINUTE rule.
197 public function test_byhour_validation() {
198 $rrule = "BYHOUR=23,45";
199 $mang = new rrule_manager($rrule);
200 $this->expectException('moodle_exception');
201 $mang->parse_rrule();
205 * Test parsing of BYDAY rule.
207 public function test_byday_validation() {
208 $rrule = "BYDAY=MO,2SE";
209 $mang = new rrule_manager($rrule);
210 $this->expectException('moodle_exception');
211 $mang->parse_rrule();
215 * Test parsing of BYDAY rule with prefixes.
217 public function test_byday_with_prefix_validation() {
218 // This is acceptable.
219 $rrule = "FREQ=MONTHLY;BYDAY=-1MO,2SA";
220 $mang = new rrule_manager($rrule);
221 $mang->parse_rrule();
223 // This is also acceptable.
224 $rrule = "FREQ=YEARLY;BYDAY=MO,2SA";
225 $mang = new rrule_manager($rrule);
226 $mang->parse_rrule();
229 $rrule = "FREQ=WEEKLY;BYDAY=MO,2SA";
230 $mang = new rrule_manager($rrule);
231 $this->expectException('moodle_exception');
232 $mang->parse_rrule();
236 * Test parsing of BYMONTHDAY rule.
238 public function test_bymonthday_upper_bound_validation() {
239 $rrule = "BYMONTHDAY=1,32";
240 $mang = new rrule_manager($rrule);
241 $this->expectException('moodle_exception');
242 $mang->parse_rrule();
246 * Test parsing of BYMONTHDAY rule.
248 public function test_bymonthday_0_validation() {
249 $rrule = "BYMONTHDAY=1,0";
250 $mang = new rrule_manager($rrule);
251 $this->expectException('moodle_exception');
252 $mang->parse_rrule();
256 * Test parsing of BYMONTHDAY rule.
258 public function test_bymonthday_lower_bound_validation() {
259 $rrule = "BYMONTHDAY=1,-31,-32";
260 $mang = new rrule_manager($rrule);
261 $this->expectException('moodle_exception');
262 $mang->parse_rrule();
266 * Test parsing of BYYEARDAY rule.
268 public function test_byyearday_upper_bound_validation() {
269 $rrule = "BYYEARDAY=1,366,367";
270 $mang = new rrule_manager($rrule);
271 $this->expectException('moodle_exception');
272 $mang->parse_rrule();
276 * Test parsing of BYYEARDAY rule.
278 public function test_byyearday_0_validation() {
279 $rrule = "BYYEARDAY=0";
280 $mang = new rrule_manager($rrule);
281 $this->expectException('moodle_exception');
282 $mang->parse_rrule();
286 * Test parsing of BYYEARDAY rule.
288 public function test_byyearday_lower_bound_validation() {
289 $rrule = "BYYEARDAY=-1,-366,-367";
290 $mang = new rrule_manager($rrule);
291 $this->expectException('moodle_exception');
292 $mang->parse_rrule();
296 * Test parsing of BYWEEKNO rule.
298 public function test_non_yearly_freq_with_byweekno() {
299 $rrule = "BYWEEKNO=1,53";
300 $mang = new rrule_manager($rrule);
301 $this->expectException('moodle_exception');
302 $mang->parse_rrule();
306 * Test parsing of BYWEEKNO rule.
308 public function test_byweekno_upper_bound_validation() {
309 $rrule = "FREQ=YEARLY;BYWEEKNO=1,53,54";
310 $mang = new rrule_manager($rrule);
311 $this->expectException('moodle_exception');
312 $mang->parse_rrule();
316 * Test parsing of BYWEEKNO rule.
318 public function test_byweekno_0_validation() {
319 $rrule = "FREQ=YEARLY;BYWEEKNO=0";
320 $mang = new rrule_manager($rrule);
321 $this->expectException('moodle_exception');
322 $mang->parse_rrule();
326 * Test parsing of BYWEEKNO rule.
328 public function test_byweekno_lower_bound_validation() {
329 $rrule = "FREQ=YEARLY;BYWEEKNO=-1,-53,-54";
330 $mang = new rrule_manager($rrule);
331 $this->expectException('moodle_exception');
332 $mang->parse_rrule();
336 * Test parsing of BYMONTH rule.
338 public function test_bymonth_upper_bound_validation() {
339 $rrule = "BYMONTH=1,12,13";
340 $mang = new rrule_manager($rrule);
341 $this->expectException('moodle_exception');
342 $mang->parse_rrule();
346 * Test parsing of BYMONTH rule.
348 public function test_bymonth_lower_bound_validation() {
349 $rrule = "BYMONTH=0";
350 $mang = new rrule_manager($rrule);
351 $this->expectException('moodle_exception');
352 $mang->parse_rrule();
356 * Test parsing of BYSETPOS rule.
358 public function test_bysetpos_without_other_byrules() {
359 $rrule = "BYSETPOS=1,366";
360 $mang = new rrule_manager($rrule);
361 $this->expectException('moodle_exception');
362 $mang->parse_rrule();
366 * Test parsing of BYSETPOS rule.
368 public function test_bysetpos_upper_bound_validation() {
369 $rrule = "BYSETPOS=1,366,367";
370 $mang = new rrule_manager($rrule);
371 $this->expectException('moodle_exception');
372 $mang->parse_rrule();
376 * Test parsing of BYSETPOS rule.
378 public function test_bysetpos_0_validation() {
379 $rrule = "BYSETPOS=0";
380 $mang = new rrule_manager($rrule);
381 $this->expectException('moodle_exception');
382 $mang->parse_rrule();
386 * Test parsing of BYSETPOS rule.
388 public function test_bysetpos_lower_bound_validation() {
389 $rrule = "BYSETPOS=-1,-366,-367";
390 $mang = new rrule_manager($rrule);
391 $this->expectException('moodle_exception');
392 $mang->parse_rrule();
396 * Test recurrence rules for daily frequency.
398 public function test_daily_events() {
401 $rrule = 'FREQ=DAILY;COUNT=3'; // This should generate 2 child events + 1 parent.
402 $mang = new rrule_manager($rrule);
403 $mang->parse_rrule();
404 $mang->create_events($this->event
);
405 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
406 $this->assertEquals(3, $count);
407 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
408 'timestart' => ($this->event
->timestart + DAYSECS
)));
409 $this->assertTrue($result);
410 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
411 'timestart' => ($this->event
->timestart +
2 * DAYSECS
)));
412 $this->assertTrue($result);
414 $until = $this->event
->timestart + DAYSECS
* 2;
415 $until = date('Y-m-d', $until);
416 $rrule = "FREQ=DAILY;UNTIL=$until"; // This should generate 1 child event + 1 parent,since by then until bound would be hit.
417 $mang = new rrule_manager($rrule);
418 $mang->parse_rrule();
419 $mang->create_events($this->event
);
420 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
421 $this->assertEquals(2, $count);
422 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
423 'timestart' => ($this->event
->timestart + DAYSECS
)));
424 $this->assertTrue($result);
426 $rrule = 'FREQ=DAILY;COUNT=3;INTERVAL=3'; // This should generate 2 child events + 1 parent, every 3rd day.
427 $mang = new rrule_manager($rrule);
428 $mang->parse_rrule();
429 $mang->create_events($this->event
);
430 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
431 $this->assertEquals(3, $count);
432 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
433 'timestart' => ($this->event
->timestart +
3 * DAYSECS
)));
434 $this->assertTrue($result);
435 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
436 'timestart' => ($this->event
->timestart +
6 * DAYSECS
)));
437 $this->assertTrue($result);
441 * Every 300 days, forever.
443 public function test_every_300_days_forever() {
446 // Change the start date for forever events to 9am of the current date.
447 $this->change_event_startdate(date('Ymd\T090000'));
448 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
450 $interval = new \
DateInterval('P300D');
451 $untildate = new \
DateTime();
452 $untildate->add(new \
DateInterval('P10Y'));
453 $until = $untildate->getTimestamp();
455 // Forever event. This should generate events for time() + 10 year period, every 300 days.
456 $rrule = 'FREQ=DAILY;INTERVAL=300';
457 $mang = new rrule_manager($rrule);
458 $mang->parse_rrule();
459 $mang->create_events($this->event
);
460 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
461 $records = $DB->get_records('event', array('repeatid' => $this->event
->id
), 'timestart ASC', 0, 100);
463 $expecteddate = clone($startdatetime);
465 foreach ($records as $record) {
466 $this->assertLessThanOrEqual($until, $record->timestart
);
467 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
468 // Go to next iteration.
469 $expecteddate->add($interval);
472 // The first instance of the event contains the UUID.
473 $this->assertEquals('uuid', $record->uuid
);
476 // Succeeding instances will not contain the UUID.
477 $this->assertEmpty($record->uuid
);
483 * Test recurrence rules for weekly frequency.
485 public function test_weekly_events() {
488 $rrule = 'FREQ=WEEKLY;COUNT=1';
489 $mang = new rrule_manager($rrule);
490 $mang->parse_rrule();
491 $mang->create_events($this->event
);
492 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
493 $this->assertEquals(1, $count);
494 for ($i = 0; $i < $count; $i++
) {
495 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
496 'timestart' => ($this->event
->timestart +
$i * DAYSECS
)));
497 $this->assertTrue($result);
499 // This much seconds after the start of the day.
500 $offset = $this->event
->timestart
- mktime(0, 0, 0, date("n", $this->event
->timestart
), date("j", $this->event
->timestart
),
501 date("Y", $this->event
->timestart
));
503 // This should generate 4 weekly Monday events.
504 $until = $this->event
->timestart + WEEKSECS
* 4;
505 $until = date('Ymd\This\Z', $until);
506 $rrule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=$until";
507 $mang = new rrule_manager($rrule);
508 $mang->parse_rrule();
509 $mang->create_events($this->event
);
510 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
511 $this->assertEquals(4, $count);
512 $timestart = $this->event
->timestart
;
513 for ($i = 0; $i < $count; $i++
) {
514 $timestart = strtotime("+$offset seconds next Monday", $timestart);
515 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
, 'timestart' => $timestart));
516 $this->assertTrue($result);
519 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
520 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
522 $offsetinterval = $startdatetime->diff($startdate, true);
523 $interval = new \
DateInterval('P3W');
525 // Every 3 weeks on Monday, Wednesday for 2 times.
526 $rrule = 'FREQ=WEEKLY;INTERVAL=3;BYDAY=MO,WE;COUNT=2';
527 $mang = new rrule_manager($rrule);
528 $mang->parse_rrule();
529 $mang->create_events($this->event
);
531 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
532 $this->assertCount(2, $records);
534 $expecteddate = clone($startdate);
535 $expecteddate->modify('1997-09-03');
536 foreach ($records as $record) {
537 $expecteddate->add($offsetinterval);
538 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
540 if (date('D', $record->timestart
) === 'Mon') {
541 // Go to the fifth day of this month.
542 $expecteddate->modify('next Wednesday');
545 $expecteddate->modify('last Monday');
546 // Go to next period.
547 $expecteddate->add($interval);
553 * Test recurrence rules for weekly frequency for RRULE with BYDAY rule set, recurring forever.
555 public function test_weekly_byday_forever() {
558 // Set the next Monday as the starting date of this event.
559 $startdate = new \
DateTime('next Monday');
560 // Change the start date of the parent event.
561 $startdate = $this->change_event_startdate($startdate->format('Ymd\T090000'));
563 // Forever event. This should generate events over time() + 10 year period, every 50 weeks.
564 $rrule = 'FREQ=WEEKLY;BYDAY=MO;INTERVAL=50';
566 $mang = new rrule_manager($rrule);
567 $mang->parse_rrule();
568 $mang->create_events($this->event
);
570 $untildate = new \
DateTime();
571 $untildate->add(new \
DateInterval('P10Y'));
572 $until = $untildate->getTimestamp();
574 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
576 $interval = new \
DateInterval('P50W');
578 // First instance of this set of recurring events.
579 $expecteddate = clone($startdate);
581 // Iterate over each record and increment the expected date accordingly.
582 foreach ($records as $record) {
583 $eventdateexpected = $expecteddate->format('Y-m-d H:i:s');
584 $eventdateactual = date('Y-m-d H:i:s', $record->timestart
);
585 $this->assertEquals($eventdateexpected, $eventdateactual);
587 $expecteddate->add($interval);
588 $this->assertLessThanOrEqual($until, $record->timestart
);
593 * Test recurrence rules for monthly frequency for RRULE with COUNT and BYMONTHDAY rules set.
595 public function test_monthly_events_with_count_bymonthday() {
598 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
599 $interval = new \
DateInterval('P1M');
601 $rrule = "FREQ=MONTHLY;COUNT=3;BYMONTHDAY=2"; // This should generate 3 events in total.
602 $mang = new rrule_manager($rrule);
603 $mang->parse_rrule();
604 $mang->create_events($this->event
);
605 $records = $DB->get_records('event', array('repeatid' => $this->event
->id
), 'timestart ASC');
606 $this->assertCount(3, $records);
608 $expecteddate = clone($startdatetime);
609 foreach ($records as $record) {
610 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
612 $expecteddate->add($interval);
617 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
619 public function test_monthly_events_with_until_bymonthday() {
622 // This should generate 10 child event + 1 parent, since by then until bound would be hit.
623 $until = strtotime('+1 day +10 months', $this->event
->timestart
);
624 $until = date('Ymd\This\Z', $until);
625 $rrule = "FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=$until";
626 $mang = new rrule_manager($rrule);
627 $mang->parse_rrule();
628 $mang->create_events($this->event
);
629 $count = $DB->count_records('event', ['repeatid' => $this->event
->id
]);
630 $this->assertEquals(11, $count);
631 for ($i = 0; $i < 11; $i++
) {
632 $time = strtotime("+$i month", $this->event
->timestart
);
633 $result = $DB->record_exists('event', ['repeatid' => $this->event
->id
, 'timestart' => $time]);
634 $this->assertTrue($result);
639 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
641 public function test_monthly_events_with_until_bymonthday_multi() {
644 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
645 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
646 $offsetinterval = $startdatetime->diff($startdate, true);
647 $interval = new \
DateInterval('P2M');
648 $untildate = clone($startdatetime);
649 $untildate->add(new \
DateInterval('P10M10D'));
650 $until = $untildate->format('Ymd\This\Z');
652 // This should generate 11 child event + 1 parent, since by then until bound would be hit.
653 $rrule = "FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=2,5;UNTIL=$until";
655 $mang = new rrule_manager($rrule);
656 $mang->parse_rrule();
657 $mang->create_events($this->event
);
659 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
660 $this->assertCount(12, $records);
662 $expecteddate = clone($startdate);
663 $expecteddate->add($offsetinterval);
664 foreach ($records as $record) {
665 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
667 if (date('j', $record->timestart
) == 2) {
668 // Go to the fifth day of this month.
669 $expecteddate->add(new \
DateInterval('P3D'));
671 // Reset date to the first day of the month.
672 $expecteddate->modify('first day of this month');
673 // Go to next month period.
674 $expecteddate->add($interval);
675 // Go to the second day of the next month period.
676 $expecteddate->modify('+1 day');
682 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY forever.
684 public function test_monthly_events_with_bymonthday_forever() {
687 // Change the start date for forever events to 9am of the 2nd day of the current month and year.
688 $this->change_event_startdate(date('Ym02\T090000'));
689 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
690 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
692 $offsetinterval = $startdatetime->diff($startdate, true);
693 $interval = new \
DateInterval('P12M');
695 // Forever event. This should generate events over a 10-year period, on 2nd day of the month, every 12 months.
696 $rrule = "FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2";
698 $mang = new rrule_manager($rrule);
699 $untildate = new \
DateTime();
700 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
701 $until = $untildate->getTimestamp();
703 $mang->parse_rrule();
704 $mang->create_events($this->event
);
706 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
708 $expecteddate = clone($startdate);
709 $expecteddate->add($offsetinterval);
710 foreach ($records as $record) {
711 $this->assertLessThanOrEqual($until, $record->timestart
);
713 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
715 // Reset date to the first day of the month.
716 $expecteddate->modify('first day of this month');
717 // Go to next month period.
718 $expecteddate->add($interval);
719 // Go to the second day of the next month period.
720 $expecteddate->modify('+1 day');
725 * Test recurrence rules for monthly frequency for RRULE with COUNT and BYDAY rules set.
727 public function test_monthly_events_with_count_byday() {
730 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
731 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
733 $offsetinterval = $startdatetime->diff($startdate, true);
734 $interval = new \
DateInterval('P1M');
736 $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=1MO'; // This should generate 3 events in total, first monday of the month.
737 $mang = new rrule_manager($rrule);
738 $mang->parse_rrule();
739 $mang->create_events($this->event
);
741 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
743 // First occurrence of this set of recurring events: 06-10-1997.
744 $expecteddate = clone($startdate);
745 $expecteddate->modify('1997-10-06');
746 $expecteddate->add($offsetinterval);
747 foreach ($records as $record) {
748 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
750 // Go to next month period.
751 $expecteddate->add($interval);
752 $expecteddate->modify('first Monday of this month');
753 $expecteddate->add($offsetinterval);
758 * Test recurrence rules for monthly frequency for RRULE with BYDAY and UNTIL rules set.
760 public function test_monthly_events_with_until_byday() {
763 // This much seconds after the start of the day.
764 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
765 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
766 $offsetinterval = $startdatetime->diff($startdate, true);
768 $untildate = clone($startdatetime);
769 $untildate->add(new \
DateInterval('P10M1D'));
770 $until = $untildate->format('Ymd\This\Z');
772 // This rule should generate 9 events in total from first Monday of October 1997 to first Monday of June 1998.
773 $rrule = "FREQ=MONTHLY;BYDAY=1MO;UNTIL=$until";
774 $mang = new rrule_manager($rrule);
775 $mang->parse_rrule();
776 $mang->create_events($this->event
);
778 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
779 $this->assertCount(9, $records);
781 $expecteddate = clone($startdate);
782 $expecteddate->modify('first Monday of October 1997');
783 foreach ($records as $record) {
784 $expecteddate->add($offsetinterval);
786 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
789 $expecteddate->modify('first day of next month');
790 // Go to the first Monday of the next month.
791 $expecteddate->modify('first Monday of this month');
796 * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
798 public function test_monthly_events_with_until_byday_multi() {
801 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
802 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
804 $offsetinterval = $startdatetime->diff($startdate, true);
805 $interval = new \
DateInterval('P2M');
807 $untildate = clone($startdatetime);
808 $untildate->add(new \
DateInterval('P10M20D'));
809 $until = $untildate->format('Ymd\This\Z');
811 // This should generate 11 events from 17 Sep 1997 to 15 Jul 1998.
812 $rrule = "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,3WE;UNTIL=$until";
813 $mang = new rrule_manager($rrule);
814 $mang->parse_rrule();
815 $mang->create_events($this->event
);
817 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
818 $this->assertCount(11, $records);
820 $expecteddate = clone($startdate);
821 $expecteddate->modify('1997-09-17');
822 foreach ($records as $record) {
823 $expecteddate->add($offsetinterval);
824 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
826 if (date('D', $record->timestart
) === 'Mon') {
827 // Go to the fifth day of this month.
828 $expecteddate->modify('third Wednesday of this month');
830 // Go to next month period.
831 $expecteddate->add($interval);
832 $expecteddate->modify('first Monday of this month');
838 * Test recurrence rules for monthly frequency for RRULE with BYDAY forever.
840 public function test_monthly_events_with_byday_forever() {
843 // Change the start date for forever events to 9am of the 2nd day of the current month and year.
844 $this->change_event_startdate(date('Ym02\T090000'));
846 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
847 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
849 $offsetinterval = $startdatetime->diff($startdate, true);
850 $interval = new \
DateInterval('P12M');
852 // Forever event. This should generate events over a 10 year period, on 1st Monday of the month every 12 months.
853 $rrule = "FREQ=MONTHLY;INTERVAL=12;BYDAY=1MO";
855 $mang = new rrule_manager($rrule);
856 $untildate = new \
DateTime();
857 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
858 $until = $untildate->getTimestamp();
860 $mang->parse_rrule();
861 $mang->create_events($this->event
);
863 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
864 $expecteddate = new \
DateTime('first Monday of this month');
865 // Move to the next interval's first Monday if the calculated start date is after this month's first Monday.
866 if ($expecteddate->getTimestamp() < $startdate->getTimestamp()) {
867 $expecteddate->add($interval);
868 $expecteddate->modify('first Monday of this month');
870 foreach ($records as $record) {
871 $expecteddate->add($offsetinterval);
872 $this->assertLessThanOrEqual($until, $record->timestart
);
874 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
876 // Go to next month period.
877 $expecteddate->add($interval);
878 // Reset date to the first Monday of the month.
879 $expecteddate->modify('first Monday of this month');
884 * Test recurrence rules for yearly frequency.
886 public function test_yearly_events() {
889 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
890 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
892 $offsetinterval = $startdatetime->diff($startdate, true);
893 $interval = new \
DateInterval('P1Y');
895 $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9"; // This should generate 3 events in total.
896 $mang = new rrule_manager($rrule);
897 $mang->parse_rrule();
898 $mang->create_events($this->event
);
900 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
901 $this->assertCount(3, $records);
903 $expecteddate = clone($startdatetime);
904 foreach ($records as $record) {
905 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
907 // Go to next period.
908 $expecteddate->add($interval);
911 // Create a yearly event, until the time limit is hit.
912 $until = strtotime('+20 day +10 years', $this->event
->timestart
);
913 $until = date('Ymd\THis\Z', $until);
914 $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until";
915 $mang = new rrule_manager($rrule);
916 $mang->parse_rrule();
917 $mang->create_events($this->event
);
918 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
919 $this->assertEquals(11, $count);
920 for ($i = 0, $time = $this->event
->timestart
; $time < $until; $i++
, $yoffset = $i * 2,
921 $time = strtotime("+$yoffset years", $this->event
->timestart
)) {
922 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
923 'timestart' => ($time)));
924 $this->assertTrue($result);
927 // This should generate 5 events in total, every second year in the given month of the event.
928 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5";
929 $mang = new rrule_manager($rrule);
930 $mang->parse_rrule();
931 $mang->create_events($this->event
);
932 $count = $DB->count_records('event', array('repeatid' => $this->event
->id
));
933 $this->assertEquals(5, $count);
934 for ($i = 0, $time = $this->event
->timestart
; $i < 5; $i++
, $yoffset = $i * 2,
935 $time = strtotime("+$yoffset years", $this->event
->timestart
)) {
936 $result = $DB->record_exists('event', array('repeatid' => $this->event
->id
,
937 'timestart' => ($time)));
938 $this->assertTrue($result);
941 $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9;BYDAY=1MO"; // This should generate 3 events in total.
942 $mang = new rrule_manager($rrule);
943 $mang->parse_rrule();
944 $mang->create_events($this->event
);
946 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
947 $this->assertCount(3, $records);
949 $expecteddate = clone($startdatetime);
950 $expecteddate->modify('first Monday of September 1998');
951 $expecteddate->add($offsetinterval);
952 foreach ($records as $record) {
953 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
955 // Go to next period.
956 $expecteddate->add($interval);
957 $monthyear = $expecteddate->format('F Y');
958 $expecteddate->modify('first Monday of ' . $monthyear);
959 $expecteddate->add($offsetinterval);
962 // Create a yearly event on the specified month, until the time limit is hit.
963 $untildate = clone($startdatetime);
964 $untildate->add(new \
DateInterval('P10Y20D'));
965 $until = $untildate->format('Ymd\THis\Z');
967 $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until;BYDAY=1MO";
968 $mang = new rrule_manager($rrule);
969 $mang->parse_rrule();
970 $mang->create_events($this->event
);
972 // 10 yearly records every first Monday of September 1998 to first Monday of September 2007.
973 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
974 $this->assertCount(10, $records);
976 $expecteddate = clone($startdatetime);
977 $expecteddate->modify('first Monday of September 1998');
978 $expecteddate->add($offsetinterval);
979 foreach ($records as $record) {
980 $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart
);
981 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
983 // Go to next period.
984 $expecteddate->add($interval);
985 $monthyear = $expecteddate->format('F Y');
986 $expecteddate->modify('first Monday of ' . $monthyear);
987 $expecteddate->add($offsetinterval);
990 // This should generate 5 events in total, every second year in the month of September.
991 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5;BYDAY=1MO";
992 $mang = new rrule_manager($rrule);
993 $mang->parse_rrule();
994 $mang->create_events($this->event
);
996 // 5 bi-yearly records every first Monday of September 1998 to first Monday of September 2007.
997 $interval = new \
DateInterval('P2Y');
998 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
999 $this->assertCount(5, $records);
1001 $expecteddate = clone($startdatetime);
1002 $expecteddate->modify('first Monday of September 1999');
1003 $expecteddate->add($offsetinterval);
1004 foreach ($records as $record) {
1005 $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart
);
1006 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1008 // Go to next period.
1009 $expecteddate->add($interval);
1010 $monthyear = $expecteddate->format('F Y');
1011 $expecteddate->modify('first Monday of ' . $monthyear);
1012 $expecteddate->add($offsetinterval);
1017 * Test for rrule with FREQ=YEARLY and INTERVAL=2 with BYMONTH rule set, recurring forever.
1019 public function test_yearly_september_every_two_years_forever() {
1022 // Change the start date for forever events to 9am on the 2nd day of September of the current year.
1023 $this->change_event_startdate(date('Y0902\T090000'));
1025 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2"; // Forever event.
1026 $mang = new rrule_manager($rrule);
1027 $untildate = new \
DateTime();
1028 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1029 $untiltimestamp = $untildate->getTimestamp();
1030 $mang->parse_rrule();
1031 $mang->create_events($this->event
);
1033 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1035 $interval = new \
DateInterval('P2Y');
1036 $expecteddate = new \
DateTime(date('Y0902\T090000'));
1037 foreach ($records as $record) {
1038 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1039 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1041 // Go to the next expected date.
1042 $expecteddate->add($interval);
1047 * Test for rrule with FREQ=YEARLY with BYMONTH and BYDAY rules set, recurring forever.
1049 public function test_yearly_bymonth_byday_forever() {
1052 // Change the start date for forever events to the first day of September of the current year at 9am.
1053 $this->change_event_startdate(date('Y0901\T090000'));
1055 // Every 2 years on the first Monday of September.
1056 $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;BYDAY=1MO";
1057 $mang = new rrule_manager($rrule);
1058 $mang->parse_rrule();
1059 $mang->create_events($this->event
);
1061 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1063 $untildate = new \
DateTime();
1064 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1065 $untiltimestamp = $untildate->getTimestamp();
1067 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1068 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
1070 $offsetinterval = $startdatetime->diff($startdate, true);
1071 $interval = new \
DateInterval('P2Y');
1073 // First occurrence of this set of events is on the first Monday of September.
1074 $expecteddate = clone($startdatetime);
1075 $expecteddate->modify('first Monday of September');
1076 $expecteddate->add($offsetinterval);
1077 foreach ($records as $record) {
1078 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1079 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1081 // Go to next period.
1082 $expecteddate->add($interval);
1083 $monthyear = $expecteddate->format('F Y');
1084 $expecteddate->modify('first Monday of ' . $monthyear);
1085 $expecteddate->add($offsetinterval);
1090 * Test for rrule with FREQ=YEARLY recurring forever.
1092 public function test_yearly_forever() {
1095 // Change the start date for forever events to 9am of the current date.
1096 $this->change_event_startdate(date('Ymd\T090000'));
1098 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1100 $interval = new \
DateInterval('P2Y');
1102 $rrule = 'FREQ=YEARLY;INTERVAL=2'; // Forever event.
1103 $mang = new rrule_manager($rrule);
1104 $mang->parse_rrule();
1105 $mang->create_events($this->event
);
1107 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1109 $untildate = new \
DateTime();
1110 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1111 $untiltimestamp = $untildate->getTimestamp();
1113 $expecteddate = clone($startdatetime);
1114 foreach ($records as $record) {
1115 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1116 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1118 // Go to next period.
1119 $expecteddate->add($interval);
1123 /******************************************************************************************************************************/
1124 /* Tests based on the examples from the RFC. */
1125 /******************************************************************************************************************************/
1128 * Daily for 10 occurrences:
1130 * DTSTART;TZID=US-Eastern:19970902T090000
1131 * RRULE:FREQ=DAILY;COUNT=10
1132 * ==> (1997 9:00 AM EDT)September 2-11
1134 public function test_daily_count() {
1137 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1138 $interval = new \
DateInterval('P1D');
1140 $rrule = 'FREQ=DAILY;COUNT=10';
1141 $mang = new rrule_manager($rrule);
1142 $mang->parse_rrule();
1143 $mang->create_events($this->event
);
1145 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1146 $this->assertCount(10, $records);
1148 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1149 foreach ($records as $record) {
1150 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1152 // Go to next period.
1153 $expecteddate->add($interval);
1158 * Daily until December 24, 1997:
1160 * DTSTART;TZID=US-Eastern:19970902T090000
1161 * RRULE:FREQ=DAILY;UNTIL=19971224T000000Z
1162 * ==> (1997 9:00 AM EDT)September 2-30;October 1-25
1163 * (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23
1165 public function test_daily_until() {
1168 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1169 $interval = new \
DateInterval('P1D');
1171 $untildate = new \
DateTime('19971224T000000Z');
1172 $untiltimestamp = $untildate->getTimestamp();
1174 $rrule = 'FREQ=DAILY;UNTIL=19971224T000000Z';
1175 $mang = new rrule_manager($rrule);
1176 $mang->parse_rrule();
1177 $mang->create_events($this->event
);
1179 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1180 // 113 daily events from 02-09-1997 to 23-12-1997.
1181 $this->assertCount(113, $records);
1183 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1184 foreach ($records as $record) {
1185 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1186 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1187 // Go to next period.
1188 $expecteddate->add($interval);
1193 * Every other day - forever:
1195 * DTSTART;TZID=US-Eastern:[Current date]T090000
1196 * RRULE:FREQ=DAILY;INTERVAL=2
1198 * Sample results (e.g. in the year 1997):
1199 * (1997 9:00 AM EDT)September2,4,6,8...24,26,28,30;October 2,4,6...20,22,24
1200 * (1997 9:00 AM EST)October 26,28,30;November 1,3,5,7...25,27,29;Dec 1,3,...
1202 public function test_every_other_day_forever() {
1205 // Change the start date for forever events to 9am of the current date in US/Eastern time.
1206 $this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
1208 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1209 $interval = new \
DateInterval('P2D');
1211 $rrule = 'FREQ=DAILY;INTERVAL=2';
1212 $mang = new rrule_manager($rrule);
1213 $mang->parse_rrule();
1214 $mang->create_events($this->event
);
1216 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1217 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1219 $untildate = new \
DateTime();
1220 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1221 $untiltimestamp = $untildate->getTimestamp();
1223 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1224 foreach ($records as $record) {
1225 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1227 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1228 // Go to next period.
1229 $expecteddate->add($interval);
1234 * Every 10 days, 5 occurrences:
1236 * DTSTART;TZID=US-Eastern:19970902T090000
1237 * RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5
1238 * ==> (1997 9:00 AM EDT)September 2,12,22;October 2,12
1240 public function test_every_10_days_5_count() {
1243 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1244 $interval = new \
DateInterval('P10D');
1246 $rrule = 'FREQ=DAILY;INTERVAL=10;COUNT=5';
1247 $mang = new rrule_manager($rrule);
1248 $mang->parse_rrule();
1249 $mang->create_events($this->event
);
1251 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1252 $this->assertCount(5, $records);
1254 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
1255 foreach ($records as $record) {
1256 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1257 // Go to next period.
1258 $expecteddate->add($interval);
1263 * Everyday in January, for 3 years:
1265 * DTSTART;TZID=US-Eastern:19980101T090000
1266 * RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
1267 * ==> (1998 9:00 AM EDT)January 1-31
1268 * (1999 9:00 AM EDT)January 1-31
1269 * (2000 9:00 AM EDT)January 1-31
1271 public function test_everyday_in_jan_for_3_years_yearly() {
1274 // Change our event's date to 01-01-1998, based on the example from the RFC.
1275 $this->change_event_startdate('19980101T090000', 'US/Eastern');
1277 $rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
1278 $mang = new rrule_manager($rrule);
1279 $mang->parse_rrule();
1280 $mang->create_events($this->event
);
1282 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1283 // 92 events from 01-01-1998 to 03-01-2000.
1284 $this->assertCount(92, $records);
1286 $untildate = new \
DateTime('20000131T090000Z');
1287 $untiltimestamp = $untildate->getTimestamp();
1288 foreach ($records as $record) {
1289 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1291 // Assert that the event's date is in January.
1292 $this->assertEquals('January', date('F', $record->timestart
));
1297 * Everyday in January, for 3 years:
1299 * DTSTART;TZID=US-Eastern:19980101T090000
1300 * RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1
1301 * ==> (1998 9:00 AM EDT)January 1-31
1302 * (1999 9:00 AM EDT)January 1-31
1303 * (2000 9:00 AM EDT)January 1-31
1305 public function test_everyday_in_jan_for_3_years_daily() {
1308 // Change our event's date to 01-01-1998, based on the example from the RFC.
1309 $this->change_event_startdate('19980101T090000', 'US/Eastern');
1311 $rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
1312 $mang = new rrule_manager($rrule);
1313 $mang->parse_rrule();
1314 $mang->create_events($this->event
);
1316 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1317 // 92 events from 01-01-1998 to 03-01-2000.
1318 $this->assertCount(92, $records);
1320 $untildate = new \
DateTime('20000131T090000Z');
1321 $untiltimestamp = $untildate->getTimestamp();
1322 foreach ($records as $record) {
1323 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1325 // Assert that the event's date is in January.
1326 $this->assertEquals('January', date('F', $record->timestart
));
1331 * Weekly for 10 occurrences
1333 * DTSTART;TZID=US-Eastern:19970902T090000
1334 * RRULE:FREQ=WEEKLY;COUNT=10
1335 * ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21
1336 * (1997 9:00 AM EST)October 28;November 4
1338 public function test_weekly_10_count() {
1341 $interval = new \
DateInterval('P1W');
1343 $rrule = 'FREQ=WEEKLY;COUNT=10';
1344 $mang = new rrule_manager($rrule);
1345 $mang->parse_rrule();
1346 $mang->create_events($this->event
);
1348 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1349 $this->assertCount(10, $records);
1351 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1352 foreach ($records as $record) {
1353 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1354 // Go to next period.
1355 $expecteddate->add($interval);
1360 * Weekly until December 24, 1997.
1362 * DTSTART;TZID=US-Eastern:19970902T090000
1363 * RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z
1364 * ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21,28
1365 * (1997 9:00 AM EST)November 4,11,18,25;December 2,9,16,23
1367 public function test_weekly_until_24_dec_1997() {
1370 $interval = new \
DateInterval('P1W');
1372 $rrule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z';
1373 $mang = new rrule_manager($rrule);
1374 $mang->parse_rrule();
1375 $mang->create_events($this->event
);
1377 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1378 // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1379 $this->assertCount(17, $records);
1381 $untildate = new \
DateTime('19971224T000000Z');
1382 $untiltimestamp = $untildate->getTimestamp();
1383 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1384 foreach ($records as $record) {
1385 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1386 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1387 // Go to next period.
1388 $expecteddate->add($interval);
1393 * Every other week - forever:
1395 * DTSTART;TZID=US-Eastern:[Current date]T090000
1396 * RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU
1398 * Sample results (e.g. in the year 1997):
1399 * (1997 9:00 AM EDT)September 2,16,30;October 14
1400 * (1997 9:00 AM EST)October 28;November 11,25;December 9,23
1401 * (1998 9:00 AM EST)January 6,20;February
1404 public function test_every_other_week_forever() {
1407 // Change the start date for forever events to 9am of the current date in US/Eastern time.
1408 $this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
1410 $interval = new \
DateInterval('P2W');
1412 $rrule = 'FREQ=WEEKLY;INTERVAL=2;WKST=SU';
1413 $mang = new rrule_manager($rrule);
1414 $mang->parse_rrule();
1415 $mang->create_events($this->event
);
1417 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1418 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1420 $untildate = new \
DateTime();
1421 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1422 $untiltimestamp = $untildate->getTimestamp();
1424 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1425 foreach ($records as $record) {
1426 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1428 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1429 // Go to next period.
1430 $expecteddate->add($interval);
1435 * Weekly on Tuesday and Thursday for 5 weeks:
1437 * DTSTART;TZID=US-Eastern:19970902T090000
1438 * RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH
1439 * ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
1441 public function test_weekly_on_tue_thu_for_5_weeks_by_until() {
1444 $rrule = 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH';
1445 $mang = new rrule_manager($rrule);
1446 $mang->parse_rrule();
1447 $mang->create_events($this->event
);
1449 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1450 // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1451 $this->assertCount(10, $records);
1453 $untildate = new \
DateTime('19971007T000000Z');
1454 $untiltimestamp = $untildate->getTimestamp();
1455 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1456 $startdate = new \
DateTime($expecteddate->format('Y-m-d'));
1457 $offset = $expecteddate->diff($startdate, true);
1458 foreach ($records as $record) {
1459 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1461 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1462 // Go to next period.
1463 if ($expecteddate->format('l') === rrule_manager
::DAY_TUESDAY
) {
1464 $expecteddate->modify('next Thursday');
1466 $expecteddate->modify('next Tuesday');
1468 $expecteddate->add($offset);
1473 * Weekly on Tuesday and Thursday for 5 weeks:
1475 * DTSTART;TZID=US-Eastern:19970902T090000
1476 * RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH
1477 * ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
1479 public function test_weekly_on_tue_thu_for_5_weeks_by_count() {
1482 $rrule = 'FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH';
1483 $mang = new rrule_manager($rrule);
1484 $mang->parse_rrule();
1485 $mang->create_events($this->event
);
1487 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1488 // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
1489 $this->assertCount(10, $records);
1491 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1492 $startdate = new \
DateTime($expecteddate->format('Y-m-d'));
1493 $offset = $expecteddate->diff($startdate, true);
1494 foreach ($records as $record) {
1495 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1496 // Go to next period.
1497 if ($expecteddate->format('l') === rrule_manager
::DAY_TUESDAY
) {
1498 $expecteddate->modify('next Thursday');
1500 $expecteddate->modify('next Tuesday');
1502 $expecteddate->add($offset);
1507 * Every other week on Monday, Wednesday and Friday until December 24, 1997, but starting on Tuesday, September 2, 1997:
1509 * DTSTART;TZID=US-Eastern:19970902T090000
1510 * RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR
1511 * ==> (1997 9:00 AM EDT)September 3,5,15,17,19,29;October 1,3,13,15,17
1512 * (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;December 8,10,12,22
1514 public function test_every_other_week_until_24_dec_1997_byday() {
1517 $rrule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR';
1518 $mang = new rrule_manager($rrule);
1519 $mang->parse_rrule();
1520 $mang->create_events($this->event
);
1522 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1523 // 24 iterations every M-W-F from 03-09-1997 13:00 UTC to 22-12-1997 13:00 UTC.
1524 $this->assertCount(24, $records);
1526 $untildate = new \
DateTime('19971224T000000Z');
1527 $untiltimestamp = $untildate->getTimestamp();
1529 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1530 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
1532 $offsetinterval = $startdatetime->diff($startdate, true);
1534 // First occurrence of this set of events is on 3 September 1999.
1535 $expecteddate = clone($startdatetime);
1536 $expecteddate->modify('next Wednesday');
1537 $expecteddate->add($offsetinterval);
1538 foreach ($records as $record) {
1539 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1540 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1542 // Go to next period.
1543 switch ($expecteddate->format('l')) {
1544 case rrule_manager
::DAY_MONDAY
:
1545 $expecteddate->modify('next Wednesday');
1547 case rrule_manager
::DAY_WEDNESDAY
:
1548 $expecteddate->modify('next Friday');
1551 $expecteddate->modify('next Monday');
1552 // Increment expected date by 1 week if the next day is Monday.
1553 $expecteddate->add(new \
DateInterval('P1W'));
1556 $expecteddate->add($offsetinterval);
1561 * Every other week on Tuesday and Thursday, for 8 occurrences:
1563 * DTSTART;TZID=US-Eastern:19970902T090000
1564 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH
1565 * ==> (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16
1567 public function test_every_other_week_byday_8_count() {
1570 $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH';
1571 $mang = new rrule_manager($rrule);
1572 $mang->parse_rrule();
1573 $mang->create_events($this->event
);
1575 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1576 // Should correspond to COUNT rule.
1577 $this->assertCount(8, $records);
1579 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1580 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
1582 $offsetinterval = $startdatetime->diff($startdate, true);
1584 // First occurrence of this set of events is on 2 September 1999.
1585 $expecteddate = clone($startdatetime);
1586 foreach ($records as $record) {
1587 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1589 // Go to next period.
1590 switch ($expecteddate->format('l')) {
1591 case rrule_manager
::DAY_TUESDAY
:
1592 $expecteddate->modify('next Thursday');
1595 $expecteddate->modify('next Tuesday');
1596 // Increment expected date by 1 week if the next day is Tuesday.
1597 $expecteddate->add(new \
DateInterval('P1W'));
1600 $expecteddate->add($offsetinterval);
1605 * Monthly on the 1st Friday for ten occurrences:
1607 * DTSTART;TZID=US-Eastern:19970905T090000
1608 * RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR
1609 * ==> (1997 9:00 AM EDT)September 5;October 3
1610 * (1997 9:00 AM EST)November 7;Dec 5
1611 * (1998 9:00 AM EST)January 2;February 6;March 6;April 3
1612 * (1998 9:00 AM EDT)May 1;June 5
1614 public function test_monthly_every_first_friday_10_count() {
1617 // Change our event's date to 05-09-1997, based on the example from the RFC.
1618 $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
1619 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
1620 $offsetinterval = $startdatetime->diff($startdate, true);
1622 $rrule = 'FREQ=MONTHLY;COUNT=10;BYDAY=1FR';
1623 $mang = new rrule_manager($rrule);
1624 $mang->parse_rrule();
1625 $mang->create_events($this->event
);
1627 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1628 // Should correspond to COUNT rule.
1629 $this->assertCount(10, $records);
1631 foreach ($records as $record) {
1632 // Get the first Friday of the record's month.
1633 $recordmonthyear = date('F Y', $record->timestart
);
1634 $expecteddate = new \
DateTime('first Friday of ' . $recordmonthyear);
1635 // Add the time of the event.
1636 $expecteddate->add($offsetinterval);
1638 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1643 * Monthly on the 1st Friday until December 24, 1997:
1645 * DTSTART;TZID=US-Eastern:19970905T090000
1646 * RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR
1647 * ==> (1997 9:00 AM EDT)September 5;October 3
1648 * (1997 9:00 AM EST)November 7;December 5
1650 public function test_monthly_every_first_friday_until() {
1653 // Change our event's date to 05-09-1997, based on the example from the RFC.
1654 $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
1655 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
1656 $offsetinterval = $startdatetime->diff($startdate, true);
1658 $rrule = 'FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR';
1659 $mang = new rrule_manager($rrule);
1660 $mang->parse_rrule();
1661 $mang->create_events($this->event
);
1663 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1664 // Should have 4 events, every first friday of September 1997 to December 1997.
1665 $this->assertCount(4, $records);
1667 foreach ($records as $record) {
1668 // Get the first Friday of the record's month.
1669 $recordmonthyear = date('F Y', $record->timestart
);
1670 $expecteddate = new \
DateTime('first Friday of ' . $recordmonthyear);
1671 // Add the time of the event.
1672 $expecteddate->add($offsetinterval);
1674 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1679 * Every other month on the 1st and last Sunday of the month for 10 occurrences:
1681 * DTSTART;TZID=US-Eastern:19970907T090000
1682 * RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
1683 * ==> (1997 9:00 AM EDT)September 7,28
1684 * (1997 9:00 AM EST)November 2,30
1685 * (1998 9:00 AM EST)January 4,25;March 1,29
1686 * (1998 9:00 AM EDT)May 3,31
1688 public function test_every_other_month_1st_and_last_sunday_10_count() {
1691 // Change our event's date to 05-09-1997, based on the example from the RFC.
1692 $startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
1693 $startdate = new \
DateTime(date('Y-m-d', $this->event
->timestart
));
1694 $offsetinterval = $startdatetime->diff($startdate, true);
1696 $rrule = 'FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU';
1697 $mang = new rrule_manager($rrule);
1698 $mang->parse_rrule();
1699 $mang->create_events($this->event
);
1701 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1702 // Should have 10 records based on COUNT rule.
1703 $this->assertCount(10, $records);
1705 // First occurrence is 07-09-1997 which is the first Sunday.
1707 foreach ($records as $record) {
1708 // Get date of the month's first/last Sunday.
1709 $recordmonthyear = date('F Y', $record->timestart
);
1710 $expecteddate = new \
DateTime($ordinal . ' Sunday of ' . $recordmonthyear);
1711 $expecteddate->add($offsetinterval);
1713 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1714 if ($ordinal === 'first') {
1723 * Monthly on the second to last Monday of the month for 6 months:
1725 * DTSTART;TZID=US-Eastern:19970922T090000
1726 * RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO
1727 * ==> (1997 9:00 AM EDT)September 22;October 20
1728 * (1997 9:00 AM EST)November 17;December 22
1729 * (1998 9:00 AM EST)January 19;February 16
1731 public function test_monthly_last_monday_for_6_months() {
1734 // Change our event's date to 05-09-1997, based on the example from the RFC.
1735 $startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
1736 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
1737 $offsetinterval = $startdatetime->diff($startdate, true);
1739 $rrule = 'FREQ=MONTHLY;COUNT=6;BYDAY=-2MO';
1740 $mang = new rrule_manager($rrule);
1741 $mang->parse_rrule();
1742 $mang->create_events($this->event
);
1744 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1745 // Should have 6 records based on COUNT rule.
1746 $this->assertCount(6, $records);
1748 foreach ($records as $record) {
1749 // Get date of the month's last Monday.
1750 $recordmonthyear = date('F Y', $record->timestart
);
1751 $expecteddate = new \
DateTime('last Monday of ' . $recordmonthyear);
1752 // Modify to get the second to the last Monday.
1753 $expecteddate->modify('last Monday');
1755 $expecteddate->add($offsetinterval);
1757 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1762 * Monthly on the third to the last day of the month, forever:
1764 * DTSTART;TZID=US-Eastern:[Current year]0928T090000
1765 * RRULE:FREQ=MONTHLY;BYMONTHDAY=-3
1767 * Sample results (e.g. in the year 1997):
1768 * (1997 9:00 AM EDT)September 28
1769 * (1997 9:00 AM EST)October 29;November 28;December 29
1770 * (1998 9:00 AM EST)January 29;February 26
1773 public function test_third_to_the_last_day_of_the_month_forever() {
1776 // Change our event's date to 28 September of the current year, based on the example from the RFC.
1777 $this->change_event_startdate(date('Y0928\T090000'), 'US/Eastern');
1779 $rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
1780 $mang = new rrule_manager($rrule);
1781 $mang->parse_rrule();
1782 $mang->create_events($this->event
);
1784 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1785 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1787 $untildate = new \
DateTime();
1788 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1789 $untiltimestamp = $untildate->getTimestamp();
1791 $subinterval = new \
DateInterval('P2D');
1792 foreach ($records as $record) {
1793 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1795 // Get date of the third to the last day of the month.
1796 $recordmonthyear = date('F Y', $record->timestart
);
1797 $expecteddate = new \
DateTime('last day of ' . $recordmonthyear);
1799 $expecteddate->setTime(9, 0);
1800 // Modify to get the third to the last day of the month.
1801 $expecteddate->sub($subinterval);
1803 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1808 * Monthly on the 2nd and 15th of the month for 10 occurrences:
1810 * DTSTART;TZID=US-Eastern:19970902T090000
1811 * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15
1812 * ==> (1997 9:00 AM EDT)September 2,15;October 2,15
1813 * (1997 9:00 AM EST)November 2,15;December 2,15
1814 * (1998 9:00 AM EST)January 2,15
1816 public function test_every_2nd_and_15th_of_the_month_10_count() {
1819 $startdatetime = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1820 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
1821 $offsetinterval = $startdatetime->diff($startdate, true);
1823 $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15';
1824 $mang = new rrule_manager($rrule);
1825 $mang->parse_rrule();
1826 $mang->create_events($this->event
);
1828 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1829 // Should have 10 records based on COUNT rule.
1830 $this->assertCount(10, $records);
1833 foreach ($records as $record) {
1834 // Get the first Friday of the record's month.
1835 $recordmonthyear = date('Y-m', $record->timestart
);
1837 // Get date of the month's last Monday.
1838 $expecteddate = new \
DateTime("$recordmonthyear-$day");
1840 $expecteddate->add($offsetinterval);
1841 if ($day === '02') {
1847 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1852 * Monthly on the first and last day of the month for 10 occurrences:
1854 * DTSTART;TZID=US-Eastern:19970930T090000
1855 * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1
1856 * ==> (1997 9:00 AM EDT)September 30;October 1
1857 * (1997 9:00 AM EST)October 31;November 1,30;December 1,31
1858 * (1998 9:00 AM EST)January 1,31;February 1
1860 public function test_every_first_and_last_day_of_the_month_10_count() {
1863 $startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
1864 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
1865 $offsetinterval = $startdatetime->diff($startdate, true);
1867 $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1';
1868 $mang = new rrule_manager($rrule);
1869 $mang->parse_rrule();
1870 $mang->create_events($this->event
);
1872 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1873 // Should have 10 records based on COUNT rule.
1874 $this->assertCount(10, $records);
1876 // First occurrence is 30-Sep-1997.
1878 foreach ($records as $record) {
1879 // Get the first Friday of the record's month.
1880 $recordmonthyear = date('F Y', $record->timestart
);
1882 // Get date of the month's last Monday.
1883 $expecteddate = new \
DateTime("$day day of $recordmonthyear");
1885 $expecteddate->add($offsetinterval);
1887 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1889 if ($day === 'first') {
1898 * Every 18 months on the 10th thru 15th of the month for 10 occurrences:
1900 * DTSTART;TZID=US-Eastern:19970910T090000
1901 * RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15
1902 * ==> (1997 9:00 AM EDT)September 10,11,12,13,14,15
1903 * (1999 9:00 AM EST)March 10,11,12,13
1905 public function test_every_18_months_days_10_to_15_10_count() {
1908 $startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
1910 $rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
1911 $mang = new rrule_manager($rrule);
1912 $mang->parse_rrule();
1913 $mang->create_events($this->event
);
1915 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
1916 // Should have 10 records based on COUNT rule.
1917 $this->assertCount(10, $records);
1919 // First occurrence is 10-Sep-1997.
1920 $expecteddate = clone($startdatetime);
1921 $expecteddate->setTimezone(new \
DateTimeZone(get_user_timezone()));
1922 foreach ($records as $record) {
1923 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1925 // Get next expected date.
1926 if ($expecteddate->format('d') == 15) {
1927 // If 15th, increment by 18 months.
1928 $expecteddate->add(new \
DateInterval('P18M'));
1929 // Then go back to the 10th.
1930 $expecteddate->sub(new \
DateInterval('P5D'));
1932 // Otherwise, increment by 1 day.
1933 $expecteddate->add(new \
DateInterval('P1D'));
1939 * Every Tuesday, every other month:
1941 * DTSTART;TZID=US-Eastern:[Next Tuesday]T090000
1942 * RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU
1944 * Sample results (e.g. in the year 1997):
1945 * (1997 9:00 AM EDT)September 2,9,16,23,30
1946 * (1997 9:00 AM EST)November 4,11,18,25
1947 * (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31
1950 public function test_every_tuesday_every_other_month_forever() {
1953 // Change the start date for forever events to 9am of the Tuesday on or before of the current date in US/Eastern time.
1954 $nexttuesday = new \
DateTime('next Tuesday');
1955 $this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'US/Eastern');
1957 $rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
1958 $mang = new rrule_manager($rrule);
1959 $mang->parse_rrule();
1960 $mang->create_events($this->event
);
1962 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
1963 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
1965 $untildate = new \
DateTime();
1966 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
1967 $untiltimestamp = $untildate->getTimestamp();
1969 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $this->event
->timestart
));
1970 $nextmonth = new \
DateTime($expecteddate->format('Y-m-d'));
1971 $offset = $expecteddate->diff($nextmonth, true);
1972 $nextmonth->modify('first day of next month');
1973 foreach ($records as $record) {
1974 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
1976 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
1978 // Get next expected date.
1979 $expecteddate->modify('next Tuesday');
1980 if ($expecteddate->getTimestamp() >= $nextmonth->getTimestamp()) {
1981 // Go to the end of the month.
1982 $expecteddate->modify('last day of this month');
1983 // Find the next Tuesday.
1984 $expecteddate->modify('next Tuesday');
1986 // Increment next month by 2 months.
1987 $nextmonth->add(new \
DateInterval('P2M'));
1989 $expecteddate->add($offset);
1994 * Yearly in June and July for 10 occurrences:
1996 * DTSTART;TZID=US-Eastern:19970610T090000
1997 * RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7
1998 * ==> (1997 9:00 AM EDT)June 10;July 10
1999 * (1998 9:00 AM EDT)June 10;July 10
2000 * (1999 9:00 AM EDT)June 10;July 10
2001 * (2000 9:00 AM EDT)June 10;July 10
2002 * (2001 9:00 AM EDT)June 10;July 10
2003 * Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components are specified, the day is gotten from DTSTART.
2005 public function test_yearly_in_june_july_10_count() {
2008 $startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
2010 $rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
2011 $mang = new rrule_manager($rrule);
2012 $mang->parse_rrule();
2013 $mang->create_events($this->event
);
2015 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2016 // Should have 10 records based on COUNT rule.
2017 $this->assertCount(10, $records);
2019 $expecteddate = $startdatetime;
2020 $expecteddate->setTimezone(new \
DateTimeZone(get_user_timezone()));
2021 $monthinterval = new \
DateInterval('P1M');
2022 $yearinterval = new \
DateInterval('P1Y');
2023 foreach ($records as $record) {
2024 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2026 // Get next expected date.
2027 if ($expecteddate->format('m') == 6) {
2028 // Go to the month of July.
2029 $expecteddate->add($monthinterval);
2031 // Go to the month of June next year.
2032 $expecteddate->sub($monthinterval);
2033 $expecteddate->add($yearinterval);
2039 * Every other year on January, February, and March for 10 occurrences:
2041 * DTSTART;TZID=US-Eastern:19970310T090000
2042 * RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3
2043 * ==> (1997 9:00 AM EST)March 10
2044 * (1999 9:00 AM EST)January 10;February 10;March 10
2045 * (2001 9:00 AM EST)January 10;February 10;March 10
2046 * (2003 9:00 AM EST)January 10;February 10;March 10
2048 public function test_every_other_year_in_june_july_10_count() {
2051 $startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
2053 $rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
2054 $mang = new rrule_manager($rrule);
2055 $mang->parse_rrule();
2056 $mang->create_events($this->event
);
2058 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2059 // Should have 10 records based on COUNT rule.
2060 $this->assertCount(10, $records);
2062 $expecteddate = $startdatetime;
2063 $expecteddate->setTimezone(new \
DateTimeZone(get_user_timezone()));
2064 $monthinterval = new \
DateInterval('P1M');
2065 $yearinterval = new \
DateInterval('P2Y');
2066 foreach ($records as $record) {
2067 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2069 // Get next expected date.
2070 if ($expecteddate->format('m') != 3) {
2071 // Go to the next month.
2072 $expecteddate->add($monthinterval);
2074 // Go to the month of January next year.
2075 $expecteddate->sub($monthinterval);
2076 $expecteddate->sub($monthinterval);
2077 $expecteddate->add($yearinterval);
2083 * Every 3rd year on the 1st, 100th and 200th day for 10 occurrences:
2085 * DTSTART;TZID=US-Eastern:19970101T090000
2086 * RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200
2087 * ==> (1997 9:00 AM EST)January 1
2088 * (1997 9:00 AM EDT)April 10;July 19
2089 * (2000 9:00 AM EST)January 1
2090 * (2000 9:00 AM EDT)April 9;July 18
2091 * (2003 9:00 AM EST)January 1
2092 * (2003 9:00 AM EDT)April 10;July 19
2093 * (2006 9:00 AM EST)January 1
2095 public function test_every_3_years_1st_100th_200th_days_10_count() {
2098 $startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
2100 $rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
2101 $mang = new rrule_manager($rrule);
2102 $mang->parse_rrule();
2103 $mang->create_events($this->event
);
2105 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2106 // Should have 10 records based on COUNT rule.
2107 $this->assertCount(10, $records);
2109 $expecteddate = $startdatetime;
2110 $expecteddate->setTimezone(new \
DateTimeZone(get_user_timezone()));
2111 $hundredthdayinterval = new \
DateInterval('P99D');
2112 $twohundredthdayinterval = new \
DateInterval('P100D');
2113 $yearinterval = new \
DateInterval('P3Y');
2115 foreach ($records as $record) {
2116 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2118 // Get next expected date.
2119 if ($expecteddate->format('z') == 0) { // January 1.
2120 $expecteddate->add($hundredthdayinterval);
2121 } else if ($expecteddate->format('z') == 99) { // 100th day of the year.
2122 $expecteddate->add($twohundredthdayinterval);
2123 } else { // 200th day of the year.
2124 $expecteddate->add($yearinterval);
2125 $expecteddate->modify('January 1');
2131 * Every 20th Monday of the year, forever:
2133 * DTSTART;TZID=US-Eastern:[20th Monday of the current year]T090000
2134 * RRULE:FREQ=YEARLY;BYDAY=20MO
2136 * Sample results (e.g. in the year 1997):
2137 * (1997 9:00 AM EDT)May 19
2138 * (1998 9:00 AM EDT)May 18
2139 * (1999 9:00 AM EDT)May 17
2142 public function test_yearly_every_20th_monday_forever() {
2145 // Change our event's date to the 20th Monday of the current year.
2146 $twentiethmonday = new \
DateTime(date('Y-01-01'));
2147 $twentiethmonday->modify('+20 Monday');
2148 $startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
2150 $interval = new \
DateInterval('P1Y');
2152 $rrule = 'FREQ=YEARLY;BYDAY=20MO';
2153 $mang = new rrule_manager($rrule);
2154 $mang->parse_rrule();
2155 $mang->create_events($this->event
);
2157 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2159 $untildate = new \
DateTime();
2160 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2161 $untiltimestamp = $untildate->getTimestamp();
2163 $expecteddate = $startdatetime;
2164 $expecteddate->setTimezone(new \
DateTimeZone(get_user_timezone()));
2165 foreach ($records as $record) {
2166 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2167 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2169 // Go to next period.
2170 $expecteddate->modify('January 1');
2171 $expecteddate->add($interval);
2172 $expecteddate->modify("+20 Monday");
2177 * Monday of week number 20 (where the default start of the week is Monday), forever:
2179 * DTSTART;TZID=US-Eastern:[1st day of the 20th week this year]T090000
2180 * RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO
2182 * Sample results (e.g. in the year 1997):
2183 * (1997 9:00 AM EDT)May 12
2184 * (1998 9:00 AM EDT)May 11
2185 * (1999 9:00 AM EDT)May 17
2188 public function test_yearly_byweekno_forever() {
2191 // Change our event's date to the start of the 20th week of the current year.
2192 $twentiethweek = new \
DateTime(date('Y-01-01'));
2193 $twentiethweek->setISODate($twentiethweek->format('Y'), 20);
2194 $startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'US/Eastern');
2196 $startdate = clone($startdatetime);
2197 $startdate->modify($startdate->format('Y-m-d'));
2199 $offset = $startdatetime->diff($startdate, true);
2201 $interval = new \
DateInterval('P1Y');
2203 $rrule = 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO';
2204 $mang = new rrule_manager($rrule);
2205 $mang->parse_rrule();
2206 $mang->create_events($this->event
);
2208 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2210 $untildate = new \
DateTime();
2211 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2212 $untiltimestamp = $untildate->getTimestamp();
2214 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
2215 foreach ($records as $record) {
2216 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2217 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2219 // Go to next period.
2220 $expecteddate->add($interval);
2221 $expecteddate->setISODate($expecteddate->format('Y'), 20);
2222 $expecteddate->add($offset);
2227 * Every Thursday in March, forever:
2229 * DTSTART;TZID=US-Eastern:[First thursday of March of the current year]T090000
2230 * RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH
2232 * Sample results (e.g. in the year 1997):
2233 * (1997 9:00 AM EST)March 13,20,27
2234 * (1998 9:00 AM EST)March 5,12,19,26
2235 * (1999 9:00 AM EST)March 4,11,18,25
2238 public function test_every_thursday_in_march_forever() {
2241 // Change our event's date to the first Thursday of March of the current year at 9am US/Eastern time.
2242 $firstthursdayofmarch = new \
DateTime('first Thursday of March');
2243 $startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'US/Eastern');
2245 $interval = new \
DateInterval('P1Y');
2247 $rrule = 'FREQ=YEARLY;BYMONTH=3;BYDAY=TH';
2248 $mang = new rrule_manager($rrule);
2249 $mang->parse_rrule();
2250 $mang->create_events($this->event
);
2252 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2253 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2255 $untildate = new \
DateTime();
2256 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2257 $untiltimestamp = $untildate->getTimestamp();
2259 $expecteddate = $startdatetime;
2260 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
2261 $offsetinterval = $startdatetime->diff($startdate, true);
2262 $expecteddate->setTimezone(new \
DateTimeZone(get_user_timezone()));
2263 $april1st = new \
DateTime('April 1');
2264 foreach ($records as $record) {
2265 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2266 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2268 // Go to next period.
2269 $expecteddate->modify('next Thursday');
2270 if ($expecteddate->getTimestamp() >= $april1st->getTimestamp()) {
2271 // Reset to 1st of March.
2272 $expecteddate->modify('first day of March');
2274 $expecteddate->add($interval);
2275 if ($expecteddate->format('l') !== rrule_manager
::DAY_THURSDAY
) {
2276 $expecteddate->modify('next Thursday');
2278 // Increment to next year's April 1st.
2279 $april1st->add($interval);
2281 $expecteddate->add($offsetinterval);
2286 * Every Thursday, but only during June, July, and August, forever:
2288 * DTSTART;TZID=US-Eastern:[First Thursday of June of the current year]T090000
2289 * RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8
2291 * Sample results (e.g. in the year 1997):
2292 * (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31;August 7,14,21,28
2293 * (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30;August 6,13,20,27
2294 * (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29;August 5,12,19,26
2297 public function test_every_thursday_june_july_august_forever() {
2300 // Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
2301 $firstthursdayofjune = new \
DateTime('first Thursday of June');
2302 $startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'US/Eastern');
2304 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
2306 $offset = $startdatetime->diff($startdate, true);
2308 $interval = new \
DateInterval('P1Y');
2310 $rrule = 'FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8';
2311 $mang = new rrule_manager($rrule);
2312 $mang->parse_rrule();
2313 $mang->create_events($this->event
);
2315 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2316 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2318 $untildate = new \
DateTime();
2319 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2320 $untiltimestamp = $untildate->getTimestamp();
2322 $expecteddate = new \
DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
2323 $september1st = new \
DateTime('September 1');
2324 foreach ($records as $record) {
2325 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2326 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2328 // Go to next period.
2329 $expecteddate->modify('next Thursday');
2330 if ($expecteddate->getTimestamp() >= $september1st->getTimestamp()) {
2331 $expecteddate->add($interval);
2332 $expecteddate->modify('June 1');
2333 if ($expecteddate->format('l') !== rrule_manager
::DAY_THURSDAY
) {
2334 $expecteddate->modify('next Thursday');
2336 $september1st->add($interval);
2338 $expecteddate->add($offset);
2343 * Every Friday the 13th, forever:
2345 * DTSTART;TZID=US-Eastern:[Current date]T090000
2346 * RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
2348 * Sample results (e.g. in the year 1997):
2349 * (1998 9:00 AM EST)February 13;March 13;November 13
2350 * (1999 9:00 AM EDT)August 13
2351 * (2000 9:00 AM EDT)October 13
2354 public function test_friday_the_thirteenth_forever() {
2357 // Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
2358 $this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
2360 $rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
2361 $mang = new rrule_manager($rrule);
2362 $mang->parse_rrule();
2363 $mang->create_events($this->event
);
2365 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2366 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2368 $untildate = new \
DateTime();
2369 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2370 $untiltimestamp = $untildate->getTimestamp();
2372 foreach ($records as $record) {
2373 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2374 // Assert that the day of the month and the day correspond to Friday the 13th.
2375 $this->assertEquals('Friday 13', date('l d', $record->timestart
));
2380 * The first Saturday that follows the first Sunday of the month, forever:
2382 * DTSTART;TZID=US-Eastern:[The Saturday after the month's first Sunday]T090000
2383 * RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13
2385 * Sample results (e.g. from 13 September 1997):
2386 * (1997 9:00 AM EDT)September 13;October 11
2387 * (1997 9:00 AM EST)November 8;December 13
2388 * (1998 9:00 AM EST)January 10;February 7;March 7
2389 * (1998 9:00 AM EDT)April 11;May 9;June 13...
2391 public function test_first_saturday_following_first_sunday_forever() {
2394 // Change our event's date to the next Saturday after the first Sunday of the the current month at 9am US/Eastern time.
2395 $firstsaturdayafterfirstsunday = new \
DateTime('first Sunday of this month');
2396 $firstsaturdayafterfirstsunday->modify('next Saturday');
2397 $startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'US/Eastern');
2398 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
2399 $offset = $startdatetime->diff($startdate, true);
2401 $rrule = 'FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13';
2402 $mang = new rrule_manager($rrule);
2403 $mang->parse_rrule();
2404 $mang->create_events($this->event
);
2406 // Get the first 100 samples. This should be enough to verify that we have generated the recurring events correctly.
2407 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 100);
2409 $untildate = new \
DateTime();
2410 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2411 $untiltimestamp = $untildate->getTimestamp();
2412 $bymonthdays = [7, 8, 9, 10, 11, 12, 13];
2413 foreach ($records as $record) {
2414 $recordmonthyear = date('F Y', $record->timestart
);
2415 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2417 // Get first Saturday after the first Sunday of the month.
2418 $expecteddate = new \
DateTime('first Sunday of ' . $recordmonthyear);
2419 $expecteddate->modify('next Saturday');
2420 $expecteddate->add($offset);
2422 // Assert the record's date corresponds to the first Saturday of the month.
2423 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2425 // Assert that the record is either the 7th, 8th, 9th, ... 13th day of the month.
2426 $this->assertContainsEquals(date('j', $record->timestart
), $bymonthdays);
2431 * Every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day):
2433 * DTSTART;TZID=US-Eastern:[Most recent election date]T090000
2434 * RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
2436 * Sample results (e.g. from 05 November 1996):
2437 * (1996 9:00 AM EST)November 5
2438 * (2000 9:00 AM EST)November 7
2439 * (2004 9:00 AM EST)November 2
2442 public function test_every_us_presidential_election_forever() {
2445 // Calculate the most recent election date, starting from 1996 (e.g. today's 2017 so the most recent election was in 2016).
2446 $currentyear = (int) date('Y');
2447 $electionyear = 1996;
2448 while ($electionyear +
4 < $currentyear) {
2451 $electiondate = new \
DateTime('first Monday of November ' . $electionyear);
2452 $electiondate->modify('+1 Tuesday');
2454 // Use the most recent election date as the starting date of our recurring events.
2455 $startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'US/Eastern');
2456 $startdate = new \
DateTime($startdatetime->format('Y-m-d'));
2457 $offset = $startdatetime->diff($startdate, true);
2459 $rrule = 'FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8';
2460 $mang = new rrule_manager($rrule);
2461 $mang->parse_rrule();
2462 $mang->create_events($this->event
);
2464 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2466 $untildate = new \
DateTime();
2467 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2468 $untiltimestamp = $untildate->getTimestamp();
2469 $bymonthdays = [2, 3, 4, 5, 6, 7, 8];
2470 foreach ($records as $record) {
2471 $recordmonthyear = date('F Y', $record->timestart
);
2472 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2474 // Get first Saturday after the first Sunday of the month.
2475 $expecteddate = new \
DateTime('first Monday of ' . $recordmonthyear);
2476 $expecteddate->modify('next Tuesday');
2477 $expecteddate->add($offset);
2479 // Assert the record's date corresponds to the first Saturday of the month.
2480 $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart
));
2482 // Assert that the record is either the 2nd, 3rd, 4th ... 8th day of the month.
2483 $this->assertContainsEquals(date('j', $record->timestart
), $bymonthdays);
2488 * The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months:
2490 * DTSTART;TZID=US-Eastern:19970904T090000
2491 * RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3
2492 * ==> (1997 9:00 AM EDT)September 4;October 7
2493 * (1997 9:00 AM EST)November 6
2495 public function test_monthly_bysetpos_3_count() {
2498 $this->change_event_startdate('19970904T090000', 'US/Eastern');
2500 $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
2501 $mang = new rrule_manager($rrule);
2502 $mang->parse_rrule();
2503 $mang->create_events($this->event
);
2505 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2506 $this->assertCount(3, $records);
2509 (new \
DateTime('1997-09-04 09:00:00 EDT'))->getTimestamp(),
2510 (new \
DateTime('1997-10-07 09:00:00 EDT'))->getTimestamp(),
2511 (new \
DateTime('1997-11-06 09:00:00 EST'))->getTimestamp()
2513 foreach ($records as $record) {
2514 $this->assertContainsEquals($record->timestart
, $expecteddates,
2515 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2520 * The 2nd to last weekday of the month:
2522 * DTSTART;TZID=US-Eastern:19970929T090000
2523 * RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7
2524 * ==> (1997 9:00 AM EDT)September 29
2525 * (1997 9:00 AM EST)October 30;November 27;December 30
2526 * (1998 9:00 AM EST)January 29;February 26;March 30
2529 * (Original RFC example is set to recur forever. But we just want to verify that the results match the dates listed from
2530 * the RFC example. So just limit the count to 7.)
2532 public function test_second_to_the_last_weekday_of_the_month() {
2535 $this->change_event_startdate('19970929T090000', 'US/Eastern');
2537 $rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7';
2538 $mang = new rrule_manager($rrule);
2539 $mang->parse_rrule();
2540 $mang->create_events($this->event
);
2542 // Get the first 7 samples. This should be enough to verify that we have generated the recurring events correctly.
2543 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart', 0, 7);
2546 (new \
DateTime('1997-09-29 09:00:00 EDT'))->getTimestamp(),
2547 (new \
DateTime('1997-10-30 09:00:00 EST'))->getTimestamp(),
2548 (new \
DateTime('1997-11-27 09:00:00 EST'))->getTimestamp(),
2549 (new \
DateTime('1997-12-30 09:00:00 EST'))->getTimestamp(),
2550 (new \
DateTime('1998-01-29 09:00:00 EST'))->getTimestamp(),
2551 (new \
DateTime('1998-02-26 09:00:00 EST'))->getTimestamp(),
2552 (new \
DateTime('1998-03-30 09:00:00 EST'))->getTimestamp(),
2555 $untildate = new \
DateTime();
2556 $untildate->add(new \
DateInterval('P' . $mang::TIME_UNLIMITED_YEARS
. 'Y'));
2557 $untiltimestamp = $untildate->getTimestamp();
2560 foreach ($records as $record) {
2561 $this->assertLessThanOrEqual($untiltimestamp, $record->timestart
);
2563 // Confirm that the first 7 records correspond to the expected dates listed above.
2564 $this->assertEquals($expecteddates[$i], $record->timestart
);
2570 * Every 3 hours from 9:00 AM to 5:00 PM on a specific day:
2572 * DTSTART;TZID=US-Eastern:19970902T090000
2573 * RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z
2574 * ==> (September 2, 1997 EDT)09:00,12:00,15:00
2576 public function test_every_3hours_9am_to_5pm() {
2579 $rrule = 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z';
2580 $mang = new rrule_manager($rrule);
2581 $mang->parse_rrule();
2582 $mang->create_events($this->event
);
2584 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2585 $this->assertCount(3, $records);
2588 (new \
DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2589 (new \
DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
2590 (new \
DateTime('1997-09-02 15:00:00 EDT'))->getTimestamp(),
2592 foreach ($records as $record) {
2593 $this->assertContainsEquals($record->timestart
, $expecteddates,
2594 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2599 * Every 15 minutes for 6 occurrences:
2601 * DTSTART;TZID=US-Eastern:19970902T090000
2602 * RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6
2603 * ==> (September 2, 1997 EDT)09:00,09:15,09:30,09:45,10:00,10:15
2605 public function test_every_15minutes_6_count() {
2608 $rrule = 'FREQ=MINUTELY;INTERVAL=15;COUNT=6';
2609 $mang = new rrule_manager($rrule);
2610 $mang->parse_rrule();
2611 $mang->create_events($this->event
);
2613 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2614 $this->assertCount(6, $records);
2617 (new \
DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2618 (new \
DateTime('1997-09-02 09:15:00 EDT'))->getTimestamp(),
2619 (new \
DateTime('1997-09-02 09:30:00 EDT'))->getTimestamp(),
2620 (new \
DateTime('1997-09-02 09:45:00 EDT'))->getTimestamp(),
2621 (new \
DateTime('1997-09-02 10:00:00 EDT'))->getTimestamp(),
2622 (new \
DateTime('1997-09-02 10:15:00 EDT'))->getTimestamp(),
2624 foreach ($records as $record) {
2625 $this->assertContainsEquals($record->timestart
, $expecteddates,
2626 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2631 * Every hour and a half for 4 occurrences:
2633 * DTSTART;TZID=US-Eastern:19970902T090000
2634 * RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4
2635 * ==> (September 2, 1997 EDT)09:00,10:30;12:00;13:30
2637 public function test_every_90minutes_4_count() {
2640 $rrule = 'FREQ=MINUTELY;INTERVAL=90;COUNT=4';
2641 $mang = new rrule_manager($rrule);
2642 $mang->parse_rrule();
2643 $mang->create_events($this->event
);
2645 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2646 $this->assertCount(4, $records);
2649 (new \
DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
2650 (new \
DateTime('1997-09-02 10:30:00 EDT'))->getTimestamp(),
2651 (new \
DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
2652 (new \
DateTime('1997-09-02 13:30:00 EDT'))->getTimestamp(),
2654 foreach ($records as $record) {
2655 $this->assertContainsEquals($record->timestart
, $expecteddates,
2656 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2661 * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
2663 * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
2664 * so just limit the count to 50).
2666 * DTSTART;TZID=US-Eastern:19970902T090000
2667 * RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50
2668 * ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2669 * (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2672 public function test_every_20minutes_daily_byhour_byminute_50_count() {
2675 $rrule = 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50';
2676 $mang = new rrule_manager($rrule);
2677 $mang->parse_rrule();
2678 $mang->create_events($this->event
);
2680 $byminuteinterval = new \
DateInterval('PT20M');
2681 $bydayinterval = new \
DateInterval('P1D');
2682 $date = new \
DateTime('1997-09-02 09:00:00 EDT');
2683 $expecteddates = [];
2685 for ($i = 0; $i < $count; $i++
) {
2686 $expecteddates[] = $date->getTimestamp();
2687 $date->add($byminuteinterval);
2688 if ($date->format('H') > 16) {
2690 $date->add($bydayinterval);
2691 // Reset time to 9am.
2692 $date->setTime(9, 0);
2696 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2697 $this->assertCount($count, $records);
2699 foreach ($records as $record) {
2700 $this->assertContainsEquals($record->timestart
, $expecteddates,
2701 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2706 * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
2708 * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
2709 * so just limit the count to 50).
2711 * DTSTART;TZID=US-Eastern:19970902T090000
2712 * RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50
2713 * ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2714 * (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
2717 public function test_every_20minutes_minutely_byhour_50_count() {
2720 $rrule = 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50';
2721 $mang = new rrule_manager($rrule);
2722 $mang->parse_rrule();
2723 $mang->create_events($this->event
);
2725 $byminuteinterval = new \
DateInterval('PT20M');
2726 $bydayinterval = new \
DateInterval('P1D');
2727 $date = new \
DateTime('1997-09-02 09:00:00');
2728 $expecteddates = [];
2730 for ($i = 0; $i < $count; $i++
) {
2731 $expecteddates[] = $date->getTimestamp();
2732 $date->add($byminuteinterval);
2733 if ($date->format('H') > 16) {
2735 $date->add($bydayinterval);
2736 // Reset time to 9am.
2737 $date->setTime(9, 0);
2741 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2742 $this->assertCount($count, $records);
2744 foreach ($records as $record) {
2745 $this->assertContainsEquals($record->timestart
, $expecteddates,
2746 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2751 * An example where the days generated makes a difference because of WKST:
2753 * DTSTART;TZID=US-Eastern:19970805T090000
2754 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO
2755 * ==> (1997 EDT)Aug 5,10,19,24
2757 public function test_weekly_byday_with_wkst_mo() {
2760 $this->change_event_startdate('19970805T090000', 'US/Eastern');
2762 $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
2763 $mang = new rrule_manager($rrule);
2764 $mang->parse_rrule();
2765 $mang->create_events($this->event
);
2767 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2768 $this->assertCount(4, $records);
2771 (new \
DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
2772 (new \
DateTime('1997-08-10 09:00:00 EDT'))->getTimestamp(),
2773 (new \
DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
2774 (new \
DateTime('1997-08-24 09:00:00 EDT'))->getTimestamp(),
2776 foreach ($records as $record) {
2777 $this->assertContainsEquals($record->timestart
, $expecteddates,
2778 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2783 * An example where the days generated makes a difference because of WKST:
2784 * Changing only WKST from MO to SU, yields different results...
2786 * DTSTART;TZID=US-Eastern:19970805T090000
2787 * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU
2788 * ==> (1997 EDT)August 5,17,19,31
2790 public function test_weekly_byday_with_wkst_su() {
2793 $this->change_event_startdate('19970805T090000', 'US/Eastern');
2795 $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
2796 $mang = new rrule_manager($rrule);
2797 $mang->parse_rrule();
2798 $mang->create_events($this->event
);
2800 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2801 $this->assertCount(4, $records);
2804 (new \
DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
2805 (new \
DateTime('1997-08-17 09:00:00 EDT'))->getTimestamp(),
2806 (new \
DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
2807 (new \
DateTime('1997-08-31 09:00:00 EDT'))->getTimestamp(),
2810 foreach ($records as $record) {
2811 $this->assertContainsEquals($record->timestart
, $expecteddates,
2812 date('Y-m-d H:i:s', $record->timestart
) . ' is not found.');
2817 * Other edge case tests.
2821 * Tests for MONTHLY RRULE with BYMONTHDAY set to 31.
2822 * Should not include February, April, June, September and November.
2824 public function test_monthly_bymonthday_31() {
2827 $rrule = 'FREQ=MONTHLY;BYMONTHDAY=31;COUNT=20';
2828 $mang = new rrule_manager($rrule);
2829 $mang->parse_rrule();
2830 $mang->create_events($this->event
);
2832 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2833 $this->assertCount(20, $records);
2835 $non31months = ['February', 'April', 'June', 'September', 'November'];
2837 foreach ($records as $record) {
2838 $month = date('F', $record->timestart
);
2839 $this->assertNotContains($month, $non31months);
2844 * Tests for the last day in February. (Leap year test)
2846 public function test_yearly_on_the_last_day_of_february() {
2849 $rrule = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1;COUNT=30';
2850 $mang = new rrule_manager($rrule);
2851 $mang->parse_rrule();
2852 $mang->create_events($this->event
);
2854 $records = $DB->get_records('event', ['repeatid' => $this->event
->id
], 'timestart ASC', 'id, repeatid, timestart');
2855 $this->assertCount(30, $records);
2857 foreach ($records as $record) {
2858 $date = new \
DateTime(date('Y-m-d H:i:s', $record->timestart
));
2859 $year = $date->format('Y');
2860 $day = $date->format('d');
2861 if ($year %
4 == 0) {
2862 $this->assertEquals(29, $day);
2864 $this->assertEquals(28, $day);
2870 * Change the event's timestart (DTSTART) based on the test's needs.
2872 * @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000.
2873 * @param null|string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
2874 * If not provided, the default timezone will be used.
2875 * @return bool|DateTime
2877 protected function change_event_startdate($datestr, $timezonestr = null) {
2878 // Use default timezone if not provided.
2879 if ($timezonestr === null) {
2880 $newdatetime = \DateTime
::createFromFormat('Ymd\THis', $datestr);
2882 $timezone = new \
DateTimeZone($timezonestr);
2883 $newdatetime = \DateTime
::createFromFormat('Ymd\THis', $datestr, $timezone);
2886 // Update the start date of the parent event.
2887 $calevent = \calendar_event
::load($this->event
->id
);
2888 $updatedata = (object)[
2889 'timestart' => $newdatetime->getTimestamp(),
2890 'repeatid' => $this->event
->id
2892 $calevent->update($updatedata, false);
2893 $this->event
->timestart
= $calevent->timestart
;
2895 return $newdatetime;