Have you already worked with a database Time column and an Active Record model together? If you had, you know: it sucks. Well, not anymore.
In the Gemfile
add:
gem 'time_of_day'
Run bundler
:
bundle install
Just that.
Sometimes we need to work with time-only objects. Suppose, for example, you’re working on a train table web application and want to register trips between stations. Every day, the train departures on a defined time and, some minutes later, it arrives at the next station.
So lets dive into Rails and do it. You’ll end up with a migration and a model like this:
# Migration class CreateTrips < ActiveRecord::Migration def self.up create_table :trips do |t| t.string :train, :leaving_from, :arriving_at t.time :departure, :arrival end end def self.down drop_table :trips end end # Dumb AR model class Trip < ActiveRecord::Base end
Cool. Just after a rake db:migrate
, we can create trips and tell if the train will be on any of them at a given time:
trip = Trip.new trip.departure = '10:00' trip.arrival = '10:10' # Will the train be on this trip at 10:05? time = Time.parse('10:05') (trip.departure..trip.arrival).cover?(time) # OR (trip.departure <= time && trip.arrival >= time) # if you are old fashioned =P
You might be surprised to know: this code does not work. At least not as we expected: both statements return false. The problem is caused by the way Active Record handles time-only attributes. SQL has the time
data type, that represents just a time of day, but Ruby doesn’t have such thing. So, Active Record represent time attributes using the Time
class on the canonical (and arbitrary) date 2000-01-01
. This is okay when you are comparing only attributes since their date will be equal and just the time will be compared. However, when you compare an attribute with another time instance, you start to need silly workarounds. In this case, the code does not work because Time#parse
yields the current date (e.g. 2010-06-25 10:05
) and of course it is not between 2000-01-01 10:00
and 2000-01-01 10:10
. To accomplish what you were trying to do, you have two alternatives:
# Always use the date 01/01/2000 for any time you want to compare (notice the Z for UTC time zone) time = Time.parse('2000-01-01 10:05Z') (trip.departure..trip.arrival).cover?(time) # => true # OR compare always considering the seconds since midnight seconds = time.seconds_since_midnight (trip.departure.seconds_since_midnight <= seconds && trip.arrival.seconds_since_midnight >= seconds) # => true
Sorry, but both options suck.
There are many ways to fix this issue. Subclassing Time
would be certainly the purest OO solution, but I found that monkey patching it is much more practical. So, it’s what time_of_day
does. It adds some methods to Time
for representing times of day, instead of time with dates.
# Suppose today is June 25, 2010 time1 = Time.parse('10:00') # => 2010-06-25 10:00:00 time2 = Time.parse('2010-06-01 10:00') # => 2010-06-01 10:00:00 time1 == time2 # => false # Ignoring the dates time1.time_of_day! time2.time_of_day! time1 == time2 # => true
So, now you can ignore the date information and compare just time. The most exciting thing, though, is that Active Record time attributes are now always mapped to times of day:
trip = Trip.new trip.departure = '10:00' trip.arrival = '10:10' trip.departure.time_of_day? # => true trip.arrival.time_of_day? # => true
At this time, the code works effortlessly:
time = Time.parse('10:05').time_of_day! (trip.departure..trip.arrival).cover?(time) # => true (trip.departure <= time && trip.arrival >= time) # => true
And you go home earlier.
Should work with Rails 3.0 and 3.1 on Ruby 1.8.7 and 1.9.x.
Copyright © 2010-2011 Lailson Bandeira (lailsonbandeira.com). See LICENSE for details.