We have been working in a project which deals with date events and we needed a recurrence feature in the application. An initial implementation could simply work with Rails ActiveSupport and use its Date helper methods, in order to shift by day, week, month and others.

>> Date.today
=> Thu, 15 Apr 2010
>> Date.today.next_month
=> Sat, 15 May 2010
>> Date.today.next_week
=> Mon, 19 Apr 2010
>> Date.today.next_week(:thursday)
=> Thu, 22 Apr 2010
>> Date.today.advance(:days => 4)
=> Mon, 19 Apr 2010

Very easy, right? But what if we now want events that occur every monday or sunday? Or events that happen at each two weeks? For each new case, we will need to add and deal with more and more logic.

Instead we have been using the Recurrence gem, created by Nando Vieira with some contributions by José Valim (yeah, our good fellow ;)). Recurrence works with ActiveSupport as its base for date calculation and provides a simple DSL to work with recurring events. Let me show a simple example:

# events every day, considering today as Apr 15th.
>> r = Recurrence.new(:starts => 2.days.ago, :every => :day, :until => Date.tomorrow)
>> r.events
=> [Tue, 13 Apr 2010, Wed, 14 Apr 2010, Thu, 15 Apr 2010, Fri, 16 Apr 2010]

If no :until is determined, it runs until year 2037 (maybe the end of world? No, it’s when the unix time 32-bit overflow…). You can check the Github repository and this blog post (in brazilian portuguese) to learn different use cases and examples for Recurrence.

Now I want to show you how we integrated it with our application. We have built an EventRecurrence class which deals internally with the recurrence logic.

# app/models/event_recurrence.rb
# It has :start_date, :every and :end_date as database columns
class EventRecurrence < ActiveRecord::Base
  def dates(options={})
    options = {:every => every, :starts => start_date, :until => end_date}.merge(options)
    options[:on] = case options[:every]
    when 'year'
      [options[:starts].month, options[:starts].day]
    when 'week'
      options[:starts].strftime('%A').downcase.to_sym
    when 'day', 'month'
      options[:starts].day
    end
    Recurrence.new(options).events
  end
end

Notice we need to setup the options hash for different cases (day, week, month and year) because the configuration options changes for each. Now, accessing all the recurring dates for one model is quite easy:

>> er = EventRecurrence.new(:start_date => Date.today, :every => :month, :end_date => 6.months.from_now)
>> er.dates
=> [Thu, 15 Apr 2010, Sat, 15 May 2010, Tue, 15 Jun 2010,
Thu, 15 Jul 2010, Sun, 15 Aug 2010, Wed, 15 Sep 2010, Fri, 15 Oct 2010]

Since EventRecurrence is a model, these options are saved in database and we can update or use them for querying freely. For instance, if we want to look for all dates in a given period, we just search for all EventRecurrences that are in the range and collect the dates returned by recurrence. Here you can see a raw implementation of this idea:

class EventRecurrence < ActiveRecord::Base
  # Retrieves a list of all dates for a period
  def self.dates_between(start_date, end_date)
    # Filtering EventRecurrence on the period using a between named scope
    recurrences = EventRecurrence.between(start_date, end_date)
 
    recurrences.inject([]) do |dates, recurrence|
      # Use the given dates instead of the ones in the DB
      dates.concat(recurrence.dates(:starts => start_date, :until => end_date))
    end
  end
end

This is just an initial post showing some ideas to get you going with recurring events. If you are interested, there is a lot of information related to this topic, like Recurring Event in Calendars, an article by Martin Fowler, and the iCal RFC2445 which includes calendar specifications.

And you, have some different solutions about recurring events to share with us?

Tags: ,

This entry was posted on Thursday, April 22nd, 2010 at 2:59 pm and is filed under English. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

  • http://trevorturk.com Trevor Turk

    Cool! Personally, I like to use the whenever gem, which provides a nice DSL for defining cron jobs. This can also be combined with delayed_job or resque, so you might have a cron job that creates a bunch of background tasks. I’ve used this technique for sending out weekly emails, etc.

  • http://trevorturk.com Trevor Turk

    Cool! Personally, I like to use the whenever gem, which provides a nice DSL for defining cron jobs. This can also be combined with delayed_job or resque, so you might have a cron job that creates a bunch of background tasks. I’ve used this technique for sending out weekly emails, etc.