-
Notifications
You must be signed in to change notification settings - Fork 84
Home
This library offers a small, complete, and very fast, implementation of the recurrence rules documented in the iCalendar RFC. It is heavily based on python-dateutil.
The constructor accepts an associative array with all the keywords defined in the RFC ("rule parts") as key. The keys and the values are case insensitive.
Example:
use RRule\RRule;
// every 2 weeks on Monday, starting now
$rrule = new RRule([
'freq' => 'weekly',
'byday' => 'MO',
'interval' => 2
]);
The only required key is FREQUENCY
, which must be one of the following strings:
YEARLY
, MONTHLY
, WEEKLY
, DAILY
, HOURLY
, MINUTELY
or SECONDLY
, or one of the following constants RRule::YEARLY
, RRule::MONTHLY
, RRule::WEEKLY
, RRule::DAILY
, RRule::HOURLY
, RRule::MINUTELY
or 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. Default is now. Unlike documented in the RFC, this is not necessarily the first recurrence instance, unless it does fit in the specified rule. |
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 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 MO (Monday) to SU (Sunday). Must be one of the following strings: MO , TU , WE , TH , FR , SA , SU . It can be a single value, or a comma-separated list or an array. |
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. |
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, a single line string, or just the RRULE
property value.
Example:
new RRule('DTSTART;TZID=America/New_York:19970901T090000
RRULE:FREQ=DAILY;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR;BYMONTH=1'),
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
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)
In addition to the Iterator
interface and the ArrayAccess
interface, the following methods are available.
Returns the number of occurrences, for finite rule only. This method will throw a LogicException
if the rule has not COUNT
or UNTIL
part, as there is an infinity of occurrences.
A RRule
object implements the Countable
interface, meaning that you can call count($rrule)
or $rrule->count()
.
Returns an array of all occurrences, for finite rules only. This method will throw a LogicException
if the rule has not COUNT
or UNTIL
part, as there is an infinity of occurrences.
Returns an array of all occurrences between $begin
and $end
(included). Both dates 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, or null
.
- If
$begin
isnull
this method will return all occurrences before$end
. - If
$end
isnull
this method will return all occurrences after$begin
. If the rule is infinite (noCOUNT
orUNTIL
) it will throw aLogicException
. - If both are
null
this method is the same asgetOccurrences()
Note: If you are working with events, remember that there is no notion of duration. In other words, this method will returns the events that start between $begin
and $end
, but not the events that started before $begin
and might still be ongoing due to their duration. If you want the later behavior, you should first subtract duration from $begin
, to get the occurrences between "begin - duration" and "end".
Returns true if $datetime
is an occurrence of the rule, or false otherwise. $datetime
can be 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.
Note: If you are working with events, remember that there is no notion of duration. In other words, this method will tell you if a event starts as $datetime
, but not if an event is still ongoing at $datetime
. If you want the later behavior, you should call getOccurencesBetween()
with "datetime - duration" and "datetime".
Will produce a RFC-compliant string representing the recurrence rule.
Will produce a 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() . Examples: 'en' , 'en_GB' , 'fr' , ... |
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. |
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 files are stored in src\i18n
folder. If your favorite language is missing, feel free to add it and submit a pull request!
Occurrences are cached the first time they are calculated. If you use the RRule
instance multiple times, caching will improve the performance considerably.
Calling this method will clear the intenval cache of the instance, and force it to recalculate the occurrences next time you try to them. DO NOT use this method while iterating the occurrences (i.e. inside a foreach loop).
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
- 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 ifCOUNT
orBYSETPOS
are used. - The current algorithm to compute occurrences is faster at lower frequencies and slower at higher the frequencies. So
YEARLY
is faster thatMONTHLY
which is faster thanWEEKLY
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
orSECONDELY
) 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.