🎅

12.20 + 0.04が12.24にならなくてサンタがやってこない

2024/12/15に公開

こちらはe-dash advent calendar 2024の15日目の記事です。

はじめに

こんにちは! e-dash株式会社のフロントエンドエンジニアtagawaです。
今年もクリスマスが近づいてきましたね!
しかしクリスマスの準備を進めていると、思わぬところで技術的な問題に直面することがあります。
例えば、ブラウザでF12を押してコンソールタブを開き、12.20 + 0.04と入力してみてください。
結果は 12.239999999999998と表示され、期待していたクリスマスイブの日付(数値)12.24になりませんでした。

> 12.20 + 0.04
< 12.239999999999998

具体的な事例

とある設定フォームを実装している時でした。
入力欄に小数点第二位までの値で数値を入力して設定します。
その際に前回の設定値より大きい値のみを受け付け、前回の値以下の場合はバリデーションがかかるものを実装しました。

const minValue = previousValue + 0.01;

return z.object({
  value: z.number().min(minValue, {message: `${minValue}以上の値を入力してください。`,})
})

下限値より少ない場合は、下記のようなエラーメッセージを表示します。

入力欄の画像

一見実装通りに+0.01した値が表示されており問題無いように見えます。
しかし、特定の数値を入れると下記のような過剰な桁数が表示されてしまったのでした。

誤差が出た入力欄の画像

下二桁の入力でこのバリデーションメッセージじゃ見た目悪いですね。

原因: 浮動小数点数の罠

この現象は、コンピュータが数値を2進数で扱うために生じる、いわゆる「表現誤差」に起因したものだそうです。
特に10進数の小数を2進数で表現しようとすると、端数が切り捨てられてしまうケースが頻繁に発生します。

例えば、「0.1」という10進数の小数を2進数で表現する場合を考えます。10進数では簡単に「0.1」と書けますが、2進数では以下のように無限に続く循環小数になります。

0.1 (10進数) = 0.00011001100110011... (2進数)

この結果、コンピュータ内部では「0.1」が完全には表現できず、丸められた値として保存されます。
このようなわずかな丸め誤差が、後の計算で目に見える形で現れるのです。

冒頭の12.20 + 0.04についても以下の様な計算がされています。

12.20 = 1100.001100110011001100110011001100110011001100110011
 0.04 =    0.00001010001111010111000010100011110101110000101000111101
---------------------------------------------------------------------
        1100.00111101011100001010001111010111000010100011110100111101

これを10進数に戻すと12.239999999999998となるわけですね。

言語ごとの違い

使用言語ごとに結果が変わるのかも見てみました。

Python

print(12.20 + 0.04) # 出力: 12.239999999999998

Pythonも同様に、浮動小数点数の計算で誤差が発生しました。

Java

double value1 = 12.20;
double value2 = 0.04;
System.out.println(value1 + value2); // 出力: 12.239999999999998

Javaでは、double型を使用した場合に同様の誤差が生じます。

C言語

printf("%.15f\n", 12.20 + 0.04); // 出力: 12.239999999999998

C言語でも、浮動小数点数の計算で誤差が見られました。
下記の様に2桁で指定した場合は四捨五入で丸められ、期待通りの結果が得られました。

printf("%.2f\n", 12.20 + 0.04); // 出力: 12.24

PHP

echo 12.20 + 0.04; // 出力: 12.24

PHPでは、内部的に丸め処理が行われるため、期待通りの結果が得られました。

Go

fmt.Println(12.20 + 0.04) // 出力: 12.24

Goも同様に丸め処理により期待通りの結果が得られました。

結果

これらの結果から、言語によって浮動小数点数の表示方法や丸め規則が異なることが確認できました。
しかし、表示上では問題無さそうでも内部計算では誤差が存在する可能性があるため注意が必要です。
また、これらの計算を繰り返すと、累積誤差がより顕著になる可能性があります。

解決方法

この問題を回避するためには、ライブラリを使用するのが一般的です。
言語によっては標準ライブラリで高精度計算をサポートしています。
しかし今回使用したJavaScriptは標準でそのような高精度計算のサポートはないためBig.jsやDecimal.jsなどの外部ライブラリを使用することになります。
以下はBig.jsを使用した例です。

const Big = require('big.js');

const value1 = new Big(12.20);
const value2 = new Big(0.04);
const result = value1.plus(value2);

console.log(result); // 出力: 12.24

また、今回のように桁数が固定である場合は、ライブラリを使用しなくても一旦小数を整数に変換して計算してから最後に元のスケールに戻すことで対処できます。

const result = Math.round((12.20 * 100) + (0.04 * 100)) / 100;
console.log(result); // 出力: 12.24

おわりに

これで無事に下二桁が正しく表示されるようになり、安心してクリスマスを迎えることができそうです。
些細な問題でしたが、どんなに小さな問題も解決することで少なからず喜びをもたらしてくれるものですね。
それでは、皆さんも良いクリスマスをお過ごしください!

Discussion