Formateo de dobles para salida en C #

Ejecutando un experimento rápido relacionado con ¿La doble multiplicación está rota en .NET? y leyendo un par de artículos sobre el formato de cadena C #, pensé que esto:

{ double i = 10 * 0.69; Console.WriteLine(i); Console.WriteLine(String.Format(" {0:F20}", i)); Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i)); Console.WriteLine(String.Format("= {0:F20}", 6.9)); } 

Sería el equivalente de C # de este código C:

 { double i = 10 * 0.69; printf ( "%f\n", i ); printf ( " %.20f\n", i ); printf ( "+ %.20f\n", 6.9 - i ); printf ( "= %.20f\n", 6.9 ); } 

Sin embargo, el C # produce la salida:

 6.9 6.90000000000000000000 + 0.00000000000000088818 = 6.90000000000000000000 

a pesar de que aparezco igual al valor 6.89999999999999946709 (en lugar de 6.9) en el depurador.

en comparación con C, que muestra la precisión solicitada por el formato:

 6.900000 6.89999999999999946709 + 0.00000000000000088818 = 6.90000000000000035527 

¿Que esta pasando?

(Microsoft .NET Framework versión 3.51 SP1 / Visual Studio C # 2008 Express Edition)


Tengo experiencia en informática numérica y experiencia en la implementación de aritmética de intervalos (una técnica para estimar errores debido a los límites de precisión en sistemas numéricos complicados) en varias plataformas. Para obtener la recompensa, no intente explicar la precisión del almacenamiento, en este caso es una diferencia de un ULP de un doble de 64 bits.

Para obtener la recompensa, quiero saber cómo (o si) .Net puede formatear un doble a la precisión solicitada como visible en el código C.

El problema es que .NET siempre redondeará un double a 15 dígitos decimales significativos antes de aplicar el formato, independientemente de la precisión solicitada por su formato y sin importar el valor decimal exacto del número binario.

Supongo que el depurador de Visual Studio tiene sus propias rutinas de formato / visualización que acceden directamente al número binario interno, de ahí las discrepancias entre su código C #, su código C y el depurador.

No hay nada incorporado que le permita acceder al valor decimal exacto de un double , o para permitirle formatear un double a un número específico de decimales, pero puede hacerlo usted mismo seleccionando el número binario interno y reconstruyendo como una representación de cadena del valor decimal.

Alternativamente, podría usar la clase DoubleConverter Jon Skeet (vinculada desde su artículo “Binary floating point and .NET” ). Esto tiene un método ToExactString que devuelve el valor decimal exacto de un double . Puede modificar esto fácilmente para permitir el redondeo de la salida a una precisión específica.

 double i = 10 * 0.69; Console.WriteLine(DoubleConverter.ToExactString(i)); Console.WriteLine(DoubleConverter.ToExactString(6.9 - i)); Console.WriteLine(DoubleConverter.ToExactString(6.9)); // 6.89999999999999946709294817992486059665679931640625 // 0.00000000000000088817841970012523233890533447265625 // 6.9000000000000003552713678800500929355621337890625 
 Digits after decimal point // just two decimal places String.Format("{0:0.00}", 123.4567); // "123.46" String.Format("{0:0.00}", 123.4); // "123.40" String.Format("{0:0.00}", 123.0); // "123.00" // max. two decimal places String.Format("{0:0.##}", 123.4567); // "123.46" String.Format("{0:0.##}", 123.4); // "123.4" String.Format("{0:0.##}", 123.0); // "123" // at least two digits before decimal point String.Format("{0:00.0}", 123.4567); // "123.5" String.Format("{0:00.0}", 23.4567); // "23.5" String.Format("{0:00.0}", 3.4567); // "03.5" String.Format("{0:00.0}", -3.4567); // "-03.5" Thousands separator String.Format("{0:0,0.0}", 12345.67); // "12,345.7" String.Format("{0:0,0}", 12345.67); // "12,346" Zero Following code shows how can be formatted a zero (of double type). String.Format("{0:0.0}", 0.0); // "0.0" String.Format("{0:0.#}", 0.0); // "0" String.Format("{0:#.0}", 0.0); // ".0" String.Format("{0:#.#}", 0.0); // "" Align numbers with spaces String.Format("{0,10:0.0}", 123.4567); // " 123.5" String.Format("{0,-10:0.0}", 123.4567); // "123.5 " String.Format("{0,10:0.0}", -123.4567); // " -123.5" String.Format("{0,-10:0.0}", -123.4567); // "-123.5 " Custom formatting for negative numbers and zero String.Format("{0:0.00;minus 0.00;zero}", 123.4567); // "123.46" String.Format("{0:0.00;minus 0.00;zero}", -123.4567); // "minus 123.46" String.Format("{0:0.00;minus 0.00;zero}", 0.0); // "zero" Some funny examples String.Format("{0:my number is 0.0}", 12.3); // "my number is 12.3" String.Format("{0:0aaa.bbb0}", 12.3); 

Eche un vistazo a esta referencia de MSDN . En las notas, indica que los números se redondean al número de decimales solicitados.

Si, en su lugar, utiliza “{0: R}”, producirá lo que se conoce como un valor de “ida y vuelta”, eche un vistazo a esta referencia de MSDN para obtener más información, aquí está mi código y el resultado:

 double d = 10 * 0.69; Console.WriteLine(" {0:R}", d); Console.WriteLine("+ {0:F20}", 6.9 - d); Console.WriteLine("= {0:F20}", 6.9); 

salida

  6.8999999999999995 + 0.00000000000000088818 = 6.90000000000000000000 

Aunque esta pregunta se ha cerrado mientras tanto, creo que vale la pena mencionar cómo esta atrocidad comenzó a existir. En cierto modo, puede culpar a la especificación de C #, que establece que un doble debe tener una precisión de 15 o 16 dígitos (el resultado de IEEE-754). Un poco más adelante (sección 4.1.6) se afirma que las implementaciones pueden usar una mayor precisión. Eso sí, más alto , no más bajo. Incluso se les permite desviarse de IEEE-754: expresiones del tipo x * y / z donde x * y produciría +/-INF pero estarían en un rango válido después de la división, no es necesario que se produzca un error. Esta característica hace que sea más fácil para los comstackdores usar una mayor precisión en las architectures, lo que produciría un mejor rendimiento.

Pero prometí una “razón”. Aquí hay una cita (solicitó un recurso en uno de sus comentarios recientes) de la CLI de fuente compartida , en clr/src/vm/comnumber.cpp :

“Con el fin de dar números que sean fáciles de mostrar y de disparar por todos los medios, analizamos el número usando 15 dígitos y luego determinamos si gira alrededor del mismo valor. Si lo hace, convertimos ese NÚMERO en una cadena, de lo contrario, volver a analizar usando 17 dígitos y mostrar eso “.

En otras palabras: el equipo de desarrollo de CLI de MS decidió ser tanto de ida y vuelta como de paso y mostrar bonitos valores que no son tan dolorosos de leer. ¿Bueno o malo? Me gustaría un opt-in u opt-out.

¿El truco para descubrir esta triplicabilidad de un número dado? Conversión a una estructura NUMBER genérica (que tiene campos separados para las propiedades de un doble) y viceversa, y luego compare si el resultado es diferente. Si es diferente, se usa el valor exacto (como en su valor medio con 6.9 - i ) si es el mismo, se usa el “valor bonito”.

Como ya comentó en un comentario a Andyp, 6.90...00 es a nivel de bit igual a 6.89...9467 . Y ahora sabes por qué se usa 0.0...8818 : es diferente de 0.0 .

Esta barrera de 15 dígitos está codificada y solo se puede cambiar al volver a comstackr la CLI, al usar Mono o al llamar a Microsoft y convencerlos de que agreguen una opción para imprimir una “precisión” completa (no es realmente precisión, sino la falta de una mejor palabra). Probablemente sea más fácil calcular usted mismo la precisión de los 52 bits o usar la biblioteca mencionada anteriormente.

EDITAR: si te gusta experimentar con los puntos flotantes IEE-754, considera esta herramienta en línea , que muestra todas las partes relevantes de un punto flotante.

Utilizar

 Console.WriteLine(String.Format(" {0:G17}", i)); 

Eso le dará los 17 dígitos que tiene. Por defecto, un valor doble contiene 15 dígitos decimales de precisión, aunque un máximo de 17 dígitos se mantiene internamente. {0: R} no siempre le dará 17 dígitos, dará 15 si el número puede representarse con esa precisión.

que devuelve 15 dígitos si el número puede representarse con esa precisión o 17 dígitos si el número solo puede representarse con la máxima precisión. No hay nada que pueda hacer para que el doble devuelva más dígitos, que es la forma en que se implementa. Si no te gusta, haz una nueva clase doble tú mismo …

.NET no puede almacenar más dígitos que 17 por lo que no puede ver 6.89999999999999946709 en el depurador que verá 6.8999999999999995. Proporcione una imagen para demostrar que estamos equivocados.

La respuesta a esto es simple y se puede encontrar en MSDN

Recuerde que un número de coma flotante solo puede aproximarse a un número decimal, y que la precisión de un número de coma flotante determina con qué precisión ese número se aproxima a un número decimal. Por defecto, un valor doble contiene 15 dígitos decimales de precisión , aunque un máximo de 17 dígitos se mantiene internamente.

En su ejemplo, el valor de i es 6.89999999999999946709 que tiene el número 9 para todas las posiciones entre el 3er y el 16º dígito (recuerde contar la parte entera en los dígitos). Al convertir a cadena, el marco redondea el número al décimo quinto dígito.

 i = 6.89999999999999 946709 digit = 111111 111122 1 23456789012345 678901 

Traté de reproducir sus hallazgos, pero cuando vi ‘i’ en el depurador apareció como ‘6.8999999999999995’ no como ‘6.89999999999999946709’ como escribió en la pregunta. ¿Puedes dar los pasos para reproducir lo que viste?

Para ver qué muestra el depurador, puede usar un DoubleConverter como en la siguiente línea de código:

 Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string))); 

