隐藏精度

2016 年 11 月 22 日

有时当我处理一些数据时,这些数据比我预期的更精确。有人可能会认为这是一件好事,毕竟精度是好的,所以越多越好。但隐藏的精度会导致一些细微的错误。

const validityStart = new Date("2016-10-01");   // JavaScript
const validityEnd = new Date("2016-11-08");
const isWithinValidity = aDate => (aDate >= validityStart && aDate <= validityEnd);
const applicationTime = new Date("2016-11-08 08:00");

assert.notOk(isWithinValidity(applicationTime));  // NOT what I want

上面代码中发生的事情是,我打算通过指定开始日期和结束日期来创建一个包含日期范围。但是,我实际上没有指定日期,而是指定了时间点,所以我没有将结束日期标记为 11 月 8 日,而是将结束日期标记为 11 月 8 日的 00:00 时。因此,11 月 8 日的任何时间(午夜除外)都将落在不包含它的日期范围之外。

隐藏精度是日期的常见问题,因为不幸的是,拥有一个实际上提供像这样的时间点的日期创建函数很常见。这是一个命名不当的例子,实际上是日期和时间的总体建模不当。

日期是隐藏精度问题的典型例子,但另一个罪魁祸首是浮点数。

const tenCharges = [
  0.10, 0.10, 0.10, 0.10, 0.10,
  0.10, 0.10, 0.10, 0.10, 0.10,
];
const discountThreshold = 1.00;
const totalCharge = tenCharges.reduce((acc, each) => acc += each);
assert.ok(totalCharge < discountThreshold);   // NOT what I want

当我运行它时,一个日志语句显示 totalCharge0.9999999999999999。这是因为浮点数不能完全表示许多值,导致一些不可见的精度,这些精度会在尴尬的时候出现。

由此得出的一个结论是,你应该非常谨慎地用浮点数表示货币。(如果你有像美分这样的货币小数部分,那么通常最好对小数部分使用整数,用 500 表示 €5.00,最好在 货币类型 中)。更一般的结论是,浮点数在比较方面很棘手(这就是为什么测试框架断言总是对比较有精度的原因)。

致谢

Arun Murali、James Birnie、Ken McCormack 和 Matteo Vaccari 在我们内部邮件列表中讨论了这篇文章的草稿。