Ruby Time Object Converted from Float Doesn't Equal to Orignial Time Object

Ruby Time object converted from float doesn't equal to orignial Time object

IEEE 754 double (which is returned by to_f) is not accurate enough to represent the exact time.

t1 = Time.now
f1 = t1.to_f
t2 = Time.at(f1)

# they look the same
t1.inspect #=> '2013-09-09 23:46:08 +0200'
t2.inspect #=> '2013-09-09 23:46:08 +0200'

# but double does not have enough precision to be accurate to the nanosecond
t1.nsec #=> 827938306
t2.nsec #=> 827938318
# ^^

# so they are different
t1 == t2 #=> false

Do the following, to preserve the exact time:

t1 = Time.now
r1 = t1.to_r # value of time as a rational number
t2 = Time.at(r1)
t1 == t2 #=> true

Citation from Time.to_r:

This methods is intended to be used to get an accurate value
representing the nanoseconds since the Epoch. You can use this method
to convert time to another Epoch.

Float not equal after division followed by multiplication

Float isn't an exact number representation, as stated in the ruby docs:

Float objects represent inexact real numbers using the native architecture's double-precision floating point representation.

This not ruby fault, as floats can only be represented by a fixed number of bytes and therefor cannot store decimal numbers correctly.

Alternatively, you can use ruby Rational or BigDecimal

Its is also fairly common to use the money gem when dealing with currency and money conversion.

Time.now does not match the same Time as generated from Time.parse or Time.at

I'll take a shot at 'splaining what you're seeing. Here are several comparisons of a Time value, and what happens when converting between different formats. I'm not going to do any equality checks, because you'll be able to see whether a value should match simply by looking at it:

require 'time'                                         
t_now = Time.now # => 2013-04-04 20:10:17 -0700

That's the inspect output, which throws away a lot of information and precision. It's good enough for use by mortals.

t_now.to_f # => 1365131417.613106

That's the value the computer sees usually, with microseconds.

t_now.to_i # => 1365131417

That's the same time with microseconds all gone.

Time.at(t_now)      # => 2013-04-04 20:10:17 -0700
Time.at(t_now.to_f) # => 2013-04-04 20:10:17 -0700
Time.at(t_now.to_i) # => 2013-04-04 20:10:17 -0700
t_now.to_s # => "2013-04-04 20:10:17 -0700"

Normal inspect and to_s output won't show any difference in the precision as long as the integer portion of the value is intact.

Time.parse(t_now.to_s)      # => 2013-04-04 20:10:17 -0700
Time.parse(t_now.to_s).to_f # => 1365131417.0

Parsing loses the resolution, unless you present a value that contains the fractional time and define the parsing format so strptime knows what to do with it. The default parser formats are designed for general-use, not high-precision, so we have to use strftime instead of allowing to_s to have its way with the value, and strptime to know what all those numbers mean:

T_FORMAT = '%Y/%m/%d-%H:%M:%S.%N' # => "%Y/%m/%d-%H:%M:%S.%N"
t_now.strftime(T_FORMAT) # => "2013/04/04-20:10:17.613106000"
Time.strptime(t_now.strftime(T_FORMAT), T_FORMAT).to_f # => 1365131417.613106

parsing string to time is not the same as the original time

The reason:

Since Ruby 1.9.2, Time implementation uses a signed 63 bit integer,
Bignum or Rational. The integer is a number of nanoseconds since the
Epoch which can represent 1823-11-12 to 2116-02-20. When Bignum or
Rational is used (before 1823, after 2116, under nanosecond), Time
works slower as when integer is used.

And if you run a = Time.now you got N seconds since the Epoch plus P nanoseconds. But when you run Time.parse b you got same number seconds but with zero nanosecond. This is what you're looking for.

Loading development environment (Rails 4.2.4)
[1] pry(main)> a = Time.now
=> 2015-10-14 23:41:12 +0300
[2] pry(main)> a.nsec
=> 733355000
[3] pry(main)> Time.parse(a.to_s).nsec
=> 0

So you have to avoid this nano stuff. For instance you can write something like that:

[16] pry(main)> a = Time.at(Time.now.to_i)
=> 2015-10-14 23:47:59 +0300
[17] pry(main)> a.nsec
=> 0

How to compare Time in ruby

Answer

t1 = Time.now
t2 = Time.at(t1.to_i)
t3 = Time.at(t1.to_i)
puts t1 # 2012-01-06 23:09:41 +0400
puts t2 # 2012-01-06 23:09:41 +0400
puts t3 # 2012-01-06 23:09:41 +0400

puts t1 == t2 # false
puts t1.equal?(t2) # false
puts t1.eql?(t2) # false

