icalendar: a Ruby library for reading and writing RFC 5545 calendars
icalendar.rb main repository
At a glance
- What is it?
- A Ruby library for the iCalendar format, with a component tree that mirrors the specification, property parameters handled as first-class values, and support for building VTIMEZONE blocks from tzinfo data. The main friction for an existing user is the 1.x to 2.0 break, which the README documents only as a diff to the README itself.
- Who is it for?
- icalendar suits a Ruby service that generates or consumes iCalendar feeds, where a calendar is assembled from a database and handed to a client, and where the specification's parameter handling matters because real calendars carry parameters like ALTREP and LANGUAGE.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A component tree that mirrors the specification
iCalendar is a Ruby library for dealing with iCalendar files in the format defined by RFC 5545. The tagline in the README header is Internet calendaring, Ruby style.
The API is a component tree, and the naming follows the specification's vocabulary. You create a `Icalendar::Calendar`, add `Icalendar::Event` objects to it, attach `Icalendar::Alarm` components to events, and add `Icalendar::Timezone` blocks to the calendar.
The basic case is short:
require 'icalendar'
# Create a calendar with an event (standard method)
cal = Icalendar::Calendar.new
cal.event do |e|
e.dtstart = Icalendar::Values::Date.new('20050428')
e.dtend = Icalendar::Values::Date.new('20050429')
e.summary = "Meeting with the man."
e.description = "Have a long lunch meeting and decide nothing..."
e.ip_class = "PRIVATE"
end
cal.publishThe `cal.event` block is the primary construction path. Inside it, properties are assigned by name, and the property names are the lowercase forms of the specification's property names, so `dtstart` for DTSTART, `dtend` for DTEND and `ip_class` for the CLASS property, which carries a privacy classification such as PRIVATE.
There is a second construction style for when you already have an event object:
event = Icalendar::Event.new
event.dtstart = DateTime.civil(2006, 6, 23, 8, 30)
event.summary = "A great event!"
cal.add_event(event)
event2 = cal.event # This automatically adds the event to the calendar
event2.dtstart = DateTime.civil(2006, 6, 24, 8, 30)
event2.summary = "Another great event!"The comment on the last line captures the difference: `cal.event` without a block returns an event that is already attached to the calendar, while `cal.add_event` attaches one you built yourself.
Property parameters are values, not strings
The feature that distinguishes this library from a hash-and-string implementation is that property parameters are handled as first-class values.
A parameter in iCalendar is a key and value pair attached to a property, such as ALTREP or LANGUAGE on a text value. The library stores them on the value object and lets you read them back:
params = {"altrep" => "http://my.language.net", "language" => "SPANISH"}
event = cal.event do |e|
e.dtstart = Icalendar::Values::Date.new('20050428')
e.dtend = Icalendar::Values::Date.new('20050429')
e.summary = Icalendar::Values::Text.new "This is a summary with params.", params
end
event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'}And the same thing can be done after the fact, by assigning to `ical_params` on the property:
event = cal.event do |e|
e.dtstart = Icalendar::Values::Date.new('20050428')
e.dtend = Icalendar::Values::Date.new('20050429')
e.summary = "This is a summary with params."
e.summary.ical_params = params
end
event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'}The value classes are per type. `Icalendar::Values::Date`, `Icalendar::Values::DateTime`, `Icalendar::Values::Text` and `Icalendar::Values::Uri` are separate classes, each knowing how to serialise itself and how to carry parameters. The `Values` namespace is the place to look when you need to understand what a property will look like on the wire.
The parameters matter in practice because real calendars are not clean. A summary with a language tag, a URI with a content type, a date with a timezone identifier: these are the shapes that appear in feeds produced by other calendar applications.
DateOrDateTime for when you do not know which you have
One specific modelling problem gets its own class. Sometimes you do not care whether an event's start or end is a `Date` or a `DateTime`, because the data came from a source that varies. For that, the library offers `DateOrDateTime.new(value)`, and calling `.call` on the returned object immediately returns the underlying `Date` or `DateTime`.
event = cal.event do |e|
e.dtstart = Icalendar::Values::DateOrDateTime.new('20140924')
e.dtend = Icalendar::Values::DateOrDateTime.new('20140925').call
e.summary = 'This is an all-day event, because DateOrDateTime will return Dates'
endThe distinction is not cosmetic in iCalendar. A DATE value is an all-day event and a DATE-TIME value is a timed one, and a client renders them differently. A feed generator that always emits a DATE-TIME for a date the user entered as a whole day produces a calendar where every event looks like it starts at midnight, which is wrong in a way that is easy to miss in testing.
The `call` method is the escape hatch. In the example the dtstart keeps the wrapper and the dtend unwraps it, and the summary explains the result, that the event is an all-day event because DateOrDateTime returns Dates.
The library also supports attaching a URL to an event, for clients that can parse and display one:
event = cal.event do |e|
e.url = 'https://example.com'
endAlarms, and the get method that creates one
Alarms are the VALARM component, attached to an event through a block in the same style as events.
cal.event do |e|
# ...other event properties
e.alarm do |a|
a.action = "EMAIL"
a.description = "This is an event reminder" # email body (required)
a.summary = "Alarm notification" # email subject (required)
a.attendee = %w(mailto:[email protected] mailto:[email protected]) # one or more email recipients (required)
a.append_attendee "mailto:[email protected]"
a.trigger = "-PT15M" # 15 minutes before
a.append_attach Icalendar::Values::Uri.new("ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary") # email attachments (optional)
end
endThe comments mark which properties are required for an EMAIL alarm, namely the description as the body, the summary as the subject and at least one attendee. The `append_attendee` and `append_attach` methods add further values, so an alarm can carry several recipients and several attachments, and the URI value carries its own parameter, the fmttype in this case.
The other two action types are DISPLAY and AUDIO, and the second example notes that setting the action is not necessary for DISPLAY because it is the default:
e.alarm do |a|
a.action = "DISPLAY" # This line isn't necessary, it's the default
a.summary = "Alarm notification"
a.trigger = "-P1DT0H0M0S" # 1 day before
endThere is a subtlety in the API that is documented explicitly, and it is the kind of thing that bites. Calling the `event.alarm` method will create an alarm if one does not exist. So to check whether an event has an alarm, you must use `has_alarm?`:
event.has_alarm?
# => false
event.alarm
# => #<Icalendar::Alarm ... >
event.has_alarm?
#=> trueThe trigger values are ISO 8601 durations relative to the event, so `-PT15M` is fifteen minutes before and `-P1DT0H0M0S` is one day before.
Timezones, and tzinfo support you have to opt into
A VTIMEZONE block declares the offset rules for a zone rather than referring to a system database, and the library lets you build one by hand:
cal = Icalendar::Calendar.new
cal.timezone do |t|
t.tzid = "America/Chicago"
t.daylight do |d|
d.tzoffsetfrom = "-0600"
d.tzoffsetto = "-0500"
d.tzname = "CDT"
d.dtstart = "19700308T020000"
d.rrule = "FREQ=YEARLY;BYMONTH=3;BYDAY=2SU"
end
t.standard do |s|
s.tzoffsetfrom = "-0500"
s.tzoffsetto = "-0600"
s.tzname = "CST"
s.dtstart = "19701101T020000"
s.rrule = "FREQ=YEARLY;BYMONTH=11;BYDAY=1SU"
end
endThe `daylight` and `standard` sub-components hold the offset from and to, the zone name, the start instant and an RRULE for the transition, and the rrules here are the classic second Sunday in March and first Sunday in November.
Writing that by hand is only half the story, because iCalendar has some basic support for creating VTIMEZONE blocks from timezone information pulled from `tzinfo`, and you must require that support manually to take advantage of it:
require 'icalendar/tzinfo'
cal = Icalendar::Calendar.new
event_start = DateTime.new 2008, 12, 29, 8, 0, 0
event_end = DateTime.new 2008, 12, 29, 11, 0, 0
tzid = "America/Chicago"
tz = TZInfo::Timezone.get tzid
timezone = tz.ical_timezone event_start
cal.add_timezone timezoneThe generated block is derived from the real rules for the given instant, which is the case where a hand-written VTIMEZONE is most likely to be wrong. The README states that iCalendar has been tested and works with `tzinfo` versions 0.3, 1.x and 2.x, and that the `tzinfo-data` gem may also be required depending on your version of `tzinfo` and potentially your operating system.
The opt-in nature of the require is a deliberate choice, since it keeps `tzinfo` out of the hard dependency list, and it is the sort of thing that surprises a user who assumes the timezone support is there.
Output, and the upgrade note that is a diff
The calendar serialises to a string through `to_ical`:
cal_string = cal.to_ical
puts cal_stringThe same is true of the individual components, and the README shows the output of the alarm example as the folded lines a client would read, with the VALARM blocks, their actions, triggers, attendees and attachments.
The documentation for moving from 1.x to 2.0 is the weakest part of the README, and the author says so. Better documentation is still to come, and in the meantime the changes needed to move from 1.x to 2.0 are summarized by the diff needed to update the README, with a link to the commit that made that change. So the upgrade guide is a diff of the README file itself, which is a clever way to show the API changes and a strange way to document them.
The rest of the project structure is conventional for a mature Ruby gem. There is a `Gemfile`, a `Rakefile`, the `icalendar.gemspec`, `lib/` for the implementation, `spec/` for the RSpec suite with a `.rspec` configuration file, a `benchmarks/` directory, and a `CHANGELOG.md`. The header of the README carries a Ruby build badge for the main workflow and a Code Climate badge.
The repository is not archived and the last push was on 2026-10-11, but it has no GitHub releases, so the version to depend on is whatever the gem publishes to RubyGems, and the `CHANGELOG.md` is the version history.
Where icalendar is the wrong choice
The first wrong case is a project that needs a calendar engine rather than a file format library. iCalendar produces and consumes the RFC 5545 format, and the library does not expand recurrence rules into occurrences, does not detect conflicts, and does not schedule. If your problem is scheduling, this is the wrong layer, and the RRULE support you see in the timezone example is about emitting a rule, not evaluating one.
The second wrong case is a team that needs bidirectional synchronisation with a live calendar server. The library reads and writes files. It does not speak CalDAV, and it does not do incremental synchronisation with a UID-keyed store. A subscription or invitation workflow is a different project.
The third is an existing user on the 1.x line who assumes a minor version bump will work. The README's upgrade section is a diff, and the library moved to 2.0 with breaking changes. Reading the diff and the `CHANGELOG.md` is not optional.
The fourth is a project that needs iTIP, the protocol for scheduling messages by email. The library covers the calendar data format, and the README does not describe scheduling request, reply or cancel objects, which are a different component set in the specification.
The fifth is a team that needs tzinfo support and does not realise it is opt-in. The require is manual, so an application that assumes timezone blocks are generated will silently emit a calendar without them, and the failure appears on the client's side as an event in the wrong time rather than as an error.
What the library does well is the part it has always done: producing a valid, parameterised, correctly typed iCalendar document from Ruby data, and reading one back.
Editorial conclusion
icalendar suits a Ruby service that generates or consumes iCalendar feeds, where a calendar is assembled from a database and handed to a client, and where the specification's parameter handling matters because real calendars carry parameters like ALTREP and LANGUAGE. It does not suit a team still on the 1.x line without reading the upgrade notes, and it does not suit a project that needs a full calendar engine, since the library is about the file format rather than recurrence expansion or scheduling. Before adopting it, read the `CHANGELOG.md` and the 1.x to 2.0 upgrade notes, and if you need timezones built automatically, require `icalendar/tzinfo` explicitly and confirm your `tzinfo` version is one of 0.3, 1.x or 2.x, since the support is opt-in.
Frequently asked questions
How do I create a calendar with an event in icalendar?
Create an `Icalendar::Calendar`, use the `cal.event` block to set the event properties such as `dtstart`, `dtend` and `summary`, and call `cal.publish`. The whole calendar serialises with `cal.to_ical`.
How do I attach a parameter such as LANGUAGE or ALTREP to a property?
Build the value with parameters, for example `Icalendar::Values::Text.new "summary", params`, or assign to the property's `ical_params` afterwards. Reading `event.summary.ical_params` returns the hash of parameters.
How do I generate a VTIMEZONE block from tzinfo?
Require `icalendar/tzinfo` explicitly, then use `tz.ical_timezone event_start` to build the timezone and add it with `cal.add_timezone`. The support is opt-in and is tested against tzinfo 0.3, 1.x and 2.x.
How do I check whether an event has an alarm?
Use the `has_alarm?` method. Calling `event.alarm` creates an alarm if none exists, so it is not a read-only check, as the README shows with the before and after output.
What changed between icalendar 1.x and 2.0?
The README says better documentation is still to come and points instead to the diff needed to update the README as the summary of the changes. The version history is in the `CHANGELOG.md` at the repository root.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/icalendar-icalendar)Was this useful?