Repository: paupino/rust-decimal (Rust)

Issue:
Loss of precision with the `f64` conversion
Hi. I've been investigating a bug today about a loss of precision, and I pointed it now to the mantissa algorithm in `Decimal`'s `to_f64`. This is the code that fails in our system, and I'd like to know is the loss of precision expected or can it be a bug?

```rust
use num::ToPrimitive;
use rust_decimal::prelude::*;

fn main() -> anyhow::Result<()> {
    let s = "1.59283191";
    let d = Decimal::from_str(s)?;
    assert_eq!(s.parse::<f64>()?, d.to_f64().unwrap());

    Ok(())
}
```

What I can do in our case is to just go from string to float, keeping the precision and our users happy. This will not be as performant though, so I'm interested is the mantissa algorithm supposed to lose some precision, or could we maybe fix it?

```sh
> uname -a
Linux lyndon 5.8.7-arch1-1 #1 SMP PREEMPT Sat, 05 Sep 2020 12:31:32 +0000 x86_64 GNU/Linux
> rustc --version
rustc 1.46.0 (04488afe3 2020-08-24)
```

rust_decimal version 1.7.0.

The PR: https://github.com/paupino/rust-decimal/pull/235

Ping @paupino @hengchu 


Discussion:
Hmm I think the loss of precision happens at this step:

https://github.com/paupino/rust-decimal/blob/916bf3d8153a0660690abad9af7a13672816d318/src/decimal.rs#L2663-L2664

This division converts to two `u128` into `f64` first, and then performs the division. There may be a better way to compute the fraction part, but it will probably involve some bit-level mangling of the actual f64 representation.

Do we know what algorithm rust's stdlib uses for parsing `String` into `f64`? I can try to take a look at that and see if it's possible to guarantee identical outputs between the two routes: `Decimal` -> `String` -> `f64` and `Decimal` -> `f64`.
That particular calculation actually looks like it's correct in my models here. The calculation causing the issue is this:

```rust
(integral_part as f64) + frac_f64
```

In fact, the result can be replicated by:
```rust
    let i = 1_u128;
    let j = 0.59283191_f64;
    println!("{}", (i as f64) + j);
```

That being said, as you mention - we could potentially leave the conversion to `f64` till after ALL calculations are done? I'll play with a couple of options and see if anything sticks.


In fact, this is not even a result of the `f64` conversion. The following will also give the same result:

```rust
    let i = 1_f64;
    let j = 0.59283191_f64;
    println!("{}", i + j);
```
outputs `1.5928319100000001`



Patch:
diff --git a/src/decimal.rs b/src/decimal.rs
index a50ab59d..fa8e804c 100644
--- a/src/decimal.rs
+++ b/src/decimal.rs
@@ -2662,7 +2662,9 @@ impl ToPrimitive for Decimal {
             let integral_part = mantissa / precision;
             let frac_part = mantissa % precision;
             let frac_f64 = (frac_part as f64) / (precision as f64);
-            Some(sign * ((integral_part as f64) + frac_f64))
+            let value = sign * ((integral_part as f64) + frac_f64);
+            let round_to = 10f64.powf(self.scale() as f64);
+            Some(value * round_to / round_to)
         }
     }
 }


Test patch:
diff --git a/tests/decimal_tests.rs b/tests/decimal_tests.rs
index da310b3c..fb83989e 100644
--- a/tests/decimal_tests.rs
+++ b/tests/decimal_tests.rs
@@ -1101,39 +1101,29 @@ fn it_can_go_from_and_into() {
 
 #[test]
 fn it_converts_to_f64() {
-    assert_eq!(5f64, Decimal::from_str("5").unwrap().to_f64().unwrap());
-    assert_eq!(-5f64, Decimal::from_str("-5").unwrap().to_f64().unwrap());
-    assert_eq!(0.1f64, Decimal::from_str("0.1").unwrap().to_f64().unwrap());
-    assert_eq!(0f64, Decimal::from_str("0.0").unwrap().to_f64().unwrap());
-    assert_eq!(0f64, Decimal::from_str("-0.0").unwrap().to_f64().unwrap());
-    assert_eq!(
-        0.25e-11f64,
-        Decimal::from_str("0.0000000000025").unwrap().to_f64().unwrap(),
-    );
-    assert_eq!(
-        1e6f64,
-        Decimal::from_str("1000000.0000000000025").unwrap().to_f64().unwrap()
-    );
-    assert_eq!(
-        0.25e-25_f64,
-        Decimal::from_str("0.000000000000000000000000025")
-            .unwrap()
-            .to_f64()
-            .unwrap(),
-    );
-    assert_eq!(
-        2.1234567890123456789012345678_f64,
-        Decimal::from_str("2.1234567890123456789012345678")
-            .unwrap()
-            .to_f64()
-            .unwrap(),
-    );
-
-    assert_eq!(
-        None,
-        // Cannot be represented in an f64
-        Decimal::from_str("21234567890123456789012345678").unwrap().to_f64(),
-    );
+    let tests = &[
+        ("5", Some(5f64)),
+        ("-5", Some(-5f64)),
+        ("0.1", Some(0.1f64)),
+        ("0.0", Some(0f64)),
+        ("-0.0", Some(0f64)),
+        ("0.0000000000025", Some(0.25e-11f64)),
+        ("1000000.0000000000025", Some(1e6f64)),
+        ("0.000000000000000000000000025", Some(0.25e-25_f64)),
+        (
+            "2.1234567890123456789012345678",
+            Some(2.1234567890123456789012345678_f64),
+        ),
+        (
+            "21234567890123456789012345678",
+            None, // Cannot be represented in an f64
+        ),
+        ("1.59283191", Some(1.59283191_f64)),
+    ];
+    for &(value, expected) in tests {
+        let value = Decimal::from_str(value).unwrap().to_f64();
+        assert_eq!(expected, value);
+    }
 }
 
 #[test]