Це питання є складним.
Припустимо, у нас є функція, roundTo2DP(num)
яка приймає float як аргумент і повертає значення, округлене до двох десяткових знаків. До чого повинен оцінювати кожен із цих виразів?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
Очевидна відповідь полягає в тому, що перший приклад повинен закруглюватися до 0,01 (тому що це ближче до 0,01, ніж до 0,02), а інші два повинні округлювати до 0,02 (тому що 0,0150000000000000001 ближче до 0,02, ніж до 0,01, а тому, що 0,015 знаходиться точно на півдорозі між їх і існує математична умова, що такі числа округляються).
Улов, про який ви, можливо, здогадалися, полягає в тому, що roundTo2DP
неможливо здійснити такі очевидні відповіді, оскільки всі три числа, передані йому, є однаковим числом . Двоєчні номери з плаваючою комою IEEE 754 (вид, який використовується JavaScript) не можуть точно представляти більшість не цілих чисел, і тому всі три числові літерали вище округляються до дійсного номера з плаваючою точкою поблизу. Це число, як це відбувається, саме так
0.01499999999999999944488848768742172978818416595458984375
що ближче до 0,01, ніж до 0,02.
Ви можете бачити, що всі три номери однакові на консолі браузера, оболонці вузла чи іншому інтерпретаторі JavaScript. Просто порівняйте їх:
> 0.014999999999999999 === 0.0150000000000000001
true
Тож коли я пишу m = 0.0150000000000000001
, точне значення, вm
якому я закінчуюсь, ближче, 0.01
ніж це 0.02
. І все-таки, якщо я переходжу m
на String ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... Я отримую 0,015, що повинно закруглюватися до 0,02, і це помітно не 56-десяткове місце, про яке я раніше говорив, що всі ці числа були точно рівними. То яка це темна магія?
Відповідь можна знайти в специфікації ECMAScript, в розділі 7.1.12.1: ToString, застосований до типу Number . Тут викладені правила перетворення деякого числа m у рядок. Ключова частина - точка 5, в якій генерується ціле число , чиї цифри будуть використані в рядковому поданні m :
нехай n , k і s будуть цілими числами такі, що k ≥ 1, 10 k -1 ≤ s <10 k , значення числа для s × 10 n - k дорівнює m , а k - якомога менше. Зауважимо, що k - кількість цифр у десятковому поданні s , що s не ділиться на 10, і що найменша значуща цифра s не обов'язково однозначно визначається цими критеріями.
Ключова частина тут - вимога, щоб " k було якомога менше". Зазначена вимога - це вимога, згідно з якою число m
, значення " String(m)
must" має якнайменше можливу кількість цифр , все ще задовольняючи вимогу, яка Number(String(m)) === m
. Оскільки ми це вже знаємо 0.015 === 0.0150000000000000001
, тепер зрозуміло, чому String(0.0150000000000000001) === '0.015'
має бути правдою.
Звичайно, жодна з цієї дискусії прямо не відповіла, що roundTo2DP(m)
слід повернути. Якщо m
точне значення дорівнює 0,01499999999999999944488848768742172978818416595458984375, але його рядкове представлення дорівнює "0,015", то яка правильна відповідь - математично, практично, філософськи чи будь-що інше - коли ми округляємо його до двох десяткових знаків?
На це немає єдиної правильної відповіді. Це залежить від вашого випадку використання. Ви, мабуть, хочете поважати представлення String і крутити вгору, коли:
- Представлене значення по суті є дискретним, наприклад, кількість валюти в валюті 3-значного десяткового місця, як динари. У цьому випадку справжнє значення числа типу 0,015 дорівнює 0,015, а подання 0,0149999999 ..., яке воно отримує у двійковій плаваючій точці, є помилкою округлення. (Звичайно, багато хто з розумом стверджує, що ви повинні використовувати десяткову бібліотеку для обробки таких значень і ніколи не представляти їх як двійкові числа з плаваючою точкою в першу чергу.)
- Значення було введено користувачем. У цьому випадку, знову ж таки, введене точне десяткове число є більш "істинним", ніж найближче двійкове подання з плаваючою комою.
З іншого боку, ви, мабуть, хочете поважати значення бінарної плаваючої точки і округлювати його вниз, коли ваше значення є за властивою безперервною шкалою - наприклад, якщо це зчитування з датчика.
Ці два підходи вимагають різного коду. Для поваги рядкового представлення числа, ми можемо (маючи досить трохи тонкий код) реалізувати власне округлення, яке діє безпосередньо на представлення рядка, цифру за цифрою, використовуючи той самий алгоритм, який ви використовували б у школі, коли ви вчили як округляти числа. Нижче наводиться приклад, який дотримується вимоги ОП щодо представлення числа до двох десяткових знаків "лише у разі необхідності", знімаючи проміжні нулі після десяткової крапки; можливо, вам, можливо, доведеться підлаштувати його до ваших точних потреб.
/**
* Converts num to a decimal string (if it isn't one already) and then rounds it
* to at most dp decimal places.
*
* For explanation of why you'd want to perform rounding operations on a String
* rather than a Number, see http://stackoverflow.com/a/38676273/1709587
*
* @param {(number|string)} num
* @param {number} dp
* @return {string}
*/
function roundStringNumberWithoutTrailingZeroes (num, dp) {
if (arguments.length != 2) throw new Error("2 arguments required");
num = String(num);
if (num.indexOf('e+') != -1) {
// Can't round numbers this large because their string representation
// contains an exponent, like 9.99e+37
throw new Error("num too large");
}
if (num.indexOf('.') == -1) {
// Nothing to do
return num;
}
var parts = num.split('.'),
beforePoint = parts[0],
afterPoint = parts[1],
shouldRoundUp = afterPoint[dp] >= 5,
finalNumber;
afterPoint = afterPoint.slice(0, dp);
if (!shouldRoundUp) {
finalNumber = beforePoint + '.' + afterPoint;
} else if (/^9+$/.test(afterPoint)) {
// If we need to round up a number like 1.9999, increment the integer
// before the decimal point and discard the fractional part.
finalNumber = Number(beforePoint)+1;
} else {
// Starting from the last digit, increment digits until we find one
// that is not 9, then stop
var i = dp-1;
while (true) {
if (afterPoint[i] == '9') {
afterPoint = afterPoint.substr(0, i) +
'0' +
afterPoint.substr(i+1);
i--;
} else {
afterPoint = afterPoint.substr(0, i) +
(Number(afterPoint[i]) + 1) +
afterPoint.substr(i+1);
break;
}
}
finalNumber = beforePoint + '.' + afterPoint;
}
// Remove trailing zeroes from fractional part before returning
return finalNumber.replace(/0+$/, '')
}
Приклад використання:
> roundStringNumberWithoutTrailingZeroes(1.6, 2)
'1.6'
> roundStringNumberWithoutTrailingZeroes(10000, 2)
'10000'
> roundStringNumberWithoutTrailingZeroes(0.015, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.015000', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(1, 1)
'1'
> roundStringNumberWithoutTrailingZeroes('0.015', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(0.01499999999999999944488848768742172978818416595458984375, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.01499999999999999944488848768742172978818416595458984375', 2)
'0.01'
Наведена вище функція - це, мабуть, те, що ви хочете використати, щоб уникнути того, що користувачі ніколи не засвідчують числа, що вони неправильно округлили.
(Як альтернатива, ви також можете спробувати бібліотеку round10, яка забезпечує функцію, що має аналогічну поведінку, з дивовижною реалізацією.)
Але що робити, якщо у вас є другий вид Числа - значення, узяте з безперервної шкали, де немає підстав вважати, що приблизні десяткові подання з меншою кількістю десяткових знаків є більш точними, ніж ті, що мають більше? У цьому випадку ми не хочемо поважати представлення String, тому що це представлення (як пояснено в специфікації) вже начебто закруглене; ми не хочемо помилятися, кажучи "0,014999999 ... 375 раундів до 0,015, що до 0,02, так що 0,014999999 ... 375, до 0,02".
Тут ми можемо просто використовувати вбудований toFixed
метод. Зауважте, що зателефонувавши Number()
до рядка, який повернувся toFixed
, ми отримаємо число, у якого в String-репрезентації немає нульових нулів (завдяки тому, як JavaScript обчислює рядкове представлення числа, обговорене раніше у цій відповіді).
/**
* Takes a float and rounds it to at most dp decimal places. For example
*
* roundFloatNumberWithoutTrailingZeroes(1.2345, 3)
*
* returns 1.234
*
* Note that since this treats the value passed to it as a floating point
* number, it will have counterintuitive results in some cases. For instance,
*
* roundFloatNumberWithoutTrailingZeroes(0.015, 2)
*
* gives 0.01 where 0.02 might be expected. For an explanation of why, see
* http://stackoverflow.com/a/38676273/1709587. You may want to consider using the
* roundStringNumberWithoutTrailingZeroes function there instead.
*
* @param {number} num
* @param {number} dp
* @return {number}
*/
function roundFloatNumberWithoutTrailingZeroes (num, dp) {
var numToFixedDp = Number(num).toFixed(dp);
return Number(numToFixedDp);
}