Skip to content
Rémi Lanvin edited this page Apr 22, 2022 · 13 revisions

Creation

A RRule object is an immutable value object. One instanciated you cannot change the recurrence rule.

The constructor accepts an associative array with all the keywords defined in the RFC ("rule parts") as key and their value as value. The keys and the values are case insensitive, so you can write FREQ or freq and MONTHLY or monthly.

Example: Every 2 weeks on Monday, starting now

$rrule = new RRule\RRule([
    'freq' => 'weekly',
    'byday' => 'MO',
    'interval' => 2
]);

The only required key is FREQUENCY, which must be one of the following:

  • "YEARLY" (or the constant RRule::YEARLY)
  • "MONTHLY" (or the constant RRule::MONTHLY)
  • "WEEKLY" (or the constant RRule::WEEKLY)
  • "DAILY" (or the constant RRule::DAILY)
  • "HOURLY" (or the constant RRule::HOURLY)
  • "MINUTELY" (or the constant RRule::MINUTELY)
  • "SECONDLY" (or the constant RRule::SECONDLY)

Here is a quick description the other parameters. For more details, check the iCalendar RFC.

Name Description
DTSTART The recurrence start date and time. Can be given as a string understandable by PHP's DateTime constructor (for example 2015-07-01 14:46:30) a UNIX Timestamp (as int) or a DateTime object. If no timezone is provided, the default PHP timezone will be used. Default is now. Unlike documented in the RFC, this is not necessarily the first recurrence instance, unless it does fit in the specified rule. Note that the microseconds component will be set to 0.
INTERVAL The interval between each FREQUENCY iteration. For example, when using YEARLY, an interval of 2 means once every two years, but with HOURLY, it means once every two hours. Default is 1.
WKST The week start day. Must be one of the following strings MO, TU, WE, TH, FR, SA, SU. This will affect recurrences based on weekly periods. Default is MO (Monday).
COUNT How many occurrences will be generated.
UNTIL The limit of the recurrence. Accepts the same formats as DTSTART. If no timezone is provided, the default PHP timezone will be used. It can be in a different timezone than DTSTART. If a recurrence instance happens to be the same the date given, this will be the last occurrence.
BYMONTH The month(s) to apply the recurrence to, from 1 (January) to 12 (December). It can be a single value, or a comma-separated list or an array.
BYWEEKNO The week number(s) to apply the recurrence to, from 1 to 53 or -53 to -1. Negative values mean that the counting start from the end of the year, so -1 means "the last week of the year". Week numbers have the meaning described in ISO8601, that is, the first week of the year is that containing at least four days of the new year. Week numbers are affected by the WKST setting. It can be a single value, or a comma-separated list or an array. Warning: negative week numbers are not fully tested yet.
BYYEARDAY The day(s) of the year to apply the recurrence to, from 1 to 366 or -366 to -1. Negative values mean that the count starts from the end of the year, so -1 means "the last day of the year". It can be a single value, or a comma-separated list or an array.
BYMONTHDAY The day(s) of the month to apply the recurrence to, from 1 to 31 or -31 to -1. Negative values mean that the count starts from the end of the month, so -1 means "the last day the month". It can be a single value, or a comma-separated list or an array.
BYDAY The day(s) of the week to apply the recurrence to from the following: MO, TU, WE, TH, FR, SA, SU. It can be a single value, a comma-separated list or an array. Each day can be preceded by a number, indicating a specific occurence within the interval. For example: 1MO (the first Monday of the interval), 3MO (the third Monday), -1MO (the last Monday), and so on.
BYHOUR The hour(s) to apply the recurrence to, from 0 to 23. It can be a single value, or a comma-separated list or an array.
BYMINUTE The minute(s) to apply the recurrence to, from 0 to 59. It can be a single value, or a comma-separated list or an array.
BYSECOND The second(s) to apply the recurrence to, from 0 to 60. It can be a single value, or a comma-separated list or an array. Warning: leap second (i.e. second 60) support is not fully tested.
BYSETPOS The Nth occurrence(s) within the valid occurrences inside a frequency period. It can be a single value, or a comma-separated list or an array. Negative values mean that the count starts from the set. For example, a bysetpos of -1 if combined with a MONTHLY frequency, and a byweekday of 'MO,TU,WE,TH FR', will result in the last work day of every month.

Example: The last workday of the month

$rrule = new RRule\RRule([
    'freq' => 'monthly',
    'byday' => 'MO,TU,WE,TH,FR',
    'bysetpos' => -1
]);

Creation from a string

It is also possible to create a RRule object from an RFC-like syntax by passing a string to the constructor. The string may be a multiple line string (DTSTART and RRULE), a single line string (only RRULE), or just the RRULE property value.

DTSTART and RRULE must be separated by \n (LF), or \r\n (CRLF).

Example:

new RRule('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1');

new RRule('RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1');

