Two equal Time objects are not equal according to == - ruby

I've found some behaviour of Time in Ruby that I don't understand while writing some tests. Am I missing something or is this a genuine problem?
I can reproduce the situation in irb as follows - first create a Time and add 30 seconds to it:
t = Time.new(2007,01,15,11,15,30.1)
# => 2007-01-15 11:15:30 +0000
t1 = t + 30
# => 2007-01-15 11:16:00 +0000
Then create another time which should be equal to t1:
t2 = Time.new(2007,01,15,11,16,0.1)
# => 2007-01-15 11:16:00 +0000
Now I'd expect t1 and t2 to be equal, but they aren't according to ==. From the rough experiments I've done it seems like == works, except when the addition of seconds moves t1 onto a new minute:
t1 == t2
# => false
However if you call to_f on them then == does return true:
t1.to_f == t2.to_f
# => true
Just to confirm that there aren't any nano seconds hanging around:
t1.nsec
# => 100000000
t2.nsec
# => 100000000
=== Added after answers from joanbm and Vitalii Elenhaupt (sorry to open this up again)
joanbm and Vitalii Elenhaupt point out that t1.to_r and t2.to_r produce different results.
But... according to the Ruby-Doc Time within the normal range is stored as a 63 bit integer of the number of nano seconds since the epoch - which suggests that float-point issues shouldn't come into it.
So... why if Time is stored as an integer, and #to_f and #nsec can produce the same result to 9 decimal places, can't == use this info to recognize the two times as equal? (Maybe Ruby uses #to_r in the equality test?).
And... is it safe to assume that t1.to_f == t2.to_f will always give an accurate test of equality to 9 decimal places and is that the best way to compare Time objects?

This is the notorious problem with inaccurate representation of floating point numbers in computers, quite unrelated to Ruby or Time class implementation.
You give to both objects floating numbers as seconds argument, and its (inaccurate) fractional part constructor stores as a rational number for its internal representation:
t = Time.new(2007,01,15,11,15,30.1)
t.subsec # (14073748835533/140737488355328) <- may vary on your system
t.subsec.to_f # 0.10000000000000142 <- this is not equal to 0.1 !!
dtto for the 2nd time:
t2 = Time.new(2007,01,15,11,16,0.1)
t2.subsec # (3602879701896397/36028797018963968)
t2.subsec.to_f # 0.1 <- there accidentally precision didn't get lost
Just use exact numerical types like Rational and you are done:
t = Time.new(2007,01,15,11,15,Rational('30.1'))
t1 = t + 30
t2=Time.new(2007,01,15,11,16,0.1r) # alternate notation, see docs
t1 == t2 # true
Or do a rough comparison with Time#round method applied on both sides.

That happens because both times are still different. Here is a good illustration with Time#to_r:
>> t1.to_r #=> (164501373566169071275213/140737488355328)
>> t2.to_r #=> (42112351632939282246454477/36028797018963968)
If you don’t care about milliseconds, you may compare timestamps using to_i method:
>> t1.to_i == t2.to_i # => true
or make a new Time object from timestamps:
>> Time.at(t1.to_i) == Time.at(t2.to_i) # => true

You are using different parameters for your Time objects
#irb
t = Time.new(2007,01,15,11,15,30,0.1)
=> 2007-01-15 11:15:30 +0000
t1 = t2 + 30
=> 2007-01-15 11:16:00 +0000
if you do not pass the minutes, on my system the timezone seems to change:
t2 = Time.new(2007,01,15,11,16,0.1)
=> 2007-01-15 11:16:00 +0100 # what happened here? '+0100'
t1 == t2
=> false
Even if that is not the case for the op, it is interesting that this happens in my irb.
Building the objects like so Time.utc(2007,01,15,11,15,30,0.1), using an explicit time zone, returned exactly the behaviour you described.
However, if you pass the same amount of parameters (0 minutes for t2) the comparison will return the expected result as you will probably not even run into the floating point problem described by joanbm.
t = Time.new(2007,01,15,11,15,30,0.1)
=> 2007-01-15 11:15:30 +0000
t1 = t + 30
=> 2007-01-15 11:16:00 +0000
t2 = Time.new(2007,01,15,11,16,0,0.1)
=> 2007-01-15 11:16:00 +0000
t1 == t2
=> true
If you print the times rational form you get:
t1.to_r
=> (42112611033072059383231283/36028797018963968)
t2.to_r
=> (42112611033072059383231283/36028797018963968)
As to_r returns the time in seconds since the epoch (or Unix Time), there is no reason for t1 and t2 not to be equal, as long as date, time and time zone match.
Unfortunately I can not give a good explenation why this is the case.
This seems interesting though and I will come back when I find a reason for this behaviour.
Edit
This does not explain the timezone change, hence does not explain the problem, but your are missing a comma for t so you're actually passing 30.1 instead of 30 seconds.
t = Time.new(2007,01,15,11,15,30.1)
It probably should be
t = Time.new(2007,01,15,11,15,30,0.1)
However this does not cause your comparison to fail.

