-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce rounding errors in .decimal_si_to_f, add safe .decimal_si_to_bigdecimal #59
Conversation
@cben Cannot apply the following label because they are not recognized: gaprindashvili/yes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice 👍
p.s.
don't you want to wait with the decimal_si_to_bigdecimal
until you want to use it ? (not a blocker)
I'm going to use it right away, for container quota history.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Nice test, is there anything special with 93?
93 is not very special. Some of these had differences with single digits like .3, but some didn't, so chose double digits everywhere for consistent look. The magnitude of the difference tends to bigger for higher fractions.
As you can see, I spent too much time on this :-)
|
👍 LGTM |
|
||
def decimal_si_to_bigdecimal | ||
multiplier = DECIMAL_SUFFIXES[self[-1]] | ||
if multiplier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this conditional be extracted to a new private method and shared between the two methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 done
expect("93f".decimal_si_to_f).to eq(0.000_000_000_000_093) | ||
expect("92p".decimal_si_to_f).to eq(0.000_000_000_092) | ||
expect("93n".decimal_si_to_f).to eq(0.000_000_093) | ||
expect("91μ".decimal_si_to_f).to eq(0.000_091) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason you're not using the same number for all of the tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted test cases that all failed with previous algorithm. This happens for different values semi-randomly, depending on how powers of 2 and 10 align...
[7] pry(main)> 93 * 1e-12
=> 9.3e-11
[8] pry(main)> 92 * 1e-12
=> 9.199999999999999e-11
[9] pry(main)> 92 * 1e-9
=> 9.2e-08
[10] pry(main)> 93 * 1e-9
=> 9.300000000000001e-08
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we change these tests back to the original format and add a new test verifying several values that used to fail for rounding errors or other issues? I see them as two separate issues and wouldn't want to lose this test because someone modified these expectations to consistently use the same number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a point having both old and new test, I view 1 as just a particularly easy special case (even changing 1 to 3 makes a couple fail).
I'm not sure if this is what you had in mind, but rewrote to test all input forms over all digit pairs. Total 192/700 failures on the fractional suffixes, all passing after 2nd commit.
PTAL.
expect("93f".decimal_si_to_bigdecimal).to eq(BigDecimal("0.000_000_000_000_093")) | ||
expect("92p".decimal_si_to_bigdecimal).to eq(BigDecimal("0.000_000_000_092")) | ||
expect("93n".decimal_si_to_bigdecimal).to eq(BigDecimal("0.000_000_093")) | ||
expect("91μ".decimal_si_to_bigdecimal).to eq(BigDecimal("0.000_091")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
For each fractional suffix, some i, j combinations fail current parsing via integer * factor, e.g. 87 * 0.001 == 0.08700000000000001 != 0.087. (total 192 failures from 700 fractions)
caf14fe
to
7abecb9
Compare
@bdunne @Fryguy PTAL. This is a normal gem, needs a release and then bump dependency in manageiq, right? |
multiplier = DECIMAL_SUFFIXES[self[-1]] | ||
if multiplier | ||
Float(self[0..-2]) * multiplier | ||
self[0..-2] + multiplier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming this is string concat, prefer "#{self[0..-2]}#{multiplier}"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternately self[0..-2] << multiplier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
The only thing concerning me with this is whether this is consistent across different people's machines or, more importantly, between versions of Ruby (i.e. as we upgrade). However, I'm not sure how we would handle that kind of testing anyway. |
Yes, we just bump and release this normally, and then in ManageIQ you would just update the dependency. |
Checked commits cben/more_core_extensions@f173bcf~...bd9acf7 with ruby 2.3.3, rubocop 0.52.0, haml-lint 0.20.0, and yamllint 1.10.0 spec/core_ext/string/decimal_suffix_spec.rb
|
The 7 "dirty" fractions fail the old implementation and pass the new one on:
CORRECTION: there are no longer 7 specific fractions, I'm testing 100 values at each scale. But the first failure at each scale is same between Intel and ARM — I guess IEEE did a good job... |
Thanks! |
Currently "87m".decimal_si_to_f == 0.08700000000000001 != 0.087, due to rounding error in multiplication. Many values do result in a "clean" float¹, but around 10%-20% are dirty like this. This PR tries to fix that, and also sidesteps the problem by adding an inherently safe
.decimal_si_to_bigdecimal
.¹ Remember that decimal fractions like 0.087 don't have a precise representation in binary floating point; however there is a close float that 0.087 parses to, that also prints back as exactly 0.087, and that's what I'm after — at least being able to print back inputs as we got them.
Float("87e-3")
instead ofFloat(87) * 1e-3
.Seems to behave well. Will break with too many decimal digits, around 18.
.decimal_si_to_bigdecimal
.https://bugzilla.redhat.com/show_bug.cgi?id=1504560
@miq-bot add-label bug, gaprindashvili/yes
The only use of this method in ManageIQ is for parsing fractional cpu in millicores.
My main motivation is quota history, see ManageIQ/manageiq-providers-kubernetes#208. While errors here don't affect quotas much after ManageIQ/manageiq-schema#151, because the DB would round small errors from parsing to closest millis, and the _to_f fix here should be enough, I prefer to switch to
.decimal_si_to_bigdecimal
for quotas.@zeari Please review.