Pourquoi Math.Round (2.5) renvoie 2 au lieu de 3?

416

En C #, le résultat de Math.Round(2.5)est 2.

Il est censé être 3, n'est-ce pas? Pourquoi est-il plutôt 2 en C #?

jeffu
la source
5
C'est en fait une fonctionnalité. Voir <a href=" msdn.microsoft.com/en-us/library/… Documentation MSDN</a> . Ce type d'arrondi est appelé arrondi bancaire. Comme pour une solution de contournement, il existe <a href = " msdn. microsoft.com/en-us/library/… overload </a> qui permet à l'appelant de spécifier comment effectuer l'arrondi.
Joe
1
Apparemment, la méthode arrondie, lorsqu'on lui demande d'arrondir un nombre exactement entre deux entiers, renvoie l'entier pair. Donc, Math.Round (3.5) renvoie 4. Voir cet article
Matthew Jones
20
Math.Round(2.5, 0, MidpointRounding.AwayFromZero);
Robert Durgin
SQL Server arrondit de cette façon; des résultats de test intéressants quand il y a un test unitaire C # pour valider l'arrondi fait en T-SQL.
idstam
7
@amed ce n'est pas un bug. C'est ainsi que fonctionnent les virgules flottantes binaires. 1.005ne peut pas être représenté exactement en double. C'est probablement 1.00499.... Si vous utilisez Decimalce problème disparaîtra. L'existence de la surcharge Math.Round qui prend un certain nombre de chiffres décimaux sur le double est un choix de conception IMO douteux, car il fonctionnera rarement de manière significative.
CodesInChaos

Réponses:

561

Tout d'abord, ce ne serait pas un bogue C # de toute façon - ce serait un bogue .NET. C # est le langage - il ne décide pas comment Math.Roundest implémenté.

Et deuxièmement, non - si vous lisez les documents , vous verrez que l'arrondi par défaut est "arrondi à égal" (arrondi du banquier):


Type de valeur de retour : System.Double
Entier le plus proche de a. Si la composante fractionnaire de a est à mi-chemin entre deux entiers, dont l'un est pair et l'autre impair, alors le nombre pair est retourné. Notez que cette méthode retourne un Doubleau lieu d'un type intégral.

Remarques
Le comportement de cette méthode suit la norme IEEE 754, section 4. Ce type d'arrondi est parfois appelé arrondi au plus proche, ou arrondi bancaire. Il minimise les erreurs d'arrondi résultant de l'arrondi constant d'une valeur médiane dans une seule direction.

Vous pouvez spécifier comment Math.Roundarrondir les points médians à l'aide d' une surcharge qui prend une MidpointRoundingvaleur. Il y a une surcharge avec un MidpointRoundingcorrespondant à chacune des surcharges qui n'en a pas:

