2014-07-05 18 views
6

Quando si converte un Doppio di precisione "alto" in un decimale, perdo la precisione con Convert.ToDecimal o il cast verso (Decimale) a causa di Arrotondamento.Da Doppio a Decimale senza arrotondamento dopo 15 cifre

Esempio:

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

Il valore decimale restituito da Convert.ToDecimal (doppio) contiene un massimo di 15 cifre significative. Se il parametro value contiene più di 15 cifre significative, viene arrotondato con arrotondamento al più vicino.

Così, al fine di mantenere la mia precisione, devo convertire il mio doppio in una stringa e quindi chiamare Convert.ToDecimal (String):

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

Questo metodo funziona, ma vorrei evitare di usare una variabile String per convertire un valore da doppio a decimale senza arrotondamento dopo 15 cifre?

+0

Conoscete in anticipo la gamma per 'd'? Posso offrire alcune soluzioni leggere in pseudocodice (non sono abbastanza familiare con C# per scriverle in C#) se sai che 'd' è in un intervallo come [-2..2] –

+0

d sarà sempre tra [- 1,1] –

+0

Suppongo che questo sia un "doppio" proveniente da qualcos'altro che non puoi cambiare? Se è dichiarato decimale dal get-go ('decimal d = -0.99999999999999956m;') mantiene tale precisione. –

risposta

4

Una possibile soluzione è decomporre la quantità esatta di n raddoppia, l'unica delle quali è piccola e contiene tutte le cifre significative finali desiderate quando convertite in decimale, e la prima (n-1) di cui si ha bisogno è n. convertire esattamente in decimale.

Per la sorgente doppia d tra -1.0 e 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.

noti che le operazioni d -= sono esatte, anche se C# calcola le operazioni a virgola mobile binario ad una precisione superiore double (che si permette di fare).

Questo è più economico di una conversione da double a stringa e fornisce alcune cifre aggiuntive di accuratezza nel risultato (quattro bit di precisione per i quattro if-then-els precedenti).

Nota: se C# non permettersi di fare calcoli in virgola mobile a una maggiore precisione, un buon trucco sarebbe stata quella di utilizzare Dekker divisione per dividere d in due valori d1 e d2 che convertire ogni esattamente in decimale. Purtroppo, la divisione di Dekker funziona solo con un'interpretazione rigorosa della moltiplicazione e dell'aggiunta di IEEE 754.


Un'altra idea è quella di utilizzare la versione C# s 'di frexp per ottenere il significante s e esponente e di d, e per calcolare (Decimal)((long) (s * 4503599627370496.0d)) * <however one computes 2^e in Decimal>.

+0

Ho provato il tuo esempio di codice ma ottengo -1. Puoi riprodurlo dalla tua parte? –

+0

@StevenMuhr Spiacente, non ho C# o nessuna piattaforma in cui possa eseguire codice .NET, ma cercherò di giocare con ideone.com e tornare con qualcosa che funzioni. –

+0

Metodo @StevenMuhr1, risultato: 0,9999999999999996. È necessario aggiungere la gestione per il segno di 'd'. http://ideone.com/MevINX –

1

Esistono due approcci, uno dei quali funzionerà per valori inferiori a 2^63 e l'altro funzionerà per valori superiori a 2^53.

Dividere i valori più piccoli in numeri interi e parti frazionarie. La parte di numero intero può essere castata precisamente a long e quindi a Decimal [si noti che un cast diretto a Decimal potrebbe non essere preciso!] La parte frazionaria può essere moltiplicata per 9007199254740992.0 (2^53), convertita in long e quindi Decimal, e quindi diviso per 9007199254740992.0m. Aggiungere il risultato di tale divisione alla parte di numero intero dovrebbe produrre un valore Decimal che si trova all'interno di una cifra meno significativa di essere corretto [potrebbe non essere arrotondato con precisione, ma sarà comunque molto meglio delle conversioni incorporate!]

Per valori maggiori, moltiplicare per (1.0/281474976710656.0) (2^-48), prendere la parte di numero intero di quel risultato, moltiplicarla per 281474976710656.0 e sottrarla dal risultato originale. Convertire i risultati dell'intero numero dalla divisione e la sottrazione a Decimal (devono essere convertiti con precisione), moltiplicare il precedente di 281474976710656m e aggiungere quest'ultimo.