Dezimalstellen im Doppel verschieben

97

Ich habe also einen doppelten Satz auf 1234, ich möchte eine Dezimalstelle verschieben, um 12,34 zu erhalten

Um dies zu tun, multipliziere ich .1 bis 1234 zweimal, irgendwie so

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.println(x);

Dadurch wird das Ergebnis "12.340000000000002" gedruckt.

Gibt es eine Möglichkeit, den Double Store 12.34 korrekt zu formatieren, ohne ihn einfach auf zwei Dezimalstellen zu formatieren?

BlackCow
quelle
30
Hier ist ein Link zum Originalartikel "Was jeder Informatiker über Gleitkomma-Arithmetik wissen sollte"
bsd
43
Gibt es einen Grund, warum du es nicht getan hast x /= 100;?
Mark Ingram

Antworten:

189

Wenn Sie doubleoder verwenden float, sollten Sie Rundungen verwenden oder erwarten, dass Rundungsfehler auftreten. Wenn Sie dies nicht tun können, verwenden Sie BigDecimal.

Das Problem, das Sie haben, ist, dass 0.1 keine exakte Darstellung ist. Wenn Sie die Berechnung zweimal durchführen, verschärfen Sie diesen Fehler.

100 können jedoch genau dargestellt werden. Versuchen Sie also:

double x = 1234;
x /= 100;
System.out.println(x);

welche druckt:

12.34

Dies funktioniert, weil Double.toString(d)in Ihrem Namen eine kleine Rundung durchgeführt wird, aber es ist nicht viel. Wenn Sie sich fragen, wie es ohne Rundung aussehen könnte:

System.out.println(new BigDecimal(0.1));
System.out.println(new BigDecimal(x));

Drucke:

0.100000000000000005551115123125782702118158340454101562
12.339999999999999857891452847979962825775146484375

Kurz gesagt, eine Rundung ist für sinnvolle Antworten im Gleitkomma unvermeidlich, unabhängig davon, ob Sie dies explizit tun oder nicht.


Hinweis: x / 100und x * 0.01sind nicht genau gleich, wenn es um Rundungsfehler geht. Dies liegt daran, dass der Rundungsfehler für den ersten Ausdruck von den Werten von x abhängt, während der 0.01im zweiten Ausdruck einen festen Rundungsfehler aufweist.

for(int i=0;i<200;i++) {
    double d1 = (double) i / 100;
    double d2 = i * 0.01;
    if (d1 != d2)
        System.out.println(d1 + " != "+d2);
}

druckt

0.35 != 0.35000000000000003
0.41 != 0.41000000000000003
0.47 != 0.47000000000000003
0.57 != 0.5700000000000001
0.69 != 0.6900000000000001
0.7 != 0.7000000000000001
0.82 != 0.8200000000000001
0.83 != 0.8300000000000001
0.94 != 0.9400000000000001
0.95 != 0.9500000000000001
1.13 != 1.1300000000000001
1.14 != 1.1400000000000001
1.15 != 1.1500000000000001
1.38 != 1.3800000000000001
1.39 != 1.3900000000000001
1.4 != 1.4000000000000001
1.63 != 1.6300000000000001
1.64 != 1.6400000000000001
1.65 != 1.6500000000000001
1.66 != 1.6600000000000001
1.88 != 1.8800000000000001
1.89 != 1.8900000000000001
1.9 != 1.9000000000000001
1.91 != 1.9100000000000001
Peter Lawrey
quelle
26
Ich kann nicht glauben, dass ich überhaupt nicht daran gedacht habe! Danke :-P
BlackCow
6
Obwohl 100 genau im Binärformat dargestellt werden kann, kann die Division durch 100 nicht genau dargestellt werden. Daher hat das Schreiben 1234/100, wie Sie es getan haben, nichts mit dem zugrunde liegenden Problem zu tun - es sollte genau dem Schreiben entsprechen 1234 * 0.01.
Brooks Moses
1
@ Peter Lawrey: Können Sie mehr erklären, warum die Zahl ungerade oder gerade die Rundung beeinflusst? Ich würde denken, dass / = 100 und * =. 01 dasselbe wären, denn obwohl 100 ein int ist, wird es aufgrund von Typenzwang sowieso in 100.0 konvertiert.
Eremzeit
1
/100und *0.01sind einander äquivalent, aber nicht den OPs *0.1*0.1.
Amadan
1
Ich sage nur, dass das zweimalige Multiplizieren mit 0,1 im Durchschnitt einen größeren Fehler verursacht als das einmalige Multiplizieren mit 0,01. Aber ich gebe gerne zu, dass @ JasperBekkers Punkt, dass 100 anders sind und genau binär darstellbar sind.
Amadan
52

