Проблема со значениями с плавающей точкой заключается в том, что они пытаются представить бесконечное количество (непрерывных) значений с фиксированным количеством битов. Естественно, в игре должны быть некоторые потери, и вы будете кусаться с некоторыми значениями.
Когда компьютер хранит 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 рекомендует округлять до ближайшего, связывается с четным в качестве поведения по умолчанию для операции с плавающей запятой. Такие модификации оставлены в качестве упражнения для читателя:)