jeremyevans / home_run

Fast Date/DateTime classes for ruby :: Unmaintained, unnecessary on ruby 1.9.3+
Other
465 stars 10 forks source link

seconds round down unexpectedly when fractional days are added to DateTime #46

Closed chittoor closed 12 years ago

chittoor commented 12 years ago

Example: tested on 1.8.7 p358

>> dt = DateTime.parse("Wed Feb 29 20:00:05 -0500 2012")
=> Wed, 29 Feb 2012 20:00:05 -0500
>> ndt = dt + 35373468 / 86400.0
=> Sun, 14 Apr 2013 05:57:52 -0500
>> ndt.sec
=> 52
>> ndt.sec_fraction
=> 1.15740740509259e-05
>> 
?> t = Time.parse("Wed Feb 29 20:00:05 -0500 2012")
=> Wed Feb 29 20:00:05 -0500 2012
>> nt = t + 35373468
=> Sun Apr 14 06:57:53 -0400 2013
>> nt.sec
=> 53
>> nt.usec
=> 0
>> 
?> ndt.sec_fraction * 86400
=> 0.999999998

This is not a remote/edge case issue. It's quite easy to catch. We have internally added a hack to the functions rhrdt__nanos_to_hms and rhrdt_sec_fraction to round the seconds up if it's close enough (100 ns). Ugly but seems to work without accidentally rounding off on DateTime.now and others which generate fractional seconds.

Please note that the floating point arith involved is still pretty accurate at this range.

jeremyevans commented 12 years ago

This is expected behavior. You should be able to get more accuracy by adding an integer value and then adding the floating point value:

dt + 35373468 / 86400 + (35373468 % 86400)/86400.0
chittoor commented 12 years ago

Was expecting you'd say that :) Closing this. Btw there is a big time gotcha when mathn is involved though. Catches almost everyone by surprise.

>> require 'mathn'
=> true 
>> (35373468 / 86400).class
=> Rational
>> dt + 35373468 / 86400 + (35373468 % 86400)/86400.0
=> Sun, 14 Apr 2013 15:55:41 -0500
>> 
?> 
?> dt + (35373468 / 86400).to_i + (35373468 % 86400)/86400.0
=> Sun, 14 Apr 2013 05:57:53 -0500

http://bugs.ruby-lang.org/issues/2121 :)

jeremyevans commented 12 years ago

Just for clarification, home_run operates similarly to the stdlib (both the <= 1.9.2 pure ruby code and the >=1.9.3 switch_hitter C extension):

$ ruby18 -v -r date -S irb
ruby 1.8.7 (2012-02-08 patchlevel 358) [x86_64-openbsd]
irb(main):001:0> dt = DateTime.parse("Wed Feb 29 20:00:05 -0500 2012")
=> #<DateTime 2012-02-29T20:00:05-05:00>
irb(main):002:0> ndt = dt + 35373468 / 86400.0
=> #<DateTime 2013-04-14T05:57:52-05:00>
irb(main):003:0> ndt.sec
=> 52
irb(main):004:0> ndt.sec_fraction
=> 1.15740740509259e-05
irb(main):005:0> ndt.sec_fraction * 86400
=> 0.999999998
$ ruby19 -v -r date -S irb
ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-openbsd]
irb(main):001:0> dt = DateTime.parse("Wed Feb 29 20:00:05 -0500 2012")
=> #<DateTime: 2012-02-29T20:00:05-05:00 ((2455988j,3605s,0n),-18000s,2299161j)>
irb(main):002:0> ndt = dt + 35373468 / 86400.0
=> #<DateTime: 2013-04-14T05:57:52-05:00 ((2456397j,39472s,999999998n),-18000s,2299161j)>
irb(main):003:0> ndt.sec
=> 52
irb(main):004:0> ndt.sec_fraction
=> (499999999/500000000)
irb(main):005:0> ndt.sec_fraction * 86400
=> (13499999973/156250)

Of course, in the stdlib, you can use rationals for precision, which doesn't work well in home_run, as it will convert the rationals to floats.

jeremyevans commented 12 years ago

Actually, my mistake, in the <= 1.9.2 stdlib, you get:

$ ruby18 -I /usr/local/lib/ruby/1.8 -v -r date -S irb
ruby 1.8.7 (2012-02-08 patchlevel 358) [x86_64-openbsd]
irb(main):001:0> dt = DateTime.parse("Wed Feb 29 20:00:05 -0500 2012")
=> #<DateTime: 42439464721/17280,-5/24,2299161>
irb(main):002:0> ndt = dt + 35373468 / 86400.0
=> #<DateTime: 2456396.95686343,-5/24,2299161>
irb(main):003:0> ndt.sec
=> 53
irb(main):004:0> ndt.sec_fraction
=> 5.65692353918424e-11
irb(main):005:0> ndt.sec_fraction * 86400
=> 4.88758193785519e-06
irb(main):007:0> ndt.strftime('%N')
=> "000004887"

You can see it is over by 4887 nanoseconds, instead of being under by 2 nanoseconds. So both >= 1.9.3 stdlib and home_run have much better accuracy with floats than the <= 1.9.2 stdlib.