Diese Frage ist kompliziert.
Angenommen, wir haben eine Funktion, roundTo2DP(num)
die ein Gleitkomma als Argument verwendet und einen auf 2 Dezimalstellen gerundeten Wert zurückgibt. Wofür sollte jeder dieser Ausdrücke ausgewertet werden?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
Die 'offensichtliche' Antwort ist, dass das erste Beispiel auf 0,01 runden sollte (weil es näher an 0,01 als an 0,02 liegt), während die anderen beiden auf 0,02 runden sollten (weil 0,0150000000000000001 näher an 0,02 als an 0,01 liegt und weil 0,015 genau in der Mitte liegt sie und es gibt eine mathematische Konvention, dass solche Zahlen aufgerundet werden).
Der Haken, den Sie vielleicht erraten haben, ist, dass er roundTo2DP
möglicherweise nicht implementiert werden kann, um diese offensichtlichen Antworten zu geben, da alle drei an ihn übergebenen Zahlen dieselbe Zahl sind . IEEE 754-Binärgleitkommazahlen (die von JavaScript verwendete Art) können die meisten nicht ganzzahligen Zahlen nicht genau darstellen. Daher werden alle drei oben genannten numerischen Literale auf eine nahegelegene gültige Gleitkommazahl gerundet. Diese Zahl ist zufällig genau
0.01499999999999999944488848768742172978818416595458984375
das ist näher an 0,01 als an 0,02.
Sie können sehen, dass alle drei Zahlen in Ihrer Browserkonsole, Node-Shell oder einem anderen JavaScript-Interpreter gleich sind. Vergleichen Sie sie einfach:
> 0.014999999999999999 === 0.0150000000000000001
true
Wenn ich also schreibe m = 0.0150000000000000001
, ist der genaue Wertm
, mit dem ich ende, näher 0.01
als er ist 0.02
. Und doch, wenn ich m
in einen String konvertiere ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... Ich bekomme 0,015, was auf 0,02 runden sollte und was merklich nicht die 56-Dezimalstellen-Zahl ist, von der ich zuvor gesagt habe, dass alle diese Zahlen genau gleich sind. Was für eine dunkle Magie ist das?
Die Antwort finden Sie in der ECMAScript-Spezifikation in Abschnitt 7.1.12.1: ToString, angewendet auf den Nummerntyp . Hier werden die Regeln für die Konvertierung einer Zahl m in einen String festgelegt. Der Schlüsselteil ist Punkt 5, in dem eine ganze Zahl s erzeugt wird, deren Ziffern in der String-Darstellung von m verwendet werden :
lassen n , k und s ganze Zahlen , so dass k ≥ 1, 10 k -1 ≤ s <10 k , der Zahlenwert für s 10 × n - k ist m , und k ist so klein wie möglich. Es ist zu beachten, dass k die Anzahl der Stellen in der Dezimaldarstellung von s ist , dass s nicht durch 10 teilbar ist und dass die niedrigstwertige Ziffer von s durch diese Kriterien nicht unbedingt eindeutig bestimmt wird.
Der Schlüsselteil hier ist die Anforderung, dass " k so klein wie möglich ist". Was die Forderung beläuft sich auf eine Anforderung , dass eine Nummer m
, den Wert String(m)
muss eine möglichst geringe Anzahl von Stellen , während immer noch die Anforderung erfüllt , dass Number(String(m)) === m
. Da wir das bereits wissen 0.015 === 0.0150000000000000001
, ist jetzt klar, warum String(0.0150000000000000001) === '0.015'
das wahr sein muss.
Natürlich hat keine dieser Diskussionen direkt beantwortet, was zurückkehren roundTo2DP(m)
sollte . Wenn m
der genaue Wert 0.01499999999999999944488848768742172978818416595458984375 ist, die Zeichenfolgendarstellung jedoch '0.015' lautet, was ist dann die richtige Antwort - mathematisch, praktisch, philosophisch oder was auch immer -, wenn wir sie auf zwei Dezimalstellen runden?
Darauf gibt es keine einzige richtige Antwort. Dies hängt von Ihrem Anwendungsfall ab. Sie möchten wahrscheinlich die Zeichenfolgendarstellung respektieren und nach oben runden, wenn:
- Der dargestellte Wert ist von Natur aus diskret, z. B. ein Währungsbetrag in einer Währung mit 3 Dezimalstellen wie Dinar. In diesem Fall wird der wahre Wert einer Zahl wie 0,015 ist 0,015, und die 0,0149999999 ... Darstellung , dass es in binärer Gleitkomma- erhält , ist ein Rundungsfehler. (Natürlich werden viele vernünftigerweise argumentieren, dass Sie eine Dezimalbibliothek für die Behandlung solcher Werte verwenden und sie niemals als binäre Gleitkommazahlen darstellen sollten.)
- Der Wert wurde von einem Benutzer eingegeben. Auch in diesem Fall ist die genaue eingegebene Dezimalzahl "wahr" als die nächste binäre Gleitkommadarstellung.
Auf der anderen Seite möchten Sie wahrscheinlich den binären Gleitkommawert berücksichtigen und nach unten runden, wenn Ihr Wert von einer inhärent kontinuierlichen Skala stammt - beispielsweise wenn es sich um einen Messwert von einem Sensor handelt.
Diese beiden Ansätze erfordern unterschiedlichen Code. Um die String-Darstellung der Zahl zu respektieren, können wir (mit ziemlich viel subtilem Code) unsere eigene Rundung implementieren, die direkt auf die String-Darstellung wirkt, Ziffer für Ziffer, wobei wir denselben Algorithmus verwenden, den Sie in der Schule verwendet hätten, als Sie wurden gelehrt, wie man Zahlen rundet. Im Folgenden finden Sie ein Beispiel, das die Anforderung des OP berücksichtigt, die Zahl "nur bei Bedarf" auf 2 Dezimalstellen darzustellen, indem nach dem Dezimalpunkt nachgestellte Nullen entfernt werden. Möglicherweise müssen Sie es natürlich an Ihre genauen Bedürfnisse anpassen.
/**
* 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+$/, '')
}
Anwendungsbeispiel:
> 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'
Die obige Funktion ist wahrscheinlich die, die Sie verwenden möchten, um zu vermeiden, dass Benutzer jemals feststellen, dass eingegebene Zahlen falsch gerundet werden.
(Alternativ können Sie auch die round10- Bibliothek ausprobieren, die eine ähnlich verhaltende Funktion mit einer völlig anderen Implementierung bietet.)
Aber was ist, wenn Sie die zweite Art von Zahl haben - einen Wert aus einer kontinuierlichen Skala, bei der es keinen Grund zu der Annahme gibt, dass ungefähre Dezimaldarstellungen mit weniger Dezimalstellen genauer sind als solche mit mehr? In diesem Fall möchten wir die String-Darstellung nicht berücksichtigen, da diese Darstellung (wie in der Spezifikation erläutert) bereits gerundet ist. Wir wollen nicht den Fehler machen, zu sagen "0.014999999 ... 375 rundet auf 0.015, was auf 0.02 rundet, also 0.014999999 ... 375 rundet auf 0.02".
Hier können wir einfach die eingebaute toFixed
Methode verwenden. Beachten Sie, dass wir durch Aufrufen Number()
des von toFixed
zurückgegebenen Strings eine Zahl erhalten, deren String-Darstellung keine nachgestellten Nullen enthält (dank der Art und Weise, wie JavaScript die String-Darstellung einer Zahl berechnet, die weiter oben in dieser Antwort erläutert wurde).
/**
* 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);
}