How to Deal with Timezones the ActiveSupport Way

Posted Almost 8 years ago. Visible to the public.

Writing software aware of different timezones can be a daunting task. Luckily, Ruby on Rails’ ActiveSupport library has some very nice built in features that can prove invaluable when facing time related issues.

ActiveSupport in Ruby on Rails 4.0+ has a built in list of all supported timezones on the TimeZone class:

ActiveSupport::TimeZone.all.map(&:name)

This list of timezones can be used when parsing strings into Time objects and converting an existing Time object from one timezone to another.One interesting detail about this list of timezones is the lack of daylight savings time qualifiers. Keeping the timezones agnostic of daylight savings helps simplify their use. A developer does not need to worry about using one timezone object over another due to the time of the year.

Time.use_zone

The ActiveSupport library adds some functionality to built in Ruby classes. One of those additions is the ability to set and retrieve the zone attribute on the Time class. After a zone is set, it can be used when parsing strings into Time objects:

Time.zone = 'Pacific Time (US & Canada)'
Time.zone.parse('2016-04-01 10:00:00')
#=> Fri, 01 Apr 2016 10:00:00 PDT -07:00

The zone attribute persists for the rest of the Ruby runtime, potentially causing unexpected behaviour at a later time. Luckily, ActiveSupport solves this problem with the use_zone method. This method accepts the same set of strings as its single argument and expects a block. Within the passed in block is the only place Time.zone is affected, eliminating the possibility of a zone sticking around longer than intended:

Time.zone.name
#=> "UTC"

Time.use_zone('Pacific Time (US & Canada)') do
  Time.zone.name
  # => "Pacific Time (US & Canada)"
  Time.zone.parse('2016-04-01 10:00:00')
  #=> Fri, 01 Apr 2016 10:00:00 PDT -07:00
end

Time.zone.name
#=> "UTC"

This temporary zone setting can be very helpful if a set of users are processed, each with their own respective time zone.

TimeWithZone.in_time_zone

When dealing with Time, Date and DateTime objects, converting each from one timezone to another can be tedious. The in_time_zone method removes some complexity from those operations. Used alone, the in_time_zone can transform an existing object into an instance of TimeWithZone:

now = Time.now
# => 2016-04-04 03:55:24 +0000
now.in_time_zone('Hawaii')
# => Sun, 03 Apr 2016 17:55:24 HST -10:00

In any sane system, timestamps in a database are always stored in UTC, aka Zulu, time. When exposing these timestamps to users, the in_time_zone method can quickly switch the timestamp to a user’s local time. To see a more creative use of in_time_zone, we can assume that an application must solve a scheduling problem. An example application wants to send its users an email at the exact same time relative to a user’s timezone. Regardless of where a user lives, they should receive an email at 10:30 AM local time.
A naive approach might not yield the intended result:

user.time_zone
# => Hawaii
send_time = Time.new(2016, 04, 01, 10, 30)
# => 2016-04-01 10:30:00 +0000
send_time.in_time_zone(user.time_zone)
# => Fri, 01 Apr 2016 00:30:00 HST -10:00

Whoops, that is not right at all. This code initialized a Time object for the “correct” send time but then in_time_zone not only moved the timezone of that object, it also modified the actual time. This would result in all users getting an email at the same exact moment but at an inconvenient time relative to where they live. Potential solutions to this problem might include modifying the timezone part of an outputted string (the "+0000" at the end) and re-parsing it, but that solution can be prone to error and hard to understand. Since the in_time_zone method works on the Date class, the simplest approach would be to initialize a new DateTime object in a user’s timezone and add 10.5 hours to it:

user.time_zone
# => Hawaii
beginning_of_day = Date.today.in_time_zone(user.time_zone)
# => Sun, 03 Apr 2016 00:00:00 HST -10:00
beginning_of_day += (10.5).hours
# => Sun, 03 Apr 2016 10:30:00 HST -10:00
sender = ImportantEmailSender.new(user)
sender.send_at!(beginning_of_day)

Great! An email will be sent at 10:30 AM Hawaii time and we successfully avoided any time string munging or other strange object casting. A user in London can also receive an email at exactly 10:30 AM their local time and this code need not grow in complexity.

Alexander M
Last edit
Over 7 years ago
Alexander M
Posted by Alexander M to Ruby and RoR knowledge base (2016-05-02 15:05)