Nein - Wenn Sie Dezimalwerte genau speichern möchten, verwenden Sie BigDecimal. kann eine Zahl wie 0,1 doubleeinfach nicht genau darstellen, genauso wenig wie Sie den Wert eines Drittels genau mit einer endlichen Anzahl von Dezimalstellen schreiben können.

Jon Skeet
quelle
46

Wenn es nur um die Formatierung geht, versuchen Sie es mit printf

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.printf("%.2f",x);

Ausgabe

12.34
Augusto
quelle
8
Die höher bewerteten Antworten sind technisch aufschlussreicher, aber dies ist die richtige Antwort auf das Problem von OP. Wir kümmern uns im Allgemeinen nicht um die leichte Ungenauigkeit von Double, daher ist BigDecimal übertrieben, aber bei der Anzeige möchten wir häufig sicherstellen, dass unsere Ausgabe unserer Intuition entspricht. Dies System.out.printf()ist der richtige Weg.
dimo414
28

In Finanzsoftware werden häufig ganze Zahlen für Pennies verwendet. In der Schule wurde uns beigebracht, wie man Festkomma anstelle von Schweben verwendet, aber das sind normalerweise Zweierpotenzen. Das Speichern von Pennys in ganzen Zahlen kann auch als "Fixpunkt" bezeichnet werden.

int i=1234;
printf("%d.%02d\r\n",i/100,i%100);

Im Unterricht wurden wir allgemein gefragt, welche Zahlen in einer Basis genau dargestellt werden können.

Für base=p1^n1*p2^n2... können Sie jedes N darstellen, wobei N = n * p1 ^ m1 * p2 ^ m2.

Lassen base=14=2^1*7^1Sie ... Sie können 1/7 1/14 1/28 1/49 aber nicht 1/3 darstellen

Ich kenne mich mit Finanzsoftware aus - ich habe die Finanzberichte von Ticketmaster von VAX asm in PASCAL konvertiert. Sie hatten ihr eigenes Format () mit Codes für Pennys. Der Grund für die Konvertierung war, dass 32-Bit-Ganzzahlen nicht mehr ausreichten. +/- 2 Milliarden Pennies sind 20 Millionen Dollar und das ist für die Weltmeisterschaft oder die Olympischen Spiele übergelaufen, habe ich vergessen.

Ich wurde zur Geheimhaltung verpflichtet. Naja. Wenn es in der Wissenschaft gut ist, veröffentlichen Sie; In der Industrie hält man es geheim.

Terrence Andrew Davis
quelle
12

Sie können die Darstellung von Ganzzahlen versuchen

int i =1234;
int q = i /100;
int r = i % 100;

