Por que Math.Round (2.5) retorna 2 em vez de 3?

415

Em C #, o resultado de Math.Round(2.5)é 2.

É suposto ser 3, não é? Por que é 2 em vez de c #?

jeffu
fonte
5
Na verdade, é um recurso. Consulte <a href=" msdn.microsoft.com/en-us/library/… documentação do MSDN</a> . Esse tipo de arredondamento é conhecido como arredondamento de banqueiro. Quanto a uma solução alternativa, existe <a href = " msdn. microsoft.com/en-us/library/… sobrecarga </a> que permite ao chamador especificar como fazer o arredondamento.
1111 Joe
1
Aparentemente, o método round, quando solicitado a arredondar um número exatamente entre dois números inteiros, retorna o número inteiro par. Então, Math.Round (3.5) retorna 4. Veja este artigo
Matthew Jones
20
Math.Round(2.5, 0, MidpointRounding.AwayFromZero);
Robert Durgin
O SQL Server contorna esse caminho; resultados interessantes de teste quando há um teste de unidade C # para validar o arredondamento feito no T-SQL.
Idstam 30/06/09
7
@amed isso não é um bug. É assim que os pontos flutuantes binários funcionam. 1.005não pode ser representado exatamente em dobro. Provavelmente é 1.00499.... Se você usar Decimalesse problema desaparecerá. A existência da sobrecarga Math.Round que leva um número de dígitos decimais em dobro é uma opção de design duvidosa do IMO, pois raramente funcionará de maneira significativa.
CodesInChaos

Respostas:

560

Em primeiro lugar, isso não seria um bug do C # de qualquer maneira - seria um bug do .NET. C # é a linguagem - ele não decide como Math.Roundé implementado.

E segundo, não - se você ler os documentos , verá que o arredondamento padrão é "arredondado para o par" (arredondamento do banqueiro):

Valor de retorno
Tipo: System.Double
O número inteiro mais próximo a. Se o componente fracionário de a estiver na metade do caminho entre dois números inteiros, um dos quais é par e o outro ímpar, o número par será retornado. Observe que esse método retorna um tipo em Doublevez de um integral.

Comentários
O comportamento desse método segue a Norma IEEE 754, seção 4. Esse tipo de arredondamento às vezes é chamado de arredondamento para o mais próximo, ou arredondamento de banqueiro. Ele minimiza os erros de arredondamento resultantes do arredondamento consistente de um valor do ponto médio em uma única direção.

Você pode especificar como Math.Roundarredondar os pontos médios usando uma sobrecarga que recebe um MidpointRoundingvalor. Há uma sobrecarga com uma MidpointRoundingcorrespondente a cada uma das sobrecargas que não possui uma:

Se esse padrão foi bem escolhido ou não é uma questão diferente. ( MidpointRoundingfoi introduzido apenas no .NET 2.0. Antes disso, não tenho certeza de que havia uma maneira fácil de implementar o comportamento desejado sem fazer você mesmo.) Em particular, o histórico mostrou que esse não é o comportamento esperado - e na maioria dos casos esse é o comportamento esperado . um pecado fundamental no design da API. Eu posso ver por que o Banker's Rounding é útil ... mas ainda é uma surpresa para muitos.

Você pode estar interessado em dar uma olhada no enum equivalente Java mais próximo ( RoundingMode), que oferece ainda mais opções. (Não se trata apenas de pontos médios.)

Jon Skeet
fonte
4
Eu não sei se isso é um bug, acho que foi por design, já que 0,5 é tão próximo ao número inteiro mais baixo mais próximo quanto ao número mais alto mais próximo.
Stan R.
3
Lembro-me desse comportamento no VB antes da aplicação do .NET.
John Fiala
7
De fato, a Norma 4 da IEEE 754, seção 4, como declara a documentação.
Jon Skeet
2
Eu me queimei com isso há um tempo atrás e pensei que era pura loucura também. Felizmente, eles acrescentaram uma maneira de especificar o arredondamento que todos nós aprendemos na escola primária; MidPointRounding.
Shea
26
+1 para "não é o comportamento esperado [...] que é um pecado
fundamental
215

