Project

General

Profile

Feature #16470

Issue with nanoseconds in Time#inspect

Added by andrykonchin (Andrew Konchin) 6 months ago. Updated 9 days ago.

Status:
Open
Priority:
Normal
Target version:
-
[ruby-core:96614]

Description

Ruby 2.7 added nanosecond representation to the return value of Time#inspect method.

Nanosecond is displayed as Rational as in the following example:

t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789)
t.inspect # => "2007-11-01 15:25:00 8483885939586761/68719476736000000 UTC"

The nanosecond value 8483885939586761/68719476736000000 can be expanded to 0.12345678900000001. This is different from the stored nanosecond:

t.nsec # => 123456789
t.strftime("%N") # => "123456789"

I assume it isn't expected, and will be fixed.


Related issues

Related to Ruby master - Bug #16445: Time#inspect shows a fractional numberClosedActions
Related to Ruby master - Feature #15958: Time#inspect with fracClosedmatz (Yukihiro Matsumoto)Actions
#1

Updated by znz (Kazuhiro NISHIYAMA) 6 months ago

  • Related to Bug #16445: Time#inspect shows a fractional number added

Updated by znz (Kazuhiro NISHIYAMA) 6 months ago

  • Assignee set to matz (Yukihiro Matsumoto)

Updated by Eregon (Benoit Daloze) 6 months ago

I'd guess it's partly due to the Float itself losing precision.
But indeed it's quite unpretty. Maybe we should use #rationalize since that's closer to the Float#inspect output?

There is a usability problem here, it's basically impossible to read the output of Time#inspect in such a case, even though the input was readable.

123456.789.to_r
# => (8483885939586761/68719476736)

123456.789.rationalize
# => (123456789/1000)

Time.utc(2007, 11, 1, 15, 25, 0, 123456.789).inspect
# => "2007-11-01 15:25:00 8483885939586761/68719476736000000 UTC"

This works as expected (using r for making it an exact Rational):

p Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r)
# => 2007-11-01 15:25:00.123456789 UTC

Updated by jeremyevans0 (Jeremy Evans) about 1 month ago

  • Backport deleted (2.5: UNKNOWN, 2.6: UNKNOWN)
  • ruby -v deleted (2.7)
  • Tracker changed from Bug to Feature

Even though this appears to be a bug, it's actually intentional behavior, introduced in 5208c431bef3240eb251f5da23723b324431a98e, and there are tests and specs for it. So changing this behavior would be a feature request.

I think the behavior of displaying the rational is less useful, and the vast majority of Ruby programmers would want:

Time.utc(2007, 11, 1, 15, 25, 0, 123456.789).inspect
# => "2007-11-01 15:25:00.123456789 UTC"

I've added a pull request to implement this: https://github.com/ruby/ruby/pull/3160

One disadvantage of the pull request's approach is that two Time objects that are not equal will have inspect output that is equal. An alternative approach that wouldn't have that issue would be to rationalize all float usecs given as input, as Eregon (Benoit Daloze) mentioned, but that could possibly have implications beyond inspect. With that approach, you would still end up with weird inspect output if providing a rational such as 8483885939586761/68719476736000000r as usec input.

#5

Updated by sawa (Tsuyoshi Sawada) about 1 month ago

  • Description updated (diff)

Updated by shyouhei (Shyouhei Urabe) 14 days ago

Note that the cryptic output does not happen for Time.now.utc.

Time.now.utc.inspect # => "2020-06-18 11:12:44.354669166 UTC"

Nor when you pass 123456.789r for Time.utc.

Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r) # => "2007-11-01 15:25:00.123456789 UTC"

So no, as Eregon (Benoit Daloze) pointed out, this is not an issue of Time#inspect. This is due to the OP is using 123456.789 for some reason not shown in the report. Time#inspect is just trying to express as much as it can.

Is this usage (passing Float instances instead of Rationals) major in some area? If so we might want to improve Time.utc's API, not Time#inspect.

#7

Updated by Eregon (Benoit Daloze) 14 days ago

Updated by jeremyevans0 (Jeremy Evans) 10 days ago

I've added an alternative approach, which uses Float#rationalize for all Float conversions in Time, except for Time.at (one test depends on Time.at(float).to_f == float): https://github.com/ruby/ruby/pull/3248

Updated by Eregon (Benoit Daloze) 10 days ago

One consideration here is performance.
For instance Time.at(Float) is quite slow, due to going through Rational, etc.
Could you measure the other methods you changed to see how they perform compared to before?

Updated by jeremyevans0 (Jeremy Evans) 10 days ago

Eregon (Benoit Daloze) wrote in #note-9:

One consideration here is performance.
For instance Time.at(Float) is quite slow, due to going through Rational, etc.
Could you measure the other methods you changed to see how they perform compared to before?

Well, Time converts Float to Rational both with and without the patch (the values are passed through num_exact, which is where the conversion takes place). It is just a question of whether to use to_r or rationalize for the conversion (neither can be considered more accurate, since floating point numbers are inexact). rationalize looks slightly faster, so this should slightly increase performance, not decrease it.

Here's an example with 2.7:

$ ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-openbsd]
$ ruby -r benchmark -ve 'eval "def a; #{"Time.utc(2007, 11, 1, 15, 25, 0, 123456.789.to_r);"*100000} end"; puts Benchmark.measure{a}'
  1.330000   0.030000   1.360000 (  1.426325)
$ ruby -r benchmark -ve 'eval "def a; #{"Time.utc(2007, 11, 1, 15, 25, 0, 123456.789.rationalize);"*100000} end"; puts Benchmark.measure{a}'
  1.230000   0.010000   1.240000 (  1.255797)
$ ruby -r benchmark -ve 'eval "def a; #{"Time.utc(2007, 11, 1, 15, 25, 0, 123456.789);"*100000} end"; puts Benchmark.measure{a}'
  1.350000   0.010000   1.360000 (  1.412875)

I tested with the patch and the test for the literal float was close to the rationalize value, not the to_r value. So the patch makes the code faster, not slower.

FWIW, it's significantly faster to pass a literal rational as opposed to converting a float to a rational or passing a literal float, both with and without the patch:

$ ruby -r benchmark -ve 'eval "def a; #{"Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r);"*100000} end"; puts Benchmark.measure{a}'
  0.350000   0.030000   0.380000 (  0.496827)

Updated by mame (Yusuke Endoh) 9 days ago

Interesting, I can reproduce the performance difference:

$ time ./miniruby -e '100000.times { Time.utc(2007, 11, 1, 15, 25, 0, 123456.789.to_r) }'

real    0m0.633s
user    0m0.622s
sys     0m0.011s

$ time ./miniruby -e '100000.times { Time.utc(2007, 11, 1, 15, 25, 0, 123456.789.rationalize) }'

real    0m0.607s
user    0m0.587s
sys     0m0.020s

Note that #to_r is 10 times faster than #rationalize:

$ time ./miniruby -e '100000.times { 123456.789.to_r }'

real    0m0.059s
user    0m0.039s
sys     0m0.020s

$ time ./miniruby -e '100000.times { 123456.789.rationalize }'

real    0m0.598s
user    0m0.587s
sys     0m0.010s

I think that this is because #to_r produces relatively big denominator, which makes addition slower during Time.utc calculation.

(I have no opinion about the Time.utc issue itself. Sorry.)

Also available in: Atom PDF