我可能在矩阵中发现了一个故障。一位客户报告了一个错误:他们通过后端设置了产品价格4.99,并说保存为4.98。
我怀疑他们输入了错误的值,所以我自己测试了它,确实该值出现了,而4.98 不是4.99。所有其他产品价格似乎不受影响。一堆 0.99s、1.99s 和 19.99s。我认为我们最近可能引入了这个错误,因为这是客户第一次也是唯一一次报告看到这样的问题。
为了给您提供更多背景信息,它是一个 React UI,它使用 apollo graphql 将数据推送到 Rust (actix) 后端。然后,后端将需要保存的任何内容保存在数据库中,并且产品价格通过其 API 发送到 stripe。4.98 是条纹的。Stripe API 接受价格的方式是单位金额,即美分,而不是美元。所以4.99应该以条纹着陆499。根据我们后端发送的他们的仪表板498。啥?!
几分钟后
我无法缩小代码中任何明显错误的范围。graphql 突变将其作为浮点值发送,它再次4.99保存在我们的数据库中,但价格最终以条带形式出现。FLOAT(4,2)4.994.98
我们为 stripe API 准备值的方式是:
let unit_amount = (product_price_row.price * 100f32) as i64;
Run Code Online (Sandbox Code Playgroud)
product_price_rowSeaORM 行所在的price位置是4.99。
在这种情况unit_amount下最终为498.
似乎没有其他值会出现这样的故障。
我意识到这是一些奇怪的浮点错误簇,但它引起了我的注意。我写了一个小的 Rust 沙箱来说明发生了什么。要亲自测试它,请检查此 存储库。
基本上可以归结为以下几点:
let i = 4.99; // any other .99 number works fine
println!("i = {}", i); // 4.99
println!("i.2 = {:.2}", i); // 4.99
println!("i.10 = {:.10}", i); // 4.9900000000
println!();
let f = format!("{:.10}", i).parse::<f32>().unwrap(); // lets introduce some errors
println!("f = {}", f); // 4.99
println!("f.2 = {:.2}", f); // 4.99
println!("f.10 = {:.10}", f); // 4.9899997711 <- here it goes
println!();
let d = f * 100f32;
println!("d = {}", d); // 498.99997 <- blyatiful
println!("d.2 = {:.2}", d); // 499.00
println!("d.10 = {:.10}", d); // 498.9999694824
println!();
let a = d as i64; // how the code used to work
println!("a = {}", a); // 498
let b = format!("{:.2}", d).parse::<f32>().unwrap() as i64; // workaround
println!("b = {}", b); // 499
Run Code Online (Sandbox Code Playgroud)
在上面的代码中,a代表客户端报告的值,并b代表我修复它的解决方法。
替换i为任何 0.99 数字的行为都不同。为了证明这4.99是一个异常值,我制作了另一个小二进制文件(在同一个存储库中)。跑去cargo run --bin itr亲自测试一下。它从 0 开始,直到爆炸或您停止它 (CTRL+C) 并显示未通过测试的“有缺陷”数字a == b。
我得到这个结果的唯一数字是4.99。
对于如何在 Rust 中更正确地使用浮动有什么建议吗?
执行浮点数学后进行转换,而不进行第一次舍入,是错误的。当你这样做时:
let unit_amount = (product_price_row.price * 100f32) as i64;
Run Code Online (Sandbox Code Playgroud)
由于浮点精度限制,数学中的product_price_row.priceas显然会产生一个更像或类似的值(我懒得去找到确切的值),并且转换只是删除小数点后的数据,而不是四舍五入,给你留下的不是你所期望的。4.99f324.99f32 * 100f32f32498.99999999998498499
相反,做:
// Casting to f64 before doing math removes risk of data loss for higher prices
let unit_amount = (product_price_row.price as f64 * 100f64).round() as i64;
Run Code Online (Sandbox Code Playgroud)
或(在小数点可能超出小数点后第二位的情况下),使用libmath:
let unit_amount = math::round::half_to_even(product_price_row.price as f64 * 100f64) as i64;
Run Code Online (Sandbox Code Playgroud)
它使用“银行家舍入”来确保半值的舍入不会始终以一种方式或另一种方式进行,从而最大限度地减少总体舍入偏差的风险。
无论哪种方式,.999999998您都会正确舍入,然后转换现在的安全值,而不是被丢弃。
| 归档时间: |
|
| 查看次数: |
102 次 |
| 最近记录: |