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

4

我对Rust还比较陌生。我正在尝试进行一个需要序列化JSON正文的API调用。

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)
}

无论自定义序列化器是否是解决问题的正确方法或方案,我仍然想学习如何编写它。所以请随意为我提供指导。 干杯!:)

7
一般建议。把钱存储为浮点数是个坏主意,因为可能会出现.1 + .2 != .3的问题。要么使用默认精度2,并将值存储为派斯(paise)数字u32,要么创建一个Money结构体,包含值和精度。https://martinfowler.com/eaaCatalog/money.html - Yury Tarabanko
“tinkering it with no luck”是什么意思?它没有编译吗?你尝试过“显而易见”的s.serialize_f32(f32::trunc(*x * 100.0) / 100.0)了吗? - user4815162342
@user4815162342 编译成功了。你给我的解决方案也编译通过了。 这是一个游乐场:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=bcd23f9a2af8212259946619cf644828 希望它能够解释清楚正在发生的事情。 - snipedown21
@YuryTarabanko 我不是在存储值。我需要将其序列化并传递给API。 - snipedown21
1
我想我知道问题出在哪里了。这个链接是否有效:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f44ec769a13d7a21c19d67d38faae1b9 - user4815162342
显示剩余4条评论
3个回答

5
TL;DR:这是一个浮点数问题,来自于serde_json将f32扩展为f64。您可以使用简单的代码复制它,例如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/2**17。1如果您尝试打印该f32值,则会得到预期的77.63输出,因为println!()知道它正在打印一个f32,其中第7位后的所有数字都是近似的副作用,应被丢弃。
`serde_json`的工作方式不同 - 它通过将`f32`值转换为`f64`来序列化,因为这是JSON和JavaScript使用的精度。不幸的是,这导致了`77.63_f32`的10175119/2**17近似被扩展到`f64`,而没有存储77.63的原始意图上下文。`f64`仅仅存储了近似值(它可以完全容纳,不会有更多的精度损失),当你打印结果的`f64`时,你得到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)

游乐场

这能够工作的原因是:数字77.63_f32被转换为对应于10175119/2 ** 17的f64(而不是77.63_f64,后者将近似于682840701314007/2 ** 43)。然后将该数字四舍五入到f64中的两位小数,并且该舍入产生了f64能够得到的最接近77.63的近似值。也就是说,现在我们得到了在Rust源代码中使用77.63_f64得到的682840701314007/2**43近似值。这是serde将使用的数字,serde_json将其格式化为JSON输出中的77.63

侧面说明:上述代码使用了trunc(),这是根据问题尝试得出的结论,但也许更合适的选择是round(),就像这里所示的那样。
您可以使用以下Python一行代码获取此比率:
>>> numpy.float32("77.63").as_integer_ratio()
(10175119, 131072)

也可以使用Python获得:

2

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

1
如果您需要精确控制JSON消息的格式,serde_json提供了所需的工具。生成JSON通过{{link1:serde_json :: ser :: Formatter }}特征进行。您可以编写write_f32 / write_f64函数,以使它们仅在小数点后产生两个数字。您可以采用已有的实现之一,并根据需要进行调整。

0
示例将 f16 序列化为三位小数。
use half::f16;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json;

fn f16ser3<S>(fv: &f16, se: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    se.serialize_f64((fv.to_f64() * 1000.).round() / 1000.)
}

fn f16des3<'de, D>(des: D) -> Result<f16, D::Error>
where
    D: Deserializer<'de>,
{
    f64::deserialize(des).and_then(|fv| Ok(f16::from_f64(fv)))
}

#[derive(Clone, Debug, Deserialize, Serialize)]
struct Foo {
    #[serde(serialize_with = "f16ser3", deserialize_with = "f16des3")]
    bar: f16,
}

fn main() {
    let foo = Foo {
        bar: f16::from_f32(1.23),
    };
    let foo_s = serde_json::to_string(&foo).unwrap();
    println!("{}", foo_s);  // {"bar":1.23}
}

https://play.rust-lang.org/?gist=4149756d2c9058f9d073f4a39a4ece9c


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接