【JavaScript】小数点計算の誤差対策で一度整数に変換する
更新日:2023/07/24
JavaScriptは小数がある数値の計算で誤差が出ます。
対策の一つとして、一度整数に変換してから計算する方法があるようなので、やってみました。
JavaScriptの小数計算は誤差が出る
JavaScriptのNumber型はIEEE 754-2019(浮動小数点演算のIEEE標準)の規格を採用しています。
この企画は、小数点計算というか、正確な小数点の値を保持するのが苦手だったりします。
1.1を小数以下32桁で表示すると次のようになります。
console.log( (1.1).toFixed(32) );
// 結果: 1.10000000000000008881784197001252
これは、1.1というリテラル値を浮動小数点形式に変換しようとしたけれど、ピッタリの値が無理だったから、近い値をセットしたのです。
しかし、コンソールなど外部出力されるときは、1.1と表示されます。
console.log( 1.1 );
// 結果: 1.1
これはJavaScriptが適当な桁数で丸めた結果を文字列に変換しているからです。
表示上は誤差が無いように見えていても、実際は誤差が出ていることが多いのです。
微妙なズレは、演算処理の度に蓄積していきます。
let n = 1.1;
for( let i = 0 ; i < 10000 ; i ++ ) n += 1.1;
console.log( n.toFixed(32)); // 結果: 11001.10000000204490788746625185012817
console.log( n ); // 結果: 11001.100000002045
少ないズレでも、回数を重ねると表示結果に表れてきます。
困りますね...
誤差対策:整数に変換する
浮動小数点形式は小数点計算が苦手なのだから、整数に変換して計算したあと、元に戻せばいいよね。
というのが一番簡単な解決法ですね。
(誤差に対応したライブラリを使う方が簡単という話もある...)
つまり、こんな感じ。
let a = 1.1;
let b = 1.1;
a = a * 10;
b = b * 10;
for( let i = 0 ; i < 10000 ; i ++ ) a += b;
a = a / 10;
console.log( a.toFixed(32)); // 結果: 11001.10000000000036379788070917129517
console.log( a ); // 結果: 11001.1
10分の1すると、ズレた。
ただ、小数のまま演算するのよりは誤差が少ないですね。
しかーし、10のn乗しても整数にならないことがあります。
console.log( (12.3456 * 10000).toFixed(32) );
// 結果: 123455.99999999998544808477163314819336
この場合は、四捨五入するといいかもしれない。
console.log( Math.round(12.3456 * 10000).toFixed(32) );
// 結果: 123456.00000000000000000000000000000000
たぶん…
対象の桁数が異なる場合
演算対象の数値の小数桁数が異なる時は、基本的には有効桁数を決めて、全てそれに合わせます。
const SIGNIFICANT_DIGITS = 10 ** 6;
let a = 1.1234567;
let b = 1.1;
a = Math.round( a * SIGNIFICANT_DIGITS );
b = Math.round( b * SIGNIFICANT_DIGITS );
console.log( a.toFixed(32) ); // 結果: 1123457.00000000000000000000000000000000
console.log( b.toFixed(32) ); // 結果: 1100000.00000000000000000000000000000000
a = (a + b) / SIGNIFICANT_DIGITS;
console.log( a.toFixed(32)); // 結果: 2.22345699999999979468157107476145
console.log( a ); // 結果: 2.223457
桁数を動的に決定したい
小数の桁数を動的に決定したいときも、あるかもしれない。
そんなときは、文字列に変換した後に小数部の桁数を数えます。
次の関数は、数値の小数部分の桁数を返します
const getdecimalPrecision = (number,significant_digits=6) =>{
const f = number.toFixed(significant_digits).split(".")[1];
if( f === undefined || f.length === 0) return 0;
console.log( f );
// 後方の"0"を取り除いた桁数取得
return f.length - ["1",...f].reverse().findIndex( e=>e!="0");
}
次のように使用します。
let a = 1.1234567;
let b = 1.1
const digits = 10 ** Math.max(
getdecimalPrecision(a) , getdecimalPrecision(b) );
a = Math.round( a * digits );
b = Math.round( b * digits );
console.log( a.toFixed(32) ); // 結果: 1123457.00000000000000000000000000000000
console.log( b.toFixed(32) ); // 結果: 1100000.00000000000000000000000000000000
a = (a + b) / digits;
console.log( a.toFixed(32)); // 結果: 2.22345699999999979468157107476145
console.log( a ); // 結果: 2.223457
前項と同じ結果になりました。
でも速度は、文字列処理をやっているので遅いです。
更新日:2023/07/24
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。