paupino/rust-decimal

`to_f64` panics when scale is large

wangxiaoying opened this issue · 3 comments

I have a decimal value 1e-130 and want to convert it into float using to_f64. But it result in panicked at 'attempt to divide by zero' at here. The reason is that since scale is 130, precision becomes 0 in this case.

I saw there is a comment saying that "scale is at most 28". What should we do if it is larger like 130? Which I think is within the valid range of float64 (please let me know if I understand incorrectly). Thanks!

Thank you for opening an issue! Do you have an isolated example that you provide to demonstrate this?

As a contrived example, this succeeds:

let value = Decimal::from_parts(1, 0, 0, false, 130);
assert_eq!(14, value.scale());
assert_eq!(Some(1e-14), value.to_f64());

Because we don't allow a scale above 28 - we effectively try to clamp it or error out. Most access calls error out however from_parts causes undefined behavior.

I'd be curious to what the state of the decimal object is before calling to_f64. My hunch is that the scale is out of bounds which is causing a non-panic in debug mode and wrapping.

Are you able to send the output of unpack before calling to_f64?

Hi @paupino , thanks for the reply!

Here is the result of unpack:

scale: 130 // val.scale()
precision: 0 // 10_u128.pow(val.scale())
unpack: UnpackedDecimal { negative: false, scale: 130, hi: 0, mid: 0, lo: 0 } // val.unpack()

Some more context

The decimal value I got is from postgres database with the db-postgres feature enabled. Here is a minimal example:

postgres setup:
CREATE TABLE test_table (key int, value numeric);
INSERT INTO test_table VALUES (0, 1.3), (1, 0.004), (2, 1e-20), (3, 1e-130), (4, 1e-200);
dependencies
postgres = {version = "0.19"}
rust_decimal = {version = "1", features = ["db-postgres"]}
test code
use postgres::{Client, NoTls};
use rust_decimal::prelude::*;

fn main() {
    let mut client = Client::connect(
        "host=localhost user=postgres password=postgres port=5432 dbname=tpch",
        NoTls,
    )
    .unwrap();

    let query = "select value from test_table2 where key = 3";

    for row in client.query(query, &[]).unwrap() {
        let val: Decimal = row.try_get(0).unwrap();
        let fval: f64 = val.to_f64().unwrap();
        println!("get float {:?}", fval);
    }
}

An interesting thing is that although the procedure is the same, the error of this example is attempt to multiply with overflow instead of attempt to divide by zero in my original project.

This is great thank you. I've issued a fix in the linked PR, however to note this will cause the behavior to return a floating point number of 0 for key 3. This is because we are losing precision by converting to Rust Decimal, which has a maximum precision of 28 at this point in time. Because 1e-130 is such a small number we effectively round this to zero when representing in rust decimal. If you need to retain this precision then unfortunately, you may need to look at another library at this point in time (e.g. bigdecimal).

Version 2 of this library may allow larger scales, however that is a limitation of this library at present (i.e. it helps us provide some fast optimizations across 3 words).