Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails/Ruby: TimeWithZone comparison inexplicably failing for equivalent values

I am having a terrible time (no pun intended) with DateTime comparison in my current project, specifically comparing two instances of ActiveSupport::TimeWithZone. The issue is that both my TimeWithZone instances have the same value, but all comparisons indicate they are different.

Pausing during execution for debugging (using RubyMine), I can see the following information:

timestamp = {ActiveSupport::TimeWithZone} 2014-08-01 10:33:36 UTC
started_at = {ActiveSupport::TimeWithZone} 2014-08-01 10:33:36 UTC

timestamp.inspect = "Fri, 01 Aug 2014 10:33:36 UTC +00:00"
started_at.inspect = "Fri, 01 Aug 2014 10:33:36 UTC +00:00"

Yet a comparison indicates the values are not equal:

timestamp <=> started_at = -1

The closest answer I found in searching (Comparison between two ActiveSupport::TimeWithZone objects fails) indicates the same issue here, and I tried the solutions that were applicable without any success (tried db:test:prepare and I don't run Spring).

Moreover, even if I try converting to explicit types, they still are not equivalent when comparing.

to_time:

timestamp.to_time = {Time} 2014-08-01 03:33:36 -0700
started_at.to_time = {Time} 2014-08-01 03:33:36 -0700

timestamp.to_time <=> started_at.to_time = -1

to_datetime:

timestamp.to_datetime = {Time} 2014-08-01 03:33:36 -0700
started_at.to_datetime = {Time} 2014-08-01 03:33:36 -0700

timestamp.to_datetime <=> started_at.to_datetime = -1    

The only "solution" I've found thus far is to convert both values using to_i, then compare, but that's extremely awkward to code everywhere I wish to do comparisons (and moreover, seems like it should be unnecessary):

timestamp.to_i = 1406889216
started_at.to_i = 1406889216

timestamp.to_i <=> started_at.to_i = 0

Any advice would be very much appreciated!

like image 882
GabeStah Avatar asked Aug 01 '14 10:08

GabeStah


2 Answers

Solved

As indicated by Jon Skeet above, the comparison was failing because of hidden millisecond differences in the times:

timestamp.strftime('%Y-%m-%d %H:%M:%S.%L') = "2014-08-02 10:23:17.000"
started_at.strftime('%Y-%m-%d %H:%M:%S.%L') = "2014-08-02 10:23:17.679"

This discovery led me down a strange path to finally discover what was ultimately causing the issue. It was a combination of this issue occurring only during testing and from using MySQL as my database.

The issues was showing only in testing because within the test where this cropped up, I'm running some tests against a couple of associated models that contain the above fields. One model's instance must be saved to the database during the test -- the model that houses the timestamp value. The other model, however, was performing the processing and thus is self-referencing the instance of itself that was created in the test code.

This led to the second culprit, which is the fact I'm using MySQL as the database, which when storing datetime values, does not store millisecond information (unlike, say, PostgreSQL).

Invariably, what this means is that the timestamp variable that was being read after its ActiveRecord was retrieved from the MySQL database was effectively being rounded and shaved of the millisecond data, while the started_at variable was simply retained in memory during testing and thus the original milliseconds were still present.

My own (sub-par) solution is to essentially force both models (rather than just one) in my test to retrieve themselves from the database.

TLDR; If at all possible, use PostgreSQL if you can!

like image 122
GabeStah Avatar answered Oct 12 '22 02:10

GabeStah


This seem to happen if you're comparing time generated in Ruby with time loaded from the database.

For example:

time = Time.zone.now
Record.create!(mark: time)
record = Record.last

In this case record.mark == time will fail because Ruby keeps time down to nanoseconds, while different databases have different precission.

In case of postgres DateTime type it'll be to miliseconds.

You can see that when you check that while record.mark.sec == time.msec - record.mark.nsec != time.nsec

like image 34
Marcin Raczkowski Avatar answered Oct 12 '22 01:10

Marcin Raczkowski