¿Por qué Math.Round (2.5) devuelve 2 en lugar de 3?


416

En C #, el resultado de Math.Round(2.5)es 2.

Se supone que es 3, ¿no? ¿Por qué es 2 en cambio en C #?


55
En realidad es una característica. Consulte <a href=" msdn.microsoft.com/en-us/library/… Documentación de MSDN</a>. Este tipo de redondeo se conoce como redondeo bancario. En cuanto a la solución, hay <a href = " msdn. microsoft.com/en-us/library/… overload </a> que permite a la persona que llama especificar cómo redondear.
Joe

1
Aparentemente, el método de redondeo, cuando se le pide que redondee un número exactamente entre dos enteros, devuelve el entero par. Entonces, Math.Round (3.5) devuelve 4. Ver este artículo
Matthew Jones

20
Math.Round(2.5, 0, MidpointRounding.AwayFromZero);
Robert Durgin

SQL Server redondea de esa manera; Resultados de prueba interesantes cuando hay una prueba de unidad C # para validar el redondeo realizado en T-SQL.
idstam

77
@amed eso no es un error. Es la forma en que funcionan los puntos flotantes binarios. 1.005no se puede representar exactamente en doble. Es probable 1.00499.... Si usa Decimaleste problema desaparecerá. La existencia de la sobrecarga de Math.Round que toma varios dígitos decimales en doble es una dudosa elección de diseño IMO, ya que rara vez funcionará de manera significativa.
CodesInChaos

Respuestas:


561

En primer lugar, este no sería un error de C # de todos modos, sería un error de .NET. C # es el lenguaje: no decide cómo Math.Roundse implementa.

Y en segundo lugar, no; si lee los documentos , verá que el redondeo predeterminado es "redondeado a par" (redondeo del banco):


Tipo de valor de retorno : System.Double
El entero más cercano a. Si el componente fraccionario de a está a medio camino entre dos enteros, uno de los cuales es par y el otro impar, entonces se devuelve el número par. Tenga en cuenta que este método devuelve un Doubletipo integral en lugar de uno integral.

Comentarios
El comportamiento de este método sigue el Estándar 754 de IEEE, sección 4. Este tipo de redondeo a veces se llama redondeo al más cercano o redondeo bancario. Minimiza los errores de redondeo que resultan de redondear constantemente un valor de punto medio en una sola dirección.

Puede especificar cómo Math.Roundredondear los puntos medios utilizando una sobrecarga que toma un MidpointRoundingvalor. Hay una sobrecarga con una MidpointRoundingcorrespondiente a cada una de las sobrecargas que no tiene una:

Si este valor predeterminado se eligió bien o no es una cuestión diferente. ( MidpointRoundingsolo se introdujo en .NET 2.0. Antes de eso, no estoy seguro de que hubiera una manera fácil de implementar el comportamiento deseado sin hacerlo usted mismo). En particular, la historia ha demostrado que no es el comportamiento esperado , y en la mayoría de los casos eso es un pecado capital en el diseño de API. Puedo ver por qué Banker's Rounding es útil ... pero sigue siendo una sorpresa para muchos.

Puede interesarle echar un vistazo al enum equivalente de Java más cercano ( RoundingMode) que ofrece aún más opciones. (No se trata solo de puntos medios).


44
No sé si esto es un error, creo que fue por diseño ya que .5 está tan cerca del entero más bajo más cercano como del entero más alto más cercano.
Stan R.

3
Recuerdo este comportamiento en VB antes de que se aplicara .NET.
John Fiala

77
De hecho, IEEE Standard 754, sección 4, como lo indica la documentación.
Jon Skeet

2
Me quemé por esto hace un tiempo y pensé que también era pura locura. Afortunadamente, agregaron una forma de especificar el redondeo que todos aprendimos en la escuela primaria; MidPointRounding.
Shea

26
+1 para "no es el comportamiento esperado [...] que es un pecado
capital

215

