#106 Time Zones (revised)
Below is a screenshot from an application called You Haiku which allows users to enter Haikus and share them with others. When we add a Haiku it will be displayed at the top of the list along with the time that at which it was added.
We’ve just added a new Haiku and it looks correct except for the time at which it was added: it’s currently 8:11pm in our timezone, not 7:11pm. By default Rails sets all times to UTC which is why the time shown doesn’t match our local time. If we look at the app’s configuration file we’ll see a commented-out option for setting the time zone and without this set the time zone will default to UTC. We can see a list of the time zones that we can set by running
rake time:zones:all. This returns a long list of time zones divided up in to their UTC offsets. We can also run
rake time:zones:local to see the time zones that match our machine’s UTC offset.
$ rake time:zones:local * UTC +01:00 * Amsterdam Belgrade Berlin Bern Bratislava Brussels Budapest Copenhagen Ljubljana Madrid Paris Prague Rome Sarajevo Skopje Stockholm Vienna Warsaw West Central Africa Zagreb
It’s a good idea to use a time zone that matches the one where most of our app’s users are located so we’ll use North American Central Time here, even though that’s not where we’re located. In development it’s a good idea to use a time zone different from the one on our local machine so that we can more easily detect time zone issues.
config.time_zone = 'Central Time (US & Canada)'
Allowing Users to Set Their Time Zone
When we restart our server now then reload the page the times are shown in this time zone but this might not match each user’s time zone so we’ll give them a way to set their preferred time zone. We already have some user authentication in place in our app and we have an “edit profile” page where we can change our name and password when we’re signed in. We’ll add the ability to associate a time zone with each account as well. To do this we’ll start by creating a migration to add a time zone field to the
users table then migrate the database.
$ rails g migration add_time_zone_to_users time_zone $ rake db:migrate
We’ll need to add this new field to the list in
attr_accessible in the
User model so that it can be set through mass-assignment.
class User < ActiveRecord::Base has_secure_password attr_accessible :name, :password, :password_confirmation, :time_zone validates_uniqueness_of :name validates_inclusion_of :time_zone, in: ActiveSupport::TimeZone.zones_map(&:name) has_many :haikus end
We’ve also added a validator to ensure that any time zone added is included in Rails’ list of valid time zones. Next we’ll add a new field to the form for a user’s profile. Rails provides a helper method called
time_zone_select that will provide a select menu of all the valid time zones so we’ll use it here.
<div class="field"> <%= f.label :time_zone %><br /> <%= f.time_zone_select :time_zone %> </div>
If most of our customers are in a specific region we might want to prioritize this time zone at the top of the menu and we can do this by adding a second argument to
time_zone_select. For example if most of the users will be in the USA we can move the US zones to the top of the list like this.
<div class="field"> <%= f.label :time_zone %><br /> <%= f.time_zone_select :time_zone, ActiveSupport::TimeZone.us_zones %> </div>
When we reload the page now the US time zones will appear at the top of the list.
Even when we change our profile’s time zone the Haiku’s times are still displayed for the Central Time Zone. We need to apply the user’s preferred time zone to these times and a good way to do this is with a filter in the
ApplicationController so that it’s applied to every action. We’ll use an
class ApplicationController < ActionController::Base protect_from_forgery around_filter :user_time_zone, if: :current_user private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user def user_time_zone(&block) Time.use_zone(current_user.time_zone, &block) end end
user_time_zone method here we use the
use_zone method on the Time class which is a method that ActiveSupport adds and we pass this the current user’s time zone. This method expects a block to be passed to it and sets the time zone for the duration of that block. We can pass in the block that’s used by the around filter so that the original time zone is set back when the request completes. If we used a
before_filter here instead the time zone would leak through to other requests so it’s necessary to use an around filter instead. When we reload the page now the Haiku’s times are correctly set to the right time zone.
Rails makes working with time zones pretty easy but it’s a good idea to understand how it works so that we can avoid unexpected problems. We’ll show some of these in the Rails console. First we’ll fetch the last Haiku and look at its
>> h = Haiku.last >> h.published_at => Thu, 15 Nov 2012 13:11:15 CST -06:00
This time looks correct but it isn’t the time that’s stored in the database. We can check that stored time by called
>> h.published_at_before_type_cast => "2012-11-15 19:11:15.517923"
This value is actually the UTC time which means that behind the scenes Rails is converting to UTC when ever it writes to or reads from the database. This is good as it means that our data is stored in a consistent time zone.
Let’s say that we want to fetch the current time. A common way to do this is to call
Time.now which returns the time of the current system based on its time zone. If we want to get the time in the zone of the Rails application we should call
Time.zone.now to return the time in the time zone set in the configuration file.
>> Time.now => 2012-11-15 23:18:22 +0100 >> Time.zone.now => Thu, 15 Nov 2012 16:18:23 CST -06:00
The output from these two commands looks different and this is because the methods return instances of different classes. One is a
Time class and the other is
ActiveSupport::TimeWithZone. This works in a similar way to
Time but has added support for different time zones. One way to convert from one to the other is to call
in_time_zone on a
Time object which will return a
Another thing we need to watch out for is dates. Calling
Date.today will use the local system’s time zone which is probably not what we want. To use the Rails-configured time zone we can call
>> Date.today => Thu, 15 Nov 2012 >> Date.current => Thu, 15 Nov 2012
These return the same date here but they could differ if we were in a different time zone. It’s easy to mix these up so we should always go through
Time.zone.now then convert it to date if that’s what we need. In short, if we want to avoid time zone headaches in a Rails application we should always go through
Time.zone. Most of the
Time class’s methods are supported here so that we can use, say,
parse to parse a string into a date. This returns a date time in the Rails application’s time zone and even handle daylight-savings time correctly too. In addition to
Time.zone we can also use the convenience methods that ActiveSupport gives us such as
weeks.ago and these will use the correct time zone.
If we want some help to detect time zone issues in our Rails applications we can use the Zonebie gem. This can set the time zone to a random one each time we run our tests. Timecop is another gem that can be useful when we work with times in our test suite. Episode 276 has more information on this.