firstfloor_calendar 1.0.4
firstfloor_calendar: ^1.0.4 copied to clipboard
iCalendar parsing that just works. RFC 5545 compliant with full recurrence support.
firstfloor_calendar #
A Dart library for parsing and working with iCalendar (.ics) files. Built with RFC 5545 compliance in mind, firstfloor_calendar provides a two-layer architecture that offers both low-level document access for custom processing and a high-level semantic API for type-safe calendar operations. Whether you're building a calendar app, processing meeting invites, or managing recurring events, this library gives you the tools to work with iCalendar data efficiently and correctly.
Table of Contents #
Features #
- Parse iCalendar files into strongly typed models
- Support for events, todos, journals, and timezones
- Full RRULE recurrence expansion
- Memory-efficient streaming for large files
- Extensible with custom property parsers
Installation #
dependencies:
firstfloor_calendar: ^1.0.0
Usage #
Basic Parsing #
Parse iCalendar text into a strongly typed Calendar object. The parser handles all RFC 5545 components including events, todos, journals, and timezones.
import 'package:firstfloor_calendar/firstfloor_calendar.dart';
final ics = '''
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//EN
BEGIN:VEVENT
UID:event-123@example.com
DTSTAMP:20240315T090000Z
DTSTART:20240315T100000Z
DTEND:20240315T110000Z
SUMMARY:Team Meeting
END:VEVENT
END:VCALENDAR''';
final parser = CalendarParser();
final calendar = parser.parseFromString(ics);
for (final event in calendar.events) {
print('${event.summary}: ${event.dtstart}');
}
Working with Events #
Access event properties with full type safety. Required properties like uid and dtstart are non-nullable, while optional properties return nullable values.
final event = calendar.events.first;
// Required properties
print('UID: ${event.uid}');
print('Start: ${event.dtstart}');
// Optional properties
print('Summary: ${event.summary ?? "Untitled"}');
print('Location: ${event.location ?? "No location"}');
print('Description: ${event.description ?? ""}');
// Attendees
for (final attendee in event.attendees) {
print('Attendee: ${attendee.address}');
}
Working with Timezones #
Handle timezone-aware dates using the timezone package. Initialize timezones before parsing calendars with timezone identifiers.
import 'package:timezone/data/latest.dart' as tz;
// Initialize timezone database (call once at app startup)
tz.initializeTimeZones();
final ics = '''
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//EN
BEGIN:VEVENT
UID:tz-event@example.com
DTSTAMP:20240315T120000Z
DTSTART;TZID=America/New_York:20240315T090000
DTEND;TZID=America/New_York:20240315T100000
SUMMARY:Morning Meeting
END:VEVENT
END:VCALENDAR''';
final parser = CalendarParser();
final calendar = parser.parseFromString(ics);
final event = calendar.events.first;
// Access timezone-aware datetime
print('Start: ${event.dtstart}');
print('Timezone: ${event.dtstart?.dateTime?.timeZone.name}');
// Convert to different timezone
final berlinTime = event.dtstart?.dateTime?.toTimeZone(tz.getLocation('Europe/Berlin'));
print('Berlin time: $berlinTime');
Recurring Events #
Generate occurrences from recurrence rules (RRULE). The occurrences() method returns a lazy stream that handles both recurring and non-recurring events gracefully.
final event = calendar.events.first;
// Get first 10 occurrences
for (final occurrence in event.occurrences().take(10)) {
print('Occurrence: $occurrence');
}
Filtering Events by Date Range #
Use the inRange extension to filter events that occur within a specific date range. This works correctly with multi-day events, all-day events, and recurring events. Results are always returned in chronological order, regardless of the order in the source file.
final start = CalDateTime.date(2024, 3, 1);
final end = CalDateTime.date(2024, 3, 31);
// Get all event occurrences in March 2024
final occurrencesInMarch = calendar.events.inRange(start, end);
for (final result in occurrencesInMarch) {
print('${result.event.summary}: ${result.occurrence}');
}
// Works with todos and journals too
final todoOccurrences = calendar.todos.inRange(start, end);
Example: Chronological ordering across multiple events
final ics = '''
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//EN
BEGIN:VEVENT
UID:event3@example.com
DTSTAMP:20240301T000000Z
DTSTART:20240315T140000
SUMMARY:Afternoon Meeting
END:VEVENT
BEGIN:VEVENT
UID:event1@example.com
DTSTAMP:20240301T000000Z
DTSTART:20240310T090000
SUMMARY:Early Meeting
END:VEVENT
BEGIN:VEVENT
UID:event2@example.com
DTSTAMP:20240301T000000Z
DTSTART:20240312T100000
SUMMARY:Mid-Month Standup
END:VEVENT
END:VCALENDAR''';
final parser = CalendarParser();
final calendar = parser.parseFromString(ics);
// Events are automatically ordered chronologically
final start = CalDateTime.date(2024, 3, 1);
final end = CalDateTime.date(2024, 3, 31);
for (final result in calendar.events.inRange(start, end)) {
print('${result.occurrence}: ${result.event.summary}');
}
// Output (chronologically sorted despite unordered source):
// 2024-03-10 09:00:00: Early Meeting
// 2024-03-12 10:00:00: Mid-Month Standup
// 2024-03-15 14:00:00: Afternoon Meeting
Streaming Large Files #
Parse large iCalendar files efficiently using the streaming parser. Components are processed one at a time without loading the entire file into memory.
import 'dart:io';
final file = File('large-calendar.ics');
final streamParser = DocumentStreamParser();
await for (final component in streamParser.parseComponents(file.openRead())) {
if (component.name == 'VEVENT') {
final summary = component.properties
.where((p) => p.name == 'SUMMARY')
.firstOrNull
?.value;
print('Event: ${summary ?? "Untitled"}');
}
}
Conditional Parsing with Stream Parser #
Process large files and selectively convert components to typed models based on specific criteria.
import 'dart:io';
final file = File('large-calendar.ics');
final streamParser = DocumentStreamParser();
final events = <EventComponent>[];
await for (final component in streamParser.parseComponents(file.openRead())) {
if (component.name == 'VEVENT') {
// Check for a specific condition before parsing
final status = component.properties
.where((p) => p.name == 'STATUS')
.firstOrNull
?.value;
// Only convert confirmed events to typed models
if (status == 'CONFIRMED') {
final event = component.toEvent();
events.add(event);
}
}
}
print('Found ${events.length} confirmed events');
for (final event in events) {
print('${event.summary}: ${event.dtstart}');
}
Custom Property Parsers #
Extend the parser with custom property handlers for vendor-specific or experimental properties. Register custom parsers before parsing your calendar data.
final parser = CalendarParser();
parser.registerPropertyRule(
componentName: 'VEVENT',
propertyName: 'X-CUSTOM-PRIORITY',
rule: PropertyRule(
parser: (property) {
final value = int.tryParse(property.value);
if (value == null || value < 1 || value > 10) {
throw ParseException(
'X-CUSTOM-PRIORITY must be between 1-10',
lineNumber: property.lineNumber,
);
}
return value;
},
),
);
final calendar = parser.parseFromString(ics);
final priority = calendar.events.first.value<int>('X-CUSTOM-PRIORITY');
Architecture #
The library uses a two-layer architecture that separates parsing concerns and provides flexibility for different use cases:
Document Layer #
The Document Layer (DocumentParser and DocumentStreamParser) handles the low-level parsing of iCalendar text. It:
- Parses .ics files into an untyped tree structure (
CalendarDocument) - Handles line unfolding, property parsing, and component nesting
- Provides streaming capabilities for large files via
DocumentStreamParser - Performs no semantic validation - just structural parsing
- Returns raw components (
CalendarDocumentComponent) and properties (CalendarProperty)
Use the Document Layer when you need:
- Low-level access to raw iCalendar data
- Custom validation or transformation logic
- Memory-efficient streaming of large files
- Access to non-standard or vendor-specific properties
Semantic Layer #
The Semantic Layer (CalendarParser) builds on top of the Document Layer to provide type-safe models. It:
- Converts document components into strongly typed models (
EventComponent,TodoComponent, etc.) - Validates property values according to RFC 5545
- Provides type-safe access to properties with proper nullability
- Supports custom property parsers via
registerPropertyRule - Handles recurrence rule expansion and date calculations
Use the Semantic Layer when you need:
- Type-safe business logic and calendar operations
- RFC 5545 validation and compliance checking
- Convenient access to common properties
- Recurrence rule processing and occurrence generation
Layer Interaction #
The layers work together seamlessly:
// Parse at document level
final document = DocumentParser().parse(ics);
// Optionally inspect/transform document
// ... custom logic ...
// Convert to semantic models
final calendar = CalendarParser().parseDocument(document);
// Or go directly to semantic layer
final calendar = CalendarParser().parseFromString(ics);
You can also bridge from document to semantic selectively using extension methods like toEvent(), toTodo(), etc.
License #
MIT License - see LICENSE file for details.