2009-09-14 8 views
57

Exécution d'une expérience rapide liée à Is double Multiplication Broken in .NET? et lire quelques articles sur la mise en forme de chaîne C#, je pensais que ceci:Mise en forme double pour la sortie en C#

{ 
    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)); 
} 

serait le C# équivalent de ce code 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); 
} 

Cependant, le C# produit la sortie:

6.9 
    6.90000000000000000000 
+ 0.00000000000000088818 
= 6.90000000000000000000 

malgré que je montre égal à la valeur 6.89999999999999946709 (plutôt que 6.9) dans le débogueur.

C par rapport à ce qui montre la précision demandée par le format:

6.900000       
    6.89999999999999946709   
+ 0.00000000000000088818   
= 6.90000000000000035527   

Que se passe-t-il?

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


J'ai une formation dans le calcul numérique et expérience dans l'implémentation arithmétique d'intervalle - une technique d'estimation des erreurs en raison des limites de précision dans les systèmes numériques complexes - sur diverses plates-formes. Pour obtenir la prime, n'essayez pas d'expliquer la précision de stockage - dans ce cas, c'est une différence d'un ULP d'un double de 64 bits.

Pour obtenir la prime, je veux savoir comment (ou si) .Net peut formater un double à la précision demandée comme visible dans le code C.

+1

Attend pour moi comme C# est montrant la même précision, juste qu'il traite avec des chiffres beaucoup plus précis que ne C++ ... –

+16

Il est également tout à fait possible que le compilateur C# regarde 10 * 0.69 et se dit "oooh, je sais celui-ci, c'est 6.9!" et sustitutes que dans inplace de faire le calcul à l'exécution –

+20

FWIW - en utilisant 'i' pour une variable double me donne la dissonance cognitive – plinth

Répondre

55

Le problème est que .NET sera toujours autour d'un double à 15 chiffres décimaux significatifs avant d'appliquer votre mise en forme, quelle que soit la précision demandée par votre format et quelle que soit la valeur décimale exacte du nombre binaire.

Je suppose que le débogueur Visual Studio a ses propres routines de format/affichage qui accèdent directement au nombre binaire interne, d'où les différences entre votre code C#, votre code C et le débogueur.

Il n'y a rien intégré qui vous permettra d'accéder à la valeur décimale exacte d'un double, ou de vous permettre de formater un double à un nombre spécifique de décimales, mais vous pouvez le faire vous-même en sélectionnant l'interne nombre binaire et le reconstruire comme une représentation sous forme de chaîne de la valeur décimale. Vous pouvez aussi utiliser le DoubleConverter class de Jon Skeet (lié à "Binary floating point and .NET" article). Cela a une méthode ToExactString qui renvoie la valeur décimale exacte d'un double. Vous pouvez facilement modifier ceci pour activer l'arrondi de la sortie à une précision spécifique.

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 
+0

La sortie correcte pour le cas 1 est 6.8999999999999995, à la fois .NET et Jon Skeet se trompent. Les chiffres de fin sont des chiffres d'ordures. – leppie

+12

Non, ce ne sont pas des chiffres poubelles, ce sont les représentations décimales de l'approximation la plus proche de la représentation binaire d'un nombre décimal. Ce n'est pas arbitraire, juste mathématique pure basée sur la spécification IEEE-754 et la fraction de 52 bits. Le numéro que vous mentionnez est un nombre arrondi. Par exemple, le nombre du milieu est égal à '1/2^50', autrement dit, c'est un bit de moins. – Abel

+4

