Ruby and Rails provide great tools for working with time and time zones. But my experience shows that Rails developers often pay little attention to how this magic really works, which often results in "ghost" problems that arise in particular configuration and time. The main gotcha for developer is that you can use "wrong" methods in development and fairly often get valid results. But then you'll face with unexpected problems on production.
Historically Ruby provides two classes to manage time:
They use different approaches internally, which caused
different abilities and performance for them.
But with Ruby 1.9.3 these differences seem to be vanished and you are free to choose whatever interface you like.
But that's in Ruby! When it comes to Rails things get a bit more complicated.
Rails gives your ability to configure application time zone. It's as easy as
Time.zone = 'EST'.
in your application.rb will eventually do the same. And this is the right thing to do since we
don't want to depend on server time zone. But the problem here that now we have 3 (!) different
time zones in our application: system time, application time and database time. And if you won't
use correct methods to deal with time all them get mixed up.
Let me give you an example. Let's say you have server in Moscow (GMT+04:00), but most of your customers are from US and you use Eastern Standard Time (GMT-05:00) in your application. For database Rails by default converts all timestamps to GMT. Let's try calling different methods and compare the results.
Time.zone = 'EST' DateTime.now.to_s # => "2012-07-06T13:30:00+04:00" Time.now.to_s # => "2012-07-06 13:30:00 +0400" Time.zone.now.to_s # => "2012-07-06 04:30:00 -0500"
You might notice that
both give you the time in system time zone. And it definitely makes sense since these are Ruby
standard library methods that know nothing about Rails time zone configuration.
on the contrary is provided by Rails and respects
value that we set in Rails.
But you might argue that if we convert all these values to one time zone they will be the same. And you are right, if you try to save this time to ActiveRecord model or compare to another value you will get expected results. Then what's the problem? Let's try some manipulations with time.
Time.zone = 'EST' Time.now.end_of_day.to_s # => "2012-07-06 23:59:59 +0400" Time.zone.now.end_of_day.to_s # => "2012-07-06 23:59:59 -0500"
Whoops, by using different time zones in initial time we got completely different results. That means that before we manipulate time we either need to explicitly convert time to required time zone or initially get time in correct time zone. Like this:
Time.zone = 'EST' Time.now.in_time_zone.end_of_day.to_s # => "2012-07-06 23:59:59 -0500" Time.zone.now.end_of_day.to_s # => "2012-07-06 23:59:59 -0500"
Here is another trick. Let's time travel to the next morning. What have we got here?
Time.zone = 'EST' Time.now.to_s # => "2012-07-07 06:30:00 +0400" Time.zone.now.to_s # => "2012-07-06 21:30:00 -0500" Date.today # => "Fri, 07 Jul 2012"
But wait, application should still find itself in 6th of July since it uses EST time zone.
As you might have guessed,
doesn't know anything about Rails time zone too and uses system time to determine current date.
So, when we know some troubles it might cause, let's find out which methods are safe to use.
You might have noticed from given examples that interesting construct
that seem to produce correct results.
So, what is it? Actually above I cheated for simplicity of introduction. It's not Rails responsible
for adding time zone, but ActiveSupport. It adds
methods to correspondingly set and get current time zone. If you look into source of these methods
you'll find that the first one finds requested
object and stores it in the current thread, while the second one just fetches if from there.
object where all the magic happens.
It defines number of methods that makes its API similar to the one of
class. That's why from the user point of view it looks like a simple switch from
But what actually happens in
Turns out it simply grabs current time and converts it do current time zone (which is
And here is the last line of defense,
method. It is added by ActiveSupport to both
Ruby standard library methods. In both cases it does the same - converts time to
object passing specified time zone to it. This class behaves exactly like Ruby
object, it even redefines class name. So you can manipulate it as you usually do in Ruby.
Now when we know what's under the hood, I will leave you with brief cheat sheet on correct and incorrect methods for dealing with time in Ruby on Rails.
You should now understand that wrong option will give you time in system time zone, which might cause problems. Second (acceptable) option will work fine as it converts time to Rails time zone. As we have seen above it is exactly what ActiveSupport is doing internally, but there is no need to use it explicitly as there is shorter and more clear option.
Time.zone.today Time.zone.today - 1.day
method that proxies call to
and it also redefines
helpers. But I personally find them quite confusing and easier to be replaced with
by mistake. So I prefer to use explicit version, but feel free to disagree here.
Time.zone.local(2012, 6, 10, 12, 00)
Time.new(2012, 6, 10, 12, 00) DateTime.new(2012, 6, 10, 12, 00)
I believe this one doesn't need any comments as Ruby classes are simply not aware of the time zone in use.
DateTime.strptime(str, "%Y-%m-%d %H:%M %Z").in_time_zone
DateTime.strptime(str, "%Y-%m-%d %H:%M")
Here I have to make one important note. Correct option will give valid results only when time string (and format of course) explicitly include time zone (note %Z in format string). But there are cases when you need to parse time without specified time zone. In this case you usually need to treat this time in current time zone. Unfortunately ActiveSupport doesn't provide convenient means for this. To fill this gap you can use my micro gem TimeZoneExt that allows you to parse time with or without explicitly specified time zone. This adds one more correct option.
Time.zone.strptime(str, "%Y-%m-%d %H:%M")
method provided by Ruby knows nothing about time zone in use. So you should use one of the
methods defined in ActiveSupport. I personally find
more explicit, although
gives the same result.
There is no rocket science in dealing with time in Rails. But it's a good idea to understand it once
and always keep in mind that when you build time or date object you should respect current time zone.
In most cases it simply means to use
I do not cover dealing with other than default time zones in Rails. This post has another purpose. But it is possible and quite easy to do. And now you have references to classes to look into.