Questa domanda è complicata.
Supponiamo di avere una funzione, roundTo2DP(num)
che accetta un float come argomento e restituisce un valore arrotondato al secondo decimale. Cosa dovrebbe valutare ciascuna di queste espressioni?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
La risposta "ovvia" è che il primo esempio dovrebbe arrotondare a 0,01 (perché è più vicino a 0,01 rispetto a 0,02) mentre gli altri due dovrebbero arrotondare a 0,02 (perché 0,015000000000000000001 è più vicino a 0,02 che a 0,01 e perché 0,015 è esattamente a metà strada tra loro e c'è una convenzione matematica che tali numeri vengono arrotondati per eccesso).
Il trucco, che potresti aver intuito, è che roundTo2DP
non è possibile implementarlo per dare quelle risposte ovvie, perché tutti e tre i numeri passati ad esso sono lo stesso numero . I numeri binari in virgola mobile IEEE 754 (il tipo utilizzato da JavaScript) non possono rappresentare esattamente la maggior parte dei numeri non interi, quindi tutti e tre i letterali numerici sopra vengono arrotondati a un numero in virgola mobile valido nelle vicinanze. Questo numero, come succede, è esattamente
0,01499999999999999944488848768742172978818416595458984375
che è più vicino a 0,01 che a 0,02.
Puoi vedere che tutti e tre i numeri sono uguali nella tua console del browser, nella shell dei nodi o in altri interpreti JavaScript. Confrontali e basta:
> 0.014999999999999999 === 0.0150000000000000001
true
Quindi quando scrivo m = 0.0150000000000000001
, il valore esatto dim
quello che finisco è più vicino di 0.01
quanto non lo sia 0.02
. Eppure, se mi converto m
in una stringa ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... ottengo 0,015, che dovrebbe rotondo a 0,02, e che è notevolmente non il numero 56 decimale-posto che in precedenza detto che tutti questi numeri erano esattamente uguale a. Quindi che magia oscura è questa?
La risposta è disponibile nella specifica ECMAScript, nella sezione 7.1.12.1: ToString applicata al tipo Numero . Qui sono stabilite le regole per convertire un numero m in una stringa. La parte chiave è il punto 5, in cui viene generato un intero s le cui cifre verranno utilizzate nella rappresentazione String di m :
lascia che n , k e s siano numeri interi tali che k ≥ 1, 10 k -1 ≤ s <10 k , il valore Numero per s × 10 n - k è m e k sia il più piccolo possibile. Si noti che k è il numero di cifre nella rappresentazione decimale di s , che s non è divisibile per 10 e che la cifra meno significativa di s non è necessariamente determinata in modo univoco da questi criteri.
La parte fondamentale qui è il requisito che " k è il più piccolo possibile". Ciò che equivale a tale requisito è un requisito che, dato un Numero m
, il valore di String(m)
deve avere il minor numero possibile di cifre pur soddisfacendo il requisito che Number(String(m)) === m
. Dato che lo sappiamo già 0.015 === 0.0150000000000000001
, ora è chiaro perché String(0.0150000000000000001) === '0.015'
deve essere vero.
Naturalmente, nessuna di queste discussioni ha risposto direttamente a ciò che roundTo2DP(m)
dovrebbe tornare. Se m
il valore esatto è 0,01499999999999999944488848768742172978818416595458984375, ma la sua rappresentazione String è '0,015', allora qual è la risposta corretta - matematicamente, praticamente, filosoficamente o qualsiasi altra cosa - quando la arrotondiamo a due decimali?
Non esiste un'unica risposta corretta a questo. Dipende dal tuo caso d'uso. Probabilmente vuoi rispettare la rappresentazione String e arrotondare verso l'alto quando:
- Il valore rappresentato è intrinsecamente discreto, ad esempio una quantità di valuta in una valuta di 3 decimali come i dinari. In questo caso, il valore vero di un numero come 0,015 è 0,015 e la rappresentazione 0,0149999999 ... che ottiene in virgola mobile binaria è un errore di arrotondamento. (Naturalmente, molti sosterranno, ragionevolmente, che dovresti usare una libreria decimale per gestire tali valori e non rappresentarli mai come numeri binari in virgola mobile in primo luogo.)
- Il valore è stato digitato da un utente. In questo caso, ancora una volta, il numero decimale esatto inserito è più "vero" rispetto alla rappresentazione binaria in virgola mobile più vicina.
D'altra parte, probabilmente vuoi rispettare il valore binario in virgola mobile e arrotondare verso il basso quando il tuo valore proviene da una scala intrinsecamente continua, ad esempio se si tratta di una lettura da un sensore.
Questi due approcci richiedono un codice diverso. Per rispettare la rappresentazione String del Numero, possiamo (con un po 'di codice ragionevolmente sottile) implementare il nostro arrotondamento che agisce direttamente sulla rappresentazione String, cifra per cifra, usando lo stesso algoritmo che avresti usato a scuola quando è stato insegnato come arrotondare i numeri. Di seguito è riportato un esempio che rispetta il requisito del PO di rappresentare il numero con 2 decimali "solo quando necessario" rimuovendo gli zero finali dopo il punto decimale; potresti ovviamente aver bisogno di modificarlo in base alle tue precise esigenze.
/**
* 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+$/, '')
}
Esempio di utilizzo:
> 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'
La funzione sopra è probabilmente ciò che si desidera utilizzare per evitare che gli utenti che assistano ai numeri che hanno inserito siano arrotondati in modo errato.
(In alternativa, puoi anche provare la libreria round10 che fornisce una funzione simile con un'implementazione completamente diversa.)
E se avessi il secondo tipo di Numero - un valore preso da una scala continua, dove non c'è motivo di pensare che le rappresentazioni decimali approssimative con meno decimali siano più accurate di quelle con più? In tal caso, non vogliamo rispettare la rappresentazione String, poiché quella rappresentazione (come spiegato nelle specifiche) è già un po 'arrotondata; non vogliamo fare l'errore di dire "0,014999999 ... 375 arrotondamenti fino a 0,015, che arrotondano fino a 0,02, quindi 0,014999999 ... 375 arrotondamenti fino a 0,02".
Qui possiamo semplicemente usare il toFixed
metodo integrato . Nota che chiamando Number()
la stringa restituita da toFixed
, otteniamo un numero la cui rappresentazione di stringa non ha zero finali (grazie al modo in cui JavaScript calcola la rappresentazione di stringa di un numero, discusso in precedenza in questa risposta).
/**
* 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);
}