Migrate local timezone dates into new Rails application - ruby-on-rails

I am migrating a Perl-based web application to Rails. The old application stores dates in a MySql database in local (Pacific) time. For example, there is a created field that might have the value 06/06/2008 14:00:00 representing 2:00 PM June 6, 2008 (PDT) whereas 02/02/2002 06:30:00 represents 6:00 AM February 2, 2002 (PST).
I have written a rake task to take all the old data and import it into the new database. The date in the new database still looks like 06/06/2008 14:00:00 but, of course, my Rails application interprets this as UTC.
The migrating task looks like this:
# Migrating old events in Perl application to new events in Rails
oldevents = OldEvent.all
oldevents.each do |oldevent|
newevent = Event.convert_old_event_to_newevent(oldevent)
newevent.save!
end
The interesting code is in the static method Event.convert_old_event_to_newevent:
def Event.convert_old_event_to_newevent(oldevent)
...
# If the "created" field in the old db contains '06/06/2008 14:00:00' (meaning
# 2:00 PM PDT (since June is during daylight savings time) then the
# "created_at" field in the new database will contain the same string which
# Rails interprets as 2:00 PM GMT.
newevent.created_at = oldevent.created
...
return newevent
end
So, in the migration process, before storing the dates in the new database I need to read the date/times from the old database, convert them to UTC, and then store that in the new database.
How can that be done?

>> Time.local(*'06/06/2008 14:00:00'.split(/[:\/ ]/).values_at(2,0,1,3..5)).utc
=> Fri Jun 06 21:00:00 UTC 2008
This obviously returns a Ruby core library Time object that can be formatted any way you like.
If your time zone is not already Pacific, run your rake import task like:
$ TZ=PST rake initdb:import # whatever
Now, Rails defines a type called DateTime which you may need, and it wants real integer parameters, so:
DateTime.civil_from_format :local, *'06/06/2008 14:00:00'.split(/[:\/ ]/).map(&:to_i).values_at(2,0,1,3..5)

Related

Rails Upgrade makes default Time format change. How to revert?

I am upgrading a Rails app from
Rails 4.2 -> 5.2 (a subsequent upgrade to Rails 6 is pending)
Ruby 2.2 -> 2.5
Postgres 9.1 -> 10
in various steps. Since the Rails upgrade requires the Postgres upgrade I can't separate the upgrades in a sensible way.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
This makes a lot of API tests fail which were previously all returning UTC times.
Example for Rails 4.2, Ruby 2.2, PG 9.1 for a PhoneNumber object:
2.2.6 :002 > p.time_weekdays_from
=> 2000-01-01 07:00:00 UTC
2.2.6 :003 > p.time_weekdays_from.class
=> Time
Example for Rails 5.2, Ruby 2.5, PG 10:
irb(main):016:0> p.time_weekdays_from
=> Sat, 01 Jan 2000 11:15:00 CET +01:00
irb(main):018:0> p.time_weekdays_from.class
=> ActiveSupport::TimeWithZone
I have added an initializer to override this for the time being and this seems to work fine, but I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
# This works, but why is it necessary?
module ActiveSupport
class TimeWithZone
def as_json(options = nil)
self.utc.iso8601
end
end
end
PS: I don't always want UTC, I just want it for the API because that's what our API clients expect.
Currently I am struggling with the way "Time" objects are handled in Rails 5.2. A "time" column in an AR object is now returned as an ActiveSupport::TimeWithZone, even if the database column has no time zone. Previously it was a plain Time object which had a different default JSON representation.
I'd nevertheless like to understnand why this change has been made and why even 'time without time zone' DB columns are being treated by Rails as if they had a timezone.
This change was made because Ruby's default Time has no understanding of time zones. ActiveSupport::TimeWithZone can. This solves a lot of problems when working with times, time zones, and databases.
For example, let's say your application's time zone is America/Chicago. Previously you had to decide whether you're going to store your times with or without time zones. If you opt for without a time zone, do you store it as UTC or as America/Chicago? If you store it as UTC, do you convert it to America/New York on load or on display? Conversion means adding and subtracting hours from the Time. When you save Time objects you have to be careful to remember what time zone the Time was converted to and to convert it back to the database's time zone. Coordinating all this leads to many bugs.
Rails 5 introduces ActiveSupport::TimeWithZone. This stores the time as UTC and the desired time zone to represent it in. Now handling time is simpler: store it as UTC (ie. timestamp) and add the application's time zone on load. No conversion is necessary. Rails handles this for you.
The change is now timestamp columns, by default, will be formatted in the application's time zone. This takes some getting used to, but ultimately will make your handling of times and time zones more robust.
> Time.zone.tzinfo.name
=> "America/Chicago"
> Time.zone.utc_offset
=> -21600
# Displayed in the application time zone
> Foo.last.created_at
=> Tue, 31 Dec 2019 17:16:14 CST -06:00
# Stored as UTC
> Foo.last.created_at.utc
=> 2019-12-31 23:16:14 UTC
If you have code which manually does time zone conversions, get rid of it. Work in UTC. Time zones are now just formatting.
As much as possible...
Work with objects, not strings.
Work in UTC, time zones are for formatting.
If you need to turn a time into a string, make the formatting explicit.
def get_api_time
'2000-01-01 07:00:00 UTC'
end
# bad: downgrading to strings, implicit formatting
expected_time = Time.utc(2000, 1, 1, 7)
expect( get_api_time ).to eq expected_time
# good: upgrading to objects, format is irrelevant
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect(
Time.zone.parse(get_api_time)
).to eq expected_time
# better: refactor method to return ActiveSupport::TimeWithZone
def get_api_time
Time.zone.parse('2000-01-01 07:00:00 UTC')
end
expected_time = Time.zone.parse('2000-01-01 07:00:00 UTC')
expect( get_api_time ).to eq expected_time
I recommend reading these articles, they clear things up.
It's About Time (Zones)
The Exhaustive Guide to Rails Time Zones

