Blog

Dealing with Timezones Effectively in Rails

Placeholder Avatar
Sebastian Porto
March 25, 2014

We all have to deal with timezones in our Rails app sooner or later (probably sooner). In this blog post I want to share some of the tricks I have learned to deal with timezones effectively in Rails.

Do it sooner

Unless you are confident that you will never need to deal with timezones, you should think about them sooner rather than later, building your application in a way that accommodates for them from the beginning. This is one of those things that can be very difficult to include later, so I don’t think it is a premature optimisation.

Config

Some tutorials and blogposts suggest that you set the timezone in your app configuration to something known e.g. config.time_zone = 'UTC'. Unless you are only going to be dealing with only that timezone all the time I think this is unnecessary and even misleading. I suggest you don’t worry about setting this at all and leave it to the server timezone. Use datetime for storing dates

When storing dates in your database always try to use absolute time, e.g. datetime in rails. Don’t use just dates (e.g. 2012-01-12 without the time part) unless you are certain that is what you need, i.e when the date is relative to the user. Add a timezone to the users

Every user should have a timezone attribute, most likely in the database, but in early stages you can just create a method in the User class that returns a known value. Showing the time / dates in the user’s timezone

This is one of the most crucial things, each time you need to show a date / time to a user, you should convert that time to the user’s timezone. For example:

ruby Time.now.in_time_zone(user.timezone).strftime(.... I18n.l(Time.now.in_time_zone(user.timezone).beginning_of_day) Just remember to convert the timezone by using in_time_zone() before anything else. Querying information relative to the user’s timezone

The same is true for queries that should be relative to the user’s timezone. For example, let’s say we want to find today’s appointments for a user (today as in their timezone):

ruby starts_at = Time.now.in_time_zone(user.time_zone).beginning_of_day ends_at = Time.now.in_time_zone(user.time_zone).end_of_day Appointment.where(starts_at: starts_at).where(ends_at: ends_at) Or use an around filter.

Another strategy you can use is to always convert the timezone in each request to the user’s timezone. You can do this with an around filter in your application controller. ruby class ApplicationController < ActionController::Base around_filter :set_time_zone def set_time_zone(&block) time_zone = current_user.try(:time_zone) || 'UTC' Time.use_zone(time_zone, &block) end end In this way you don’t need to convert the timezone inside your controllers, although I like the explicitness of using in_time_zone().

What if I don’t have logged in users

If your are building a site that needs to show times and you don’t have logged in users, setting the server time zone is not a solution either. You have the following options:

  • Use Javascript on the front end to figure out the correct time to display, based on the user’s browser
  • Try this gem https://github.com/scottwater/detect_timezone_rails
  • Use the user’s IP address to infer the timezone, this can be a hit or miss Instead of having current_user as nil consider setting current_user to a special user object that represents a non logged in user, in that way this object can respond to normal user methods e.g. user.time_zone.

Querying from the front end

When you need to make ajax request where times are relevant the most reliable way is to use the epoch timestamps, this the number of second since January 1st, 1970 in UTC. So this is an absolute number regardless of the browser timezone. js var epoch = (new Date()).getTime() / 1000; The in Rails convert that value to a time: ruby Time.at(params[:epoch]) ##Testing If you are building an application that uses server-generated views, testing is very straightforward. You can test that your timezones are working correctly by changing the timezone for the current user and checking that your views show the correct date/time for their timezone. Integration testing with client side JS

However if you are doing integration testing that involves client side JavaScript, things get more difficult. For example the client side could get the timezone from the browser and then use that for querying the server, this is really hard to mock. So the best strategy is to use the same timezone across the stack i.e. Browser, user model and server. To do this you need to set the application timezone in your tests to the timezone of the server running the CI. A particular gem that is very useful is timezone_local. If you are using factories you can then set the user timezone to the server timezone like this: ruby User.blueprint do time_zone { TimeZone::Local.get().name } end ##Conclusion

  • In general you don’t need to worry about setting the server timezone.
  • You also don’t need to worry about which methods in rails use timezones and which don’t
  • Add a timezone property to the user
  • Convert the timezone when dealing with users (using in_time_zone)
  • Use a consistent timezone across the stack for integration testing that involves client side JS