new RRule('FREQ=DAILY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1');

new RRule('FREQ=DAILY', new DateTime('1997-12-24')); // since version 1.5

Warning In the RFC, DTSTART and RRULE are two different properties, and therefore must be on a two separate lines. It is not valid to have DTSTART inside RRULE.

For versions >= 1.5 when creating from a string without DTSTART you can optionally specify the date to be used in the second argument. By default, current date will be used.

When creating from a string, there is less flexibility on the date formats for DTSTART and UNTIL. Here is a quick recap of the RFC rules that applies:

DTSTART UNTIL Comment
Date (19970901) Date
Date with local time (19970901T090000) Date with local time PHP's default timezone will be used
Date with UTC time (19970901T090000Z) Date with UTC time
Date with local time and time zone reference (TZID=America/New_York:19980119T020000) Date with UTC time (19970901T090000Z) There is no way to specify a different timezone for UNTIL in the RFC syntax.
Provided in the 2nd argument Date with UTC time (19970901T090000Z) Not recommended, if UNTIL is present in the string, so should DTSTART

For versions >= 1.3, the lib will throw an InvalidArgumentException if the string is invalid.

For versions >= 1.5, the factory method RRule::createFromRfcString() can be used to parse a string and create either a RRule or a RSet object, depending on the string.

Iteration

An instance of RRule can be used directly in a foreach loop to obtain occurrences. This is most efficient way to use this class, as the occurrences are only computed one at the time, as you need them.

Occurrences are DateTime objects, of the same timezone as the original DTSTART date.

Warning: if your rule doesn't have UNTIL or COUNT parts, it will be an infinite loop! You MUST take care of exiting the loop yourself.

Example:

use RRule\RRule;

// every 2 days, 5 times
$rrule = new RRule([
    'freq' => RRule::DAILY,
    'interval' => 2,
    'count' => 5,
    'dtstart' => '1997-09-02 09:00:00'
]);

foreach ( $rrule as $occurrence ) {
    echo $occurrence->format('r'),"\n";
}

// output:
// Tue, 02 Sep 1997 09:00:00 +0000
// Thu, 04 Sep 1997 09:00:00 +0000
// Sat, 06 Sep 1997 09:00:00 +0000
// Mon, 08 Sep 1997 09:00:00 +0000
// Wed, 10 Sep 1997 09:00:00 +0000

Array access

A RRule object can also be used as an array to access the Nth occurrence directly.

Example:

use RRule\RRule;

// every 2 days, 5 times
$rrule = new RRule([
    'freq' => RRule::DAILY,
    'interval' => 2,
    'count' => 5,
    'dtstart' => '1997-09-02 09:00:00'
]);

echo $rrule[0]->format('r'),"\n"; // Tue, 02 Sep 1997 09:00:00 +0000
echo $rrule[4]->format('r'),"\n"; // Wed, 10 Sep 1997 09:00:00 +0000

var_dump(isset($rrule[0])); // bool(true)
var_dump(isset($rrule[5])); // bool(false)

Methods

RRuleInterface

In addition to the Iterator interface and the ArrayAccess interface, the methods from the RRuleInterface are available.

Please check the corresponding wiki page for a list of all the methods.

getRule()

(version >= 1.4)

Returns the internal array representing the rule. Useful if you want to populate a web form for example.

String representation

rfcString($include_timezone = true)

Will produce a RFC-compliant string representing the recurrence rule.

By default, the lib will produce a DTSTART value with local time and timezone reference, even if only a date was passed originally (the time will be 00:00:00 and the timezone your default timezone). If the UNTIL part is present, it will be in UTC. For version >= 1.3, if the parameter $include_timezone is false, DTSTART and UNTIL will be dates with local time (e.g. 19970901T120000) without timezone information. UNTIL will be converted to the same timezone as DTSTART if they were different.

humanReadable(array $opt)

Will produce a (basic) human readable description of the recurrence rule. Optionally, you may pass an array of options as the first argument (see table below). Note: this option method will produce better results if PHP intl extension is available.

Name Description
locale A locale string to determine the language of the result as well as the date and time format. Default is the current locale, as defined in Locale::getDefault() (with intl loaded) or setlocale() (without intl loaded). Examples: 'en', 'en_GB', 'fr', ...
fallback A locale to fallback to, in case the main language is not found. Default is en. Set it to null to disable fallback. (version >= 1.2)
date_formatter A function that will be called to format DTSTART and UNTIL (if applicable). Default is to use the PHP intl extension if loaded, or format the date as Y-m-d H:i:s. If you wish to provide your own, the method takes a \DateTime object as first and only argument.
date_format Only if intl extension is available. One of the IntlDateFormatter predefined constants. Default is IntlDateFormatter::SHORT
time_format Only if intl extension is available. One of the IntlDateFormatter predefined constants. Default is IntlDateFormatter::NONE, IntlDateFormatter::SHORT or IntlDateFormatter::LONG depending how much the time part is relevant to the rule.
explicit_infinite Explicitely say if the rule is infinite (default true). Example: Every day, forever. (version >= 1.5)
include_start Include the start date (default true). Example: Every day, starting from Mon, 01 Sep 1997. (version >= 1.5)
include_until Include the until date or count (default true). (version >= 1.6)
custom_path Filesystem path to look for custom translation. If the option is present, it'll first look for a file in this folder before looking into the default folder. (version >= 2.0)

