Przenoszenie miejsc dziesiętnych w podwójnym

97

Więc mam podwójny zestaw równy 1234, chcę przesunąć miejsce dziesiętne, aby uzyskać 12,34

Aby to zrobić, mnożę 0,1 do 1234 dwa razy, trochę w ten sposób

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

Spowoduje to wydrukowanie wyniku „12.340000000000002”

Czy istnieje sposób, bez prostego formatowania go do dwóch miejsc po przecinku, aby poprawnie zapisać podwójny zapis 12,34?

BlackCow
źródło
43
Czy jest powód, dla którego tego nie zrobiłeś x /= 100;?
Mark Ingram

Odpowiedzi:

189

Jeśli używasz doublelub float, powinieneś użyć zaokrąglenia lub spodziewać się błędów zaokrągleń. Jeśli nie możesz tego zrobić, użyj BigDecimal.

Problem polega na tym, że 0.1 nie jest dokładną reprezentacją i wykonując dwa obliczenia, potęgujesz ten błąd.

Jednak 100 można dokładnie przedstawić, więc spróbuj:

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

który drukuje:

12.34

To działa, ponieważ Double.toString(d)wykonuje niewielkie zaokrąglenia w Twoim imieniu, ale nie jest to dużo. Jeśli zastanawiasz się, jak mogłoby to wyglądać bez zaokrąglania:

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

wydruki:

0.100000000000000005551115123125782702118158340454101562
12.339999999999999857891452847979962825775146484375

Krótko mówiąc, zaokrąglanie jest nieuniknione w przypadku rozsądnych odpowiedzi zmiennoprzecinkowych, niezależnie od tego, czy robisz to jawnie, czy nie.


Uwaga: x / 100i x * 0.01nie są dokładnie takie same, jeśli chodzi o błąd zaokrąglania. Dzieje się tak, ponieważ błąd zaokrąglenia dla pierwszego wyrażenia zależy od wartości x, podczas 0.01gdy drugi ma stały błąd zaokrąglenia.

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

wydruki

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
źródło
26
Nie mogę uwierzyć, że nie pomyślałem o zrobieniu tego w pierwszej kolejności! Dzięki :-P
BlackCow
6
Chociaż 100 można dokładnie przedstawić w formacie binarnym, dzielenia przez 100 nie można dokładnie przedstawić. Zatem pisanie 1234/100, tak jak to zrobiłeś, tak naprawdę nie wpływa na podstawowy problem - powinno być dokładnie równe pisaniu 1234 * 0.01.
Brooks Moses,
1
@Peter Lawrey: Czy możesz wyjaśnić więcej, dlaczego liczba nieparzysta czy parzysta wpływa na zaokrąglanie? Myślę, że / = 100 i * =. 01 byłyby takie same, ponieważ nawet jeśli 100 jest liczbą int, i tak zostanie przekonwertowana na 100,0 w wyniku wymuszenia typu.
eremzeit
1
/100i *0.01są sobie równe, ale nie OP *0.1*0.1.
Amadan
1
Mówię tylko, że dwukrotne pomnożenie przez 0,1 spowoduje średnio większy błąd niż jednokrotne pomnożenie przez 0,01; ale szczęśliwie zgodzę się z tezą @ JasperBekkersa, że ​​100 jest inny, dokładnie reprezentowalny binarnie.
Amadan
52

Nie - jeśli chcesz dokładnie przechowywać wartości dziesiętne, użyj BigDecimal. doublepo prostu nie może dokładnie reprezentować liczby takiej jak 0,1, tak samo jak nie można zapisać dokładnie jednej trzeciej przy użyciu skończonej liczby cyfr dziesiętnych.

Jon Skeet
źródło
46

jeśli to tylko formatowanie, spróbuj printf

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

wynik

12.34
Augusto
źródło
8
Wyższe oceny są bardziej wnikliwe technicznie, ale jest to poprawna odpowiedź na problem OP. Generalnie nie przejmujemy się drobną niedokładnością double, więc BigDecimal to przesada, ale podczas wyświetlania często chcemy mieć pewność, że nasze wyjście pasuje do naszej intuicji, więc System.out.printf()jest to właściwa droga.
dimo414
28

W oprogramowaniu finansowym często używa się liczb całkowitych zamiast groszy. W szkole uczono nas, jak używać stałego punktu zamiast pływającego, ale zwykle są to potęgi dwóch. Przechowywanie groszy w liczbach całkowitych można również nazwać „punktem stałym”.

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

Na zajęciach zapytano nas ogólnie, jakie liczby można dokładnie przedstawić w bazie.

Dla base=p1^n1*p2^n2... możesz przedstawić dowolne N, gdzie N = n * p1 ^ m1 * p2 ^ m2.

