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:

class Time
  def to_dt
    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.

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 }
# 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:

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')
# 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.

Comments are closed.

About

QuirkeyBlog is Aaron Quint's perspective on the ongoing adventure of Code, Life, Work and the Web.

twitter/@aq.

instagram/@quirkey.

github/quirkey.

QuirkeyBlog is proudly powered by WordPress

Categories