Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 39 and 401]

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