A Fresh Cup is Mike Gunderloy's software development weblog, covering Ruby on Rails and whatever else I find interesting in the universe of software. I'm a full-time Rails developer and contributor, available for long- or short-term consulting, with solid experience in working as part of a distributed team. If you'd like to hire me, drop me a line. I'm also the author of Rails Rescue Handbook and Rails Freelancing Handbook.

Navigation
« Double Shot #509 | Main | Double Shot #508 »
Friday
Jul312009

Recurring Items in a Rails Application

I recently had to implement some recurring items in a Rails application - we'll call them "entries" here though you can use this technique for appointments, bills, or whatever else. After thinking about it for a while, I settled on:

  • At any point, we needed to know what the next recurrence date was.

  • The potential schedules needed to be data-driven (because the client was unsure what would be needed).


Here's what I came up with. If your needs are different, you'll no doubt need to alter it. First, the schedule model has a schedule_type, an interval, a specific value, and a description. The combination of these things lets me implement Schedule#next_date to return the fate for the next date given a schedule object and the current date. Here's the model:


[sourcecode language='ruby']
class Schedule

has_many :entries

attr_accessible :schedule_type, :description, :interval, :specific

SCHEDULE_TYPE = {:first => 0, :last => 1, :weekly => 2, :monthly => 3}

def next_date(current_date)
case schedule_type
when SCHEDULE_TYPE[:first]
first_day = (current_date + 1.month).beginning_of_month
wday = first_day.wday
if specific >= wday
first_day + specific - wday
else
first_day + 7 + specific - wday
end
when SCHEDULE_TYPE[:last]
if specific == -1
(current_date + 1.month).end_of_month
else
last_day = (current_date + 1.month).end_of_month
wday = last_day.wday
if wday >= specific
last_day - wday + specific
else
last_day - wday - 7 + specific
end
end
when SCHEDULE_TYPE[:weekly]
current_date + interval.weeks
when SCHEDULE_TYPE[:monthly]
current_date + interval.months
end
end
[/sourcecode]

So we can have a schedule that is "first Monday of the month" or "every 3 weeks" among other things. I stock these up using db-populate; here's some of the population file:

[sourcecode language='ruby']
Schedule.create_or_update(:id => 1,
:schedule_type => Schedule::SCHEDULE_TYPE[:monthly],
:interval => 1, :description => "Every month")
Schedule.create_or_update(:id => 2,
:schedule_type => Schedule::SCHEDULE_TYPE[:monthly],
:interval => 2, :description => "Every other month")

Schedule.create_or_update(:id => 4,
:schedule_type => Schedule::SCHEDULE_TYPE[:weekly],
:interval => 1, :description => "Every week")

Schedule.create_or_update(:id => 8,
:schedule_type => Schedule::SCHEDULE_TYPE[:first],
:specific => 0, :description => "First Sunday of every month")

Schedule.create_or_update(:id => 15,
:schedule_type => Schedule::SCHEDULE_TYPE[:last],
:specific => 0, :description => "Last Sunday of every month")

Schedule.create_or_update(:id => 22,
:schedule_type => Schedule::SCHEDULE_TYPE[:last],
:specific => -1, :description => "Last day of every month")
[/sourcecode]

Then the entry model ties into the schedule model. Here are the important (for this purpose) bits of the entry class:

[sourcecode language='ruby']
class Entry < ActiveRecord::Base
belongs_to :schedule
named_scope :ready_to_recur, lambda { |date|
{:conditions => ["recurring = 1 AND next_date <= ? AND schedule_id IS NOT NULL AND next_created = 0", date ]} }
before_save :set_up_recurrence

def make_next_recurrence
if recurring? && !schedule_id.nil? && !next_created?
entry = Entry.create(
:entry_date => next_date,
:reference => reference,

:recurring => true,
:schedule_id => schedule_id
)
update_attribute(:next_created, true)
end
entry
end

def set_up_recurrence
if recurring? && !schedule_id.nil?
self.next_date = Schedule.find(schedule_id).next_date(entry_date)
self.next_created = false if next_created.nil?
end
true
end
end
[/sourcecode]

Finally, the whole thing is driven by a rake task that we run every night. This task finds all the entries that are ready to recur and creates the next entry:

[sourcecode language='ruby']
desc 'Create the recurring entries for today'
task :daily_recurring_entries => :environment do
Entry.ready_to_recur(Date.today).each do |entry|
entry.make_next_recurrence
end
end
[/sourcecode]

Reader Comments (7)

I suppose you know about runt gem
http://runt.rubyforge.org/

July 31, 2009 | Unregistered CommenterAnonymous

The amazing Tracks project also implements recurring items. http://getontracks.org

July 31, 2009 | Unregistered CommenterMatt Katz

http://github.com/joevandyk/fixie_events/tree/master is a rails plugin that lets you do recurring events pretty easily with activerecord.

July 31, 2009 | Unregistered CommenterJoe Van Dyk

Mike, FWIW, I like your approach much more than the two plugins mentioned.

Very easy to understand. Thanks for sharing!

July 31, 2009 | Unregistered CommenterIván V.

Very clean way of doing recurring "things". Thanks for sharing. This will be of immediate help to me!
-Ryan

August 3, 2009 | Unregistered CommenterRyan Ripley

hi, the concept of recurring is what i was looking for i need some idea regarding recurring projects . Here's some brief introduction about my application . People can login into my application and create there own projects/services like filling income tax, it returning etc. i need to apply recurring concept on these projects/services if u could throw some light on it.

August 6, 2009 | Unregistered CommenterDeepak

It appears that you only create one recurrence at a time instead of all the recurrences at one time. This seems to make it so you couldn't see a recurring meeting on a calendar four months out. Am I missing something?

December 13, 2009 | Unregistered CommenterDerek Neighbors

PostPost a New Comment

Enter your information below to add a new comment.
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>