mad.ly rails, jquery, flash, etc    about »

We're looking for senior developer. More Info

Posted by
Geoff Buesing

Posted on
9 April 2008 @ 10pm

Tagged
rails, time zones

Rails 2.1 Time Zone Support: An Overview

A Portuguese translation of this article can be found here.

This will be the first of several posts I’ll create about the new time zone features in the upcoming Rails 2.1 release. In this post, I’ll give an overview of the new features, by walking through the setup of a new app.

I’ll start with a fresh Rails 2.1 app created via the rails command. In 2.1, time zone support will be turned on by default in environment.rb, via the config.time_zone option:

# config/environment.rb
config.time_zone = ‘UTC’

– this will be set to UTC as the default, but you’ll most likely want to set this to a time zone appropriate for your locale. The new rake tasks time:zones:all, time:zones:us, and time:zones:local have been added to help one find appropriate time zone names. time:zones:local will make an educated guess based on the system local time, so that’s a good place to start:

$ rake time:zones:local

* UTC -06:00 *
Central America
Central Time (US & Canada)
Guadalajara
Mexico City
Monterrey
Saskatchewan

For this example, I’ll set config.time_zone to US Central Time:

# config/environment.rb
config.time_zone = ‘Central Time (US & Canada)

Next, I’ll create a simple scaffold for a Task model with an alert_at datetime attribute:

$ script/generate scaffold Task name:string alert_at:datetime
$ rake db:migrate
$ script/server

I’ll go to the new task form, and create a new task:

new_task1.png

The show action displays the date and time I entered, followed by the UTC offset:

show_task1.png

… for this example, the UTC offset is -0500, which is the offset for US Central Time during daylight savings.

To show how this time is stored in the database, I’ll go to script/console, and check #alert_at_before_type_cast:

>> t = Task.find_by_name(‘foo’)
=> #< Task … >
>> t.alert_at
=> Sun, 06 Apr 2008 10:30:00 CDT -05:00
>> t.alert_at_before_type_cast
=> "2008-04-06 15:30:00"

The database is storing the UTC representation of our time: 15:30 UTC is simultaneous with 10:30 CDT (Central Daylight Time.) The difference between the two times is the UTC offset (-5 hours, in this case.)

Next, I’ll edit the task and change the month to January:

edit_task.png

Notice the offset parameter is -0600 now — that’s because the alert_at date is no longer in daylight savings time.

show_task_updated.png

script/console will confirm that the database received the correct UTC representation:

>> t = Task.find_by_name(‘foo’)
=> #< Task … >
>> t.alert_at
=> Sun, 06 Jan 2008 10:30:00 CST -06:00
>> t.alert_at_before_type_cast
=> "2008-01-06 16:30:00"

The database time is now 16:30 instead of 15:30, because the UTC offset is -6 hours now.

User-specific time zones

What I’ve set up so far will work fine for an app where all of its users are in one time zone. If the app eventually needs to support users in different time zones, that’s easy enough to add.

First, I’ll create a user scaffold, with a string attribute to store the user’s time zone

$ script/generate scaffold User name:string time_zone:string
$ rake db:migrate

I’ll change the user create and edit forms to use the time zone select instead of a text field:

# views/users/new.html.erb
<%= f.time_zone_select :time_zone, TimeZone.us_zones %>

The new user form now looks like this — I’ll use it to create a couple users with different time zones:

new_user.png

For demo purposes, I’ll add a simple login_from_querystring before_filter to the application controller:

# controllers/application.rb
before_filter :login_from_querystring

def login_from_querystring
@current_user = User.find_by_name(params[:user])
end

I’ll then add a set_time_zone before_filter, which will set Time.zone to the current logged-in user’s time zone:

# controllers/application.rb
before_filter :set_time_zone

def set_time_zone
Time.zone = @current_user.time_zone if @current_user
end

I’ll add a header in the layout to show who’s logged in, their time zone, and the current time in their zone:

# views/layouts/tasks.html.erb

Current user: <%= @current_user.name if @current_user %>
Current time zone: <%= Time.zone.name %>
Current time: <%= Time.zone.now.inspect %>
<hr />

Finally, I’ll change the tasks index view to use the #inspect representation of alert at, to reveal additional detail:

# views/tasks/index.html.erb
<%=h task.alert_at.inspect %>

Now, if I log in as one of the users I created, I’ll see the task I created before, with the alert_at time adjusted to the current user’s time zone:

index_ralf.png

… notice the time displayed for the task is 11:30 EST — that’s simultaneous with 10:30 CST.

For a user in US Mountain Time, the task displays as 9:30 MST:

index_florian.png

Without a logged in user, the time zone defaults to the zone set in config.time_zone:

index_no_user.png

Methods for creating times in the current Time.zone

So far, we’ve been relying on ActiveRecord to automatically convert model attributes to the user’s local time. For cases where you need to create new time instances in the user’s local zone, the methods Time.zone.local(), Time.zone.parse() and Time.zone.at() are available, as well as Time.zone.now:

>> Time.zone = ‘Hawaii’
=> "Hawaii"
>> Time.zone.now
=> Wed, 09 Apr 2008 15:48:18 HST -10:00
>> Time.zone.local(2008, 4, 9, 15, 48, 18)
=> Wed, 09 Apr 2008 15:48:18 HST -10:00
>> Time.zone.parse(2008-04-09 15:48:18)
=> Wed, 09 Apr 2008 15:48:18 HST -10:00
>> Time.zone.at(1207792098)
=> Wed, 09 Apr 2008 15:48:18 HST -10:00

Time and DateTime #in_time_zone will convert any instance to the zone stored in Time.zone:

>> Time.zone = ‘Alaska’
=> "Alaska"
>> t = Time.utc(2000)
=> Sat Jan 01 00:00:00 UTC 2000
>> t.in_time_zone
=> Fri, 31 Dec 1999 15:00:00 AKST -09:00

…or, to any zone or zone identifier (i.e., name, integer or Duration) passed in as an argument:

>> t.in_time_zone(‘Hawaii’)
=> Fri, 31 Dec 1999 14:00:00 HST -10:00
>> t.in_time_zone(-6.hours)
=> Fri, 31 Dec 1999 18:00:00 CST -06:00

Tips Upgrading Existing Apps

  1. the new time zone features assume that the database is storing times in UTC, so if you’ve currently storing times in the database in a zone other than UTC, you’ll need to migrate existing data to UTC
  2. if the tzinfo_timezone plugin is installed, you’ll need to remove it, given that it overrides the TimeZone class in ActiveSupport
  3. the TZInfo gem is no longer required, given that it’s now bundled in ActiveSupport. However, if you do have a recent version of this gem installed, Rails will favor the gem over the bundled version.
  4. The bundled TZInfo is a slimmed-down version of the gem, so if you’re interacting with the TZInfo API directly, you should have the gem installed
  5. If you *don’t* wish to use the new time zone features — the new features shouldn’t interfere with your existing code, as long as you don’t declare config.time_zone in environment.rb

More To Come

In future posts, I’ll try to cover more of the under-the-hood stuff, but hopefully this post will help to get folks up to speed.

If you find time zones, UTC offsets, and daylight savings time confusing, you might want to check out these Time Zone Visualizations, which might just confuse you even more…

Related Posts:
Making Rails time zone aware attributes and Chronic play well together

Two fixes to ActiveSupport::TimeWithZone


38 Comments

Posted by
Neeraj
10 April 2008 @ 8am

Awesome.


Posted by
Eric Anderson
10 April 2008 @ 12pm

Great post. I had used the mix of plugins and gems before this and it was still a pain. This implementation feels so out-of-the-box and natural.


Posted by
Alistair Holt
10 April 2008 @ 2pm

Very nice write up.


Posted by
Deepak Jois
10 April 2008 @ 8pm

Really helpful. Thanks!


Posted by
AkitaOnRails
11 April 2008 @ 1pm

Great post. If you’re writing another article one thing I would like to see discussed is Daylight Saving and appointments between 2 people in 2 different time zones. The 1st guy sets an appointment when it is in his daylight saving period. A few months later, the recurrent event is going to coincide with the 2nd user for a meeting or something. The UTC will differ because of DST. How to handle these kinds of situations. I did somethings but I am interested in how other people are solving this situation.


Posted by
Daniel Fischer
12 April 2008 @ 12am