Eso se llama redondeo a par (o redondeo bancario), que es una estrategia de redondeo válida para minimizar los errores acumulados en las sumas (MidpointRounding.ToEven). La teoría es que, si siempre redondea un número 0.5 en la misma dirección, los errores se acumularán más rápido (se supone que redondear a par minimiza eso) (a) .

Siga estos enlaces para las descripciones de MSDN de:

  • Math.Floor, que se redondea hacia el infinito negativo.
  • Math.Ceiling, que se redondea hacia el infinito positivo.
  • Math.Truncate, que se redondea hacia arriba o hacia abajo hacia cero.
  • Math.Round, que se redondea al número entero más cercano o al número especificado de lugares decimales. Puede especificar el comportamiento si es exactamente equidistante entre dos posibilidades, como redondear para que el dígito final sea par (" Round(2.5,MidpointRounding.ToEven)" se convierte en 2) o para que esté más alejado de cero (" Round(2.5,MidpointRounding.AwayFromZero)" se convierta en 3).

El siguiente diagrama y tabla pueden ayudar:

-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

Tenga en cuenta que Roundes mucho más poderoso de lo que parece, simplemente porque puede redondear a un número específico de decimales. Todos los demás redondean a cero decimales siempre. Por ejemplo:

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

Con las otras funciones, debe usar el truco de multiplicar / dividir para lograr el mismo efecto:

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

(a) Por supuesto, esa teoría depende del hecho de que sus datos tienen una distribución bastante uniforme de valores entre las mitades pares (0.5, 2.5, 4.5, ...) y las mitades impares (1.5, 3.5, ...).

Si todos los "valores medios" son pares (por ejemplo), los errores se acumularán tan rápido como si siempre hubiera redondeado.


3
También conocido como redondeo
bancario

¡Buena explicación! Quería ver por mí mismo cómo se acumula el error y escribí un script que muestra que los valores redondeados usando el redondeo de los banqueros, a la larga, tienen sumas y promedios mucho más cercanos a estos valores originales. github.com/AmadeusW/RoundingDemo (fotos de parcelas disponibles)
Amadeusz Wieczorek

Poco tiempo después: ¿no debería emarcar (= 2.8) más a la derecha que 2marcar?
superjos

Una manera simple de recordar, y asumiendo que el lugar de las décimas es 5: - el lugar de las unidades y el lugar de las décimas son impares = redondear hacia arriba - el lugar de las unidades y el lugar de las décimas se mezclan = redondear hacia abajo * Cero no es impar * Invertido para números negativos
Arkham Angel

@ArkhamAngel, eso en realidad parece más difícil de recordar que simplemente "hacer el último dígito incluso" :-)
paxdiablo

42

Desde MSDN, Math.Round (doble a) devuelve:

El entero más cercano a. Si el componente fraccionario de a está a medio camino entre dos enteros, uno de los cuales es par y el otro impar, entonces se devuelve el número par.

... y entonces 2.5, estando a medio camino entre 2 y 3, se redondea al número par (2). Esto se llama redondeo bancario (o redondeo a par), y es un estándar de redondeo de uso común.

Mismo artículo de MSDN:

El comportamiento de este método sigue el estándar IEEE 754, sección 4. Este tipo de redondeo a veces se llama redondeo al más cercano o redondeo bancario. Minimiza los errores de redondeo que resultan de redondear constantemente un valor de punto medio en una sola dirección.

Puede especificar un comportamiento de redondeo diferente llamando a las sobrecargas de Math.Round que toman un MidpointRoundingmodo.


37

Debe verificar MSDN para Math.Round:

El comportamiento de este método sigue el Estándar 754 de IEEE, sección 4. Este tipo de redondeo a veces se llama redondeo al más cercano o redondeo bancario.

Puede especificar el comportamiento de Math.Roundusar una sobrecarga:

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

Math.Round(2.5, 0, MidpointRounding.ToEven); // gives 2

31

La naturaleza del redondeo.

Considere la tarea de redondear un número que contiene una fracción a, por ejemplo, un número entero. El proceso de redondeo en esta circunstancia es determinar qué número entero representa mejor el número que está redondeando.

