Assign Time/time string in UTC to ActiveRecord date attribute
TL;DR: gem 'date_timezone' in Gemfile and include DateTimezone in ActiveRecord models if you are on the east side of the prime meridian.
ActiveRecord is great. It automatically converts data according to the type of database column. That's why we can throw request params, whose values are often strings, into mass assignment methods like create and update without manual conversion. The conversion works perfectly in most of the cases except for date attribute.
To date attribute, we can assign Date and date string like '2015-03-02' without any problem. However, Time and time string with different time zone, usualy UTC, don't work well here. Their own time zones are not taken into account when converted to Date.
class Application < Rails::Application
config.time_zone = 'Tokyo'
end
class Person < ActiveRecord::Base
# birth_date :date
end
expect(Person.new(birth_date: '2015-03-02').birth_date).to eq(Date.new(2015, 3, 2))
expect(Person.new(birth_date: Date.new(2015, 3, 2)).birth_date).to eq(Date.new(2015, 3, 2))
expect(Person.new(birth_date: Time.zone.local(2015, 3, 2)).birth_date).to eq(Date.new(2015, 3, 2))
# But...
expect(Person.new(birth_date: Time.utc(2015, 3, 1, 15)).birth_date).to eq(Date.new(2015, 3, 1))
expect(Person.new(birth_date: '2015-03-01T15:00:00.000Z').birth_date).to eq(Date.new(2015, 3, 1))
There may be several cases that we have to assign Time or time string in different time zone to date attribute. My own case was a Single Page Application built with AngularJS that sends JavaScript's Date object to Rails API. JavaScript's JSON.parse() serializes Date into a string of ISO 8601 format in UTC time zone. This is problematic to the people on the east side of the prime meridian because they get different date when they express their beginning of date in UTC.
JSON.stringify({ date: new Date(2015, 3 - 1, 2) });
// '{"date":"2015-03-01T15:00:00.000Z"}'
I could have controlled front-end code to always send date string like '2015-03-02' or converted the ISO 8601 string with Time.zone.parse in Rails controllers. But those approaches seemed error prone. I wanted to take care of it at the bottom, ActiveRecord model. I created a concern to override date-column mutators like the following. It converts Time and time string to TimeWithZone with the application's time zone.
def birth_date=(value)
self[:birth_date] = case value
when String then Time.zone.parse(value)
when Time then value.in_time_zone
else value
end
end
The concern was extracted as a gem, date_timezone.
gem 'date_timezone'
class Person < ActiveRecord::Base
include DateTimezone
# birth_date :date
end
If you are creating Rails application on the east side of the prime meridian and in trouble with the same issue as mine, please try it and share what you think.