Search code examples
rustserde-json

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


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.


Solution

  • 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