adding timezone to your rails app

Courtenay : February 23rd, 2007

This is a nasty job, no doubt about it. But, a necessary one, particularly since I set the server time to UTC and all my applications' timestamps are now weirdly off-kilter. Unfortunately the seminal article on this topic got lost in the tubes so I'm documenting my experiences. Much of this was stolen from Jamis' announcement of tztime

Install all the gems and supporting libraries. You may have some of these already.

# gem install tzinfo

TZInfo provides the various conversions and cross-platform magic. Now the plugin for your rails app,

$ script/plugin install tzinfo_timezone

which will give you some conversion helpers and such. Finally, the new Jamis Buck wizardry

$ script/plugin install tztime

which now should replace Time in your app, so it'll do a lot of conversion to and from UTC for you. This plugin is useful if you're calling Time in your application. Anywhere you called Time.now you want to call TzTime.now. We'll also be using it as a singleton store to keep the current request's timezone.

Now, you're ready for the pain. Set your server's timezone to UTC. Depending on OS and arch, this may be as simple as one of these methods

# cp /usr/share/zoneinfo/UCT /etc/localtime
or
# export ENV['TZ']
or
# tzselect

This is going to screw a lot of things up if you aren't already on UTC and have other apps running. Maybe. Or maybe your OS can handle it seamlessly (hopefully). Caveat hackor.

Open up environment.rb and uncomment this line (requires a restart)

config.active_record.default_timezone = :utc

Open up application_helper.rb and write yourself a handy conversion method.

def tz(time_at)
  TzTime.zone.utc_to_local(time_at.utc)
end

Slick. We then need to configure the application to set the timezone.

class ApplicationController < ActionController::Base
  before_filter :login_required
  around_filter :set_timezone

  private
    def set_timezone
      TzTime.zone = current_user.tz
      yield
      TzTime.reset!
    end
end

You'll notice current_user has a tz field. Where does this come from? Add a string column to your the user model, called time_zone, and have it default to "UTC" or whatever your preferred default timezone is. FYI, this step depends largely on your application. You might have sites, stores or users as the top-level, so it may not be current_user but @site here.

Once you've added the time_zone column to the model, we need to have it parse the string representation of timezone info a proper zone.

class User < ActiveRecord::Base
  composed_of :tz, :class_name => 'TZInfo::Timezone', :mapping => %w( time_zone time_zone )
end

This creates the @user.tz method. You'll want people to edit their timezone no doubt, so users/_form.rhtml gets a new field,

<label>Default timezone</label>
<%= time_zone_select 'user', 'time_zone', TZInfo::Timezone.all.sort, :model => TZInfo::Timezone %>

There are some custom timezone selectors out there, but this should get you started.

Finally, search all occurances of date fields in your application, something like

<%= created_on %>
<%= updated_at %>

and make them use the tz helper

<%=tz user.created_at %>

And that's it!

(photo by *eclaire on flickr.)

9 Responses to “adding timezone to your rails app”

  1. Cyrille Says:

    Hi this is a nice post very usefull !

    Where can I find the custom timezone selectors you are talking about.

    I found the default list pretty long and would like a shorter liste like : GMT +1 (Paris, Madrid, Monaco…)

    Thanks

  2. Brian Says:

    We use javascript to determine the time zone automatically from their system settings by calling new Date().getTimezoneOffset(). Then we store this value in a cookie so that we can pass it back to the rails app on the server, which stays in our local timezone but formats times appropriately for the user’s settings.

    Obviously, the tradeoff is that your users must have javascript enabled and they must have their computer’s timezone settings correct. For our target audience, these are reasonable assumptions.

  3. atmos Says:

    I’d kill for this took hook into toformatteds instead of replacing all my time references in my views.

  4. Eric Baker Says:

    Ok, I’m a newbie to RoR, so please bear with me…

    In my app controller, my current_user is defined with:

    def current_user
        User.find(session[:user_id])
    end
    

    So before the user logs in, there is no current user, and therefore, the system is unable to get a current time zone. I could include a conditional that defaults to a specific zone, but wondering if there is a best practices strategy for dealing with this?

  5. Cyrille Says:

    Eric, you can try to replace:

    TzTime.zone = current_user.tz

    with:

    TzTime.zone = currentuser.tz if loggedin?

    And add:

    def logged_in? current_user != :false end

    Meaning that time zone will not be set if no user is logged in the system.

  6. Marcus Says:

    great article, here’s the archived version of the Lunchroom article: http://web.archive.org/web/20060425190845/http://lunchroom.lunchboxsoftware.com/pages/tzinfo_rails

    You’ve pretty much covered it but there it is for posterity.

  7. Alexandre Says:

    Thank you for the clarification. Is there any way to adopt this solution if you can’t change the local machine time to utc for some reason.

  8. Alexandre Says:

    Why, for the same hour of the day, the conversion change from winter to summer DST. I don’t get it. Is it a bug or is UTC different from GMT?

    TzTime.zone = TzinfoTimezone[“London”] => #<tzinfotimezone:0xb79234c4><tzinfo::datatimezone: />> tsummer = “Fri Aug 18 14:00:00 UTC 2006”.totime => Fri Aug 18 14:00:00 UTC 2006 twinter = “Sat Feb 18 14:00:00 UTC 2006”.totime => Sat Feb 18 14:00:00 UTC 2006 TzTime.zone.utcto_local( tsummer.utc) => Fri Aug 18 15:00:00 UTC 2006 TzTime.zone.utcto_local( twinter.utc) => Sat Feb 18 14:00:00 UTC 2006 Thank you for your help

  9. Alexandre (again) Says:

    Here is my example again:

    TzTime.zone = TzinfoTimezone[“London”]

    => #<tzinfotimezone:0xb79234c4><tzinfo::datatimezone: />> tsummer = “Fri Aug 18 14:00:00 UTC 2006”.totime => Fri Aug 18 14:00:00 UTC 2006 twinter = “Sat Feb 18 14:00:00 UTC 2006”.totime => Sat Feb 18 14:00:00 UTC 2006 TzTime.zone.utcto_local( tsummer.utc) => Fri Aug 18 15:00:00 UTC 2006 TzTime.zone.utcto_local( twinter.utc) => Sat Feb 18 14:00:00 UTC 2006

Sorry, comments are closed for this article.