¡Espero que esto ayude!

Editar: Supongo que estoy más cansado de lo que pensaba, por supuesto, esto es lo mismo que formatear el valor de ida y vuelta (como se mencionó anteriormente).

La respuesta es sí, la doble impresión está rota en .NET, están imprimiendo dígitos de basura al final.

Puede leer cómo implementarlo correctamente aquí .

He tenido que hacer lo mismo con IronScheme.

 > (* 10.0 0.69) 6.8999999999999995 > 6.89999999999999946709 6.8999999999999995 > (- 6.9 (* 10.0 0.69)) 8.881784197001252e-16 > 6.9 6.9 > (- 6.9 8.881784197001252e-16) 6.8999999999999995 

Nota: Tanto C como C # tienen el valor correcto, solo impresión rota.

Actualización: todavía estoy buscando la conversación de la lista de correo que tuve que conducir a este descubrimiento.

Encontré esta solución rápida.

  double i = 10 * 0.69; System.Diagnostics.Debug.WriteLine(i); String s = String.Format("{0:F20}", i).Substring(0,20); System.Diagnostics.Debug.WriteLine(s + " " +s.Length ); 

Console.WriteLine (string.Format (“Tarifas del curso es {0: 0.00}”, + cfees));

 internal void DisplaycourseDetails() { Console.WriteLine("Course Code : " + cid); Console.WriteLine("Couse Name : " + cname); //Console.WriteLine("Couse Name : " + string.Format("{0:0.00}"+ cfees)); // string s = string.Format("Course Fees is {0:0.00}", +cfees); // Console.WriteLine(s); Console.WriteLine( string.Format("Course Fees is {0:0.00}", +cfees)); } static void Main(string[] args) { Course obj1 = new Course(101, "C# .net", 1100.00); obj1.DisplaycourseDetails(); Course obj2 = new Course(102, "Angular", 7000.00); obj2.DisplaycourseDetails(); Course obj3 = new Course(103, "MVC", 1100.00); obj3.DisplaycourseDetails(); Console.ReadLine(); } }