time_of_day
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.
Setup
In the Gemfile
add:
gem 'time_of_day', :git => 'git://github.com/lailsonbm/time_of_day'
Run bundler
:
bundle install
Just that.
The problem
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.
How time_of_day handles the problem
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.
Compatibility
Tested with Rails 3.0.0 on Ruby 1.9.2-p0 and Ruby 1.8.7.
Copyright
Copyright © 2010 Lailson Bandeira (lailsonbandeira.com). See LICENSE for details.