JavaScriptにかぎらず、プログラミングでの小数を含む演算では誤差が発生してしまうことがよくあります。例えば、『0.2+0.1』という単純な足し算すら、『0.30000000000000004』という結果になってしまいます(Chromeで確認した場合)。その対応方法が改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用までに書いてあったのでメモしておきます。
対応方法といっても単純なことで、ようは小数をいったん小数の長さ分の10の累乗を掛けて整数にしてから演算し、それから10の累乗分した数を割って正しい値を取得するというもの。最後の10の累乗で割るときに誤差が発生してしまうのではないかと思いそうですが、割られる方の数が整数であれば誤差は発生しないようです(例えば、『12.35/10』の結果は『1.2349999999999999』となるけど、『1235/1000』の結果は『1.235』となる)。
というわけで、小数の演算に対応した関数を書いてみました。
// 小数対応の足し算 function decimalAddition(a, b){ if(!isFinite(a) || !isFinite(b)){ throw "数値を指定してください"; } // 指数表記は未対応 if( (a.toString().indexOf('e') != -1) || (b.toString().indexOf('e') != -1) ){ throw "指数表記は未対応です"; } var aArr = a.toString().split('.'), bArr = b.toString().split('.'); var aDecimalPart = (aArr.length === 2) ? aArr[1].length : 0; var bDecimalPart = (bArr.length === 2) ? bArr[1].length : 0; var longDecimalPart = (aDecimalPart > bDecimalPart) ? aDecimalPart : bDecimalPart; // 小数部を整数にしたうえで演算する var transCalcNumber = (a * Math.pow(10, longDecimalPart)) + (b * Math.pow(10, longDecimalPart)); return transCalcNumber / Math.pow(10, longDecimalPart); } // 小数対応の引き算 function decimalSubstract(a, b){ if(!isFinite(a) || !isFinite(b)){ throw "数値を指定してください"; } // 指数表記は未対応 if( (a.toString().indexOf('e') != -1) || (b.toString().indexOf('e') != -1) ){ throw "指数表記は未対応です"; } var aArr = a.toString().split('.'), bArr = b.toString().split('.'); var aDecimalPart = (aArr.length === 2) ? aArr[1].length : 0; var bDecimalPart = (bArr.length === 2) ? bArr[1].length : 0; var longDecimalPart = (aDecimalPart > bDecimalPart) ? aDecimalPart : bDecimalPart; // 小数部を整数にしたうえで演算する var transCalcNumber = (a * Math.pow(10, longDecimalPart)) - (b * Math.pow(10, longDecimalPart)); return transCalcNumber / Math.pow(10, longDecimalPart); } // 小数対応の掛け算 function decimalMultiplication(a, b){ if(!isFinite(a) || !isFinite(b)){ throw "数値を指定してください"; } // 指数表記は未対応 if( (a.toString().indexOf('e') != -1) || (b.toString().indexOf('e') != -1) ){ throw "指数表記は未対応です"; } var aArr = a.toString().split('.'), bArr = b.toString().split('.'); var aDecimalPart = (aArr.length === 2) ? aArr[1].length : 0; var bDecimalPart = (bArr.length === 2) ? bArr[1].length : 0; // 小数部を整数にしたうえで演算する var transCalcNumber = (a * Math.pow(10, aDecimalPart)) * (b * Math.pow(10, bDecimalPart)); // 正しい値になるように除算する数を求める var lastCalcNumber = Math.pow(10, aDecimalPart + bDecimalPart); return transCalcNumber / lastCalcNumber; } // 小数対応の割り算 function decimalDivision(a, b){ if(!isFinite(a) || !isFinite(b)){ throw "数値を指定してください"; } // 指数表記は未対応 if( (a.toString().indexOf('e') != -1) || (b.toString().indexOf('e') != -1) ){ throw "指数表記は未対応です"; } var aArr = a.toString().split('.'), bArr = b.toString().split('.'); var aDecimalPart = (aArr.length === 2) ? aArr[1].length : 0; var bDecimalPart = (bArr.length === 2) ? bArr[1].length : 0; var longDecimalPart = (aDecimalPart > bDecimalPart) ? aDecimalPart : bDecimalPart; // 小数部が長いほうにあわせて整数にしたうえで割り算を行う return (a * Math.pow(10, longDecimalPart)) / (b * Math.pow(10, longDecimalPart)); }
足し算、引き算、掛け算、割り算の4つの関数がありますが、上半分はすべて同じです。本来ならこの部分は共通化しておいたほうがいいでしょうね。エラー処理は余計だったかもしれません(この処理がなくても、どのみち例外エラーが発生するのだし、throwで返すのではなく、0やnullを返すとかでもいいかも)。
エラー処理はまず引数が数値かどうかチェックし、その後に指数表記かどうかを調べています。
実行した結果は下記のとおりです(『//=>』より右側が実行結果)。
// 足し算 console.log( 1+1.235 ); //=> 2.2350000000000003 console.log( decimalAddition(1, 1.235) ); //=> 2.235 console.log( 0.2+0.1 ); //=> 0.30000000000000004 console.log( decimalAddition(0.2, 0.1) ); //=> 0.3 // 引き算 console.log( 10-9.9999 ); //=> 0.00009999999999976694 console.log( decimalSubstract(10, 9.9999) ); //=> 0.0001 console.log( 9.9999-9 ); //=> 0.9999000000000002 console.log( decimalSubstract(9.9999, 9) ); //=> 0.9999 // 掛け算 console.log( 0.2*3 ); //=> 0.6000000000000001 console.log( decimalMultiplication(0.2, 3) ); //=> 0.6 console.log( 0.33*0.33 ); //=> 0.10890000000000001 console.log( decimalMultiplication(0.33, 0.33) ); //=> 0.1089 // 割り算 console.log( 10.89/100 ); //=> 0.10890000000000001 console.log( decimalDivision(10.89, 100) ); //=> 0.1089 console.log( 0.6/0.2 ); //=> 2.9999999999999996 console.log( decimalDivision(0.6, 0.2) ); //=> 3
ところで今回、小数って英語でなんていうんだけと思って調べて、英語でdecimalということを初めて知りました。decimalというと、10進数というイメージがあるのだけど、小数という意味もあったのかと。どっちの意味で使ってるか分かりづらいことってないのだろうか。
コメント
この方法は、筋が悪いです。小数は不正確数で、ピッタリ行かないほうが正解です。結果を有効数字にあわせて四捨五入したり、切り捨てたりするのが、正しいやり方です。