Related

Approximate comparison of 2 Ruby DateTimes

I have 2 DateTimes and I want to check if they're roughly 24 hours apart, plus or minus a small amount, say 5 minutes. Is there a built in way to do this?
There is not, but it is easy enough:
(-5 * 60 .. 5 * 60).include?((t2 - t1).abs - 24 * 3600)
"is the absolute difference between the two dates, when you subtract a full day, within plus or minus five minutes?"
Version 1: Works for both DateTime and Time. Converts everything to seconds.
def time_apart_within_drift?(t1, t2, diff: 24*60*60, drift: 5*60)
(t1.to_i - t2.to_i).abs <= diff + drift
end
As per the Ruby Style Guide, you should almost always use Time instead of DateTime. A Time object can still include the date, and it will make your calculations much cleaner.
This is because subtracting two DateTime objects gives you the difference in days as a Rational object, whereas subtracting two Time objects gives you the difference in seconds as a Float.
This allows you to write your function like so:
def time_apart_within_drift?(t1, t2, diff: 24*60*60, drift: 5*60)
(t1 - t2).abs <= diff + drift
end
# true
time_apart_within_drift?(Time.new(2020,3,5), Time.new(2020,3,4))
# true
time_apart_within_drift?(Time.new(2020,3,5), Time.new(2020,3,3,23,57))
# false
time_apart_within_drift?(Time.new(2020,3,5), Time.new(2020,3,3,23,47))
If using Time as well as rails and/or activesupport, the args can be made more readable using Duration objects:
def time_apart_within_drift?(t1, t2, diff: 24.hours, drift: 5.minutes)
(t1 - t2).abs <= diff + drift
end

Comparison using Ruby's .days.ago seems to use reverse logic

Can anyone help me make sense of this, please?
I am getting a very weird behaviour (reverse logic), when I am trying to use the following code.
require 'active_support/all'
c = {
id: 5,
years_of_experience: 4,
github_points: 293,
languages: ['C', 'Ruby', 'Python', 'Clojure'],
date_applied: 5.days.ago.to_date,
age: 26
}
c["date_applied"] > 15.days.ago.to_date - #works
c["date_applied"] < 15.days.ago.to_date - #doesnt work
c["date_applied"] gives a date value stored in a hash.
The latter makes more logical sense, but the first returns the right answer.
The code's behavior is correct, but I think I understand the confusion.
You're reading
c["date_applied"] > 15.days.ago
as:
Is the date applied more than 15 days ago?
and
c["date_applied"] < 15.days.ago
as:
Is the date applied less than 15 days ago?
and it's giving you the reverse of the answer you expect, right?
If that's the case, you should take a moment to understand how time comparisons operate. When you type date1 > date2, you're actually saying,
If I plot date1 and date2 on a number line with time increasing from left to right,
is date1 to the right of date2?
This is the same as when you type 2 > 1. It means,
If I plot 1 and 2 on a number line with the numbers increasing from left to right,
is 2 to the right of 1?
Given that this is how time comparisons operate, let's reexamine your code.
require 'active_support/all'
c = { date_applied: 5.days.ago.to_date }
c[:date_applied] > 15.days.ago.to_date
Correctly interpreted, this says
Is the date 5 days ago further rightward on a left-to-right timeline than the date 15 days ago?
and the answer is yes, or true.
If, on the other hand, you were to incorrectly interpret this as
Is 5 days ago more than 15 days ago?
you would get (or expect to get) the mistaken answer of no, or false.
The correct way to think about the task in English is to reframe the question of
Is date d more than n days ago?
and instead think of it as
Is date d earlier than the date n days ago?
and the correct code becomes apparent:
d.to_date < n.days.ago.to_date
If I understood your question correctly, this should explain it.
irb ## ruby-1.9.3-p448
require 'active_support/time'
c = {
id: 5,
years_of_experience: 4,
github_points: 293,
languages: ['C', 'Ruby', 'Python', 'Clojure'],
date_applied: 5.days.ago.to_date,
age: 26
}
(c[:date_applied] > 15.days.ago.to_date) - #true
(c[:date_applied] < 15.days.ago.to_date) - #false
###or you can try it by adding your own private methods###
class Fixnum
def days
self * 60 * 60 * 24 # we store seconds in a day
end
def ago
Time.now - self
end
end

Ruby - new_offset losing a second

