浮動小数点値の問題は、固定量のビットで無限の(連続した)値を表現しようとしていることです。当然のことながら、プレイにはある程度の損失が必要であり、いくつかの値で噛まれるでしょう。
コンピュータが1.275を浮動小数点値として格納する場合、それが1.275、1.27499999999999993、または1.27500000000000002のいずれであったかを実際には記憶しません。これらの値は、小数点以下2桁に四捨五入した後、異なる結果になるはずですが、コンピューターでは浮動小数点値として格納した後はまったく同じに見え、失われたデータを復元する方法がないため、結果は異なります。それ以降の計算では、そのような不正確さが蓄積されるだけです。
したがって、精度が重要な場合は、最初から浮動小数点値を回避する必要があります。最も簡単なオプションは
- 献身的なライブラリを使用する
- 文字列を使用して値を格納および渡します(文字列操作を伴う)
- 整数を使用します(たとえば、実際の値の100分の1の量を渡すことができます。たとえば、ドルの金額ではなくセントの金額)
たとえば、整数を使用して100分の1の数を格納する場合、実際の値を見つける関数は非常に簡単です。
function descale(num, decimals) {
var hasMinus = num < 0;
var numString = Math.abs(num).toString();
var precedingZeroes = '';
for (var i = numString.length; i <= decimals; i++) {
precedingZeroes += '0';
}
numString = precedingZeroes + numString;
return (hasMinus ? '-' : '')
+ numString.substr(0, numString.length-decimals)
+ '.'
+ numString.substr(numString.length-decimals);
}
alert(descale(127, 2));
文字列では、丸めが必要になりますが、それでも管理できます。
function precise_round(num, decimals) {
var parts = num.split('.');
var hasMinus = parts.length > 0 && parts[0].length > 0 && parts[0].charAt(0) == '-';
var integralPart = parts.length == 0 ? '0' : (hasMinus ? parts[0].substr(1) : parts[0]);
var decimalPart = parts.length > 1 ? parts[1] : '';
if (decimalPart.length > decimals) {
var roundOffNumber = decimalPart.charAt(decimals);
decimalPart = decimalPart.substr(0, decimals);
if ('56789'.indexOf(roundOffNumber) > -1) {
var numbers = integralPart + decimalPart;
var i = numbers.length;
var trailingZeroes = '';
var justOneAndTrailingZeroes = true;
do {
i--;
var roundedNumber = '1234567890'.charAt(parseInt(numbers.charAt(i)));
if (roundedNumber === '0') {
trailingZeroes += '0';
} else {
numbers = numbers.substr(0, i) + roundedNumber + trailingZeroes;
justOneAndTrailingZeroes = false;
break;
}
} while (i > 0);
if (justOneAndTrailingZeroes) {
numbers = '1' + trailingZeroes;
}
integralPart = numbers.substr(0, numbers.length - decimals);
decimalPart = numbers.substr(numbers.length - decimals);
}
} else {
for (var i = decimalPart.length; i < decimals; i++) {
decimalPart += '0';
}
}
return (hasMinus ? '-' : '') + integralPart + (decimals > 0 ? '.' + decimalPart : '');
}
alert(precise_round('1.275', 2));
alert(precise_round('1.27499999999999993', 2));
この関数は0から離れて最も近い値に丸めますが、IEEE 754は浮動小数点演算のデフォルトの動作としても、最も近い値に丸めることを推奨しています。そのような変更は読者のための練習問題として残されています:)