JavaScript 浮動小数点演算の精度誤差と解決策

数値計算における精度の問題

Web 開発、特に財務管理や決済システムを構築する際、数値の正確性は極めて重要です。しかし、JavaScript 標準の数値型を用いた演算では、期待される結果と異なる値が返されるケースが多々あります。金銭に関わる処理において、このような誤差は許容されない重大な問題となります。

典型的な誤差の事例

以下のコンソール出力例は、JavaScript エンジンが内部で数を処理する際に発生する代表的な精度のずれを示しています。

console.log(0.1 + 0.2); 
// 出力:0.30000000000000004

console.log(0.3 - 0.2); 
// 出力:0.09999999999999998

console.log(0.8 * 3); 
// 出力:2.4000000000000004

console.log(0.3 / 0.1); 
// 出力:2.9999999999999996

console.log((0.1 + 0.2) === 0.3); 
// 出力:false

技術的な要因

JavaScript における数値は、すべて IEEE 754 規格の倍精度浮動小数点形式(64 ビット)で管理されています。整数型は存在せず、1 と 1.0 は同一として扱われます。この形式では、0.1 や 0.2 といった 10 進法の小数を 2 進法で正確に表現しようとすると無限小数になってしまうため、有限のビット数(仮数部 53 ビット)で切り捨てが発生します。この切り捨て誤差が蓄積することで、演算結果にずれが生じます。

主な対策手法

この問題に対処するには、主に以下の 2 つのアプローチがあります。

  • 専用ライブラリの導入: decimal.js, big.js, math.js などのライブラリを利用し、任意精度演算を行います。
  • 整数化による演算: 小数点を一旦整数に変換して計算を行い、その後で元の桁数に戻す手法を自作関数で実装します。

精度保証ユーティリティの実装

外部ライブラリに依存せず、小数桁数を考慮して整数演算に変換するユーティリティオブジェクトの例を示します。プロトタイプ拡張ではなく、静的メソッドとして利用することで、より安全なコード構造を維持できます。

const PrecisionMath = {
  // 小数部の桁数を取得するヘルパー
  getDecimalLength: function(num) {
    const str = num.toString();
    const parts = str.split('.');
    return parts.length > 1 ? parts[1].length : 0;
  },

  // 正確な加算
  add: function(num1, num2) {
    const len1 = this.getDecimalLength(num1);
    const len2 = this.getDecimalLength(num2);
    const maxLen = Math.max(len1, len2);
    const multiplier = 10 ** maxLen;

    const int1 = Number(num1.toString().replace('.', ''));
    const int2 = Number(num2.toString().replace('.', ''));
    
    // 桁合わせのための補正
    const val1 = len1 < maxLen ? int1 * (10 ** (maxLen - len1)) : int1;
    const val2 = len2 < maxLen ? int2 * (10 ** (maxLen - len2)) : int2;

    return (val1 + val2) / multiplier;
  },

  // 正確な減算
  sub: function(num1, num2) {
    const len1 = this.getDecimalLength(num1);
    const len2 = this.getDecimalLength(num2);
    const maxLen = Math.max(len1, len2);
    const multiplier = 10 ** maxLen;

    const int1 = Number(num1.toString().replace('.', ''));
    const int2 = Number(num2.toString().replace('.', ''));

    const val1 = len1 < maxLen ? int1 * (10 ** (maxLen - len1)) : int1;
    const val2 = len2 < maxLen ? int2 * (10 ** (maxLen - len2)) : int2;

    const result = (val1 - val2) / multiplier;
    // 減算結果の桁数を保持
    return Number(result.toFixed(maxLen));
  },

  // 正確な乗算
  mul: function(num1, num2) {
    const str1 = num1.toString();
    const str2 = num2.toString();
    let totalDecimalLen = 0;

    if (str1.includes('.')) totalDecimalLen += str1.split('.')[1].length;
    if (str2.includes('.')) totalDecimalLen += str2.split('.')[1].length;

    const int1 = Number(str1.replace('.', ''));
    const int2 = Number(str2.replace('.', ''));

    return (int1 * int2) / (10 ** totalDecimalLen);
  },

  // 正確な除算
  div: function(num1, num2) {
    const len1 = this.getDecimalLength(num1);
    const len2 = this.getDecimalLength(num2);

    const int1 = Number(num1.toString().replace('.', ''));
    const int2 = Number(num2.toString().replace('.', ''));

    return (int1 / int2) * (10 ** (len2 - len1));
  }
};

// 使用例
console.log(PrecisionMath.add(0.1, 0.2)); // 0.3
console.log(PrecisionMath.mul(19.9, 100)); // 1990

タグ: javascript ieee-754 floating-point precision-error web-engineering

6月9日 20:31 投稿