*/
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');
/** 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;
/** @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
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);
$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');
}
}
* Sets Frequency property.
*
* @param string $freq Frequency of event
- * @throws \moodle_exception
+ * @throws moodle_exception
*/
protected function set_frequency($freq) {
switch ($freq) {
break;
default:
// We should never get here, something is very wrong.
- throw new \moodle_exception('errorrrulefreq', 'calendar');
+ throw new moodle_exception('errorrrulefreq', 'calendar');
}
}
* 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.
*/
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
+}
--- /dev/null
+<?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;
+ }
+}
+++ /dev/null
-<?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');
- }
-}