Isso é chamado de arredondamento para o par (ou arredondamento do banqueiro), que é uma estratégia de arredondamento válida para minimizar os erros acumulados nas somas (MidpointRounding.ToEven). A teoria é que, se você sempre arredondar um número 0,5 na mesma direção, os erros serão acumulados mais rapidamente (o arredondamento para o par deve minimizar isso) (a) .

Siga estes links para as descrições do MSDN de:

  • Math.Floor, que arredonda para o infinito negativo.
  • Math.Ceiling, que arredonda para o infinito positivo.
  • Math.Truncate, que arredonda para cima ou para baixo em direção a zero.
  • Math.Round, que arredonda para o número inteiro mais próximo ou o número especificado de casas decimais. Você pode especificar o comportamento se ele for exatamente equidistante entre duas possibilidades, como o arredondamento para que o dígito final seja par (" Round(2.5,MidpointRounding.ToEven)" se torne 2) ou para que fique mais longe de zero (" Round(2.5,MidpointRounding.AwayFromZero)" se torne 3).

O diagrama e tabela a seguir podem ajudar:

-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

Observe que Roundé muito mais poderoso do que parece, simplesmente porque pode arredondar para um número específico de casas decimais. Todos os outros arredondam para zero decimais sempre. Por exemplo:

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

Com as outras funções, você deve usar truques de multiplicar / dividir para obter o mesmo efeito:

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

(a) Obviamente, essa teoria depende do fato de que seus dados têm uma distribuição bastante uniforme de valores pelas metades pares (0,5, 2,5, 4,5, ...) e metades ímpares (1,5, 3,5, ...).

Se todos os "meios-valores" forem iguais (por exemplo), os erros acumularão tão rápido quanto se você sempre arredondasse.

paxdiablo
fonte
3
Também conhecida como arredondamento do banqueiro
Pondidum
Boa explicação! Eu queria ver por mim mesmo como o erro se acumula e escrevi um script que mostra que os valores arredondados usando o arredondamento bancário, a longo prazo, têm suas somas e médias muito mais próximas das dos valores originais. github.com/AmadeusW/RoundingDemo (fotos das parcelas disponíveis)
Amadeusz Wieczorek
Pouco tempo depois: o etiquetaque (= 2,8) não deveria estar mais certo do que o 2tiquetaque?
Superjo
Uma maneira simples de lembrar, e assumindo que o décimo lugar é 5: - o local e o décimo são todos ímpares = arredondado para cima - o local e o décimo lugar são misturados = arredondado para baixo * Zero não é ímpar * Invertido para números negativos
Arkham Angel
@ArkhamAngel, que realmente parece mais difícil de lembrar do que apenas "fazer o último dígito mesmo" :-)
paxdiablo
42

No MSDN, Math.Round (duplo a) retorna:

O número inteiro mais próximo de a. Se o componente fracionário de a estiver na metade do caminho entre dois números inteiros, um dos quais é par e o outro ímpar, o número par será retornado.

... e então 2,5, estando entre 2 e 3, é arredondado para o número par (2). isso é chamado de arredondamento do banqueiro (ou arredondado para o par) e é um padrão de arredondamento comumente usado.

Mesmo artigo do MSDN:

O comportamento deste método segue a Norma IEEE 754, seção 4. Esse tipo de arredondamento às vezes é chamado de arredondamento para o mais próximo, ou arredondamento de banqueiro. Minimiza os erros de arredondamento resultantes do arredondamento consistente de um valor do ponto médio em uma única direção.

Você pode especificar um comportamento de arredondamento diferente chamando as sobrecargas de Math.Round que assumem um MidpointRoundingmodo.

Michael Petrotta
fonte
37

Você deve verificar o MSDN para Math.Round:

O comportamento deste método segue a Norma IEEE 754, seção 4. Esse tipo de arredondamento às vezes é chamado de arredondamento para o mais próximo, ou arredondamento de banqueiro.

Você pode especificar o comportamento de Math.Roundusar uma sobrecarga:

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

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

A natureza do arredondamento

Considere a tarefa de arredondar um número que contenha uma fração para, digamos, um número inteiro. O processo de arredondamento nessa circunstância é determinar qual número inteiro representa melhor o número que você está arredondando.

Em comum, ou arredondamento 'aritmético', é claro que 2.1, 2.2, 2.3 e 2.4 arredondam para 2.0; e 2.6, 2.7, 2.8 e 2.9 a 3.0.

Isso deixa 2,5, o que não é mais próximo de 2,0 do que de 3,0. Cabe a você escolher entre 2,0 e 3,0, ou seria igualmente válido.

Para números negativos, -2,1, -2,2, -2,3 e -2,4 se tornariam -2,0; e -2,6, 2,7, 2,8 e 2,9 se tornariam -3,0 sob arredondamento aritmético.

Para -2,5, é necessária uma escolha entre -2,0 e -3,0.

Outras formas de arredondamento

'Arredondar' pega qualquer número com casas decimais e o torna o próximo número 'inteiro'. Assim, não apenas 2,5 e 2,6 arredondam para 3,0, mas também 2,1 e 2,2.

O arredondamento para cima move os números positivos e negativos para longe de zero. Por exemplo. 2,5 a 3,0 e -2,5 a -3,0.

'Arredondar para baixo' trunca números cortando dígitos indesejados. Isso tem o efeito de mover números para zero. Por exemplo. 2,5 a 2,0 e -2,5 a -2,0

No "arredondamento do banqueiro" - em sua forma mais comum -, o 0,5 a ser arredondado é arredondado para cima ou para baixo, de modo que o resultado do arredondamento seja sempre um número par. Assim, 2,5 arredonda para 2,0, 3,5 para 4,0, 4,5 para 4,0, 5,5 para 6,0 e assim por diante.

'Arredondamento alternativo' alterna o processo para qualquer 0,5 entre arredondamento para baixo e arredondamento para cima.

'Arredondamento aleatório' arredonda 0,5 para cima ou para baixo de maneira totalmente aleatória.

Simetria e assimetria

Diz-se que uma função de arredondamento é 'simétrica' se arredondar todos os números para longe de zero ou arredondar todos os números para zero.

Uma função é 'assimétrica' se arredondar números positivos para zero e números negativos longe de zero. 2,5 a 2,0; e -2,5 a -3,0.

Também assimétrica é uma função que arredonda números positivos para longe de zero e números negativos para zero. Por exemplo. 2,5 a 3,0; e -2,5 a -2,0.

Na maioria das vezes, as pessoas pensam no arredondamento simétrico, onde -2,5 será arredondado para -3,0 e 3,5 será arredondado para 4,0. (em c #Round(AwayFromZero))

Patrick Peters
fonte
28

O padrão MidpointRounding.ToEven, ou o arredondamento dos banqueiros ( 2,5 passa a 2, 4,5 passa a 4 e assim por diante ) me picou antes com a elaboração de relatórios para contabilidade, por isso vou escrever algumas palavras do que descobri anteriormente e de procurar por ele esta postagem.

Quem são esses banqueiros que estão arredondando para baixo em números pares (banqueiros britânicos, talvez!)?

Da wikipedia

A origem do termo arredondamento de banqueiros permanece mais obscura. Se esse método de arredondamento já foi um padrão no setor bancário, as evidências se mostraram extremamente difíceis de encontrar. Pelo contrário, a seção 2 do relatório da Comissão Europeia A Introdução do Euro e o Arredondamento de Valores em Moeda sugere que anteriormente não havia uma abordagem padrão para o arredondamento no setor bancário; e especifica que os valores "intermediários" devem ser arredondados para cima.

Parece uma maneira muito estranha de arredondar especialmente para o setor bancário, a menos que os bancos usem para receber muitos depósitos de quantias iguais. Deposite 2,4 milhões de libras, mas chamaremos de 2 milhões de libras.

A norma IEEE 754 remonta a 1985 e oferece os dois modos de arredondamento, mas com os banqueiros recomendados pela norma. Este artigo da wikipedia possui uma lista longa de como os idiomas implementam o arredondamento (corrija-me se alguma das opções abaixo estiver errada) e a maioria não usa os Bankers, mas o arredondamento que você ensina na escola:

  • C / C ++ arredondado () de math.h arredonda para longe de zero (não arredondamento de banqueiro)
  • Java Math.Round arredonda o zero ( calcula o resultado, adiciona 0,5 e projeta para um número inteiro). Existe uma alternativa no BigDecimal
  • Perl usa uma maneira semelhante a C
  • Javascript é o mesmo que Math.Round do Java.
Chris S
fonte
Obrigado pela informação. Eu nunca percebi isso. Seu exemplo sobre os milhões ridiculariza um pouco, mas mesmo se você pagar centavos, ter que pagar juros em 10 milhões de contas bancárias custará muito ao banco se todos os centavos forem arredondados, ou custará muito aos clientes se tudo meio centavo são arredondados para baixo. Então, eu posso imaginar que este é o padrão acordado. Não tenho certeza se isso é realmente usado pelos banqueiros. A maioria dos clientes não vai notar o arredondamento para baixo, enquanto trazendo um monte de dinheiro, mas posso imaginar que este é obrigado por lei, se você vive em um país com leis favoráveis ao cliente
Harald Coppoolse
15

Do MSDN:

Por padrão, Math.Round usa MidpointRounding.ToEven. A maioria das pessoas não está familiarizada com o "arredondamento para o par" como alternativa, "o arredondamento para zero" é mais comumente ensinado na escola. O padrão do .NET é "Arredondar para o par", pois é estatisticamente superior porque não compartilha a tendência de "arredondar para zero" para arredondar um pouco mais frequentemente do que arredondar para baixo (assumindo que os números sendo arredondados tendem a ser positivos. )

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

Cristian Donoso
fonte
3

Como o Silverlight não suporta a opção MidpointRounding, você deve escrever sua própria. Algo como:

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;

}