Thank you very much for this introduction to a new feature. This looks awesome :)


Posted by
John Topley
12 April 2008 @ 2am

Very helpful – thank you!


Posted by
iGEL
12 April 2008 @ 2am

Hi!

Is it possible to use tzdata names like Europe/Berlin or America/New_York instead of the time zone names? That might be more future compatible, if some state changes it’s time rules.

iGEL


Posted by
Eduardo Pérez Ureta
12 April 2008 @ 3am

For client/server applications where the client is not dumb, like web applications, I think it would be a better idea to show the user the date/time in the use format and timezone. ROR already needs javascript support in the browser and javascript supports date/time internationalization and localization using code like:
dlocale = new Date();
dlocale.toLocaleDateString() + ” ” + dlocale.toLocaleTimeString();

You just need to send unencoded (ISO 8601 and UTC) date/time and let the client javascript process the date/time to the user selected format and timezone.


Posted by
Geoff Buesing
12 April 2008 @ 1pm

Everyone: thanks for the nice feedback :)

iGel: Indeed, you can use tzdata/Olson names like Europe/Berlin if you want — any name that’s recognized by either the Rails TimeZone class or the TZInfo gem will work. As far as future compatibility, you’re fine with using either naming scheme — ultimately, you’re relying on the transition rules in TZInfo, which gets updated frequently as these rules change. Update the gem, and your app will be using the latest rules.

Akita: I’d be interested to hear about how you’re dealing with recurring events, if you’ve got a blog post on that. That might be a good starting point.

Eduardo: that solution will work fine for some cases, but not all. Keep in mind that the client might not be as smart as you need it to be — not all operating systems keep historical time zone information, and for those that do, not all users have kept this information up-to-date, via service pack updates etc.


Posted by
Carlos Júnior
13 April 2008 @ 8am

I’ve following this implementation in rubyonrails-core list. This is really awesome!!! With your permission, I’ll translate it to portuguese (pt_BR), is it ok for you?

Btw, congratulations to your initiative, the article is really useful and well written…


Posted by
Geoff Buesing
13 April 2008 @ 10am

Carlos: thanks! Please feel free to do a Portuguese translation — I’ll gladly link to it from here. Same for anyone else who wants to do a translation.


Posted by
Steven Soroka
13 April 2008 @ 1pm

re: “15:30 UTC is simultaneous with 10:30 CDT”; I think you mean it’s synonymous. Though simultaneous almost fits there. ;)


Posted by
Scott McMillin
13 April 2008 @ 2pm

@Steven Soroka:
Actually I think Geoff does mean *simultaneous* the definition of which is “occurring, operating, or done at the same time” as opposed to synonymous which is defined as “having the same meaning” and connotes equivalence of words or terms–which I don’t think applies in this case.


[...] be released, if you are curious about the improved support for Time Zones, don’t miss this well explained overview by Geoff Buesing. The new features are just plain awesome and will make the trouble of upgrading [...]


Posted by
SitePoint Blogs
13 April 2008 @ 10pm

[...] Geoff Buesing goes through the new Timezone support [...]


Posted by
Jared Fine
14 April 2008 @ 8am

Great work Geoff!


Posted by
Joe Grossberg
14 April 2008 @ 9am

Will we still need to use plugins, though, for AM/PM? (Sorry if this was covered previously.)

Lots of people have no idea what, for example, 13:00:00 means. :/


Posted by
Nicolás Sanguinetti
15 April 2008 @ 11am

Sweet. I have to migrate all my apps to 2.1 now :) (only this and the has_finder thingy makes the time totally worth it)

