Проблема со значениями с плавающей запятой заключается в том, что они пытаются представить бесконечное количество (непрерывных) значений с фиксированным количеством битов. Естественно, в игре должны быть некоторые потери, и вы будете укушены некоторыми ценностями.
Когда компьютер хранит 1,275 как значение с плавающей запятой, он на самом деле не будет помнить, было ли это 1,275 или 1,27499999999999993 или даже 1,27500000000000002. Эти значения должны давать разные результаты после округления до двух десятичных знаков, но они не будут, поскольку для компьютера они выглядят точно так же после сохранения в виде значений с плавающей запятой, и нет никакого способа восстановить потерянные данные. Любые дальнейшие вычисления только накапливают такую неточность.
Так что, если точность имеет значение, вы должны избегать значений с плавающей точкой с самого начала. Самые простые варианты
- использовать выделенную библиотеку
- использовать строки для хранения и передачи значений (сопровождается строковыми операциями)
- используйте целые числа (например, вы можете передавать сумму, равную сотым от вашей фактической стоимости, например, сумма в центах вместо суммы в долларах)
Например, при использовании целых чисел для хранения числа сотых функция поиска действительного значения довольно проста:
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));
Обратите внимание, что эта функция округляет до ближайшего, связывает от нуля , в то время как IEEE 754 рекомендует округлять до ближайшего, даже в качестве поведения по умолчанию для операций с плавающей запятой. Такие модификации оставлены в качестве упражнения для читателя :)