JavaScriptで数値計算を行うとき、時として予期せぬ結果に遭遇することがあります。例えば、次のような計算結果を見たことはないでしょうか?
console.log(0.1 + 0.2); // => 0.30000000000000004
console.log(0.1 + 0.2 === 0.3) // => false
この「不可解な」振る舞いは、実はJavaScriptが採用しているIEEE 754規格に基づく浮動小数点数の仕組みによるものです。この記事では、IEEE 754規格の基本から実践的な対処法まで、詳しく解説していきます。
IEEE 754規格とは
IEEE 754は、浮動小数点数の表現と演算に関する技術標準です。1985年に最初のバージョンが策定され、2008年と2019年に改訂されています。JavaScriptを含む多くのプログラミング言語で採用されている重要な規格です。
基本的な数値表現
JavaScriptでは、IEEE 754の倍精度浮動小数点数(64ビット)形式を使用しています。この形式は以下の3つの部分から構成されています:
-
符号部(1ビット):数値の正負を表す
- 0:正の数
- 1:負の数
-
指数部(11ビット):2の累乗の指数を表す
- バイアス値1023を使用
- 実際の指数 = ビット表現の値 - 1023
-
仮数部(52ビット):実際の数値を表す
- 1.xxxxx...の形式(先頭の1は暗黙的)
- 52ビットの精度で数値を表現
例えば、123.456は以下のように表現されます:
// 123.456の内部表現
// 符号部: 0(正の数)
// 指数部: 1000000110(6 + 1023 = 1029)
// 仮数部: 1111011001100110011001100110011001100110011001100110
IEEE 754における数値の種類
1. 通常の数値(正規化数)
最も一般的な数値表現で、以下の形式を取ります:
- ±1.xxxxx × 2^n
- 仮数部の先頭は必ず1(暗黙的な1)
const normal = 123.456;
console.log(normal.toString(2)); // 2進数表現を表示
2. 非正規化数(デノーマル数)
非常に小さな値を表現するための特別な形式です:
- 指数部がすべて0
- 暗黙的な先頭の1を使用しない
console.log(Number.MIN_VALUE); // => 5e-324
// これは最小の正の非正規化数
3. 特殊な値
ゼロ
- 正のゼロ(+0)と負のゼロ(-0)が存在
- 指数部と仮数部がすべて0
console.log(Object.is(0, -0)); // => false
console.log(1 / 0); // => Infinity
console.log(1 / -0); // => -Infinity
無限大(Infinity)
- 指数部がすべて1で仮数部が0
- オーバーフローや特定の演算の結果として発生
console.log(1 / 0); // => Infinity
console.log(Math.pow(2, 1024)); // => Infinity
非数(NaN)
- 指数部がすべて1で仮数部が非ゼロ
- 無効な演算の結果として発生
console.log(0 / 0); // => NaN
console.log(Math.sqrt(-1)); // => NaN
console.log(NaN === NaN); // => false
丸め処理の仕組み
IEEE 754規格では、4つの丸めモードを定義しています:
1. 最近接丸め(round to nearest)
最も近い表現可能な数値に丸めます。これが最も一般的に使用されるモードです。
// JavaScriptのMath.roundは最近接丸めを使用
console.log(Math.round(1.5)); // => 2
console.log(Math.round(2.5)); // => 3
中間値(例:1.5)の場合は、偶数への丸め(round to even)が適用されます:
console.log(Math.round(2.5)); // => 2 (偶数に丸める)
console.log(Math.round(3.5)); // => 4 (偶数に丸める)
2. 切り上げ(round toward +∞)
より大きい方の表現可能な数値に丸めます。
console.log(Math.ceil(1.1)); // => 2
console.log(Math.ceil(-1.1)); // => -1
3. 切り捨て(round toward -∞)
より小さい方の表現可能な数値に丸めます。
console.log(Math.floor(1.9)); // => 1
console.log(Math.floor(-1.1)); // => -2
4. 0方向への丸め(round toward zero)
0に近い方の表現可能な数値に丸めます。
console.log(Math.trunc(3.7)); // => 3
console.log(Math.trunc(-3.7)); // => -3
正確な計算のための対策
IEEE 754の特性を理解した上で、正確な計算を行うためのいくつかの方法を紹介します。
1. 整数化による計算
小数点の計算を一時的に整数に変換して行います:
function addWithPrecision(x, y, precision = 2) {
const scale = Math.pow(10, precision);
return Math.round((x + y) * scale) / scale;
}
console.log(addWithPrecision(0.1, 0.2)); // => 0.3
2. 専用ライブラリの使用
高精度な計算が必要な場合は、decimal.js
などのライブラリを使用します:
const Decimal = require('decimal.js');
const x = new Decimal('0.1');
const y = new Decimal('0.2');
console.log(x.plus(y).toString()); // => "0.3"
3. Number.EPSILONを使用した比較
浮動小数点数の比較では、微小な誤差を考慮する必要があります:
function isApproximatelyEqual(x, y, epsilon = Number.EPSILON) {
return Math.abs(x - y) < epsilon;
}
console.log(isApproximatelyEqual(0.1 + 0.2, 0.3)); // => true
まとめ
IEEE 754規格は、コンピュータにおける浮動小数点数の表現と演算を統一的に定義する重要な標準です。この規格を理解することで:
- 浮動小数点数の振る舞いが予測可能になる
- 数値計算の誤差の原因が理解できる
- 適切な対策を選択できるようになる
特に実務では、以下の点に注意を払うことが重要です:
- 金額計算など正確さが重要な場合は、整数化して計算を行う
- 必要に応じて専用のライブラリを使用する
- 浮動小数点数の比較では適切なイプシロンを使用する
参考資料
主要な資料:
- What Every Computer Scientist Should Know About Floating-Point Arithmetic - David Goldbergによる浮動小数点数に関する基礎的な解説論文(Oracle提供の無料版)
- ECMAScript® 2024 Language Specification - JavaScriptの言語仕様における数値型の定義(6.1.6 The Number Type)
- IEEE 754の解説(Wikipedia) - IEEE 754規格の概要と歴史(日本語)
実装に関する資料:
- JavaScriptにおける数値の扱い(MDN) - JavaScriptでの数値操作の実践的なガイド
- Number型のプリミティブ(MDN) - JavaScriptの数値型の詳細な解説
- IEEE 754の実装に関する解説(Microsoft) - 浮動小数点数の実装詳細について
IEEE 754規格の入手:
- IEEE 754-2019 - IEEE策定による浮動小数点数の技術標準(IEEE会員向け)
- Open Standards - C言語規格における浮動小数点数の実装に関する解説(無料)