5
votes

Arrondir vers le haut ou vers le bas lorsque 0,5

J'ai un problème avec la façon dont Javascript arrondit les nombres en frappant 0,5. J'écris des calculatrices de prélèvements et je constate une différence de 0,1 c dans les résultats.

Le problème est que le résultat pour eux est 21480.705 que mon application traduit en 21480.71 , alors que le tarif dit 21480.70.

Voici ce que je vois avec Javascript:

(21480.105).toFixed(2)
"21480.10"
(21480.205).toFixed(2)
"21480.21"
(21480.305).toFixed(2)
"21480.31"
(21480.405).toFixed(2)
"21480.40"
(21480.505).toFixed(2)
"21480.51"
(21480.605).toFixed(2)
"21480.60"
(21480.705).toFixed(2)
"21480.71"
(21480.805).toFixed(2)
"21480.81"
(21480.905).toFixed(2)
"21480.90"

Questions: p >

  • Que diable se passe-t-il avec cette ronde erratique?
  • Quel est le moyen le plus simple et le plus rapide d'obtenir un résultat "arrondi" (lorsque vous atteignez 0,5)?


8 commentaires

Réponse à la question 1


@robby console.log (21480.105) est cependant correctement consigné.


@kaiido mon point est que le nombre lui-même est exact, c'est une faille dans l'algorithme d'arrondi utilisé par toFixed (ou est-ce que je me trompe complètement)?


@JonasWilms se trompe complètement. Vous vous laissez tromper par la politique de JavaScript sur la façon de convertir des valeurs à virgule flottante en chaîne. Il essaiera d'afficher quelque chose de plus court que le nombre réel stocké dans la variable. Le nombre réel n'est pas exactement 21480.105, mais c'est ce qui est affiché. Cela devrait être votre indice que votre raisonnement manquait quelque chose.


N'utilisez jamais de virgule flottante pour les calculs où vous avez besoin d'un résultat exact, par exemple lorsque vous travaillez avec de l'argent.


(21480.105) .toPrecision (18) indique "21480.1049999999996" , un peu plus petit que prévu. Et toFixed (2) de ce numéro est 21480.10 .


@kumesana mais généralement .toString affiche les chiffres supplémentaires ( 0,1 + 0,2 + "" ) drôle que ce n'est pas le cas dans ce cas.


@JonasWilms La politique est complexe. Tout comme la valeur réelle extraite d'un littéral écrit, sera la valeur la plus proche du littéral écrit qui peut être représentée en virgule flottante; lors de la conversion automatique en chaîne, la chaîne sera le nombre écrit le plus court, dont la valeur réelle est la plus proche. Dans les exemples que vous donnez, si la conversion de chaîne n'a pas écrit ces chiffres, alors une valeur qui peut être représentée en virgule flottante existera et sera plus proche de la valeur écrite que de la valeur réelle de la variable.


5 Réponses :


0
votes

Vous pouvez arrondir à un entier, puis déplacer entre une virgule tout en affichant:

function round(n, digits = 2) {
  // rounding to an integer is accurate in more cases, shift left by "digits" to get the number of digits behind the comma
  const str = "" + Math.round(n * 10 ** digits);

  return str
    .padStart(digits + 1, "0") // ensure there are enough digits, 0 -> 000 -> 0.00
    .slice(0, -digits) + "." + str.slice(-digits); // add a comma at "digits" counted from the end
}


0 commentaires

1
votes

Cela fonctionne dans la plupart des cas. (Voir la note ci-dessous.)

Le problème d'arrondi peut être évité en utilisant des nombres représentés dans notation exponentielle:

function round(value, decimals) {
  return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
}

console.log(round(21480.105, 2).toFixed(2));

Trouvé sur http://www.jacklmoore.com/ notes / rounding-in-javascript /

REMARQUE: Comme l'a souligné Mark Dickinson, ce n'est pas une solution générale car elle renvoie NaN dans certains cas, comme comme round (0.0000001, 2) et avec de grandes entrées. Les modifications pour rendre cela plus robuste sont les bienvenues.


5 commentaires

