Adding Recurrence Rule to Generated ICS file in Rails

If you need to integrate your Rails app with some calendaring service, such as Google Calendar, sooner or later you will find yourself writing code that outputs your Event object as an ICS file. But if your events have recurrence rules, you are out of luck, the popular Icalendar gem doesn’t support them. I’ll show how to make it work.

ICS is a calendar standard supported by a wide range of Calendar applications. In my case, I needed to attach ICS file to outgoing emails that are sent to users every time a new event is scheduled. The reason? The nice “Add to your Calendar” box that is added automatically in Gmail and other clients, that allows the user to quickly add the event to his calendar.

 

 

Icalendar gem is a simple library that allows populating an Icalendar::Event object and outputting it as ICS string.

require 'icalendar'

module Event::ExportsICS

  def to_ics
    @cal = Icalendar::Calendar.new
    @cal.event do |e|
      e.dtstart = self.starts_at
      e.dtend = self.ends_at
      e.summary = self.calendar_title
      e.organizer = Icalendar::Values::CalAddress.new(
                    "mailto:#{self.organizers.first.email}",
                    cn: self.organizers.first.full_name)
      e.description = self.description
      e.location = self.location
    end
  end
end

But it’s missing a crucial component – a support for recurring events. After some searching, I ended up writing a build_recurrence_rule method that builds a recurrence rule from my Event object (referred as self in the code)

def build_recurrence_rule
    rule = "RRULE:FREQ=#{self.repetition.repeat_frequency.upcase};"\
           "INTERVAL=#{self.repetition.repeat_interval};"
    if self.repetition.end_date.present?
      rule << "UNTIL=#{self.repetition.end_date.iso8601};"
    end
    if self.repetition.yearly?
      rule << "BYMONTHDAY=#{self.starts_at.day};"\
              "BYMONTH=#{self.starts_at.month};"
    elsif self.repetition.monthly? && self.repetition.week_of_month.present?
      by_month_day = self.repetition.week_of_month.to_s +
               self.starts_at.strftime("%A")&.upcase&.slice(0..1)
      rule << "BYDAY=#{by_month_day};"
    elsif self.repetition.days_of_week.present?
      by_week_day = self.repetition.days_of_week.map do |d|
        d.slice(0..1).upcase
      end.join(',')
      rule << "BYDAY=#{by_week_day};"
    end
    [rule]
end

Now, all that is left is to somehow add the generated recurrence rule to the Icalendar::Event object we created earlier and output the result as a string.

require 'icalendar'

module Event::ExportsICS

  def to_ics
    @cal = Icalendar::Calendar.new
    @cal.event do |e|
     # populate the Icalendar::Event as I've shown earlier
    end
    # there is no way to add the recurrence rule to the object, 
    # so we have to work with the output string
    ical_str = @cal.to_ical
    if self.repetition.present?
      recurrence_string = build_recurrence_rule.join('\r\n')
      ical_str = ical_str.gsub('END:VEVENT', 
                          "#{recurrence_string}\r\nEND:VEVENT")
    end
    ical_str
  end

end

And now, all you have to do is add the generated ICS as an attachment in your mailer:

def add_ics_attachment
  attachments['event.ics'] = { mime_type: 'text/calendar', 
                               content: @event.to_ics }
end

And you are good to go.