En común, o redondeo 'aritmético', está claro que 2.1, 2.2, 2.3 y 2.4 se redondean a 2.0; y 2.6, 2.7, 2.8 y 2.9 a 3.0.

Eso deja 2.5, que no está más cerca de 2.0 que de 3.0. Depende de usted elegir entre 2.0 y 3.0, ya sea igualmente válido.

Para los números menos, -2.1, -2.2, -2.3 y -2.4, se convertirían en -2.0; y -2.6, 2.7, 2.8 y 2.9 se convertirían en -3.0 bajo redondeo aritmético.

Para -2.5 se necesita una elección entre -2.0 y -3.0.

Otras formas de redondeo

'Redondear' toma cualquier número con decimales y lo convierte en el próximo número 'entero'. Por lo tanto, no solo 2.5 y 2.6 redondean a 3.0, sino también 2.1 y 2.2.

El redondeo aleja los números positivos y negativos de cero. P.ej. 2.5 a 3.0 y -2.5 a -3.0.

'Redondear hacia abajo' trunca los números cortando dígitos no deseados. Esto tiene el efecto de mover los números hacia cero. P.ej. 2.5 a 2.0 y -2.5 a -2.0

En el "redondeo bancario", en su forma más común, el .5 a redondear se redondea hacia arriba o hacia abajo para que el resultado del redondeo sea siempre un número par. Por lo tanto, 2.5 se redondea a 2.0, 3.5 a 4.0, 4.5 a 4.0, 5.5 a 6.0, y así sucesivamente.

El 'redondeo alternativo' alterna el proceso para cualquier .5 entre redondeo hacia abajo y redondeo hacia arriba.

El 'redondeo aleatorio' redondea un .5 hacia arriba o hacia abajo de forma completamente aleatoria.

Simetría y asimetría

Se dice que una función de redondeo es 'simétrica' si redondea todos los números desde cero o redondea todos los números hacia cero.

Una función es 'asimétrica' si redondea los números positivos hacia cero y los números negativos lejos de cero. Ej. 2.5 a 2.0; y -2.5 a -3.0.

También asimétrica es una función que redondea los números positivos desde cero y los negativos hacia cero. P.ej. 2.5 a 3.0; y -2.5 a -2.0.

