Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Two equal Time objects are not equal according to ==

Tags:

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?

like image 748
djboardman Avatar asked Sep 27 '22 04:09

djboardman


1 Answers

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.

like image 141
joanbm Avatar answered Oct 11 '22 17:10

joanbm