---
title: It's About Time (Zones)
teaser: An overview of time zones in Rails.
tags: web,rails,ruby
author: Elle Meredith
published_on: 2015-07-23
---

This is the first article of a 2-part series.
The second article is about [a case study in multiple time zones].

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:

```bash
$ 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:

```bash
> Time.zone
=> #<ActiveSupport::TimeZone:0x007fbf46947b38
 @current_period=#<TZInfo::TimezonePeriod: nil,nil,#<TZInfo::TimezoneOffset: 0,0,UTC>>>,
 @name="UTC",
 @tzinfo=#<TZInfo::TimezoneProxy: Etc/UTC>,
 @utc_offset=nil>
```

Still in console, to temporarily set a different time zone:

```bash
# in console
> Time.zone = "Perth"
```

We can also permanently change our application time zone by
setting a config option in `config/application.rb`:

```ruby
# 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:

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

```erb
<%= f.input :time_zone %>
```

We can then use an `around_action` in `ApplicationController` to apply
our user's preferred time zone.

```ruby
# 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.

If you are using Rails 3.2.13 or lower, instead of using `around_action` you
will need to use `around_filter`.

And lastly, to display times in a specific user's time zone,
we can use `Time`'s `in_time_zone` method:

```erb
<%= 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:

```bash
> 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:

```bash
> 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, and
* database time.

Say we set our time zone to be Fiji. Let's see what happens:

```ruby
# 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.

```ruby
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

```ruby
* 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

```ruby
* 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
```

## Testing time zones

Rails 4.1 added a [`ActiveSupport::Testing::TimeHelpers`] module,
with three useful methods: `travel`, `travel_back`, and `travel_to`.
We can use these methods to freeze time within blocks in our tests.

If using an older version of Rails, there are three gems
to help us set and freeze the time in our tests:
[Timecop], [Delorean], and [Zonebie].
As much as I love the reference to go `back_to_the_present`,
I usually use Timecop.

```ruby
new_time = Time.zone.parse("2014-10-19 1:00:00")

# With Timecop, we can freeze the time,
Timecop.freeze new_time

# or
Timecop.travel new_time

# but will need to clean up after the spec, and return to current time
Timecop.return

# Alternatively we can use blocks, which only freeze the time inside our block
Time.use_zone("Sydney") do
end

# With Delorean, the syntax is a touch different
Delorean.time_travel_to("1 month ago") do
end
Delorean.back_to_the_present

# And Zonebie sets the time zone to a random one each time we run our tests
Zonebie.set_random_timezone
```

If you want to learn how to persist future datetimes: check out
[How to save datetimes for future events] article by Lau Taarnskov.

## In summary

* Always work with UTC.
* Use `Time.current` or `Time.zone.today`.
* Use testing helper methods of your choice to freeze the time in your tests,
  preferably by using a block.

Also remember to check out the second part of this article at
[a case study in multiple time zones].

[`Time`]: http://ruby-doc.org/core-2.2.2/Time.html
[`DateTime`]: http://ruby-doc.org/stdlib-2.0.0/libdoc/date/rdoc/DateTime.html
[`TZInfo`]: https://tzinfo.github.io/
[`ActiveSupport::TimeZone`]: http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html
[`ActiveSupport::TimeWithZone`]: http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
[`SimpleForm`]: https://github.com/plataformatec/simple_form#priority
[Timecop]: https://github.com/travisjeffery/timecop
[Delorean]: https://github.com/myusuf3/delorean
[Zonebie]: https://github.com/alindeman/zonebie
[`ActiveSupport::Testing::TimeHelpers`]: http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html
[How to save datetimes for future events]: http://www.creativedeletion.com/2015/03/19/persisting_future_datetimes.html
[a case study in multiple time zones]: https://thoughtbot.com/blog/a-case-study-in-multiple-time-zones
