Проблема зі значеннями з плаваючою комою полягає в тому, що вони намагаються представляти нескінченну кількість (безперервних) значень з фіксованою кількістю біт. Отже, природно, в грі повинні бути певні втрати, і ви збираєтеся бути покусані якимись значеннями.
Якщо комп’ютер зберігає 1,275 як значення з плаваючою комою, він насправді не пам’ятатиме, був він 1,275 або 1,227499999999999993, або навіть 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 рекомендує округлення до найближчого, прив'язується навіть до поведінки за замовчуванням для операцій з плаваючою комою. Такі модифікації залишаються читачем як вправа :)