こんにちは、iimonの山根です。 本記事はiimonアドベントカレンダー24日目の記事となります。 クリぼっち確定です、喉を痛めたのが原因なので本当はクリぼっち回避できましたよ😷 それでは本題に入ります。
背景
jsで10.12*10000が101199.99999999999になってしまいました。 このようになった理由を知り、正しく計算する方法を理解する為です。
なぜ
JavaScript の数値 (Number) 型は IEEE 754 の倍精度 64ビットバイナリー形式です。
IEEE 754 の倍精度数
3 つの部分を表すのに 64 ビットを使用します。 (https://ja.wikipedia.org/wiki/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:IEEE_754_Double_Floating_Point_Format.svg)
- sign・・・符号です。正か負かを決めます。
- exponent・・・指数部です。2の補数で表現するので10進数で言うと-1022 から 1023の値になります。
- mantissa・・・仮数部で、実際の値を表す部分(有効数字)です。
Number型の数値は以下のように数式で表現できます。 (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number)
今回の10.12*10000を例に説明します。 10.12は2進数で表すと、
10.12.toString(2) 1010.0001111010111000010100011110101110000101000111101
になります。
(10.12 * 10000).toString(2) 11000101101001111.111111111111111111111111111111111111
これは10進数を2進数に表せない数値(10.12)との計算で発生した丸め誤差(仮数部の53ビット目で打ち切られた)によるものです。
対応方法
ライブラリを使わない
小数点を整数に直して計算する
- こちらの記事で紹介されていた方法を紹介します
- 数値を文字列に変えて、小数点を取り除いて整数値のみで計算し最後に割ります。
// 小数点以下の桁数を取得する関数 function getDotPosition(value) { var dotPosition = 0; var stringValue = value.toString(); var dotIndex = stringValue.indexOf('.'); if (dotIndex !== -1) { dotPosition = stringValue.length - dotIndex - 1; } return dotPosition; } // 乗算の関数 function calcMultiply(value1, value2) { // それぞれの小数点の位置を取得 const dotPosition1 = getDotPosition(value1); const dotPosition2 = getDotPosition(value2); // 位置の値を合算して小数の桁を求める const totalDotPosition = dotPosition1 + dotPosition2; // 10^N の値を計算 const power = Math.pow(10, totalDotPosition); // 大きな桁数で計算し、10^N で割る return (value1 * power) * (value2 * power) / (power * power); } // テスト var result = calcMultiply(5.56, 10000); console.log(result); // 出力: 55600
注意点は非常に大きな桁数の計算をする時です。
整数、小数ともに使用可能なライブラリを使う
以下の条件を満たすものを選定したいと思います。 * 計算は加算、減算、乗算、除算ができれば十分 * 軽量のもの * メンテが1年間までにされていて、ユーザの規模が大きい big.jsとbignumber.jsで迷っているので気になる計算速度について検証します。
こちらでライブラリのnpm trendsによる比較をしています。 npmtrends.com
こちらではbig.js, bignumber.js and decimal.jsの比較についてまとめられていました。 github.com
BigDecimal
Tags · iriscouch/bigdecimal.js · GitHub 12年前からメンテされてないそうです。
decimal
decimal.js - npm * メンテは1年前です。 * cosine等の高度な計算ができます。 * 非整数の累乗をサポートし、すべての演算を指定された有効桁数まで実行するのも特徴だそうです。 * 桁数の多い数値の計算の際にbig.jsよりも速い可能性があるらしいです。
big
- ユーザ数が最も多いです。
- メンテは1年前です。 big.js - npm
BigNumber
- メンテも2022年12月にされているようです。 Release v9.1.1 · MikeMcl/bignumber.js · GitHub
- 桁数の多い数値の計算の際にbig.jsよりも速い可能性があるらしいです。
js-big-decimal
js-big-decimal - npm * こちらはまだユーザ数が少なそうです。
最後に
数値計算でのバグは扱うサービスによっては避けないと大きなバグになることがあります。 改めて気をつけて実装していきたいと思いました。
参考
- https://qiita.com/k_moto/items/0b576a3351b77fb0aa98
- JavaScriptにおける丸め誤差と対応
- Number - JavaScript | MDN
- JavaScriptの数値型完全理解 #JavaScript - Qiita
- JavaScriptにおける丸め誤差と対応
- JavaScript:小数点以下の丸め誤差に対応する - 技術とか戦略とか
この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします! Wantedly / Green
次のアドベントカレンダーの記事はフロント挑戦中のデッサンです!! どんな記事を書くのか楽しみですね!