Comparing datetime in Rails always false - ruby-on-rails

I'm trying to compare two dates of which both are datetime.
closed_at and updated_at (while updated_at is the one already created by rails)
This is my code:
<% if #task.closed_at == #task.updated_at %>
*Some stuff*
<% end %>
With closing a task it gets automatically updated.
irb(main):011:0> Task.first.closed_at
=> Fri, 07 Aug 2015 10:47:58 CEST +02:00
irb(main):012:0> Task.first.updated_at
=> Fri, 07 Aug 2015 10:47:58 CEST +02:00
irb(main):013:0> Task.first.closed_at == Task.first.updated_at
=> false
irb(main):014:0> Task.first.closed_at.to_datetime == Task.first.updated_at.to_datetime
=> false
I've read this two posts:
comparing datetime and time produces a false result
DateTime comparison fails in Rails
But neither the data type is different nor the time zone.
Environment:
ruby 2.2.2p95
rails 4.2.3
pg 0.18.2

This can happen because ruby Time makes comparison with fractions of seconds. So the "print" may look equal, but there is some time difference between both dates.
Try to use to_f method to see the difference between #task.closed_at and #task.updated_at. Pretty sure is this issue.
To solve your problem, just ignore the milliseconds part of dates:
<% if #task.closed_at.to_i == #task.updated_at.to_i %>
*Some stuff*
<% end %>

Try this little experiment with any model that has a created_at and updated_at in the console:
user = User.new
user.created_at = 'Fri, 07 Aug 2015 10:47:58 CEST +02:00'.to_datetime
user.updated_at = 'Fri, 07 Aug 2015 10:47:58 CEST +02:00'.to_datetime
user.created_at == user.updated_at
The last statement above returns true. So there has to be a difference in the time not visible when the time is displayed.

It happens because of the little difference (in milliseconds) between those 2 variables. There is one blog post regarding this issue:
http://railsware.com/blog/2014/04/01/time-comparison-in-ruby/
Basically, an easy fix would be to convert the time values to int:
<% if #task.closed_at.to_i == #task.updated_at.to_i %>
*Some stuff*
<% end %>

Related

Select object based on date range

