-
Notifications
You must be signed in to change notification settings - Fork 247
Working with recurring elements
Preface: It's impossible to provide an example for every type of recurrence scenario, so here are a few that represent some common use cases.
If you create an event or alarm that happens more than once, it typically has a start time, an end time, and some rules about how and when it repeats:
- "Daily, forever"
- "Daily, forever — except for a certain date"
- "Every other Tuesday until the end of the year"
- "The fourth Thursday of every November"
You then want to search for occurrences of that event during a given time period. This triggers the machinery that generates the set of occurrences matching your search criteria.
It's like creating a normal event, but you add one or more RecurrencePattern
objects to the event to make it recurring.
Note
Make sure the event's start date is the first occurrence in the series.
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Daily,
Interval = 2,
Count = 5
// Add other properties like ByDay, ByMonth, etc.
};
var calendarEvent = new CalendarEvent
{
DtStart = new CalDateTime(2025, 07, 10),
// Add the rule to the event.
RecurrenceRules = [recurrence]
};
// Get all occurrences of the series.
IEnumerable<Occurrence> allOccurrences = calendarEvent.GetOccurrences();
Assert.That(allOccurrences.Count(), Is.EqualTo(5));
5 occurrences:
Start: 07/10/2025
Period: P1D
End: 07/11/2025
Start: 07/12/2025
Period: P1D
End: 07/13/2025
Start: 07/14/2025
Period: P1D
End: 07/15/2025
Start: 07/16/2025
Period: P1D
End: 07/17/2025
Start: 07/18/2025
Period: P1D
End: 07/19/2025
After creating a recurring event, you can call GetOccurrences()
to get all occurrences in the series, or GetOccurrences(CalDateTime startTime)
to get all occurrences after a given date. You can further limit the evaluation by adding TakeWhileBefore(CalDateTime periodEnd)
to get occurrences up to a specific point in time.
IEnumerable<Occurrence> occurrences =
calendarEvent.GetOccurrences().TakeWhileBefore(CalDateTime periodEnd);
Note
The end date provided to the TakeWhileBefore
method is exclusive.
Warning
Calculating a series with no end may result in an EvaluationOutOfRangeException
. Make sure you use TakeWhileBefore
to avoid this.
Tip
You can call the GetOccurrences
method on both CalendarEvent
and Calendar
.
Suppose you want to create an event for July 10, between 09:00 and 10:00, that recurs every other day for 2 occurrences.
// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Daily,
Interval = 2,
Count = 2
};
var calendarEvent = new CalendarEvent
{
DtStart = start,
DtEnd = start.AddHours(1),
RecurrenceRules = [recurrence]
};
// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);
// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
Assert.That(occurrences.Count(), Is.EqualTo(2));
ICS string:
BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250710T100000
DTSTART;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=DAILY;INTERVAL=2;COUNT=2
END:VEVENT
END:VCALENDAR
Occurrences:
2 occurrences:
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/10/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/12/2025 10:00:00 +02:00 Europe/Zurich
Suppose you want to create a series of events for July 10 and 12, between 09:00 and 10:00, that recurs every year for two years.
Warning
The UNTIL
date must have either UTC or no time zone.
Note
The UNTIL
date is inclusive.
// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Yearly,
ByMonthDay = [10, 12],
// 2027-07-10 09:00:00 Europe/Zurich (07:00:00 UTC)
Until = start.AddYears(2).ToTimeZone("UTC")
};
var calendarEvent = new CalendarEvent
{
DtStart = start,
DtEnd = start.AddHours(1),
RecurrenceRules = [recurrence]
};
// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);
// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
ICS string:
BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250710T100000
DTSTART;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=YEARLY;UNTIL=20270710T070000Z;BYMONTHDAY=10,12
END:VEVENT
END:VCALENDAR
Occurrences:
5 occurrences:
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/10/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/12/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/10/2026 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/10/2026 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2026 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/12/2026 10:00:00 +02:00 Europe/Zurich
Start: 07/10/2027 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/10/2027 10:00:00 +02:00 Europe/Zurich
Tip
Notice the last occurrence has the same start as the UNTIL
date of the recurrence. This is the meaning of 'inclusive' (valid as long as DTSTART
<= UNTIL
).
Suppose you decide to play poker with your friends every last Sunday of the month for the next three months, and also on July 10th just to stay sharp.
// Create the CalendarEvent
var start = new CalDateTime(2025, 06, 29, 16, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Monthly,
ByDay = [new(DayOfWeek.Sunday, FrequencyOccurrence.Last)],
Count = 3
};
var calendarEvent = new CalendarEvent
{
DtStart = start,
DtEnd = start.AddHours(4),
RecurrenceRules = [recurrence],
};
// Add additional an occurrence to the series.
calendarEvent.RecurrenceDates
.Add(new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich"));
// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);
// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
ICS string:
BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250629T200000
DTSTART;TZID=Europe/Zurich:20250629T160000
RDATE;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=-1SU
END:VEVENT
END:VCALENDAR
Note
See the RDATE
property, which defines our additional occurrence.
Occurrences:
4 occurrences:
Start: 06/29/2025 16:00:00 +02:00 Europe/Zurich
Period: PT4H
End: 06/29/2025 20:00:00 +02:00 Europe/Zurich
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
Period: PT4H
End: 07/10/2025 13:00:00 +02:00 Europe/Zurich
Start: 07/27/2025 16:00:00 +02:00 Europe/Zurich
Period: PT4H
End: 07/27/2025 20:00:00 +02:00 Europe/Zurich
Start: 08/31/2025 16:00:00 +02:00 Europe/Zurich
Period: PT4H
End: 08/31/2025 20:00:00 +02:00 Europe/Zurich
Note
We only specified the start of our additional occurrence; the duration is inherited from the base series.
Suppose you decide to read for 15 minutes every full hour until midnight — except at 22:00, when you have dinner.
// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 20, 00, 00, "UTC");
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Hourly,
Until = start.AddHours(4)
};
var calendarEvent = new CalendarEvent
{
DtStart = start,
DtEnd = start.AddMinutes(15),
RecurrenceRules = [recurrence],
};
// Add the exception date to the series.
calendarEvent.ExceptionDates
.Add(new CalDateTime(2025, 07, 10, 22, 00, 00, "UTC"));
// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);
// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
ICS string:
BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND:20250710T201500Z
DTSTART:20250710T200000Z
EXDATE:20250710T220000Z
RRULE:FREQ=HOURLY;UNTIL=20250711T000000Z
END:VEVENT
END:VCALENDAR
Note
See the EXDATE
property, which defines our removed occurrence.
Occurrences:
4 occurrences:
Start: 07/10/2025 20:00:00 +00:00 UTC
Period: PT15M
End: 07/10/2025 20:15:00 +00:00 UTC
Start: 07/10/2025 21:00:00 +00:00 UTC
Period: PT15M
End: 07/10/2025 21:15:00 +00:00 UTC
Start: 07/10/2025 23:00:00 +00:00 UTC
Period: PT15M
End: 07/10/2025 23:15:00 +00:00 UTC
Start: 07/11/2025 00:00:00 +00:00 UTC
Period: PT15M
End: 07/11/2025 00:15:00 +00:00 UTC
Suppose you decide to go for a walk every other day at 09:00 (4 times) — but not on the third occurrence, when you have a packed schedule and can only go for 13 minutes at 13:00.
Since the start, end, duration, and title of the event are changing, you need to create a new event and tell the series to replace an occurrence with this 'special' event (child event). Link the child event to the series by using the same UID
, and link it to the original occurrence by adding the original occurrence's start date in the RECURRENCE-ID
property.
Note
Link moved events:
- Create a
CalendarEvent
with the changes (child event). - Link the child event to the series by using the same
Uid
(UID
). - Link the child event to the original occurrence date by using the
RecurrenceId
(RECURRENCE-ID
).
// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Daily,
Interval = 2,
Count = 4
};
var calendarEvent = new CalendarEvent
{
// UID links master with child.
Uid = "my-custom-id",
Summary = "Walking",
DtStart = start,
DtEnd = start.AddHours(1),
RecurrenceRules = [recurrence],
Sequence = 0 // default value
};
var startMoved = new CalDateTime(2025, 07, 13, 13, 00, 00, "Europe/Zurich");
var movedEvent = new CalendarEvent
{
// UID links master with child.
Uid = "my-custom-id",
// Overwrite properties of the original occurrence.
Summary = "Short after lunch walk",
// Set new start and end time.
DtStart = startMoved,
DtEnd = startMoved.AddMinutes(13),
// Set the original date of the occurrence (2025-07-14 09:00:00).
RecurrenceId = start.AddDays(4),
// The first change for this RecurrenceId
Sequence = 1
};
// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
calendar.Events.Add(movedEvent);
// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);
// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
ICS-string:
BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250710T100000
DTSTART;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=DAILY;INTERVAL=2;COUNT=4
SUMMARY:Walking
UID:my-custom-id
END:VEVENT
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250713T131300
DTSTART;TZID=Europe/Zurich:20250713T130000
RECURRENCE-ID;TZID=Europe/Zurich:20250714T090000
SUMMARY:Short after lunch walk
UID:my-custom-id
END:VEVENT
END:VCALENDAR
Note
See the SUMMARY
property, which we have overridden.
Occurrences:
4 occurrences:
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/10/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/12/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/13/2025 13:00:00 +02:00 Europe/Zurich
Period: PT13M
End: 07/13/2025 13:13:00 +02:00 Europe/Zurich
Start: 07/16/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 07/16/2025 10:00:00 +02:00 Europe/Zurich
Suppose you want to create a weekly meeting every Monday at 09:00 in the "Europe/Zurich" time zone, starting before a daylight saving time change and continuing after. The event's local time remains 09:00, but the UTC offset changes due to daylight saving.
// Create the CalendarEvent
var start = new CalDateTime(2025, 03, 24, 09, 00, 00, "Europe/Zurich"); // Before DST starts
var recurrence = new RecurrencePattern
{
Frequency = FrequencyType.Weekly,
Count = 3 // Three Mondays: before, on, and after DST change
};
var calendarEvent = new CalendarEvent
{
DtStart = start,
DtEnd = start.AddHours(1),
RecurrenceRules = [recurrence]
};
// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);
// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
ICS string:
BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250324T100000
DTSTART;TZID=Europe/Zurich:20250324T090000
RRULE:FREQ=WEEKLY;COUNT=3
END:VEVENT
END:VCALENDAR
Occurrences:
3 occurrences:
Start: 03/24/2025 09:00:00 +01:00 Europe/Zurich
Period: PT1H
End: 03/24/2025 10:00:00 +01:00 Europe/Zurich
Start: 03/31/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 03/31/2025 10:00:00 +02:00 Europe/Zurich
Start: 04/07/2025 09:00:00 +02:00 Europe/Zurich
Period: PT1H
End: 04/07/2025 10:00:00 +02:00 Europe/Zurich
Note
Notice how the UTC offset changes from +01:00 to +02:00 after the daylight saving time change, while the local time remains consistent at 09:00.
var rrule1 = new RecurrencePattern(FrequencyType.Weekly, 2)
{
Until = new CalDateTime(2026, 1, 1)
};
var rrule2 = new RecurrencePattern(FrequencyType.Monthly)
{
ByMonthDay = [2], // Your day of the month goes here
Count = 5
};
// The 4th Thursday of November every year
var rrule3 = new RecurrencePattern(FrequencyType.Yearly, 1)
{
Frequency = FrequencyType.Yearly,
Interval = 1,
ByMonth = [11],
ByDay = [new WeekDay { DayOfWeek = DayOfWeek.Thursday, Offset = 4 }],
};
// Every day in 2025, except Sundays
var rrule4 = new RecurrencePattern(FrequencyType.Daily)
{
// Start: 2025-01-01, End: 2025-12-31
Until = new CalDateTime(2025, 12, 31),
// Exclude Sundays
ByDay = [
new WeekDay(DayOfWeek.Monday),
new WeekDay(DayOfWeek.Tuesday),
new WeekDay(DayOfWeek.Wednesday),
new WeekDay(DayOfWeek.Thursday),
new WeekDay(DayOfWeek.Friday),
new WeekDay(DayOfWeek.Saturday)
]
};
Yes, you can. The RecurrenceRules
property is a list of RecurrencePattern
objects, so you can add as many as you need.