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:

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):

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.

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/detecttimezonerails

Use the user's IP address to infer the timezone, this can be a hit or miss

Instead of having currentuser as nil consider setting currentuser 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.

var epoch = ( new Date ()). getTime () / 1000 ;

The in Rails convert that value to a time:

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:

User . blueprint do time_zone { TimeZone :: Local . get (). name } end

Conclusion