Or what is the difference between Time.zone.now and Time.now
6 July 2012
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:
Time
and
DateTime
.
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'
.
Setting
config.time_zone
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.
What's the trick?
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.
1
2
3
4
5
6
7
|
Time.zone = 'EST'
DateTime.now.to_s
Time.now.to_s
Time.zone.now.to_s
|
You might notice that
DateTime.now
and
Time.now
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.
Time.zone.now
on the contrary is provided by Rails and respects
Time.zone
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.
1
2
3
4
5
|
Time.zone = 'EST'
Time.now.end_of_day.to_s
Time.zone.now.end_of_day.to_s
|
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:
1
2
3
4
5
|
Time.zone = 'EST'
Time.now.in_time_zone.end_of_day.to_s
Time.zone.now.end_of_day.to_s
|
Here is another trick. Let's time travel to the next morning. What have we got here?
1
2
3
4
5
6
7
|
Time.zone = 'EST'
Time.now.to_s
Time.zone.now.to_s
Date.today
|
But wait, application should still find itself in 6th of July since it uses EST time zone.
As you might have guessed,
Date.today
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.
The Right Way to Go
You might have noticed from given examples that interesting construct
Time.zone
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
Time.zone=
and
Time.zone
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
ActiveSupport::TimeZone
object and stores it in the current thread, while the second one just fetches if from there.
So, it's
ActiveSupport::TimeZone
object where all the magic happens.
It defines number of methods that makes its API similar to the one of
Time
class. That's why from the user point of view it looks like a simple switch from
Time.now
to
Time.zone.now
.
But what actually happens in
Time.zone.now
method?
Turns out it simply grabs current time and converts it do current time zone (which is
self
).
1
2
3
4
5
|
class ActiveSupport::TimeZone
def now
Time.now.utc.in_time_zone(self)
end
end |
And here is the last line of defense,
in_time_zone
method. It is added by ActiveSupport to both
Time
and
DateTime
Ruby standard library methods. In both cases it does the same - converts time to
ActiveSupport::TimeWithZone
object passing specified time zone to it. This class behaves exactly like Ruby
Time
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.
Get Current Time
Correct
Acceptable
1
2
|
Time.now.in_time_zone
DateTime.now.in_time_zone |
Wrong
1
2
|
Time.now
DateTime.now |
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.
Get Day (Today, Yesterday, etc.)
Correct
1
2
|
Time.zone.today
Time.zone.today - 1.day |
Acceptable
1
2
|
Date.current
Date.yesterday |
Wrong
ActiveSupport defines
Date.current
method that proxies call to
Time.zone.today
and it also redefines
Date.yesterday
and
Date.tomorrow
helpers. But I personally find them quite confusing and easier to be replaced with
Date.today
by mistake. So I prefer to use explicit version, but feel free to disagree here.
Build Time
Correct
1
|
Time.zone.local(2012, 6, 10, 12, 00) |
Wrong
1
2
|
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.
Time From Timestamp
Correct
1
|
Time.zone.at(timestamp) |
Acceptable
1
|
Time.at(timestamp).in_time_zone |
Wrong
Parse Time (Simple)
Correct
Wrong
Parse Time (With Explicit Format)
Acceptable
1
|
DateTime.strptime(str, "%Y-%m-%d %H:%M %Z").in_time_zone |
Wrong
1
|
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.
Correct
1
|
Time.zone.strptime(str, "%Y-%m-%d %H:%M") |
Get Time For Date
Correct
Acceptable
1
|
date.to_time_in_current_zone |
Wrong
Again, default
to_time
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
beginning_of_day
more explicit, although
to_time_in_current_zone
gives the same result.
Conclusion
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
Time.zone
instead of
Time
,
Date
or
DateTime
.
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.
For more high-quality posts check out
blog at toptal.com