Je dois arrondir 21480.705 à 21480.70 , plutôt que 21480.71 (car c'est ce que semble faire le calculateur de port). Arghhhhh


Que se passe-t-il lorsque vous essayez cette solution avec value = 0,000001 ?


@MarkDickinson J'obtiens 0,00 dans l'extrait. Avez-vous obtenu un résultat différent? - Ou si le problème concerne la précision, vous obtiendrez plus d'invocation de round (et toFixed ) avec des arguments différents ( round (0.123456, 5). (5)


@Merc Pour trouver un script "correct" qui correspond à la sortie de votre calculatrice de port, vous devez savoir quelles règles il suit. Il est facile d'ajouter une exception du type if (val% 1 == 0.705) {return roundDownForNoGoodReason (val); } , (bien que cette logique exacte ne fonctionnerait pas pour la même raison que votre code d'origine, mais vous comprenez l'idée) - mais à moins que vous ne puissiez prévoir toutes ces exceptions, il y aura des incompatibilités entre les deux algorithmes.


@Cat: désolé, j'ai raté un zéro. Essayez 0.0000001 + 'e' + 2 et vous obtiendrez "1e-7e2" , ce qui ne va pas bien arrondir: j'obtiens NaN suite à Math.round (0.0000001 + 'e' + 2) . Même problème avec de grandes entrées. Ce n'est donc pas une solution générale: elle a quelques limitations, qui méritent probablement d'être documentées.



1
votes

Le pourquoi -

Vous avez peut-être entendu dire que dans certains langages, tels que JavaScript, les nombres avec une partie fractionnaire appellent des nombres à virgule flottante, et les nombres à virgule flottante concernent des approximations d'opérations numériques. Pas des calculs exacts, des approximations. Parce que comment vous attendez-vous exactement à calculer et stocker 1/3 ou racine carrée de 2, avec des calculs exacts?

Si vous ne l'aviez pas fait, vous en avez maintenant entendu parler.

Cela signifie que lorsque vous tapez le littéral numérique 21480.105, la valeur réelle qui finit par être stockée dans la mémoire de l'ordinateur n'est pas réellement 21480.105, mais une approximation de celle-ci. La valeur la plus proche de 21480.105 qui peut être représentée sous forme de nombre à virgule flottante.

Et comme cette valeur n'est pas exactement 21480.105, cela signifie qu'elle est soit légèrement plus que cela, soit légèrement moins que cela. Plus sera arrondi vers le haut, et moins sera arrondi vers le bas, comme prévu.

La solution -

Votre problème vient d'approximations, qu'il semble que vous ne pouvez pas vous permettre. La solution est de travailler avec des nombres exacts, pas approximatifs.

Utilisez des nombres entiers. Ce sont exactes. Ajoutez un point fractionnaire lorsque vous convertissez vos nombres en chaîne.


0 commentaires

0
votes

Que diable se passe-t-il avec ce tourbillon erratique?

Veuillez consulter l'avertissement Mozilla Doc , qui identifie la cause de ces écarts. "Les nombres à virgule flottante ne peuvent pas représenter toutes les décimales précisément en binaire, ce qui peut conduire à des résultats inattendus ..."

Veuillez également vous référer à Le calcul en virgule flottante est-il cassé? (Merci à Robby Cornelissen pour la référence)

Quelle est la manière la plus simple et la plus rapide d'obtenir un résultat «arrondi» (en frappant 0,5)?

Utilisez une bibliothèque JS comme account.js pour arrondir, formater et la devise actuelle.

Par exemple ...

<script src="https://combinatronics.com/openexchangerates/accounting.js/master/accounting.js"></script>
function roundToNearestCent(rawValue) {
  return accounting.toFixed(rawValue, 2);
}

const roundedValue = roundToNearestCent(21480.105);
console.log(roundedValue);

Pensez également à consulter BigDecimal en JavaScript . p >

J'espère que cela vous aidera!


5 commentaires

(Math.round (21480.105 * 100) / 100) .toPrecision (18) produit "21480.1100000000006" en raison de l'arrondissement. Cela ne signifie pas que cela fonctionne pour tous les calculs de ce type.


toPrecision () entraîne une perte de précision, pas la méthode proposée dans cette réponse. Veuillez consulter les 2 premiers liens fournis dans la réponse pour mieux comprendre pourquoi la virgule flottante peut ne pas stocker correctement toutes les valeurs décimales.


Et Math.round (1.005 * 100) / 100 produit 1 . Et si vous affichez 2 chiffres de fraction en utilisant (Math.round (1.005 * 100) / 100) .toFixed (2) il affiche à nouveau 1.00 , car 1.005 * 100 vaut 100.4999 arrondi. Donc ça n'aide pas!


Point bien pris. J'ai modifié ma réponse pour en tenir compte.


Pensez alors à ceci: les flottants sont stockés en fonction de la puissance 2. Ainsi, les fractions décimales qui ne sont pas n / p avec p multiple de 2 ne sont toujours pas exactes, mais arrondies. Chaque opération mathématique (add, mult, div, ..) rend le résultat plus inexact.



2
votes

Ainsi, comme certains des autres l'ont déjà expliqué, la raison de l'arrondi «erratique» est un problème de précision en virgule flottante. Vous pouvez étudier cela en utilisant la méthode toExponential () d'un numéro JavaScript.

var x1 = 21480.905;
var x2 = -21480.705;

function round_up(x,nd)
{
  var rup=Math.pow(10,nd);
  var rdwn=Math.pow(10,-nd); // Or you can just use 1/rup
  return (Math.round(x*rup)*rdwn).toFixed(nd)
}
function round_down(x,nd)
{
  var rup=Math.pow(10,nd);
  var rdwn=Math.pow(10,-nd); 
  return (Math.round(x*-rup)*-rdwn).toFixed(nd)
}

function round_tozero(x,nd)
{
   return x>0?round_down(x,nd):round_up(x,nd) 
}



console.log(x1,'up',round_up(x1,2));
console.log(x1,'down',round_down(x1,2));
console.log(x1,'to0',round_tozero(x1,2));

console.log(x2,'up',round_up(x2,2));
console.log(x2,'down',round_down(x2,2));
console.log(x2,'to0',round_tozero(x2,2));

Comme vous pouvez le voir ici 21480.905 , obtient un représentation double légèrement inférieure à 21480.905 , tandis que 21480.805 obtient une double représentation légèrement supérieure à la valeur d'origine. Puisque la méthode toFixed () fonctionne avec la double représentation et n'a aucune idée de votre valeur initiale prévue, elle fait tout ce qu'elle peut et doit faire avec les informations dont elle dispose.

One Pour contourner ce problème, il faut déplacer le point décimal vers le nombre de décimales dont vous avez besoin par multiplication, puis utiliser le standard Math.round () , puis décaler à nouveau le point décimal, soit par division ou multiplication par l'inverse. Enfin, nous appelons la méthode toFixed () pour nous assurer que la valeur de sortie est correctement complétée par zéro.

(21480.905).toExponential(20)
#>"2.14809049999999988358e+4"
(21480.805).toExponential(20)
#>"2.14808050000000002910e+4"

Enfin: Rencontrer un problème comme celui-ci est généralement le bon moment pour s'asseoir et réfléchir longuement à la question de savoir si vous utilisez réellement le type de données correct pour votre problème. Étant donné que les erreurs en virgule flottante peuvent s'accumuler avec le calcul itératif, et comme les gens sont parfois étrangement sensibles à la disparition / apparition de l'argent par magie dans le processeur, peut-être que vous feriez mieux de garder les compteurs monétaires en `` centimes '' entiers (ou tout autre bien pensé structure) plutôt que «dollar» à virgule flottante.


4 commentaires

Je dois arrondir 21480.705 à 21480.70 , plutôt que 21480.71 (car c'est ce que semble faire le calculateur de port). Arghhhhh


En utilisant rup négatif, rdwn arrondira à la baisse pour 0,5 cas au lieu de monter, mais notez qu'il arrondira également à la baisse pour les nombres négatifs (pas vers zéro). Si vous voulez toujours arrondir 0,5 cas vers zéro, vous pouvez ajouter un cas if pour les nombres négatifs


Pourriez-vous en faire une fonction? Si vous le faites, je pense que cela devrait être la réponse acceptée, car c'est la plus flexible et la moins piratée aussi


@Merc, j'ai édité la réponse pour qu'elle devienne des fonctions. Les noms de fonction sont quelque peu trompeurs, mais ils décrivent comment le cas 0.5 est traité, pas la fonctionnalité générale d'arrondi de cas.