MDL-57658 calendar: Fix rrulemanager and unit tests
authorJun Pataleta <jun@moodle.com>
Thu, 19 Jan 2017 07:22:06 +0000 (15:22 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 9 Mar 2017 08:10:08 +0000 (16:10 +0800)
Issues fixed:
* Additional rule validations.
* Rewrote recurrence logic.
* Additional unit tests, especially from the examples in the RFC.
* The date format "YmdThis" results into an incorrect date for the
  "UNTIL" parameter. The literal "T" should be escaped, and a literal
  "Z" is also needed at the end of the format string to indicate UTC.
* Implemented handling of negative modifier values for BYxxx rules.
* Implemented handling of BYWEEKNO, BYYEARDAY, BYSETPOS, BYHOUR,
  BYMINUTE, BYSECOND rules.

calendar/classes/rrule_manager.php
calendar/tests/rrule_manager_test.php [new file with mode: 0644]
calendar/tests/rrule_manager_tests.php [deleted file]
lang/en/calendar.php

index 81c3117..5ac1baf 100644 (file)
  */
 
 namespace core_calendar;
+
+use calendar_event;
+use DateInterval;
+use DateTime;
+use moodle_exception;
+use NumberFormatter;
+use stdClass;
+
 defined('MOODLE_INTERNAL') || die();
 require_once($CFG->dirroot . '/calendar/lib.php');
 
@@ -134,6 +142,17 @@ class rrule_manager {
     /** const int For forever repeating events, repeat for this many years */
     const TIME_UNLIMITED_YEARS = 10;
 
+    /** const array Array of days in a week. */
+    const DAYS_OF_WEEK = [
+        'MO' => self::DAY_MONDAY,
+        'TU' => self::DAY_TUESDAY,
+        'WE' => self::DAY_WEDNESDAY,
+        'TH' => self::DAY_THURSDAY,
+        'FR' => self::DAY_FRIDAY,
+        'SA' => self::DAY_SATURDAY,
+        'SU' => self::DAY_SUNDAY,
+    ];
+
     /** @var string string representing the recurrence rule */
     protected $rrule;
 
@@ -176,8 +195,8 @@ class rrule_manager {
     /** @var array List of setpos rules */
     protected $bysetpos = array();
 
-    /** @var array week start rules */
-    protected $wkst;
+    /** @var string Week start rule. Default is Monday. */
+    protected $wkst = self::DAY_MONDAY;
 
     /**
      * Constructor for the class
@@ -199,13 +218,50 @@ class rrule_manager {
         foreach ($rules as $rule) {
             $this->parse_rrule_property($rule);
         }
+        // Validate the rules as a whole.
+        $this->validate_rules();
+    }
+
+    /**
+     * Create events for specified rrule.
+     *
+     * @param calendar_event $passedevent Properties of event to create.
+     * @throws moodle_exception
+     */
+    public function create_events($passedevent) {
+        global $DB;
+
+        $event = clone($passedevent);
+        // If Frequency is not set, there is nothing to do.
+        if (empty($this->freq)) {
+            return;
+        }
+
+        // Delete all child events in case of an update. This should be faster than verifying if the event exists and updating it.
+        $where = "repeatid = ? AND id != ?";
+        $DB->delete_records_select('event', $where, array($event->id, $event->id));
+        $eventrec = $event->properties();
+
+        // Generate timestamps that obey the rrule.
+        $eventtimes = $this->generate_recurring_event_times($eventrec);
+
+        // Adjust the parent event's timestart, if necessary.
+        if (count($eventtimes) > 0 && !in_array($eventrec->timestart, $eventtimes)) {
+            $calevent = new calendar_event($eventrec);
+            $updatedata = (object)['timestart' => $eventtimes[0], 'repeatid' => $eventrec->id];
+            $calevent->update($updatedata, false);
+            $eventrec->timestart = $calevent->timestart;
+        }
+
+        // Create the recurring calendar events.
+        $this->create_recurring_events($eventrec, $eventtimes);
     }
 
     /**
      * Parse a property of the recurrence rule.
      *
      * @param string $prop property string with type-value pair
-     * @throws \moodle_exception
+     * @throws moodle_exception
      */
     protected function parse_rrule_property($prop) {
         list($property, $value) = explode('=', $prop);
@@ -214,47 +270,47 @@ class rrule_manager {
                 $this->set_frequency($value);
                 break;
             case 'UNTIL' :
-                $this->until = strtotime($value);
+                $this->set_until($value);
                 break;
             CASE 'COUNT' :
-                $this->count = intval($value);
+                $this->set_count($value);
                 break;
             CASE 'INTERVAL' :
-                $this->interval = intval($value);
+                $this->set_interval($value);
                 break;
             CASE 'BYSECOND' :
-                $this->bysecond = explode(',', $value);
+                $this->set_bysecond($value);
                 break;
             CASE 'BYMINUTE' :
-                $this->byminute = explode(',', $value);
+                $this->set_byminute($value);
                 break;
             CASE 'BYHOUR' :
-                $this->byhour = explode(',', $value);
+                $this->set_byhour($value);
                 break;
             CASE 'BYDAY' :
-                $this->byday = explode(',', $value);
+                $this->set_byday($value);
                 break;
             CASE 'BYMONTHDAY' :
-                $this->bymonthday = explode(',', $value);
+                $this->set_bymonthday($value);
                 break;
             CASE 'BYYEARDAY' :
-                $this->byyearday = explode(',', $value);
+                $this->set_byyearday($value);
                 break;
             CASE 'BYWEEKNO' :
-                $this->byweekno = explode(',', $value);
+                $this->set_byweekno($value);
                 break;
             CASE 'BYMONTH' :
-                $this->bymonth = explode(',', $value);
+                $this->set_bymonth($value);
                 break;
             CASE 'BYSETPOS' :
-                $this->bysetpos = explode(',', $value);
+                $this->set_bysetpos($value);
                 break;
             CASE 'WKST' :
                 $this->wkst = $this->get_day($value);
                 break;
             default:
                 // We should never get here, something is very wrong.
-                throw new \moodle_exception('errorrrule', 'calendar');
+                throw new moodle_exception('errorrrule', 'calendar');
         }
     }
 
@@ -262,7 +318,7 @@ class rrule_manager {
      * Sets Frequency property.
      *
      * @param string $freq Frequency of event
-     * @throws \moodle_exception
+     * @throws moodle_exception
      */
     protected function set_frequency($freq) {
         switch ($freq) {
@@ -289,7 +345,7 @@ class rrule_manager {
                 break;
             default:
                 // We should never get here, something is very wrong.
-                throw new \moodle_exception('errorrrulefreq', 'calendar');
+                throw new moodle_exception('errorrrulefreq', 'calendar');
         }
     }
 
@@ -297,7 +353,7 @@ class rrule_manager {
      * Gets the day from day string.
      *
      * @param string $daystring Day string (MO, TU, etc)
-     * @throws \moodle_exception
+     * @throws moodle_exception
      *
      * @return string Day represented by the parameter.
      */
@@ -326,341 +382,965 @@ class rrule_manager {
                 break;
             default:
                 // We should never get here, something is very wrong.
-                throw new \moodle_exception('errorrruleday', 'calendar');
+                throw new moodle_exception('errorrruleday', 'calendar');
         }
     }
 
     /**
-     * Create events for specified rrule.
+     * Sets the UNTIL rule.
      *
-     * @param \calendar_event $passedevent Properties of event to create.
-     * @throws \moodle_exception
+     * @param string $until The date string representation of the UNTIL rule.
+     * @throws moodle_exception
      */
-    public function create_events($passedevent) {
-        global $DB;
+    protected function set_until($until) {
+        $this->until = strtotime($until);
+    }
 
-        $event = clone($passedevent);
-        // If Frequency is not set, there is nothing to do.
-        if (empty($this->freq)) {
-            return;
-        }
+    /**
+     * Sets the COUNT rule.
+     *
+     * @param string $count The count value.
+     * @throws moodle_exception
+     */
+    protected function set_count($count) {
+        $this->count = intval($count);
+    }
 
-        // Delete all child events in case of an update. This should be faster than verifying if the event exists and updating it.
-        $where = "repeatid = ? AND id != ?";
-        $DB->delete_records_select('event', $where, array($event->id, $event->id));
-        $eventrec = $event->properties();
+    /**
+     * Sets the INTERVAL rule.
+     *
+     * The INTERVAL rule part contains a positive integer representing how often the recurrence rule repeats.
+     * The default value is "1", meaning:
+     *  - every second for a SECONDLY rule, or
+     *  - every minute for a MINUTELY rule,
+     *  - every hour for an HOURLY rule,
+     *  - every day for a DAILY rule,
+     *  - every week for a WEEKLY rule,
+     *  - every month for a MONTHLY rule and
+     *  - every year for a YEARLY rule.
+     *
+     * @param string $intervalstr The value for the interval rule.
+     * @throws moodle_exception
+     */
+    protected function set_interval($intervalstr) {
+        $interval = intval($intervalstr);
+        if ($interval < 1) {
+            throw new moodle_exception('errorinvalidinterval', 'calendar');
+        }
+        $this->interval = $interval;
+    }
 
-        switch ($this->freq) {
-            case self::FREQ_DAILY :
-                $this->create_repeated_events($eventrec, DAYSECS);
-                break;
-            case self::FREQ_WEEKLY :
-                $this->create_weekly_events($eventrec);
-                break;
-            case self::FREQ_MONTHLY :
-                $this->create_monthly_events($eventrec);
-                break;
-            case self::FREQ_YEARLY :
-                $this->create_yearly_events($eventrec);
-                break;
-            default :
-                // We should never get here, something is very wrong.
-                throw new \moodle_exception('errorrulefreq', 'calendar');
+    /**
+     * Sets the BYSECOND rule.
+     *
+     * The BYSECOND rule part specifies a comma-separated list of seconds within a minute.
+     * Valid values are 0 to 59.
+     *
+     * @param string $bysecond Comma-separated list of seconds within a minute.
+     * @throws moodle_exception
+     */
+    protected function set_bysecond($bysecond) {
+        $seconds = explode(',', $bysecond);
+        $bysecondrules = [];
+        foreach ($seconds as $second) {
+            if ($second < 0 || $second > 59) {
+                throw new moodle_exception('errorinvalidbysecond', 'calendar');
+            }
+            $bysecondrules[] = (int)$second;
+        }
+        $this->bysecond = $bysecondrules;
+    }
 
+    /**
+     * Sets the BYMINUTE rule.
+     *
+     * The BYMINUTE rule part specifies a comma-separated list of seconds within an hour.
+     * Valid values are 0 to 59.
+     *
+     * @param string $byminute Comma-separated list of minutes within an hour.
+     * @throws moodle_exception
+     */
+    protected function set_byminute($byminute) {
+        $minutes = explode(',', $byminute);
+        $byminuterules = [];
+        foreach ($minutes as $minute) {
+            if ($minute < 0 || $minute > 59) {
+                throw new moodle_exception('errorinvalidbyminute', 'calendar');
+            }
+            $byminuterules[] = (int)$minute;
         }
+        $this->byminute = $byminuterules;
+    }
 
+    /**
+     * Sets the BYHOUR rule.
+     *
+     * The BYHOUR rule part specifies a comma-separated list of hours of the day.
+     * Valid values are 0 to 23.
+     *
+     * @param string $byhour Comma-separated list of hours of the day.
+     * @throws moodle_exception
+     */
+    protected function set_byhour($byhour) {
+        $hours = explode(',', $byhour);
+        $byhourrules = [];
+        foreach ($hours as $hour) {
+            if ($hour < 0 || $hour > 23) {
+                throw new moodle_exception('errorinvalidbyhour', 'calendar');
+            }
+            $byhourrules[] = (int)$hour;
+        }
+        $this->byhour = $byhourrules;
     }
 
     /**
-     * Create repeated events.
+     * Sets the BYDAY rule.
+     *
+     * The BYDAY rule part specifies a comma-separated list of days of the week;
+     *  - MO indicates Monday;
+     *  - TU indicates Tuesday;
+     *  - WE indicates Wednesday;
+     *  - TH indicates Thursday;
+     *  - FR indicates Friday;
+     *  - SA indicates Saturday;
+     *  - SU indicates Sunday.
+     *
+     * Each BYDAY value can also be preceded by a positive (+n) or negative (-n) integer.
+     * If present, this indicates the nth occurrence of the specific day within the MONTHLY or YEARLY RRULE.
+     * For example, within a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday within the month,
+     * whereas -1MO represents the last Monday of the month.
+     * If an integer modifier is not present, it means all days of this type within the specified frequency.
+     * For example, within a MONTHLY rule, MO represents all Mondays within the month.
      *
-     * @param \stdClass $event Event properties to create event
-     * @param int $timediff Time difference between events in seconds
-     * @param bool $currenttime If set, the event timestart is used as the timestart for the first event,
-     *                          else timestart + timediff used as the timestart for the first event. Set to true if
-     *                          parent event is not a part of this chain.
+     * @param string $byday Comma-separated list of days of the week.
+     * @throws moodle_exception
      */
-    protected function create_repeated_events($event, $timediff, $currenttime = false) {
+    protected function set_byday($byday) {
+        $weekdays = array_keys(self::DAYS_OF_WEEK);
+        $days = explode(',', $byday);
+        $bydayrules = [];
+        foreach ($days as $day) {
+            $suffix = substr($day, -2);
+            if (!in_array($suffix, $weekdays)) {
+                throw new moodle_exception('errorinvalidbydaysuffix', 'calendar');
+            }
 
-        $event = clone($event); // We don't want to edit the master record.
-        $event->repeatid = $event->id; // Set parent id for all events.
-        unset($event->id); // We want new events created, not update the existing one.
-        unset($event->uuid); // uuid should be unique.
-        $count = $this->count;
+            $bydayrule = new stdClass();
+            $bydayrule->day = substr($suffix, -2);
+            $bydayrule->value = (int)str_replace($suffix, '', $day);
 
-        // Multiply by interval if used.
-        if ($this->interval) {
-            $timediff *= $this->interval;
-        }
-        if (!$currenttime) {
-            $event->timestart += $timediff;
+            $bydayrules[] = $bydayrule;
         }
 
-        // Create events.
-        if ($count > 0) {
-            // Count specified, use it.
-            if (!$currenttime) {
-                $count--; // Already a parent event has been created.
+        $this->byday = $bydayrules;
+    }
+
+    /**
+     * Sets the BYMONTHDAY rule.
+     *
+     * The BYMONTHDAY rule part specifies a comma-separated list of days of the month.
+     * Valid values are 1 to 31 or -31 to -1. For example, -10 represents the tenth to the last day of the month.
+     *
+     * @param string $bymonthday Comma-separated list of days of the month.
+     * @throws moodle_exception
+     */
+    protected function set_bymonthday($bymonthday) {
+        $monthdays = explode(',', $bymonthday);
+        $bymonthdayrules = [];
+        foreach ($monthdays as $day) {
+            // Valid values are 1 to 31 or -31 to -1.
+            if ($day < -31 || $day > 31 || $day == 0) {
+                throw new moodle_exception('errorinvalidbymonthday', 'calendar');
             }
-            for ($i = 0; $i < $count; $i++, $event->timestart += $timediff) {
-                unset($event->id); // It is set during creation.
-                \calendar_event::create($event, false);
+            $bymonthdayrules[] = (int)$day;
+        }
+
+        // Sort these MONTHDAY rules in ascending order.
+        sort($bymonthdayrules);
+
+        $this->bymonthday = $bymonthdayrules;
+    }
+
+    /**
+     * Sets the BYYEARDAY rule.
+     *
+     * The BYYEARDAY rule part specifies a comma-separated list of days of the year.
+     * Valid values are 1 to 366 or -366 to -1. For example, -1 represents the last day of the year (December 31st)
+     * and -306 represents the 306th to the last day of the year (March 1st).
+     *
+     * @param string $byyearday Comma-separated list of days of the year.
+     * @throws moodle_exception
+     */
+    protected function set_byyearday($byyearday) {
+        $yeardays = explode(',', $byyearday);
+        $byyeardayrules = [];
+        foreach ($yeardays as $day) {
+            // Valid values are 1 to 366 or -366 to -1.
+            if ($day < -366 || $day > 366 || $day == 0) {
+                throw new moodle_exception('errorinvalidbyyearday', 'calendar');
             }
-        } else {
-            // No count specified, use datetime constraints.
-            $until = $this->until;
-            if (empty($until)) {
-                // Forever event. We don't have any such concept in Moodle, hence we repeat it for a constant time.
-                $until = time() + (YEARSECS * self::TIME_UNLIMITED_YEARS);
+            $byyeardayrules[] = (int)$day;
+        }
+        $this->byyearday = $byyeardayrules;
+    }
+
+    /**
+     * Sets the BYWEEKNO rule.
+     *
+     * The BYWEEKNO rule part specifies a comma-separated list of ordinals specifying weeks of the year.
+     * Valid values are 1 to 53 or -53 to -1. This corresponds to weeks according to week numbering as defined in [ISO 8601].
+     * A week is defined as a seven day period, starting on the day of the week defined to be the week start (see WKST).
+     * Week number one of the calendar year is the first week which contains at least four (4) days in that calendar year.
+     * This rule part is only valid for YEARLY rules. For example, 3 represents the third week of the year.
+     *
+     * Note: Assuming a Monday week start, week 53 can only occur when Thursday is January 1 or if it is a leap year and Wednesday
+     * is January 1.
+     *
+     * @param string $byweekno Comma-separated list of number of weeks.
+     * @throws moodle_exception
+     */
+    protected function set_byweekno($byweekno) {
+        $weeknumbers = explode(',', $byweekno);
+        $byweeknorules = [];
+        foreach ($weeknumbers as $week) {
+            // Valid values are 1 to 53 or -53 to -1.
+            if ($week < -53 || $week > 53 || $week == 0) {
+                throw new moodle_exception('errorinvalidbyweekno', 'calendar');
             }
-            for (; $event->timestart < $until; $event->timestart += $timediff) {
-                unset($event->id); // It is set during creation.
-                \calendar_event::create($event, false);
+            $byweeknorules[] = (int)$week;
+        }
+        $this->byweekno = $byweeknorules;
+    }
+
+    /**
+     * Sets the BYMONTH rule.
+     *
+     * The BYMONTH rule part specifies a comma-separated list of months of the year.
+     * Valid values are 1 to 12.
+     *
+     * @param string $bymonth Comma-separated list of months of the year.
+     * @throws moodle_exception
+     */
+    protected function set_bymonth($bymonth) {
+        $months = explode(',', $bymonth);
+        $bymonthrules = [];
+        foreach ($months as $month) {
+            // Valid values are 1 to 12.
+            if ($month < 1 || $month > 12) {
+                throw new moodle_exception('errorinvalidbymonth', 'calendar');
             }
+            $bymonthrules[] = (int)$month;
         }
+        $this->bymonth = $bymonthrules;
     }
 
     /**
-     * Create repeated events based on offsets.
+     * Sets the BYSETPOS rule.
      *
-     * @param \stdClass $event
-     * @param int $secsoffset Seconds since the start of the day that this event occurs
-     * @param int $dayoffset Day offset.
-     * @param int $monthoffset Months offset.
-     * @param int $yearoffset Years offset.
-     * @param int $start timestamp to apply offsets onto.
-     * @param bool $currenttime If set, the event timestart is used as the timestart for the first event,
-     *                          else timestart + timediff(monthly offset + yearly offset) used as the timestart for the first
-     *                          event.Set to true if parent event is not a part of this chain.
+     * The BYSETPOS rule part specifies a comma-separated list of values which corresponds to the nth occurrence within the set of
+     * events specified by the rule. Valid values are 1 to 366 or -366 to -1.
+     * It MUST only be used in conjunction with another BYxxx rule part.
+     *
+     * For example "the last work day of the month" could be represented as: RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
+     *
+     * @param string $bysetpos Comma-separated list of values.
+     * @throws moodle_exception
      */
-    protected function create_repeated_events_by_offsets($event, $secsoffset, $dayoffset, $monthoffset, $yearoffset, $start,
-                                                         $currenttime = false) {
+    protected function set_bysetpos($bysetpos) {
+        $setposes = explode(',', $bysetpos);
+        $bysetposrules = [];
+        foreach ($setposes as $pos) {
+            // Valid values are 1 to 366 or -366 to -1.
+            if ($pos < -366 || $pos > 366 || $pos == 0) {
+                throw new moodle_exception('errorinvalidbysetpos', 'calendar');
+            }
+            $bysetposrules[] = (int)$pos;
+        }
+        $this->bysetpos = $bysetposrules;
+    }
 
-        $event = clone($event); // We don't want to edit the master record.
-        $event->repeatid = $event->id; // Set parent id for all events.
-        unset($event->id); // We want new events created, not update the existing one.
-        unset($event->uuid); // uuid should be unique.
-        $count = $this->count;
-        // First event time in this chain.
-        $event->timestart = strtotime("+$dayoffset days", $start) + $secsoffset;
+    /**
+     * Validate the rules as a whole.
+     *
+     * @throws moodle_exception
+     */
+    protected function validate_rules() {
+        // UNTIL and COUNT cannot be in the same recurrence rule.
+        if (!empty($this->until) && !empty($this->count)) {
+            throw new moodle_exception('errorhasuntilandcount', 'calendar');
+        }
 
-        if (!$currenttime) {
-            // Skip one event, since parent event is a part of this chain.
-            $event->timestart = strtotime("+$monthoffset months +$yearoffset years", $event->timestart);
+        // BYSETPOS only be used in conjunction with another BYxxx rule part.
+        if (!empty($this->bysetpos) && empty($this->bymonth) && empty($this->bymonthday) && empty($this->bysecond)
+            && empty($this->byday) && empty($this->byweekno) && empty($this->byhour) && empty($this->byminute)
+            && empty($this->byyearday)) {
+            throw new moodle_exception('errormustbeusedwithotherbyrule', 'calendar');
         }
 
-        // Create events.
-        if ($count > 0) {
-            // Count specified, use it.
-            if (!$currenttime) {
-                $count--; // Already a parent event has been created.
+        // Integer values preceding BYDAY rules can only be present for MONTHLY or YEARLY RRULE.
+        foreach ($this->byday as $bydayrule) {
+            if (!empty($bydayrule->value) && $this->freq != self::FREQ_MONTHLY && $this->freq != self::FREQ_YEARLY) {
+                throw new moodle_exception('errorinvalidbydayprefix', 'calendar');
             }
-            for ($i = 0; $i < $count; $i++) {
-                unset($event->id); // It is set during creation.
-                \calendar_event::create($event, false);
-                $event->timestart = strtotime("+$monthoffset months +$yearoffset years", $event->timestart);
-            }
-        } else {
-            // No count specified, use datetime constraints.
-            $until = $this->until;
-            if (empty($until)) {
-                // Forever event. We don't have any such concept in Moodle, hence we repeat it for a constant time.
-                $until = time() + (YEARSECS * self::TIME_UNLIMITED_YEARS );
+        }
+
+        // The BYWEEKNO rule is only valid for YEARLY rules.
+        if (!empty($this->byweekno) && $this->freq != self::FREQ_YEARLY) {
+            throw new moodle_exception('errornonyearlyfreqwithbyweekno', 'calendar');
+        }
+    }
+
+    /**
+     * Creates calendar events for the recurring events.
+     *
+     * @param stdClass $event The parent event.
+     * @param int[] $eventtimes The timestamps of the recurring events.
+     */
+    protected function create_recurring_events($event, $eventtimes) {
+        $count = false;
+        if ($this->count) {
+            $count = $this->count;
+        }
+
+        foreach ($eventtimes as $time) {
+            // Skip if time is the same time with the parent event's timestamp.
+            if ($time == $event->timestart) {
+                continue;
             }
-            for (; $event->timestart < $until;) {
-                unset($event->id); // It is set during creation.
-                \calendar_event::create($event, false);
-                $event->timestart = strtotime("+$monthoffset months +$yearoffset years", $event->timestart);
 
+            // Decrement count, if set.
+            if ($count !== false) {
+                $count--;
+                if ($count == 0) {
+                    break;
+                }
             }
+
+            // Create the recurring event.
+            $cloneevent = clone($event);
+            $cloneevent->repeatid = $event->id;
+            $cloneevent->timestart = $time;
+            unset($cloneevent->id);
+            calendar_event::create($cloneevent, false);
         }
     }
 
     /**
-     * Create repeated events based on offsets from a fixed start date.
+     * Generates recurring events based on the parent event and the RRULE set.
      *
-     * @param \stdClass $event
-     * @param int $secsoffset Seconds since the start of the day that this event occurs
-     * @param string $prefix Prefix string to add to strtotime while calculating next date for the event.
-     * @param int $monthoffset Months offset.
-     * @param int $yearoffset Years offset.
-     * @param int $start timestamp to apply offsets onto.
-     * @param bool $currenttime If set, the event timestart is used as the timestart + offset for the first event,
-     *                          else timestart + timediff(monthly offset + yearly offset) + offset used as the timestart for the
-     *                          first event, from the given fixed start time. Set to true if parent event is not a part of this
-     *                          chain.
+     * If multiple BYxxx rule parts are specified, then after evaluating the specified FREQ and INTERVAL rule parts,
+     * the BYxxx rule parts are applied to the current set of evaluated occurrences in the following order:
+     * BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND and BYSETPOS;
+     * then COUNT and UNTIL are evaluated.
+     *
+     * @param stdClass $event The event object.
+     * @return array The list of timestamps that obey the given RRULE.
      */
-    protected function create_repeated_events_by_offsets_from_fixedstart($event, $secsoffset, $prefix, $monthoffset,
-                                                                         $yearoffset, $start, $currenttime = false) {
+    protected function generate_recurring_event_times($event) {
+        $interval = $this->get_interval();
+
+        // Candidate event times.
+        $eventtimes = [];
 
-        $event = clone($event); // We don't want to edit the master record.
-        $event->repeatid = $event->id; // Set parent id for all events.
-        unset($event->id); // We want new events created, not update the existing one.
-        unset($event->uuid); // uuid should be unique.
-        $count = $this->count;
+        $eventdatetime = new DateTime(date('Y-m-d H:i:s', $event->timestart));
 
-        // First event time in this chain.
-        if (!$currenttime) {
-            // Skip one event, since parent event is a part of this chain.
-            $moffset = $monthoffset;
-            $yoffset = $yearoffset;
-            $event->timestart = strtotime("+$monthoffset months +$yearoffset years", $start);
-            $event->timestart = strtotime($prefix, $event->timestart) + $secsoffset;
+        $until = null;
+        if (empty($this->count)) {
+            if ($this->until) {
+                $until = $this->until;
+            } else {
+                // Forever event. However, since there's no such thing as 'forever' (at least not in Moodle),
+                // we only repeat the events until 10 years from the current time.
+                $untildate = new DateTime();
+                $foreverinterval = new DateInterval('P' . self::TIME_UNLIMITED_YEARS . 'Y');
+                $untildate->add($foreverinterval);
+                $until = $untildate->getTimestamp();
+            }
         } else {
-            $moffset = 0;
-            $yoffset = 0;
-            $event->timestart = strtotime($prefix, $event->timestart) + $secsoffset;
-        }
-        // Create events.
-        if ($count > 0) {
-            // Count specified, use it.
-            if (!$currenttime) {
-                $count--; // Already a parent event has been created.
+            // If count is defined, let's define a tentative until date. We'll just trim the number of events later.
+            $untildate = clone($eventdatetime);
+            $count = $this->count;
+            while ($count >= 0) {
+                $untildate->add($interval);
+                $count--;
             }
-            for ($i = 0; $i < $count; $i++) {
-                unset($event->id); // It is set during creation.
-                \calendar_event::create($event, false);
-                $moffset += $monthoffset;
-                $yoffset += $yearoffset;
-                $event->timestart = strtotime("+$moffset months +$yoffset years", $start);
-                $event->timestart = strtotime($prefix, $event->timestart) + $secsoffset;
+            $until = $untildate->getTimestamp();
+        }
+
+        // No filters applied. Generate recurring events right away.
+        if (!$this->has_by_rules()) {
+            // Get initial list of prospective events.
+            $tmpstart = clone($eventdatetime);
+            while ($tmpstart->getTimestamp() <= $until) {
+                $eventtimes[] = $tmpstart->getTimestamp();
+                $tmpstart->add($interval);
             }
-        } else {
-            // No count specified, use datetime constraints.
-            $until = $this->until;
-            if (empty($until)) {
-                // Forever event. We don't have any such concept in Moodle, hence we repeat it for a constant time.
-                $until = time() + (YEARSECS * self::TIME_UNLIMITED_YEARS );
+            return $eventtimes;
+        }
+
+        // Get all of potential dates covered by the periods from the event's start date until the last.
+        $dailyinterval = new DateInterval('P1D');
+        $boundslist = $this->get_period_bounds_list($eventdatetime->getTimestamp(), $until);
+        foreach ($boundslist as $bounds) {
+            $tmpdate = new DateTime(date('Y-m-d H:i:s', $bounds->start));
+            while ($tmpdate->getTimestamp() >= $bounds->start && $tmpdate->getTimestamp() < $bounds->next) {
+                $eventtimes[] = $tmpdate->getTimestamp();
+                $tmpdate->add($dailyinterval);
             }
-            for (; $event->timestart < $until;) {
-                unset($event->id); // It is set during creation.
-                \calendar_event::create($event, false);
-                $moffset += $monthoffset;
-                $yoffset += $yearoffset;
-                $event->timestart = strtotime("+$moffset months +$yoffset years", $start);
-                $event->timestart = strtotime($prefix, $event->timestart) + $secsoffset;
+        }
+
+        // Evaluate BYMONTH rules.
+        $eventtimes = $this->filter_by_month($eventtimes);
+
+        // Evaluate BYWEEKNO rules.
+        $eventtimes = $this->filter_by_weekno($eventtimes);
+
+        // Evaluate BYYEARDAY rules.
+        $eventtimes = $this->filter_by_yearday($eventtimes);
+
+        // If BYYEARDAY, BYMONTHDAY and BYDAY are not set, default to BYMONTHDAY based on the DTSTART's day.
+        if ($this->freq != self::FREQ_DAILY && empty($this->byyearday) && empty($this->bymonthday) && empty($this->byday)) {
+            $this->bymonthday = [$eventdatetime->format('j')];
+        }
+
+        // Evaluate BYMONTHDAY rules.
+        $eventtimes = $this->filter_by_monthday($eventtimes);
+
+        // Evaluate BYDAY rules.
+        $eventtimes = $this->filter_by_day($event, $eventtimes, $until);
+
+        // Evaluate BYHOUR rules.
+        $eventtimes = $this->apply_hour_minute_second_rules($eventdatetime, $eventtimes);
+
+        // Evaluate BYSETPOS rules.
+        $eventtimes = $this->filter_by_setpos($event, $eventtimes, $until);
+
+        // Sort event times in ascending order.
+        sort($eventtimes);
+
+        // Finally, filter candidate event times to make sure they are within the DTSTART and UNTIL/tentative until boundaries.
+        $results = [];
+        foreach ($eventtimes as $time) {
+            // Skip out-of-range events.
+            if ($time < $eventdatetime->getTimestamp()) {
+                continue;
+            }
+            // End if event time is beyond the until limit.
+            if ($time > $until) {
+                break;
             }
+            $results[] = $time;
         }
+
+        return $results;
     }
 
     /**
-     * Create events for weekly frequency.
+     * Generates a DateInterval object based on the FREQ and INTERVAL rules.
      *
-     * @param \stdClass $event Event properties to create event
+     * @return DateInterval
+     * @throws moodle_exception
      */
-    protected function create_weekly_events($event) {
-        // If by day is not present, it means all days of the week.
-        if (empty($this->byday)) {
-            $this->byday = array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU');
-        }
-        // This much seconds after the start of the day.
-        $offset = $event->timestart - mktime(0, 0, 0, date("n", $event->timestart), date("j", $event->timestart), date("Y",
-                $event->timestart));
-        foreach ($this->byday as $daystring) {
-            $day = $this->get_day($daystring);
-            if (date('l', $event->timestart) == $day) {
-                // Parent event is a part of this day chain.
-                $this->create_repeated_events($event, WEEKSECS, false);
-            } else {
-                // Parent event is not a part of this day chain.
-                $cpyevent = clone($event); // We don't want to change timestart of master record.
-                $cpyevent->timestart = strtotime("+$offset seconds next $day", $cpyevent->timestart);
-                $this->create_repeated_events($cpyevent, WEEKSECS, true);
+    protected function get_interval() {
+        $intervalspec = null;
+        switch ($this->freq) {
+            case self::FREQ_YEARLY:
+                $intervalspec = 'P' . $this->interval . 'Y';
+                break;
+            case self::FREQ_MONTHLY:
+                $intervalspec = 'P' . $this->interval . 'M';
+                break;
+            case self::FREQ_WEEKLY:
+                $intervalspec = 'P' . $this->interval . 'W';
+                break;
+            case self::FREQ_DAILY:
+                $intervalspec = 'P' . $this->interval . 'D';
+                break;
+            case self::FREQ_HOURLY:
+                $intervalspec = 'PT' . $this->interval . 'H';
+                break;
+            case self::FREQ_MINUTELY:
+                $intervalspec = 'PT' . $this->interval . 'M';
+                break;
+            case self::FREQ_SECONDLY:
+                $intervalspec = 'PT' . $this->interval . 'S';
+                break;
+            default:
+                // We should never get here, something is very wrong.
+                throw new moodle_exception('errorrrulefreq', 'calendar');
+        }
+
+        return new DateInterval($intervalspec);
+    }
+
+    /**
+     * Determines whether the RRULE has BYxxx rules or not.
+     *
+     * @return bool True if there is one or more BYxxx rules to process. False, otherwise.
+     */
+    protected function has_by_rules() {
+        return !empty($this->bymonth) || !empty($this->bymonthday) || !empty($this->bysecond) || !empty($this->byday)
+            || !empty($this->byweekno) || !empty($this->byhour) || !empty($this->byminute) || !empty($this->byyearday);
+    }
+
+    /**
+     * Filter event times based on the BYMONTH rule.
+     *
+     * @param int[] $eventdates Timestamps of event times to be filtered.
+     * @return int[] Array of filtered timestamps.
+     */
+    protected function filter_by_month($eventdates) {
+        if (empty($this->bymonth)) {
+            return $eventdates;
+        }
+
+        $filteredbymonth = [];
+        foreach ($eventdates as $time) {
+            foreach ($this->bymonth as $month) {
+                $prospectmonth = date('n', $time);
+                if ($month == $prospectmonth) {
+                    $filteredbymonth[] = $time;
+                    break;
+                }
             }
         }
+        return $filteredbymonth;
     }
 
     /**
-     * Create events for monthly frequency.
+     * Filter event times based on the BYWEEKNO rule.
      *
-     * @param \stdClass $event Event properties to create event
+     * @param int[] $eventdates Timestamps of event times to be filtered.
+     * @return int[] Array of filtered timestamps.
      */
-    protected function create_monthly_events($event) {
-        // Either bymonthday or byday should be set.
-        if (empty($this->bymonthday) && empty($this->byday)
-                || !empty($this->bymonthday) && !empty($this->byday)) {
-            return;
+    protected function filter_by_weekno($eventdates) {
+        if (empty($this->byweekno)) {
+            return $eventdates;
+        }
+
+        $filteredbyweekno = [];
+        $weeklyinterval = null;
+        foreach ($eventdates as $time) {
+            $tmpdate = new DateTime(date('Y-m-d H:i:s', $time));
+            foreach ($this->byweekno as $weekno) {
+                if ($weekno > 0) {
+                    if ($tmpdate->format('W') == $weekno) {
+                        $filteredbyweekno[] = $time;
+                        break;
+                    }
+                } else if ($weekno < 0) {
+                    if ($weeklyinterval === null) {
+                        $weeklyinterval = new DateInterval('P1W');
+                    }
+                    $weekstart = new DateTime();
+                    $weekstart->setISODate($tmpdate->format('Y'), $weekno);
+                    $weeknext = clone($weekstart);
+                    $weeknext->add($weeklyinterval);
+
+                    $tmptimestamp = $tmpdate->getTimestamp();
+
+                    if ($tmptimestamp >= $weekstart->getTimestamp() && $tmptimestamp < $weeknext->getTimestamp()) {
+                        $filteredbyweekno[] = $time;
+                        break;
+                    }
+                }
+            }
+        }
+        return $filteredbyweekno;
+    }
+
+    /**
+     * Filter event times based on the BYYEARDAY rule.
+     *
+     * @param int[] $eventdates Timestamps of event times to be filtered.
+     * @return int[] Array of filtered timestamps.
+     */
+    protected function filter_by_yearday($eventdates) {
+        if (empty($this->byyearday)) {
+            return $eventdates;
+        }
+
+        $filteredbyyearday = [];
+        foreach ($eventdates as $time) {
+            $tmpdate = new DateTime(date('Y-m-d', $time));
+
+            foreach ($this->byyearday as $yearday) {
+                $dayoffset = abs($yearday) - 1;
+                $dayoffsetinterval = new DateInterval("P{$dayoffset}D");
+
+                if ($yearday > 0) {
+                    $tmpyearday = (int)$tmpdate->format('z') + 1;
+                    if ($tmpyearday == $yearday) {
+                        $filteredbyyearday[] = $time;
+                        break;
+                    }
+                } else if ($yearday < 0) {
+                    $yeardaydate = new DateTime('last day of ' . $tmpdate->format('Y'));
+                    $yeardaydate->sub($dayoffsetinterval);
+
+                    $tmpdate->getTimestamp();
+
+                    if ($yeardaydate->format('z') == $tmpdate->format('z')) {
+                        $filteredbyyearday[] = $time;
+                        break;
+                    }
+                }
+            }
+        }
+        return $filteredbyyearday;
+    }
+
+    /**
+     * Filter event times based on the BYMONTHDAY rule.
+     *
+     * @param int[] $eventdates The event times to be filtered.
+     * @return int[] Array of filtered timestamps.
+     */
+    protected function filter_by_monthday($eventdates) {
+        if (empty($this->bymonthday)) {
+            return $eventdates;
         }
-        // This much seconds after the start of the day.
-        $offset = $event->timestart - mktime(0, 0, 0, date("n", $event->timestart), date("j", $event->timestart), date("Y",
-                $event->timestart));
-        $monthstart = mktime(0, 0, 0, date("n", $event->timestart), 1, date("Y", $event->timestart));
-        if (!empty($this->bymonthday)) {
+
+        $filteredbymonthday = [];
+        foreach ($eventdates as $time) {
+            $eventdatetime = new DateTime(date('Y-m-d', $time));
             foreach ($this->bymonthday as $monthday) {
-                $dayoffset = $monthday - 1; // Number of days we want to add to the first day.
-                if ($monthday == date("j", $event->timestart)) {
-                    // Parent event is a part of this day chain.
-                    $this->create_repeated_events_by_offsets($event, $offset, $dayoffset, $this->interval, 0, $monthstart,
-                        false);
-                } else {
-                    // Parent event is not a part of this day chain.
-                    $this->create_repeated_events_by_offsets($event, $offset, $dayoffset, $this->interval, 0, $monthstart, true);
+                // Days to add/subtract.
+                $daysoffset = abs($monthday) - 1;
+                $dayinterval = new DateInterval("P{$daysoffset}D");
+
+                if ($monthday > 0) {
+                    if ($eventdatetime->format('j') == $monthday) {
+                        $filteredbymonthday[] = $time;
+                        break;
+                    }
+                } else if ($monthday < 0) {
+                    $tmpdate = clone($eventdatetime);
+                    // Reset to the first day of the month.
+                    $tmpdate->modify('first day of this month');
+                    // Then go to last day of the month.
+                    $tmpdate->modify('last day of this month');
+                    if ($daysoffset > 0) {
+                        // Then subtract the monthday value.
+                        $tmpdate->sub($dayinterval);
+                    }
+                    if ($eventdatetime->format('j') == $tmpdate->format('j')) {
+                        $filteredbymonthday[] = $time;
+                        break;
+                    }
                 }
             }
-        } else {
-            foreach ($this->byday as $dayrule) {
-                $day = substr($dayrule, strlen($dayrule) - 2); // Last two chars.
-                $prefix = str_replace($day, '', $dayrule);
-                if (empty($prefix) || !is_numeric($prefix)) {
-                    return;
+        }
+        return $filteredbymonthday;
+    }
+
+    /**
+     * Filter event times based on the BYDAY rule.
+     *
+     * @param stdClass $event The parent event.
+     * @param int[] $eventdates The event times to be filtered.
+     * @param int $until Event times generation limit date.
+     * @return int[] Array of filtered timestamps.
+     */
+    protected function filter_by_day($event, $eventdates, $until) {
+        if (empty($this->byday)) {
+            return $eventdates;
+        }
+
+        $filteredbyday = [];
+
+        $formatter = new NumberFormatter('en_utf8', NumberFormatter::SPELLOUT);
+        $formatter->setTextAttribute(NumberFormatter::DEFAULT_RULESET, "%spellout-ordinal");
+
+        $bounds = $this->get_period_bounds_list($event->timestart, $until);
+
+        $nextmonthinterval = new DateInterval('P1M');
+        foreach ($eventdates as $time) {
+            $tmpdatetime = new DateTime(date('Y-m-d', $time));
+
+            foreach ($this->byday as $day) {
+                $dayname = self::DAYS_OF_WEEK[$day->day];
+
+                // Skip if they day name of the event time does not match the day part of the BYDAY rule.
+                if ($tmpdatetime->format('l') !== $dayname) {
+                    continue;
                 }
-                $day = $this->get_day($day);
-                if ($day == date('l', $event->timestart)) {
-                    // Parent event is a part of this day chain.
-                    $this->create_repeated_events_by_offsets_from_fixedstart($event, $offset, "$prefix $day", $this->interval, 0,
-                        $monthstart, false);
+
+                if (empty($day->value)) {
+                    // No modifier value. Applies to all weekdays of the given period.
+                    $filteredbyday[] = $time;
+                    break;
+                } else if ($day->value > 0) {
+                    // Positive value.
+                    if ($this->freq === self::FREQ_YEARLY && empty($this->bymonth)) {
+                        // Nth week day of the year.
+                        $expecteddate = new DateTime($tmpdatetime->format('Y') . '-01-01');
+                        $count = $day->value;
+                        $expecteddate->modify("+$count $dayname");
+                        if ($expecteddate->format('Y-m-d') === $tmpdatetime->format('Y-m-d')) {
+                            $filteredbyday[] = $time;
+                            break;
+                        }
+                    } else {
+                        // Nth week day of the month.
+                        $expectedmonthyear = $tmpdatetime->format('F Y');
+                        $expectedordinal = $formatter->format($day->value);
+                        $expecteddate = new DateTime("$expectedordinal $dayname of $expectedmonthyear");
+                        if ($expecteddate->format('Y-m-d') === $tmpdatetime->format('Y-m-d')) {
+                            $filteredbyday[] = $time;
+                            break;
+                        }
+                    }
+
                 } else {
-                    // Parent event is not a part of this day chain.
-                    $this->create_repeated_events_by_offsets_from_fixedstart($event, $offset, "$prefix $day", $this->interval, 0,
-                        $monthstart, true);
-                }
+                    // Negative value.
+                    $count = $day->value;
+                    if ($this->freq === self::FREQ_YEARLY && empty($this->bymonth)) {
+                        // The -Nth week day of the year.
+                        $eventyear = (int)$tmpdatetime->format('Y');
+                        // Get temporary DateTime object starting from the first day of the next year.
+                        $expecteddate = new DateTime((++$eventyear) . '-01-01');
+                        while ($count < 0) {
+                            // Get the start of the previous week.
+                            $expecteddate->modify('last ' . $this->wkst);
+                            $tmpexpecteddate = clone($expecteddate);
+                            if ($tmpexpecteddate->format('l') !== $dayname) {
+                                $tmpexpecteddate->modify('next ' . $dayname);
+                            }
+                            if ($this->in_bounds($tmpexpecteddate->getTimestamp(), $bounds)) {
+                                $expecteddate = $tmpexpecteddate;
+                                $count++;
+                            }
+                        }
+                        if ($expecteddate->format('l') !== $dayname) {
+                            $expecteddate->modify('next ' . $dayname);
+                        }
+                        if ($expecteddate->getTimestamp() == $time) {
+                            $filteredbyday[] = $time;
+                            break;
+                        }
+
+                    } else {
+                        // The -Nth week day of the month.
+                        $expectedmonthyear = $tmpdatetime->format('F Y');
+                        $expecteddate = new DateTime("first day of $expectedmonthyear");
+                        $expecteddate->add($nextmonthinterval);
+                        while ($count < 0) {
+                            // Get the start of the previous week.
+                            $expecteddate->modify('last ' . $this->wkst);
+                            $tmpexpecteddate = clone($expecteddate);
+                            if ($tmpexpecteddate->format('l') !== $dayname) {
+                                $tmpexpecteddate->modify('next ' . $dayname);
+                            }
+                            if ($this->in_bounds($tmpexpecteddate->getTimestamp(), $bounds)) {
+                                $expecteddate = $tmpexpecteddate;
+                                $count++;
+                            }
+                        }
 
+                        // Compare the expected date with the event's timestamp.
+                        if ($expecteddate->getTimestamp() == $time) {
+                            $filteredbyday[] = $time;
+                            break;
+                        }
+                    }
+                }
             }
         }
+        return $filteredbyday;
     }
 
     /**
-     * Create events for yearly frequency.
+     * Applies BYHOUR, BYMINUTE and BYSECOND rules to the calculated event dates.
+     * Defaults to the DTSTART's hour/minute/second component when not defined.
      *
-     * @param \stdClass $event Event properties to create event
+     * @param DateTime $eventdatetime The parent event DateTime object pertaining to the DTSTART.
+     * @param int[] $eventdates Array of candidate event date timestamps.
+     * @return array List of updated event timestamps that contain the time component of the event times.
      */
-    protected function create_yearly_events($event) {
+    protected function apply_hour_minute_second_rules(DateTime $eventdatetime, $eventdates) {
+        // If BYHOUR rules are not set, set the hour of the events from the DTSTART's hour component.
+        if (empty($this->byhour)) {
+            $this->byhour = [$eventdatetime->format('G')];
+        }
+        // If BYMINUTE rules are not set, set the hour of the events from the DTSTART's minute component.
+        if (empty($this->byminute)) {
+            $this->byminute = [(int)$eventdatetime->format('i')];
+        }
+        // If BYSECOND rules are not set, set the hour of the events from the DTSTART's second component.
+        if (empty($this->bysecond)) {
+            $this->bysecond = [(int)$eventdatetime->format('s')];
+        }
 
-        // This much seconds after the start of the month.
-        $offset = $event->timestart - mktime(0, 0, 0, date("n", $event->timestart), date("j", $event->timestart), date("Y",
-                $event->timestart));
+        $results = [];
+        foreach ($eventdates as $time) {
+            $datetime = new DateTime(date('Y-m-d', $time));
+            foreach ($this->byhour as $hour) {
+                foreach ($this->byminute as $minute) {
+                    foreach ($this->bysecond as $second) {
+                        $datetime->setTime($hour, $minute, $second);
+                        $results[] = $datetime->getTimestamp();
+                    }
+                }
+            }
+        }
+        return $results;
+    }
 
-        if (empty($this->bymonth)) {
-            // Event's month is taken if not specified.
-            $this->bymonth = array(date("n", $event->timestart));
-        }
-        foreach ($this->bymonth as $month) {
-            if (empty($this->byday)) {
-                // If byday is not present, the rule must represent the same month as the event start date. Basically we only
-                // have to add + $this->interval number of years to get the next event date.
-                if ($month == date("n", $event->timestart)) {
-                    // Parent event is a part of this month chain.
-                    $this->create_repeated_events_by_offsets($event, 0, 0, 0, $this->interval, $event->timestart, false);
+    /**
+     * Filter event times based on the BYSETPOS rule.
+     *
+     * @param stdClass $event The parent event.
+     * @param int[] $eventtimes The event times to be filtered.
+     * @param int $until Event times generation limit date.
+     * @return int[] Array of filtered timestamps.
+     */
+    protected function filter_by_setpos($event, $eventtimes, $until) {
+        if (empty($this->bysetpos)) {
+            return $eventtimes;
+        }
+
+        $filteredbysetpos = [];
+        $boundslist = $this->get_period_bounds_list($event->timestart, $until);
+        sort($eventtimes);
+        foreach ($boundslist as $bounds) {
+            // Generate a list of candidate event times based that are covered in a period's bounds.
+            $prospecttimes = [];
+            foreach ($eventtimes as $time) {
+                if ($time >= $bounds->start && $time < $bounds->next) {
+                    $prospecttimes[] = $time;
                 }
-            } else {
-                $dayrule = reset($this->byday);
-                $day = substr($dayrule, strlen($dayrule) - 2); // Last two chars.
-                $prefix = str_replace($day, '', $dayrule);
-                if (empty($prefix) || !is_numeric($prefix)) {
-                    return;
+            }
+            if (empty($prospecttimes)) {
+                continue;
+            }
+            // Add the event times that correspond to the set position rule into the filtered results.
+            foreach ($this->bysetpos as $pos) {
+                $tmptimes = $prospecttimes;
+                if ($pos < 0) {
+                    rsort($tmptimes);
                 }
-                $day = $this->get_day($day);
-                $monthstart = mktime(0, 0, 0, $month, 1, date("Y", $event->timestart));
-                if ($day == date('l', $event->timestart)) {
-                    // Parent event is a part of this day chain.
-                    $this->create_repeated_events_by_offsets_from_fixedstart($event, $offset, "$prefix $day", 0,
-                            $this->interval, $monthstart, false);
-                } else {
-                    // Parent event is not a part of this day chain.
-                    $this->create_repeated_events_by_offsets_from_fixedstart($event, $offset, "$prefix $day", 0,
-                        $this->interval, $monthstart, true);
+                $index = abs($pos) - 1;
+                if (isset($tmptimes[$index])) {
+                    $filteredbysetpos[] = $tmptimes[$index];
                 }
             }
         }
+        return $filteredbysetpos;
+    }
+
+    /**
+     * Gets the list of period boundaries covered by the recurring events.
+     *
+     * @param int $eventtime The event timestamp.
+     * @param int $until The end timestamp.
+     * @return array List of period bounds, with start and next properties.
+     */
+    protected function get_period_bounds_list($eventtime, $until) {
+        $interval = $this->get_interval();
+        $periodbounds = $this->get_period_boundaries($eventtime);
+        $periodstart = $periodbounds['start'];
+        $periodafter = $periodbounds['next'];
+        $bounds = [];
+        if ($until !== null) {
+            while ($periodstart->getTimestamp() < $until) {
+                $bounds[] = (object)[
+                    'start' => $periodstart->getTimestamp(),
+                    'next' => $periodafter->getTimestamp()
+                ];
+                $periodstart->add($interval);
+                $periodafter->add($interval);
+            }
+        } else {
+            $count = $this->count;
+            while ($count > 0) {
+                $bounds[] = (object)[
+                    'start' => $periodstart->getTimestamp(),
+                    'next' => $periodafter->getTimestamp()
+                ];
+                $periodstart->add($interval);
+                $periodafter->add($interval);
+                $count--;
+            }
+        }
+
+        return $bounds;
+    }
+
+    /**
+     * Determine whether the date-time in question is within the bounds of the periods that are covered by the RRULE.
+     *
+     * @param int $time The timestamp to be evaluated.
+     * @param array $bounds Array of period boundaries covered by the RRULE.
+     * @return bool
+     */
+    protected function in_bounds($time, $bounds) {
+        foreach ($bounds as $bound) {
+            if ($time >= $bound->start && $time < $bound->next) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Determines the start and end DateTime objects that serve as references to determine whether a calculated event timestamp
+     * falls on the period defined by these DateTimes objects.
+     *
+     * @param int $eventtime Unix timestamp of the event time.
+     * @return DateTime[]
+     * @throws moodle_exception
+     */
+    protected function get_period_boundaries($eventtime) {
+        $nextintervalspec = null;
+
+        switch ($this->freq) {
+            case self::FREQ_YEARLY:
+                $nextintervalspec = 'P1Y';
+                $timestart = date('Y-01-01', $eventtime);
+                break;
+            case self::FREQ_MONTHLY:
+                $nextintervalspec = 'P1M';
+                $timestart = date('Y-m-01', $eventtime);
+                break;
+            case self::FREQ_WEEKLY:
+                $nextintervalspec = 'P1W';
+                if (date('l', $eventtime) === $this->wkst) {
+                    $weekstarttime = $eventtime;
+                } else {
+                    $weekstarttime = strtotime('last ' . $this->wkst, $eventtime);
+                }
+                $timestart = date('Y-m-d', $weekstarttime);
+                break;
+            case self::FREQ_DAILY:
+                $nextintervalspec = 'P1D';
+                $timestart = date('Y-m-d', $eventtime);
+                break;
+            case self::FREQ_HOURLY:
+                $nextintervalspec = 'PT1H';
+                $timestart = date('Y-m-d H:00:00', $eventtime);
+                break;
+            case self::FREQ_MINUTELY:
+                $nextintervalspec = 'PT1M';
+                $timestart = date('Y-m-d H:i:00', $eventtime);
+                break;
+            case self::FREQ_SECONDLY:
+                $nextintervalspec = 'PT1S';
+                $timestart = date('Y-m-d H:i:s', $eventtime);
+                break;
+            default:
+                // We should never get here, something is very wrong.
+                throw new moodle_exception('errorrrulefreq', 'calendar');
+        }
+
+        $eventstart = new DateTime($timestart);
+        $eventnext = clone($eventstart);
+        $nextinterval = new DateInterval($nextintervalspec);
+        $eventnext->add($nextinterval);
+
+        return [
+            'start' => $eventstart,
+            'next' => $eventnext,
+        ];
     }
-}
\ No newline at end of file
+}
diff --git a/calendar/tests/rrule_manager_test.php b/calendar/tests/rrule_manager_test.php
new file mode 100644 (file)
index 0000000..986b3ec
--- /dev/null
@@ -0,0 +1,2716 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines test class to test manage rrule during ical imports.
+ *
+ * @package core_calendar
+ * @category test
+ * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+use core_calendar\rrule_manager;
+
+/**
+ * Defines test class to test manage rrule during ical imports.
+ *
+ * @package core_calendar
+ * @category test
+ * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_rrule_manager_testcase extends advanced_testcase {
+
+    /** @var calendar_event a dummy event */
+    protected $event;
+
+    /**
+     * Set up method.
+     */
+    protected function setUp() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Set our timezone based on the timezone in the RFC's samples (US/Eastern).
+        $tz = 'US/Eastern';
+        $this->setTimezone($tz);
+        $timezone = new DateTimeZone($tz);
+        // Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
+        $time = DateTime::createFromFormat('Ymd\THis', '19970902T090000', $timezone);
+        $timestart = $time->getTimestamp();
+
+        $user = $this->getDataGenerator()->create_user();
+        $sub = new stdClass();
+        $sub->url = '';
+        $sub->courseid = 0;
+        $sub->groupid = 0;
+        $sub->userid = $user->id;
+        $sub->pollinterval = 0;
+        $subid = $DB->insert_record('event_subscriptions', $sub, true);
+
+        $event = new stdClass();
+        $event->name = 'Event name';
+        $event->description = '';
+        $event->timestart = $timestart;
+        $event->timeduration = 3600;
+        $event->uuid = 'uuid';
+        $event->subscriptionid = $subid;
+        $event->userid = $user->id;
+        $event->groupid = 0;
+        $event->courseid = 0;
+        $event->eventtype = 'user';
+        $eventobj = calendar_event::create($event, false);
+        $DB->set_field('event', 'repeatid', $eventobj->id, array('id' => $eventobj->id));
+        $eventobj->repeatid = $eventobj->id;
+        $this->event = $eventobj;
+    }
+
+    /**
+     * Test parse_rrule() method.
+     */
+    public function test_parse_rrule() {
+        $rules = [
+            'FREQ=YEARLY',
+            'COUNT=3',
+            'INTERVAL=4',
+            'BYSECOND=20,40',
+            'BYMINUTE=2,30',
+            'BYHOUR=3,4',
+            'BYDAY=MO,TH',
+            'BYMONTHDAY=20,30',
+            'BYYEARDAY=300,-20',
+            'BYWEEKNO=22,33',
+            'BYMONTH=3,4'
+        ];
+        $rrule = implode(';', $rules);
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+
+        $bydayrules = [
+            (object)[
+                'day' => 'MO',
+                'value' => 0
+            ],
+            (object)[
+                'day' => 'TH',
+                'value' => 0
+            ],
+        ];
+
+        $props = [
+            'freq' => rrule_manager::FREQ_YEARLY,
+            'count' => 3,
+            'interval' => 4,
+            'bysecond' => [20, 40],
+            'byminute' => [2, 30],
+            'byhour' => [3, 4],
+            'byday' => $bydayrules,
+            'bymonthday' => [20, 30],
+            'byyearday' => [300, -20],
+            'byweekno' => [22, 33],
+            'bymonth' => [3, 4],
+        ];
+
+        $reflectionclass = new ReflectionClass($mang);
+        foreach ($props as $prop => $expectedval) {
+            $rcprop = $reflectionclass->getProperty($prop);
+            $rcprop->setAccessible(true);
+            $this->assertEquals($expectedval, $rcprop->getValue($mang));
+        }
+    }
+
+    /**
+     * Test exception is thrown for invalid property.
+     */
+    public function test_parse_rrule_validation() {
+        $rrule = "RANDOM=PROPERTY;";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test exception is thrown for invalid frequency.
+     */
+    public function test_freq_validation() {
+        $rrule = "FREQ=RANDOMLY;";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of rules with both COUNT and UNTIL parameters.
+     */
+    public function test_until_count_validation() {
+        $until = $this->event->timestart + DAYSECS * 4;
+        $until = date('Y-m-d', $until);
+        $rrule = "FREQ=DAILY;COUNT=2;UNTIL=$until";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of INTERVAL rule.
+     */
+    public function test_interval_validation() {
+        $rrule = "INTERVAL=0";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYSECOND rule.
+     */
+    public function test_bysecond_validation() {
+        $rrule = "BYSECOND=30,45,60";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMINUTE rule.
+     */
+    public function test_byminute_validation() {
+        $rrule = "BYMINUTE=30,45,60";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMINUTE rule.
+     */
+    public function test_byhour_validation() {
+        $rrule = "BYHOUR=23,45";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYDAY rule.
+     */
+    public function test_byday_validation() {
+        $rrule = "BYDAY=MO,2SE";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYDAY rule with prefixes.
+     */
+    public function test_byday_with_prefix_validation() {
+        // This is acceptable.
+        $rrule = "FREQ=MONTHLY;BYDAY=-1MO,2SA";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+
+        // This is also acceptable.
+        $rrule = "FREQ=YEARLY;BYDAY=MO,2SA";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+
+        // This is invalid.
+        $rrule = "FREQ=WEEKLY;BYDAY=MO,2SA";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMONTHDAY rule.
+     */
+    public function test_bymonthday_upper_bound_validation() {
+        $rrule = "BYMONTHDAY=1,32";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMONTHDAY rule.
+     */
+    public function test_bymonthday_0_validation() {
+        $rrule = "BYMONTHDAY=1,0";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMONTHDAY rule.
+     */
+    public function test_bymonthday_lower_bound_validation() {
+        $rrule = "BYMONTHDAY=1,-31,-32";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYYEARDAY rule.
+     */
+    public function test_byyearday_upper_bound_validation() {
+        $rrule = "BYYEARDAY=1,366,367";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYYEARDAY rule.
+     */
+    public function test_byyearday_0_validation() {
+        $rrule = "BYYEARDAY=0";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYYEARDAY rule.
+     */
+    public function test_byyearday_lower_bound_validation() {
+        $rrule = "BYYEARDAY=-1,-366,-367";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYWEEKNO rule.
+     */
+    public function test_non_yearly_freq_with_byweekno() {
+        $rrule = "BYWEEKNO=1,53";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYWEEKNO rule.
+     */
+    public function test_byweekno_upper_bound_validation() {
+        $rrule = "FREQ=YEARLY;BYWEEKNO=1,53,54";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYWEEKNO rule.
+     */
+    public function test_byweekno_0_validation() {
+        $rrule = "FREQ=YEARLY;BYWEEKNO=0";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYWEEKNO rule.
+     */
+    public function test_byweekno_lower_bound_validation() {
+        $rrule = "FREQ=YEARLY;BYWEEKNO=-1,-53,-54";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMONTH rule.
+     */
+    public function test_bymonth_upper_bound_validation() {
+        $rrule = "BYMONTH=1,12,13";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYMONTH rule.
+     */
+    public function test_bymonth_lower_bound_validation() {
+        $rrule = "BYMONTH=0";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYSETPOS rule.
+     */
+    public function test_bysetpos_without_other_byrules() {
+        $rrule = "BYSETPOS=1,366";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYSETPOS rule.
+     */
+    public function test_bysetpos_upper_bound_validation() {
+        $rrule = "BYSETPOS=1,366,367";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYSETPOS rule.
+     */
+    public function test_bysetpos_0_validation() {
+        $rrule = "BYSETPOS=0";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test parsing of BYSETPOS rule.
+     */
+    public function test_bysetpos_lower_bound_validation() {
+        $rrule = "BYSETPOS=-1,-366,-367";
+        $mang = new rrule_manager($rrule);
+        $this->expectException('moodle_exception');
+        $mang->parse_rrule();
+    }
+
+    /**
+     * Test recurrence rules for daily frequency.
+     */
+    public function test_daily_events() {
+        global $DB;
+
+        $rrule = 'FREQ=DAILY;COUNT=3'; // This should generate 2 child events + 1 parent.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(3, $count);
+        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                'timestart' => ($this->event->timestart + DAYSECS)));
+        $this->assertTrue($result);
+        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                'timestart' => ($this->event->timestart + 2 * DAYSECS)));
+        $this->assertTrue($result);
+
+        $until = $this->event->timestart + DAYSECS * 2;
+        $until = date('Y-m-d', $until);
+        $rrule = "FREQ=DAILY;UNTIL=$until"; // This should generate 1 child event + 1 parent,since by then until bound would be hit.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(2, $count);
+        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                'timestart' => ($this->event->timestart + DAYSECS)));
+        $this->assertTrue($result);
+
+        $rrule = 'FREQ=DAILY;COUNT=3;INTERVAL=3'; // This should generate 2 child events + 1 parent, every 3rd day.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(3, $count);
+        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                'timestart' => ($this->event->timestart + 3 * DAYSECS)));
+        $this->assertTrue($result);
+        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                'timestart' => ($this->event->timestart + 6 * DAYSECS)));
+        $this->assertTrue($result);
+    }
+
+    /**
+     * Every 300 days, forever.
+     */
+    public function test_every_300_days_forever() {
+        global $DB;
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+
+        $interval = new DateInterval('P300D');
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P10Y'));
+        $until = $untildate->getTimestamp();
+
+        // Forever event. This should generate events for time() + 10 year period, every 300 days.
+        $rrule = 'FREQ=DAILY;INTERVAL=300';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $records = $DB->get_records('event', array('repeatid' => $this->event->id));
+
+        $expecteddate = clone($startdatetime);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($until, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next iteration.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Test recurrence rules for weekly frequency.
+     */
+    public function test_weekly_events() {
+        global $DB;
+
+        $rrule = 'FREQ=WEEKLY;COUNT=1';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(1, $count);
+        for ($i = 0; $i < $count; $i++) {
+            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                    'timestart' => ($this->event->timestart + $i * DAYSECS)));
+            $this->assertTrue($result);
+        }
+        // This much seconds after the start of the day.
+        $offset = $this->event->timestart - mktime(0, 0, 0, date("n", $this->event->timestart), date("j", $this->event->timestart),
+                date("Y", $this->event->timestart));
+
+        // This should generate 4 weekly Monday events.
+        $until = $this->event->timestart + WEEKSECS * 4;
+        $until = date('Ymd\This\Z', $until);
+        $rrule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=$until";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(4, $count);
+        $timestart = $this->event->timestart;
+        for ($i = 0; $i < $count; $i++) {
+            $timestart = strtotime("+$offset seconds next Monday", $timestart);
+            $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $timestart));
+            $this->assertTrue($result);
+        }
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P3W');
+
+        // Every 3 weeks on Monday, Wednesday for 2 times.
+        $rrule = 'FREQ=WEEKLY;INTERVAL=3;BYDAY=MO,WE;COUNT=2';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(2, $records);
+
+        $expecteddate = clone($startdate);
+        $expecteddate->modify('1997-09-03');
+        foreach ($records as $record) {
+            $expecteddate->add($offsetinterval);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            if (date('D', $record->timestart) === 'Mon') {
+                // Go to the fifth day of this month.
+                $expecteddate->modify('next Wednesday');
+            } else {
+                // Reset to Monday.
+                $expecteddate->modify('last Monday');
+                // Go to next period.
+                $expecteddate->add($interval);
+            }
+        }
+
+        // Forever event. This should generate events over time() + 10 year period, every 50th Monday.
+        $rrule = 'FREQ=WEEKLY;BYDAY=MO;INTERVAL=50';
+
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P10Y'));
+        $until = $untildate->getTimestamp();
+
+        $interval = new DateInterval('P50W');
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        // First instance of this set of recurring events: Monday, 17-08-1998.
+        $expecteddate = clone($startdate);
+        $expecteddate->modify('1998-08-17');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $eventdateexpected = $expecteddate->format('Y-m-d H:i:s');
+            $eventdateactual = date('Y-m-d H:i:s', $record->timestart);
+            $this->assertEquals($eventdateexpected, $eventdateactual);
+
+            $expecteddate->add($interval);
+            $this->assertLessThanOrEqual($until, $record->timestart);
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with COUNT and BYMONTHDAY rules set.
+     */
+    public function test_monthly_events_with_count_bymonthday() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $interval = new DateInterval('P1M');
+
+        $rrule = "FREQ=MONTHLY;COUNT=3;BYMONTHDAY=2"; // This should generate 3 events in total.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $records = $DB->get_records('event', array('repeatid' => $this->event->id));
+        $this->assertCount(3, $records);
+
+        $expecteddate = clone($startdatetime);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next month.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
+     */
+    public function test_monthly_events_with_until_bymonthday() {
+        global $DB;
+
+        // This should generate 10 child event + 1 parent, since by then until bound would be hit.
+        $until = strtotime('+1 day +10 months', $this->event->timestart);
+        $until = date('Ymd\This\Z', $until);
+        $rrule = "FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=$until";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', ['repeatid' => $this->event->id]);
+        $this->assertEquals(11, $count);
+        for ($i = 0; $i < 11; $i++) {
+            $time = strtotime("+$i month", $this->event->timestart);
+            $result = $DB->record_exists('event', ['repeatid' => $this->event->id, 'timestart' => $time]);
+            $this->assertTrue($result);
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
+     */
+    public function test_monthly_events_with_until_bymonthday_multi() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P2M');
+        $untildate = clone($startdatetime);
+        $untildate->add(new DateInterval('P10M10D'));
+        $until = $untildate->format('Ymd\This\Z');
+
+        // This should generate 11 child event + 1 parent, since by then until bound would be hit.
+        $rrule = "FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=2,5;UNTIL=$until";
+
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(12, $records);
+
+        $expecteddate = clone($startdate);
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            if (date('j', $record->timestart) == 2) {
+                // Go to the fifth day of this month.
+                $expecteddate->add(new DateInterval('P3D'));
+            } else {
+                // Reset date to the first day of the month.
+                $expecteddate->modify('first day of this month');
+                // Go to next month period.
+                $expecteddate->add($interval);
+                // Go to the second day of the next month period.
+                $expecteddate->modify('+1 day');
+            }
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY forever.
+     */
+    public function test_monthly_events_with_bymonthday_forever() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P12M');
+
+        // Forever event. This should generate events over 10 year period, on 2nd day of every 12th month.
+        $rrule = "FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2";
+
+        $mang = new rrule_manager($rrule);
+        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
+
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $expecteddate = clone($startdate);
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($until, $record->timestart);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Reset date to the first day of the month.
+            $expecteddate->modify('first day of this month');
+            // Go to next month period.
+            $expecteddate->add($interval);
+            // Go to the second day of the next month period.
+            $expecteddate->modify('+1 day');
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with COUNT and BYDAY rules set.
+     */
+    public function test_monthly_events_with_count_byday() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P1M');
+
+        $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=1MO'; // This should generate 3 events in total, first monday of the month.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        // First occurrence of this set of recurring events: 06-10-1997.
+        $expecteddate = clone($startdate);
+        $expecteddate->modify('1997-10-06');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next month period.
+            $expecteddate->add($interval);
+            $expecteddate->modify('first Monday of this month');
+            $expecteddate->add($offsetinterval);
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with BYDAY and UNTIL rules set.
+     */
+    public function test_monthly_events_with_until_byday() {
+        global $DB;
+
+        // This much seconds after the start of the day.
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $untildate = clone($startdatetime);
+        $untildate->add(new DateInterval('P10M1D'));
+        $until = $untildate->format('Ymd\This\Z');
+
+        // This rule should generate 9 events in total from first Monday of October 1997 to first Monday of June 1998.
+        $rrule = "FREQ=MONTHLY;BYDAY=1MO;UNTIL=$until";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(9, $records);
+
+        $expecteddate = clone($startdate);
+        $expecteddate->modify('first Monday of October 1997');
+        foreach ($records as $record) {
+            $expecteddate->add($offsetinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next month.
+            $expecteddate->modify('first day of next month');
+            // Go to the first Monday of the next month.
+            $expecteddate->modify('first Monday of this month');
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with BYMONTHDAY and UNTIL rules set.
+     */
+    public function test_monthly_events_with_until_byday_multi() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P2M');
+
+        $untildate = clone($startdatetime);
+        $untildate->add(new DateInterval('P10M20D'));
+        $until = $untildate->format('Ymd\This\Z');
+
+        // This should generate 11 events from 17 Sep 1997 to 15 Jul 1998.
+        $rrule = "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,3WE;UNTIL=$until";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(11, $records);
+
+        $expecteddate = clone($startdate);
+        $expecteddate->modify('1997-09-17');
+        foreach ($records as $record) {
+            $expecteddate->add($offsetinterval);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            if (date('D', $record->timestart) === 'Mon') {
+                // Go to the fifth day of this month.
+                $expecteddate->modify('third Wednesday of this month');
+            } else {
+                // Go to next month period.
+                $expecteddate->add($interval);
+                $expecteddate->modify('first Monday of this month');
+            }
+        }
+    }
+
+    /**
+     * Test recurrence rules for monthly frequency for RRULE with BYDAY forever.
+     */
+    public function test_monthly_events_with_byday_forever() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P12M');
+
+        // Forever event. This should generate events over 10 year period, on 2nd day of every 12th month.
+        $rrule = "FREQ=MONTHLY;INTERVAL=12;BYDAY=1MO";
+
+        $mang = new rrule_manager($rrule);
+        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
+
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $expecteddate = new DateTime('first Monday of September 1998');
+        foreach ($records as $record) {
+            $expecteddate->add($offsetinterval);
+            $this->assertLessThanOrEqual($until, $record->timestart);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next month period.
+            $expecteddate->add($interval);
+            // Reset date to the first Monday of the month.
+            $expecteddate->modify('first Monday of this month');
+        }
+    }
+
+    /**
+     * Test recurrence rules for yearly frequency.
+     */
+    public function test_yearly_events() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P1Y');
+
+        $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9"; // This should generate 3 events in total.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(3, $records);
+
+        $expecteddate = clone($startdatetime);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+
+        // Create a yearly event, until the time limit is hit.
+        $until = strtotime('+20 day +10 years', $this->event->timestart);
+        $until = date('Ymd\THis\Z', $until);
+        $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until"; // Forever event.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(11, $count);
+        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
+            $time = strtotime("+$yoffset years", $this->event->timestart)) {
+            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                    'timestart' => ($time)));
+            $this->assertTrue($result);
+        }
+
+        // This should generate 5 events in total, every second year in the given month of the event.
+        $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
+        $this->assertEquals(5, $count);
+        for ($i = 0, $time = $this->event->timestart; $i < 5; $i++, $yoffset = $i * 2,
+            $time = strtotime("+$yoffset years", $this->event->timestart)) {
+            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                    'timestart' => ($time)));
+            $this->assertTrue($result);
+        }
+
+        $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2"; // Forever event.
+        $mang = new rrule_manager($rrule);
+        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
+            $time = strtotime("+$yoffset years", $this->event->timestart)) {
+            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
+                    'timestart' => ($time)));
+            $this->assertTrue($result);
+        }
+
+        $rrule = "FREQ=YEARLY;COUNT=3;BYMONTH=9;BYDAY=1MO"; // This should generate 3 events in total.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(3, $records);
+
+        $expecteddate = clone($startdatetime);
+        $expecteddate->modify('first Monday of September 1998');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+            $monthyear = $expecteddate->format('F Y');
+            $expecteddate->modify('first Monday of ' . $monthyear);
+            $expecteddate->add($offsetinterval);
+        }
+
+        // Create a yearly event on the specified month, until the time limit is hit.
+        $untildate = clone($startdatetime);
+        $untildate->add(new DateInterval('P10Y20D'));
+        $until = $untildate->format('Ymd\THis\Z');
+
+        $rrule = "FREQ=YEARLY;BYMONTH=9;UNTIL=$until;BYDAY=1MO";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        // 10 yearly records every first Monday of September 1998 to first Monday of September 2007.
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(10, $records);
+
+        $expecteddate = clone($startdatetime);
+        $expecteddate->modify('first Monday of September 1998');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+            $monthyear = $expecteddate->format('F Y');
+            $expecteddate->modify('first Monday of ' . $monthyear);
+            $expecteddate->add($offsetinterval);
+        }
+
+        // This should generate 5 events in total, every second year in the month of September.
+        $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;COUNT=5;BYDAY=1MO";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        // 5 bi-yearly records every first Monday of September 1998 to first Monday of September 2007.
+        $interval = new DateInterval('P2Y');
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(5, $records);
+
+        $expecteddate = clone($startdatetime);
+        $expecteddate->modify('first Monday of September 1999');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untildate->getTimestamp(), $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+            $monthyear = $expecteddate->format('F Y');
+            $expecteddate->modify('first Monday of ' . $monthyear);
+            $expecteddate->add($offsetinterval);
+        }
+    }
+
+    /**
+     * Test for rrule with FREQ=YEARLY with BYMONTH and BYDAY rules set, recurring forever.
+     */
+    public function test_yearly_bymonth_byday_forever() {
+        global $DB;
+
+        // Every 2 years on the first Monday of September.
+        $rrule = "FREQ=YEARLY;BYMONTH=9;INTERVAL=2;BYDAY=1MO";
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $interval = new DateInterval('P2Y');
+
+        // First occurrence of this set of events is on the first Monday of September 1999.
+        $expecteddate = clone($startdatetime);
+        $expecteddate->modify('first Monday of September 1999');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+            $monthyear = $expecteddate->format('F Y');
+            $expecteddate->modify('first Monday of ' . $monthyear);
+            $expecteddate->add($offsetinterval);
+        }
+    }
+
+    /**
+     * Test for rrule with FREQ=YEARLY recurring forever.
+     */
+    public function test_yearly_forever() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+
+        $interval = new DateInterval('P2Y');
+
+        $rrule = 'FREQ=YEARLY;INTERVAL=2'; // Forever event.
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = clone($startdatetime);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /******************************************************************************************************************************/
+    /* Tests based on the examples from the RFC.                                                                                  */
+    /******************************************************************************************************************************/
+
+    /**
+     * Daily for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=DAILY;COUNT=10
+     *   ==> (1997 9:00 AM EDT)September 2-11
+     */
+    public function test_daily_count() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $interval = new DateInterval('P1D');
+
+        $rrule = 'FREQ=DAILY;COUNT=10';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(10, $records);
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Daily until December 24, 1997:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=DAILY;UNTIL=19971224T000000Z
+     *   ==> (1997 9:00 AM EDT)September 2-30;October 1-25
+     *       (1997 9:00 AM EST)October 26-31;November 1-30;December 1-23
+     */
+    public function test_daily_until() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $interval = new DateInterval('P1D');
+
+        $untildate = new DateTime('19971224T000000Z');
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $rrule = 'FREQ=DAILY;UNTIL=19971224T000000Z';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 113 daily events from 02-09-1997 to 23-12-1997.
+        $this->assertCount(113, $records);
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Every other day - forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=DAILY;INTERVAL=2
+     *   ==> (1997 9:00 AM EDT)September2,4,6,8...24,26,28,30;October 2,4,6...20,22,24
+     *       (1997 9:00 AM EST)October 26,28,30;November 1,3,5,7...25,27,29;Dec 1,3,...
+     */
+    public function test_every_other_day_forever() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $interval = new DateInterval('P2D');
+
+        $rrule = 'FREQ=DAILY;INTERVAL=2';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Every 10 days, 5 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5
+     *   ==> (1997 9:00 AM EDT)September 2,12,22;October 2,12
+     */
+    public function test_every_10_days_5_count() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $interval = new DateInterval('P10D');
+
+        $rrule = 'FREQ=DAILY;INTERVAL=10;COUNT=5';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(5, $records);
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Everyday in January, for 3 years:
+     *
+     * DTSTART;TZID=US-Eastern:19980101T090000
+     * RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
+     *   ==> (1998 9:00 AM EDT)January 1-31
+     *       (1999 9:00 AM EDT)January 1-31
+     *       (2000 9:00 AM EDT)January 1-31
+     */
+    public function test_everyday_in_jan_for_3_years_yearly() {
+        global $DB;
+
+        // Change our event's date to 01-01-1998, based on the example from the RFC.
+        $this->change_event_startdate('19980101T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 92 events from 01-01-1998 to 03-01-2000.
+        $this->assertCount(92, $records);
+
+        $untildate = new DateTime('20000131T090000Z');
+        $untiltimestamp = $untildate->getTimestamp();
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            // Assert that the event's date is in January.
+            $this->assertEquals('January', date('F', $record->timestart));
+        }
+    }
+
+    /**
+     * Everyday in January, for 3 years:
+     *
+     * DTSTART;TZID=US-Eastern:19980101T090000
+     * RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1
+     *   ==> (1998 9:00 AM EDT)January 1-31
+     *       (1999 9:00 AM EDT)January 1-31
+     *       (2000 9:00 AM EDT)January 1-31
+     */
+    public function test_everyday_in_jan_for_3_years_daily() {
+        global $DB;
+
+        // Change our event's date to 01-01-1998, based on the example from the RFC.
+        $this->change_event_startdate('19980101T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 92 events from 01-01-1998 to 03-01-2000.
+        $this->assertCount(92, $records);
+
+        $untildate = new DateTime('20000131T090000Z');
+        $untiltimestamp = $untildate->getTimestamp();
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            // Assert that the event's date is in January.
+            $this->assertEquals('January', date('F', $record->timestart));
+        }
+    }
+
+    /**
+     * Weekly for 10 occurrences
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;COUNT=10
+     *   ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21
+     *       (1997 9:00 AM EST)October 28;November 4
+     */
+    public function test_weekly_10_count() {
+        global $DB;
+
+        $interval = new DateInterval('P1W');
+
+        $rrule = 'FREQ=WEEKLY;COUNT=10';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(10, $records);
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Weekly until December 24, 1997.
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z
+     *   ==> (1997 9:00 AM EDT)September 2,9,16,23,30;October 7,14,21,28
+     *       (1997 9:00 AM EST)November 4,11,18,25;December 2,9,16,23
+     */
+    public function test_weekly_until_24_dec_1997() {
+        global $DB;
+
+        $interval = new DateInterval('P1W');
+
+        $rrule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
+        $this->assertCount(17, $records);
+
+        $untildate = new DateTime('19971224T000000Z');
+        $untiltimestamp = $untildate->getTimestamp();
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Every other week - forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU
+     *   ==> (1997 9:00 AM EDT)September 2,16,30;October 14
+     *       (1997 9:00 AM EST)October 28;November 11,25;December 9,23
+     *       (1998 9:00 AM EST)January 6,20;February
+     *        ...
+     */
+    public function test_every_other_week_forever() {
+        global $DB;
+
+        $interval = new DateInterval('P2W');
+
+        $rrule = 'FREQ=WEEKLY;INTERVAL=2;WKST=SU';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            $expecteddate->add($interval);
+        }
+    }
+
+    /**
+     * Weekly on Tuesday and Thursday for 5 weeks:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH
+     *   ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
+     */
+    public function test_weekly_on_tue_thu_for_5_weeks_by_until() {
+        global $DB;
+
+        $rrule = 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
+        $this->assertCount(10, $records);
+
+        $untildate = new DateTime('19971007T000000Z');
+        $untiltimestamp = $untildate->getTimestamp();
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime($expecteddate->format('Y-m-d'));
+        $offset = $expecteddate->diff($startdate, true);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) {
+                $expecteddate->modify('next Thursday');
+            } else {
+                $expecteddate->modify('next Tuesday');
+            }
+            $expecteddate->add($offset);
+        }
+    }
+
+    /**
+     * Weekly on Tuesday and Thursday for 5 weeks:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH
+     *   ==> (1997 9:00 AM EDT)September 2,4,9,11,16,18,23,25,30;October 2
+     */
+    public function test_weekly_on_tue_thu_for_5_weeks_by_count() {
+        global $DB;
+
+        $rrule = 'FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 17 iterations from 02-09-1997 13:00 UTC to 23-12-1997 13:00 UTC.
+        $this->assertCount(10, $records);
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime($expecteddate->format('Y-m-d'));
+        $offset = $expecteddate->diff($startdate, true);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            // Go to next period.
+            if ($expecteddate->format('l') === rrule_manager::DAY_TUESDAY) {
+                $expecteddate->modify('next Thursday');
+            } else {
+                $expecteddate->modify('next Tuesday');
+            }
+            $expecteddate->add($offset);
+        }
+    }
+
+    /**
+     * Every other week on Monday, Wednesday and Friday until December 24, 1997, but starting on Tuesday, September 2, 1997:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR
+     *   ==> (1997 9:00 AM EDT)September 3,5,15,17,19,29;October 1,3,13,15,17
+     *       (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28;December 8,10,12,22
+     */
+    public function test_every_other_week_until_24_dec_1997_byday() {
+        global $DB;
+
+        $rrule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // 24 iterations every M-W-F from 03-09-1997 13:00 UTC to 22-12-1997 13:00 UTC.
+        $this->assertCount(24, $records);
+
+        $untildate = new DateTime('19971224T000000Z');
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        // First occurrence of this set of events is on 3 September 1999.
+        $expecteddate = clone($startdatetime);
+        $expecteddate->modify('next Wednesday');
+        $expecteddate->add($offsetinterval);
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            switch ($expecteddate->format('l')) {
+                case rrule_manager::DAY_MONDAY:
+                    $expecteddate->modify('next Wednesday');
+                    break;
+                case rrule_manager::DAY_WEDNESDAY:
+                    $expecteddate->modify('next Friday');
+                    break;
+                default:
+                    $expecteddate->modify('next Monday');
+                    // Increment expected date by 1 week if the next day is Monday.
+                    $expecteddate->add(new DateInterval('P1W'));
+                    break;
+            }
+            $expecteddate->add($offsetinterval);
+        }
+    }
+
+    /**
+     * Every other week on Tuesday and Thursday, for 8 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH
+     *   ==> (1997 9:00 AM EDT)September 2,4,16,18,30;October 2,14,16
+     */
+    public function test_every_other_week_byday_8_count() {
+        global $DB;
+
+        $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should correspond to COUNT rule.
+        $this->assertCount(8, $records);
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        // First occurrence of this set of events is on 2 September 1999.
+        $expecteddate = clone($startdatetime);
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            switch ($expecteddate->format('l')) {
+                case rrule_manager::DAY_TUESDAY:
+                    $expecteddate->modify('next Thursday');
+                    break;
+                default:
+                    $expecteddate->modify('next Tuesday');
+                    // Increment expected date by 1 week if the next day is Tuesday.
+                    $expecteddate->add(new DateInterval('P1W'));
+                    break;
+            }
+            $expecteddate->add($offsetinterval);
+        }
+    }
+
+    /**
+     * Monthly on the 1st Friday for ten occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970905T090000
+     * RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR
+     *   ==> (1997 9:00 AM EDT)September 5;October 3
+     *       (1997 9:00 AM EST)November 7;Dec 5
+     *       (1998 9:00 AM EST)January 2;February 6;March 6;April 3
+     *       (1998 9:00 AM EDT)May 1;June 5
+     */
+    public function test_monthly_every_first_friday_10_count() {
+        global $DB;
+
+        // Change our event's date to 05-09-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;COUNT=10;BYDAY=1FR';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should correspond to COUNT rule.
+        $this->assertCount(10, $records);
+
+        foreach ($records as $record) {
+            // Get the first Friday of the record's month.
+            $recordmonthyear = date('F Y', $record->timestart);
+            $expecteddate = new DateTime('first Friday of ' . $recordmonthyear);
+            // Add the time of the event.
+            $expecteddate->add($offsetinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+        }
+    }
+
+    /**
+     * Monthly on the 1st Friday until December 24, 1997:
+     *
+     * DTSTART;TZID=US-Eastern:19970905T090000
+     * RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR
+     *   ==> (1997 9:00 AM EDT)September 5;October 3
+     *       (1997 9:00 AM EST)November 7;December 5
+     */
+    public function test_monthly_every_first_friday_until() {
+        global $DB;
+
+        // Change our event's date to 05-09-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 4 events, every first friday of September 1997 to December 1997.
+        $this->assertCount(4, $records);
+
+        foreach ($records as $record) {
+            // Get the first Friday of the record's month.
+            $recordmonthyear = date('F Y', $record->timestart);
+            $expecteddate = new DateTime('first Friday of ' . $recordmonthyear);
+            // Add the time of the event.
+            $expecteddate->add($offsetinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+        }
+    }
+
+    /**
+     * Every other month on the 1st and last Sunday of the month for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970907T090000
+     * RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
+     *   ==> (1997 9:00 AM EDT)September 7,28
+     *       (1997 9:00 AM EST)November 2,30
+     *       (1998 9:00 AM EST)January 4,25;March 1,29
+     *       (1998 9:00 AM EDT)May 3,31
+     */
+    public function test_every_other_month_1st_and_last_sunday_10_count() {
+        global $DB;
+
+        // Change our event's date to 05-09-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
+        $startdate = new DateTime(date('Y-m-d', $this->event->timestart));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        // First occurrence is 07-09-1997 which is the first Sunday.
+        $ordinal = 'first';
+        foreach ($records as $record) {
+            // Get date of the month's first/last Sunday.
+            $recordmonthyear = date('F Y', $record->timestart);
+            $expecteddate = new DateTime($ordinal . ' Sunday of ' . $recordmonthyear);
+            $expecteddate->add($offsetinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+            if ($ordinal === 'first') {
+                $ordinal = 'last';
+            } else {
+                $ordinal = 'first';
+            }
+        }
+    }
+
+    /**
+     * Monthly on the second to last Monday of the month for 6 months:
+     *
+     * DTSTART;TZID=US-Eastern:19970922T090000
+     * RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO
+     *   ==> (1997 9:00 AM EDT)September 22;October 20
+     *       (1997 9:00 AM EST)November 17;December 22
+     *       (1998 9:00 AM EST)January 19;February 16
+     */
+    public function test_monthly_last_monday_for_6_months() {
+        global $DB;
+
+        // Change our event's date to 05-09-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;COUNT=6;BYDAY=-2MO';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 6 records based on COUNT rule.
+        $this->assertCount(6, $records);
+
+        foreach ($records as $record) {
+            // Get date of the month's last Monday.
+            $recordmonthyear = date('F Y', $record->timestart);
+            $expecteddate = new DateTime('last Monday of ' . $recordmonthyear);
+            // Modify to get the second to the last Monday.
+            $expecteddate->modify('last Monday');
+            // Add offset.
+            $expecteddate->add($offsetinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+        }
+    }
+
+    /**
+     * Monthly on the third to the last day of the month, forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970928T090000
+     * RRULE:FREQ=MONTHLY;BYMONTHDAY=-3
+     *   ==> (1997 9:00 AM EDT)September 28
+     *       (1997 9:00 AM EST)October 29;November 28;December 29
+     *       (1998 9:00 AM EST)January 29;February 26
+     *       ...
+     */
+    public function test_third_to_the_last_day_of_the_month_forever() {
+        global $DB;
+
+        // Change our event's date to 05-09-1997, based on the example from the RFC.
+        $this->change_event_startdate('19970928T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $subinterval = new DateInterval('P2D');
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            // Get date of the third to the last day of the month.
+            $recordmonthyear = date('F Y', $record->timestart);
+            $expecteddate = new DateTime('last day of ' . $recordmonthyear);
+            // Set time to 9am.
+            $expecteddate->setTime(9, 0);
+            // Modify to get the third to the last day of the month.
+            $expecteddate->sub($subinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+        }
+    }
+
+    /**
+     * Monthly on the 2nd and 15th of the month for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15
+     *   ==> (1997 9:00 AM EDT)September 2,15;October 2,15
+     *       (1997 9:00 AM EST)November 2,15;December 2,15
+     *       (1998 9:00 AM EST)January 2,15
+     */
+    public function test_every_2nd_and_15th_of_the_month_10_count() {
+        global $DB;
+
+        $startdatetime = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        $day = '02';
+        foreach ($records as $record) {
+            // Get the first Friday of the record's month.
+            $recordmonthyear = date('Y-m', $record->timestart);
+
+            // Get date of the month's last Monday.
+            $expecteddate = new DateTime("$recordmonthyear-$day");
+            // Add offset.
+            $expecteddate->add($offsetinterval);
+            if ($day === '02') {
+                $day = '15';
+            } else {
+                $day = '02';
+            }
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+        }
+    }
+
+    /**
+     * Monthly on the first and last day of the month for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970930T090000
+     * RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1
+     *   ==> (1997 9:00 AM EDT)September 30;October 1
+     *       (1997 9:00 AM EST)October 31;November 1,30;December 1,31
+     *       (1998 9:00 AM EST)January 1,31;February 1
+     */
+    public function test_every_first_and_last_day_of_the_month_10_count() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        // First occurrence is 30-Sep-1997.
+        $day = 'last';
+        foreach ($records as $record) {
+            // Get the first Friday of the record's month.
+            $recordmonthyear = date('F Y', $record->timestart);
+
+            // Get date of the month's last Monday.
+            $expecteddate = new DateTime("$day day of $recordmonthyear");
+            // Add offset.
+            $expecteddate->add($offsetinterval);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            if ($day === 'first') {
+                $day = 'last';
+            } else {
+                $day = 'first';
+            }
+        }
+    }
+
+    /**
+     * Every 18 months on the 10th thru 15th of the month for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970910T090000
+     * RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15
+     *   ==> (1997 9:00 AM EDT)September 10,11,12,13,14,15
+     *       (1999 9:00 AM EST)March 10,11,12,13
+     */
+    public function test_every_18_months_days_10_to_15_10_count() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        // First occurrence is 10-Sep-1997.
+        $expecteddate = clone($startdatetime);
+        $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Get next expected date.
+            if ($expecteddate->format('d') == 15) {
+                // If 15th, increment by 18 months.
+                $expecteddate->add(new DateInterval('P18M'));
+                // Then go back to the 10th.
+                $expecteddate->sub(new DateInterval('P5D'));
+            } else {
+                // Otherwise, increment by 1 day.
+                $expecteddate->add(new DateInterval('P1D'));
+            }
+        }
+    }
+
+    /**
+     * Every Tuesday, every other month:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU
+     *   ==> (1997 9:00 AM EDT)September 2,9,16,23,30
+     *       (1997 9:00 AM EST)November 4,11,18,25
+     *       (1998 9:00 AM EST)January 6,13,20,27;March 3,10,17,24,31
+     *       ...
+     */
+    public function test_every_tuesday_every_other_month_forever() {
+        global $DB;
+
+        $rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $this->event->timestart));
+        $nextmonth = new DateTime($expecteddate->format('Y-m-d'));
+        $offset = $expecteddate->diff($nextmonth, true);
+        $nextmonth->modify('first day of next month');
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Get next expected date.
+            $expecteddate->modify('next Tuesday');
+            if ($expecteddate->getTimestamp() >= $nextmonth->getTimestamp()) {
+                // Go to the end of the month.
+                $expecteddate->modify('last day of this month');
+                // Find the next Tuesday.
+                $expecteddate->modify('next Tuesday');
+
+                // Increment next month by 2 months.
+                $nextmonth->add(new DateInterval('P2M'));
+            }
+            $expecteddate->add($offset);
+        }
+    }
+
+    /**
+     * Yearly in June and July for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970610T090000
+     * RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7
+     *   ==> (1997 9:00 AM EDT)June 10;July 10
+     *       (1998 9:00 AM EDT)June 10;July 10
+     *       (1999 9:00 AM EDT)June 10;July 10
+     *       (2000 9:00 AM EDT)June 10;July 10
+     *       (2001 9:00 AM EDT)June 10;July 10
+     * Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components are specified, the day is gotten from DTSTART.
+     */
+    public function test_yearly_in_june_july_10_count() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        $expecteddate = $startdatetime;
+        $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
+        $monthinterval = new DateInterval('P1M');
+        $yearinterval = new DateInterval('P1Y');
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Get next expected date.
+            if ($expecteddate->format('m') == 6) {
+                // Go to the month of July.
+                $expecteddate->add($monthinterval);
+            } else {
+                // Go to the month of June next year.
+                $expecteddate->sub($monthinterval);
+                $expecteddate->add($yearinterval);
+            }
+        }
+    }
+
+    /**
+     * Every other year on January, February, and March for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970310T090000
+     * RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3
+     *   ==> (1997 9:00 AM EST)March 10
+     *       (1999 9:00 AM EST)January 10;February 10;March 10
+     *       (2001 9:00 AM EST)January 10;February 10;March 10
+     *       (2003 9:00 AM EST)January 10;February 10;March 10
+     */
+    public function test_every_other_year_in_june_july_10_count() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        $expecteddate = $startdatetime;
+        $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
+        $monthinterval = new DateInterval('P1M');
+        $yearinterval = new DateInterval('P2Y');
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Get next expected date.
+            if ($expecteddate->format('m') != 3) {
+                // Go to the next month.
+                $expecteddate->add($monthinterval);
+            } else {
+                // Go to the month of January next year.
+                $expecteddate->sub($monthinterval);
+                $expecteddate->sub($monthinterval);
+                $expecteddate->add($yearinterval);
+            }
+        }
+    }
+
+    /**
+     * Every 3rd year on the 1st, 100th and 200th day for 10 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970101T090000
+     * RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200
+     *   ==> (1997 9:00 AM EST)January 1
+     *       (1997 9:00 AM EDT)April 10;July 19
+     *       (2000 9:00 AM EST)January 1
+     *       (2000 9:00 AM EDT)April 9;July 18
+     *       (2003 9:00 AM EST)January 1
+     *       (2003 9:00 AM EDT)April 10;July 19
+     *       (2006 9:00 AM EST)January 1
+     */
+    public function test_every_3_years_1st_100th_200th_days_10_count() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        // Should have 10 records based on COUNT rule.
+        $this->assertCount(10, $records);
+
+        $expecteddate = $startdatetime;
+        $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
+        $hundredthdayinterval = new DateInterval('P99D');
+        $twohundredthdayinterval = new DateInterval('P100D');
+        $yearinterval = new DateInterval('P3Y');
+
+        foreach ($records as $record) {
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Get next expected date.
+            if ($expecteddate->format('z') == 0) { // January 1.
+                $expecteddate->add($hundredthdayinterval);
+            } else if ($expecteddate->format('z') == 99) { // 100th day of the year.
+                $expecteddate->add($twohundredthdayinterval);
+            } else { // 200th day of the year.
+                $expecteddate->add($yearinterval);
+                $expecteddate->modify('January 1');
+            }
+        }
+    }
+
+    /**
+     * Every 20th Monday of the year, forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970519T090000
+     * RRULE:FREQ=YEARLY;BYDAY=20MO
+     *   ==> (1997 9:00 AM EDT)May 19
+     *       (1998 9:00 AM EDT)May 18
+     *       (1999 9:00 AM EDT)May 17
+     *       ...
+     */
+    public function test_yearly_every_20th_monday_forever() {
+        global $DB;
+
+        // Change our event's date to 19-05-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970519T090000', 'US/Eastern');
+
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+
+        $offset = $startdatetime->diff($startdate, true);
+
+        $interval = new DateInterval('P1Y');
+
+        $rrule = 'FREQ=YEARLY;BYDAY=20MO';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = $startdatetime;
+        $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->modify('January 1');
+            $expecteddate->add($interval);
+            $expecteddate->modify("+20 Monday");
+            $expecteddate->add($offset);
+        }
+    }
+
+    /**
+     * Monday of week number 20 (where the default start of the week is Monday), forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970512T090000
+     * RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO
+     * ==> (1997 9:00 AM EDT)May 12
+     *     (1998 9:00 AM EDT)May 11
+     *     (1999 9:00 AM EDT)May 17
+     *     ...
+     */
+    public function test_yearly_byweekno_forever() {
+        global $DB;
+
+        // Change our event's date to 12-05-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970512T090000', 'US/Eastern');
+
+        $startdate = clone($startdatetime);
+        $startdate->modify($startdate->format('Y-m-d'));
+
+        $offset = $startdatetime->diff($startdate, true);
+
+        $interval = new DateInterval('P1Y');
+
+        $rrule = 'FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->add($interval);
+            $expecteddate->setISODate($expecteddate->format('Y'), 20);
+            $expecteddate->add($offset);
+        }
+    }
+
+    /**
+     * Every Thursday in March, forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970313T090000
+     * RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH
+     *   ==> (1997 9:00 AM EST)March 13,20,27
+     *       (1998 9:00 AM EST)March 5,12,19,26
+     *       (1999 9:00 AM EST)March 4,11,18,25
+     *       ...
+     */
+    public function test_every_thursday_in_march_forever() {
+        global $DB;
+
+        // Change our event's date to 12-05-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970313T090000', 'US/Eastern');
+
+        $interval = new DateInterval('P1Y');
+
+        $rrule = 'FREQ=YEARLY;BYMONTH=3;BYDAY=TH';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = $startdatetime;
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+        $offsetinterval = $startdatetime->diff($startdate, true);
+        $expecteddate->setTimezone(new DateTimeZone(get_user_timezone()));
+        $april1st = new DateTime('1997-04-01');
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->modify('next Thursday');
+            if ($expecteddate->getTimestamp() >= $april1st->getTimestamp()) {
+                // Reset to 1st of March.
+                $expecteddate->modify('first day of March');
+                // Go to next year.
+                $expecteddate->add($interval);
+                if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) {
+                    $expecteddate->modify('next Thursday');
+                }
+                // Increment to next year's April 1st.
+                $april1st->add($interval);
+            }
+            $expecteddate->add($offsetinterval);
+        }
+    }
+
+    /**
+     * Every Thursday, but only during June, July, and August, forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970605T090000
+     * RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8
+     *   ==> (1997 9:00 AM EDT)June 5,12,19,26;July 3,10,17,24,31;August 7,14,21,28
+     *       (1998 9:00 AM EDT)June 4,11,18,25;July 2,9,16,23,30;August 6,13,20,27
+     *       (1999 9:00 AM EDT)June 3,10,17,24;July 1,8,15,22,29;August 5,12,19,26
+     *       ...
+     */
+    public function test_every_thursday_june_july_august_forever() {
+        global $DB;
+
+        // Change our event's date to 05-06-1997, based on the example from the RFC.
+        $startdatetime = $this->change_event_startdate('19970605T090000', 'US/Eastern');
+
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+
+        $offset = $startdatetime->diff($startdate, true);
+
+        $interval = new DateInterval('P1Y');
+
+        $rrule = 'FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $expecteddate = new DateTime(date('Y-m-d H:i:s', $startdatetime->getTimestamp()));
+        $september1st = new DateTime('1997-09-01');
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Go to next period.
+            $expecteddate->modify('next Thursday');
+            if ($expecteddate->getTimestamp() >= $september1st->getTimestamp()) {
+                $expecteddate->add($interval);
+                $expecteddate->modify('June 1');
+                if ($expecteddate->format('l') !== rrule_manager::DAY_THURSDAY) {
+                    $expecteddate->modify('next Thursday');
+                }
+                $september1st->add($interval);
+            }
+            $expecteddate->add($offset);
+        }
+    }
+
+    /**
+     * Every Friday the 13th, forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * EXDATE;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13
+     *   ==> (1998 9:00 AM EST)February 13;March 13;November 13
+     *       (1999 9:00 AM EDT)August 13
+     *       (2000 9:00 AM EDT)October 13
+     */
+    public function test_friday_the_thirteenth_forever() {
+        global $DB;
+
+        $rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+            // Assert that the day of the month and the day correspond to Friday the 13th.
+            $this->assertEquals('Friday 13', date('l d', $record->timestart));
+        }
+    }
+
+    /**
+     * The first Saturday that follows the first Sunday of the month, forever:
+     *
+     * DTSTART;TZID=US-Eastern:19970913T090000
+     * RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13
+     *   ==> (1997 9:00 AM EDT)September 13;October 11
+     *       (1997 9:00 AM EST)November 8;December 13
+     *       (1998 9:00 AM EST)January 10;February 7;March 7
+     *       (1998 9:00 AM EDT)April 11;May 9;June 13...
+     */
+    public function test_first_saturday_following_first_sunday_forever() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19970913T090000', 'US/Eastern');
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+        $offset = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+        $bymonthdays = [7, 8, 9, 10, 11, 12, 13];
+        foreach ($records as $record) {
+            $recordmonthyear = date('F Y', $record->timestart);
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            // Get first Saturday after the first Sunday of the month.
+            $expecteddate = new DateTime('first Sunday of ' . $recordmonthyear);
+            $expecteddate->modify('next Saturday');
+            $expecteddate->add($offset);
+
+            // Assert the record's date corresponds to the first Saturday of the month.
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Assert that the record is either the 7th, 8th, 9th, ... 13th day of the month.
+            $this->assertContains(date('j', $record->timestart), $bymonthdays);
+        }
+    }
+
+    /**
+     * Every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day):
+     *
+     * DTSTART;TZID=US-Eastern:19961105T090000
+     * RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
+     *   ==> (1996 9:00 AM EST)November 5
+     *       (2000 9:00 AM EST)November 7
+     *       (2004 9:00 AM EST)November 2
+     *       ...
+     */
+    public function test_every_us_presidential_election_forever() {
+        global $DB;
+
+        $startdatetime = $this->change_event_startdate('19961105T090000', 'US/Eastern');
+        $startdate = new DateTime($startdatetime->format('Y-m-d'));
+        $offset = $startdatetime->diff($startdate, true);
+
+        $rrule = 'FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+        $bymonthdays = [2, 3, 4, 5, 6, 7, 8];
+        foreach ($records as $record) {
+            $recordmonthyear = date('F Y', $record->timestart);
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            // Get first Saturday after the first Sunday of the month.
+            $expecteddate = new DateTime('first Monday of ' . $recordmonthyear);
+            $expecteddate->modify('next Tuesday');
+            $expecteddate->add($offset);
+
+            // Assert the record's date corresponds to the first Saturday of the month.
+            $this->assertEquals($expecteddate->format('Y-m-d H:i:s'), date('Y-m-d H:i:s', $record->timestart));
+
+            // Assert that the record is either the 2nd, 3rd, 4th ... 8th day of the month.
+            $this->assertContains(date('j', $record->timestart), $bymonthdays);
+        }
+    }
+
+    /**
+     * The 3rd instance into the month of one of Tuesday, Wednesday or Thursday, for the next 3 months:
+     *
+     * DTSTART;TZID=US-Eastern:19970904T090000
+     * RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3
+     *   ==> (1997 9:00 AM EDT)September 4;October 7
+     *       (1997 9:00 AM EST)November 6
+     */
+    public function test_monthly_bysetpos_3_count() {
+        global $DB;
+
+        $this->change_event_startdate('19970904T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(3, $records);
+
+        $expecteddates = [
+            (new DateTime('1997-09-04 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-10-07 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-11-06 09:00:00 EST'))->getTimestamp()
+        ];
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * The 2nd to last weekday of the month:
+     *
+     * DTSTART;TZID=US-Eastern:19970929T090000
+     * RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2
+     *   ==> (1997 9:00 AM EDT)September 29
+     *       (1997 9:00 AM EST)October 30;November 27;December 30
+     *       (1998 9:00 AM EST)January 29;February 26;March 30
+     *       ...
+     */
+    public function test_second_to_the_last_weekday_of_the_month_forever() {
+        global $DB;
+
+        $this->change_event_startdate('19970929T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+
+        $expecteddates = [
+            (new DateTime('1997-09-29 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-10-30 09:00:00 EST'))->getTimestamp(),
+            (new DateTime('1997-11-27 09:00:00 EST'))->getTimestamp(),
+            (new DateTime('1997-12-30 09:00:00 EST'))->getTimestamp(),
+            (new DateTime('1998-01-29 09:00:00 EST'))->getTimestamp(),
+            (new DateTime('1998-02-26 09:00:00 EST'))->getTimestamp(),
+            (new DateTime('1998-03-30 09:00:00 EST'))->getTimestamp(),
+        ];
+
+        $untildate = new DateTime();
+        $untildate->add(new DateInterval('P' . $mang::TIME_UNLIMITED_YEARS . 'Y'));
+        $untiltimestamp = $untildate->getTimestamp();
+
+        $i = 0;
+        foreach ($records as $record) {
+            $this->assertLessThanOrEqual($untiltimestamp, $record->timestart);
+
+            // Confirm that the first 7 records correspond to the expected dates listed above.
+            if ($i < 7) {
+                $this->assertEquals($expecteddates[$i], $record->timestart);
+                $i++;
+            }
+        }
+    }
+
+    /**
+     * Every 3 hours from 9:00 AM to 5:00 PM on a specific day:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z
+     *   ==> (September 2, 1997 EDT)09:00,12:00,15:00
+     */
+    public function test_every_3hours_9am_to_5pm() {
+        global $DB;
+
+        $rrule = 'FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T210000Z';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(3, $records);
+
+        $expecteddates = [
+            (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 15:00:00 EDT'))->getTimestamp(),
+        ];
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * Every 15 minutes for 6 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6
+     *   ==> (September 2, 1997 EDT)09:00,09:15,09:30,09:45,10:00,10:15
+     */
+    public function test_every_15minutes_6_count() {
+        global $DB;
+
+        $rrule = 'FREQ=MINUTELY;INTERVAL=15;COUNT=6';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(6, $records);
+
+        $expecteddates = [
+            (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 09:15:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 09:30:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 09:45:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 10:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 10:15:00 EDT'))->getTimestamp(),
+        ];
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * Every hour and a half for 4 occurrences:
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4
+     *   ==> (September 2, 1997 EDT)09:00,10:30;12:00;13:30
+     */
+    public function test_every_90minutes_4_count() {
+        global $DB;
+
+        $rrule = 'FREQ=MINUTELY;INTERVAL=90;COUNT=4';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(4, $records);
+
+        $expecteddates = [
+            (new DateTime('1997-09-02 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 10:30:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 12:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-09-02 13:30:00 EDT'))->getTimestamp(),
+        ];
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
+     *
+     * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
+     * so just limit the count to 50).
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50
+     *   ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
+     *       (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
+     *       ...
+     */
+    public function test_every_20minutes_daily_byhour_byminute_50_count() {
+        global $DB;
+
+        $rrule = 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40;COUNT=50';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $byminuteinterval = new DateInterval('PT20M');
+        $bydayinterval = new DateInterval('P1D');
+        $date = new DateTime('1997-09-02 09:00:00 EDT');
+        $expecteddates = [];
+        $count = 50;
+        for ($i = 0; $i < $count; $i++) {
+            $expecteddates[] = $date->getTimestamp();
+            $date->add($byminuteinterval);
+            if ($date->format('H') > 16) {
+                // Go to next day.
+                $date->add($bydayinterval);
+                // Reset time to 9am.
+                $date->setTime(9, 0);
+            }
+        }
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount($count, $records);
+
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * Every 20 minutes from 9:00 AM to 4:40 PM every day for 100 times:
+     *
+     * (Original RFC example is set to everyday forever, but that will just take a lot of time for the test,
+     * so just limit the count to 50).
+     *
+     * DTSTART;TZID=US-Eastern:19970902T090000
+     * RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50
+     *   ==> (September 2, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
+     *       (September 3, 1997 EDT)9:00,9:20,9:40,10:00,10:20,...16:00,16:20,16:40
+     *       ...
+     */
+    public function test_every_20minutes_minutely_byhour_50_count() {
+        global $DB;
+
+        $rrule = 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16;COUNT=50';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $byminuteinterval = new DateInterval('PT20M');
+        $bydayinterval = new DateInterval('P1D');
+        $date = new DateTime('1997-09-02 09:00:00');
+        $expecteddates = [];
+        $count = 50;
+        for ($i = 0; $i < $count; $i++) {
+            $expecteddates[] = $date->getTimestamp();
+            $date->add($byminuteinterval);
+            if ($date->format('H') > 16) {
+                // Go to next day.
+                $date->add($bydayinterval);
+                // Reset time to 9am.
+                $date->setTime(9, 0);
+            }
+        }
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount($count, $records);
+
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * An example where the days generated makes a difference because of WKST:
+     *
+     * DTSTART;TZID=US-Eastern:19970805T090000
+     * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO
+     *   ==> (1997 EDT)Aug 5,10,19,24
+     */
+    public function test_weekly_byday_with_wkst_mo() {
+        global $DB;
+
+        $this->change_event_startdate('19970805T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(4, $records);
+
+        $expecteddates = [
+            (new DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-08-10 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-08-24 09:00:00 EDT'))->getTimestamp(),
+        ];
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * An example where the days generated makes a difference because of WKST:
+     * Changing only WKST from MO to SU, yields different results...
+     *
+     * DTSTART;TZID=US-Eastern:19970805T090000
+     * RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU
+     *   ==> (1997 EDT)August 5,17,19,31
+     */
+    public function test_weekly_byday_with_wkst_su() {
+        global $DB;
+
+        $this->change_event_startdate('19970805T090000', 'US/Eastern');
+
+        $rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
+        $mang = new rrule_manager($rrule);
+        $mang->parse_rrule();
+        $mang->create_events($this->event);
+
+        $records = $DB->get_records('event', ['repeatid' => $this->event->id], 'timestart ASC', 'id, repeatid, timestart');
+        $this->assertCount(4, $records);
+
+        $expecteddates = [
+            (new DateTime('1997-08-05 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-08-17 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-08-19 09:00:00 EDT'))->getTimestamp(),
+            (new DateTime('1997-08-31 09:00:00 EDT'))->getTimestamp(),
+        ];
+
+        foreach ($records as $record) {
+            $this->assertContains($record->timestart, $expecteddates, date('Y-m-d H:i:s', $record->timestart) . ' is not found.');
+        }
+    }
+
+    /**
+     * Change the event's timestart (DTSTART) based on the test's needs.
+     *
+     * @param string $datestr The date string. In YYYYmmddThhiiss format. e.g. 19990902T090000.
+     * @param string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
+     * @return bool|DateTime
+     */
+    protected function change_event_startdate($datestr, $timezonestr) {
+        $timezone = new DateTimeZone($timezonestr);
+        $newdatetime = DateTime::createFromFormat('Ymd\THis', $datestr, $timezone);
+
+        // Update the start date of the parent event.
+        $calevent = calendar_event::load($this->event->id);
+        $updatedata = (object)[
+            'timestart' => $newdatetime->getTimestamp(),
+            'repeatid' => $this->event->id
+        ];
+        $calevent->update($updatedata, false);
+        $this->event->timestart = $calevent->timestart;
+
+        return $newdatetime;
+    }
+}
diff --git a/calendar/tests/rrule_manager_tests.php b/calendar/tests/rrule_manager_tests.php
deleted file mode 100644 (file)
index a68d615..0000000
+++ /dev/null
@@ -1,537 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Defines test class to test manage rrule during ical imports.
- *
- * @package core_calendar
- * @category test
- * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class core_calendar_rrule_manager_testcase extends advanced_testcase {
-
-    /** @var stdClass a dummy event */
-    protected $event;
-
-    /**
-     * Set up method.
-     */
-    protected function setUp() {
-        global $DB, $CFG;
-        $this->resetAfterTest();
-
-        $this->setTimezone('Australia/Perth');
-
-        $user = $this->getDataGenerator()->create_user();
-        $sub = new stdClass();
-        $sub->url = '';
-        $sub->courseid = 0;
-        $sub->groupid = 0;
-        $sub->userid = $user->id;
-        $sub->pollinterval = 0;
-        $subid = $DB->insert_record('event_subscriptions', $sub, true);
-
-        $event = new stdClass();
-        $event->name = 'Event name';
-        $event->description = '';
-        $event->timestart = 1385913700; // A 2013-12-2 Monday event.
-        $event->timeduration = 3600;
-        $event->uuid = 'uuid';
-        $event->subscriptionid = $subid;
-        $event->userid = $user->id;
-        $event->groupid = 0;
-        $event->courseid = 0;
-        $event->eventtype = 'user';
-        $eventobj = calendar_event::create($event, false);
-        $DB->set_field('event', 'repeatid', $eventobj->id, array('id' => $eventobj->id));
-        $eventobj->repeatid = $eventobj->id;
-        $this->event = $eventobj;
-    }
-
-    /**
-     * Test parse_rrule() method.
-     */
-    public function test_parse_rrule() {
-
-        $rrule = "FREQ=DAILY;COUNT=3;INTERVAL=4;BYSECOND=20,40;BYMINUTE=2,30;BYHOUR=3,4;BYDAY=MO,TH;BYMONTHDAY=20,
-                30;BYYEARDAY=300,-20;BYWEEKNO=22,33;BYMONTH=3,4";
-        $mang = new core_tests_calendar_rrule_manager($rrule);
-        $mang->parse_rrule();
-        $this->assertEquals(\core_calendar\rrule_manager::FREQ_DAILY, $mang->freq);
-        $this->assertEquals(3, $mang->count);
-        $this->assertEquals(4, $mang->interval);
-        $this->assertEquals(array(20, 40), $mang->bysecond);
-        $this->assertEquals(array(2, 30), $mang->byminute);
-        $this->assertEquals(array(3, 4), $mang->byhour);
-        $this->assertEquals(array('MO', 'TH'), $mang->byday);
-        $this->assertEquals(array(20, 30), $mang->bymonthday);
-        $this->assertEquals(array(300, -20), $mang->byyearday);
-        $this->assertEquals(array(22, 33), $mang->byweekno);
-        $this->assertEquals(array(3, 4), $mang->bymonth);
-    }
-
-    /**
-     * Test exception is thrown for invalid property.
-     *
-     * @expectedException moodle_exception
-     */
-    public function test_parse_rrule_validation() {
-
-        $rrule = "RANDOM=PROPERTY;";
-        $mang = new core_tests_calendar_rrule_manager($rrule);
-        $mang->parse_rrule();
-    }
-
-    /**
-     * Test exception is thrown for invalid frequency.
-     *
-     * @expectedException moodle_exception
-     */
-    public function test_freq_validation() {
-
-        $rrule = "FREQ=RANDOMLY;";
-        $mang = new core_tests_calendar_rrule_manager($rrule);
-        $mang->parse_rrule();
-    }
-
-    /**
-     * Test recurrence rules for daily frequency.
-     */
-    public function test_daily_events() {
-        global $DB;
-
-        $rrule = 'FREQ=DAILY;COUNT=3'; // This should generate 2 child events + 1 parent.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(3, $count);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + DAYSECS)));
-        $this->assertTrue($result);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + 2 * DAYSECS)));
-        $this->assertTrue($result);
-
-        $until = $this->event->timestart + DAYSECS * 2;
-        $until = date('Y-m-d', $until);
-        $rrule = "FREQ=DAILY;UNTIL=$until"; // This should generate 1 child event + 1 parent,since by then until bound would be hit.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(2, $count);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + DAYSECS)));
-        $this->assertTrue($result);
-
-        $rrule = 'FREQ=DAILY;COUNT=3;INTERVAL=3'; // This should generate 2 child events + 1 parent, every 3rd day.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(3, $count);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + 3 * DAYSECS)));
-        $this->assertTrue($result);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + 6 * DAYSECS)));
-        $this->assertTrue($result);
-
-        // Forever event. This should generate events for time() + 10 year period, every 300th day.
-        $rrule = 'FREQ=DAILY;INTERVAL=300';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $time = $this->event->timestart + 300 * DAYSECS * $i) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-        }
-    }
-
-    /**
-     * Test recurrence rules for weekly frequency.
-     */
-    public function test_weekly_events() {
-        global $DB;
-
-        $rrule = 'FREQ=WEEKLY;COUNT=1'; // This should generate 7 events in total, one for each day.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(7, $count);
-        for ($i = 0; $i < 7; $i++) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($this->event->timestart + $i * DAYSECS)));
-            $this->assertTrue($result);
-        }
-
-        // This should generate 4 child event + 1 parent, since by then until bound would be hit.
-        $until = $this->event->timestart + WEEKSECS * 4;
-        $until = date('YmdThis', $until);
-        $rrule = "FREQ=WEEKLY;BYDAY=MO;UNTIL=$until";
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(5, $count);
-        for ($i = 0; $i < 5; $i++) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($this->event->timestart + $i * WEEKSECS)));
-            $this->assertTrue($result);
-        }
-
-        // This should generate 4 events in total every monday and Wednesday of every 3rd week.
-        $rrule = 'FREQ=WEEKLY;INTERVAL=3;BYDAY=MO,WE;COUNT=2';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(4, $count);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + 3 * WEEKSECS))); // Monday event.
-        $this->assertTrue($result);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + 2 * DAYSECS))); // Wednesday event.
-        $this->assertTrue($result);
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                'timestart' => ($this->event->timestart + 3 * WEEKSECS + 2 * DAYSECS))); // Wednesday event.
-        $this->assertTrue($result);
-
-        // Forever event. This should generate events over time() + 10 year period, every 50th monday.
-        $rrule = 'FREQ=WEEKLY;BYDAY=MO;INTERVAL=50';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $time = $this->event->timestart + 50 * WEEKSECS * $i) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-        }
-    }
-
-    /**
-     * Test recurrence rules for monthly frequency.
-     */
-    public function test_monthly_events() {
-        global $DB;
-        $rrule = 'FREQ=MONTHLY;COUNT=3;BYMONTHDAY=2'; // This should generate 3 events in total.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(3, $count);
-        for ($i = 0; $i < 3; $i++) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => (strtotime("+$i month", $this->event->timestart))));
-            $this->assertTrue($result);
-        }
-
-        // This much seconds after the start of the day.
-        $offset = $this->event->timestart - mktime(0, 0, 0, date("n", $this->event->timestart), date("j", $this->event->timestart),
-                date("Y", $this->event->timestart));
-        $monthstart = mktime(0, 0, 0, date("n", $this->event->timestart), 1, date("Y", $this->event->timestart));
-
-        $rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=1MO'; // This should generate 3 events in total, first monday of the month.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(3, $count);
-        $time = strtotime('1 Monday', strtotime("+1 months", $monthstart)) + $offset;
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $time));
-        $this->assertTrue($result);
-        $time = strtotime('1 Monday', strtotime("+2 months", $monthstart)) + $offset;
-        $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $time));
-        $this->assertTrue($result);
-
-        // This should generate 10 child event + 1 parent, since by then until bound would be hit.
-        $until = strtotime('+1 day +10 months', $this->event->timestart);
-        $until = date('YmdThis', $until);
-        $rrule = "FREQ=MONTHLY;BYMONTHDAY=2;UNTIL=$until";
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(11, $count);
-        for ($i = 0; $i < 11; $i++) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => (strtotime("+$i month", $this->event->timestart))));
-            $this->assertTrue($result);
-        }
-
-        // This should generate 10 child event + 1 parent, since by then until bound would be hit.
-        $until = strtotime('+1 day +10 months', $this->event->timestart);
-        $until = date('YmdThis', $until);
-        $rrule = "FREQ=MONTHLY;BYDAY=1MO;UNTIL=$until";
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(10, $count);
-        for ($i = 0; $i < 10; $i++) {
-            $time = strtotime('1 Monday', strtotime("+$i months", $monthstart)) + $offset;
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $time));
-            $this->assertTrue($result);
-        }
-
-        // This should generate 11 child event + 1 parent, since by then until bound would be hit.
-        $until = strtotime('+10 day +10 months', $this->event->timestart); // 12 oct 2014.
-        $until = date('YmdThis', $until);
-        $rrule = "FREQ=MONTHLY;INTERVAL=2;BYMONTHDAY=2,5;UNTIL=$until";
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(12, $count);
-        for ($i = 0; $i < 6; $i++) {
-            $moffset = $i * 2;
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => (strtotime("+$moffset month", $this->event->timestart))));
-            $this->assertTrue($result);
-            // Event on the 5th of a month.
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => (strtotime("+3 days +$moffset month", $this->event->timestart))));
-            $this->assertTrue($result);
-        }
-
-        // This should generate 11 child event + 1 parent, since by then until bound would be hit.
-        $until = strtotime('+20 day +10 months', $this->event->timestart); // 22 oct 2014.
-        $until = date('YmdTHis', $until);
-        $rrule = "FREQ=MONTHLY;INTERVAL=2;BYDAY=1MO,3WE;UNTIL=$until";
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(12, $count);
-        for ($i = 0; $i < 6; $i++) {
-            $moffset = $i * 2;
-            $time = strtotime("+$moffset month", $monthstart);
-            $time2 = strtotime("+1 Monday", $time) + $offset;
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $time2));
-            $this->assertTrue($result);
-            $time2 = strtotime("+3 Wednesday", $time) + $offset;
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => $time2)); // Event on the 5th of a month.
-            $this->assertTrue($result);
-        }
-
-        // Forever event. This should generate events over 10 year period, on 2nd of every 12th month.
-        $rrule = 'FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=2';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $moffset = $i * 12,
-                $time = strtotime("+$moffset month", $this->event->timestart)) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-        }
-
-        // Forever event. This should generate 10 child events + 1 parent over 10 year period, every 50th Monday.
-        $rrule = 'FREQ=MONTHLY;BYDAY=1MO;INTERVAL=12';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(11, $count);
-        for ($i = 0, $moffset = 0, $time = $this->event->timestart; $time < $until; $i++, $moffset = $i * 12) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => ($time)));
-            $this->assertTrue($result);
-            $time = strtotime("+$moffset month", $monthstart);
-            $time = strtotime("+1 Monday", $time) + $offset;
-        }
-    }
-
-    /**
-     * Test recurrence rules for yearly frequency.
-     */
-    public function test_yearly_events() {
-        global $DB;
-
-        $rrule = 'FREQ=YEARLY;COUNT=3;BYMONTH=12'; // This should generate 3 events in total.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(3, $count);
-        for ($i = 0, $time = $this->event->timestart; $i < 3; $i++, $time = strtotime("+$i years", $this->event->timestart)) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $time));
-            $this->assertTrue($result);
-        }
-
-        // Create an event every december, until the time limit is hit.
-        $until = strtotime('+20 day +10 years', $this->event->timestart);
-        $until = date('YmdTHis', $until);
-        $rrule = "FREQ=YEARLY;BYMONTH=12;UNTIL=$until"; // Forever event.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(11, $count);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
-            $time = strtotime("+$yoffset years", $this->event->timestart)) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-        }
-
-        // This should generate 5 events in total, every second year in the month of december.
-        $rrule = 'FREQ=YEARLY;BYMONTH=12;INTERVAL=2;COUNT=5';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(5, $count);
-        for ($i = 0, $time = $this->event->timestart; $i < 5; $i++, $yoffset = $i * 2,
-            $time = strtotime("+$yoffset years", $this->event->timestart)) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-        }
-
-        $rrule = 'FREQ=YEARLY;BYMONTH=12;INTERVAL=2'; // Forever event.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(6, $count);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2,
-            $time = strtotime("+$yoffset years", $this->event->timestart)) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-        }
-
-        // This much seconds after the start of the day.
-        $offset = $this->event->timestart - mktime(0, 0, 0, date("n", $this->event->timestart), date("j", $this->event->timestart),
-                date("Y", $this->event->timestart));
-        $yearstart = mktime(0, 0, 0, 1, 1, date("Y", $this->event->timestart));
-
-        $rrule = 'FREQ=YEARLY;COUNT=3;BYMONTH=12;BYDAY=1MO'; // This should generate 3 events in total.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(3, $count);
-        for ($i = 0; $i < 3; $i++) {
-            $time = strtotime("+11 months +$i years", $yearstart);
-            $time = strtotime("+1 Monday", $time) + $offset;
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id, 'timestart' => $time));
-            $this->assertTrue($result);
-        }
-
-        // Create an event every december, until the time limit is hit.
-        $until = strtotime('+20 day +10 years', $this->event->timestart);
-        $until = date('YmdTHis', $until);
-        $rrule = "FREQ=YEARLY;BYMONTH=12;UNTIL=$until;BYDAY=1MO";
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(11, $count);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-            $time = strtotime("+11 months +$i years", $yearstart);
-            $time = strtotime("+1 Monday", $time) + $offset;
-        }
-
-        // This should generate 5 events in total, every second year in the month of december.
-        $rrule = 'FREQ=YEARLY;BYMONTH=12;INTERVAL=2;COUNT=5;BYDAY=1MO';
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(5, $count);
-        for ($i = $yoffset = 0, $time = $this->event->timestart; $i < 5; $i++, $yoffset = $i * 2) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-            $time = strtotime("+11 months +$yoffset years", $yearstart);
-            $time = strtotime("+1 Monday", $time) + $offset;
-        }
-
-        $rrule = 'FREQ=YEARLY;BYMONTH=12;INTERVAL=2;BYDAY=1MO'; // Forever event.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(6, $count);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-            $time = strtotime("+11 months +$yoffset years", $yearstart);
-            $time = strtotime("+1 Monday", $time) + $offset;
-        }
-
-        $rrule = 'FREQ=YEARLY;INTERVAL=2'; // Forever event.
-        $mang = new \core_calendar\rrule_manager($rrule);
-        $until = time() + (YEARSECS * $mang::TIME_UNLIMITED_YEARS);
-        $mang->parse_rrule();
-        $mang->create_events($this->event);
-        $count = $DB->count_records('event', array('repeatid' => $this->event->id));
-        $this->assertEquals(6, $count);
-        for ($i = 0, $time = $this->event->timestart; $time < $until; $i++, $yoffset = $i * 2) {
-            $result = $DB->record_exists('event', array('repeatid' => $this->event->id,
-                    'timestart' => ($time)));
-            $this->assertTrue($result);
-            $time = strtotime("+11 months +$yoffset years", $yearstart);
-            $time = strtotime("+1 Monday", $time) + $offset;
-        }
-    }
-}
-
-/**
- * Class core_calendar_test_rrule_manager
- *
- * Wrapper to access protected vars for testing.
- *
- * @package core_calendar
- * @category test
- * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class core_tests_calendar_rrule_manager extends \core_calendar\rrule_manager{
-
-    /**
-     * Magic method to get properties.
-     *
-     * @param $prop string property
-     *
-     * @return mixed
-     * @throws coding_exception
-     */
-    public function __get($prop) {
-        if (property_exists($this, $prop)) {
-            return $this->$prop;
-        }
-        throw new coding_exception('invalidproperty');
-    }
-}
index c2fd1e4..760df77 100644 (file)
@@ -61,12 +61,26 @@ $string['erroraddingevent'] = 'Failed to add event';
 $string['errorbadsubscription'] = 'Calendar subscription not found.';
 $string['errorbeforecoursestart'] = 'Cannot set event before course start date';
 $string['errorcannotimport'] = 'You cannot set up a calendar subscription at this time.';
