JavaScript で演算結果の誤差を目立たなくする (unibon)

2001年10月21日: 新規作成。
JavaScript で足し算をするとたとえば、
0.01 + 0.05 = 0.060000000000000005

0.01 + 0.06 = 0.06999999999999999
のように 0 や 9 がたくさん並ぶことがあります。
これは結局は 2 進数と 10 進数の変換に絡むもの なので、根本的にはあくまでも計算も10進数をもとにしておこなうことで解決すべきものです。

ただ、あらかじめ演算結果が有限の桁数以下であることが確実ならば、
toFixed 関数 http://www.microsoft.com/japan/developer/scripting/jscript/doc/jsmthtoFixed.htm
のように、桁数を指定して丸めてしまうことで回避できます。

今回は、これを一歩推し進めて、桁数を自動判断して丸めるようにして、利用者に桁数を意識させないような仕組みを作ってみました。
と言っても、演算結果の情報だけから必要な桁数を判断することは理論的にできません。 しかし、演算のための入力用の引数が持つ桁数情報を入手することで、演算結果に要求される桁数が分かります。したがって、演算結果を補正する関数としてではなく、加減乗の関数という形態になっています。
ロジックについてはここではとくに説明は書きませんが、長くはないコードなので解析は用意でしょう。
使用上の注意としては、これはあくまでも入力が 10 進数を前提とした値に使うものです。10 進数の世界で誤差がなくなるように演算結果を補正しているため、逆にその補正が 2 進数の世界では誤差となっています。

なお、除算は理論的に難しいと考えていますので、実装していません。 たとえば数学的に 15 / 6 = 2.5 ピッタリになるとしても、これがピッタリであることを求めるロジックは難しそうです。これを求めるくらいならば内部処理を 10 進数で計算してしまったほうが手っ取り早いかもしれません。
ただ、除算の代わりに逆数を乗算することで回避することができることも多いです。たとえば 30 / 5 は 30 * 0.2 に置き換えることができ、これにより mul 関数を使うことができます。

実際に、このページに JavaScript のコードが埋め込んであります。
以下のフォーム中の計算ボタンを押して結果を比較してみてください。

+
=
(+ を使用)
(add 関数を使用)

-
=
(- を使用)
(sub 関数を使用)

*
=
(* を使用)
(mul 関数を使用)


10進数を考慮した加減乗関数(add, sub, mul)
(まだ、ベータ版です。もう少しテストが必要です。VBScript 版も鋭意作成中です。)
function decimalOperator(t, a, b) {
    var x = "" + a;
    var p = x.indexOf(".");
    var m;
    if (p >= 0) {
        m = x.length - (p + 1);
    } else {
        m = 0;
    }

    var y = "" + b;
    var q = y.indexOf(".");
    var n;
    if (q >= 0) {
        n = y.length - (q + 1);
    } else {
        n = 0;
    }

    var k;
    var c;
    if (t == "+") { // add
        k = Math.max(m, n);
        c = (a - 0) + (b - 0);
    } else if (t == "-") { // sub
        k = Math.max(m, n);
        c = (a - 0) - (b - 0);
    } else if (t == "*") { // mul
        k = m + n;
        c = (a - 0) * (b - 0);
    } else {
        return null;
    }

    var z = "" + c;
    if (x.indexOf("e") >= 0 || y.indexOf("e") >= 0 || z.indexOf("e") >= 0) {
        return c;
    }

    var r = z.indexOf(".");
    var d;
    if (r >= 0) {
        var u = z.substring(r + 1 + k, r + 1 + k + 1);
        d = z.substring(0, r + 1 + k);
        if (u >= "5") {
            var e = "";
            var w = 1;
            for (var i = d.length - 1; i >= 0; i--) {
                var g = d.substring(i, i + 1);
                if (g >= "0" && g <= "9") {
                    var h = g - 0;
                    h += w;
                    w = 0;
                    if (h >= 10) {
                        h -= 10;
                        w++;
                    }
                    e = ("" + h) + e;
                } else if (w > 0 && (g == "+" || g == "-")) {
                    e = g + ("" + w) + e;
                } else {
                    e = g + e;
                }
            }
            d = e;
        }
    } else {
        d = z;
    }
    var f = d - 0;
    return f;
}

function add(a, b) {
    return decimalOperator("+", a, b);
}

function sub(a, b) {
    return decimalOperator("-", a, b);
}

function mul(a, b) {
    return decimalOperator("*", a, b);
}

呼び出し例:
var a = 0.01;
var b = 0.05;
var c = add(a, b);
var d = sub(a, b);
var e = mul(a, b);

JavaScript の目次
ホーム
(このページ自身の絶対的な URL)