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 }
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:
class Time
def to_dt
Time.local(self.year,self.month,self.day,0,0,1)
end
end
>> Time.now.to_dt
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.
class Time
def span_times_as_days(to, &block)
cur = 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
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 }
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:
def bind_array_to_calendar(items,time_attribute,since = false)
calendar = []
first_date = since
first_date = items.first.attributes[time_attribute].to_dt 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
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')
@calendared = bind_array_to_calendar(@assignments,:due_at)
Its been fun playing with Time. Let me know if you see something that can be slickified.