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
Differences Between Literals and Constructors? ([] VS Array.New and {} VS Hash.New)
Gem Install Mongrel Fails with Ruby 1.9.1
How to Run a Simple Ruby Script in Any Web Server (Apache or Mongrel or Any Thing Else)
Rake Db:Migration Not Working on Travis-Ci Build
How to Get List of All Countries and Cities in Rails
Rails: Difference Between Env.Fetch() and Env[]
Wicked-Pdf Not Showing Images, 'Wicked_Pdf_Image_Tag' Undefined
In Ruby What's the Difference Between Self.Method and a Method Within Class << Self
Ruby Amazon S3 Access Denied When Listing Buckets
Split Array Up into N-Groups of M Size
Rspec: Testing Assignment of Instance Variable
Custom Rails Configuration Section
Carrierwave How to Get the File Extension
Activeadmin Forbiddenattributeserror