I'm not sure if this behaviour is intended, but it seems a bit weird to me. I'm using the code from How do you get DateTime.parse to return a time in your time zone?
require 'date'
estHoursOffset = +10 # Brisbane/Australia
estOffset = Rational(estHoursOffset, 24)
With some times, the DateTime that's returned is a second earlier:
(DateTime.parse("2012-07-15 16:56:00") - (estHoursOffset/24.0)).new_offset(estOffset)
=> #<DateTime: 2012-07-15T16:55:59+10:00 (2456123.788888889,5/12,2299161)>
But with other times, it seems correct:
(DateTime.parse("2012-07-15 16:16:00") - (estHoursOffset/24.0)).new_offset(estOffset)
=> #<DateTime: 2012-07-15T16:16:00+10:00 (2456123.7611111114,5/12,2299161)>
The program I'm writing only cares about the minutes, which means I'm getting back 16:55 when I want 16:56.
So my questions are;
Is this intentional? (If so, is it documented somewhere - I haven't been able to find anything.)
Is there a simple way of fixing this programmatically? Since I don't care about seconds,I suppose I could "round up" the DateTimes returned, but it'd be good to know if this could bring up any other problems in edge cases.
This is probably because floating point numbers are imprecise - the 10/24.0 you are subtracting cannot be represented exactly.
If instead of subtracting that float you subtracted a rational, ie Rational(estHoursOffset, 24) then you should be ok
I tried both times ("2012-07-15 16:56:00" & "2012-07-15 16:16:00") and Ruby was always yielding the times parsed initially. I don't know mate how you managed to get 1 sec less; it is a miracle!! Only joking :)
If this still is giving you a hard time try getting the date (& time) - simpler like this..:
require 'date'
$date = Time.now #current date/time
puts $date
puts $date.min #if you want to use only the minutes
$date="2012-07-15 16:56:00" #if you want to parse it yourself
Moving on to your questions:
-No this is not international and it could be intermittent as well. I've tested your code above (+10h Australia) & from my location London, England (+1h). ALWAYS GOT the time parsed; never a second less or more.
Now if you need to round up the seconds so you will be 100% sure that each & every time you are getting the same results..:
def round_up(seconds)
divisor = 10**Math.log10(seconds).floor
i = seconds / divisor
remainder = seconds % divisor
if remainder == 0
i * divisor
else
(i + 1) * divisor
end
end
I cannot see why the rounding will cause problems in boundary conditions; as long as you always keep rounding everything! Hope this helps! Good luck mate :)

Get the count of elements in a ruby range made of Time objects

