Rundungsfehler in .NETs Double.ToString-Methode
Betrachten Sie für diese Frage mathematisch die rationale Zahl
8725724278030350 / 2**48
woher**
im Nenner bedeutet Exponentiation, d. h. der Nenner ist2
zum48
th macht. (Der Bruch ist nicht in niedrigsten Begriffen, er kann um 2 reduziert werden.) Diese Zahl istgenau darstellbar alsSystem.Double
. Ihre Dezimalerweiterung ist
31.0000000000000'49'73799150320701301097869873046875 (exact)
wobei die Apostrophe keine fehlenden Ziffern darstellen, sondern lediglich die Grenzen markieren, auf die gerundet wirdfünfzehn bzw.17 Stellen ist durchzuführen.
Beachten Sie Folgendes: Wenn diese Zahl auf 15 Stellen gerundet ist, ist das Ergebnis31
(gefolgt von dreizehn0
s) weil die nächsten Ziffern (49...
) beginnen mit a4
(bedeutet rundNieder). Aber wenn die Nummer istzuerst auf 17 Stellen gerundet unddann Auf 15 Stellen gerundet könnte das Ergebnis sein31.0000000000001
. Dies liegt daran, dass die erste Aufrundung durch Erhöhen des49...
Ziffern bis50 (terminates)
(Die nächsten Ziffern waren73...
), und die zweite Rundung wird möglicherweise erneut aufgerundet (wenn die Rundungsregel für den Mittelpunkt "von Null abrunden" lautet).
(Es gibt natürlich viel mehr Zahlen mit den oben genannten Merkmalen.)
Nun stellt sich heraus, dass .NET die Standard-Zeichenfolgendarstellung dieser Zahl ist"31.0000000000001"
. Die Frage: Ist das nicht ein Bug? Mit Standard-Stringdarstellung ist das gemeintString
erzeugt durch die parameterlesDouble.ToString()
Instanzmethode, die natürlich mit der identisch ist, die von erzeugt wirdToString("G")
.
Eine interessante Sache zu beachten ist, dass, wenn Sie die oben genannte Zahl zu werfenSystem.Decimal
dann bekommst du adecimal
das ist31
genau! SehenDiese Stapelüberlauf-Frage für eine Diskussion der überraschenden Tatsache, dass Casting aDouble
zuDecimal
Dabei wird zunächst auf 15 Stellen gerundet. Dies bedeutet, dass Casting zuDecimal
macht eine korrekte Runde auf 15 Stellen, während der AufrufToSting()
macht einen falschen.
Zusammenfassend haben wir eine Gleitkommazahl, die bei der Ausgabe an den Benutzer gleich ist31.0000000000001
, aber wenn konvertiert zuDecimal
(woher29 Ziffern stehen zur Verfügung), wird31
genau. Das ist unglücklich.
Hier ist ein C # -Code, mit dem Sie das Problem überprüfen können:
static void Main()
{
const double evil = 31.0000000000000497;
string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx
Console.WriteLine("Exact value (Jon Skeet): {0}", exactString); // writes 31.00000000000004973799150320701301097869873046875
Console.WriteLine("General format (G): {0}", evil); // writes 31.0000000000001
Console.WriteLine("Round-trip format (R): {0:R}", evil); // writes 31.00000000000005
Console.WriteLine();
Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));
Console.WriteLine();
decimal converted = (decimal)evil;
Console.WriteLine("Decimal version: {0}", converted); // writes 31
decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
Console.WriteLine("Better decimal: {0}", preciseDecimal); // writes 31.000000000000049737991503207
}
Der obige Code verwendet SkeetsToExactString
Methode. Wenn Sie sein Material nicht verwenden möchten (kann über die URL gefunden werden), löschen Sie einfach die obigen Codezeilen abhängig vonexactString
. Sie können immer noch sehen, wie dieDouble
fraglich (evil
) ist abgerundet und gegossen.
ZUSATZ:
OK, also habe ich noch ein paar Zahlen getestet und hier ist eine Tabelle:
exact value (truncated) "R" format "G" format decimal cast
------------------------- ------------------ ---------------- ------------
6.00000000000000'53'29... 6.0000000000000053 6.00000000000001 6
9.00000000000000'53'29... 9.0000000000000053 9.00000000000001 9
30.0000000000000'49'73... 30.00000000000005 30.0000000000001 30
50.0000000000000'49'73... 50.00000000000005 50.0000000000001 50
200.000000000000'51'15... 200.00000000000051 200.000000000001 200
500.000000000000'51'15... 500.00000000000051 500.000000000001 500
1020.00000000000'50'02... 1020.000000000005 1020.00000000001 1020
2000.00000000000'50'02... 2000.000000000005 2000.00000000001 2000
3000.00000000000'50'02... 3000.000000000005 3000.00000000001 3000
9000.00000000000'54'56... 9000.0000000000055 9000.00000000001 9000
20000.0000000000'50'93... 20000.000000000051 20000.0000000001 20000
50000.0000000000'50'93... 50000.000000000051 50000.0000000001 50000
500000.000000000'52'38... 500000.00000000052 500000.000000001 500000
1020000.00000000'50'05... 1020000.000000005 1020000.00000001 1020000
Die erste Spalte gibt den exakten (wenn auch abgeschnittenen) Wert an, den derDouble
vertreten. Die zweite Spalte enthält die Zeichenfolgendarstellung aus dem"R"
Zeichenfolge formatieren. Die dritte Spalte enthält die übliche Zeichenfolgendarstellung. Und schließlich gibt die vierte Spalte dieSystem.Decimal
das ergibt sich aus der Konvertierung diesesDouble
.
Wir schließen folgendes:
Runden Sie auf 15 Stellen vonToString()
und durch Umrechnung in auf 15 Stellen rundenDecimal
stimme in sehr vielen Fällen nicht zuUmstellung aufDecimal
runden auch in vielen Fällen falsch, und die Fehler in diesen Fällen können nicht als "Rundungsfehler" beschrieben werdenIn meinen FällenToString()
scheint eine größere Zahl als zu ergebenDecimal
Umwandlung, wenn sie nicht übereinstimmen (egal welche der beiden Runden richtig ist)Ich habe nur mit solchen Fällen experimentiert. Ich habe nicht überprüft, ob Rundungsfehler mit Zahlen anderer "Formulare" vorliegen.