puts t2.equal? t3 # false
puts t2.eql? t3 # true
puts t2 == t3 # true

Explanation:

eql?(other_time)


Return true if time and other_time are both Time objects with the same seconds and fractional seconds.

Link: Time#eql?

So, apparently, fractions of seconds get dropped when performing #to_i and then restored time is not exactly the same as original one. But if we restore two copies, they will be equal.

One may think, "Hey, let's use #to_f then!". But, surprisingly enough, results are the same! Maybe it's because of rounding errors or floating point comparison, not sure.

Alternate answer

Don't convert integer back to time for comparison. Convert original time to int instead!

t1 = Time.now
t2 = Time.at(t1.to_i)

puts t1 # 2012-01-06 23:44:06 +0400
puts t2 # 2012-01-06 23:44:06 +0400

t1int, t2int = t1.to_i, t2.to_i

puts t1int == t2int # true
puts t1int.equal?(t2int.to_i) # true
puts t1int.eql?(t2int) # true

Ruby Time#to_json as a float

A straightforward approach is to monkey patch Time, although I'm not fond of doing that, especially to core classes

There's no JSON format for dates, as far as JSON cares they're just strings. Most languages understand ISO 8601, and that's what Time#to_json produces. So long as Time#to_json continues to produce an ISO 8601 datetime you'll remain backwards compatible.

require 'json'
require 'time' # for Time#iso8601 and Time.parse

class Time
def to_json
return self.iso8601(6).to_json
end
end

time = Time.at(1000.123456)
puts "Before: #{time.iso8601(6)}"

json_time = Time.at(1000.123456).to_json
puts "As JSON: #{json_time}"

# Demonstrate round tripping.
puts "Round trip: #{Time.parse(JSON.parse(json_time)).iso8601(6)}"
Before: 1969-12-31T16:16:40.123456-08:00
As JSON: "1969-12-31T16:16:40.123456-08:00"
Round trip: 1969-12-31T16:16:40.123456-08:00

If you're not comfortable with monkey patching globally, you can monkey patch in isolation by implementing around.

class Time
require 'time'
require 'json'

def precise_to_json(*args)
return iso8601(6).to_json(*args)
end

alias_method :original_to_json, :to_json
end

module PreciseJson
def self.around
# Swap in our precise_to_json as Time#to_json
Time.class_eval {
alias_method :to_json, :precise_to_json
}

# This block will use Time#precise_to_json as Time#to_json
yield

# Always put the original Time#to_json back.
ensure
Time.class_eval {
alias_method :to_json, :original_to_json
}
end
end

obj = {
time: Time.at(1000.123456),
string: "Basset Hounds Got Long Ears"
}

puts "Before: #{obj.to_json}"

PreciseJson.around {
puts "Around: #{obj.to_json}"
}

puts "After: #{obj.to_json}"

begin
PreciseJson.around {
raise Exception
}
rescue Exception
end

puts "After exception: #{obj.to_json}"
Before: {"time":"1969-12-31 16:16:40 -0800","string":"Basset Hounds Got Long Ears"}
Around: {"time":"1969-12-31T16:16:40.123456-08:00","string":"Basset Hounds Got Long Ears"}
After: {"time":"1969-12-31 16:16:40 -0800","string":"Basset Hounds Got Long Ears"}
After exception: {"time":"1969-12-31 16:16:40 -0800","string":"Basset Hounds Got Long Ears"}

Can't iterate over Time objects in Ruby

Exception

@fivedigit has explained why the exception was raised.

Other problems

You need any? where you have each:

appointment_times = []
#=> []
appointment = 4
#=> 4
conflicts = [(1..3), (5..7)]
#=> [1..3, 5..7]

appointment_times << 5 unless conflicts.each { |r| r.cover?(appointment) }
#=> nil
appointment_times
#=> []

appointment_times << 5 unless conflicts.any? { |r| r.include?(appointment) }
#=> [5]
appointment_times
#=> [5]

I suggest you covert appointment_time to a Time object, make conflicts and array of elements [start_time, end_time] and then compare appointment_time to the endpoints:

...unless conflicts.any?{ |start_time, end_time|
start_time <= appointment_time && appointment_time <= end_time }

Aside: Range#include? only looks at endpoints (as Range#cover? does) when the endpoints are "numeric". Range#include? need only look at endpoints when they are Time objects, but I don't know if Ruby regards Time objects as "numeric". I guess one could look at the source code. Anybody know offhand?

Alternative approach

I would like to suggest a different way to implement your method. I will do so with an example.

Suppose appointments were in blocks of 15 minutes, with the first block being 10:00am-10:15am and the last 4:45pm-5:00pm. (blocks could be shorter, of course, as small as 1 second in duration.)

