How to Deal with Timezones the Active Support Way03 Apr 2016
“Well the code is bad because we had to…” is a phrase one might hear when discussing timezone offsets or daylight savings time considerations.
Unless a developer is fortunate enough to work at a company whose userbase is entirely located in the UTC timezone, 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
ActiveSupport::TimeZone.all.map(&:name) #=> ["American Samoa", "International Date Line West", "Midway Island", "Hawaii", "Alaska", "Pacific Time (US & Canada)", "Tijuana", "Arizona", "Chihuahua", "Mazatlan", "Mountain Time (US & Canada)", "Central America", "Central Time (US & Canada)", "Guadalajara", ...]
This list of timezones can be used when parsing strings into
Time objects and converting an existing
Time object from one timezone to another. For working with timezones in the United States, a handy
us_zones method is also available on the same class.
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.
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.zone = 'Pacific Time (US & Canada)' Time.zone.parse('2016-04-01 10:00:00') #=> Fri, 01 Apr 2016 10:00:00 PDT -07:00
This code takes a timestamp string without a timezone specified and uses the value of
Time.zone to correctly represent the time in the Pacific timezone.
However, an immediate red flag is present in this code. The
zone attribute persists for the rest of the Ruby runtime, potentially causing unexpected behaviour at a later time.
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"
zone setting can be very helpful if a set of users are processed, each with their own respective timezone.
When dealing with
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
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.
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.
Whether in code or everyday life, it seems that struggling with time is something we as humans are simply destined to do. Some good news is that
ActiveSupport may not grant you any more wall clock time, but could save you some when wrangling different pieces of scheduling code together.
As usual, the API of the
ActiveSupport::TimeZone class is much larger than just the two methods illustrated here. For more information, the offical ActiveSupport documentation is a great place to read further.