Using UTC Offset in a Rails app.
Time zone math is no pleasure cruise, especially if, like me, you are new to the matter. Having users scattered all around the globe, apps need to be intelligent enough to be aware of the correct time for each user - particularly when there are scheduled events and notifications that need to be triggered at a specific time for a specific user.
I pretty much exhausted the internet recently looking for articles about it, and I certainly could have used some more good ones.
The UTC* offset is without a doubt the one indispensable thing when dealing with time zone calculations, and more specifically the UTC offset expressed in hours. For those not familiar with the concept, the UTC offset is the time offset of a specific time zone from the UTC. In my case I first derived the latitude and longitude from the user’s address using the geokit-rails3 gem, then I used earthtools.org to get the utc_offset. I then saved it in the user table.
In the user model:
1 class User < ActiveRecord::Base
2 def utc_offset
3 location = "#{self.address}, #{self.city} #{self.state} #{self.zip}"
4 geo = Geokit::Geocoders::MultiGeocoder.geocode(location)
5 url = "http://www.earthtools.org/timezone/#{geo.lat}/#{geo.lng}"
6 response = Array(HTTParty.get(url).parsed_response)[0][1]
7 offset = Array(response)[9][1]
8 self.update_attributes(:utc_offset => offset)
9 end
10 end
I picked Earthtools.org because it’s free, quick and dirty, and I was really eager to dive into the somewhat frustrating subject that is time zone math.
Rails stores all dates in the database in UTC, so that really helps. Also to avoid any other problems I also set up my app default time to UTC.
In the config/application.rb:
config.time_zone = 'UTC'
Finally, since we are using the DateTime new_offset method to “localize” the date/time needed for our actions, it’s good practice also to store the dates and time in the database as datetime data type.
The “localization” happens like so:
1 t = User.find(n).date
2 offset = Rational(user.utc_offset,24)
3 localize_datetime = t.new_offset(offset)
The new_offset method creates a new DateTime object, identical to the current one, except with a new time zone offset (where offset is the new offset from UTC as a fraction of a day). That is why I used Rational to convert the offset into a fraction.
*UTC (Coordinated Universal Time), differently from GMT (Greenwich Mean Time), is based on atomic time and includes leap seconds as they are added to our clock every so often.