I just run into a weird issue when comparing dates in Rails. In years of development, I think it’s one of the few times I’ve run accross floating point precission issues.

For a syncing engine with an iOS app, I’m looking for records on a Rails app where their updated_at field is newer than a parameter I’m sending.

My original code, in the controller, looked like this:

def index
  if params[:since]
    timestamp = Time.zone.at(params[:since]))
    @products = Product.where("updated_at > ?", timestamp)
  else
    @products = Product.all
  end

  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @products }
  end
end

and I was calling this with the following URL: /products.json?since=1370362746.67886.

The date corresponded to the last updated_at date on the database, so it should have not returned any object. To my surprise, I was still getting an object. Here’s the irb session:

1.9.3p194 :001 > ts = Time.zone.at(1370362746.67886)
 => Tue, 04 Jun 2013 16:19:06 UTC +00:00 
1.9.3p194 :002 > Product.where("updated_at > ?", ts)
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE (updated_at > '2013-06-04 16:19:06.678859')
 => [#<Product id: 5, created_at: "2013-06-04 16:19:06", updated_at: "2013-06-04 16:19:06", name: "E", deleted: false, global_uuid: "B6ABA6EF-30FA-4728-A304-C39D232BBB51">] 
1.9.3p194 :003 > 

If you check the query that is being generated, you’ll see that the date is sent as 2013-06-04 16:19:06.678859, even though the original timestamp is 1370362746.67886.

What’s going on then? Of course, floating point precission issues!

Check this irb session:

1.9.3p194 :001 > Time.zone.at(1370362746.67886).to_r
 => (5747717949846129/4194304) 
1.9.3p194 :002 > Product.last.updated_at.to_f
  Product Load (0.2ms)  SELECT "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT 1
 => 1370362746.67886 
1.9.3p194 :003 > Product.last.updated_at.to_r
  Product Load (0.3ms)  SELECT "products".* FROM "products" ORDER BY "products"."id" DESC LIMIT 1
 => (68518137333943/50000) 
1.9.3p194 :004 > 

As you can see, the float representation of the last updated_at value is the same I’m sending. However, when using to_r to create a Rational, you can see the two numbers are slightly different.

Workaround

The workaround I settled for is to use Rational instead of floats when first converting the floating point representation to a date.

Here’s the current version of the code:

def index
  if params[:since]
    timestamp = Time.zone.at(Rational("#{params[:since]}"))
    @products = Product.where("updated_at > ?", timestamp)
  else
    @products = Product.all
  end

  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @products }
  end
end

And here’s an irb session showing it working as expected:

1.9.3p194 :001 > ts = Time.zone.at(Rational("1370362746.67886"))
 => Tue, 04 Jun 2013 16:19:06 UTC +00:00 
1.9.3p194 :002 > Product.where("updated_at > ?", ts)
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE (updated_at > '2013-06-04 16:19:06.678860')
 => [] 
1.9.3p194 :003 > 

It’s very important to note that the parameter you send to Rational has to be a String and not a number, otherwise it will be coalesced into a float and lose precision before creating the new Rational instance.