Que cette valeur par défaut ait été bien choisie ou non est une autre affaire. ( MidpointRoundingn'a été introduit que dans .NET 2.0. Avant cela, je ne suis pas sûr qu'il y ait un moyen facile d'implémenter le comportement souhaité sans le faire vous-même.) En particulier, l'histoire a montré que ce n'est pas le comportement attendu - et dans la plupart des cas, c'est un péché cardinal dans la conception des API. Je peux voir pourquoi l'arrondi bancaire est utile ... mais c'est encore une surprise pour beaucoup.

Vous pourriez être intéressé de jeter un œil à l'énum ( RoundingMode) Java équivalent le plus proche qui offre encore plus d'options. (Il ne s'agit pas seulement des points médians.)

Jon Skeet
la source
4
je ne sais pas si c'est un bug, je pense que c'était par conception car .5 est aussi proche de l'entier le plus bas le plus proche que de l'entier le plus haut le plus proche.
Stan R.
3
Je me souviens de ce comportement dans VB avant l'application de .NET.
John Fiala
7
En effet, la norme IEEE 754, section 4 comme l'indique la documentation.
Jon Skeet
2
J'ai été brûlé par ça il y a un moment et j'ai pensé que c'était aussi de la folie. Heureusement, ils ont ajouté un moyen de spécifier l'arrondissement que nous avons tous appris à l'école primaire; MidPointRounding.
Shea
26
+1 pour "ce n'est pas le comportement attendu [...] qui est un péché cardinal dans la conception d'API"
BlueRaja - Danny Pflughoeft
215

C'est ce qu'on appelle l'arrondi à pair (ou arrondi bancaire), qui est une stratégie d'arrondi valide pour minimiser les erreurs accumulées dans les sommes (MidpointRounding.ToEven). La théorie est que, si vous arrondissez toujours un nombre 0,5 dans la même direction, les erreurs s'accumuleront plus rapidement (l'arrondi au pair est censé minimiser cela) (a) .

Suivez ces liens pour les descriptions MSDN de:

  • Math.Floor, qui arrondit vers l'infini négatif.
  • Math.Ceiling, qui arrondit vers l'infini positif.
  • Math.Truncate, qui arrondit vers le haut ou vers le bas vers zéro.
  • Math.Round, qui arrondit à l'entier le plus proche ou au nombre spécifié de décimales. Vous pouvez spécifier le comportement s'il est exactement à égale distance entre deux possibilités, telles que l'arrondi de sorte que le dernier chiffre soit pair (" Round(2.5,MidpointRounding.ToEven)" devenant 2) ou qu'il soit plus éloigné de zéro (" Round(2.5,MidpointRounding.AwayFromZero)" devenant 3).

Le diagramme et le tableau suivants peuvent vous aider:

-3        -2        -1         0         1         2         3
 +--|------+---------+----|----+--|------+----|----+-------|-+
    a                     b       c           d            e

                       a=-2.7  b=-0.5  c=0.3  d=1.5  e=2.8
                       ======  ======  =====  =====  =====
Floor                    -3      -1      0      1      2
Ceiling                  -2       0      1      2      3
Truncate                 -2       0      0      1      2
Round(ToEven)            -3       0      0      2      3
Round(AwayFromZero)      -3      -1      0      2      3

Notez que Roundc'est beaucoup plus puissant qu'il n'y paraît, simplement parce qu'il peut arrondir à un nombre spécifique de décimales. Tous les autres arrondissent toujours à zéro décimale. Par exemple:

n = 3.145;
a = System.Math.Round (n, 2, MidpointRounding.ToEven);       // 3.14
b = System.Math.Round (n, 2, MidpointRounding.AwayFromZero); // 3.15

Avec les autres fonctions, vous devez utiliser la supercherie multiplier / diviser pour obtenir le même effet:

c = System.Math.Truncate (n * 100) / 100;                    // 3.14
d = System.Math.Ceiling (n * 100) / 100;                     // 3.15

(a) Bien sûr, cette théorie dépend du fait que vos données ont une répartition assez uniforme des valeurs entre les moitiés paires (0,5, 2,5, 4,5, ...) et les moitiés impaires (1,5, 3,5, ...).

Si toutes les "demi-valeurs" sont égales (par exemple), les erreurs s'accumuleront aussi vite que si vous arrondissiez toujours.

paxdiablo
la source
3
Également connu sous le nom de Banker's Rounding
Pondidum,
Bonne explication! Je voulais voir par moi-même comment l'erreur s'accumule et j'ai écrit un script qui montre que les valeurs arrondies à l'aide de l'arrondi bancaire, à long terme, ont leurs sommes et leurs moyennes beaucoup plus proches de celles des valeurs d'origine. github.com/AmadeusW/RoundingDemo (photos des parcelles disponibles)
Amadeusz Wieczorek
Peu de temps après: ne doit-il pas ecocher (= 2,8) plus à droite que 2cocher?
superjos
Un moyen simple de se souvenir, et en supposant que la dixième place est 5: - la place de l'un et la dixième place sont toutes impaires = arrondir - la place de l'un et la dixième place sont mélangées = arrondir vers le bas * Zéro n'est pas impair * Inversé pour les nombres négatifs
Arkham Angel
@ArkhamAngel, cela semble en fait plus difficile à retenir que de simplement "faire le dernier chiffre" :-)
paxdiablo
42

À partir de MSDN, Math.Round (double a) renvoie:

L'entier le plus proche de a. Si la composante fractionnaire de a est à mi-chemin entre deux entiers, dont l'un est pair et l'autre impair, alors le nombre pair est retourné.

... et ainsi 2,5, étant à mi-chemin entre 2 et 3, est arrondi au nombre pair (2). c'est ce qu'on appelle l'arrondi bancaire (ou arrondi à égal), et c'est une norme d'arrondi couramment utilisée.

Même article MSDN:

Le comportement de cette méthode suit la norme IEEE 754, section 4. Ce type d'arrondi est parfois appelé arrondi au plus proche, ou arrondi bancaire. Il minimise les erreurs d'arrondi résultant de l'arrondi constant d'une valeur médiane dans une seule direction.

Vous pouvez spécifier un comportement d'arrondi différent en appelant les surcharges de Math.Round qui prennent un MidpointRoundingmode.

Michael Petrotta
la source
37

Vous devez vérifier MSDN pour Math.Round:

Le comportement de cette méthode suit la norme IEEE 754, section 4. Ce type d'arrondi est parfois appelé arrondi au plus proche, ou arrondi bancaire.

Vous pouvez spécifier le comportement d' Math.Roundutilisation d'une surcharge:

Math.Round(2.5, 0, MidpointRounding.AwayFromZero); // gives 3

Math.Round(2.5, 0, MidpointRounding.ToEven); // gives 2
Dirk Vollmar
la source
31

La nature de l'arrondi

Considérez la tâche d'arrondir un nombre qui contient une fraction pour, disons, un nombre entier. Dans ce cas, le processus d'arrondi consiste à déterminer le nombre entier qui représente le mieux le nombre que vous arrondissez.

Dans l'arrondissement commun ou «arithmétique», il est clair que 2.1, 2.2, 2.3 et 2.4 arrondissent à 2.0; et 2,6, 2,7, 2,8 et 2,9 à 3,0.

Cela laisse 2,5, ce qui n'est pas plus proche de 2,0 que de 3,0. C'est à vous de choisir entre 2.0 et 3.0, l'un ou l'autre serait également valable.

Pour les nombres négatifs, -2,1, -2,2, -2,3 et -2,4, deviendrait -2,0; et -2,6, 2,7, 2,8 et 2,9 deviendraient -3,0 avec l'arrondissement arithmétique.

Pour -2,5, un choix est nécessaire entre -2,0 et -3,0.

Autres formes d'arrondi

«Arrondir» prend n'importe quel nombre avec des décimales et en fait le prochain «entier». Ainsi, non seulement 2,5 et 2,6 arrondissent à 3,0, mais également 2.1 et 2.2.

L'arrondi éloigne les nombres positifs et négatifs de zéro. Par exemple. 2,5 à 3,0 et -2,5 à -3,0.

«Arrondir» tronque les nombres en supprimant les chiffres indésirables. Cela a pour effet de déplacer les nombres vers zéro. Par exemple. 2,5 à 2,0 et -2,5 à -2,0

Dans "l'arrondissement bancaire" - dans sa forme la plus courante - le .5 à arrondir est arrondi vers le haut ou vers le bas de sorte que le résultat de l'arrondi soit toujours un nombre pair. Ainsi, 2,5 arrondis à 2,0, 3,5 à 4,0, 4,5 à 4,0, 5,5 à 6,0, etc.

«Arrondi alternatif» alterne le processus pour tout .5 entre l'arrondi vers le bas et l'arrondi vers le haut.

L'arrondi aléatoire arrondit a .5 vers le haut ou vers le bas sur une base entièrement aléatoire.

Symétrie et asymétrie

Une fonction d'arrondi est dite «symétrique» si elle arrondit tous les nombres à partir de zéro ou arrondit tous les nombres à zéro.

Une fonction est «asymétrique» si elle arrondit des nombres positifs vers zéro et des nombres négatifs loin de zéro. Par exemple. 2,5 à 2,0; et -2,5 à -3,0.

Également asymétrique est une fonction qui arrondit les nombres positifs loin de zéro et les nombres négatifs vers zéro. Par exemple. 2,5 à 3,0; et -2,5 à -2,0.

La plupart du temps, les gens pensent à l'arrondi symétrique, où -2,5 sera arrondi vers -3,0 et 3,5 sera arrondi vers 4,0. (en C #Round(AwayFromZero))

Patrick Peters
la source
28

La valeur par défaut MidpointRounding.ToEven, ou l'arrondi des banquiers ( 2,5 devient 2, 4,5 devient 4 et ainsi de suite ) m'a déjà piqué avec la rédaction de rapports pour la comptabilité, donc j'écrirai quelques mots de ce que j'ai découvert, précédemment et en le recherchant ce post.

Qui sont ces banquiers qui arrondissent aux nombres pairs (banquiers britanniques peut-être!)?