Rails Records with times in different timestamps that occur on a date

Rails 4.2 and PostgreSQL 9.4.
I have events with starts_at and time_zone columns.
I'd like a SQL query to return all events that start, in their local time zone, on a specified date.
I.e. if an event starts at 11pm in Hawaii on Jan 24th:
t1 = ActiveSupport::TimeZone['Hawaii'].parse("2015-01-24 23:00:00")
=> Sat, 24 Jan 2015 23:00:00 HST -10:00
t1.in_time_zone('UTC')
=> Sun, 25 Jan 2015 09:00:00 UTC +00:00
And if an event starts at 1am in Hong Kong on Jan 24th:
t2 = ActiveSupport::TimeZone['Hong Kong'].parse("2015-01-24 01:00:00")
=> Sat, 24 Jan 2015 01:00:00 HKT +08:00
t2.in_time_zone('UTC')
=> Fri, 23 Jan 2015 17:00:00 UTC +00:00
The events occur on the 25th and 23rd of Jan in Rails' implicit UTC database zone, but both events occur on the 24th in their own time zone.
I'd like to write a SQL query akin to:
SELECT * FROM events WHERE (events.starts_at at time zone 'UTC' at time zone events.time_zone)::date = '2015-01-24';
(The initial conversion to UTC is required for a timestamp I believe)
However I get an error ERROR: time zone "Hawaii" not recognized
This is because PostgreSQL time zone names do not match with that of Rails (SELECT * FROM pg_timezone_names; for a list.)
Is there an alternative method where I can still do the query in SQL but not have this problem? I don't want to load the entire events set into Rails and process it there as it is very large and this is a frequent query.
As a small context as to why I need this query: I have a membership pass valid over a certain date range which gives you access to events which can occur in different time zones.
If there is no other way to do this, is there any tool that converts between the Rails names for time zones and that used by PostgreSQL so that I can save both the Rails zone and the PostgreSQL zone in the database record?
Using time zone abbreviations such as PDT instead (I don't even know if these are consistent between Rails and PostgreSQL either) doesn't deal with daylight savings time (which, granted, may not be an issue here but is not satisfactory).

Finding records based on converted timezone?

I have records with a created column whose datetime is stored in UTC.
For instance, this is the datetime stored in one record: 2014-02-01 00:33:25
But if I retrieve that record and confer it to CST, it'd look like this:
Article.find(1).created.in_time_zone('Central Time (US & Canada)') => Fri, 31 Jan 2014 18:33:25 CST -06:00
Which is a different day/month as far as that timezone goes.
The problem I'm having is finding records based on a date. So say I wanted to pull all records for January 31, 2014 (CST). I want to include my example record.
How do I do that in a Rails app? I'm running Rails 4.0.1 and Ruby 2.0.0 with Postgres 9.3.1.0.
Use AT TIME ZONE to change timezone
Article.where("date(created AT TIME ZONE 'UTC' AT TIME ZONE 'CST') = '01/31/2014'")
Got some help outside of SO and got what I was looking for.
Say I wanted to find all records in a date range, this is what it'd be:
Article.where(created: Date.parse("January 1, 2014").in_time_zone('Central Time (US & Canada)').beginning_of_day.utc..Date.parse("January 31, 2014").in_time_zone('Central Time (US & Canada)').end_of_day.utc)

Rails4: Saving and displaying date in user's timezone

I am working on a rail4 app. Where I want to store dates in all mysql tables in UTC. However I store user's timezone in a specific table, called users. When user logs in, I get user's timezone form user table and save in session.
I am able to save date in all tables in UTC as default value of config.time_zone is UTC for activerecords and activemodels. But while displaying I want to show dates in user's timezone. As well as, when any user inputs a date/time in any html form, then I want to save it in the equivalent UTC format.
What is the best way to achieve this?
Rails, activerecord and MySQL will save all the timestamp fields in UTC. Without you having to do anything.
In your application.rb file where the configuration of the Application is done, you define the default time zone if you want the display of timestamps to take place on time zone different from UTC.
Hence
config.time_zone = 'Central Time (US & Canada)'
will display the timestamp fields (without you having to do anything special in other piece of code) using the Central Time.
When you want each of your users to have timestamps displayed in different time zone you can store the time zone in a column along side the user data. The column can be called time_zone and can contain the string of the user preferred time zone.
But, you have to tell the timestamp object to display itself to the specific timezone. This is done with the help of the method in_time_zone(timezone) that DateTime object responds to.
Example (when the default time zone is UTC):
1.9.3-p194 :004 > d = DateTime.new(2012, 9, 1, 6, 30, 0)
=> Sat, 01 Sep 2012 06:30:00 +0000
1.9.3-p194 :005 > d.in_time_zone("Central Time (US & Canada)")
=> Sat, 01 Sep 2012 01:30:00 CDT -05:00
Or you can change the time zone globally for the request at hand on a before or around filter. There is a documentation on internet if you do a google on that.
Read also this one: http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html
for various alternatives to approach the problem.
You could store the time in UTC, and store the timezone separately. Timezones are commonly stored as a UTC-offset in seconds (seconds are the SI unit of time).
Then you can display it like so:
utime = Time.now.utc.to_i # this value can be any format that Time.at can understand. In this example I'll use a unix timestamp in UTC. Chances are any time format you store in your DB will work.
=> 1375944780
time = Time.at(utime) # parses the time value (by default, the local timezone is set, e.g. UTC+08:00)
=> 2013-08-08 14:53:00 +0800
time_in_brisbane = time.in_time_zone(ActiveSupport::TimeZone[36000]) # sets the timezone, in this case to UTC+10:00 (see http://stackoverflow.com/a/942865/72176)
=> Thu, 08 Aug 2013 16:53:00 EST +10:00
time_brisbane.strftime("%d %b %Y, %l:%M %p %z") # format with strftime however you like!
=> "08 Aug 2013, 4:53 PM +1000"

How to store UTC time values in Mongo with Mongoid?

The behavior I'm observing with the Mongoid adapter is that it'll save 'time' fields with the current system timezone into the database. Note that it's the system time and not Rail's environment's Time.zone. If I change the system timezone, then subsequent saves will pick up the current system timezone.
# system currently at UTC -7
#record.time_attribute = Time.now.utc
#record.save
# in mongo, the value is "time_attribute" : "Mon May 17 2010 12:00:00 GMT-0700 (QYZST)"
#record.reload.time_attribute.utc? # false
Try setting the use_utc mongoid config parameter to true.
It tells Mongoid that you want to return times in UTC:
http://github.com/durran/mongoid/blob/master/lib/mongoid/config.rb#L22

Resources