Calculating the fractional part of a number inside a `serde_json::Value::Number`

70 views Asked by At

My goal is to take a serde_json::Value::Number and if the number it contains is represented as a non-integral value with a fractional part of 0.0, convert it into a serde_json::Value::Number containing the equivalent integer representation.

I have the following snippet that attempts this:

use serde_json::{Number, Value};

fn trim_trailing_zeros(val: Value) -> Value {
    match val {
        Value::Number(n) => Value::Number({
            match n.as_f64() {
                Some(m) => {
                    if m.fract() == 0.0 {
                        Number::from(m as i64)
                    } else {
                        n
                    }
                }
                _ => n,
            }
        }),
        var => var,
    }
}

The problem with this is if the number can't be accurately converted to an f64 as this Rust Playground link shows, the output is not equal to the input. This seems insurmountable because the field of a Number is private, so I can only use these few methods, and one that seems unavoidable is as_f64. The others seem to not be of use at all in this problem.

Can anyone think of another way to do this? Is there some way I can convert a Value::Number into a string and try to parse it without loss of accuracy?

Edit: As pointed out in the comments, first checking that the Number contains an i64 or f64 with is_i64() or is_f64() before attempting an as_f64() also could theoretically run into conversion issues.

2

There are 2 answers

0
PitaJ On BEST ANSWER

One way to achieve this is using the arbitrary_precision feature of serde_json. That feature stores the number as a string, and then you can do the parsing yourself:

[dependencies]
serde_json = { version = "*", features = ["arbitrary_precision"] }
Value::Number(n) => Value::Number({
    if let Some((whole, fract)) = n.as_str().split_once('.') {
        if fract.as_bytes().iter().all(|&b| b == b'0') {
            let i: i64 = whole.parse().unwrap();
            Number::from(i)
        } else {
            n
        }
    } else {
        let i: i64 = n.as_str().parse().unwrap();
        Number::from(i)
    }
}),

playground

1
possum On

If you want to deserialize a floating point number without loss you should consider using rust_decimal::Decimal. You didn't provide a lot of code, but for your object you can do something like

struct Item {
    name: String,
    price: Decimal,
}

and then from

let data = r#"
        {
            "name": "Apple",
            "price": "2.99"
        }"#;


let item: Item = serde_json::from_str(data)?;

You can then differentiate integers and non-integers with .is_integer, pick apart the fraction parts and integral parts after the deserialization from that with .round and .fract() (or some of the others like floor and ceil depending on your rules)