Examples:

$rule = new RRule('DTSTART;TZID=America/New_York:19970901T090000
       RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR');

echo $rule->humanReadable();
// every other week on Monday, Wednesday and Friday, starting from 9/1/97, until 12/23/97

echo $rule->humanReadable(['locale' => 'fr']);
// une semaine sur deux le lundi, mercredi et vendredi, à partir du 01/09/97, jusqu'au 23/12/97

echo $rrule->humanReadable([
    'locale' => 'en_US',
    'date_format' => IntlDateFormatter::MEDIUM
]),"\n\n";
// every other week on Monday, Wednesday and Friday, starting from Sep 1, 1997, until Dec 23, 1997

echo $rule->humanReadable(['date_formatter' => function($date) { return $date->format('r'); }]);
// every other week on Monday, Wednesday and Friday, starting from Mon, 01 Sep 1997 09:00:00 -0400, until Wed, 24 Dec 1997 00:00:00 +0000

Translations

Translations files are stored in src/i18n folder. If your favorite language is missing, feel free to add it and submit a pull request!

Cache

Occurrences are cached the first time they are calculated. If you use the RRule instance multiple times, caching will improve the performance considerably.

clearCache()

Calling this method will clear the internal cache of the instance, and force it to recalculate the occurrences next time you try to them.

Examples

These examples were converted from the RFC. More examples can be found in the unit tests.

Daily, for 10 occurrences.

$rrule = new RRule([
    'freq' => 'daily',
    'count' => 10,
    'dtstart' => '1997-09-02 09:00:00'
]);
// 1997-09-02 09:00:00
// 1997-09-03 09:00:00
// 1997-09-04 09:00:00
// 1997-09-05 09:00:00
// 1997-09-06 09:00:00
// 1997-09-07 09:00:00
// 1997-09-08 09:00:00
// 1997-09-09 09:00:00
// 1997-09-10 09:00:00
// 1997-09-11 09:00:00

Daily until December 24, 1997

$rrule = new RRule([
    'freq' => 'daily',
    'dtstart' => '1997-09-02 09:00:00',
    'until' => '1997-12-24 00:00:00'
]);
// 1997-09-02 09:00:00
// 1997-09-03 09:00:00
// 1997-09-04 09:00:00
// [...]
// 1997-12-22 09:00:00
// 1997-12-23 09:00:00

Everyday in January, for 3 years.

$rrule = new RRule([
    'freq' => 'yearly',
    'bymonth' => 1,
    'byday' => 'MO,TU,WE,TH,FR,SA,SU',
    'dtstart' => '1997-09-02 09:00:00',
    'until' => '2000-01-31 09:00:00'
]);
// 1998-01-01 09:00:00
// 1998-01-02 09:00:00
// [...]
// 1998-01-31 09:00:00
// 1999-01-01 09:00:00
// 1999-01-02 09:00:00
// [...]
// 1999-01-31 09:00:00
// 2000-01-01 09:00:00
// [...]
// 2000-01-31 09:00:00

Notes

  • Unlike documented in the RFC, and like in the Python version, the starting datetime (DTSTART) is not the first recurrence instance, unless it does fit in the specified rules. This behavior makes more sense than otherwise and is easier to work with. This will only change the results if COUNT or BYSETPOS are used.
  • The current algorithm to compute occurrences is faster at lower frequencies and slower at higher the frequencies. So YEARLY is faster that MONTHLY which is faster than WEEKLY and so on. So if you want to achieve the fastest calculation time, whenever possible, try to use the lowest possible frequency. For example, to get "every day in january" it is slightly faster to write ['freq' => 'yearly','bymonth' => 1,'bymonthday' => range(1,31)] rather than ['freq' => 'daily','bymonth' => 1].
  • Computing all occurrences of rule might get noticeably slow with very high frequencies (MINUTELY or SECONDELY) if the rule last a long period of time (such as many years).
  • Some perfectly RFC-compliant rules are actually impossible and will produce no result. For example "every year, on week 40, in February" (week 40 will never occur in February). In a cases like that, the library will still try to find occurrences, because the rule is technically valid, before returning an empty set once all the possibilities have been exhausted.
  • To avoid issues with date comparison, this library ignores microseconds. Any microseconds provided as input, or created by default (starting PHP 7.1) are reset to 0.