|
Peter Marklund's Home |
Rails Recipe: A Timezone Aware Datetime Picker
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
Steven said about 1 year ago:
I used this instead:
composed_of :tz, :class_name => ‘TZInfo::Timezone’,
:mapping => %w(time_zone identifier)
end
seems to work ok. :)
robert said about 1 year ago:
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 about 1 year 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 about 1 year 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)
# 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 about 1 year ago:
def format_time(time)
# Unfortunately very verbose
"<script>document.write(new Date(#{time.to_i}*1000).toLocaleString())</script>"
end
Brandon said about 1 year 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 11 months ago:
I am getting the same error.
TZInfo::Timezone constructed directly
please explain us the problem PETER!!!
Damu said 10 months 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 8 months 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 6 months 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)
rssnewsdigest said about 1 month ago:
Try rssnewsdigest.com, a new comprehensive news aggregator. With rssnewsdigest, you don ’t really have to go anywhere else.
http://rssnewsdigest.com
stewie.halo@yahoo.com said 28 days 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 27 days ago:
opoi
madhukarpadma@yahoo.com said 25 days 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 22 days 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.





Morten said about 1 year ago: