Peter Marklund

Peter Marklund's Home

2006-08-16

Rails Recipe: A Timezone Aware Datetime Picker

NOTE! Only follow this recipe if you are using an earlier version of Rails than 2.1. As of Rails 2.1 timezone support is built into Rails in a way that makes it much easier to create timezone aware applications.

Quite often in web applications we display dates and times and also have users input them and store them in our databases. If your users are spread across the globe you really want to be able display times in the users own timezone. How do we accomplish this with Rails?

First off, as Scott Baron has pointed out, you want to grab a copy of the TZInfo Ruby timezone library. We need TZInfo since it can deal with daylight savings time (summer/winter hourly adjustments), something that the Timezone library that ships with Rails is not able to do. Installation is as simple as downloading the latest tgz file from RubyForge, extracting it into vendor/tzinfo, and requiring the library from config/environment.rb. We also add some new settings:

# In config/environment.rb
ActiveRecord::Base.default_timezone = :utc # Store all times in the db in UTC
require 'tzinfo/lib/tzinfo' # Use tzinfo library to convert to and from the users timezone
ENV['TZ'] = 'UTC' # This makes Time.now return time in UTC

The strategy we are adopting here is that times in the UI when shown to or entered by the user are in the users own timezone. In the application on the other hand, i.e. in Ruby code and in the database, times are always kept in the UTC timezone. Our job then becomes to allow the user to select a timezone, and then to convert back and forth between this timezone and the UTC timezone as needed. To store the user timezone we can add a time_zone string column to the users table and use the composed_of macro just like Scott Baron describes:

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

Then add a timezone select on a "Set Timezone" preference page that the user can access:

<% # Select using TZInfo timezone names such as "Europe - Amsterdam"
   # In the controller on submit you can then do 
   # @user.tz = TZInfo::Timezone.new(params[:user][:timezone_name])
<%= time_zone_select 'user', 'timezone_name', TZInfo::Timezone.all.sort, :model => TZInfo::Timezone %>

If you prefer the timezone names of the Rails Timezone class you can use them instead and then convert to a TZInfo timezone object:

<% # Select using the Rails Timezone names such as "(GMT+01:00) Amsterdam". Will require a
   # conversion to the TZInfo timezone on submit. %>
<%= time_zone_select 'user', 'timezone_name' %>

# Helper method in the controller
def tzinfo_from_timezone(timezone) 
  TZInfo::Timezone.all.each do |tz|
    if tz.current_period.utc_offset.to_i == timezone.utc_offset.to_i
      return tz
    end
  end
  return nil   
end

