← All Articles

Understanding Time, DateTime, and timezones in Rails

I am sitting here in Singapore, where it is currently 1pm on 6th March (my laptop's system time is also set to this time).

I'm working for a client with their Rails app setup in Palo Alto, where it is currently 9pm on 5th March.

Their application is processing some data from London, where it is currently 5am on 6th March. 

So... how do we handle time (and time-like) variables in this app? This is a long article, so you can also skip straight to the summary.

3 timezones - system time, application time, selected time

For this article, we are concerned with 3 different "times":

LocationLabelCurrent Time
SingaporeMy time / System time1pm, 6th March
LondonSelected time5am, 6th March
Palo AltoApplication time9pm, 5th March

 

Some notes to remember:

  1. System time is set in your OS. You can set this to be different to your physical location, but for today let's assume system time is the same as the time at my location.

  2. Application time is the time in which your Rails application is running. This can be set either from a line in your application.rb like config.time_zone = "Pacific Time (US & Canada)" or manually setting via Time.zone = "Pacific Time (US & Canada)".

  3. Selected time is just the particular data point we are considering at the moment. 

In this example, London time is in the GMT timezone which makes it easier since it is also UTC (+00:00 offset). However this is not always the case.

Differences between GMT and UTC

Although both of these are used interchangeably in casual language, they are technically different.

  • GMT is a timezone used in several locations.
  • A location can switch into / out of using GMT at different times in the year. For example, London is in BST (British Summer Time) with daylight savings for some part of the year, and GMT for the rest.
  • UTC is not a timezone, it is a universal standard for keeping time based on atomic time. 
  • However, for most purposes we can treat these as interchangeable since the GMT timezone is on a +00:00 offset to UTC. ie there is no hourly offset.
  • More background info on GMT vs UTC and different timezones.

Manipulating Time and DateTime objects in Rails

Here's what happens:

CommandReturnsNotes
Time.nowTime
2015-03-06 13:00:00 +0800
System timezone (SG)
Time.now.in_time_zone
Time.current
ActiveSupport::TimeWithZone
Thu, 05 Mar 2015 21:00:00 PST -08:00
Application timezone (CA)
Time.now.in_time_zone(nil)Time
2015-03-06 13:00:00 +0800
System timezone
Careful! A nil timezone defaults to system time and returns a Time object. Time.now.in_time_zone and Time.now.in_time_zone(nil) are different!
Time.now.in_time_zone("London")ActiveSupport::TimeWithZone
Fri, 06 Mar 2015 05:00:00 GMT +00:00
Selected timezone (London)
Time.now.in_time_zone("UTC")ActiveSupport::TimeWithZone
Fri, 06 Mar 2015 05:00:00 UTC +00:00
Selected timezone (UTC)
Time.now.utcTime
2015-03-06 05:00:00 UTC
Selected timezone (UTC)
Careful!Time.now.in_time_zone("UTC") and Time.now.utc are equivalent but different classes.
DateTime.nowDateTime
Fri, 06 Mar 2015 13:00:00 +0800
System timezone (SG)
DateTime.now.in_time_zoneActiveSupport::TimeWithZone
Thu, 05 Mar 2015 21:00:00 PST -08:00
Application timezone (CA)
DateTime.now.in_time_zone("UTC")ActiveSupport::TimeWithZone
Fri, 06 Mar 2015 05:00:00 UTC +00:00
Selected timezone (UTC)
DateTime.now.utcDateTime
Fri, 06 Mar 2015 05:00:00 +0000
Selected timezone (UTC)
Careful!DateTime.now.in_time_zone("UTC") and DateTime.now.utc are equivalent but different classes.

 

Converting times to strings

You can format any Time, DateTime, or ActiveSupport::TimeWithZone object into a string using a list of format characters.

VariableCommandReturns
Time

t = Time.now

t.strftime("%Y-%m-%d %H:%M")

"2015-03-06 13:00"
(System time, 1pm in SG)
ActiveSupport::TimeWithZone

t = Time.now.in_time_zone("London")

t.strftime("%Y-%m-%d %H:%M")

"2015-03-06 05:00" 
(Selected time, 5am in London))
DateTime

t = DateTime.now

t.strftime("%Y-%m-%d %H:%M")

"2015-03-06 13:00"
(System time, 1pm in SG)

 

Parsing times from strings (no timezone info)