La mayoría de las veces la gente piensa en el redondeo simétrico, donde -2.5 se redondeará hacia -3.0 y 3.5 se redondeará hacia 4.0. (en C #Round(AwayFromZero))


28

El valor predeterminado MidpointRounding.ToEven, o redondeo de los banqueros ( 2.5 se convierte en 2, 4.5 se convierte en 4 y así sucesivamente ) me ha picado antes con la redacción de informes para la contabilidad, por lo que escribiré algunas palabras de lo que descubrí, anteriormente y al buscarlo esta publicación.

¿Quiénes son estos banqueros que están redondeando los números pares (quizás los banqueros británicos)?

De wikipedia

El origen del término redondeo de los banqueros sigue siendo más oscuro. Si este método de redondeo fue alguna vez un estándar en la banca, la evidencia ha resultado extremadamente difícil de encontrar. Por el contrario, la sección 2 del informe de la Comisión Europea La introducción del euro y el redondeo de los montos de divisas sugiere que anteriormente no había habido un enfoque estándar para el redondeo en la banca; y especifica que las cantidades "intermedias" deben redondearse.

Parece una forma muy extraña de redondeo, particularmente para la banca, a menos, por supuesto, que los bancos usen para recibir muchos depósitos de cantidades pares. Deposite £ 2.4m, pero lo llamaremos £ 2m señor.

El estándar 754 de IEEE se remonta a 1985 y ofrece dos formas de redondeo, pero con el estándar bancario recomendado por el estándar. Este artículo de wikipedia tiene una larga lista de cómo los idiomas implementan el redondeo (corríjame si alguno de los siguientes es incorrecto) y la mayoría no usa los Banqueros sino el redondeo que se le enseña en la escuela:

  • C / C ++ round () de math.h se redondea desde cero (no el redondeo del banco)
  • Java Math.Round se redondea desde cero (pone el resultado, agrega 0.5, se convierte en un entero). Hay una alternativa en BigDecimal
  • Perl usa una forma similar a C
  • Javascript es lo mismo que Math.Round de Java.

Gracias por la información. Nunca me di cuenta de esto. Su ejemplo sobre los millones ridiculiza un poco, pero incluso si redondea centavos, tener que pagar intereses en 10 millones de cuentas bancarias le costará mucho al banco si se redondean todos los medios centavos, o costará mucho a los clientes si todo los medios centavos se redondean hacia abajo. Así que puedo imaginar que este es el estándar acordado. Sin embargo, no estoy seguro de si esto es realmente utilizado por los banqueros. La mayoría de los clientes no notarán el redondeo, mientras que traen mucho dinero, pero me imagino que esto está obligado por las leyes si vives en un país con leyes amigables para el cliente
Harald Coppoolse

15

De MSDN:

Por defecto, Math.Round usa MidpointRounding.ToEven. La mayoría de las personas no están familiarizadas con el "redondeo para igualar" como alternativa, el "redondeo desde cero" se enseña más comúnmente en la escuela. El valor predeterminado de .NET es "Redondear a par", ya que es estadísticamente superior porque no comparte la tendencia de "redondear desde cero" a redondear un poco más de lo que se redondea (suponiendo que los números que se redondean tienden a ser positivos. )

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


3

Como Silverlight no admite la opción MidpointRounding, debe escribir la suya. 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 ver ejemplos que incluyen cómo usar esto como una extensión, vea la publicación: .NET y Silverlight Rounding


3

Tuve este problema donde mi servidor SQL redondea 0.5 a 1 mientras que mi aplicación C # no lo hizo. Entonces verías dos resultados diferentes.

Aquí hay una implementación con int / long. Así es como se redondea Java.

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

Probablemente sea el método más eficiente que se te ocurra.

Si desea mantener un doble y usar precisión decimal, en realidad solo es cuestión de usar exponentes de 10 en función de cuántos decimales.

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

Puede ingresar un decimal negativo para los puntos decimales y su palabra también está bien.

getRounding(239, -2) = 200


0

Esta publicación tiene la respuesta que estás buscando:

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

Básicamente esto es lo que dice:

Valor de retorno

El valor numérico más cercano con precisión igual a dígitos. Si el valor está a medio camino entre dos números, uno de los cuales es par y el otro impar, entonces se devuelve el número par. Si la precisión del valor es menor que los dígitos, el valor se devuelve sin cambios.

El comportamiento de este método sigue el Estándar 754 de IEEE, sección 4. Este tipo de redondeo a veces se llama redondeo al más cercano o redondeo bancario. Si los dígitos son cero, este tipo de redondeo a veces se llama redondeo hacia cero.


0

Silverlight no admite la opción MidpointRounding. Aquí hay un método de extensión para Silverlight que agrega la enumeración 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;
        }
    }
}

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


-1

utilizando un redondeo 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);
}

>.5produce el mismo comportamiento que Math.Round. La pregunta es qué sucede cuando la parte decimal es exactamente 0.5. Math.Round le permite especificar el tipo de algoritmo de redondeo que desea
Panagiotis Kanavos

-2

Esto es feo como el infierno, pero siempre produce un redondeo aritmético correcto.

public double ArithRound(double number,int places){

  string numberFormat = "###.";

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

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

}

55
También lo hace llamar Math.Roundy especificar cómo desea que se redondee.
configurador

-2

Esta es la forma en que tuve que solucionarlo:

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

¡Probar con 1.905 con 2 decimales dará 1.91 como se esperaba pero Math.Round(1.905,2,MidpointRounding.AwayFromZero)da 1.90! El método Math.Round es absolutamente inconsistente e inutilizable para la mayoría de los problemas básicos que los programadores pueden encontrar. Tengo que verificar si (int) 1.905 * decimalPowerOfTen = Math.Round(number * decimalPowerOfTen, 2)no quiero redondear lo que debería ser redondeado.


Math.Round(1.905,2,MidpointRounding.AwayFromZero)vuelve1.91
Panagiotis Kanavos
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.