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

2001年11月04日: 新規作成。
2002年09月07日: 指数表示に対応。Java 版および VB/VBScript 版を追加。
2002年09月09日: -0.9999999999999999 が -1 にならず 1 になってしまうバグを修正。 1.16 - 1.13 = 0.030000000000000026 も 0.03 にできるように処理を追加。
JavaScript で足し算をするとたとえば、
0.01 + 0.05 = 0.060000000000000005

0.01 + 0.06 = 0.06999999999999999
のように 0 や 9 がたくさん並ぶことがあります。
と、ここまでは JavaScript で演算結果の誤差を目立たなくする と同じ話ですが、今回(パート 2)は違うソリューションを試みてみました。
JavaScript で演算結果の誤差を目立たなくする では、加算・減算・乗算のみ対応可能でしたが、今回は除算にも対応可能です。というより、別段、演算の種類を問わず、どんな演算をおこなった後でも、その演算結果を trimFixed という「魔法」の関数に与えることにより、自動的に端数を取り除いて「奇麗」な出力結果を得ることができます。

ただ、注意していだだきたいのは、これは結局は、奇麗さと引き換えに、精度を落としていることになる、という点です。
したがって、これを使ってよい状況というのは、「演算結果として 0 や 9 がたくさん並ぶことが必ずない」ということがあらかじめ保証されている場合だけです。換言すれば、必ず結果が有限の桁数になると分かっている演算であり、有限の桁数同士の加算・減算・乗算や、特別な除算の場合のみです。
ちなみに、はなから JavaScript (に限ったことではありませんが) の処理系が、このような補正処理をおこなっておけばよいという考えも、まったくできなくはありません。しかし、演算結果を文字列として伝送したり、ファイルとして保存・復元するような場合は、このような補正をすると、確実に精度が落ちてしまうので、好ましくありません。

ロジックについて、ごく簡単に解説しますと、まずダミーとして 1 / 3 を計算して、動作している処理系で扱える有効桁数を求めます。
そして、与えられた数値の桁数がその有効桁数を目一杯使っている場合、最下位の桁は誤差が含まれているとみなして無視し、その桁よりひとつ上位の桁が 0 か 9 であれば、その 0 か 9 が延々と続くまでひとつづつ上位の桁を調べます。
0 か 9 以外の桁に出会った時点で、その桁よりも下位の桁は切り捨てます。なお、今まで 9 を探して来ていた場合は切り上げる必要があるので、別途、切り上げの処理をおこなっています。
ちなみに指数表示(たとえば 1.23456e-7 など)にも対応しています。

2002年09月09日 追加: 当初から trimFixed(-0.9999999999999999) が -1 にならず 1 になってしまうバグがあることが分かりましたので、これを修正しました(これは致命的なバグでした)。 また、1.16 - 1.13 = 0.030000000000000026 ですがこれを trimFixed に渡しても 0.030000000000000026 のままになり 0.03 にならなかったバグも修正しました(こちらは致命的ではありませんが、好ましくない挙動でした)。

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

加算:
+
=
(+ を使用しただけ)
(+ の後に trimFixed 関数を適用)

減算:
-
=
(- を使用しただけ)
(- の後に trimFixed 関数を適用)

乗算:
*
=
(* を使用しただけ)
(* の後に trimFixed 関数を適用)

除算:
/
=
(/ を使用しただけ)
(/ の後に trimFixed 関数を適用)

乗算(指数表示):
*
=
(* を使用しただけ)
(* の後に trimFixed 関数を適用)

今回は JavaScript 以外に Java および VB/VBScript に書き換えたものも示します。

VB/VBScript について補足:
なお、VB/VBScript については実際にはプログラムは不要です。 VBScript は数値を文字列として表示する際には、すでに丸めをおこなっているためです。 ただし、数値としては丸めない状態(精度が高い状態)で保持しているため、演算を繰り返すと端数が累積して、その繰り返された演算結果を文字列として表現すると端数が出てしまいます。 したがって演算のたびに補正はやはり必要ですが、これは単に数値を一旦文字列型に変換(CStr 関数)し、それを再び数値に戻す(CDbl 関数)だけでよく、簡単です。 これについては VBScript での演算誤差とその対処について を参照してください。 もっとも、VB/VBScript には通貨型(Currency 型、CCur 関数により変換)があり、さらに VB には Decimal 型(CDec 関数により変換)があるので、多くの場合はこれらの型・関数の使用でも対処できます。


