Asial Blog

Recruit! Asialで一緒に働きませんか?

JavaScriptによる小数計算の誤差を無くす

カテゴリ :
フロントエンド(HTML5)
タグ :
Tech
JavaScript
小数
浮動小数点
こんちは。松田です。
最近もJavaScriptばっかり書いています。
JavaScriptで計算を行う時、気を付けなければいけないのが小数点を含んだ数値の計算です。
小数点を含んだ数値で計算を行うと、思わぬ所で予想外の値に出会うことになります。


例えば以下の様な仕様があったとします。

  1. 昨日の体重から今日の体重を引いて、体重の増減量を表示させる。
  2. 増減量の小数点第2位以下は切り捨てて表示。

これを単純に実装すると以下のようになります。

  1. var oldValue, newValue, diff;
  2. oldValue = 67;
  3. newValue = 66.9;
  4. diff = oldValue - newValue;
  5. diff = Math.floor(diff * 10) / 10; // 小数点第2位以下切り捨て
  6.  
  7. console.log("今日は" + diff + "kgやせました!");

そして実行結果です。

  1. 今日は0kgやせました!
ファッ!?

0.1kg痩せたはずが、0kgと表示されてしまいました。
たかだか100gとはいえ、こんな表示になったらガックリきますね。

一行ずつ値を追っていくとわかるのですが、4行目で減算した時点でdiffの値が "0.09999999999999432" とズレてしまっています。

これはJavaScriptがIEEE 754という規格に従って実装されているためです。
つまり、この計算結果はJavaScriptの仕様なのでJavaScript的には正しい値であり、避けようがありません。
このIEEE 754と小数計算の誤差についての関係は下記のURLの解説が分かりやすくオススメです。
http://pc.nikkeibp.co.jp/pc21/special/gosa/eg4.shtml


これの対処法として、小数値に10^Nの数値をかけて整数値にしてから計算してしまおう!
といった対処法を見かけることもありますが、厳密にはこれも間違いです。
  1. console.log(66.9 * 10);   // => 669 
  2. console.log(66.9 * 100); // => 6690.000000000001
上記のように、小数を整数値に変換しようとした時点で計算結果がズレてしまうことがあるので、正確な値が出せるとは言えません。
(...でも 66.9 * 10 * 10 で計算すると正確に出せたりします。)


というわけで前置きが長くなってしまいましたが、小数の減算・乗算を正確に計算するfunctionを作ってみました。

  1. /**
  2.  * Mathオブジェクトを拡張 
  3.  */
  4. var Math = Math || {};
  5.  
  6.  
  7. /**
  8.  * 与えられた値の小数点以下の桁数を返す 
  9.  * multiply, subtractで使用
  10.  * 
  11.  * 例)
  12.  *   10.12  => 2  
  13.  *   99.999 => 3
  14.  *   33.100 => 1
  15.  */
  16. Math._getDecimalLength = function(value) {
  17.     var list = (value + '').split('.'), result = 0;
  18.     if (list[1] !== undefined && list[1].length > 0) {
  19.         result = list[1].length;
  20.     }
  21.     return result;
  22. };
  23.  
  24.  
  25. /**
  26.  * 乗算処理
  27.  *
  28.  * value1, value2から小数点を取り除き、整数値のみで乗算を行う。 
  29.  * その後、小数点の桁数Nの数だけ10^Nで除算する
  30.  */
  31. Math.multiply = function(value1, value2) {
  32.     var intValue1 = +(value1 + '').replace('.', ''),
  33.         intValue2 = +(value2 + '').replace('.', ''),
  34.         decimalLength = Math._getDecimalLength(value1) + Math._getDecimalLength(value2),
  35.         result;
  36.  
  37.     result = (intValue1 * intValue2) / Math.pow(10, decimalLength);
  38.  
  39.     return result;
  40. };
  41.  
  42.  
  43. /**
  44.  * 減算処理
  45.  *
  46.  * value1,value2を整数値に変換して減算
  47.  * その後、小数点の桁数分だけ小数点位置を戻す
  48.  */
  49. Math.subtract = function(value1, value2) {
  50.     var max = Math.max(Math._getDecimalLength(value1), Math._getDecimalLength(value2)),
  51.         k = Math.pow(10, max);
  52.     return (Math.multiply(value1, k) - Math.multiply(value2, k)) / k;
  53. };
  54.  
  55.  
  56. // 減算テスト
  57. console.log(67 - 66.9);                 // => 0.09999999999999432  NG
  58. console.log(Math.subtract(67, 66.9));   // => 0.1  OK
  59.  
  60. // 乗算テスト
  61. console.log(66.9 * 100);               // => 6690.000000000001  NG
  62. console.log(Math.multiply(66.9, 100)); // => 6690  OK

Math.subtract()で減算、Math.multiply()で乗算を行います。
基本的には全て整数値に変換してからの計算を行なっているのですが、 整数値に変換する際に10のN乗を掛けるのではなく、数値文字列からドットを取り除くようにしています。

これで小数点計算は誤差なく出来るようになったんじゃないかと思います。
正確なテストを行ったわけではないのでどこかに見落としがあるかもしれませんが、この記事で小数が原因のバグに出会う人が少しでも少なくなれば幸いです。

コメント

  • r-de-r

    C にある printf がないのが,諸悪の根源で,それを実装することなく一時しのぎに
    Math.floor(diff * 10) / 10
    とするのがエラーのおおもと。
    せめて,
    Math.floor(diff * 10+0.5) / 10
    とすれば,問題が顕在化することも少ないかも知れない。+0.5 してから floor して元に戻すというのが良策だろう(数値計算の定石)。
    0.5 を足すのか,それ以外の適切な数値を足すのか,考えてもあまりメリットはないと思う。

  • 通りすがり

    提示された実装だと、うまくいかないケースが出てきます。
    たとえば以下の場合です。

    Math.multiply(0.00000000123, 100); /* 1.2300000000000001e-10 */
    0.00000000123 * 100; /* 1.23e-7 */

    Math.subtract(0.00000000123, 100); /* -99.99999999999876 */
    0.00000000123 - 100; /* -99.99999999877 */

    明らかに桁が異なっています。
    この違いは、オペランドの十進文字列表現を考えるとわかると思います。

  • 通りすがり2

    横からすみません。

    > var intValue1 = +(value1 + '').replace('.', ''),
    > intValue2 = +(value2 + '').replace('.', ''),

    上の二つを "+" ではなく parseIntで変換してはいかがでしょうか?
    例)var intValue1 = parseInt( (value1 + '').replace('.', ''), 10);

  • AaronKim

    勉強になりました。ありがとうございます。

コメントフォーム



captcha_key

アシアルの会社情報

アシアル株式会社はPHP、HTML5、JavaScriptに特化したWebエンジニアリング企業です。ユーザーエクスペリエンス設計から大規模システム構築まで、アシアルメンバーが各々の専門性を通じてインターネットの進化に貢献します。

会社情報詳細

最近の記事