# On submit in the controller we convert from Rails Timezone to TZInfo timezone via the UTC offset
# and store the user timezone in the database.
@user.tz = tzinfo_from_timezone(TimeZone.new(params[:user][:timezone_name])
@user.save

Now when displaying times in the UI we can consistently convert them to the users timezone with a helper like this:

  def format_datetime(datetime)
    return datetime if !datetime.respond_to?(:strftime)
    datetime = @user.tz.utc_to_local(datetime) if @user
    datetime.strftime("%m-%d-%Y %I:%M %p")
  end

To have the user enter a date and a time in a user friendly fashion you can install the bundled_resource plugin and use its JavaScript based calendar date picker. When displaying the date to the user in an HTML form we use @user.tz.utc_to_local to convert from UTC to the users timezone, and when receiving a date from a form submit we convert back to UTC with @user.tz.local_to_utc:

# The new action:
def new
  @email = BulkEmail.new
  @email.schedule_date = @user.tz.utc_to_local(Time.now) # Default schedule date in local time
end

# new.rhtml
<%= dynarch_datetime_select('email', 'schedule_date', :select_time => true) %>

# The create action that the new form submits to
def create
  @email = BulkEmail.new(params[:email])
  # Convert the local schedule date from the form to UTC time
  @email.schedule_date = @user.tz.local_to_utc(@email.schedule_date)
  if @email.save
    ...
  else
    ...
  end
end

# The edit action
def edit
  @email = BulkEmail.find_by_id_and_group_id(id, session[:group_id])
  # Show scheduled date in local time
  @email.schedule_date = @user.tz.utc_to_local(@email.schedule_date) 
end

# The update action that edit submits to
def update
  @email = BulkEmail.find_by_id_and_group_id(id, session[:group_id])
  @email.attributes = params[:email]
  @email.schedule_date = @user.tz.local_to_utc(@email.schedule_date)

  if @email.save
    ...
  else
    ...
  end  
end

Apparently there is also a Ruby on Rails TZInfo plugin that I discovered only now as I was writing this post and I haven't looked into using it yet. A very helpful page when dealing timezones is the timeanddate.com World Clock.

Testing of the functionality described here is left as an exercise for the reader...

124 comment(s)

Comments

utangac said 2010-11-23 21:53:

eline salık
--------------------------------------------------------------------------------

grammar checking said 2010-11-19 23:02:

Nice, having to convert times/dates from one area user to the next is always a hassle..with rails it truely is less time sensitive.
--------------------------------------------------------------------------------

mike said 2010-10-29 21:12:

Great Posts! keep up the good work peter.
--------------------------------------------------------------------------------

meningitis symptoms said 2010-10-26 18:23:

Here is my function to display a timestamp (from application_helper.rb): def std_timestamp(datetime) return datetime if !datetime.respond_to?(:strftime) datetime = session['account'].tz.utc_to_local(datetime) if session['account'] datetime.strftime("%b %d, %Y %I:%M %p") end #account.rb: composed_of :tz, :class_name => 'TZInfo::Timezone', :mapping => %w(time_zone identifier)
--------------------------------------------------------------------------------

Emre Çolak said 2010-10-06 14:53:

good job. thanx.
--------------------------------------------------------------------------------

Nokia 2690 said 2010-09-22 10:54:

Get the latest downloads, wallpapers, themes and ringtones for your nokia mobile! http://www.nokia1616.com http://www.nokia2690.com http://www.nokia2710.com http://www.nokia6350.com
--------------------------------------------------------------------------------

surgery games said 2010-09-17 20:23:

The patients are prepped up and ready for the operation. Nurses will be assisting you as always, so take the scalpel and have fun. http://www.surgery-games.org http://www.hospital-games.net http://www.mydoctorgames.com
--------------------------------------------------------------------------------

2 player games said 2010-09-16 22:34:

all games are to play for 2 or more players
--------------------------------------------------------------------------------

David Bieber said 2010-07-26 07:55:

Here is my function to display a timestamp (from application_helper.rb): def std_timestamp(datetime) return datetime if !datetime.respond_to?(:strftime) datetime = session['account'].tz.utc_to_local(datetime) if session['account'] datetime.strftime("%b %d, %Y %I:%M %p") end #account.rb: composed_of :tz, :class_name => 'TZInfo::Timezone', :mapping => %w(time_zone identifier) http://internethostingweb.com/ http://gas4freeweb.co.cc/ http://electroniccigarettesmokes.com/ http://errorfixweb.com/
--------------------------------------------------------------------------------

David Bieber said 2010-07-26 07:53:

I used this instead: composed_of :tz, :class_name => ‘TZInfo::Timezone’, :mapping => %w(time_zone identifier) end http://tavateaweb.com/
--------------------------------------------------------------------------------

Sell Gold said 2010-07-13 17:42:

I've been getting the same error as a few other people, TZInfo::Timezone constructed directly. What should I do?
--------------------------------------------------------------------------------

free promote products said 2010-07-11 09:10:

Very good work Peter, thank for the solution !
--------------------------------------------------------------------------------

Nye Mobiltelefoner said 2010-06-11 04:39:

Now when displaying times in the UI we can consistently convert them to the users timezone with a helper like this: def format_datetime(datetime) return datetime if !datetime.respond_to?(:strftime) datetime = @user.tz.utc_to_local(datetime) if @user datetime.strftime("%m-%d-%Y %I:%M %p") end ty for the wonderfun tip here. <a href="http://www.nyemobiltelefoner.dk/">Nye Mobiltelefoner</a>
--------------------------------------------------------------------------------

maria smith said 2010-05-28 04:31:

<a href="http://www.printdesigns.com/">roller banner</a><br>
--------------------------------------------------------------------------------

Strathwood Falkner Dining Table said 2010-04-24 08:52:

You guys do a great website ,thanks for your sharing this information
--------------------------------------------------------------------------------

usa shop online said 2010-03-24 14:54:

Good job! THANKS! You guys do a great blog
--------------------------------------------------------------------------------

fein multimaster top said 2010-03-18 11:52:

i love it please bookmark this page in digg or mixx because I can find it easily http://astore.amazon.com/fein-multimaster-top-fein-multimaster-start-fmm-fien-20
--------------------------------------------------------------------------------

baby diapers said 2010-02-05 23:01:

Good job! You guys do a great blog, and have great contents. Keep up the good work. THANKS!
--------------------------------------------------------------------------------

Wrist Weights said 2010-02-02 03:04:

How about some food recipies now? Im kind of hungry hmm.
--------------------------------------------------------------------------------

sofas sectional said 2010-01-30 18:19:

I love the blogging and you. http://sofassectional.com http://outdoorlivingpatio.com
--------------------------------------------------------------------------------

rockwell sonicrafter said 2010-01-30 18:17:

http://astore.amazon.com/rockwell-sonicraft-20 Every problem has a solution, thank you. Saviez vous que le radiateur à inertie est la solution la plus économique. http://astore.amazon.com/rockwell-sonicraft-multimaster-sonic-rafter-sonicraft-tool-20
--------------------------------------------------------------------------------

4 multimaster said 2010-01-30 18:16:

Thank for blogging http://mintpowertool.com rockwell rk5102k sonicrafter http://astore.amazon.com/rockwell-sonicraft-20 rockwell soniccrafter http://astore.amazon.com/rockwell-sonicraft-multimaster-sonic-rafter-sonicraft-tool-20 rockwell soniccrafter http://giftpowertools.com fein multimaster
--------------------------------------------------------------------------------

free online advertising said 2009-12-29 15:50:

Good job! THANKS! You guys do a great blog, and have some great contents. Keep up the good work. best regards, http://www.tengwar.org
--------------------------------------------------------------------------------

ridons said 2009-11-22 10:27:

Such a supreme topic about this good post. This is interesting to see the essay writing service that would like to do the <a href="http://www.exclusivepapers.com">essay writing</a> and <a href="http://www.exclusivepapers.com">custom essay</a>. Thus students will buy a paper.
--------------------------------------------------------------------------------

Karina Jett said 2009-11-05 20:18:

I'm getting the an error simlar to Robert above. In my controller I have @email.start_time = @user.tz.local_to_utc(@email.start_time). Any idea why?
--------------------------------------------------------------------------------

radiateur inertie said 2009-08-05 08:21:

Every problem has a solution, thank you. Saviez vous que le radiateur à inertie est la solution la plus économique.
--------------------------------------------------------------------------------

voyant said 2009-08-05 07:59:

I made a french translation for France. Pour ceux qui souhaite faire une consultation de voyance en ligne.
--------------------------------------------------------------------------------

accessoires pour chiens said 2009-07-01 21:44:

No solution for problems on my <a href="http://www.lookatmydog.net/en">dog accessories</a> e-shop:(
--------------------------------------------------------------------------------

Mike - Music Notation Software said 2009-06-17 19:03:

I am currently getting a project developed on Ruby...I wonder if a newer version of Ruby can handle the time zone differences?
--------------------------------------------------------------------------------

blog voyance said 2009-06-10 12:27:

Very good work Peter, thank for the solution !
--------------------------------------------------------------------------------

photographe liège said 2009-06-02 19:48:

Thanks for sharing the solution:)
--------------------------------------------------------------------------------

Bwin said 2009-05-08 09:30:

I always used this instead: composed_of :tz, :class_name => ‘TZInfo::Timezone’, :mapping => %w(time_zone identifier) end seems to work ok. :) thanks for this info....
--------------------------------------------------------------------------------