Para obter exemplos, incluindo como usar isso como uma extensão, consulte a publicação: .NET e Silverlight Rounding

JBrooks
fonte
3

Eu tive esse problema em que meu servidor SQL arredonda 0,5 para 1 enquanto meu aplicativo C # não. Então você veria dois resultados diferentes.

Aqui está uma implementação com int / long. É assim que o Java arredonda.

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

É provavelmente o método mais eficiente que você pode pensar também.

Se você deseja manter o dobro e usar a precisão decimal, é realmente apenas uma questão de usar expoentes de 10 com base em quantas casas decimais.

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

Você pode inserir um decimal negativo para pontos decimais e a palavra também é fina.

getRounding(239, -2) = 200
Pavio curto
fonte
1

Maneira simples é:

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

Este post tem a resposta que você está procurando:

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

Basicamente, é o que diz:

Valor de retorno

O valor do número mais próximo com precisão igual a dígitos. Se o valor estiver na metade do caminho entre dois números, um dos quais é par e o outro ímpar, o número par será retornado. Se a precisão do valor for menor que dígitos, o valor será retornado inalterado.

O comportamento deste método segue a Norma IEEE 754, seção 4. Esse tipo de arredondamento às vezes é chamado de arredondamento para o mais próximo, ou arredondamento de banqueiro. Se os dígitos forem zero, esse tipo de arredondamento às vezes é chamado de arredondamento para zero.

Vaccano
fonte
0

O Silverlight não suporta a opção MidpointRounding. Aqui está um método de extensão para o Silverlight que adiciona a enumeração 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;
        }
    }
}

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

jascur2
fonte
-1

usando um arredondamento personalizado

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
fonte
>.5produz o mesmo comportamento que Math.Round. A questão é o que acontece quando a parte decimal é exatamente 0.5. Math.Round permite que você especifique o tipo de arredondamento algoritmo que você quer
Panagiotis Kanavos
-2

Isso é feio como o inferno, mas sempre produz arredondamentos aritméticos corretos.

public double ArithRound(double number,int places){

  string numberFormat = "###.";

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

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

}
James Montagne
fonte
5
O mesmo acontece com a chamada Math.Rounde a especificação de como você deseja arredondar.
configurator
-2

Aqui está a maneira que eu tive que contornar isso:

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

Tentar com 1,905 com 2 casas decimais dará 1,91 conforme o esperado, mas Math.Round(1.905,2,MidpointRounding.AwayFromZero)dará 1,90! O método Math.Round é absolutamente inconsistente e inutilizável para a maioria dos problemas básicos que os programadores podem encontrar. Eu tenho que verificar se (int) 1.905 * decimalPowerOfTen = Math.Round(number * decimalPowerOfTen, 2)porque eu não quero arredondar o que deve ser arredondado para baixo.

MikeMuffinMan
fonte
Math.Round(1.905,2,MidpointRounding.AwayFromZero)retorna1.91
Panagiotis Kanavos 12/08