如何使用serde_json序列化包含f32的结构?

68bkxrlz  于 2022-11-26  发布在  其他
关注(0)|答案(2)|浏览(141)

我正在尝试进行一个API调用,它需要序列化JSON主体。
JSON主体包含一个order_amount键,其值只能采用INR格式为100.36的值,即卢比100和派塞36。更多示例为10.48、3.20、1.09。
我面临的问题是,在serde_json中使用json!()进行序列化之后,浮点值变成了类似于100.359765464332的值。
API随后失败,因为它期望order_amount只有两个小数位。
下面是我的代码:
进口

use lambda_runtime::{handler_fn, Context, Error};
use reqwest::header::ACCEPT;
use reqwest::{Response, StatusCode};
use serde_json::json;
use std::env;

#[macro_use]
extern crate serde_derive;

我正在序列化的结构体

#[derive(Serialize, Deserialize, Clone, Debug)]
struct OrderCreationEvent {
    order_amount: f32,
    customer_details: ...,
    order_meta: ...,
}

例如,此处order_amount的值为15.38

async fn so_my_function(
    e: OrderCreationEvent,
    _c: Context,
) -> std::result::Result<CustomOutput, Error> {
let resp: Response = client
        .post(url)
        .json::<serde_json::Value>(&json!(e))
        .send()
        .await?;

在json!()之后,金额被序列化为15.379345234542。我需要15.38
我读过一些关于为f32编写一个自定义序列化程序的文章,它可以截断到2个小数,但我的熟练程度在Rust方面是有限的。
所以,我发现了这个代码,并一直在修补它没有运气:

fn order_amount_serializer<S>(x: &f32, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.serialize_f32(*x)
    // Ok(f32::trunc(x * 100.0) / 100.0)
}

无论自定义序列化器是否是解决问题的正确方法或解决方案,我仍然想学习如何编写一个,所以请随时在那里启发我。干杯!:)

xwmevbvl

xwmevbvl1#

TL;DR这是serde_json扩展f32f64时产生的浮点问题。您可以使用println!("{}", 77.63_f32 as f64)这样简单的代码重现此问题。要修复此问题,您需要转换为f64,然后取整,并将其序列化为f64

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)

详细说明

代码中的问题与你所想的不同--它与浮点精度有关,而与serde无关。

let lat = 77.63_f32;

...您指示编译器将分数7763/100转换为f32。但该数字不能用f32精确表示,因为f32(像所有二进制浮点类型一样)使用二进制分数,即分母在一定大小限制内为2的幂的有理数。7763/100近似为10175119/217.1如果你试图打印f32的值,你将输出get the expected 77.63,因为println!()知道它打印的是f32,其中第7位之后的所有数字都是近似的副作用,将被丢弃。
serde_json的工作方式不同-它通过将f32值转换为f64来序列化它们,因为这是JSON和JavaScript使用的精度。不幸的结果是77.63_f32的近似值10175119/2
17被扩展为f64,而没有最初存储77.63的上下文。f64只存储近似值(它可以精确地容纳近似值,而不会进一步损失精度),当你打印得到的f64时,你会得到get 77.62999725341797,这就是10175119/2**17在十进制到16位精度时的样子。
这就是将自定义序列化实现为s.serialize_f32(f32::trunc(*x * 100.0) / 100.0)没有效果的原因-您将f32舍入为两位十进制数(在您的程序中是空操作,因为它一开始是舍入的),然后将其传递给serialize_f32()serialize_f32()继续将f32的值扩展为f64,这使得f32近似值中的额外数字可见-您将返回到使用serde生成的实现开始的位置。
正确的版本必须将f32转换为f64,然后去掉f64类型 * 中的多余数字 *,然后将其传递给serialize_f64()进行打印:

s.serialize_f64((*n as f64 * 100.0).trunc() / 100.0)

Playground
这是因为:将数字77.63_f32转换为对应于10175119/217的f64(即,不是77.63_f64,它将近似为2到682840701314007/243)。然后,此数字在f64中舍入为两位数,并且舍入产生了f64所能达到的最接近的77.63的近似值。也就是说,现在我们得到的近似值与在Rust源代码中使用77.63_f64得到的近似值相同。s是serde将处理的数字,serde_json将在JSON输出中将其格式化为77.63
边注:上面的代码在问题中的尝试之后使用trunc(),但是round()as shown here可能是更合适的选择。
1你可以用下面的Python一行程序得到这个比率:

>>> numpy.float32("77.63").as_integer_ratio()
(10175119, 131072)

2也可使用Python获得:

>>> n = 10175119/131072
>>> rounded = round(n*100.0)/100.0
>>> rounded
77.63
>>> rounded.as_integer_ratio()
(682840701314007, 8796093022208)
l2osamch

l2osamch2#

如果你需要精确控制JSON消息的格式,serde_json提供了你所需要的工具。生成JSON需要通过serde_json::ser::Formatter特性。你可以用某种方式编写write_f32/write_f64函数,它们只在点后面生成两位数。你可以使用现有的实现之一,并根据需要对其进行修改。

相关问题