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

parse_from_f64 is not precise #103

Closed
sanpii opened this issue Jul 6, 2023 · 4 comments
Closed

parse_from_f64 is not precise #103

sanpii opened this issue Jul 6, 2023 · 4 comments

Comments

@sanpii
Copy link

sanpii commented Jul 6, 2023

With the last release (0.4.0), this code no longer works:

fn main() {
    let from_f64 = bigdecimal::BigDecimal::try_from(20_000.000001_f64).unwrap();
    let from_text = "20_000.000001".parse().unwrap();

    assert_eq!(from_f64, from_text);
}
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `BigDecimal("20000.0000010000003385357558727264404296875")`,
 right: `BigDecimal("20000.000001")`', src/main.rs:5:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
@akubera
Copy link
Owner

akubera commented Jul 7, 2023

Yes, this is expected behavior now. The floating point value 20_000.000001_f64 cannot be "trusted" as a decimal value, as binary representations can only exactly represent numbers as sums of 2^-n (just how humans prefer decimal numbers, which only represent sums of 10^-n).

Putting 20000.000001 into https://baseconvert.com/ieee-754-floating-point confirms this is the correct result: 20000.0000010000003385357558727264404296875

Same with Python's Decimal library:

>>> from decimal import Decimal
>>> Decimal(20_000.000001)
Decimal('20000.0000010000003385357558727264404296875')

It's also true for 0.1:

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

And I'm sure this is standard behavior for decimal libraries.

This is actually the primary reason you use decimal libraries, as the floating point values we are shown are often lies

>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(0.1 + 0.2)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> 0.3 < 0.1+0.2
True

vs dealing with sums of negative-two powers:

>>> 2**-1 + 2**-5
0.53125
>>> Decimal(2**-1 + 2**-5)
Decimal('0.53125')

@akubera
Copy link
Owner

akubera commented Jul 7, 2023

This was the previous implementation, which wrote the given number to an allocated string in exponential form with fixed precision:

BigDecimal::from_str(
    &format!("{:.PRECISION$e}", n, PRECISION = ::std::f64::DIGITS as usize)
)

(std::f64::DIGITS == 15)

Which for your case would parse 2.000000000100000e4 (playground)

That implementation was replaced with a couple bit shifts and masks.

What specifically broke? Or was this just unexpected behavior?

@akubera
Copy link
Owner

akubera commented Jul 7, 2023

Apparently BigDecimal Ruby gem disallows building BigDecimal from float without specifying precision:
https://github.com/ruby/bigdecimal/blob/master/test/bigdecimal/test_bigdecimal.rb#L167

That doesn't really work for us if we want to keep the trait.

@sanpii
Copy link
Author

sanpii commented Jul 7, 2023

Ok, thank you for your explanation.

That breaks unit tests in my case, nothing critical.

@sanpii sanpii closed this as completed Jul 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants