这绝对不是为什么不使用 Double 或 Float 来表示货币? - 我知道我不应该使用 float 来表示货币,自从它的原始版本以来,问题中已经明确地提到了这一点,并且也回复了评论里提出这个问题的人。我进一步编辑了问题以使这一点更加明确。
这也可能被认为是一个数学问题,但我想专门使用 Rust 来演示。
出于对 Rust 的兴趣,使用浮点数表示百分比而不是整数基点会失去精度吗?
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::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);
}