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