隠された精度

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日の午前0時としています。したがって、11月8日の午前0時以外の時刻は範囲外になり、含まれるべきものでも除外されてしまいます。

隠された精度は日付にまつわる一般的な問題です。このようなインスタンスを生成する日付作成関数が存在するためです。これはネーミングの失敗の一例であり、一般的に日付と時刻のモデリングが悪いため発生しています。

日付は隠された精度の問題の適切な例ですが、浮動小数点数も隠れた精度をもたらします。

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となったことが示されました。これは、浮動小数点では多くの値を正確に表すことができないため、わずかな精度が失われ、不適切な方法で表示されるためです。

このことから、浮動小数点で通貨を表すことは極めて危険であるという結論が出ます。(ユーロセントなど、通貨に小数部分がある場合は、通常、小数部分には整数を使用し、€5.00は通貨タイプ内で500と表記するのが最善です。)より一般的な結論として、浮動小数点は比較に関してトリッキーです(テストフレームワークのアサートが、常に比較の精度を持つのはそのためです)。

謝辞

Arun Murali、James Birnie、Ken McCormack、およびMatteo Vaccariは、社内メーリングリストでこの投稿のドラフトについて議論しました。