Ils sont des ordures dans le sens où ils peuvent être jetés sans rien perdre de la valeur - une valeur double représente un intervalle sur la ligne de nombres réels, pas une valeur exacte, donc 6.8999999999999995 est autant une valeur correcte que la 'exacte 'version, car les deux tombent dans l'intervalle. Trois comportements différents sont corrects: la valeur décimale avec le plus petit nombre de chiffres compris dans l'intervalle (la valeur de l'aller-retour ou la valeur du schéma de Leppie), le centre de l'intervalle (la valeur 'exacte' ci-dessus) ou une plage d'un demi-ULP de chaque côté de la valeur exacte. –

8

Jetez un oeil à ce MSDN reference. Dans les notes, il est indiqué que les nombres sont arrondis au nombre de décimales demandé.

Si au lieu que vous utilisez « {0: R} » il produira c'est appelé valeur « aller-retour », jetez un oeil à ce MSDN reference pour plus d'informations, voici mon code et la sortie:

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

sortie

6.8999999999999995 
+ 0.00000000000000088818 
= 6.90000000000000000000 
+0

Je ne veux pas la valeur aller-retour, je veux savoir comment arrondir la valeur au nombre de décimales que je demande, ce qui dans ce cas serait 6.89999999999999946709. –

+0

Si je me souviens bien, le double type de données est précis à 16 chiffres, donc le nombre dans mon échantillon est le plus précis que vous obtiendrez, plus de chiffres que cela nécessiterait un type de données plus grand. –

+0

Voici une citation de la documentation MSDN: Par défaut, une valeur double contient 15 chiffres décimaux de précision, bien qu'un maximum de 17 chiffres est maintenu en interne –

4

utilisation

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

Cela vous donnera tous les 17 chiffres qu'il a. Par défaut, une valeur Double contient 15 chiffres décimaux de précision, bien qu'un maximum de 17 chiffres soit maintenu en interne. {0: R} ne vous donnera pas toujours 17 chiffres, il donnera 15 si le nombre peut être représenté avec cette précision.

qui renvoie 15 chiffres si le nombre peut être représenté avec cette précision ou 17 chiffres si le nombre ne peut être représenté avec une précision maximale. Il n'y a rien que vous puissiez faire pour que le double retourne plus de chiffres, comme c'est le cas. Si vous ne l'aimez pas faire une nouvelle classe double vous-même ...

.NET double peine de stocker plus de chiffres que 17 de sorte que vous ne pouvez pas voir 6.89999999999999946709 dans le débogueur que vous verriez 6.8999999999999995. S'il vous plaît fournir une image pour nous prouver le contraire.

+0

double ne stocke pas 17 chiffres décimaux - il stocke 52 + 1 chiffres binaires. La représentation exacte de 1/2^52 est de 52 chiffres décimaux, donc il devrait être capable de produire 52 chiffres décimaux avant d'avoir des zéros à la fin. J'essaie de contourner la troncature en une valeur décimale. La valeur dans le débogueur est comme je décris - la même chose que la sortie dans C. –

+0

ce que je voulais dire était "17 chiffres de précision" – ceciliaSHARP

2

La réponse est simple et se trouve sur MSDN

Rappelez-vous qu'un nombre à virgule flottante ne peut qu'approcher un nombre décimal, et que la précision d'un nombre à virgule flottante détermine la précision que nombre se rapproche d'un nombre décimal. Par défaut, une valeur Double contient 15 chiffres décimaux de précision, bien qu'un maximum de 17 chiffres soit maintenu en interne.

Dans votre exemple, la valeur de i est 6,89999999999999946709 qui a le numéro 9 pour toutes les positions entre le 3 et le 16 chiffres (souvenez-vous de compter la partie entière dans les chiffres). Lors de la conversion en chaîne, le cadre arrondit le nombre au 15ème chiffre.

i  = 6.89999999999999 946709 
digit =   111111 111122 
     1 23456789
+0

Tout cela est vrai, mais passe en revue comment fonctionne le double IEE dans les systèmes x87 et SSE qui sont Cela est pertinent pour le genre de travail que je fais - en particulier si vous pouvez implémenter l'arithmétique d'intervalle sur .net, où la visibilité de l'ULP est importante. La question est de savoir comment formater la sortie pour obtenir la valeur interne au nombre d'emplacements spécifié par l'utilisateur, et non la valeur arrondie qui le tronque. –

2

j'ai essayé de reproduire vos résultats, mais quand je regardais « i » dans le débogueur, il est apparu que comme vous avez écrit « 6,8999999999999995 » pas « 6,89999999999999946709 » dans la question. Pouvez-vous fournir des étapes pour reproduire ce que vous avez vu?

Pour voir ce que le débogueur vous montre, vous pouvez utiliser un DoubleConverter comme dans la ligne de code suivante:

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

Hope this helps! Edit: Je suppose que je suis plus fatigué que je pensais, bien sûr, c'est la même chose que le formatage à la valeur de l'aller-retour (comme mentionné précédemment).

+0

La valeur apparaît dans le débogueur comme égale à la valeur rendue par le formatage C comme "6.89999999999999946709", plutôt que "6.90..0" qui est sortie par le formatage de chaîne de .Net. 6.8999999999999995 est bit à égale à 6.89999999999999946709, donc différencier entre les deux comme une valeur n'est pas utile. Le problème est que les formats .Net 6.89999999999999946709 comme "6.90 ...". –

+0

Après un peu de sommeil, je comprends maintenant que ce ne sont que deux représentations du même nombre (avec un nombre différent de chiffres décimaux) et que votre question ne concerne que le formatage. J'ai cherché MSDN pendant un certain temps, mais je n'ai pas trouvé de moyen de supprimer l'arrondi lors du formatage, ce qui m'a semblé être ce que vous voulez faire. – andyp

0

La réponse est oui, l'impression double est cassé dans .NET, ils sont l'impression de fuite chiffres des ordures.

Vous pouvez lire comment l'implémenter correctement here.

J'ai dû faire la même chose pour 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 

Remarque: Les valeurs C et C# ont toutes les deux la valeur correcte, juste une impression interrompue.Mise à jour: Je suis toujours à la recherche de la conversation de liste de diffusion que j'ai eu qui a conduit à cette découverte.

7

Bien que cette question soit entre-temps close, je crois qu'il vaut la peine de mentionner comment cette atrocité a vu le jour. D'une certaine manière, vous pouvez blâmer la spécification C#, qui stipule qu'un double doit avoir une précision de 15 ou 16 chiffres (le résultat de IEEE-754). Un peu plus loin (section 4.1.6), il est indiqué que les implémentations sont autorisées à utiliser la précision supérieure. Rappelez-vous: plus élevé, pas plus bas. Ils sont même autorisés à dévier de IEEE-754: expressions du type x * y/zx * y donnerait +/-INF mais serait dans une plage valide après la division, ne doit pas entraîner une erreur. Cette fonctionnalité facilite l'utilisation par les compilateurs d'une plus grande précision dans les architectures où les performances sont meilleures. Mais j'ai promis une "raison". Voici une citation (vous avez demandé une ressource dans un de vos commentaires récents) de la Shared Source CLI, dans clr/src/vm/comnumber.cpp:

« Afin de donner des chiffres qui sont à la fois amicale à afficher et rond-déclenchable, nous Parse le numéro en utilisant 15 chiffres, puis déterminer si il arrondit voyages à la même valeur.Si il le fait, nous convertissons ce nombre en une chaîne , sinon nous réorganiser en utilisant 17 chiffres et l'afficher. "

En d'autres termes: l'équipe de développement CLI de MS a décidé d'être ronds-déclenchable et montrer les valeurs jolies qui ne sont pas une telle douleur à lire. Bon ou Mauvais? Je souhaiterais un opt-in ou un opt-out.

L'astuce est-elle de trouver cette capacité de triabilité ronde d'un nombre donné? Conversion vers une structure générique NUMBER (qui a des champs séparés pour les propriétés d'un double) et retour, puis comparez si le résultat est différent. Si elle est différente, la valeur exacte est utilisée (comme dans votre valeur moyenne avec 6.9 - i) si elle est la même, la "jolie valeur" est utilisée.

Comme vous l'avez déjà remarqué dans un commentaire sur Andyp, 6.90...00 est bit à bit égal à 6.89...9467. Et maintenant vous savez pourquoi 0.0...8818 est utilisé: il est bit à bit différent de 0.0.

Cette barrière 15 chiffres est codé en dur et ne peut être modifié en recompilant la CLI, en utilisant Mono ou en appelant Microsoft et les convaincre d'ajouter une option d'impression complète « précision » (il est pas vraiment précision , mais par l'absence d'un meilleur mot). Il est probablement plus facile de calculer vous-même la précision de 52 bits ou d'utiliser la bibliothèque mentionnée plus haut.

EDIT: si vous aimez expérimenter avec des points flottants IEE-754, consider this online tool, qui vous montre toutes les parties pertinentes d'un point flottant.

18
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); 
+1

Je peux trouver http://www.csharp-examples.net/string-format-double/ par moi-même; la question portait sur le comportement lorsque la valeur avec le nombre de chiffres demandés n'était pas en accord avec la précision de la variable. –

0

J'ai trouvé cette solution rapide.

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); 
-3

Console.WriteLine (string.Format («Frais de cours est {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(); 
     } 
    } 
+1

Cela n'a rien à voir avec la question que j'ai posée. Veuillez le supprimer Merci. –

+0

Salut Pete, au-dessus de la ligne Console.WriteLine (string.Format ("Course Fees is {0: 0.00}", donnez le résultat désiré.) Vous n'utilisez pas le code ci-dessus, alors votre sortie aimerait --Course fees 7000. Mais je veux les frais de production comme 7000.00. –