Let 10:00am-10:15am be block 0, 10:15am-10:30am be block 1, and so on, until block 27, 4:45pm-5:00pm.

Next, express conflicts as an array of block ranges, given by [start, end]. Suppose there were appointments at:

10:45am-11:30am (blocks 3, 4 and 5)
1:00pm- 1:30pm (blocks 12 and 13)
2:15pm- 3:30pm (blocks 17, 18 and 19)

Then:

conflicts = [[3,5], [12,13], [17,19]]

You must write a method reserved_blocks(appointment_date) that returns conflicts.

The remaining code is as follows:

BLOCKS = 28
MINUTES = ["00", "15", "30", "45"]
BLOCK_TO_TIME = (BLOCKS-1).times.map { |i|
"#{i<12 ? 10+i/4 : (i-8)/4}:#{MINUTES[i%4]}#{i<8 ? 'am' : 'pm'}" }
#=> ["10:00am", "10:15am", "10:30am", "10:45am",
# "11:00am", "11:15am", "11:30am", "11:45am",
# "12:00pm", "12:15pm", "12:30pm", "12:45pm",
# "1:00pm", "1:15pm", "1:30pm", "1:45pm",
# "2:00pm", "2:15pm", "2:30pm", "2:45pm",
# "3:00pm", "3:15pm", "3:30pm", "3:45pm",
# "4:00pm", "4:15pm", "4:30pm", "4:45pm"]

def available_times(appointment_date)
available = [*(0..BLOCKS-1)]-reserved_blocks(appointment_date)
.flat_map { |s,e| (s..e).to_a }
last = -2 # any value will do, can even remove statement
test = false
available.chunk { |b| (test=!test) if b > last+1; last = b; test }
.map { |_,a| [BLOCK_TO_TIME[a.first],
(a.last < BLOCKS-1) ? BLOCK_TO_TIME[a.last+1] : "5:00pm"] }
end

def reserved_blocks(date) # stub for demonstration.
[[3,5], [12,13], [17,19]]
end

Let's see what we get:

available_times("anything") 
#=> [["10:00am", "10:45am"],
# ["11:30am", "1:00pm"],
# [ "1:45pm", "2:15pm"],
# [ "3:00pm", "5:00pm"]]

Explanation

Here is what's happening:

appointment_date = "anything" # dummy for demonstration

all_blocks = [*(0..BLOCKS-1)]
#=> [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
# 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]
reserved_ranges = reserved_blocks(appointment_date)
#=> [[3, 5], [12, 13], [17, 19]]
reserved = reserved_ranges.flat_map { |s,e| (s..e).to_a }
#=> [3, 4, 5, 12, 13, 17, 18, 19]
available = ALL_BLOCKS - reserved
#=> [0, 1, 2, 6, 7, 8, 9, 10, 11, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27]

last = -2
test = false
enum1 = available.chunk { |b| (test=!test) if b > last+1; last = b; test }
#=> #<Enumerator: #<Enumerator::Generator:0x00000103063570>:each>

We can convert it to an array to see what values it would pass into the block if map did not follow:

enum1.to_a
#=> [[true, [0, 1, 2]],
# [false, [6, 7, 8, 9, 10, 11]],
# [true, [14, 15, 16]],
# [false, [20, 21, 22, 23, 24, 25, 26, 27]]]

Enumerable#chunk groups consecutive values of the enumerator. It does so by grouping on the value of test and flipping its value between true and false whenever a non-consecutive value is encountered.

enum2 = enum1.map
#=> #<Enumerator: #<Enumerator: (cont.)
#<Enumerator::Generator:0x00000103063570>:each>:map>

enum2.to_a
#=> [[true, [0, 1, 2]],
# [false, [6, 7, 8, 9, 10, 11]],
# [true, [14, 15, 16]],
# [false, [20, 21, 22, 23, 24, 25, 26, 27]]]

You might think of enum2 as a "compound" enumerator.

Lastly, we convert the second element of each value of enum2 that is passed into the block (the block variable a, which equals [0,1,2] for the first element passed) to a range expressed as a 12-hour time. The first element of each value of enum2 (true or false) is not used, so so I've replaced its block variable with an underscore. This provides the desired result:

enum2.each { |_,a|[BLOCK_TO_TIME[a.first], \
(a.last < BLOCKS-1) ? BLOCK_TO_TIME[a.last+1] : "5:00pm"] }
#=> [["10:00am", "10:45am"],
# ["11:30am", "1:00pm"],
# [ "1:45pm", "2:15pm"],
# [ "3:00pm", "5:00pm"]]


Related Topics



Leave a reply



Submit