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 2 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 2 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 2 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 2 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 2 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 2 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 2 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 2 years ago:

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

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

Damu said about 1 year 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 about 1 year 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 about 1 year 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 about 1 year 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 about 1 year ago:

opoi

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

madhukarpadma@yahoo.com said about 1 year 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 about 1 year 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 about 1 year 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 about 1 year 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 about 1 year 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 about 1 year 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 3 months ago:

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

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

tdq said 3 months 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 3 months 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 3 months 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 3 months 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 3 months ago:

very good post thank

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

buy%20wow%20gold said 2 months 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 2 months 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 about 1 month 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 about 1 month ago:

Thanks for sharing the solution:)

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

blog voyance said 23 days ago:

Very good work Peter, thank for the solution !

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

Mike - Music Notation Software said 16 days 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 2 days ago:

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

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

Leave a Comment




 


Use plain text for your comments. HTML will be quoted. URLs will be turned into hyperlinks. Linebreaks will be preserved.