JavaScriptにおける計算精度が失われた問題(一)


要約:
コンピュータはバイナリで数字を保存して処理するので、浮動小数点を正確に表現できません.JavaScriptには該当するパッケージ類がないので、浮動小数点演算を処理します.直接計算すると、演算精度が失われます.
精度の違いを避けるために、計算が必要な数字をコンピュータで正確に識別できる整数にアップグレードし、計算が完了したら降格する(10のn乗で割る).これはほとんどのプログラミング言語の処理精度の違いのための一般的な方法である.
キーワード:
計算精度は四捨五入四則で計算精度が失われます.
1.疑惑
ほとんどのプログラミング言語が通貨計算に適したクラスを提供していることを知っています.例えばC((zhi)はdecimalを提供しています.JavaはBigDecimalを提供しています.JavaScriptはNumberを提供しています.
前はdecimalとBigDecimalを使っていましたが、精度に問題がないので、JavaScriptのNumberタイプを疑ったことがありません.直接にNumberタイプを使って計算できると思います.直接使うのは問題があります.
まず四捨五入のコードを見ます.
alert(Number(0.009).toFixed(2));
alert(Number(162.295).toFixed(2));
正常な結果によって、それぞれ0.01と162.30をイジェクトするべきです.しかし、実際のテスト結果は異なるブラウザで得られたものとは異なる結果である.
ie 6、7、8の下で0.00と162.30を得て、最初の数は切り取りが正しくないです.
firefoxの中で0.01と162.29を得て、第二の数字は正しく切り取られません.
operaの下で0.01と162.29を得て、第二の数字は正しく切り取られません.
四則演算のコードをもう一度見てみます.
alert(1/3);//  : 0.3333333333333333
alert(0.1 + 0.2);// : 0.30000000000000004
alert(-0.09 - 0.01);// : -0.09999999999999999
alert(0.012345 * 0.000001);// : 1.2344999999999999e-8
alert(0.000001 / 0.0001);// : 0.009999999999999998
正常な結果によると、第一行以外は(自分では割り切れないので)、他のすべてが正確な結果を得るべきです.ポップアップの結果から、私たちが望む正しい結果ではないことが分かりました.Numberタイプに変換されていないからですか?Numberに変えてから計算してみます.
alert(Number(1)/Number(3));//  : 0.3333333333333333   	
alert(Number(0.1) + Number(0.2));// : 0.30000000000000004
alert(Number(-0.09) – Number(0.01));// : -0.09999999999999999
alert(Number(0.012345) * Number(0.000001));// : 1.2344999999999999e-8
alert(Number(0.000001) / Number(0.0001));// : 0.009999999999999998
やはり同じ結果です.javascriptは数字をnumberタイプとしてデフォルトで認識しているようです.この点を検証するために、私達はタイプをタイプタイプタイプで弾いてみます.
alert(typeof(1));//  : number
alert(typeof(1/3));// : number
alert(typeof(-0.09999999));// : number
2.原因
なぜこのような精度が失われる問題が発生しますか?javascript言語のbugですか?
大学時代に習ったコンピュータの原理を思い出します.コンピュータはバイナリ算術を実行しています.十進数を正確に二進数に変換できない場合、この精度誤差は避けられません.
javascriptの関連資料を調べてみます.javascriptの数字は全部浮動小数点で表していることを知っています.そしてIEEE 754標準を使うダブル精度浮動小数点表示を規定しています.
IEEE 754は、2つの基本浮動小数点フォーマットを規定している.単精度と二重精度.
IEEE単精度フォーマットは、24ビットの有効デジタル精度(符号を含む)を有し、合計32ビットを占める.
IEEEデュアル精度フォーマットは、53ビットの有効デジタル精度(符号を含む)を有し、合計64ビットを占有しています.
このような構造は科学的な表現法で、記号(正または負)、指数と端数で表され、底数は2に確定されます.つまり、一つの浮動小数点を端数に2をかける指数二乗として表します.具体的な規格を見てみます.
[テーブル]
𞓜𞓜指数桁の小数部124;指数のオフセット量
|単精度浮動小数点数|1位(31)|8位(30-23)|23位(22-00)124; 127|
|双精度浮動小数点数𞓜1位(63)124; 11位(62-52)|52位(51-00)124; 1023|
[/テーブル]
シングル精度の浮動小数点数で説明します.
指数は8桁です.表現できる範囲は0から255までです.
対応する実際の指数は-127から+128までです.
ここでは、-127と+128の2つのデータはIEEEにおいて保持されており、様々な用途として使用されている.
-127が示す数字は0
128と他の桁数の組み合わせは様々な意味を表し、最も典型的なのはNAN状態である.
これらを知って、私達はコンピュータの進数転換の計算をシミュレートして、簡単な0.1+0.2を探して推論しましょう.http://blog.csdn.net/xujiaxuliang/archive/2010/10/13/5939573.aspx)
   0.1  
=> 0.00011001100110011…( 0011)
=> 1.1001100110011001100…1100( 52 , 1), -4( 00000000010), 0
=> :0 00000000100 10011001100110011…11001
=> 52 , 0.00011001100110011001100110011001100110011001100110011001
0.2
=> 0.0011001100110011…( 0011)
=> 1.1001100110011001100…1100( 52 , 1), -3( 00000000011), 0
=> :0 00000000011 10011001100110011…11001
52 , 0.00110011001100110011001100110011001100110011001100110011
  :
 0.00011001100110011001100110011001100110011001100110011001
+ 0.00110011001100110011001100110011001100110011001100110011
= 0.01001100110011001100110011001100110011001100110011001100
10 :0.30000000000000004
上記のプロモーションプロセスから、この誤差は避けられないことが分かりました.cximalとJavaのBigDecimalには精度の差がないのは、その内部で相応の処理をしたからです.この精度の違いをブロックしました.javascriptは弱いタイプの足本言語です.計算精度に対しては対応する処理をしていません.これは私達が別途に解決しなければなりません.
3.解決策
3.1グレードダウン
上述のように、javascriptで精度の差が生じた原因は、コンピュータが浮動小数点を正確に表現できなくなり、自分でも正確に計算できなくなり、より正確な結果が得られなくなることが分かりました.計算する数をコンピューターに正確に認識させるにはどうすればいいですか?
十進数の整数と二進法は互いに正確に変換できると知っていますが、浮動小数点を(10のn乗を乗じて)計算機で正確に識別できる整数にアップグレードして計算します.計算が終わったら降格します.(10のn乗を割る)精確な結果が得られますか?はい、そうします.
Math.pow(10、scale)は10のscaleの二乗を得ることができると知っていますが、浮動小数点を直接にMath.pow(10、scale)にかければいいですか?最初はそう思っていましたが、後になっていくつかの数字が計算された後、実際の結果は私たちの予想と一致していませんでした.この簡単な計算を見てみましょう.
alert(512.06*100);
普通なら51206に戻るはずですが、実際の結果は51205.99999999です.おかしいでしょう?これは浮動小数点が正確に乗算に参加できないからです.この演算が特殊でも(10を掛けたscaleの二乗だけでアップグレードします.)これでは10のscaleを直接乗じてアップグレードすることができません.小数点を自分で動かしましょう.
小数点をどうやって移動すればいいですか?みんなそれぞれの妙技があります.ここに私の書いたいくつかの方法を添付します.
/**
*
*
* @param nSize
*
* @param ch
*
* @return
*/
String.prototype.padLeft = function(nSize, ch)
{
var len = 0;
var s = this ? this : "";
ch = ch ? ch : '0';// 0

len = s.length;
while (len < nSize)
{
s = ch + s;
len++;
}
return s;
}

/**
*
*
* @param nSize
*
* @param ch
*
* @return
*/
String.prototype.padRight = function(nSize, ch)
{
var len = 0;
var s = this ? this : "";
ch = ch ? ch : '0';// 0

len = s.length;
while (len < nSize)
{
s = s + ch;
len++;
}
return s;
}
/**
* ( , Math.pow(10,scale))
*
* @param scale
*
* @return
*/
String.prototype.movePointLeft = function(scale)
{
var s, s1, s2, ch, ps, sign;
ch = '.';
sign = '';
s = this ? this : "";

if (scale <= 0) return s;
ps = s.split('.');
s1 = ps[0] ? ps[0] : "";
s2 = ps[1] ? ps[1] : "";
if (s1.slice(0, 1) == '-')
{
s1 = s1.slice(1);
sign = '-';
}
if (s1.length <= scale)
{
ch = "0.";
s1 = s1.padLeft(scale);
}
return sign + s1.slice(0, -scale) + ch + s1.slice(-scale) + s2;
}
/**
* ( , Math.pow(10,scale))
*
* @param scale
*
* @return
*/
String.prototype.movePointRight = function(scale)
{
var s, s1, s2, ch, ps;
ch = '.';
s = this ? this : "";

if (scale <= 0) return s;
ps = s.split('.');
s1 = ps[0] ? ps[0] : "";
s2 = ps[1] ? ps[1] : "";
if (s2.length <= scale)
{
ch = '';
s2 = s2.padRight(scale);
}
return s1 + s2.slice(0, scale) + ch + s2.slice(scale, s2.length);
}
/**
* ( , ( / )Math.pow(10,scale))
*
* @param scale
* ( ; ;0 )
* @return
*/
String.prototype.movePoint = function(scale)
{
if (scale >= 0)
return this.movePointRight(scale);
else
return this.movePointLeft(-scale);
}
このようにレベルを下げて文字列に変換してSteringオブジェクトのカスタム方法movePointを呼び出します.10を掛けたscaleの二乗は正の整数scaleを伝えます.10のscaleの二乗を除いて負の整数-scaleを伝えます.
また、私達は以前512.06のコードをアップグレードしました.カスタム方法を採用したコールコードはこうなりました.
alert(512.06.toString().movePoint(2)); //  : 51206
このように直接小数点を移動すると、話を聞かずに長い数字が出てくるのが怖くなります.もちろん、movePointメソッドで得られた結果は文字列です.Numberタイプにするには便利です.
3.2四捨五入
はい、レベルアップの基礎があります.四捨五入の方法を見に来ました.いろいろなブラウザがNumberのtoFixed方法に対して異なるサポートがありますので、私達は自分の方法でブラウザのデフォルトの実現をカバーする必要があります.
簡単な方法があります.データを切り取る後の桁が5以上かどうかを自分で判断してから捨てようとします.Math.ceir方法は指定数以上の最小整数を取ることを知っています.Math.flor方法は指定数以下の最大整数を取ることです.そこで、この二つの方法を使って丸め処理を行います.まず切り込みを行う数を切り上げるビットscale(10を乗じたscaleの次数)をアップグレードして、ceilまたはflorを行って整理した後、降格して桁数のscaleを切ります.(10のscale次数で割る)
コードは以下の通りです
Number.prototype.toFixed = function(scale)
{
var s, s1, s2, start;

s1 = this + "";
start = s1.indexOf(".");
s = s1.movePoint(scale);

if (start >= 0)
{
s2 = Number(s1.substr(start + scale + 1, 1));
if (s2 >= 5 && this >= 0 || s2 < 5 && this < 0)
{
s = Math.ceil(s);
}
else
{
s = Math.floor(s);
}
}

return s.toString().movePoint(-scale);
}
NumberタイプのtoFixed方法をカバーした後、以下の方法を実行します.
alert(Number(0.009).toFixed(2));//  0.01
alert(Number(162.295).toFixed(2));// 162.30
ie 6、7、8、firefox、Operaの下でそれぞれ検証して、すべて相応する正しい結果を得ることができます.
もう一つの方法はネットで見つけた正規表現を使って四捨五入します.コードは以下の通りです.
Number.prototype.toFixed = function(scale)
{
var s = this + "";
if (!scale) scale = 0;
if (s.indexOf(".") == -1) s += ".";
s += new Array(scale + 1).join("0");
if (new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (scale + 1) + "})?)\\d*$").test(s))
{
var s = "0" + RegExp.$2, pm = RegExp.$1, a = RegExp.$3.length, b = true;
if (a == scale + 2)
{
a = s.match(/\d/g);
if (parseInt(a[a.length - 1]) > 4)
{
for (var i = a.length - 2; i >= 0; i--)
{
a[i] = parseInt(a[i]) + 1;
if (a[i] == 10)
{
a[i] = 0;
b = i != 1;
}
else
break;
}
}
s = a.join("").replace(new RegExp("(\\d+)(\\d{" + scale + "})\\d$"), "$1.$2");
}
if (b) s = s.substr(1);
return (pm + s).replace(/\.$/, "");
}
return this + "";
}
経験証では、この二つの方法は正確な四捨五入ができますが、どの方法を採用すればいいですか?本当の知識を実践して、簡単な方法を書いて、二つの方式の性能を検証してみます.
function testRound()
{
var dt, dtBegin, dtEnd, i;
dtBegin = new Date();
for (i=0; i<100000; i++)
{
dt = new Date();
Number("0." + dt.getMilliseconds()).toFixed(2);
}
dtEnd = new Date();
alert(dtEnd.getTime()-dtBegin.getTime());
}
同じ数字を四捨五入してキャッシュすることを避けるために、現在のミリ秒数を四捨五入します.経験証は同じマシンで10万回計算した場合、movePoint方法で平均2500ミリ秒かかります.正規表現では、平均4000ミリ秒かかります.