Skip to content
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

Merged
merged 4 commits into from
Feb 28, 2018

Conversation

cben
Copy link
Contributor

@cben cben commented Feb 7, 2018

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.

  • Changed all fractions in test to "dirty" ones, failing on current implementation.
  • Changed implementation to run Float("87e-3") instead of Float(87) * 1e-3.
    Seems to behave well. Will break with too many decimal digits, around 18.
  • Added inherently safe .decimal_si_to_bigdecimal.
    • Returned decimal precicion will depend on input, but is guaranteed to preserve input digits.

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.

@miq-bot
Copy link
Member

miq-bot commented Feb 7, 2018

@cben Cannot apply the following label because they are not recognized: gaprindashvili/yes

@miq-bot miq-bot added the bug label Feb 7, 2018
@cben
Copy link
Contributor Author

cben commented Feb 11, 2018

@enoodle @yaacov Please review

Copy link
Member

@yaacov yaacov left a 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)

@cben
Copy link
Contributor Author

cben commented Feb 11, 2018 via email

Copy link

@enoodle enoodle left a 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?

@cben
Copy link
Contributor Author

cben commented Feb 11, 2018 via email

@zeari
Copy link

zeari commented Feb 12, 2018

👍 LGTM

@cben
Copy link
Contributor Author

cben commented Feb 13, 2018

@bdunne @Fryguy please review.


def decimal_si_to_bigdecimal
multiplier = DECIMAL_SUFFIXES[self[-1]]
if multiplier
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

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"))
Copy link
Member

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)
@cben
Copy link
Contributor Author

cben commented Feb 19, 2018

@bdunne @Fryguy PTAL.
This is needed for ManageIQ/manageiq-providers-kubernetes#198 & ManageIQ/manageiq#16722.

This is a normal gem, needs a release and then bump dependency in manageiq, right?
How does this work for gaprindashvili backport? I see gaprindashvili depends on latest version (3.5) so I suppose I can just bump it equally.

multiplier = DECIMAL_SUFFIXES[self[-1]]
if multiplier
Float(self[0..-2]) * multiplier
self[0..-2] + multiplier
Copy link
Member

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}"

Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@Fryguy
Copy link
Member

Fryguy commented Feb 26, 2018

Changed all fractions in test to "dirty" ones, failing on current implementation.

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.

@Fryguy
Copy link
Member

Fryguy commented Feb 26, 2018

This is a normal gem, needs a release and then bump dependency in manageiq, right?
How does this work for gaprindashvili backport? I see gaprindashvili depends on latest version (3.5) so I suppose I can just bump it equally.

Yes, we just bump and release this normally, and then in ManageIQ you would just update the dependency.

@miq-bot
Copy link
Member

miq-bot commented Feb 26, 2018

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
2 files checked, 2 offenses detected

spec/core_ext/string/decimal_suffix_spec.rb

@cben
Copy link
Contributor Author

cben commented Feb 26, 2018

The 7 "dirty" fractions fail the old implementation and pass the new one on:

  • Intel i7-5600U, ruby 2.0.0-p648
  • Intel i7-5600U, ruby 2.3.4
  • Intel i7-5600U, ruby 2.4.1
  • Intel i7-5600U, ruby 2.5.0-dev.
  • ARMv7 BCM2835 (raspberry pi 3 😁), ruby 2.1.5p273 [arm-linux-gnueabihf].

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...

@bdunne bdunne merged commit 02695be into ManageIQ:master Feb 28, 2018
@bdunne bdunne self-assigned this Feb 28, 2018
bdunne added a commit that referenced this pull request Mar 1, 2018
Added
- String#decimal_si_to_big_decimal [[#59](#59)]
@bdunne
Copy link
Member

bdunne commented Mar 1, 2018

@cben v3.6.0 has been released with your changes.

@cben
Copy link
Contributor Author

cben commented Mar 4, 2018

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants