Problems with saving and querying dates in Rails - ruby-on-rails

I have an Appointment model, and for one particular appointment, I saved it having a start_time of 12:15am on 3/5/2011. Look at this:
irb(main):002:0> a = Appointment.find(15)
=> #<Appointment id: 15, start_time: "2011-03-05 05:15:00", created_at: "2011-03-05 03:42:03", updated_at: "2011-03-05 03:42:03", stylist_id: 13, client_id: 8>
irb(main):003:0> a.start_time
=> Sat, 05 Mar 2011 00:15:00 EST -05:00
As you can see, the date got saved wrong. Interestingly, though, Rails compensates for it when the data comes back out.
I assume my app has always behaved this way and I just didn't notice. Recently, though, I wrote a query that pulls the dates out with raw SQL, so I'm getting the wrong time and it's causing problems.
Can anyone shed some light on why this is happening and what I can do to get around the problem?

Rails does this on purpose. Check your time zone settings:
config.active_record.default_timezone
config.time_zone
http://guides.rubyonrails.org/configuring.html#configuring-active-record
Time zone features were introduced in 2.1 and haven't changed much. This article gives a good explanation:
http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/

Rails save datetime in database in UTC time(zero offset) and converts the time to the time zone which we intend when it displays it. So, when we parse directly from the database, you will be getting UTC time. If you try to convert it into time like this:
time_string = #raw string obtained from database(like "2011-03-05 05:15:00")
time_object = Time.parse(time_string)
You will get the time with offset according to the timezone of your machine or server. Ruby parses time like that. It will take the time and give the timezone of your machine(which is incorrect here as the timezone is actually UTC as it is from database). I ran into this problem and solved it by adding UTC when I parse time from raw sql strings from database.
time_string << " UTC"
time_object = Time.parse(time_string) # gives time zone as UTC
then if you use it, the result will be correct.

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

Timezones in Rails 5 when using beginnig_of_month

My Rails 5 app is set to BRT (Brazilian timezone) and my Postgres database to UTC. I never had to worry about that because Rails always calculated the correct time when reading from or writing to the database. So for example when I had an input field with time = 15:00 it would write 18:00 to the database and when reading from the database it would return 15:00 again. Perfect!
But now I want to write the beginning of the month to the database:
Time.now.beginning_of_month which is 2017-04-01 00:00:00
Payment.create(
:time => Time.now.beginning_of_month
)
Now it writes exactly this (in BRT) to the database without converting to UTC. When I read this from the database later it converts it to BRT 2017-03-31 21:00:00 which is wrong.
Of course I could convert the time to UTC before saving it to the database. But I find it strange that Rails always took care of converting and in this case it does not.
I hope the problem became clear.
Any ideas?
I found a very good article about this subject:
https://www.varvet.com/blog/working-with-time-zones-in-ruby-on-rails/
Basically what I had to to is change Time.now to Time.current as Time.now ignores the timezone settings.

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)

Rails 3 DateTime and Time beginning_of_day end_of_day format incorrect

I am trying to find events on certain days with this code:
Event.where('starttime BETWEEN ? AND ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day)
In rails console if I run DateTime.now.beginning_of_day I get exactly what I expect:
Mon, 09 Apr 2012 00:00:00 -0700
I can see where the problem is occurring but looking at the SQL in rails console. Somehow when the date makes it into the SQL query it gets automatically formatted to the wrong date and time.
SELECT "events".* FROM "events" WHERE (starttime BETWEEN '2012-04-09 07:00:00' AND '2012-04-10 06:59:59')
This is giving me results that vary from today until tomorrow, as the sql above says. I can see that when the DateTime.now.beginning_of_day also DateTime.now.end_of_day are being formatted incorrectly once they make it into the sql query. Do I need to be formatting this in a certain way? Any idea why it would go to 7:00 of today and 7:00 of tomorrow?
I don't know if it makes a difference but I'm using PostgreSQL.
Thanks!
See: http://railscasts.com/episodes/106-time-zones-in-rails-2-1
I think the times you're quoting above are actually the same time, just one is UTC time (you're on pacific time right?). To fix this you could try setting:
config.time_zone = "Pacific Time (US & Canada)"
in your environment.rb
You could also try:
DateTime.now.utc.beginning_of_day
DateTime.now.utc.end_of_day
so the you're using UTC as per the DB
You are probably using UTC time in your database. This is a good thing but it leads to all sorts of confusion. The -0700 offset is converted to 07:00:00 since that's when your day starts in UTC time.
You could use the PostgreSQL AT TIME ZONE construct with UTC timestamps:
SELECT e.*
FROM events e
WHERE starttime >= '2012-04-09 00:00' AT TIME ZONE 'UTC'
AND starttime < '2012-04-10 00:00' AT TIME ZONE 'UTC'
I recently explained PostgreSQL timestamp and time zone handling in a related answer.

Rails timestamps don't use the right timezone

I'm a bit confused about timezones in rails. I want my rails app to use British Summer Time (like daylight savings in the US) for the timestamps set in updated_at and created_at in my models. I changed my environment.rb to say
config.time_zone = 'London'
The ubuntu server my app is on seems to use BST for it's time: in the command line, for example, if i type 'date' i get the current time (not offset by an hour). In the rails console, i see the following:
>> time = Time.now
=> Wed Oct 27 16:29:17 +0100 2010
>> time.zone
=> "BST"
All fine. However, if i make a new AR model object and save it, the timestamps are from an hour ago. So, it looks like this is using UTC. Now, i can see the logic in this: since the timestamps might be used in the model logic, you want them to be based on an unvarying yardstick time, ie UTC. But, this is a weird bit of behaviour that i don't understand:
#change a record and save it
>> someobj.save
=> true
#object's updated_at is one hour ago
>> someobj.updated_at
=> Wed, 27 Oct 2010 15:34:22 UTC +00:00
>> Time.now
=> Wed Oct 27 16:34:31 +0100 2010
#however, Time.now - object's updated at is just a few seconds.
>> Time.now - someobj.updated_at
=> 15.305549
So, before doing the subtraction, updated_at is converted into the current time zone.
The reason i want to show the date in the current time zone is just for status reports etc in the views: if someone updates something i want them to see that it was updated '1 minute ago' not 'one hour ago'.
Can anyone unconfuse me? cheers, max
EDIT: My immediate problem, of showing the right time in the status, is solved by using the 'time_ago_in_words' helper, which adjusts for time zone. I'd still like someone to explain what's going on with the timestamps though :)
Timestamps are stored in UTC by default, and this is probably the best way to do it. If you move from one server environment to another, you don't want all of your times shifting around just because you switched time zones.
If you want to know what the timestamp is in your local time zone, you just have to ask for it that way:
someobj.updated_at.localtime
Note the offset listed at the end of the times -- the first offset is 0, the second is 1. When the time calculation occurs, the offset is included automatically, so that the subtraction gives you the correct result. someobj.updated_at and Time.now each displays its value in a different time zone, so they are really only 9 seconds apart, not 1 hour and 9 seconds.

Resources