Why does this rails query behave differently depending on timezone? - ruby-on-rails

I have a rails time-based query which has some odd timezone sensitive behaviour, even though as far as I know I'm using UTC. In a nutshell, these queries give different answers:
>> Model.find(:all,:conditions=>['created_at<=?',(Time.now-1.hours).gmtime]).length
=> 279
>> Model.find(:all,:conditions=>['created_at<=?',(Time.now-1.hours)]).length
=> 280
Where the DB actually does contain one model created in the last hour, and the total number of models is 280. So only the first query is correct.
However, in environment.rb I have:
config.time_zone = 'UTC'
The system time zone (as reported by 'date') is BST (which is GMT+1) - so somehow this winds up getting treated as UTC and breaking queries.
This is causing me all sorts of problems as I need to parameterise the query passing in different times to an action (which are then converted using Time.parse()), and even though I send in UTC times, this 'off by one hour' DST issue crops a lot. Even using '.gmtime()' doesn't always seem to fix it.
Obviously the difference is caused somehow by an implicit conversion somewhere resulting in BST being incorrectly treated as UTC, but why? Doesn't rails store the timestamps in UTC? Isn't the Time class timezone aware? I am using Rails 2.2.2
So what is going on here - and what is the safe way to program around it?
edit, some additional info to show what the DB and Time class are doing:
>> Model.find(:last).created_at
=> Tue, 11 Aug 2009 20:31:07 UTC +00:00
>> Time.now
=> Tue Aug 11 22:00:18 +0100 2009
>> Time.now.gmtime
=> Tue Aug 11 21:00:22 UTC 2009

The Time class isn't directly aware of your configured timezone. Rails 2.1 added a bunch of timezone support, but Time will still act upon your local timezone. This is why Time.now returns a BST time.
What you likely want is to interact with Time.zone. You can call methods on this like you would the Time class itself but it will return it in the specified time zone.
Time.zone.now # => Tue, 11 Aug 2009 21:31:45 UTC +00:00
Time.zone.parse("2:30 PM Aug 23, 2009") # => Sun, 23 Aug 2009 14:30:00 UTC +00:00
Another thing you have to be careful with is if you ever do queries on the database where you are comparing times, but sure to use the UTC time (even if you have a different time zone specified) because Rails always stores UTC in the database.
Item.all(:conditions => ["published_at <= ?", Time.now.utc])
Also, instead of Time.now-1.hour do 1.hour.ago. It is easier to read and Rails will automatically use the configured timezone.

The TimeZone you need to set is UK, this will automatically handle BST
Time.zone = 'UK'
Time.zone.now
=> Sun, 17 Oct 2010 02:09:54 BST +01:00

