Peter Marklund

Peter Marklund's Home

Wed August 16, 2006
Programming

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...

Comments

Morten said over 7 years ago:

Nice write-up Peter. Your mapping won’t work it appears:

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

--------------------------------------------------------------------------------

Steven said over 7 years ago:

I used this instead:

composed_of :tz, :class_name => ‘TZInfo::Timezone’,
:mapping => %w(time_zone identifier)
end

seems to work ok. :)

--------------------------------------------------------------------------------

robert said over 7 years ago:

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


  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)

I’m not sure where the problem is. Any ideas?

Thanks

--------------------------------------------------------------------------------

Thorsten said over 7 years ago:

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!

--------------------------------------------------------------------------------

Brad said over 7 years ago:

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)

  1. Unfortunately very verbose
    “<script>document.write(new Date(#{time.to_i}*1000).toLocaleString())</script>”
    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?

--------------------------------------------------------------------------------

Brad said over 7 years ago:

My apologies, my helper should look like this:


def format_time(time)
  # Unfortunately very verbose
  "<script>document.write(new Date(#{time.to_i}*1000).toLocaleString())</script>" 
end
--------------------------------------------------------------------------------

Brandon said over 7 years ago:

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?

--------------------------------------------------------------------------------

lofd23@yahoo.de said over 6 years ago:

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

--------------------------------------------------------------------------------

Damu said over 6 years ago:

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.

--------------------------------------------------------------------------------

mjmac said over 6 years ago:

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!

--------------------------------------------------------------------------------

Jeff said over 6 years ago:

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)

--------------------------------------------------------------------------------

stewie.halo@yahoo.com said over 6 years ago:


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

--------------------------------------------------------------------------------

oipi said over 6 years ago:

opoi

--------------------------------------------------------------------------------

madhukarpadma@yahoo.com said over 6 years ago:

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

--------------------------------------------------------------------------------

mmo said over 6 years ago:

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 over 5 years ago:

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

--------------------------------------------------------------------------------

gdhy@yahoo.com said over 5 years ago:

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

--------------------------------------------------------------------------------

fashion said over 5 years ago:

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!!

--------------------------------------------------------------------------------

Justin Meyer said over 5 years ago:

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.

--------------------------------------------------------------------------------

betclic said over 5 years ago:

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

--------------------------------------------------------------------------------

tdq said over 5 years ago:

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

--------------------------------------------------------------------------------

Anonymous said over 5 years ago:

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,

--------------------------------------------------------------------------------

Anonymous said over 5 years ago:

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>

--------------------------------------------------------------------------------

PHR said over 5 years ago:

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

--------------------------------------------------------------------------------

http://www.telavenir.com said over 5 years ago:

very good post thank

--------------------------------------------------------------------------------

buy%20wow%20gold said over 5 years ago:

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?

--------------------------------------------------------------------------------

pornhub said over 5 years ago:

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

--------------------------------------------------------------------------------

Bwin said over 4 years ago:

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....

--------------------------------------------------------------------------------

photographe liège said over 4 years ago:

Thanks for sharing the solution:)

--------------------------------------------------------------------------------

blog voyance said over 4 years ago:

Very good work Peter, thank for the solution !

--------------------------------------------------------------------------------

Mike - Music Notation Software said over 4 years ago:

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

--------------------------------------------------------------------------------

accessoires pour chiens said over 4 years ago:

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

--------------------------------------------------------------------------------

voyant said over 4 years ago:

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

--------------------------------------------------------------------------------

radiateur inertie said over 4 years ago:

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

--------------------------------------------------------------------------------

Karina Jett said over 4 years ago:

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?

--------------------------------------------------------------------------------

ridons said over 4 years ago:

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.

--------------------------------------------------------------------------------

free online advertising said over 4 years ago:

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

--------------------------------------------------------------------------------

4 multimaster said over 4 years ago:

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

--------------------------------------------------------------------------------

rockwell sonicrafter said over 4 years ago:

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

--------------------------------------------------------------------------------

sofas sectional said over 4 years ago:

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

--------------------------------------------------------------------------------

Wrist Weights said over 4 years ago:

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

--------------------------------------------------------------------------------

baby diapers said over 4 years ago:

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

--------------------------------------------------------------------------------

fein multimaster top said over 4 years ago:

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

--------------------------------------------------------------------------------

usa shop online said over 4 years ago:

Good job! THANKS! You guys do a great blog

--------------------------------------------------------------------------------

Strathwood Falkner Dining Table said over 4 years ago:

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

--------------------------------------------------------------------------------

maria smith said over 3 years ago:

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

--------------------------------------------------------------------------------

Nye Mobiltelefoner said over 3 years ago:

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>

--------------------------------------------------------------------------------

free promote products said over 3 years ago:

Very good work Peter, thank for the solution !

--------------------------------------------------------------------------------

Sell Gold said over 3 years ago:

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

--------------------------------------------------------------------------------

David Bieber said over 3 years ago:

I used this instead:

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

--------------------------------------------------------------------------------

David Bieber said over 3 years ago:

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/

--------------------------------------------------------------------------------

2 player games said over 3 years ago:

all games are to play for 2 or more players

--------------------------------------------------------------------------------

surgery games said over 3 years ago:

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

--------------------------------------------------------------------------------

Nokia 2690 said over 3 years ago:

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

--------------------------------------------------------------------------------

Emre Çolak said over 3 years ago:

good job. thanx.

--------------------------------------------------------------------------------

meningitis symptoms said over 3 years ago:

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)

--------------------------------------------------------------------------------

mike said over 3 years ago:

Great Posts! keep up the good work peter.

--------------------------------------------------------------------------------

grammar checking said over 3 years ago:

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.

--------------------------------------------------------------------------------

utangac said over 3 years ago:

eline salık

--------------------------------------------------------------------------------