Recursive AI

Read an ICS calendar file in C#

All onemail recipes

Parse the file with ContentDocument.Parse, take the calendar, and call CalendarRecurrence.Expand for every occurrence in a date range. Repeat rules, exceptions, moved instances and time zones are applied for you. The program below reads a small calendar with a weekly meeting that crosses the US daylight saving change.

Terminal
dotnet new console -o ReadIcs && cd ReadIcs
dotnet add package RecursiveAI.OneMail.Calendar --version 0.2.0-preview.2

Save this as team.ics: a standup on Mondays and Wednesdays in New York time, eight times, minus March 10, with March 15 moved to the afternoon, and an all-day launch.

Text
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//Team calendar//EN
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20270101T000000Z
SUMMARY:Team standup
LOCATION:Room 4
DTSTART;TZID=America/New_York:20270301T093000
DTEND;TZID=America/New_York:20270301T094500
RRULE:FREQ=WEEKLY;BYDAY=MO,WE;COUNT=8
EXDATE;TZID=America/New_York:20270310T093000
END:VEVENT
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20270101T000000Z
RECURRENCE-ID;TZID=America/New_York:20270315T093000
SUMMARY:Team standup (moved)
DTSTART;TZID=America/New_York:20270315T140000
DTEND;TZID=America/New_York:20270315T141500
END:VEVENT
BEGIN:VEVENT
UID:[email protected]
DTSTAMP:20270101T000000Z
SUMMARY:Launch day
DTSTART;VALUE=DATE:20270322
END:VEVENT
END:VCALENDAR

Put this in Program.cs and run dotnet run:

C#
using RecursiveAI.OneMail.Calendar;
using RecursiveAI.OneMail.Calendar.Ics;
using RecursiveAI.OneMail.Calendar.Recurrence;
 
ContentDocument doc = ContentDocument.Parse(File.ReadAllBytes("team.ics"));
IcsComponent calendar = IcsDocument.Calendars(doc).First();
 
// The events as written: one entry per VEVENT
foreach (IcsComponent ev in calendar.Events)
    Console.WriteLine($"{ev.Summary,-21} {ev.Start,-16} {ev.GetText("RRULE") ?? "-"}");
 
// Every occurrence: RRULE, EXDATE and moved instances applied, time zones resolved
Console.WriteLine();
var window = new OccurrenceWindow(new DateTime(2027, 3, 1), new DateTime(2027, 4, 1));
foreach (Occurrence o in CalendarRecurrence.Expand(calendar, window))
{
    string utc = o.StartUtc is { } u ? $"{u:yyyy-MM-dd HH:mm} UTC" : "all day";
    string format = o.Start.IsDate ? "ddd yyyy-MM-dd" : "ddd yyyy-MM-dd HH:mm";
    string local = o.Start.Value.ToString(format);
    Console.WriteLine($"{local,-20}  {utc,-20}  {o.Component.Summary}");
}
Text
Team standup          20270301T093000  FREQ=WEEKLY;BYDAY=MO,WE;COUNT=8
Team standup (moved)  20270315T140000  -
Launch day            20270322         -
 
Mon 2027-03-01 09:30  2027-03-01 14:30 UTC  Team standup
Wed 2027-03-03 09:30  2027-03-03 14:30 UTC  Team standup
Mon 2027-03-08 09:30  2027-03-08 14:30 UTC  Team standup
Mon 2027-03-15 14:00  2027-03-15 18:00 UTC  Team standup (moved)
Wed 2027-03-17 09:30  2027-03-17 13:30 UTC  Team standup
Mon 2027-03-22        all day               Launch day
Mon 2027-03-22 09:30  2027-03-22 13:30 UTC  Team standup
Wed 2027-03-24 09:30  2027-03-24 13:30 UTC  Team standup

The three VEVENTs become eight occurrences: the rule’s eight dates, minus the excluded March 10, with March 15 replaced by its moved copy. The standup stays at 9:30 in New York while its UTC time moves from 14:30 to 13:30 after daylight saving starts on March 14. All-day events have no UTC instant.

What you get

  • The typed view. IcsComponent has Summary, Start, End, Location, Uid and the rest. GetText reads any property.
  • Time zones. A zone resolves from a VTIMEZONE in the file, the IANA database, or a Windows zone name.
  • Lossless. An untouched file writes back byte for byte (doc.Write(stream)), and only the lines you edit are rewritten.
  • No exceptions on bad input. Repairs and guesses are collected in an ExpansionReport you can pass to Expand.

Limits

  • A preview. 0.2.0-preview.2: the API can change before 1.0.
  • Mind the window. A rule with no COUNT or UNTIL never ends. Expansion stops at the window’s end, and ExpansionOptions caps the count.
  • RSCALE is refused. Non-Gregorian rules (RFC 7529) aren’t expanded.
  • Windows. IANA names resolve through the Windows zone each maps to, so US dates before 2007 follow today’s rules there.
  • Outlook appointments in PST and MSG files aren’t read as events yet.

Every rule and choice: Recurrence and time zones. The parser: ICS and vCard.