Recurrence Pattern Generation

This topic describes the interactions of the various recurrence rule parts with the recurrence frequencies.

Overview

The following table maps the recurrence rule (RRULE) parts to their corresponding properties in the Recurrence class. Refer to the class properties for more information about their valid values and usage. Because the recurrence class is implemented separately from the other calendar classes, it can be used by itself to generate a single set of recurring date/times without the need of a containing calendar object such as VEvent. If you need support for multiple recurrence rules, recurrence dates (RDATE), exception rules (EXRULE), and/or exception dates (EXDATE), you will need to use the calendar objects to generate the recurrence sets. See the EWSoftware.PDI.Objects and EWSoftware.PDI.Properties namespaces for more information.

RRULE Property

EWSoftware.PDI.Recurrence Property

FREQ

Frequency

INTERVAL

Interval

BYMONTH

ByMonth

BYWEEKNO

ByWeekNo

BYYEARDAY

ByYearDay

BYMONTHDAY

ByMonthDay

BYDAY

ByDay

BYHOUR

ByHour

BYMINUTE

ByMinute

BYSECOND

BySecond

BYSETPOS

BySetPos

UNTIL

RecurUntil

COUNT

MaximumOccurrences

WKST

WeekStart

The following class properties have no matching recurrence rule parts.

  • The StartDateTime property is equivalent to the DTSTART property of a calendar object (VEVENT, VTODO, VJOURNAL). Set it to indicate the date and time at which the recurrence starts. Note that when recurrences are resolved via a calendar object, the calendar object will set the recurrence start date/time automatically in each of its rules based on its DTSTART value.

  • The CanOccurOnHoliday property is a custom rule that is not defined by the iCalendar specification. Use this rule extension to filter out recurrence instances that fall on a defined set of holiday dates. This rule is applied after all of the other standard recurrence rule parts have been applied.

  • The Holidays property defines the holiday dates that are used in conjunction with the CanOccurOnHoliday property. This property is static and the collection is shared by all instances of the Recurrence class.

When used separately from the calendar objects, the following Recurrence class methods can be used to define and generate recurrence instances:

  • The Parse method can be used to parse recurrence properties from a string in vCalendar basic grammar format or iCalendar RRULE format.

  • RecurDaily, RecurEveryWeekday, RecurWeekly, RecurMonthly, and RecurYearly can be used to quickly set up common recurrence patterns rather than setting the properties individually. They define patterns similar to those that can be defined in Microsoft Outlook. For finer control over the recurrence pattern, you can set the individual properties directly.

  • AllInstances can be used to return a collection of date/times that represent all instances generated by the recurrence pattern.

  • OccursOn can be used to see if a specific date/time is generated by the recurrence pattern.

  • InstancesBetween can be used to return a collection of date/times that represent all instances occurring between two specified dates/times.

  • NextInstance can be used to return a single instance of the recurrence that occurs on or after a specified date/time.

  • Although the Recurrence class is not itself a collection and does not implement the IEnumerable interface, it does provide a type-safe enumerator via the GetEnumerator method so that you can use a recurrence object in a foreach loop to enumerate the entire set of date/times generated by a recurrence pattern.

Recurrence Interval and Duration

The INTERVAL parameter defines the interval between occurrences (i.e. an interval of 2 on a daily pattern means every other day). The COUNT and UNTIL parameters are mutually exclusive and are used to define the duration of the recurrence pattern. If COUNT is specified, it defines the number of occurrences that will be generated. If UNTIL is defined, it defines the date/time after which no more instances will be generated. If neither is defined, the recurrence will continue forever.

Frequency and Rule Part Interactions

Below is a chart summarizing the effect of each recurrence rule part when used with the different recurrence frequencies. The iCalendar 2.0 specification is rather vague about the interactions of some of the rules when used together on the YEARLY frequency. It is likely that some combinations of frequency and rules are either not valid or do not make sense. However, the recurrence class will handle them as best it can based on examples given in the specification. The SECONDLY through WEEKLY frequencies are fairly straightforward. Rule parts representing units of time smaller than the frequency expand the number of instances generated. Rule parts representing units of time larger than or equal to the frequency filter the number of instances generated. The MONTHLY and YEARLY frequencies have some distinct variations and are explained in the sections below. For all frequencies, if a rule part is omitted, the corresponding value is take from the StartDate (DTSTART) property. For example, if no BYHOUR rule is specified, the hour for all instances generated is defaulted to the hour specified in StartDate (DTSTART).

