【kintone カスタマイズ Tips】JavaScriptによる少数計算で生じる誤差
前記事では JavaScriptについて、割と好意的に書いている。だが、この際はっきり言うと、JavaScript にはいくつか罠がある。今回、記事にとりあげる少数計算(少数点以下の数値を含む計算)もそのひとつだ。
1. 突如現れる誤差の衝撃
それは前触れもなくやってくる。そう、少数を含む計算を行う際に。
let result = 0.1 * 0.1;
console.log(result); //0.010000000000000002
なん・・・だと・・・。0.01が正解ではないのか?0.000000000000000002はどこから来た?と思うだろう。
なぜ、このような結果になるかというと、それはJavaScriptで扱う数値がIEEE754で規定されている浮動少数であることに起因する。更になぜ浮動少数かと掘り下げると、コンピューター内で数値は最終的にバイナリ形式で、1と0の並び(2進数)で表現されることにより密接に起因する。例えば、0.1 のように10進数では綺麗に表現できる数値でも、2進数で表すと0.00011001100… のように小数点第2位から0011が循環して永遠に終わることのない無限少数となる。これを決められたデータ型に格納するために有効桁数以下を丸めるべく2進法では0捨1入するわけで、その丸めた結果誤差が生じるのだ。(丸め誤差、と呼ばれる。)
2. どうすれば回避できるのか
よく取り沙汰されるのが、少数を整数にして計算し、再度少数に戻す方法。しかしながら、これは少数を整数にする際に少数計算が発生するため、結局誤差が生まれかねない。
そこで、一番シンプルで有効な回避策は、①いったん数値を文字列化し、文字列操作でドット(.)を排除して整数とする。②整数同士で計算を行ったうえで、元のドットの位置をもとに10^Nで割って元の少数へ戻す、という手順だと考える。うむ、これなら少数計算は発生しないので、誤差も生じない。
let strA = '0.1'; let strB = '0.1'; let dotposiA = (strA.length-1)-strA.indexOf('.'); //ドットの位置からstr1の少数点以下桁数を取得 let dotposiB = (strB.length-1)-strB.indexOf('.'); //ドットの位置からstr2の少数点以下桁数を取得 let preresult = parseInt(strA.replace('.','')) * parseInt(strA.replace('.','')); //1 let N = dotposiA + dotposiB; let result = preresult / Math.pow(10, N); //0.01
期待する結果が得られ、なかなか良さげ。
そのほか、ライブラリを使う方法もある。有名どころでは、BigDecimal.js と decimal.js の2つがあるが、試しに decimal.js の方を使ってみた。
<script type="text/javascript" src="decimal.js"></script>
<script>
let a = 0.1;
let b = 0.1;
let dec = new Decimal(a);
console.log(dec.times(b).toNumber()); //0.01
</script>
これもよりシンプルでいい感じ。
* * *
このように JavaScript は、いくつか落し穴的な要素があるので、今後も機会を見て紹介していきたい。(他によく取り沙汰されるものとしては「日付の罠」がある。これは近日記事にしたいと思う。)