Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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