Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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