Now.
You must set both
Time.zone = "UTC" and
ActiveRecord::Base.default_timezone = :utc. Since I’m using Sinatra, not Rails, this stuff goes either in
main (i.e. not inside any class) right after
require 'active_record', or in a
configure block in your app, depending on your preference.
When ActiveRecord creates queries — which are used for both reading and writing, mind you — it will only convert to UTC times that are instances of ActiveSupport’s proprietary
TimeWithZone class. It will not convert regular Ruby
Time objects, even though
Time objects are perfectly aware of their time zones, and AR is perfectly aware that you’d prefer they be written as UTC (due to the
default_timezone setting). This is clearly a bug IMHO, but the Rails core marked the bug as “will not fix”, so w/e. Here’s a monkey patch, courtesy.utc.to_s(:db) : value.to_s(:db) end end end end
When outputting timestamps to a UI — either inside HTML or in a JSON API — you’ll probably want to use
Time#strftime. Beware: on Mac OS X under Ruby 1.8, the
%z (lowercase Z) selector will emit the local time zone, not the zone of the Time object you’ve called
strftime on. The solution is to either use
%Z (capital Z) or just a plain
Z which stands for Zulu Time. The latter is OK if you know you’re using UTC, which, if you’ve followed my advice, you probably do. This is a pretty annoying issue, since it’s much safer to use
%z‘s hour offsets than
%Z‘s three-letter codes, since the three-letter codes can be ambiguous, and in any case require an extra conversion to time offset, so you may as well just emit the offset.
Here are some methods on Time you may want to use that work around this
%z issue:
# Note: do NOT call this file 'time.rb' :-D require 'time' class Time def full_date_and_time strftime('%Y-%m-%d %H:%M:%S %Z') end def iso8601 strftime('%Y-%m-%dT%H:%M:%SZ') # the final "Z" means "Zulu time" which is ok since we're now doing all times in UTC end end
That
iso8601 method comes in really handy when you’re using the excellent timeago jQuery plugin by Ryan McGeary (@rmm5t).
By default PostgreSQL saves timestamps sans time zone, which means that ActiveRecord interprets them as being in the
default_timezone. If you want to be extra clear and save them with time zone, you’ll have to change the Postgres adapter’s type mapping. ActiveRecord doesn’t let you configure this but here’s a monkey patch, courtesy of
Chirag Patel (with a couple of mods):
require 'active_record/connection_adapters/postgresql_adapter' class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter def native_database_types { :primary_key => "serial primary key".freeze, :string => { :name => "character varying", :limit => 255 }, :text => { :name => "text" }, :integer => { :name => "integer" }, :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "timestamp with time zone" }, :timestamp => { :name => "timestamp with time zone" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "bytea" }, :boolean => { :name => "boolean" } } end end
It turned out that I didn’t need this, so I ended up commenting it out. It may be that storing timestamps with time zones will cause a hiccup with some other random DB code, so watch out. If you do use it, and you’ve already got some data, make sure to write a migration that changes the types of all extant
datetime and
timestamp fields, and maybe a migration that shifts the times too.
That’s all I’ve got for right now. I’m sure some more problems will come up on March 14, 2010…
There’s a potential slight issue with the implementation of the `quoted_date` method that you propose above: calling `.utc` on an instance of Time modifies the receiver, which means that if the time you passed-in when generating the query wasn’t UTC, after the query is generated it will be UTC.
Using `Time#getutc` instead of `Time#utc` will instead return a new Time object and thus not have unintended consequences on the receiver:
def quoted_date(value)
value.respond_to?(:getutc) ? value.getutc.to_s(:db) : value.to_s(:db)
end
It’s also worth noting that on edge ActiveRecord (and thus in the upcoming 3.0) this bug is fixed, and regular times get converted to UTC as well. Their implementation is:
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
else
value
end.to_s(:db)
end
and since Time#acts_like?(:time) is true (both in 2.3.x and 3.0.x), this method is safe for backporting as well.
Cheers!
January 23, 2010 at 12:49 am
Vladimir – thanks for the fixes.
January 27, 2010 at 11:10 am
any particular reason you’re not just using extlib’s Time#iso8601 ?
January 28, 2010 at 9:11 pm
mattly – Only reason is that I’d never heard of it before :-)
Looks like some useful stuff in the extlib gem. Thanks.
One thing: after digging into the gem code, it looks like the iso8601 method is actually implemented by the json gem, not the extlib gem.
February 3, 2010 at 7:06 am