Project

General

Profile

Feature #16470

Issue with nanoseconds in Time#inspect

Added by andrykonchin (Andrew Konchin) 11 months ago. Updated 4 months ago.

Status:
Closed
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
Related to Ruby master - Bug #17025: `Time#ceil` does not work like `Rational#ceil` or `Float#ceil`ClosedActions
#1

Updated by znz (Kazuhiro NISHIYAMA) 11 months ago

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

Updated by znz (Kazuhiro NISHIYAMA) 11 months ago

  • Assignee set to matz (Yukihiro Matsumoto)

Updated by Eregon (Benoit Daloze) 11 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) 6 months 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) 6 months ago

  • Description updated (diff)

Updated by shyouhei (Shyouhei Urabe) 6 months 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) 6 months ago

Updated by jeremyevans0 (Jeremy Evans) 5 months 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) 5 months 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) 5 months 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) 5 months 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.)

Updated by mame (Yusuke Endoh) 5 months ago

We need to understand the use case precisely. Does OP want to pass a general Float value to Time.utc? Or does he just want to specify nanosecond?

I think of no practical use case for the former (passing a general/calculated Float). If we need to specify nanosecond, I don't think that Float is a good API for that. Time.utc(2007, 11, 1, 15, 25, 0, nanosecond: 123456789) or something is better.

In addition, the following point is wrong.

The nanosecond value 8483885939586761/68719476736000000 can be expanded to 0.12345678900000001.

A correct expansion is 0.123456789000000004307366907596588134765625. So, which is better?

t.inspect # => "2007-11-01 15:25:00 8483885939586761/68719476736000000 UTC"
t.inspect # => "2007-11-01 15:25:00.123456789000000004307366907596588134765625 UTC"

Personally, I prefer the latter to the former because decimal is much easier to understand.

However, I'm afraid if the expansion might be very long. Truncation is a possible option, of course. But, it will break the original reason why the fraction part is added in Time#inspect (#15958):

But recently we encounters some troubles the comparison of Time objects whose frac parts are different.

So, naive truncation may bring the same troubles again. I guess nanosecond (nine digits after the decimal point) would be enough in many use cases, but I'm not 100% sure.

Updated by jeremyevans0 (Jeremy Evans) 5 months ago

mame (Yusuke Endoh) wrote in #note-12:

I guess nanosecond (nine digits after the decimal point) would be enough in many use cases, but I'm not 100% sure.

I don't think support for fractional nanoseconds is important. Does anyone have a use case for fractional nanoseconds? If not, I think it would be best to change Time to just store nanoseconds as integer instead of a rational. However, that's definitely a more involved change.

Updated by Eregon (Benoit Daloze) 5 months ago

I also don't see much use for fractional nanoseconds.
clock_gettime() never has higher resolution than nanoseconds.

In fact both JRuby and TruffleRuby always round to an integer number of nanonseconds, because java.time supports nanoseconds but not more fine-grained than that (since nothing could provide such precision).
https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html

#15

Updated by jeremyevans0 (Jeremy Evans) 5 months ago

  • Related to Bug #17025: `Time#ceil` does not work like `Rational#ceil` or `Float#ceil` added

Updated by akr (Akira Tanaka) 4 months ago

There are several examples time needs more than nanoseconds.

Updated by akr (Akira Tanaka) 4 months ago

  • Status changed from Open to Feedback

As others already pointed,
the original description of this issue misunderstand the actual Ruby behavior.

Time.utc(2007, 11, 1, 15, 25, 0, 123456.789) creates a Time object which has
0.123456789000000004307366907596588134765625 as a fractional second
because the exact value of float 123456.789 is
123456.789000000004307366907596588134765625.

I think the description "This is different from the stored nanosecond: ..." is based on misunderstanding of actual Ruby behavior.

So, it is difficult to determine actual problem.
We'd like to hear.

Updated by jeremyevans0 (Jeremy Evans) 4 months ago

akr (Akira Tanaka) wrote in #note-16:

There are several examples time needs more than nanoseconds.

SQLite ignores values after the millisecond when converting:

sqlite> SELECT CAST(strftime('%f', '2020-10-20 11:12:13.1237') AS NUMERIC);
13.124

This is a file archive format. How many filesystems have greater than nanosecond resolution?

Similarly, what camera supports greater than nanosecond resolution?

These seem to be better reasons to support sub-nanosecond resolution. I think either storing picoseconds or storing sec fraction as 64-bit integer are better approaches than storing a rational. However, either change would be very invasive, and it seems unlikely to be worth the effort.

As rational is used internally, it seems reasonably for inspect output to include rational. If a user wants to specific nanosecond, they should provide a rational instead of a float. So I think this feature request can be closed.

Note that sub-nanosecond resolution should be considered Ruby-implementation-specific behavior, since JRuby and TruffleRuby support nanosecond, and mruby supports microsecond.

#19

Updated by jeremyevans0 (Jeremy Evans) 4 months ago

  • Status changed from Feedback to Closed

Also available in: Atom PDF