@Eduardo: Actually, no. Javascript timezone support isn’t great across browsers (different browsers will use different ways of showing you the offset, and they might –iirc ie6, but it was a long time ago– don’t even show it under some circumstances. So you’d end up having to fall back to the backend for all this.

For whatever your dating purposes are, stick to your server, and keep clients as dumb as possible. It’s gonna be soooo much easier for you.

If what worries you is getting the time localized to the client, then there are some good localization options for rails (I’ve used globalite a couple times and it worked great).


Posted by
Geoff Buesing
16 April 2008 @ 7am

@Nicolas: thanks for the heads up on cross-browser inconsistencies with showing the offset via Javascript — if you’ve happen to have a link to more information on this, please post here.

@Joe: correct, for a datetime select with am/pm you’ll need to rely on a plugin for now.


Posted by
Binh Ly
16 April 2008 @ 5pm

If you have an application that is running in Pacific time, and it does hourly alerts which users can schedule, what is the best way to go about finding all of the alerts that you should send out that hour? Would you just iterate through all the timezones and computing based on offset?

If a user scheduled an alert for 6 PM Eastern time, the app would have to check for that at 3 PM. So at 3 PM would the app just check for all scheduled alerts one timezone at a time or is there a more recommended approach?

I apologize if this was the wrong place to ask this. I am very excited and grateful for the work put in; now I’m just wondering if there is any additional magic that might help solve my problem.

Thank you in advance.


Posted by
David
17 April 2008 @ 8am

Hi, This addition to rails makes working with timezones a pleasure at last! :)

I do have some questions though, I noticed that the time_zone_select form helper now shows the time zones based on the UTC offset, e.g. (UTC-10:00 Hawaii). Is there any way to have this show the time with GMT instead of UTC? UTC doesn’t mean very much to non technical users.

Also do you know how to show a more resticted set of TimeZones (1 entry per offset) rather than one for each country and region, e.g. “(GMT) Western Europe Time, London, Lisbon, Casablanca”


Posted by
Geoff Buesing
18 April 2008 @ 8am

@David: as far as the time zone select displaying UTC instead of GMT — there’s no way to change this, outside of monkeypatching. As to whether non-technical users would be better off seeing GMT as opposed to UTC: a case can certainly be made for this. If you want to pursue this further, I suggest submitting a patch to Rails, and/or raising this question on the Rails core list.

Regarding building a time zone select with more restricted options — you’d have to build these options yourself. Keep in mind that if you roll up multiple zones into one selection — e.g. “(GMT) Western Europe Time, London, Lisbon, Casablanca” — each one of those zones may have different DST rules. So you might not be doing the user any favors here.

@Binh: the problem you pose is really an application design challenge. I haven’t seen much floating around the Rails world about these kind of scenarios. You might try posting something on the Rails talk list.

If anyone can put forth a potential solution to this kind of scenario, I’ll be happy to add my 2 cents in as to how to best do the necessary time zone conversions.


Posted by
iGEL
20 April 2008 @ 2pm

Thanks for your reply. I’ve froze my previous at stable release developed app to edge today for this feature. So far, it seems to work fine. But one question:

How do I convert a Time object to TimeWithZone? Isn’t there a more comfortable way than that?

ActiveSupport::TimeWithZone.new(Time.now, TZInfo::Timezone.get(“Asia/Seoul”))

And is there an mktime method?


Posted by
Geoff Buesing
20 April 2008 @ 6pm

@iGEL: You shouldn’t need to create a TimeWithZone directly — any Time or DateTime instance can be coerced to the current Time.zone via #in_time_zone:

>> Time.zone = ‘Asia/Seoul’
=> “Asia/Seoul”
>> Time.now.in_time_zone
=> Mon, 21 Apr 2008 09:43:51 KST +09:00

For Time.now in Time.zone, you can also do Time.zone.now:

>> Time.zone.now
=> Mon, 21 Apr 2008 09:44:33 KST +09:00

And for a mktime/local factory method, you can use TimeZone#local:

>> Time.zone.local(2008,4,20)
=> Sun, 20 Apr 2008 00:00:00 KST +09:00

The section in this article titled “Methods for creating times in the current Time.zone” will show you a few more examples.


Posted by
Chris Hobbs
21 April 2008 @ 2pm

spongecell.com uses ActiveResource to create API calendar apps. The process is slightly messy because of poor time zone support in rails and ruby. From what I see here I imagine that ActiveResource in rails 2.1 could be a lot easier to use.

Geoff, thanks for the hard work!


Posted by
Zach
9 May 2008 @ 7am

Info about migrating an existing database to UTC would be greatly appreciated. Especially as it relates to DST (presumably half of what I have is GMT, half BST).

Thanks for the write up.


Posted by
Matt Darby
16 May 2008 @ 3pm

