Rails Timestamps

After implementing a few features with a recent project, I found that when I tried to display the created_at dates from tables within my database an inconsistency with my local timezone. First, make it work. Then make it better. The created_at column is automatically generated by Rails with the t.timestamps method upon table creation which produces this t.datetime “created_at”column.  By default, these automatically generated created_at and updated_at timestamps are stored by default as UTC (Unix) format which is necessary for server-side languages like Ruby to ensure consistency. UTC time is stored as the number of seconds since the Epoch, January 1, 1970 00:00 UTC. However, this clearly isn’t going to work for our user experience because UTC isn’t a human-friendly format so we need to find a way to translate these stored dates into something that makes a bit more sense.

UTC, GMT, & ISO-8601

Your first thought may be to research a bit about GMT since most everyone is familiar with this term, but you’ll find that UTC and GMT are actually interpreted as the same thing by the Ruby Time class. To Ruby GMT (Greenwich Mean Time) is just deprecated terminology for UTC (Coordinated Universal Time). Next, you come across ISO-8601 which inherently sounds a bit scary at first but it’s really pretty simple to understand. ISO-8601 displays timestamp data in a human-readable string format. ISO-8601 format is emerging as the preferred data type for REST APIs. ISO is standard format time while UTC is the primary time standard used worldwide to regulate clocks and time. One important distinction is that ISO also supports milliseconds. First, let’s check what our database is storing our date as.

Understanding UTC & Timezone Offsets

Let’s play a bit with the Ruby Time class methods in our terminal and see what we find.

#Retrieving local time within Rails console
Time.now #=> 2018-05-21 13:32:54 -0700
Time.now.getlocal #=> 2018-05-21 13:31:35 -0700

#Local time within the console is not utc(gmt) unless explicitly converted
Time.now.utc #=> 2018-05-21 19:15:18 UTC
Time.now.utc? #=> false
Time.now.gmt? #=> false
Time.now.utc.utc? #=> true
Time.now.utc.gmt? #=> true

#However, If we take a look at our SQLite Database Models' created_at column
#we'll see that the date stored here is default UTC
Company.first.created_at #=> Mon, 14 May 2018 21:40:14 UTC +00:00
Company.first.created_at.utc? #=> true

#Woooaah. Wait a second. The database UTC time looks different than the local one
Company.first.created_at.utc #=> 2018-05-14 21:40:14 UTC
#Hey. That looks a bit more like our console UTC output!

#it looks like these database stored dates don't have time zone info as seen here 
Company.first.created_at.to_time #=> 2018-05-14 21:40:14 +0000
#Let's see if we can get their zone using built in methods
#In Ruby we can get the local timezone like this
Time.now.zone #=> "PDT"
Time.now.utc_offset #=> -25200
Company.first.created_at.zone #=> "UTC"
Company.first.created_at.utc_offset #=> 0
#Now, this confirms that that there isn't a timezone saved!

How to Show UTC as Local Timezone + Moment JS

Now I know exactly what my database is storing but I’m not sure how to translate that UTC time with an offset quite yet. I know we’ll have to get the user’s timezone via some mechanism client-side. After a bit of Googling I came across Moment.js with a couple of libraries that have a lot of super useful methods for handling just this. We’re working in Rails so naturally I searched for a gem. Add these to your project and then run bundle install.

#Add to gemfile
gem 'momentjs-rails'
gem 'moment_timezone-rails', '~> 0.5.0'
#Add to application.js
//= require moment

// moment-timezone without timezone data
//= require moment-timezone

// moment-timezone with timezone data from 2010-2020
//= require moment-timezone-with-data-2010-2020

// moment-timezone all timezone data
//= require moment-timezone-with-data

I wasn’t super thrilled with the layout of the Moment.js documentation so I naturally turned to StackOverflow to quickly find the methods I was searching for. Here’s my thought process.

  1. Retrieve the created_at time from the DB in my erb view
  2. Ensure the format of the created_at time is normalized
  3. Get the user’s current time
  4. From the user’s current time determine the timezone (aka UTC offset)
  5. Display the date from my DB with the user’s UTC offset (Timezone)
window.onload = function(){
  <!-- Test if time is valid using Moment. It is. -->
  var test = moment("<%=Company.first.created_at%>").isValid();
  <!-- Check the user's current time -->
  var m = moment();
  <!-- Get the user's timezone aka UTC offset from their current time-->
  var userzone = moment.parseZone(m).utcOffset(); <!-- Eg. -420 is PDT-->
  <!-- Create a new Moment.js Date object in the user's timezone-->
  var newdate = moment("<%=mt.created_at.to_time.iso8601%>").utcOffset(userzone).format('YYYY-MM-DD HH:mm')
  <!-- Display your newly formatted local date-->
  document.getElementById('output').innerHTML = newdate;
};

The utcOffset of -420 mentioned above translates to 420 minutes behind UTC. There you have it! Dates stored as UTC adjusted for the user’s timezone and displayed in the view.

Honorable Methods + Resources

Time.now.iso8601 #=> "2018-05-21T12:15:55-07:00"

Time.now.utc.iso8601 #=> "2018-05-21T19:18:38Z"

Time.now.to_f #=> 1526931984.299364

test = Time.at(628232400) #=> 1989-11-27 21:00:00 -0800

#Change the Time back to a float
test.to_f #=> 628232400.0

# convert to milliseconds since 1970-01-01 00:00:00 UTC.
test.to_f * 1000 #=> 628232400000.0

# Returns the offset in seconds between the timezone of time and UTC.
Time.now.gmt_offset #=> -25200

https://momentjs.com/timezone/docs/

Moment Timezone uses the Internationalization API (Intl.DateTimeFormat().resolvedOptions().timeZone) in supported browsers to determine the user’s time zone.