How would I be able to get the size or count of a range made up of Time objects?
Something that would achieve the same result as my pseudo Ruby code, which doesn't work:
((Time.now.end_of_day - 31.days)..(Time.now.end_of_day - 1.day)).size == 30
currently doing the above gives an error:
NoMethodError: undefined method `size' for 2012-05-18 23:59:59 -0400..2012-06-17 23:59:59 -0400:Range
and trying to turn it into array (range).to_a :
can't iterate from Time
update
Interesting, Just tried to do
((Date.today.end_of_day - 31.days)..(Date.today.end_of_day - 1.day)).count
Users/.../gems/ruby/1.9.1/gems/activesupport-3.0.15/lib/active_support/time_with_zone.rb:322: warning: Time#succ is obsolete; use time + 1
However
((Date.today - 31.days)..(Date.today - 1.day)).count == 31
I would be willing to settle for that?
Also ((Date.today - 31.days)..(Date.yesterday)).count == 31
update 2
On the other hand, taking Mu's hint we can do:
(((Time.now.end_of_day - 31.days)..(Time.now.end_of_day - 1.day)).first.to_date..((Time.now.end_of_day - 31.days)..(Time.now.end_of_day - 1.day)).last.to_date).count == 31
There's no such method as Range#size, try Range#count (as suggested by Andrew Marshall), though it still won't work for a range of Time objects.
If you want to perform number-of-days computations, you're better off using Date objects, either by instantiating them directly (Date.today - 31, for example), or by calling #to_date on your Time objects.
Date objects can be used for iteration too:
((Date.today - 2)..(Date.today)).to_a
=> [#<Date: 2012-06-17 ((2456096j,0s,0n),+0s,2299161j)>,
#<Date: 2012-06-18 ((2456097j,0s,0n),+0s,2299161j)>,
#<Date: 2012-06-19 ((2456098j,0s,0n),+0s,2299161j)>]
((Date.today - 2)..(Date.today)).map(&:to_s)
=> ["2012-06-17", "2012-06-18", "2012-06-19"]
It's because a size for a date range doesn't make sense—it doesn't know if you want to view it as days, minutes, seconds, months, or something else. The reason the error mentions iterating is that in order to determine the size of the range, it must know how to iterate over them so that it may count the number of elements.
Since what you want is the difference in days, just do that:
date_one = Time.now.end_of_day - 31.days
date_two = Time.now.end_of_day - 1.day
((date_one - date_two) / 1.day).abs
#=> 30.0
You must divide by 1.day since a difference of Times returns seconds.
To have any chance of your code working you should wrap everything before .size in parentheses.
Instead of using a range, maybe you can just subtract one time object from another?
I know you make ranges out of Date objects so you could convert to that.

How to time an operation in milliseconds in Ruby?

I'm wishing to figure out how many milliseconds a particular function uses. So I looked high and low, but could not find a way to get the time in Ruby with millisecond precision.
How do you do this? In most programming languages its just something like
start = now.milliseconds
myfunction()
end = now.milliseconds
time = end - start
You can use ruby's Time class. For example:
t1 = Time.now
# processing...
t2 = Time.now
delta = t2 - t1 # in seconds
Now, delta is a float object and you can get as fine grain a result as the class will provide.
You can also use the built-in Benchmark.measure function:
require "benchmark"
puts(Benchmark.measure { sleep 0.5 })
Prints:
0.000000 0.000000 0.000000 ( 0.501134)
Using Time.now (which returns the wall-clock time) as base-lines has a couple of issues which can result in unexpected behavior. This is caused by the fact that the wallclock time is subject to changes like inserted leap-seconds or time slewing to adjust the local time to a reference time.
If there is e.g. a leap second inserted during measurement, it will be off by a second. Similarly, depending on local system conditions, you might have to deal with daylight-saving-times, quicker or slower running clocks, or the clock even jumping back in time, resulting in a negative duration, and many other issues.
A solution to this issue is to use a different time of clock: a monotonic clock. This type of clock has different properties than the wall clock.
It increments monitonically, i.e. never goes back and increases at a constant rate. With that, it does not represent the wall-clock (i.e. the time you read from a clock on your wall) but a timestamp you can compare with a later timestamp to get a difference.
In Ruby, you can use such a timestamp with Process.clock_gettime(Process::CLOCK_MONOTONIC) like follows:
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# => 63988.576809828
sleep 1.5 # do some work
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# => 63990.08359163
delta = t2 - t1
# => 1.5067818019961123
delta_in_milliseconds = delta * 1000
# => 1506.7818019961123
The Process.clock_gettime method returns a timestamp as a float with fractional seconds. The actual number returned has no defined meaning (that you should rely on). However, you can be sure that the next call will return a larger number and by comparing the values, you can get the real time difference.
These attributes make the method a prime candidate for measuring time differences without seeing your program fail in the least opportune times (e.g. at midnight at New Year's Eve when there is another leap-second inserted).
The Process::CLOCK_MONOTONIC constant used here is available on all modern Linux, BSD, and macOS systems as well as the Linux Subsystem for Windows. It is however not yet available for "raw" Windows systems. There, you can use the GetTickCount64 system call instead of Process.clock_gettime which also returns a timer value in millisecond granularity on Windows (>= Windows Vista, Windows Server 2008).
With Ruby, you can call this function like this:
require 'fiddle'
# Get a reference to the function once
GetTickCount64 = Fiddle::Function.new(
Fiddle.dlopen('kernel32.dll')['GetTickCount64'],
[],
-Fiddle::TYPE_LONG_LONG # unsigned long long
)
timestamp = GetTickCount64.call / 1000.0
# => 63988.576809828
You should take a look at the benchmark module to perform benchmarks. However, as a quick and dirty timing method you can use something like this:
def time
now = Time.now.to_f
yield
endd = Time.now.to_f
endd - now
end
Note the use of Time.now.to_f, which unlike to_i, won't truncate to seconds.
Also we can create simple function to log any block of code:
def log_time
start_at = Time.now
yield if block_given?
execution_time = (Time.now - start_at).round(2)
puts "Execution time: #{execution_time}s"
end
log_time { sleep(2.545) } # Execution time: 2.55s
Use Time.now.to_f
The absolute_time gem is a drop-in replacement for Benchmark, but uses native instructions to be far more accurate.
If you use
date = Time.now.to_i
You're obtaining time in seconds, that is far from accurate, specially if you are timing little chunks of code.
The use of Time.now.to_i return the second passed from 1970/01/01. Knowing this you can do
date1 = Time.now.to_f
date2 = Time.now.to_f
diff = date2 - date1
With this you will have difference in second magnitude. If you want it in milliseconds, just add to the code
diff = diff * 1000
I've a gem which can profile your ruby method (instance or class) - https://github.com/igorkasyanchuk/benchmark_methods.
No more code like this:
t = Time.now
user.calculate_report
puts Time.now - t
Now you can do:
benchmark :calculate_report # in class
And just call your method
user.calculate_report

Resources