2010-02-12 4 views
18

Veo que, en C#, redondeando un decimal, por defecto, usa MidpointRounding.ToEven. Esto es lo que se espera, y es lo que dicta la especificación de C#. Sin embargo, dada la siguiente:¿Por qué .NET decimal.ToString (cadena) se aleja de cero, aparentemente inconsistente con la especificación de idioma?

  • Un decimal dVal
  • Un formato string sFmt que, cuando se pasa a dVal.ToString(sFmt), dará lugar a una cadena que contiene una versión redondeada de dVal

... es aparente que decimal.ToString(string) devuelve un valor redondeado usando MidpointRounding.AwayFromZero. Esto parece ser una contradicción directa de la especificación de C#.

Mi pregunta es esta: ¿Hay alguna buena razón para que este sea el caso? ¿O es esto solo una inconsistencia en el lenguaje?

A continuación, como referencia, he incluido un código que escribe para consolar una variedad de resultados de operación de redondeo y resultados de operación decimal.ToString(string), cada uno en cada valor en una matriz de valores decimal. Las salidas reales están incrustadas. Después de eso, he incluido un párrafo relevante de la sección Especificación del lenguaje C# en el tipo decimal.

El código de ejemplo:

static void Main(string[] args) 
{ 
    decimal[] dArr = new decimal[] { 12.345m, 12.355m }; 

    OutputBaseValues(dArr); 
    // Base values: 
    // d[0] = 12.345 
    // d[1] = 12.355 

    OutputRoundedValues(dArr); 
    // Rounding with default MidpointRounding: 
    // Math.Round(12.345, 2) => 12.34 
    // Math.Round(12.355, 2) => 12.36 
    // decimal.Round(12.345, 2) => 12.34 
    // decimal.Round(12.355, 2) => 12.36 

    OutputRoundedValues(dArr, MidpointRounding.ToEven); 
    // Rounding with mr = MidpointRounding.ToEven: 
    // Math.Round(12.345, 2, mr) => 12.34 
    // Math.Round(12.355, 2, mr) => 12.36 
    // decimal.Round(12.345, 2, mr) => 12.34 
    // decimal.Round(12.355, 2, mr) => 12.36 

    OutputRoundedValues(dArr, MidpointRounding.AwayFromZero); 
    // Rounding with mr = MidpointRounding.AwayFromZero: 
    // Math.Round(12.345, 2, mr) => 12.35 
    // Math.Round(12.355, 2, mr) => 12.36 
    // decimal.Round(12.345, 2, mr) => 12.35 
    // decimal.Round(12.355, 2, mr) => 12.36 

    OutputToStringFormatted(dArr, "N2"); 
    // decimal.ToString("N2"): 
    // 12.345.ToString("N2") => 12.35 
    // 12.355.ToString("N2") => 12.36 

    OutputToStringFormatted(dArr, "F2"); 
    // decimal.ToString("F2"): 
    // 12.345.ToString("F2") => 12.35 
    // 12.355.ToString("F2") => 12.36 

    OutputToStringFormatted(dArr, "###.##"); 
    // decimal.ToString("###.##"): 
    // 12.345.ToString("###.##") => 12.35 
    // 12.355.ToString("###.##") => 12.36 

    Console.ReadKey(); 
} 

private static void OutputBaseValues(decimal[] dArr) 
{ 
    Console.WriteLine("Base values:"); 
    for (int i = 0; i < dArr.Length; i++) Console.WriteLine("d[{0}] = {1}", i, dArr[i]); 
    Console.WriteLine(); 
} 

private static void OutputRoundedValues(decimal[] dArr) 
{ 
    Console.WriteLine("Rounding with default MidpointRounding:"); 
    foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2) => {1}", d, Math.Round(d, 2)); 
    foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2) => {1}", d, decimal.Round(d, 2)); 
    Console.WriteLine(); 
} 

private static void OutputRoundedValues(decimal[] dArr, MidpointRounding mr) 
{ 
    Console.WriteLine("Rounding with mr = MidpointRounding.{0}:", mr); 
    foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2, mr) => {1}", d, Math.Round(d, 2, mr)); 
    foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2, mr) => {1}", d, decimal.Round(d, 2, mr)); 
    Console.WriteLine(); 
} 

private static void OutputToStringFormatted(decimal[] dArr, string format) 
{ 
    Console.WriteLine("decimal.ToString(\"{0}\"):", format); 
    foreach (decimal d in dArr) Console.WriteLine("{0}.ToString(\"{1}\") => {2}", d, format, d.ToString(format)); 
    Console.WriteLine(); 
} 