De wikipedia

L'origine de l'arrondissement des banquiers terme reste plus obscure. Si cette méthode d'arrondi a jamais été une norme dans le secteur bancaire, les preuves se sont révélées extrêmement difficiles à trouver. Au contraire, la section 2 du rapport de la Commission européenne, L'introduction de l'euro et l'arrondissement des montants en devises, suggère qu'il n'existait pas auparavant d'approche standard pour l'arrondi dans le secteur bancaire; et il précise que les montants "à mi-chemin" doivent être arrondis.

Cela semble une manière très étrange d'arrondir, en particulier pour les banques, à moins bien sûr que les banques n'utilisent pour recevoir beaucoup de dépôts de montants pairs. Déposez 2,4 M £, mais nous l'appellerons 2 M £ monsieur.

La norme IEEE 754 remonte à 1985 et donne les deux façons d'arrondir, mais avec banquier comme recommandé par la norme. Cet article de wikipedia contient une longue liste de la façon dont les langues implémentent l'arrondi (corrigez-moi si l'une des erreurs ci-dessous est erronée) et la plupart n'utilisent pas celle de Bankers mais l'arrondi qu'on vous enseigne à l'école:

  • C / C ++ round () de math.h arrondit à partir de zéro (pas d'arrondi bancaire)
  • Java Math.Round arrondit à partir de zéro (il plancher le résultat, ajoute 0,5, transforme en un entier). Il y a une alternative dans BigDecimal
  • Perl utilise une manière similaire à C
  • Javascript est le même que le Math.Round de Java.
Chris S
la source
Merci pour l'information. Je ne m'en suis jamais rendu compte. Votre exemple sur les millions de ridiculise un peu, mais même si vous arrondissez en cents, devoir payer des intérêts sur 10 millions de comptes bancaires coûtera beaucoup à la banque si tous les demi-cents sont arrondis, ou coûtera beaucoup aux clients si tous les demi-cents sont arrondis. Je peux donc imaginer que c'est la norme convenue. Je ne sais pas si cela est vraiment utilisé par les banquiers. La plupart des clients ne remarqueront pas l'arrondissement, tout en rapportant beaucoup d'argent, mais je peux imaginer que cela est obligé par les lois si vous vivez dans un pays avec des lois conviviales
Harald Coppoolse
15

Depuis MSDN:

Par défaut, Math.Round utilise MidpointRounding.ToEven. La plupart des gens ne sont pas familiers avec "arrondir à égal" comme alternative, "arrondir loin de zéro" est plus communément enseigné à l'école. .NET par défaut est "Arrondi à égal" car il est statistiquement supérieur car il ne partage pas la tendance à "arrondir à partir de zéro" pour arrondir légèrement plus souvent qu'il arrondit (en supposant que les nombres arrondis ont tendance à être positifs. )

http://msdn.microsoft.com/en-us/library/system.math.round.aspx

Cristian Donoso
la source
3

Étant donné que Silverlight ne prend pas en charge l'option MidpointRounding, vous devez écrire la vôtre. Quelque chose comme:

public double RoundCorrect(double d, int decimals)
{
    double multiplier = Math.Pow(10, decimals);

    if (d < 0)
        multiplier *= -1;

    return Math.Floor((d * multiplier) + 0.5) / multiplier;

}

Pour les exemples, y compris comment utiliser cela comme une extension, consultez l'article: .NET et Silverlight Rounding

JBrooks
la source
3

J'ai eu ce problème où mon serveur SQL arrondit 0,5 à 1 alors que mon application C # ne l'a pas fait. Vous verriez donc deux résultats différents.

Voici une implémentation avec int / long. Voilà comment Java arrondit.

int roundedNumber = (int)Math.Floor(d + 0.5);

C'est probablement la méthode la plus efficace à laquelle vous pourriez penser.

Si vous voulez garder un double et utiliser une précision décimale, il s'agit vraiment d'utiliser des exposants de 10 en fonction du nombre de décimales.

public double getRounding(double number, int decimalPoints)
{
    double decimalPowerOfTen = Math.Pow(10, decimalPoints);
    return Math.Floor(number * decimalPowerOfTen + 0.5)/ decimalPowerOfTen;
}

Vous pouvez entrer une décimale négative pour les décimales et c'est bien aussi.

getRounding(239, -2) = 200
Mèche courte
la source
1

La manière la plus simple est:

Math.Ceiling(decimal.Parse(yourNumber + ""));
Nalan Madheswaran
la source
0

Ce message a la réponse que vous recherchez:

http://weblogs.asp.net/sfurman/archive/2003/03/07/3537.aspx

Fondamentalement, c'est ce qu'il dit:

Valeur de retour

Le nombre le plus proche avec une précision égale aux chiffres. Si la valeur est à mi-chemin entre deux nombres, dont l'un est pair et l'autre impair, le nombre pair est retourné. Si la précision de value est inférieure à chiffres, la valeur est retournée inchangée.

Le comportement de cette méthode suit la norme IEEE 754, section 4. Ce type d'arrondi est parfois appelé arrondi au plus proche, ou arrondi bancaire. Si les chiffres sont nuls, ce type d'arrondi est parfois appelé arrondi vers zéro.

Vaccano
la source
0

Silverlight ne prend pas en charge l'option MidpointRounding. Voici une méthode d'extension pour Silverlight qui ajoute l'énumération MidpointRounding:

public enum MidpointRounding
{
    ToEven,
    AwayFromZero
}

public static class DecimalExtensions
{
    public static decimal Round(this decimal d, MidpointRounding mode)
    {
        return d.Round(0, mode);
    }

    /// <summary>
    /// Rounds using arithmetic (5 rounds up) symmetrical (up is away from zero) rounding
    /// </summary>
    /// <param name="d">A Decimal number to be rounded.</param>
    /// <param name="decimals">The number of significant fractional digits (precision) in the return value.</param>
    /// <returns>The number nearest d with precision equal to decimals. If d is halfway between two numbers, then the nearest whole number away from zero is returned.</returns>
    public static decimal Round(this decimal d, int decimals, MidpointRounding mode)
    {
        if ( mode == MidpointRounding.ToEven )
        {
            return decimal.Round(d, decimals);
        }
        else
        {
            decimal factor = Convert.ToDecimal(Math.Pow(10, decimals));
            int sign = Math.Sign(d);
            return Decimal.Truncate(d * factor + 0.5m * sign) / factor;
        }
    }
}

Source: http://anderly.com/2009/08/08/silverlight-midpoint-rounding-solution/

jascur2
la source
-1

en utilisant un arrondi personnalisé

public int Round(double value)
{
    double decimalpoints = Math.Abs(value - Math.Floor(value));
    if (decimalpoints > 0.5)
        return (int)Math.Round(value);
    else
        return (int)Math.Floor(value);
}
anand360
la source
>.5produit le même comportement que Math.Round. La question est de savoir ce qui se passe lorsque la partie décimale est exactement 0.5. Math.Round vous permet de spécifier le type d'algorithme d'arrondi que vous souhaitez
Panagiotis Kanavos
-2

C'est laid comme tout l'enfer, mais produit toujours un arrondi arithmétique correct.

public double ArithRound(double number,int places){

  string numberFormat = "###.";

  numberFormat = numberFormat.PadRight(numberFormat.Length + places, '#');

  return double.Parse(number.ToString(numberFormat));

}
James Montagne
la source
5
Il en va de même pour appeler Math.Roundet spécifier comment vous souhaitez l'arrondir.
configurateur
-2

Voici comment je devais le contourner:

Public Function Round(number As Double, dec As Integer) As Double
    Dim decimalPowerOfTen = Math.Pow(10, dec)
    If CInt(number * decimalPowerOfTen) = Math.Round(number * decimalPowerOfTen, 2) Then
        Return Math.Round(number, 2, MidpointRounding.AwayFromZero)
    Else
        Return CInt(number * decimalPowerOfTen + 0.5) / 100
    End If
End Function

Essayer avec 1,905 avec 2 décimales donnera 1,91 comme prévu mais Math.Round(1.905,2,MidpointRounding.AwayFromZero)donne 1,90! La méthode Math.Round est absolument incohérente et inutilisable pour la plupart des problèmes de base que les programmeurs peuvent rencontrer. Je dois vérifier si (int) 1.905 * decimalPowerOfTen = Math.Round(number * decimalPowerOfTen, 2)je ne veux pas arrondir ce qui devrait être arrondi.

MikeMuffinMan
la source
Math.Round(1.905,2,MidpointRounding.AwayFromZero)retours1.91
Panagiotis Kanavos