Cette question est compliquée.
Supposons que nous ayons une fonction roundTo2DP(num)
,, qui prend un flottant comme argument et renvoie une valeur arrondie à 2 décimales. Que doit évaluer chacune de ces expressions?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
La réponse `` évidente '' est que le premier exemple devrait arrondir à 0,01 (car il est plus proche de 0,01 que de 0,02) tandis que les deux autres devraient arrondir à 0,02 (car 0,0150000000000000001 est plus proche de 0,02 que de 0,01, et parce que 0,015 est exactement à mi-chemin entre et il existe une convention mathématique selon laquelle ces nombres sont arrondis).
Le problème, que vous avez peut-être deviné, est qu'il roundTo2DP
ne peut pas être mis en œuvre pour donner ces réponses évidentes, car les trois nombres qui lui sont transmis sont le même nombre . Les nombres à virgule flottante binaires IEEE 754 (le type utilisé par JavaScript) ne peuvent pas représenter exactement la plupart des nombres non entiers, et donc les trois littéraux numériques ci-dessus sont arrondis à un nombre à virgule flottante valide proche. Il se trouve que ce nombre est exactement
0,01499999999999999944488848768742172978818416595458984375
qui est plus proche de 0,01 que de 0,02.
Vous pouvez voir que les trois nombres sont les mêmes sur la console de votre navigateur, le shell Node ou tout autre interpréteur JavaScript. Il suffit de les comparer:
> 0.014999999999999999 === 0.0150000000000000001
true
Donc, quand j'écris m = 0.0150000000000000001
, la valeur exacte de cem
que je me retrouve est plus proche 0.01
qu'elle ne l'est 0.02
. Et pourtant, si je me convertis m
en une chaîne ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... J'obtiens 0,015, qui devrait arrondir à 0,02, et qui n'est visiblement pas le nombre à 56 décimales. J'ai dit plus tôt que tous ces nombres étaient exactement égaux. Alors, quelle magie noire est-ce?
La réponse peut être trouvée dans la spécification ECMAScript, dans la section 7.1.12.1: ToString appliquée au type Number . Ici, les règles de conversion d'un nombre m en chaîne sont définies. La partie clé est le point 5, dans lequel un entier s est généré dont les chiffres seront utilisés dans la représentation String de m :
soit n , k et s des entiers tels que k ≥ 1, 10 k -1 ≤ s <10 k , la valeur numérique pour s × 10 n - k est m et k est aussi petit que possible. Notez que k est le nombre de chiffres dans la représentation décimale de s , que s n'est pas divisible par 10 et que le chiffre le moins significatif de s n'est pas nécessairement déterminé de manière unique par ces critères.
L'élément clé ici est l'exigence que " k soit aussi petit que possible". Ce que signifie cette exigence est une exigence selon laquelle, étant donné un nombre m
, la valeur de String(m)
doit avoir le moins de chiffres possible tout en satisfaisant à l'exigence que Number(String(m)) === m
. Puisque nous le savons déjà 0.015 === 0.0150000000000000001
, il est maintenant clair pourquoi cela String(0.0150000000000000001) === '0.015'
doit être vrai.
Bien sûr, aucune de ces discussions n'a directement répondu à ce qui roundTo2DP(m)
devrait revenir. Si m
la valeur exacte est 0,01499999999999999944488848768742172978818416595458984375, mais sa représentation de chaîne est '0,015', alors quelle est la bonne réponse - mathématiquement, pratiquement, philosophiquement, ou autre - quand nous l'arrondissons à deux décimales?
Il n'y a pas de réponse correcte unique à cela. Cela dépend de votre cas d'utilisation. Vous voudrez probablement respecter la représentation String et arrondir vers le haut lorsque:
- La valeur représentée est intrinsèquement discrète, par exemple un montant de devise dans une devise à 3 décimales comme les dinars. Dans ce cas, la vraie valeur d'un nombre comme 0,015 est 0,015 et la représentation 0,0149999999 ... qu'il obtient en virgule flottante binaire est une erreur d'arrondi. (Bien sûr, beaucoup diront, raisonnablement, que vous devez utiliser une bibliothèque décimale pour gérer ces valeurs et ne jamais les représenter comme des nombres à virgule flottante binaire en premier lieu.)
- La valeur a été saisie par un utilisateur. Dans ce cas, encore une fois, le nombre décimal exact entré est plus «vrai» que la représentation binaire flottante la plus proche.
D'un autre côté, vous voulez probablement respecter la valeur à virgule flottante binaire et arrondir vers le bas lorsque votre valeur provient d'une échelle intrinsèquement continue - par exemple, s'il s'agit d'une lecture provenant d'un capteur.
Ces deux approches nécessitent un code différent. Pour respecter la représentation String du nombre, nous pouvons (avec un peu de code raisonnablement subtil) implémenter notre propre arrondi qui agit directement sur la représentation String, chiffre par chiffre, en utilisant le même algorithme que vous auriez utilisé à l'école lorsque vous ont appris à arrondir les nombres. Voici un exemple qui respecte l'exigence du PO de représenter le nombre à 2 décimales "uniquement lorsque cela est nécessaire" en supprimant les zéros de fin après la virgule; vous devrez peut-être, bien sûr, l'adapter à vos besoins précis.
/**
* 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+$/, '')
}
Exemple d'utilisation:
> 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 fonction ci-dessus est probablement celle que vous souhaitez utiliser pour éviter que les utilisateurs ne voient jamais les nombres qu'ils ont entrés être arrondis incorrectement.
(Comme alternative, vous pouvez également essayer la bibliothèque round10 qui fournit une fonction similaire avec une implémentation très différente.)
Mais que se passe-t-il si vous avez le deuxième type de nombre - une valeur prise sur une échelle continue, où il n'y a aucune raison de penser que les représentations décimales approximatives avec moins de décimales sont plus précises que celles avec plus? Dans ce cas, nous ne voulons pas respecter la représentation String, car cette représentation (comme expliqué dans la spécification) est déjà en quelque sorte arrondie; nous ne voulons pas faire l'erreur de dire "0,014999999 ... 375 arrondit à 0,015, ce qui arrondit à 0,02, donc 0,014999999 ... 375 arrondit à 0,02".
Ici, nous pouvons simplement utiliser la toFixed
méthode intégrée . Notez qu'en appelant Number()
la chaîne renvoyée par toFixed
, nous obtenons un nombre dont la représentation de chaîne n'a pas de zéros de fin (grâce à la façon dont JavaScript calcule la représentation de chaîne d'un nombre, discutée plus haut dans cette réponse).
/**
* 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);
}