隱藏精度

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 在我們的內部郵件清單上討論了這篇文章的草稿。