start_date_format = DateTime.strptime(#start_date, date_format)
start_date_format_with_hour =
DateTime.strptime((start_date_format.to_i + timezone_offset*60*60).to_s,'%s').strftime(date_format)
end_date_format = DateTime.strptime(#end_date, date_format)
end_date_format_with_hour = DateTime.strptime((end_date_format.to_i + timezone_offset*60*60).to_s,'%s').strftime(date_format)
#filters_date = "invoices.created_at >= ? AND invoices.created_at < ?", start_date_format_with_hour, end_date_format_with_hour

Related

Is there an elegant way to use the app timezone when running a Rails ActiveRecord aggregate method?

When I use an ActiveRecord object's time field or use pluck on that field, I always (correctly) get back the time in the application's timezone:
> ClaritySurveyData::SurveyResponse.order(:id).first.submitted_at
=> Mon, 17 Sep 2012 09:46:33 PDT -07:00
> ClaritySurveyData::SurveyResponse.order(:id).pluck(:submitted_at).first
=> Mon, 17 Sep 2012 09:46:33 PDT -07:00
However, when a use an ActiveRecord aggregate method on the same field, I (incorrectly) get back the time in UTC:
> ClaritySurveyData::SurveyResponse.minimum(:submitted_at)
=> 2012-09-17 16:46:33 UTC
This bug has been biting me every few years, just infrequently enough that every time I have to figure it out afresh.
Obviously, I can easily work around it in specific cases using .in_time_zone.
However, I'd love to be able to patch it generally, so I don't ever have to figure it out again.
Any suggestions?

Rails Json response automatically changes timezeone only on some requests

We have a simple json API in rails 4 that returns the data using jbuilder. One of the fields is a datetime field "date_of_birth" the json formatting is acting strangely. The same request to the API can randomly generate 2 different results
"date_of_birth":"1989-06-14T20:52:00-07:00"
"date_of_birth":"1989-06-15T03:52:00Z"
As you can see the first one is in local time as the other one is in UTC timezone. We have the timezone globally set to "UTC".
this is the line in the jbuilder view that produces the output, nothing special'
json.array!(#patients) do |patient|
json.extract! patient, :id, :first_name, :last_name, :gender, :groups_code, :date_of_birth
end
What could cause this issue?
Rails is doing some pretty clever work with times in order to cover users with different time-zones and server independent hosting location. Basically times are stored in the database in UTC zone, and converted when needed to the local server/or user time zone.
In a standard Rails app you have 3 times zones that can be the same or can be different: Database time zone, default time zone, and user time zone.
Have a look at this very good article that says it all. Then apply the right method to your date_of_birth field while exporting to json
DOs
2.hours.ago # => Fri, 02 Mar 2012 20:04:47 JST +09:00
1.day.from_now # => Fri, 03 Mar 2012 22:04:47 JST +09:00
Date.today.to_time_in_current_zone # => Fri, 02 Mar 2012 22:04:47 JST +09:00
Date.current # => Fri, 02 Mar
Time.zone.parse("2012-03-02 16:05:37") # => Fri, 02 Mar 2012 16:05:37 JST +09:00
Time.zone.now # => Fri, 02 Mar 2012 22:04:47 JST +09:00
Time.current # Same thing but shorter. (Thank you Lukas Sarnacki pointing this out.)
Time.zone.today # If you really can't have a Time or DateTime for some reason
Time.zone.now.utc.iso8601 # When supliyng an API (you can actually skip .zone here, but I find it better to always use it, than miss it when it's needed)
Time.strptime(time_string, '%Y-%m-%dT%H:%M:%S%z').in_time_zone(Time.zone) # If you can't use Time#parse
DON'Ts
Time.now # => Returns system time and ignores your configured time zone.
Time.parse("2012-03-02 16:05:37") # => Will assume time string given is in the system's time zone.
Time.strptime(time_string, '%Y-%m-%dT%H:%M:%S%z') # Same problem as with Time#parse.
Date.today # This could be yesterday or tomorrow depending on the machine's time zone.
Date.today.to_time # => # Still not the configured time zone.
You may have Time.zone = ... set somewhere based on user time zone setting.
I have seen this same problem and have not been able to figure out why it happens. The other answers here seem to be missing the point that which time format is returned is seemingly random, with no changes to code/config whatsoever.
I was able to "fix" this issue by using .to_time.iso8601 my JBuilder block which will force the 1989-06-15T03:52:00Z format.

Correcting time for daylight savings in rails DateTime.parse

Just adding to the million questions about time zone and DST issues out there.
I have a form with separate date and time fields that I combine to create a DateTime like so
start_time = DateTime.parse("#{parse_date(form_date)} #{form_start_time} #{Time.zone}")
If I fill out my form with 21 Aug 2012 and 15:00, then these are the values that I see when I reload my form. If I then look at my start_time attribute in my model it is correctly set to Tue, 21 Aug 2012 15:00:00 EST +10:00.
The problem I am having occurs if I use a date later this year once daylight savings kicks in (I am in Australia). If I use 21 Dec 2012 and 15:00 then check start_time I see Fri, 21 Dec 2012 16:00:00 EST +11:00.
My interpretation of the problem is that the date is being saved in my current time zone (+10:00) as this is what I have told DateTime.parse to do. However when the value is returned, Rails is looking at the date and saying 'hey, it's daylight savings time in December' and returning the time in the +11:00 time zone.
What I want to do is tell DateTime.parse to save the time in the +11:00 time zone if DST is in effect. Clearly passing Time.zone into my string doesn't achieve this. Is there a simple way of doing this? I can see ways of doing it using Time#dst? but I suspect that this is going to create some really ugly convoluted code. I thought there might be a built in way that I'm missing.
(Answer for Rails 4.2.4, didn't check for older or newer versions)
Instead of using fixed shift +01:00, +02:00, etc, I recommend to use the in_time_zone String method with time zone name as argument :
Summer time :
ruby :001 > "2016-07-02 00:00:00".in_time_zone('Paris')
=> Sat, 02 Jul 2016 00:00:00 CEST +02:00
Winter time :
ruby :002 > "2016-11-02 00:00:00".in_time_zone('Paris')
=> Wed, 02 Nov 2016 00:00:00 CET +01:00
String#in_time_zone is the equivalent of :
ruby :003 > Time.find_zone!("Paris").parse("2016-07-02 00:00:00")
=> Sat, 02 Jul 2016 00:00:00 CEST +02:00
ruby :004 > Time.find_zone!("Paris").parse("2016-11-02 00:00:00")
=> Wed, 02 Nov 2016 00:00:00 CET +01:00
You can get the time zone names by :
$ rake time:zones:all
Or in rails console :
ruby :001 > ActiveSupport::TimeZone.all.map(&:name)
Or build collection for select tag :
ActiveSupport::TimeZone.all.map do |timezone|
formatted_offset = Time.now.in_time_zone(timezone.name).formatted_offset
[ "(GMT#{formatted_offset}) #{timezone.name}", timezone.name ]
end
And store the time zone name instead of the shift.
Note : don't confuse String#in_time_zone method and the Time#in_time_zone method.
consider the time zone for my system is 'Paris'.
ruby :001 > Time.parse("2016-07-02 00:00:00")
=> 2016-07-02 00:00:00 +0200
ruby :002 > Time.parse("2016-07-02 00:00:00").in_time_zone("Nuku'alofa")
=> Sat, 02 Jul 2016 11:00:00 TOT +13:00
Here's my solution so far. I'm hoping someone has a better one.
start_time = DateTime.parse "#{date} #{(form_start_time || start_time)} #{Time.zone}"
start_time = start_time - 1.hour if start_time.dst? && !Time.now.dst?
start_time = start_time + 1.hour if Time.now.dst? && start_time.dst?
It seems to work but I haven't rigorously tested it. I suspect it could be prettied up and shortened but I think this is readable and understandable. Any improvements?
I ran into this exact issue. My app allows users to see upcoming events. In the US we fall of DST on November 2nd and all events on and after that date were showing times an hour early.
We require the opportunity to have the timezone selected and stored to its own field. Before I was using the following to store my datetime:
timezone_offset = Time.now.in_time_zone(params[:opportunity][:time_zone]).strftime("%z") #-0700
DateTime.parse("#{params[:opportunity][:start_datetime]} #{timezone_offset}")
To fix the issue I have changed to:
start_datetime = Time.zone.parse(params[:opportunity][:start_datetime])
To display the correct times we use:
#opportunity.start_datetime.in_time_zone(#opportunity.time_zone)
I wouuld try and use
Australian Eastern Standard Time (AEST) (UTC +10).
Australian Central Standard Time (ACST) (UTC +9 ½).
Australian Western Standard Time (AWST) (UTC +8).
which adjust for Daylight Savings.
With Rails, we can use ActiveSupport::TimeZone for this:
tz = ActiveSupport::TimeZone.new 'Pacific Time (US & Canada)'
tz.parse(date_str_without_zone).to_datetime
I use TZip to get TimeZone strings (e.g. "Pacific Time (US & Canada)") from zip codes.
In case you have a custom date/time format, different than the supported by String#in_time_zone, you could also use (since rails 5) strptime like:
Time.find_zone!('Auckland').strptime('2021-02-02 08.00.00', '%Y-%m-%d %H.%M.%S')

DateTime.now or Time.now?

Which of the two should I be using in my Rails application:
DateTime.now or Time.now
Is there any harm in using both in the application?
Can there ever be any differences between the two in case of the above (now) example? (On my current system they are both showing the same time)
Use (Date)Time.current instead of (Date)Time.now
Rails extends the Time and DateTime objects, and includes the current property for retrieving the time the Rails environment is set to (default = UTC), as opposed to the server time (Could be anything).
This is critical- You should always be working in UTC time except when converting between timezones for user input or display- but many production systems are not UTC by default. (e.g. Heroku is set to PST (GMT -8))
See article here
In reference to Time.now (not DateTime.now):
The object created will be created using the resolution available on your system clock, and so may include fractional seconds.
a = Time.new #=> Wed Apr 09 08:56:03 CDT 2003
b = Time.new #=> Wed Apr 09 08:56:03 CDT 2003
a == b #=> false
"%.6f" % a.to_f #=> "1049896563.230740"
"%.6f" % b.to_f #=> "1049896563.231466"
If you want to get the time in the application's time zone, you'll need to call Time.zone.now, so that's what I generally use.
Time.now and DateTime.now will both return a time in the system's time, which often is set to UTC.

How to convert Date into UTC in MongoMapper & Ruby/Rails?

I added this line of code
self.auth_history.push [start_date, self.coupon_code]
And got this error message
Date is not currently supported; use a UTC Time instance instead.
I also tried start_date.utc, but it didn't work either.
Please help. Thanks.
I got this answer from Seattle Brigade group -
===
I didn't see start_date defined in your code as a key in MongoMapper, so
I'll assume you're creating your own date object, either directly via Ruby,
or wrapped by Rails. As far as I know, and someone please correct me, Mongo
stores dates as UTC time in milliseconds since epoch. So when you define a
key with a :date mapping in MongoMapper, you're wrapping a Time object in
Ruby.
Therefore, if you want to store a date inside of Mongo, and it wasn't
created by MongoMapper, make sure you create a Time object in UTC.
MongoMapper comes with a Date mixin method called to_mongo that you can use.
>> Time.now.utc
=> Fri Jan 28 03:47:50 UTC 2011
>> require 'date'
=> true
>> date = Date.today
=> #<Date: 4911179/2,0,2299161>
>> Time.utc(date.year, date.month, date.day)
=> Thu Jan 27 00:00:00 UTC 2011
>> require 'rubygems'
=> true
>> require 'mongo_mapper'
=> true
>> Date.to_mongo(date)
=> Thu Jan 27 00:00:00 UTC 2011
But watch out for the time change.
>> Date.to_mongo(Time.now)
=> Thu Jan 27 00:00:00 UTC 2011
>> Date.to_mongo(Time.now.utc)
=> Fri Jan 28 00:00:00 UTC 2011
Good luck.
===
And by using
Date.to_mongo(start_date)
it works for me.
First, I think the question title is bad in description. Actually, the difference between different timezone is on Time not on Date. So, it's really not proper to say I want to convert a date to UTC format.
Here is another way in Ruby to convert DateTime to its UTC format:
DateTime.now.new_offset(0)
Here's another option:
Time.at(Date.today.to_datetime.to_i).utc
Here I am using Date.today as an arbitrary example date. Replace with whatever date you want to convert. Once the date is converted to a Time instance, it can be serialized to BSON without any problem, as Time is a supported primitive type, that is to say, it can be saved using MongoMapper to the database.
As per EfratBlaier's comment I have updated the answer.
Date.today.to_time.utc

Resources