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.
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.
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:
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}"); }
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.
IcsComponenthasSummary,Start,End,Location,Uidand the rest.GetTextreads any property. - Time zones. A zone resolves from a
VTIMEZONEin 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
ExpansionReportyou can pass toExpand.
Limits
- A preview. 0.2.0-preview.2: the API can change before 1.0.
- Mind the window. A rule with no
COUNTorUNTILnever ends. Expansion stops at the window’s end, andExpansionOptionscaps 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.