System.out.printf("%d.%02d",q, r);
Engel Koh
quelle
5
@ Dan: Warum? Dies ist der richtige Ansatz für Finanz-Apps (oder andere Apps, bei denen selbst ein kleiner Rundungsfehler nicht akzeptabel ist), während die Geschwindigkeit auf Hardware-Ebene beibehalten wird. (Natürlich würde es in eine Klasse eingewickelt werden, normalerweise nicht jedes Mal ausgeschrieben)
Amadan
7
Bei dieser Lösung gibt es ein kleines Problem: Wenn der Rest rkleiner als 10 ist, tritt keine 0-Auffüllung auf und 1204 würde ein Ergebnis von 12,4 ergeben. Die korrekte Formatierungszeichenfolge ähnelt eher "% d.% 02d"
jakebman
10

Dies wird durch die Art und Weise verursacht, wie Computer Gleitkommazahlen speichern. Sie machen das nicht genau. Als Programmierer sollten Sie diesen Gleitkomma-Leitfaden lesen , um sich mit den Schwierigkeiten beim Umgang mit Gleitkommazahlen vertraut zu machen.

CanSpice
quelle
Argh, ich habe gerade eine Erklärung geschrieben, die genau auf den gleichen Ort verweist. +1.
Pops
@ Lord Haha, sorry. Ich wurde sowieso skeptisch. :-)
CanSpice
Ich dachte mir, warum, aber ich frage mich, ob es eine kreative Möglichkeit gibt, die Dezimalstelle zu verschieben. Da es möglich ist, 12.34 sauber im Doppel zu speichern, mag es einfach nicht, mit 0,1 zu multiplizieren
BlackCow
1
Wenn es möglich wäre, 12.34 sauber im Doppel zu speichern, hätte Java das nicht getan? Es ist nicht. Sie müssen einen anderen Datentyp verwenden (z. B. BigDecimal). Warum dividierst du nicht einfach durch 100, anstatt es in einer Schleife zu tun?
CanSpice
Do'h ... ja, das Teilen durch 100 ergibt eine saubere 12.34 ... danke :-P
BlackCow
9

Witzig, dass in zahlreichen Posts die Verwendung von BigDecimal erwähnt wird, aber niemand stört, um die richtige Antwort basierend auf BigDecimal zu geben? Denn auch mit BigDecimal können Sie Fehler machen, wie dieser Code zeigt

String numstr = "1234";
System.out.println(new BigDecimal(numstr).movePointLeft(2));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal(0.01)));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal("0.01")));

Gibt diese Ausgabe

12.34
12.34000000000000025687785232264559454051777720451354980468750
12.34

Der BigDecimal-Konstruktor erwähnt ausdrücklich, dass es besser ist, den String-Konstruktor als einen numerischen Konstruktor zu verwenden. Die ultimative Präzision wird auch durch den optionalen MathContext beeinflusst.

Laut BigDecimal Javadoc ist es möglich , ein BigDecimal zu erstellen, das genau gleich 0,1 ist, vorausgesetzt, Sie verwenden den String-Konstruktor.

Justin Rowe
quelle
5

Ja da ist. Mit jeder Doppeloperation können Sie an Genauigkeit verlieren, aber die Genauigkeit ist für jede Operation unterschiedlich und kann durch Auswahl der richtigen Abfolge von Operationen minimiert werden. Wenn Sie beispielsweise einen Satz von Zahlen multiplizieren, ist es am besten, den Satz vor dem Multiplizieren nach Exponenten zu sortieren.

Jedes anständige Buch über das Knacken von Zahlen beschreibt dies. Zum Beispiel: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Und um Ihre Frage zu beantworten:

Verwenden Sie dividieren statt multiplizieren. Auf diese Weise erhalten Sie das richtige Ergebnis.

double x = 1234;
for(int i=1;i<=2;i++)
{
  x =  x / 10.0;
}
System.out.println(x);
Jan Kotek
quelle
3

Nein, da Java-Gleitkommatypen (in der Tat alle Gleitkommatypen) einen Kompromiss zwischen Größe und Genauigkeit darstellen. Während sie für viele Aufgaben sehr nützlich sind, sollten Sie sie verwenden, wenn Sie willkürliche Präzision benötigen BigDecimal.

biziclop
quelle