出于对 Rust 的兴趣,为什么我的测试通过使用百分比浮点数而不是全整数基点而不会损失精度?

问题描述 投票:0回答:1

这绝对不是为什么不使用 Double 或 Float 来表示货币? - 我知道我不应该使用 float 来表示货币,自从它的原始版本以来,问题中已经明确地提到了这一点,并且也回复了评论里提出这个问题的人。我进一步编辑了问题以使这一点更加明确。

这也可能被认为是一个数学问题,但我想专门使用 Rust 来演示。

出于对 Rust 的兴趣,使用浮点数表示百分比而不是整数基点会失去精度吗?

我知道计算机不应该使用浮点处理小数。例如,0.02 是二进制的
1.010001111010111000010100011110101110...
。我曾开发过其他财务软件,并且通常使用整数和基点(例如 200 而不是 0.02)以避免计算机在处理小数值时出现问题。

我已经编写了以下代码,并且两个测试都通过了:

use std::f64::consts::E; pub fn get_compounded_value_using_ints( initial_amount: u64, elapsed_periods: u64, interest_rate_basis_points: u64, ) -> u64 { let multiplier = E.powf(interest_rate_basis_points as f64 * elapsed_periods as f64 / 10_000 as f64); (initial_amount as f64 * multiplier) as u64 } pub fn get_compounded_value_using_floats( initial_amount: u64, elapsed_periods: u64, interest_rate: f64, ) -> u64 { let multiplier = E.powf(interest_rate * elapsed_periods as f64); (initial_amount as f64 * multiplier) as u64 } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_compounded_value_using_ints() { // Start with 100_000, 12 periods, 2% interest assert_eq!(get_compounded_value_using_ints(100_000, 12, 200), 127_124); } #[test] fn test_get_compounded_value_using_floats() { // Start with 100_000, 12 periods, 2% interest assert_eq!( get_compounded_value_using_floats(100_000, 12, 0.02), 127_124 ); } }
我什至修改了这两个测试以使用更大的数字(接近 u64)最大值,但它们仍然通过。

由于两项测试都通过了,这使得人们似乎可以愉快地使用浮点数来表示百分比值。我想知道测试是否有问题,并且在某些时候浮动版本将开始不精确。

使用浮点数与整数作为百分比值会失去小数精度吗?

rust decimal
1个回答
2
投票

是的,您需要担心浮点数的精度损失。看起来您的两个测试最终都使用了浮点数,数字很小,舍入的位置有限,并且您忽略了输出中的分。他们通过并不奇怪。

花车对于金钱来说有几个弱点:

    舍入误差可能会在多次迭代中累积
  • 二进制而不是十进制
    • 更难四舍五入到小数点后 N 位
    • 有些小数,如 0.02,是无法表示的
在这里查看更多内容:

为什么不使用 Double 或 Float 来表示货币?

幸运的是,浮点数与“整数”是一个错误的选择。对于货币,您应该使用小数类型,例如

fpdec::Decimal

 表示定点小数,rust_decimal::Decimal
 表示浮点小数,
rusty-money
 表示针对货币优化的定点十进制,或 
bigdecimal::Decimal
 对于任意大小。

这里有一个类似的测试来证明差异。通过在许多离散时间进行复合,浮点数很难产生准确的答案。我评论了一些发生浮动相关问题的地方。

use rust_decimal::{Decimal, RoundingStrategy}; pub fn get_compounded_value_using_decimals( initial_amount: Decimal, elapsed_periods: u64, interest_rate: Decimal, ) -> Decimal { let mut amount = initial_amount; for _ in 0..elapsed_periods { amount = amount .checked_add( amount .checked_mul(interest_rate) .unwrap() .round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven), ) .unwrap(); } amount } pub fn get_compounded_value_using_floats( initial_amount: f64, elapsed_periods: u64, interest_rate: f64, ) -> f64 { let mut amount = initial_amount; // CON: Floats build up rounding error with each iteration. for _ in 0..elapsed_periods { // CON: Rounding is trickier. amount += (amount * interest_rate * 100.0).round_ties_even() * 0.01; } amount } fn compare(initial: u64, periods: u64, percent: u64) { let with_decimals = get_compounded_value_using_decimals( Decimal::from(initial), periods, Decimal::from(percent) / Decimal::from(100), ); // CON: 0.02 isn't precisely representable by `f64`, instead becoming 0.0199999995529651641845703125. let with_floats = get_compounded_value_using_floats(initial as f64, periods, percent as f64 / 100.0); println!("${initial}, {periods} periods, {percent}%"); println!("decimals ${with_decimals}"); // CON: added cent-precision numbers and got rounding errors, need to truncate. println!("floats ${with_floats:.2}"); println!(); } pub fn main() { // Note: for the purpose of the comments, 2% means // 2% monthly or 24% annually (equivalent of 26.824% // compounded annually). This might be unrealistically // high for an investment, but is typical of certain loans. // 1 year compounded every month -> $0.01 difference // decimals $126824.17 // floats $126824.18 compare(100_000, 12, 2); // ~83 years compounded every month -> $28 difference // decimals $19502.56 // floats $19530.56 compare(1, 1000, 1); }
    
© www.soinside.com 2019 - 2024. All rights reserved.