Two equal Time objects are not equal according to ==

I found some Time behavior in Ruby that I don't understand when writing some tests. Am I missing something or is this a real 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 would expect t1 and t2 be equal, but they do not match == . From the rude experiments I performed, it seems that == works, except when adding seconds moves t1 to a new minute:

 t1 == t2 # => false 

However, if you call to_f on them, then == returns true :

 t1.to_f == t2.to_f # => true 

Just to confirm there are no nano seconds around:

 t1.nsec # => 100000000 t2.nsec # => 100000000 

=== Added after replies from joanbm and Vitalii Elenhaupt (sorry to open it again)

joanbm and Vitaly Elenhaupt indicate that t1.to_r and t2.to_r give different results.

But ... according to Ruby-Doc Time , within the normal range, it is stored as a 63-bit integer nano seconds from the era - which suggests that floating point problems should not go into it.

So ... why, if Time is stored as an integer, and #to_f and #nsec can lead to the same result 9 decimal places, can't == use this information to recognize twice as equal? (Maybe Ruby uses #to_r in the equality test?).

And ... is it possible to assume that t1.to_f == t2.to_f will always give an exact criterion for equality up to nine decimal places and is the best way to compare Time objects?

+5
source share
3 answers

This is the notorious problem with the inaccurate representation of floating point numbers in computers, completely unrelated to the implementation of the Ruby or Time class.

You pass both objects floating numbers as an argument to seconds, and its (inaccurate) fractional constructor of parts 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 a second 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 numeric types like Rational , and you're 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 the Time#round method used on both sides.

+3
source

You use different parameters for 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 don’t miss the minutes, in my system the time zone will 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 this is not the case for op, it is interesting that this happens in my irb. Building such Time.utc(2007,01,15,11,15,30,0.1) objects Time.utc(2007,01,15,11,15,30,0.1) using an explicit time zone returned exactly the behavior that you described.

However, if you pass the same number of parameters (0 minutes for t2), the comparison will return the expected result, since you probably will not even encounter 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 a rational form of time, you get:

 t1.to_r => (42112611033072059383231283/36028797018963968) t2.to_r => (42112611033072059383231283/36028797018963968) 

As to_r returns the time in seconds with epoch (or Unix Time), there is no reason for t1 and t2 not to be equal if the date, time and time zone match.

Unfortunately, I cannot give a good explanation of why this is so. This seems interesting, although I will return when I find the reason for this behavior.

Edit

This does not explain the time zone change, therefore it does not explain the problem, but you do not have a comma for t , so you actually pass 30.1 instead of 30 seconds.

 t = Time.new(2007,01,15,11,15,30.1) 

It should probably be

 t = Time.new(2007,01,15,11,15,30,0.1) 

However, this does not fail your comparison.

0
source

This is 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 need milliseconds, you can compare timestamps using the to_i method:

 >> t1.to_i == t2.to_i # => true 

or create a new Time object from timestamps:

 >> Time.at(t1.to_i) == Time.at(t2.to_i) # => true 
0
source

All Articles