Tech specs: ruby 2.1.5p273, Rails 4.2.3.
I have an array of Days that I want to loop through to pick the right Exits (model) that fall within a date range.
#exits has :start_date and :end_date
#days is an array of dates like:
=> [Sun, 06 Sep 2015, Sat, 12 Sep 2015, Tue, 15 Sep 2015, Fri, 18 Sep 2015, Sat, 19 Sep 2015, Sun, 20 Sep 2015, Wed, 23 Sep 2015]
I thought something like this would work:
#days.each do |day|
#exits.where(:start_date..:end_date).include?(day)
end
but I get an error:
TypeError: Cannot visit Range
What is the best way to query an object that has a date range (between two fields) by comparing it against a single date? Thanks!
You can use the following:
#days.each do |day|
exits = Exit.where('? BETWEEN start_date AND end_date', day)
# etc.
end
If you don't want to loop over them then you can do:
Event.where("start_date IN (:days) AND end_date IN (:days)", { days: #days })
or
Event.where(start_date: #days, end_date: #days)
Exit.where(day: #exit.start_date..#exits.end_date)
or
Exit.where('day >= ? AND day <= ?', #exit.start_date, #exits.end_date)
Doing SQL queries in a loop is probably a bad idea, it could be refactored to be be one call most likely. And this should happen in the controller not in the view.

Ruby on Rails time zone strange behaviour

There is task model with attributes when and duration.
create_table "tasks", force: true do |t|
...
t.datetime "when"
t.integer "duration"
...
end
I wrote method for checking if task is active so I can show it on page.
This is active method:
def active?
if (self.when + self.duration) > Time.now
true
end
end
I tried in console to inspect object:
t.when + t.duration
=> Sun, 08 Sep 2013 01:01:00 UTC +00:00
DateTime.now
=> Sun, 08 Sep 2013 01:57:13 +0200
t.active?
=> true
It's true but I entered 1:00 time and 1 minute for duration and I hoped it shouldn't be true.
It seems that when column in database is not saved in correct time zone, so it gives incorrect results. How to solve this issue?
It seems that when column in database is not saved in correct time zone
1) Rails automatically converts times to UTC time before inserting them in the database (which is a good thing), which means the times have an offset of "+0000" . That means if you save a time of 8pm to the database, and your server is located in a timezone with an offset of "+0600", then the equivalent UTC time is 2pm, so 2pm gets saved in the database. In other words, your local server's time is 6 hours ahead of UTC time, which means that when it's 8pm in your server's time zone, it's 2pm in the UTC timezone.
2) When you compare dates, ruby takes the timezone offset into account--in other words ruby converts all times to the same timezone and then compares the times. Here is an example:
2.0.0p247 :086 > x = DateTime.strptime('28-01-2013 08:00:00 PM +6', '%d-%m-%Y %I:%M:%S %p %z')
=> Mon, 28 Jan 2013 20:00:00 +0600
2.0.0p247 :087 > y = DateTime.strptime('28-01-2013 08:20:00 PM +7', '%d-%m-%Y %I:%M:%S %p %z')
=> Mon, 28 Jan 2013 20:20:00 +0700
2.0.0p247 :088 > x < y
=> false
If you just compare the times of the two Datetime objects, x is less than y. However, y has a time of 8:20pm in a timezone that has an offset of +7, which is equivalent to the time 7:20pm in a timezone with an offset of +6. Therefore, y is actually less than x. You need to compare apples to apples, which means you need to mentally compare times that have been converted to the same timezone to get the same results as ruby/rails produces.
3) You can convert Time.now to a UTC time using the rails utc() method:
2.0.0p247 :089 > x = Time.now
=> 2013-09-07 8:00:00 +0600
2.0.0p247 :090 > x.utc
=> 2013-09-07 02:00:00 UTC
That's what ruby does before comparing Time.now to task.when + task.duration
4) You might find it more convenient to create a DateTime object with the time you want using:
DateTime.strptime('28-01-2013 08:00:00 PM +0', '%d-%m-%Y %I:%M:%S %p %z'
Because you are able to specify the offset as zero, you don't have to create a time that anticipates the conversion to UTC time.
Or you can use the change() method, which causes the offset() to change without converting the time:
2.0.0p247 :011 > x = DateTime.now
=> Sun, 08 Sep 2013 00:34:08 +0600
2.0.0p247 :012 > x.change offset: "+0000"
=> Sun, 08 Sep 2013 00:34:08 +0000
ActiveRecord stores timestamps in UTC by default. See How to change default timezone for Active Record in Rails? for changing default time zone.
You can also just use Time#in_time_zone to convert t.when to your timezone, see http://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html.

Rails arel table query with Date and Time

I am having problems with a query because midnight conversion is not working as expected.
time = Date.today.midnight #=> Mon, 15 Jul 2013 00:00:00 BRT -03:00
time.class #=> ActiveSupport::TimeWithZone
condition = Task.arel_table[:scheduled_to].gt(time)
condition.to_sql #=> "`tasks`.`scheduled_to` > '2013-07-15 03:00:00'"
I was expecting the generated sql to be
`tasks`.`scheduled_to` > '2013-07-15 00:00:00'"
My Time zone is GMT -3. If i change the time zone so it matches GMT -5 the generated sql is
condition.to_sql #=> "`tasks`.`scheduled_to` > '2013-07-15 05:00:00'"
Rails 4.0.0
Ruby 2.0.0p247
Is there any way to ignore the timezone so the query behaves like expected?
Timezones are relative to UTC (0000), so you gotta remove it from your Date.
DateTime.now.midnight.utc #=> '2013-08-23 03:00:00 +0000'
Now, just get rid from the compensated hours.
DateTime.now.midnight.utc.change({:hour => 0, :min => 0}) #=> '2013-08-23 00:00:00 +0000'
Not sure if there is a cleaner way to do it, but it worked for me (Ruby 1.9.3p385).
Date.current or Date.today.midnight.utc Should have solved the problem.

Ruby / Rails - Change the timezone of a Time, without changing the value

I have a record foo in the database which has :start_time and :timezone attributes.
The :start_time is a Time in UTC - 2001-01-01 14:20:00, for example.
The :timezone is a string - America/New_York, for example.
I want to create a new Time object with the value of :start_time but whose timezone is specified by :timezone. I do not want to load the :start_time and then convert to :timezone, because Rails will be clever and update the time from UTC to be consistent with that timezone.
Currently,
t = foo.start_time
=> 2000-01-01 14:20:00 UTC
t.zone
=> "UTC"
t.in_time_zone("America/New_York")
=> Sat, 01 Jan 2000 09:20:00 EST -05:00
Instead, I want to see
=> Sat, 01 Jan 2000 14:20:00 EST -05:00
ie. I want to do:
t
=> 2000-01-01 14:20:00 UTC
t.zone = "America/New_York"
=> "America/New_York"
t
=> 2000-01-01 14:20:00 EST
Sounds like you want something along the lines of
ActiveSupport::TimeZone.new('America/New_York').local_to_utc(t)
This says convert this local time (using the zone) to utc. If you have Time.zone set then you can of course to
Time.zone.local_to_utc(t)
This won't use the timezone attached to t - it assumes that it's local to the time zone you are converting from.
One edge case to guard against here is DST transitions: the local time you specify may not exist or may be ambiguous.
I've just faced the same problem and here is what I'm going to do:
t = t.asctime.in_time_zone("America/New_York")
Here is the documentation on asctime
If you're using Rails, here is another method along the lines of Eric Walsh's answer:
def set_in_timezone(time, zone)
Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
You need to add the time offset to your time after you convert it.
The easiest way to do this is:
t = Foo.start_time.in_time_zone("America/New_York")
t -= t.utc_offset
I am not sure why you would want to do this, though it is probably best to actually work with times the way they are built. I guess some background on why you need to shift time and timezones would be helpful.
Actually, I think you need to subtract the offset after you convert it, as in:
1.9.3p194 :042 > utc_time = Time.now.utc
=> 2013-05-29 16:37:36 UTC
1.9.3p194 :043 > local_time = utc_time.in_time_zone('America/New_York')
=> Wed, 29 May 2013 12:37:36 EDT -04:00
1.9.3p194 :044 > desired_time = local_time-local_time.utc_offset
=> Wed, 29 May 2013 16:37:36 EDT -04:00
Depends on where you are going to use this Time.
When your time is an attribute
If time is used as an attribute, you can use the same date_time_attribute gem:
class Task
include DateTimeAttribute
date_time_attribute :due_at
end
task = Task.new
task.due_at_time_zone = 'Moscow'
task.due_at # => Mon, 03 Feb 2013 22:00:00 MSK +04:00
task.due_at_time_zone = 'London'
task.due_at # => Mon, 03 Feb 2013 22:00:00 GMT +00:00
When you set a separate variable
Use the same date_time_attribute gem:
my_date_time = DateTimeAttribute::Container.new(Time.zone.now)
my_date_time.date_time # => 2001-02-03 22:00:00 KRAT +0700
my_date_time.time_zone = 'Moscow'
my_date_time.date_time # => 2001-02-03 22:00:00 MSK +0400
Here's another version that worked better for me than the current answers:
now = Time.now
# => 2020-04-15 12:07:10 +0200
now.strftime("%F %T.%N").in_time_zone("Europe/London")
# => Wed, 15 Apr 2020 12:07:10 BST +01:00
It carries over nanoseconds using "%N". If you desire another precision, see this strftime reference.
The question's about Rails but it seems, like me, not everyone here is on the ActiveSupport train, so yet another option:
irb(main):001:0> require "time"
=> true
irb(main):003:0> require "tzinfo"
=> true
irb(main):004:0> t = Time.parse("2000-01-01 14:20:00 UTC")
=> 2000-01-01 14:20:00 UTC
irb(main):005:0> tz = TZInfo::Timezone.get("America/New_York")
=> #<TZInfo::DataTimezone: America/New_York>
irb(main):008:0> utc = tz.local_to_utc(t)
=> 2000-01-01 19:20:00 UTC
irb(main):009:0> tz.utc_to_local(utc)
=> 2000-01-01 14:20:00 -0500
irb(main):010:0>
local_to_utc not doing the opposite of utc_to_local might look like a bug but it is at least documented: https://github.com/tzinfo/tzinfo says:
The offset of the time is ignored - it is treated as if it were a local time for the time zone
I managed to do this by calling change with the desired time zone:
>> t = Time.current.in_time_zone('America/New_York')
=> Mon, 08 Aug 2022 12:04:36.934007000 EDT -04:00
>> t.change(zone: 'Etc/UTC')
=> Mon, 08 Aug 2022 12:04:36.934007000 UTC +00:00
https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html#method-i-change
def relative_time_in_time_zone(time, zone)
DateTime.parse(time.strftime("%d %b %Y %H:%M:%S #{time.in_time_zone(zone).formatted_offset}"))
end
Quick little function I came up with to solve the job. If someone has a more efficient way of doing this please post it!
I spent significant time struggling with TimeZones as well, and after tinkering with Ruby 1.9.3 realized that you don't need to convert to a named timezone symbol before converting:
my_time = Time.now
west_coast_time = my_time.in_time_zone(-8) # Pacific Standard Time
east_coast_time = my_time.in_time_zone(-5) # Eastern Standard Time
What this implies is that you can focus on getting the appropriate time setup first in the region you want, the way you would think about it (at least in my head I partition it this way), and then convert at the end to the zone you want to verify your business logic with.
This also works for Ruby 2.3.1.
I have created few helper methods one of which just does the same thing as is asked by the original author of the post at Ruby / Rails - Change the timezone of a Time, without changing the value.
Also I have documented few peculiarities I observed and also these helpers contains methods to completely ignore automatic day-light savings applicable while time-conversions which is not available out-of-the-box in Rails framework:
def utc_offset_of_given_time(time, ignore_dst: false)
# Correcting the utc_offset below
utc_offset = time.utc_offset
if !!ignore_dst && time.dst?
utc_offset_ignoring_dst = utc_offset - 3600 # 3600 seconds = 1 hour
utc_offset = utc_offset_ignoring_dst
end
utc_offset
end
def utc_offset_of_given_time_ignoring_dst(time)
utc_offset_of_given_time(time, ignore_dst: true)
end
def change_offset_in_given_time_to_given_utc_offset(time, utc_offset)
formatted_utc_offset = ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, false)
# change method accepts :offset option only on DateTime instances.
# and also offset option works only when given formatted utc_offset
# like -0500. If giving it number of seconds like -18000 it is not
# taken into account. This is not mentioned clearly in the documentation
# , though.
# Hence the conversion to DateTime instance first using to_datetime.
datetime_with_changed_offset = time.to_datetime.change(offset: formatted_utc_offset)
Time.parse(datetime_with_changed_offset.to_s)
end
def ignore_dst_in_given_time(time)
return time unless time.dst?
utc_offset = time.utc_offset
if utc_offset < 0
dst_ignored_time = time - 1.hour
elsif utc_offset > 0
dst_ignored_time = time + 1.hour
end
utc_offset_ignoring_dst = utc_offset_of_given_time_ignoring_dst(time)
dst_ignored_time_with_corrected_offset =
change_offset_in_given_time_to_given_utc_offset(dst_ignored_time, utc_offset_ignoring_dst)
# A special case for time in timezones observing DST and which are
# ahead of UTC. For e.g. Tehran city whose timezone is Iran Standard Time
# and which observes DST and which is UTC +03:30. But when DST is active
# it becomes UTC +04:30. Thus when a IRDT (Iran Daylight Saving Time)
# is given to this method say '05-04-2016 4:00pm' then this will convert
# it to '05-04-2016 5:00pm' and update its offset to +0330 which is incorrect.
# The updated UTC offset is correct but the hour should retain as 4.
if utc_offset > 0
dst_ignored_time_with_corrected_offset -= 1.hour
end
dst_ignored_time_with_corrected_offset
end
Examples which can be tried on rails console or a ruby script after wrapping the above methods in a class or module:
dd1 = '05-04-2016 4:00pm'
dd2 = '07-11-2016 4:00pm'
utc_zone = ActiveSupport::TimeZone['UTC']
est_zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
tehran_zone = ActiveSupport::TimeZone['Tehran']
utc_dd1 = utc_zone.parse(dd1)
est_dd1 = est_zone.parse(dd1)
tehran_dd1 = tehran_zone.parse(dd1)
utc_dd1.dst?
est_dd1.dst?
tehran_dd1.dst?
ignore_dst = true
utc_to_est_time = utc_dd1.in_time_zone(est_zone.name)
if utc_to_est_time.dst? && !!ignore_dst
utc_to_est_time = ignore_dst_in_given_time(utc_to_est_time)
end
puts utc_to_est_time
Hope this helps.
This worked well for me
date = '23/11/2020'
time = '08:00'
h, m = time.split(':')
timezone = 'Europe/London'
date.to_datetime.in_time_zone(timezone).change(hour: h, min: m)
This changes the timezone to 'EST' without changing the time:
time = DateTime.current
Time.find_zone("EST").local(
time.year,
time.month,
time.day,
time.hour,
time.min,
time.sec,
)

Daylight Savings Time not calculated properly in Rails 2.3.5?

I have a very strange issue with Daylight Savings Time (DST) in my app. For some reason, whenever I receive a time from the table, it doesn't adjust itself for DST. For example, if I create a new Time in the console, in the appropriate time zone, write it to the database, and then try to retrieve it from the database, it comes back as one hour earlier.
Here's an example:
Here, we can see that using the console, creating a new Time at 15:00 EST is equal to 19:00 UTC (since adjusted for DST, which makes it -0400 instead of the usual -0500):
ruby-1.8.6-p114 > Time.zone
=> #<ActiveSupport::TimeZone:0x12b1b68 #name="UTC", #tzinfo=nil, #utc_offset=0>
ruby-1.8.6-p114 > Time.zone = "Eastern Time (US & Canada)"
=> "Eastern Time (US & Canada)"
ruby-1.8.6-p114 > Time.zone.parse("15:00")
=> Thu, 09 Sep 2010 15:00:00 EDT -04:00
ruby-1.8.6-p114 > Time.zone.parse("15:00").utc
=> Thu Sep 09 19:00:00 UTC 2010
ruby-1.8.6-p114 > Time.zone.parse("15:00").dst?
=> true
Now, I try to write that same time to the database, and retrieve it back:
ruby-1.8.6-p114 > b = Book.new
=> #<Book id: nil, return_time: nil, created_at: nil, updated_at: nil>
ruby-1.8.6-p114 > b.return_time = Time.zone.parse("15:00")
=> Thu, 09 Sep 2010 15:00:00 EDT -04:00
ruby-1.8.6-p114 > b.save
=> true
ruby-1.8.6-p114 > result = Book.find(:last).return_time
=> Sat Jan 01 19:00:00 UTC 2000
ruby-1.8.6-p114 > result.zone
=> "UTC"
ruby-1.8.6-p114 > result.in_time_zone
=> Sat, 01 Jan 2000 14:00:00 EST -05:00
ruby-1.8.6-p114 > result.dst?
=> false
My environment.rb has this:
config.time_zone = 'UTC'
And application_controller.rb has this:
before_filter :set_user_time_zone
def set_user_time_zone
if current_user
Time.zone = current_user.time_zone
else
Rails.logger.error '[Time.zone.now.to_s][ERROR]: Missing current_user from in set_user_time_zone!'
end
end
Any ideas as to what could be happening here and how to fix it? I've been at this for days now, so really, any help would be greatly appreciated!
Thank you very much.
It looks like you're only saving the time portion; note how the date part goes from Thu, 09 Sep 2010 to Sat, 01 Jan 2000. Since the DST calculation depends on the date, this is probably where you're losing the information. (January 1st is not in DST, so assuming that date, the DST calculation is correct). You probably need to save a DATETIME in the database, not just TIME.
I'm not sure what version of Rails you're using, but I had the same problem in 2.1.2. It seems the time zone library included just doesn't work half the year, but the developers didn't think it was worth switching; hopefully they've rethought that decision by now.
In any case, we ended up using the tzinfo_timezone plugin, which computes UTC offsets correctly during daylight savings time.

Resources