Using Time Zones with Rails
Eli Fatsi, Former Development Director
Article Category:
Posted on
On a recent Rails application I built, I needed to determine which time zone a user was in to correctly display and use time-related information. I'll walk through two techniques I've found to get the job done.
The theory around working with time zones is pretty straightforward. Save all time-related data in Coordinated Universal Time (UTC), and display all time-related data in the time zone of any given user. If you're unfamiliar with ActiveSupport's TimeWithZone
, quickly check out this blog post from David Eisinger and familiarize yourself with the in_time_zone
method for display purposes.
The question then becomes, "How do we know what time zone a user is in?"
Method 1 - Quick, simple, 90% right
In search for the answer, I first discovered some simple JavaScript code:
var currentTime = new Date();
var utcOffset = currentTime.getTimezoneOffset()
document.cookie = 'time_zone_offset='+utcOffset+';';
new Date()
constructs a date object with the current time based on your system clock. getTimezoneOffset()
returns the number of minutes from which you differ from UTC. Throw that in a cookie and you're done with the JavaScript.
On the Rails side of things, you can grab that offset from the cookies and run the following code to determine which time zone has that offset.
offset = cookies["time_zone_offset"].to_i
time_zone = ActiveSupport::TimeZone[-offset.minutes]
current_user.update_attribute(:time_zone => time_zone)
However, This doesn't exactly work. There are multiple time zones that share a common offset. If you use this technique on the east coast of America, it's likely you'll get (GMT-05:00) Bogota
, the capitol of Columbia, or (GMT-04:00) Atlantic Time (Canada)
, depending on if you've recently sprung forward or fallen back. This is due to the fact that these time zones come before (GMT-04/05:00) Eastern Time (US & Canada)
alphabetically.
While this approach will still give you the accurate time for the present day, Daylight Savings standards of other regions could affect your code in the future. If your app does not care about the future, this solution is fine. My app does care about the future, though, and suddenly all my tests started failing when Daylight Savings Time came around.
Method 2 - Quick, a little less simple, 100% right
Enter JSTZ! This stands for jsTimezoneDetect and is a sweet bit of code which parses out the actual time zone from your computer's system time. Source code can be found here.
With the set of functions jstz
offers, the following lines of code will correctly determine the user's system time zone and store it in a cookie called jstz_time_zone
.
var timeZone = jstz.determine();
document.cookie = 'jstz_time_zone='+timeZone.name()+';';
From there, you can retrieve the cookie in any controller and update the time_zone
attribute on the user.
current_user.update_attribute(:time_zone => cookies["jstz_time_zone"])
Excellent! Now we are correctly retrieving a user's time zone from their computer and saving it on the user.
Confirming
At this point you have retrieved the user's system time zone, however it's possible that this is not the correct time zone. While it's a good guess to have when asking the user for input on the matter, it's necessary that you ask the user to confirm or select a time zone to guarantee correctness.
<% unless cookies[:selected_a_time_zone] %>
<div class="time-zone-confirmation">
<%= "Are you currently in the following time zone: #{current_user.time_zone}?" %>
<%= button_tag "Yes", :class => 'hide-time-zone-confirmation' %>
<%= button_tag "No", :class => 'show-time-zone-selection' %>
<div class="time-zone-selection" style="display:none;">
<%= simple_form_for @user, :url => "/time_zone", :method => :put do |f| %>
<%= f.input :time_zone, :collection => ActiveSupport::TimeZone.us_zones.map(&:name) %>
<%= f.submit "Submit" %>
<% end %>
</div>
</div>
<% end %>
The following jQuery code accompanies the form to make for a nice user interaction.
$('button.hide-time-zone-confirmation').on('click', function() {
document.cookie = 'selected_a_time_zone=true;';
$('.time-zone-confirmation').hide();
});
$('button.show-time-zone-selection').on('click', function() {
$('.time-zone-selection').show();
});
A route is added in routes.rb
to direct the post to the correct controller.
put "/time_zone" => "time_zones#update"
And lastly, here is the controller code to save the decision.
def update
current_user.update_attribute(:time_zone => params["user"]["time_zone"])
cookies[:selected_a_time_zone] = true
redirect_to :back
end
To sum up, we first make our best guess as to what time zone the user is in. We then ask the user to either confirm our guess, or select a time zone from a given list. In both cases, a cookie named selected_a_time_zone
is set to true
, which prevents the confirmation form from being displayed in the future. Updating a time_zone
attribute on the current_user
can then be used to correctly display all time-related info.