Rails - Time Zones & Daylight Savings Time
Today, while putting the final touches on an appointment scheduling engine, I noticed some odd behaviour in my tests.
I discovered that the failing tests were related time zones, and more specifically, to crossing daylight savings time boundaries. This brief article will demonstrate how to easily show time (in the relevant time zone) in your rails app, as well as correctly traversing date time boundaries.
Table of contents:
- Always store time in UTC
- How to convert UTC time into a users' local time zone
- How to traverse daylight savings time boundaries
- How to convert local time back to UTC
1. Always store time in UTC
Always store time in UTC (Coordinated Universal Time) in your database (often the default setting).
Why? Because it will ensure that you can easily compare times in your database across time zones. Is 5:00 pm EST before 3:00 pm PST? This question would be much easier to answer if both times were in UTC and were same same.
If you wanted to compare relative wealth, you wouldn't compare two peoples' net worth in USD vs Yuan. You would pick one currency to compare wealth in.
Keep your times in UTC in your database, and thank me later.
2. How to convert UTC time into a users' local time zone
Lucky for us, rails makes converting time from UTC to another time zone very easy.
It is this easy:
$ irb
:001 > require 'active_support/all'
:002 > time_zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
:003 > Time.now.in_time_zone(time_zone).advance(days: 1)
=> Tue, 19 Nov 2019 23:52:47 EST -05:00
:004 > Time.now.in_time_zone(time_zone)
=> Thu, 14 Nov 2019 23:52:48 EST -05:00
3. How to traverse daylight savings time boundaries
To avoid surprises when traversing daylight savings time boundaries, use the advance method:
$ rails console
:001 > time_zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
:002 > Time.now.in_time_zone(time_zone).advance(days: 115)
=> Sun, 08 Mar 2020 23:52:47 EDT -04:00
:003 > Time.now.in_time_zone(time_zone)
=> Thu, 14 Nov 2019 23:52:48 EST -05:00
If instead of advance you try one of the following when traversing daylight savings time boundaries...
{{X}}.days.ago.in_time_zone(...)
{{X}}.weeks.from_now.in_time_zone(...)
(Time.now + {{X}}.days).in_time_zone(...)
... You will find your output is inexplicably off by 1 hour.
$ rails console
:001 > time_zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
:002 > Time.now.in_time_zone(time_zone)
=> Fri, 15 Nov 2019 00:00:01 EST -05:00
:003 > 115.days.from_now.in_time_zone(time_zone)
=> Mon, 09 Mar 2020 01:00:02 EDT -04:00
Always use advance when traversing daylight savings time to save yourself the headache of being off by 1 hour (as per above).
4. How to convert local time back to UTC
Finally, let's say that you want to convert local time back to UTC and into a DB ready string. It is this easy:
Time.now
.in_time_zone(ActiveSupport::TimeZone["Eastern Time (US & Canada)"])
.advance(days: 1)
.utc.to_s(:db)