+$string['errorhasuntilandcount'] = 'Either UNTIL or COUNT may appear in a recurrence rule, but UNTIL and COUNT MUST NOT occur in the same recurrence rule.';
+$string['errorinvalidbydaysuffix'] = 'Valid values for the day of the week parts of the BYDAY rule are MO, TU, WE, TH, FR, SA and SU';
+$string['errorinvalidbydayprefix'] = 'Integer values preceding BYDAY rules can only be present for MONTHLY or YEARLY RRULE.';
+$string['errorinvalidbyhour'] = 'Valid values for the BYHOUR rule are 0 to 59.';
+$string['errorinvalidinterval'] = 'The value for the INTERVAL rule must be a positive integer.';
+$string['errorinvalidbyminute'] = 'Valid values for the BYMINUTE rule are 0 to 59.';
+$string['errorinvalidbymonth'] = 'Valid values for the BYMONTH rule are 1 to 12.';
+$string['errorinvalidbymonthday'] = 'Valid values for the BYMONTHDAY rule are 1 to 31 or -31 to -1.';
+$string['errorinvalidbysetpos'] = 'Valid values for the BYSETPOS rule are 1 to 366 or -366 to -1.';
+$string['errorinvalidbyweekno'] = 'Valid values for the BYWEEKNO rule are 1 to 53 or -53 to -1.';
+$string['errorinvalidbyyearday'] = 'Valid values for the BYYEARDAY rule are 1 to 366 or -366 to -1.';
+$string['errorinvalidbysecond'] = 'Valid values for the BYSECOND rule are 0 to 59.';
 $string['errorinvaliddate'] = 'Invalid date';
 $string['errorinvalidminutes'] = 'Specify duration in minutes by giving a number between 1 and 999.';
 $string['errorinvalidrepeats'] = 'Specify the number of events by giving a number between 1 and 99.';
 $string['errorinvalidicalurl'] = 'The given iCal URL is invalid.';
+$string['errormustbeusedwithotherbyrule'] = 'The BYSETPOS rule must only be used in conjunction with another BYxxx rule part.';
 $string['errornodescription'] = 'Description is required';
 $string['errornoeventname'] = 'Name is required';
+$string['errornonyearlyfreqwithbyweekno'] = 'The BYWEEKNO rule is only valid for YEARLY rules.';
 $string['errorrequiredurlorfile'] = 'Either a URL or a file is required to import a calendar.';
 $string['errorrrule'] = 'The passed rrule seems incorrect';
 $string['errorrrulefreq'] = 'The rrule has an invalid frequency parameter';