pornhub said 2009-04-15 08:00:

Very good blog Peter, tell us about whether your log works for s p a m. I also looking for for my blog. Thank you
--------------------------------------------------------------------------------

buy%20wow%20gold said 2009-04-13 10:41:

I'm getting the same error as Robert above. In my controller I have @email.start_time = @user.tz.local_to_utc(@email.start_time) Any idea what causes this error?
--------------------------------------------------------------------------------

http://www.telavenir.com said 2009-04-04 03:41:

very good post thank
--------------------------------------------------------------------------------

PHR said 2009-04-03 19:44:

I am using Rails with Java on my health management site, and have this issue TZInfo::UnknownTimezone (TZInfo::Timezone constructed directly): /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:220:in `unknown’ -Philips R
--------------------------------------------------------------------------------

Anonymous said 2009-04-03 11:00:

Hi, I think I have a problem, it seems to be bad. Have you got an idea ? class User < ActiveRecord:Base composed_of :tz, :class_name => ‘TZInfo::Timezone’, :mapping => %w(time_zone time_zone) end <a href="http://www.menuiserie-lepape.fr">menuiserie lannion</a>
--------------------------------------------------------------------------------

Anonymous said 2009-04-03 03:42:

If you are familiar[URL=http://www.firstcrazy.com]hair straighteners[/URL] with the map[URL=http://www.firstcrazy.com/ghd-hair-straighteners.html]GHD[/URL] editor [URL=http://www.firstcrazy.com/chi-hair-straighteners.html]chi hair straighteners[/URL]before,
--------------------------------------------------------------------------------

tdq said 2009-04-03 03:41:

If you are familiar <a href="http://www.firstcrazy.com" title="hair straighteners">hair straighteners</a>with the map editor before,<a href="http://www.firstcrazy.com/ghd-hair-straighteners.html" title="GHD">GHD</a> you will have <a href="http://www.firstcrazy.com/chi-hair-straighteners.html" title="chi hair straighteners">chi hair straighteners</a>the same map editor<a href="http://www.bagsset.com" title="replica handbags">replica handbags
--------------------------------------------------------------------------------

betclic said 2009-04-02 15:38:

Thanks for sharing your knowledge ! very usefull and interesting for us !
--------------------------------------------------------------------------------

Justin Meyer said 2008-06-24 22:18:

Is there a way to automatically grab the user's timezone from rails? I could have it sent through JavaScript, but I'm wondering if you can get that from somewhere else.
--------------------------------------------------------------------------------

fashion said 2008-05-20 01:58:

I have working the Timezone plugin. I use the <%= time_zone_select 'user', 'timezone_name' %> select. But when I edit again this field, in the select not appear the zone selected previously. It happen because I have save the time_zone en the plugin format. I code a method to get timezone from tzinfo: def timezone_from_tzinfo(tzinfo) TimeZone.all.each do |timezone| if tzinfo.current_period.utc_offset.to_i == timezone.utc_offset.to_i return timezone end end return nil end But, When I set a zone, for example, GMT -3, I have many cities with this timezone, and in the select appear selected another of the GMT -3 cities, not the one a have selected previously. __________________________________________________________ hah!yea!!
--------------------------------------------------------------------------------

gdhy@yahoo.com said 2008-05-20 01:56:

I used this instead: composed_of :tz, :class_name => ‘TZInfo::Timezone’, :mapping => %w(time_zone identifier) end http://linkatopia.com/saleisha seems to work ok. :) http://www.interiordesign-office.com/office-renovation.html
--------------------------------------------------------------------------------

madhukarpadma@yahoo.com said 2008-05-15 11:17:

Hi , Is there anyway I can display time in “US-Central” or “US-Eastern” format instead of “ America-New york” or “ Europe-Athens” format at the top of drop down list using Tzinfo . I am presently using “time_zone_select” which displays US zones on top of drop down in “ America-New york” format . <%= time_zone_select ('profile', 'time_zone',TZInfo::Timezone.us_zones, :model => TZInfo::Timezone, :default => "America/Chicago" )%> I can display US zones in “ (GMT - 06:00 ) CentralTime (US & Canada)” by using standard rails TimeZone ( <%= time_zone_select ('profile', 'time_zone' > ) . But , I will have problem when I use “utc_to_local” method . Thanks, Madhu Nallamani
--------------------------------------------------------------------------------

mmo said 2008-04-21 14:05:

Nice, having to convert times/dates from one area user to the next is always a hassle..with rails it truely is less time sensitive.
--------------------------------------------------------------------------------

madhukarpadma@yahoo.com said 2008-04-18 18:18:

Guys , I found solution to problem you guys are facing . TZInfo::UnknownTimezone (TZInfo::Timezone constructed directly): /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:220:in `identifier’ /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:485:in `_dump’ The solution is that the time_zone column in table should in "Europe/Samara" . In order to store data in this format you have to have <%= time_zone_select 'profile', 'time_zone', TZInfo::Timezone.all.sort, :model => TZInfo::Timezone %> Thanks, Madhu Nallamani
--------------------------------------------------------------------------------

oipi said 2008-04-16 05:14:

opoi
--------------------------------------------------------------------------------

stewie.halo@yahoo.com said 2008-04-15 11:09:

I am getting this error " TZInfo::Timezone constructed directly " This is how my code in view looks like . <%=@user.profile.tz.utc_to_local(event.timestamp).strftime("%m/%d/%Y %H:%M")%> Any help is appreciated . Thanks, Nallamani
--------------------------------------------------------------------------------

Jeff said 2007-11-04 13:31:

seems better to me to not bother with the mapping and just store the string in the user record. Or store both. The reason, you can't map back the other way so you can't show what timezone they are actually in (as someone mentioned it will pick the first one that matches)
--------------------------------------------------------------------------------

mjmac said 2007-08-20 11:29:

I've found the solution to the TZInfo::UnknownTimezone error, and I thought I'd post it here for intartubes reference. Look carefully at your :composed_of statement... In particular, the :mapping clause. Look again. I'm willing to bet a dirty nickel that you have %(time_zone time_zone) instead of %w(time_zone time_zone). D'OH!
--------------------------------------------------------------------------------

Damu said 2007-07-05 15:08:

I have working the Timezone plugin. I use the <%= time_zone_select 'user', 'timezone_name' %> select. But when I edit again this field, in the select not appear the zone selected previously. It happen because I have save the time_zone en the plugin format. I code a method to get timezone from tzinfo: def timezone_from_tzinfo(tzinfo) TimeZone.all.each do |timezone| if tzinfo.current_period.utc_offset.to_i == timezone.utc_offset.to_i return timezone end end return nil end But, When I set a zone, for example, GMT -3, I have many cities with this timezone, and in the select appear selected another of the GMT -3 cities, not the one a have selected previously.
--------------------------------------------------------------------------------

lofd23@yahoo.de said 2007-05-27 15:55:

I am getting the same error. TZInfo::Timezone constructed directly please explain us the problem PETER!!!
--------------------------------------------------------------------------------

Brandon said 2007-03-26 09:16:

I'm getting the same error as Robert above. In my controller I have @email.start_time = @user.tz.local_to_utc(@email.start_time) but when I hit that line I get: TZInfo::UnknownTimezone (TZInfo::Timezone constructed directly): /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:220:in `identifier’ /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:485:in `_dump’ Any idea what causes this error?
--------------------------------------------------------------------------------

Brad said 2006-12-19 12:44:

My apologies, my helper should look like this: <pre><code> def format_time(time) # Unfortunately very verbose "<script>document.write(new Date(#{time.to_i}*1000).toLocaleString())</script>" end </code></pre>
--------------------------------------------------------------------------------

Brad said 2006-12-19 12:33:

What do you think about letting the client handle all the time zone conversion? I store my times in UTC by making the environment.rb changes above, but my helper looks like this: def format_time(time) # Unfortunately very verbose "&lt;script&gt;document.write(new Date(#{time.to_i}*1000).toLocaleString())&lt;/script&gt;" end Now I'm trying to work out how to modify the datetime_select to convert from local to utc on the client. I figure there are 2 ways: 1) On submit, create a local javascript date and refill all the params using the various getUTC methods. The thing I don't like about this is that the functionality (onsubmit) isn't completely contained inside the datetime_select. 2) Create a local javascript date based on the params and pass along getTimezoneOffset as one of the params. All of these have onchange events associated with them to recreate the offset as needed. Being a newbie, I'm still trying to work out how the offset param will work with the Time multi-parameter assignment. Thoughts?
--------------------------------------------------------------------------------

Thorsten said 2006-12-03 18:15:

Peter, this is exactly what I was looking for: thanks! But I do have a question: where did you get the line ENV['TZ'] = 'UTC' # This makes Time.now return time in UTC from? When I call Time.now I get local time (PST on my box). I checked Ruby's Time.now implementation and it calls gettimeofday. I read the Linux man pages and I do not believe that gettimeofday does anything with time zones. It's a system call, not a C library function. On my windows box I also get local time for Time.now, no matter what ENV['TZ'] is set to. Regards!
--------------------------------------------------------------------------------

robert said 2006-11-08 13:15:

i used Steven's code but am still getting the following: TZInfo::UnknownTimezone (TZInfo::Timezone constructed directly): /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:220:in `identifier' /usr/local/lib/ruby/gems/1.8/gems/tzinfo-0.3.2/lib/tzinfo/timezone.rb:485:in `_dump' Here is my function to display a timestamp (from application_helper.rb): <pre><code> def std_timestamp(datetime) return datetime if !datetime.respond_to?(:strftime) datetime = session['account'].tz.utc_to_local(datetime) if session['account'] datetime.strftime("%b %d, %Y %I:%M %p") end </code></pre> <pre><code> #account.rb: composed_of :tz, :class_name => 'TZInfo::Timezone', :mapping => %w(time_zone identifier) </code></pre> I'm not sure where the problem is. Any ideas? Thanks
--------------------------------------------------------------------------------

Steven said 2006-10-01 20:24:

I used this instead: composed_of :tz, :class_name => 'TZInfo::Timezone', :mapping => %w(time_zone identifier) end seems to work ok. :)
--------------------------------------------------------------------------------

Morten said 2006-09-11 07:38:

Nice write-up Peter. Your mapping won't work it appears: bq. class User < ActiveRecord::Base composed_of :tz, :class_name => 'TZInfo::Timezone', :mapping => %w(time_zone time_zone) end There's no time_zone accessor in TZInfo::Timezone
--------------------------------------------------------------------------------