firstfloor_calendar 1.0.4 copy "firstfloor_calendar: ^1.0.4" to clipboard
firstfloor_calendar: ^1.0.4 copied to clipboard

iCalendar parsing that just works. RFC 5545 compliant with full recurrence support.

firstfloor_calendar #

Pub Package codecov License: MIT

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.

3
likes
0
points
17
downloads

Publisher

verified publisherfirstfloorsoftware.com

Weekly Downloads

iCalendar parsing that just works. RFC 5545 compliant with full recurrence support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

collection, timezone

More

Packages that depend on firstfloor_calendar