Assuming these variables:

  • t = "2015-11-06T13:00:00" Note no timezone...
  • format = "%Y-%m-%dT%H:%M:%S"
CommandReturnsNotes
Time.strptime(t, format)
Time.parse(sample_time)
Time
2015-03-06 13:00:00 +0800
Has system timezone info, but not converted.
Time.zone.parse(t)ActiveSupport::TimeWithZone
Fri, 06 Mar 2015 13:00:00 PST -08:00
Has application timezone info, but not converted.
DateTime.strptime(t, format)
DateTime.parse(sample_time)
DateTime
Fri, 06 Mar 2015 13:00:00 +0000
Careful!
The numbers show system time (ie 1pm in SG), but this DateTime object is actually in UTC time.
t.in_time_zoneActiveSupport::TimeWithZone
Fri, 06 Mar 2015 13:00:00 PST -08:00
Careful!
The numbers show system time (ie 1pm in SG), but this ActiveSupport::TimeWithZone object is actually in application time (ie CA).
t.in_time_zone(nil)Time
Fri, 06 Mar 2015 13:00:00 +0800
Careful!
The numbers show system time (ie 1pm in SG), but this Time object is actually in system time (ie SG).
t.in_time_zone("London")ActiveSupport::TimeWithZone
Fri, 06 Mar 2015 13:00:00 GMT +00:00
Careful!
The numbers show system time (ie 1pm in SG), but this ActiveSupport::TimeWithZone object is actually in selected time (ie London).

 

Parsing times from strings (with timezone info)

Assuming these variables:

  • t = "2015-11-06T13:00:00 +00:00" (ie London)
  • format = "%Y-%m-%dT%H:%M:%S"
CommandReturnsNotes
Time.strptime(t, format)
Time.parse(sample_time)
Time
2015-11-06 13:00:00 +0800
Has system timezone info, but not converted.
Careful!
If you don’t include timezone format in your format variable, the timezone does not get parsed.
Time.zone.parse(t)ActiveSupport::TimeWithZone
Fri, 06 Mar 2015 05:00:00 PST -08:00
Has application timezone info, and also converted.
DateTime.strptime(t, format)DateTime
Fri, 06 Mar 2015 13:00:00 +0000
Has selected timezone info, but not converted.
Careful!
If you don’t include timezone format in your format variable, the timezone does not get parsed.
DateTime.parse(t)DateTime
Fri, 06 Mar 2015 13:00:00 +0000
Has selected timezone info, and also converted.
t.in_time_zoneActiveSupport::TimeWithZone
Fri, 06 Mar 2015 05:00:00 PST -08:00
Has application timezone info, and also converted.

 

Other time-related helpers

CommandReturnsNotes
2.hours.agoActiveSupport::TimeWithZone
Thu, 05 Mar 2015 19:00:00 PST
Application timezone
Date.currentDate
Thu, 05 Mar 2015
Application timezone
Date.todayDate
Fri, 06 Mar 2015
System timezone

 

There's also the use_zone method:


Time.now  # => System time, e.g. 2015-03-06 13:00:00 +0800
Time.zone.now # => Application time, e.g. Thu, 05 Mar 2015 21:00:00 PST -08:00
Time.use_zone("London") do
  p Time.now  # => still System time, e.g. 2015-03-06 13:00:00 +0800
  p Time.zone.now # => London time, e.g. Fri, 06 Mar 2015 05:00:00 GMT +00:00
end

Summary

After this long list of different commands, here's what I keep in mind:

  1. Time.now or Time.now.utc returns Time objects in system time and UTC respectively, e.g. 2015-11-06 05:28:28 UTC
  2. DateTime.now or DateTime.now.utc returns DateTime objects in system time and UTC respectively, e.g. 2015-11-06 05:28:28 UTC
  3. Any use of in_time_zone or zone returns ActiveSupport::TimeWithZone objects in application time, and will be the same whether it is called by a Time or DateTime object. e.g. Fri, 06 Nov 2015 05:25:03 GMT +00:00
  4. Time.current is equivalent to Time.now.in_time_zone in application time.
  5. in_time_zone and current use application timezone. Date.today and Time.now uses system timezone.
  6. Time.now.in_time_zone and Time.now.in_time_zone(nil) are different! A nil timezone defaults to system time and returns a Time object. Not passing a parameter gives application time and returns ActiveSupport::TimeWithZone object.
  7. Get list of timezones from ActiveSupport::TimeZone.zones_map
Made with JoyBird