Niech base=14=2^1*7^1... możesz reprezentować 1/7 1/14 1/28 1/49, ale nie 1/3

Wiem o oprogramowaniu finansowym - przekonwertowałem raporty finansowe Ticketmaster z VAX asm na PASCAL. Mieli swój własny formatln () z kodami na grosze. Przyczyną konwersji było to, że 32-bitowe liczby całkowite już nie wystarczały. +/- 2 miliardy groszy to 20 milionów dolarów, a to przelane na Mistrzostwa Świata lub Igrzyska Olimpijskie, zapomniałem.

Przysięgałem zachować tajemnicę. No cóż. W środowisku akademickim, jeśli dobrze, to publikujesz; w przemyśle zachowujesz to w tajemnicy.

Terrence Andrew Davis
źródło
12

możesz spróbować reprezentacji liczb całkowitych

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

System.out.printf("%d.%02d",q, r);
Angel Koh
źródło
5
@Dan: Dlaczego? Jest to właściwe podejście do aplikacji finansowych (lub wszelkich innych aplikacji, w których nawet niewielki błąd zaokrąglenia jest niedopuszczalny), przy jednoczesnym zachowaniu szybkości na poziomie sprzętowym. (Oczywiście byłoby to zapakowane w klasę, normalnie, nie za każdym razem)
Amadan
7
Z tym rozwiązaniem jest mały problem - jeśli reszta rjest mniejsza niż 10, nie występuje wypełnienie 0 i 1204 dałoby wynik 12,4. Prawidłowy ciąg formatujący jest bardziej podobny do „% d.% 02d”
jakebman
10

Jest to spowodowane sposobem, w jaki komputery przechowują liczby zmiennoprzecinkowe. Nie robią tego dokładnie. Jako programista powinieneś przeczytać ten przewodnik zmiennoprzecinkowy, aby zapoznać się z próbami i problemami związanymi z obsługą liczb zmiennoprzecinkowych.

CanSpice
źródło
Argh, właśnie pisałem wyjaśnienie odnoszące się do tego samego miejsca. +1.
Wyskakuje
@Lord Haha, przepraszam. I tak mam Skeeted. :-)
CanSpice
Pomyślałem, że to dlatego, ale zastanawiam się, czy istnieje jakiś kreatywny sposób na przeniesienie miejsca dziesiętnego? Ponieważ możliwe jest czyste przechowywanie 12,34 w double, po prostu nie lubi mnożenia przez 0,1
BlackCow,
1
Gdyby możliwe było czyste przechowywanie 12.34 w double, nie sądzisz, że Java by to zrobiła? To nie jest. Będziesz musiał użyć innego typu danych (np. BigDecimal). Dlaczego po prostu nie podzielisz przez 100 zamiast robić to w pętli?
CanSpice,
Do'h ... tak, podzielenie przez 100 daje czysty 12.34 ... dzięki :-P
BlackCow
9

Zabawne, że liczne posty wspominają o używaniu BigDecimal, ale nikt nie zadaje sobie trudu, aby podać poprawną odpowiedź na podstawie BigDecimal? Ponieważ nawet w przypadku BigDecimal nadal możesz popełnić błąd, jak pokazano w tym kodzie

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

Daje ten wynik

12.34
12.34000000000000025687785232264559454051777720451354980468750
12.34

Konstruktor BigDecimal wyraźnie wspomina, że ​​lepiej jest użyć konstruktora String niż konstruktora liczbowego. Na najwyższą precyzję ma również wpływ opcjonalny MathContext.

Zgodnie z BigDecimal Javadoc możliwe jest utworzenie BigDecimal, który jest dokładnie równy 0,1, pod warunkiem, że użyjesz konstruktora String.

Justin Rowe
źródło
5

Tak jest. Każda podwójna operacja może spowodować utratę dokładności, ale poziom dokładności jest różny dla każdej operacji i można go zminimalizować, wybierając odpowiednią sekwencję operacji. Na przykład podczas mnożenia zbioru liczb, przed pomnożeniem najlepiej posortować zestaw według wykładnika.

Opisuje to każda przyzwoita książka o chrupaniu liczb. Na przykład: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

I aby odpowiedzieć na twoje pytanie:

Użyj dzielenia zamiast mnożenia, w ten sposób otrzymasz poprawny wynik.

double x = 1234;
for(int i=1;i<=2;i++)
{
  x =  x / 10.0;
}
System.out.println(x);
Jan Kotek
źródło
3

Nie, ponieważ typy zmiennoprzecinkowe Java (a właściwie wszystkie typy zmiennoprzecinkowe) są kompromisem między rozmiarem a dokładnością. Chociaż są one bardzo przydatne w wielu zadaniach, jeśli potrzebujesz dowolnej precyzji, powinieneś użyć BigDecimal.

biziclop
źródło