El párrafo de la sección 4.1.7 de la especificación del lenguaje C# ("El tipo decimal") (obtener la especificación completa here (.doc)):

El resultado de una operación en valores de tipo decimal es aquel que resultaría del cálculo de un resultado exacto (preservando la escala, como se define para cada operador) y luego redondeando para ajustarse a la representación. Los resultados se redondean al valor representable más cercano y, cuando un resultado es igualmente cercano a dos valores representables, al valor que tiene un número par en la posición de dígito menos significativo (esto se conoce como "redondeo bancario"). Un resultado cero siempre tiene un signo de 0 y una escala de 0.

Es fácil ver que pueden no haber estado considerando ToString(string) en este párrafo, pero me inclino a pensar que cabe en esta descripción.

+2

Es posible que deba considerar que C# no tiene el método 'ToString (string)'. El .NET Framework sí. No estoy seguro de que .NET Framework esté obligado a obedecer las reglas de cualquier lenguaje de programación en particular. –

Respuesta

1

ToString() por defecto formatea de acuerdo con Culture, no según un aspecto computacional de la especificación. Aparentemente, el Culture para su localidad (y la mayoría, por lo que parece) espera redondear desde cero.

Si desea un comportamiento diferente, se puede pasar un IFormatProvider a ToString()

pensé que el anterior, pero estás en lo correcto de que siempre redondea lejos de cero sin importar el Culture. Verifique los enlaces en los comentarios para prueba.

+0

Pensé esto también recientemente, pero después de que me lo pidieron no pude encontrar ningún lugar en CultureInfo o NumberFormatInfo donde se especifica o determina el redondeo. ¿Puedes señalar dónde ocurre esto en realidad? –

+0

No puedo, y parece que probablemente estoy equivocado. * Aviso de licencia: origen de CLR tras enlace * Parece que http://www.koders.com/cpp/fid03737280F05F3996789AC863BDE66ACB337C1E9B.aspx?s=NumberToStringFormat#L1457 NumberToStringFormat llama a http://www.koders.com/cpp/fid03737280F05F3996789AC863BDE66ACB337C1E9B.aspx ? s = NumberToStringFormat # L838 RoundNumber que siempre redondea desde cero. –

+1

Dado que la mejor respuesta parece ser que es una incoherencia en el idioma, voy a marcar esta respuesta como la respuesta. ¡Gracias! – stack

1

Probablemente porque esta es la forma estándar de tratar con la moneda. El ímpetu para la creación del decimal fue que el punto flotante hace un mal trabajo al lidiar con los valores de la moneda, por lo que esperaría que sus reglas estén más alineadas con los estándares de contabilidad que la corrección matemática.

+0

No estoy seguro de entender su respuesta en lo que se refiere a la pregunta. – stack

6

Si lee las especificaciones cuidadosamente, verá que no hay inconsistencia aquí.

He aquí que el párrafo de nuevo, con las partes importantes destacan:

El resultado de una operación en valores de tipo decimales es la que resultaría de calcular un resultado exacto (preservación de la escala, como se define para cada operador) y luego se redondea para ajustarse a la representación. Los resultados se redondean al valor representable más cercano y, cuando un resultado es igualmente cercano a dos valores representables, al valor que tiene un número par en la posición de dígito menos significativo (esto se conoce como "redondeo bancario"). Un resultado de cero siempre tiene un signo de 0 y una escala de 0.

Esta parte de la especificación se aplica a operaciones aritméticas en decimal; el formato de cadena no es uno de esos, e incluso si lo fuera, no importaría porque sus ejemplos son de baja precisión.

para demostrar el comportamiento se hace referencia en la especificación, utilice el siguiente código:

Decimal d1 = 0.00000000000000000000000000090m; 
Decimal d2 = 0.00000000000000000000000000110m; 

// Prints: 0.0000000000000000000000000004 (rounds down) 
Console.WriteLine(d1/2); 

// Prints: 0.0000000000000000000000000006 (rounds up) 
Console.WriteLine(d2/2); 

Esa es toda la especificación está hablando. Si el resultado de algún cálculo excede el límite de precisión del tipo decimal (29 dígitos), el redondeo bancario se utiliza para determinar cuál será el resultado.

+0

"Es fácil ver que quizás no hayan considerado ToString (cadena) en este párrafo, pero me inclino a pensar que cabe en esta descripción". – stack

Cuestiones relacionadas