Calendaring with Ruby’s Time class
Part of preparing one of my ongoing projects for release was getting some calendaring functionality built. The Calendar Helper is a great resource but has limited functionality for printing items within dates. Specifically, I wanted to work with time within dates, e.g. “Weds. Jan 3, 2007 at 10:30AM” instead of just “1/3/07”. I have had a lot of fun working with Ruby’s Time class and find it a bit more flexible then the Date or DateTime classes.
In order to make a calendar, I needed to be able to iterate through dates and print or manipulate as I go. Finally, an opportunity to make use of what I’ve been learning about blocks! For a number you would do something like this to iterate through integers.
1.upto(5) { |n| puts n+1 } #=> 2 3 4 5 6
Why not the same for Time? In this case, though I wanted to iterate through Time as days, not seconds or microseconds. I added a method to Time:
Time.local(self.year,self.month,self.day,0,0,1)
end
end
>> Time.now.to_dt
#=> Wed Jan 03 00:00:01 EST 2007
Now I can use time.to_dt as a date representation of a Time. Meaning, if something is calendared for Jan 3, 2007 at 00:00:01 I know its calendared for that day in general instead of for a specific time. Now to iterate with some block action.
self.to_dt
while cur <= to.to_dt
yield cur
yes = cur
cur += 1.day
if yes.dst? != cur.dst?
if cur.mon > 6
cur += 1.hour
else
cur -= 1.hour
end
end
end
end
end
cur =
Pretty straight forward. Its an equivalent of num.upto for the Time class. The only thing thats a little confusing is the last little bit, that has to add or subtract hours based on Daylight Savings. This is necessary because we’re moving through the block by adding hours (actually seconds) instead of days. The block returns a Time object in the form of to_dt.
>> Time.now.span_times_as_days(Time.now + 1.week) { |day| puts day }
# Wed Jan 03 00:00:01 EST 2007
# Thu Jan 04 00:00:01 EST 2007
# Fri Jan 05 00:00:01 EST 2007
# Sat Jan 06 00:00:01 EST 2007
# Sun Jan 07 00:00:01 EST 2007
# Mon Jan 08 00:00:01 EST 2007
# Tue Jan 09 00:00:01 EST 2007
# Wed Jan 10 00:00:01 EST 2007
I’ve used this method to block through the dates and print it out, similar to the calendar helper. Heres another helpful function using span_times_as_days:
unless first_date
last_date = items.last.attributes[time_attribute].to_dt
first_date.span_times_as_days(last_date) do |dt|
calendar_day = {:date => dt, :items => []}
while !items.empty? && items.first.attributes[time_attribute].to_dt == dt
item = items.shift
calendar_day[:items] << item
end
calendar << calendar_day
end
calendar
end
calendar = []
first_date = since
first_date = items.first.attributes[time_attribute].to_dt
Give this method an array of ActiveRecord (or ActiveRecord-like) items that have a specific attribute (or method) that returns a Time object. For example, I have a class called Assignment, that had a ‘datetime’ attribute ‘due_at’.
@assignments = Assignment.find(:all,:order => 'due_at ASC')
# Assignment 1 - due_at Sat Jan 06 10:00:00 EST 2007
# Assignment 2 - due_at Sun Jan 07 00:00:01 EST 2007
@calendared = bind_array_to_calendar(@assignments,:due_at)
#=> [{:date => Sat Jan 06 00:00:01 EST 2007, :items => [Assignment 1]},
# {:date => Sun Jan 07 00:00:01 EST 2007, :items => [Assignment 2]}]
Its been fun playing with Time. Let me know if you see something that can be slickified.