trimFixed 関数(JavaScript 版):
function trimFixed(a) {
    var x = "" + a;
    var m = 0;
    var e = x.length;
    for (var i = 0; i < x.length; i++) {
        var c = x.substring(i, i + 1);
        if (c >= "0" && c <= "9") {
            if (m == 0 && c == "0") {
            } else {
                m++;
            }
        } else if (c == " " || c == "+" || c == "-" || c == ".") {
        } else if (c == "E" || c == "e") {
            e = i;
            break;
        } else {
            return a;
        }
    }

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

    if (m < n) {
        return a;
    }

    var p = x.indexOf(".");
    if (p == -1) {
        return a;
    }
    var w = " ";
    for (var i = e - (m - n) - 1; i >= p + 1; i--) {
        var c = x.substring(i, i + 1);
        if (i == e - (m - n) - 1) {
            continue;
        }
        if (i == e - (m - n) - 2) {
            if (c == "0" || c == "9") {
                w = c;
                continue;
            } else {
                return a;
            }
        }
        if (c != w) {
            if (w == "0") {
                var z = (x.substring(0, i + 1) + x.substring(e, x.length)) - 0;
                return z;
            } else if (w == "9") {
                var z = (x.substring(0, i) + ("" + ((c - 0) + 1)) + x.substring(e, x.length)) - 0;
                return z;
            } else {
                return a;
            }
        }
    }
    if (w == "0") {
        var z = (x.substring(0, p) + x.substring(e, x.length)) - 0;
        return z;
    } else if (w == "9") {
        var z = x.substring(0, p) - 0;
        var f;
        if (a > 0) {
            f = 1;
        } else if (a < 0) {
            f = -1;
        } else {
            return a;
        }
        var r = (("" + (z + f)) + x.substring(e, x.length)) - 0;
        return r;
    } else {
        return a;
    }
}
呼び出し例:
var a = 0.01;
var b = 0.05;
var c = a + b;
var x = trimFixed(c);

trimFixed 関数(Java 版):
public class Unibon {
    public static double trimFixed(double a) {
        String x = String.valueOf(a);
        int m = 0;
        int e = x.length();
        for (int i = 0; i < x.length(); i++) {
            int c = x.charAt(i);
            if (c >= '0' && c <= '9') {
                if (m == 0 && c == '0') {
                } else {
                    m++;
                }
            } else if (c == ' ' || c == '+' || c == '-' || c == '.') {
            } else if (c == 'E' || c == 'e') {
                e = i;
                break;
            } else {
                return a;
            }
        }
    
        double b = 1.0 / 3.0;
        String y = String.valueOf(b);
        int q = y.indexOf(".");
        int n;
        if (q >= 0) {
            n = y.length() - (q + 1);
        } else {
            return a;
        }
    
        if (m < n) {
            return a;
        }
    
        int p = x.indexOf(".");
        if (p == -1) {
            return a;
        }
        char w = ' ';
        for (int i = e - (m - n) - 1; i >= p + 1; i--) {
            char c = x.charAt(i);
            if (i == e - (m - n) - 1) {
                continue;
            }
            if (i == e - (m - n) - 2) {
                if (c == '0' || c == '9') {
                    w = c;
                    continue;
                } else {
                    return a;
                }
            }
            if (c != w) {
                if (w == '0') {
                    Double z = new Double(x.substring(0, i + 1) + x.substring(e, x.length()));
                    return z.doubleValue();
                } else if (w == '9') {
                    Double z = new Double(x.substring(0, i) + String.valueOf((c - '0') + 1) + x.substring(e, x.length()));
                    return z.doubleValue();
                } else {
                    return a;
                }
            }
        }
        if (w == '0') {
            Double z = new Double(x.substring(0, p) + x.substring(e, x.length()));
            return z.doubleValue();
        } else if (w == '9') {
            Double z = new Double(x.substring(0, p));
            int f;
            if (a > 0) {
                f = 1;
            } else if (a < 0) {
                f = -1;
            } else {
                return a;
            }
            Double r = new Double(String.valueOf(z.doubleValue() + f) + x.substring(e, x.length()));
            return r.doubleValue();
        } else {
            return a;
        }
    }
}
使用例:
public class Test {
    public static void main(String[] args) {
        double a = 0.01;
        double b = 0.05;
        double c = a + b;
        double x = Unibon.trimFixed(c);
        System.out.println("" + a + " + " + b + " = " + c + "(補正なし), " + x + "(補正あり)");
    }
}

trimFixed 関数(VB/VBScript 版):
Function trimFixed(ByVal a)
    trimFixed = CDbl(CStr(a))
End Function
使用例(WSH):
Dim x
x = 0
Dim y
y = 0
Dim i
For i = 0 To 10000 - 1
    x = x + 0.0001
    y = trimFixed(y + 0.0001)
Next
Call WScript.Echo("補正なし = " & x & ", 補正あり = " & y)


このページの古いバージョンは こちら です。
JavaScript の目次
ホーム
(このページ自身の絶対的な URL)