Time (Zones) Explained
Ruby provides two classes to manage time: Time and DateTime. Since Ruby 1.9.3 there are fewer differences between the two libraries, with Time covering concepts of leapseconds and daylight saving time. For the rest of this article, Time will be used in all the examples.
TZInfo is another time zone library, which provides daylight-saving-aware transformations between times in different time zones. It is available as a gem and includes data on 582 different time zones.
Time zones in Rails
Rails’ ActiveSupport::TimeZone is a wrapper around TZInfo that limits the set of zones provided by TZInfo to a meaningful subset of 146 zones. It displays zones with a friendlier name (e.g. “Eastern Time (US & Canada)” instead of “America/New_York”). And together with ActiveSupport::TimeWithZone, Rails provides the same API as Ruby Time instances, so that Time
and ActiveSupport::TimeWithZone
instances are interchangeable, and you should never need to create a TimeWithZone
instance directly via new
.
In Rails, to see all the available time zones, run:
$ rake time:zones:all
* UTC -11:00 *
American Samoa
International Date Line West
Midway Island
Samoa
* UTC -10:00 *
Hawaii
* UTC -09:00 *
Alaska
...
To check for the current set time zone, in console:
> Time.zone
=> #<ActiveSupport::TimeZone:0x007fbf46947b38
@current_period=#<TZInfo::TimezonePeriod: nil,nil,#<TZInfo::TimezoneOffset: 0,0,UTC<span class='break-line margin-m'></span>,
@name="UTC",
@tzinfo=#<TZInfo::TimezoneProxy: Etc/UTC>,
@utc_offset=nil>
Still in console, to temporarily set a different time zone:
# in console
> Time.zone = "Perth"
We can also permanently change our application time zone by setting a config option in config/application.rb
:
# config/application.rb
config.time_zone = "Perth"
The default time zone in Rails is UTC. As tempting as it may seem, it is best to leave the application-wide time zone as UTC and instead allow each individual user to set their own time zone. Check a case study in multiple time zones to see an example why.
With user time zones
Let’s decide that each of our users have their time zone defined. This can be done by adding :time_zone
attribute to the User
model. Our migration might look like so:
create_table :users do |t|
t.string :time_zone, default: "UTC"
...
end
We want to store the time zone as a string because most of Rails’ time-zone-related methods use strings. Above all, avoid storing time zones as :enums
.
We can allow a user to set their own desired time zone when editing their profile. SimpleForm supports :time_zone
and provides a form helper so the user can select a time zone option from a select menu.
<%= f.input :time_zone %>
We can then use an around_action
in ApplicationController
to apply our user’s preferred time zone.
# app/controllers/application_controller.rb
around_action :set_time_zone, if: :current_user
private
def set_time_zone(&block)
Time.use_zone(current_user.time_zone, &block)
end
We pass the current user’s time zone to use_zone
method on the Time
class (a method which was added by ActiveSupport
). This method expects a block to be passed to it and sets the time zone for the duration of that block, so that when the request completes, the original time zone is set back.
And lastly, to display times in a specific user’s time zone, we can use Time
‘s in_time_zone
method:
<%= time.in_time_zone(current_user.time_zone) %>
Working with APIs
When working with APIs, it is best to use the ISO8601
standard, which represents date/time information as a string. ISO8601
’s advantages are that the string is unambiguous, human readable, widely supported, and sortable. The string looks like:
> timestamp = Time.now.utc.iso8601
=> "2015-07-04T21:53:23Z"
The Z at the end of the string indicates that this time is in UTC, not a local time zone. To convert the string back to a Time
instance, we can say:
> Time.iso8601(timestamp)
=> 2015-07-04 21:53:23 UTC
Three time zones
In a Rails app, we have three different time zones:
- system time
- application time
- database time
Say we set our time zone to be Fiji. Let’s see what happens:
# This is the time on my machine, also commonly described as "system time"
> Time.now
=> 2015-07-04 17:53:23 -0400
# Let's set the time zone to be Fiji
> Time.zone = "Fiji"
=> "Fiji"
# But we still get my system time
> Time.now
=> 2015-07-04 17:53:37 -0400
# However, if we use `zone` first, we finally get the current time in Fiji
> Time.zone.now
=> Sun, 05 Jul 2015 09:53:42 FJT +12:00
# We can also use `current` to get the same
> Time.current
=> Sun, 05 Jul 2015 09:54:17 FJT +12:00
# Or even translate the system time to application time with `in_time_zone`
> Time.now.in_time_zone
=> Sun, 05 Jul 2015 09:56:57 FJT +12:00
# Let's do the same with Date (we are still in Fiji time, remember?)
# This again is the date on my machine, system date
> Date.today
=> Sat, 04 Jul 2015
# But going through `zone` again, and we are back to application time
> Time.zone.today
=> Sun, 05 Jul 2015
# And gives us the correct tomorrow according to our application's time zone
> Time.zone.tomorrow
=> Mon, 06 Jul 2015
# Going through Rails' helpers, we get the correct tomorrow as well
> 1.day.from_now
=> Mon, 06 Jul 2015 10:00:56 FJT +12:00
Time zone related querying
Rails saves timestamps to the database in UTC time zone. We should always use Time.current
for any database queries, so that Rails will translate and compare the correct times.
Post.where("published_at > ?", Time.current)
# SELECT "posts".* FROM "posts" WHERE (published_at > '2015-07-04 17:45:01.452465')
A summary of do’s and don'ts with time zones
DON’T USE
* Time.now
* Date.today
* Date.today.to_time
* Time.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")
DO USE
* Time.current
* 2.hours.ago
* Time.zone.today
* Date.current
* 1.day.from_now
* Time.zone.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z").in_time_zone
In summary
- Always work with UTC.
- Use
Time.current
orTime.zone.today