The order of evaluation for the rule parts is from left to right. BYWEEKNO is only used by the YEARLY frequency. The BYSETPOS rule, not shown in the table, always filters the results for all frequencies and is always evaluated last.

 

BYMONTH

BYWEEKNO

BYYEARDAY

BYMONTHDAY

BYDAY

BYHOUR

BYMINUTE

BYSECOND

SECONDLY

Filter

N/A

Filter

Filter

Filter

Filter

Filter

Filter

MINUTELY

Filter

N/A

Filter

Filter

Filter

Filter

Filter

Expand

HOURLY

Filter

N/A

Filter

Filter

Filter

Filter

Expand

Expand

DAILY

Filter

N/A

Filter

Filter

Filter

Expand

Expand

Expand

WEEKLY

Filter

N/A

Filter

Filter

Expand

Expand

Expand

Expand

MONTHLY

Filter

N/A

Filter

Expand

Expand/Filter

Expand

Expand

Expand

YEARLY

Expand

Expand

Expand

Expand/Filter

Expand/Filter

Expand

Expand

Expand

MONTHLY Frequency Notes

If the BYMONTHDAY and BYDAY rules are specified together, the recurrence is expanded using BYMONTHDAY and then filtered using BYDAY. If only one or the other of those rules is present, the recurrence is expanded by that rule.

Examples from the iCalendar 2.0 specification (RFC 2445). The "r" variable is an instance of the Recurrence class:

 
Every other month on the 1st and last Sunday of the month for 10 occurrences:

    DTSTART:19970907T090000
    RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU

    r.StartDateTime = new DateTime(1997, 9, 7, 9, 0, 0);
    r.Frequency = RecurFrequency.Monthly;
    r.Interval = 2;
    r.MaximumOccurrences = 10;
    r.ByDay.AddRange(new DayInstance[] { new DayInstance(1, DayOfWeek.Sunday),
        new DayInstance(-1, DayOfWeek.Sunday) });

    ==> (1997 9:00 AM) September 7, 28; November 2, 30
        (1998 9:00 AM) January 4, 25; March 1, 29; May 3, 31

Monthly on the 2nd and 15th of the month for 10 occurrences:

    DTSTART:19970902T090000
    RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15

    r.StartDateTime = new DateTime(1997, 9, 2, 9, 0, 0);
    r.Frequency = RecurFrequency.Monthly;
    r.MaximumOccurrences = 10;
    r.ByMonthDay.AddRange(new int[] { 2, 15 });

    ==> (1997 9:00 AM) September 2, 15; October 2, 15; November 2, 15; December 2, 15
        (1998 9:00 AM) January 2, 15

Every Friday the 13th, forever.  Here, BYDAY is used to filter the BYMONTHDAY
rule rather than expand the frequency:

    DTSTART:19970902T090000
    RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13

    r.StartDateTime = new DateTime(1997, 9, 2, 9, 0, 0);
    r.Frequency = RecurFrequency.Monthly;
    r.ByDay.Add(DayOfWeek.Friday);
    r.ByMonthDay.Add(13);

    ==> (1998 9:00 AM) February 13; March 13; November 13
        (1999 9:00 AM) August 13
        (2000 9:00 AM) October 13

YEARLY Frequency Notes

For the YEARLY frequency, the BYMONTH, BYWEEKNO, and BYYEARDAY rules are expanded separately from each other if specified.

If the BYMONTH rule is specified, the following occurs. If the BYMONTHDAY and BYDAY rules are specified together, the BYMONTH recurrence set is expanded using BYMONTHDAY and then filtered using BYDAY. If only one or the other of those rules is present, the recurrence set is expanded by that rule. If the BYDAY rule is used as an expansion, the recurrence instances are expanded only in the months specified in the BYMONTH rule.

If the BYMONTH rule is not specified, the following occurs. The recurrence is expanded separately using the BYMONTHDAY rule if it is specified. If the BYDAY rule has been specified and the BYWEEKNO rule has not been specified, the recurrence is expanded separately using the BYDAY rule.

If the BYWEEKNO rule has been specified, it is expanded separately. If the BYDAY rule has been specified in conjunction with the BYWEEKNO rule, its recurrence set is expanded by the specified days of the week in the those week numbers.

If BYYEARDAY is specified, it is expanded separately.

Once all of the above expansions have taken place, each of the separate result sets is combined into a single set and it is expanded by the BYHOUR, BYMINUTE, and BYSECOND rules if they are specified.

Examples from the iCalendar 2.0 specification (RFC 2445). The "r" variable is an instance of the Recurrence class:

 
Every day in January, for 3 years:

    DTSTART:19980101T090000
    RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;
    BYDAY=SU,MO,TU,WE,TH,FR,SA

    r.StartDateTime = new DateTime(1998, 1, 1, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.RecurUntil = new DateTime(2000, 1, 31, 9, 0, 0);
    r.ByMonth.Add(1);

    ==> (1998 9:00 AM) January 1-31
        (1999 9:00 AM) January 1-31
        (2000 9:00 AM) January 1-31

Yearly in June and July for 10 occurrences:

    DTSTART:19970610T090000
    RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7

    r.StartDateTime = new DateTime(1997, 6, 10, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.MaximumOccurrences = 10;
    r.ByMonth.AddRange(new int[] { 6, 7 });

    ==> (1997 9:00 AM) June 10; July 10
        (1998 9:00 AM) June 10; July 10
        (1999 9:00 AM) June 10; July 10
        (2000 9:00 AM) June 10; July 10
        (2001 9:00 AM) June 10; July 10

    Note: Since none of the BYDAY, BYMONTHDAY or BYYEARDAY components are specified,
    the day of the month (10) is taken from DTSTART.

Every other year on January, February, and March for 10 occurrences:

    DTSTART:19970310T090000
    RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3

    r.StartDateTime = new DateTime(1997, 3, 10, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.Interval = 2;
    r.MaximumOccurrences = 10;
    r.ByMonth.AddRange(new int[] { 1, 2, 3 });

    ==> (1997 9:00 AM) March 10
        (1999 9:00 AM) January 10; February 10; March 10
        (2001 9:00 AM) January 10; February 10; March 10
        (2003 9:00 AM) January 10; February 10; March 10

Every 3rd year on the 1st, 100th and 200th day for 10 occurrences:

    DTSTART:19970101T090000
    RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200

    r.StartDateTime = new DateTime(1997, 1, 1, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.Interval = 3;
    r.MaximumOccurrences = 10;
    r.ByYearDay.AddRange(new int[] { 1, 100, 200 });

    ==> (1997 9:00 AM) January 1; April 10; July 19
        (2000 9:00 AM) January 1; April 9; July 18
        (2003 9:00 AM) January 1; April 10; July 19
        (2006 9:00 AM) January 1

Every 20th Monday of the year, forever:

    DTSTART:19970519T090000
    RRULE:FREQ=YEARLY;BYDAY=20MO

    r.StartDateTime = new DateTime(1997, 5, 19, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.ByDay.Add(20, DayOfWeek.Monday);

    ==> (1997 9:00 AM) May 19
        (1998 9:00 AM) May 18
        (1999 9:00 AM) May 17
        ...

Monday of week number 20 (where the default start of the week is Monday), forever:

    DTSTART:19970512T090000
    RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO

    r.StartDateTime = new DateTime(1997, 5, 12, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.ByWeekNo.Add(20);
    r.ByDay.Add(DayOfWeek.Monday);

    ==> (1997 9:00 AM) May 12
        (1998 9:00 AM) May 11
        (1999 9:00 AM) May 17
        ...

Every Thursday in March, forever:

    DTSTART:19970313T090000
    RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH

    r.StartDateTime = new DateTime(1997, 3, 13, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.ByMonth.Add(3);
    r.ByDay.Add(DayOfWeek.Thursday);

    ==> (1997 9:00 AM) March 13, 20, 27
        (1998 9:00 AM) March 5, 12, 19, 26
        (1999 9:00 AM) March 4, 11, 18, 25
        ...

Every Thursday, but only during June, July, and August, forever:

    DTSTART:19970605T090000
    RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8

    r.StartDateTime = new DateTime(1997, 6, 5, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.ByMonth.AddRange(new int[] { 6, 7, 8 });
    r.ByDay.Add(DayOfWeek.Thursday);

    ==> (1997 9:00 AM) June 5, 12, 19, 26; July 3, 10, 17, 24, 31; August 7, 14, 21, 28
        (1998 9:00 AM) June 4, 11, 18, 25; July 2, 9, 16, 23, 30; August 6, 13, 20, 27
        (1999 9:00 AM) June 3, 10, 17, 24; July 1, 8, 15, 22, 29; August 5, 12, 19, 26
        ...

Every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential
Election day):

    DTSTART:19961105T090000
    RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8

    r.StartDateTime = new DateTime(1996, 11, 5, 9, 0, 0);
    r.Frequency = RecurFrequency.Yearly;
    r.Interval = 4;
    r.ByMonth.Add(11);
    r.ByDay.Add(DayOfWeek.Tuesday);
    r.ByMonthDay.AddRange(new int[] { 2, 3, 4, 5, 6, 7, 8 });

    ==> (1996 9:00 AM) November 5
        (2000 9:00 AM) November 7
        (2004 9:00 AM) November 2
        ...

BYSETPOS Rule Notes

Once a recurrence has been expanded, the BYSETPOS rule is used to select specific instances from the set if it is specified. This occurs at each interval. For example:

 
The 2nd to last weekday of the month:

    DTSTART:19970929T090000
    RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2

    r.StartDateTime = new DateTime(1997, 9, 29, 9, 0, 0);
    r.Frequency = RecurFrequency.Monthly;
    r.BySetPos.Add(-2);
    r.ByDay.AddRange(new DayOfWeek[] { DayOfWeek.Monday, DayOfWeek.Tuesday,
        DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday });

    ==> (1997 9:00 AM) September 29; October 30; November 27; December 30
        (1998 9:00 AM) January 29; February 26; March 30
        ...

WKST Rule Notes

For recurrence rules, a week is defined as a seven day period that starts on the day specified by the WKST rule part. Week number one of the calendar year is the first seven day period starting on the specified day of the week that contains at least four (4) days in that calendar year.

Valid values for WKST are MO, TU, WE, TH, FR, SA and SU. The default value is MO (Monday). This is significant when a WEEKLY recurrence has an interval greater than one and a BYDAY rule part is specified. It is also significant in a YEARLY recurrence when a BYWEEKNO rule part is specified.

Examples from the iCalendar 2.0 specification (RFC 2445):

 
An example where the days generated makes a difference because of WKST:

    DTSTART:19970805T090000
    RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO

    // Uses default week start of Monday
    r.StartDateTime = new DateTime(1997, 8, 5, 9, 0, 0);
    r.Frequency = RecurFrequency.Weekly;
    r.Interval = 2;
    r.MaximumOccurrences = 4;
    r.ByDay.AddRange(new DayOfWeek[] { DayOfWeek.Tuesday, DayOfWeek.Sunday });

    ==> (1997)Aug 5, 10, 19, 24

Changing only WKST from MO to SU, yields different results:

    DTSTART:19970805T090000
    RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU

    r.StartDateTime = new DateTime(1997, 8, 5, 9, 0, 0);
    r.Frequency = RecurFrequency.Weekly;
    r.Interval = 2;
    r.MaximumOccurrences = 4;
    r.WeekStart = DayOfWeek.Sunday;
    r.ByDay.AddRange(new DayOfWeek[] { DayOfWeek.Tuesday, DayOfWeek.Sunday });

    ==> (1997)August 5, 17, 19, 31

When using Monday as the starting date, the first week is August 4th (Monday) to August 10th (Sunday). As such, it picks August 5th and August 10th, skips two weeks and picks up August 19th and August 24th and stops as it has its four days.

When using Sunday as the starting date, the first week is August 3rd (Sunday) to August 9th (Saturday). Since the pattern starts on August 5th (Tuesday), it won't pick up August 3rd as it is before the pattern start date. It then skips two weeks and picks up August 17th and August 19th, skips another two weeks and picks up August 31st and stops as it has its four days.

Duplicate and Invalid Dates

During the course of generating instances, duplicate and/or invalid date/times may be encountered. Such instances are discarded and will not become part of the final recurrence set. The returned set will only include unique occurrences of valid date/time values. For example, the following monthly recurrence pattern will not generate instances for months with less than 31 days:

 
The 31st day of every month:

    DTSTART:20041016T000000
    RRULE:FREQ=MONTHLY;COUNT=20;BYMONTHDAY=31

    r.StartDateTime = new DateTime(2004, 10, 16);
    r.Frequency = RecurFrequency.Monthly;
    r.MaximumOccurrences = 20;
    r.ByMonthDay.Add(31);

    ==> 10/31/2004, 12/31/2004, 01/31/2004, 03/31/2004, ...

Working Examples

See the RFC2445RecurTest demo application for many examples of setting up and using the Recurrence class in code. The demo shows using the Recurrence class by itself as well as in conjunction with the calendar classes with full time zone support. The Windows Forms demo application (PDIWinFormsTest) also contains some test forms that show examples of parsing and calculating recurrence patterns from iCalendar properties. In addition, it also demonstrates the RecurrencePattern user control and the RecurrencePropertiesDlg dialog box that can be used to modify recurrence properties in a more user-friendly fashion.

See Also

Other Resources