I’ve written up a blog post about converting existing non-UTC timestamps to UTC aware timestamps: http://blog.matt-darby.com/2008/05/16/iterating-over-all-models-in-rails/


Posted by
Alex
17 May 2008 @ 7am

Thank you for posting this guide. It has helped a lot. I’m kind of in the same situation as Binh in that I am trying to identify the best way to iterate through each user’s time zone as a cron job that way the app can take action based on the time users entered to get reminders etc.

Looking forward to future posts! Thanks for your help and contributions to Rails!


Posted by
mima
17 May 2008 @ 5pm

Time zones in Rails! Unicode in Rails! After all these years, they’ve come to realize that there’s a world out there. Now I hope for a strftime with locales.


Posted by
Danimal
18 May 2008 @ 1pm

Geoff,

Thanks for putting this together! Quick question: for those of us who aren’t able to use Edge Rails (yet), can I get all this goodness in a plugin (or two?) Is it just the TZInfo gem? or is there more?

Also, with Binh’s question: isn’t the whole thing moot if you are using UTC for everything? I.e. you do your hourly run, and find any alerts that match… in UTC… the current server time… in UTC. Then you are comparing apples-to-apples. The whole point being: timezones are for end-user simplicity. “flatten” everything to UTC internally for technical simplicity.

Woo!

-Danimal


Posted by
Ernie
3 June 2008 @ 6am

Geoff, thanks for this! I played around with the new 2.1 time zone support and it works very cleanly. I tossed together a quick blog post discussing one of the gotchas you listed, migrating existing non-UTC data to UTC, specifically discussing MySQL.

I figure it may be of some help to newer Rails developers upgrading an app to 2.1, and MySQL’s convert_tz is a simple way to get even large databases updated very quickly. The post is at http://thebalance.metautonomo.us/2008/06/02/updating-mysql-datetimes-for-rails-21-time-zones/ … hope it helps someone.


Posted by
Pankaj
4 June 2008 @ 8am

Hi Geoff,

Very nice feature…and helfull article.

Is it mandatory that mysql timezone should be ‘UTC’?
In one of my application I want to show data to user based on his local timezone.And our server timezone is PDT.

Can you give any tactics?…

Thanks..


Posted by
Trevor Turk
9 June 2008 @ 10am

Here’s a quick migration for people moving from time zone support in Rails < 2.1:

http://almosteffortless.com/2008/06/03/migrate-to-the-rails-default-time-zones/


Posted by
kent katayama
1 July 2008 @ 5am

Thank you very much! This was very helpful to start off. I will be back to look at this and more to understand Ruby on Rails. I am just a beginner, but given some time to absorb this … I hope to become very efficient …

Mahalo and Aloha,

Kent


Posted by
Fran
14 July 2008 @ 9am

Hello and thanks for this!
Problem: a year ago Venezuela (Caracas) changed its timezone from -4:00 to -4:30. How can this be corrected on the timezones database in Ruby?


Posted by
Robert Vogel
21 July 2008 @ 9am

Great article & Thanks!

I’m implementing Time Zones, and so far it is going pretty smoothly. I write about one Gotcha.

My code is peppered with calls to Time.now – and I was worried about how Rails would interprete that object. Time.now.class = Time whereas Time.zone.now.class = ActiveSupport::TimeWithZone.

SO, I’ve been concerned that I would need to replace my Time.now’s with Time.zone.now’s.

So far Rails knows what do to with Time.now – it compares it against TimeWithZone fine, and handles assignments fine: E.g., Model.updated_at = Time.now converts the Time to TimeWithZone fine.

However, there is one place where I do need to change my code: in placeholders. For example, I need to change:

Invite.count(:conditions => ['created_at > ?', Time.now.yesterday])

to

Invite.count(:conditions => ['created_at > ?', Time.zone.now.yesterday])

It appears that the code to interpolate the placeholders does not know about Time.zone.


Posted by
David Baldwin
20 August 2008 @ 4pm

I wrote a blog post about a plugin called “models_to_utc” that provides a migration method to easily convert legacy timestamps to UTC. This method is not dependent on any code within your application so is perfectly suited for ports from PHP or others…

http://bilsonrails.wordpress.com/2008/08/17/models_to_utc-rails-plugin/