2014-07-05 11 views
6

Podczas konwertowania "wysokiej" precyzji z podwójnej na dziesiętną tracę precyzję z Konwertuj.Tabela dziesiętna lub Odlewanie do (Dziesiętna) z powodu zaokrąglania.Podwójnie do dziesiętnego bez zaokrąglania po 15 cyfrach

przykład:

double d = -0.99999999999999956d; 
decimal result = Convert.ToDecimal(d); // Result = -1 
decimal result = (Decimal)(d); // Result = -1 

wartość dziesiętną zwrócony przez Convert.ToDecimal (podwójne) zawiera maksymalnie 15 cyfr znaczących. Jeśli parametr wartości zawiera więcej niż 15 cyfr znaczących, jest zaokrąglany za pomocą zaokrągleń do najbliższego.

więc aby utrzymać moje precyzji, muszę konwertować mój podwójny do String, a następnie zadzwonić Convert.ToDecimal (String):

decimal result = System.Convert.ToDecimal(d.ToString("G20")); // Result = -0.99999999999999956d 

Ta metoda działa, ale chciałbym, aby unikać zmienną String w celu zamiany podwójnej na dziesiętną bez zaokrąglania po 15 cyfrach?

+0

Czy znasz wcześniej zakres dla 'd'? Mogę zaoferować lekkie rozwiązania w pseudokodach (nie jestem dostatecznie zaznajomiony z C#, aby zapisać je w C#), jeśli wiesz, że 'd' jest w zakresie takim jak [-2..2] –

+0

d zawsze będzie pomiędzy [- 1,1] –

+0

Zakładam, że jest to "podwójne" pochodzące z innego miejsca, którego nie można zmienić? Jeśli zostanie zadeklarowany jako dziesiętny od początku ('decimal d = -0.99999999999999956m;') zachowuje tę dokładność. –

Odpowiedz

4

Jednym z możliwych rozwiązań jest rozłożenie d jako dokładnej sumy n podwójnych, z których ostatnia jest mała i zawiera wszystkie znaczące cyfry, które są pożądane po przekonwertowaniu na dziesiętne, a pierwsze (n-1) z przelicz dokładnie na dziesiętne.

Dla źródła podwójnego d między -1,0 a 1,0:

decimal t = 0M; 
    bool b = d < 0; 
    if (b) d = -d; 
    if (d >= 0.5) { d -= 0.5; t = 0.5M; } 
    if (d >= 0.25) { d -= 0.25; t += 0.25M; } 
    if (d >= 0.125) { d -= 0.125; t += 0.125M; } 
    if (d >= 0.0625) { d -= 0.0625; t += 0.0625M; } 
    t += Convert.ToDecimal(d); 
    if (b) t = -t; 

Test it on ideone.com.

nocie, że operacje d -= są dokładne, nawet jeśli C# oblicza binarnych operacji zmiennoprzecinkowych na większą dokładnością niż double (co pozwala sobie zrobić).

Jest to tańszy niż konwersja z double na ciąg, i zapewnia kilka dodatkowych cyfr dokładności w wyniku (cztery bity dokładności dla powyższych czterech if-then-elses).

Uwaga: jeśli C# nie pozwalał sobie robić obliczeń zmiennoprzecinkowych w wyższej precyzji, dobra sztuczka byłoby użyć Dekker podział podzielić d do dwóch wartości d1 i d2 które przekształcają każdy dokładnie na dziesiętne. Niestety, dzielenie Dekkera działa tylko przy ścisłej interpretacji mnożenia i dodawania IEEE 754.


Innym pomysłem jest użycie wersji C# 's frexp uzyskanie mantysy s oraz wykładnik e z d i obliczyć (Decimal)((long) (s * 4503599627370496.0d)) * <however one computes 2^e in Decimal>.

+0

Próbowałem twojej próbki kodu, ale otrzymałem -1. Czy możesz go odtworzyć na swojej stronie? –

+0

@StevenMuhr Przepraszam, nie mam C# ani nawet żadnej platformy, na której mógłbym uruchomić kod .NET, ale spróbuję grać z ideone.com i wrócić z czymś, co działa. –

+0

@StevenMuhr Method1, wynik: 0.9999999999999996. Musisz dodać obsługę znaku "d". http://ideone.com/MevINX –

1

Istnieją dwa podejścia, z których jeden będzie działać dla wartości poniżej 2^63, a drugi z nich będzie działać dla wartości większych niż 2^53.

Podziel mniejsze wartości na części całkowite i ułamkowe. Całościowa część może być dokładnie odlana do long, a następnie Decimal [zauważ, że bezpośredni rzut do Decimal może nie być dokładny!] Część ułamkowa może być precyzyjnie pomnożona przez 9007199254740992.0 (2^53), przekształcona na long, a następnie Decimal, a następnie podzielone przez 9007199254740992,0m. Dodanie wyniku tego podziału do liczby całkowitej powinno dać wartość Decimal, która jest w granicach jednej najmniej znaczącej cyfry jest poprawna [może nie być dokładnie zaokrąglona, ​​ale nadal będzie dużo lepsza niż wbudowane konwersje!]

Dla większych wartości, pomnóż przez (1,0/281474976710656,0) (2^-48), weź całkowitą część tego wyniku, pomnóż ją ponownie przez 281474976710656.0 i odejmij od oryginalnego wyniku. Konwertuj liczby całkowite z podziału i odejmowanie na Decimal (powinny one dokładnie konwertować), pomnóż poprzednią przez 281474976710656m i dodaj drugą.