Domanda

L'esecuzione di un rapido esperimento relativo a La doppia moltiplicazione è interrotta in .NET? e leggendo un paio di articoli sulla formattazione di stringhe C #, ho pensato che questo:

{
    double i = 10 * 0.69;
    Console.WriteLine(i);
    Console.WriteLine(String.Format("  {0:F20}", i));
    Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
    Console.WriteLine(String.Format("= {0:F20}", 6.9));
}

Sarebbe l'equivalente C # di questo codice C:

{
    double i = 10 * 0.69;

    printf ( "%f\n", i );
    printf ( "  %.20f\n", i );
    printf ( "+ %.20f\n", 6.9 - i );
    printf ( "= %.20f\n", 6.9 );
}

Tuttavia, C # produce l'output:

6.9
  6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000

nonostante io appaia uguale al valore 6.89999999999999946709 (anziché 6.9) nel debugger.

rispetto a C che mostra la precisione richiesta dal formato:

6.900000                          
  6.89999999999999946709          
+ 0.00000000000000088818          
= 6.90000000000000035527          

Cosa sta succedendo?

(Microsoft .NET Framework Versione 3.51 SP1 / Visual Studio C # 2008 Express Edition)


Ho un background nel calcolo numerico ed esperienza nell'implementazione dell'aritmetica degli intervalli - una tecnica per stimare gli errori dovuti ai limiti di precisione nei sistemi numerici complicati - su varie piattaforme. Per ottenere la taglia, non cercare di spiegare la precisione di archiviazione - in questo caso è la differenza di un ULP di un doppio a 64 bit.

Per ottenere la taglia, voglio sapere come (o se) .Net può formattare un doppio con la precisione richiesta come visibile nel codice C.

È stato utile?

Soluzione

Il problema è che .NET arrotonderà sempre un double a 15 cifre decimali significative prima applicando la formattazione, indipendentemente dalla precisione richiesta dal formato e indipendentemente dal valore decimale esatto del numero binario.

Immagino che il debugger di Visual Studio abbia le proprie routine di formattazione / visualizzazione che accedono direttamente al numero binario interno, quindi le discrepanze tra il codice C #, il codice C e il debugger.

Non c'è nulla di incorporato che ti permetta di accedere al valore decimale esatto di un doppio o di formattare un doppio su un numero specifico di decimali luoghi, ma puoi farlo tu stesso separando il numero binario interno e ricostruendolo come una rappresentazione di stringa del valore decimale.

In alternativa, puoi utilizzare la DoubleConverter di Jon Skeet (collegata dal suo " Binary floating point e .NET " article ). Questo ha un metodo ToExactString che restituisce il valore decimale esatto di un double . Puoi facilmente modificarlo per abilitare l'arrotondamento dell'output con una precisione specifica.

double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));

// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625

Altri suggerimenti

Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567);      // "123.46"
String.Format("{0:0.00}", 123.4);         // "123.40"
String.Format("{0:0.00}", 123.0);         // "123.00"

// max. two decimal places
String.Format("{0:0.##}", 123.4567);      // "123.46"
String.Format("{0:0.##}", 123.4);         // "123.4"
String.Format("{0:0.##}", 123.0);         // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567);      // "123.5"
String.Format("{0:00.0}", 23.4567);       // "23.5"
String.Format("{0:00.0}", 3.4567);        // "03.5"
String.Format("{0:00.0}", -3.4567);       // "-03.5"

Thousands separator
String.Format("{0:0,0.0}", 12345.67);     // "12,345.7"
String.Format("{0:0,0}", 12345.67);       // "12,346"

Zero
Following code shows how can be formatted a zero (of double type).

String.Format("{0:0.0}", 0.0);            // "0.0"
String.Format("{0:0.#}", 0.0);            // "0"
String.Format("{0:#.0}", 0.0);            // ".0"
String.Format("{0:#.#}", 0.0);            // ""

Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567);    // "     123.5"
String.Format("{0,-10:0.0}", 123.4567);   // "123.5     "
String.Format("{0,10:0.0}", -123.4567);   // "    -123.5"
String.Format("{0,-10:0.0}", -123.4567);  // "-123.5    "

Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567);   // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567);  // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0);        // "zero"

Some funny examples
String.Format("{0:my number is 0.0}", 12.3);   // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);

Dai un'occhiata a questo riferimento MSDN . Nelle note si afferma che i numeri sono arrotondati al numero di cifre decimali richieste.

Se invece usi " {0: R} " produrrà ciò che viene definito come "andata e ritorno" valore, dai un'occhiata a questo riferimento MSDN per ulteriori informazioni, ecco il mio codice e l'output:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);

uscita

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000

Sebbene nel frattempo questa domanda sia chiusa, credo che valga la pena menzionare come è nata questa atrocità. In un certo senso, potresti incolpare la specifica C #, che afferma che un doppio deve avere una precisione di 15 o 16 cifre (il risultato di IEEE-754). Un po 'più avanti (sezione 4.1.6) si afferma che le implementazioni possono usare una precisione maggiore . Intendiamoci: superiore , non inferiore. Possono anche deviare da IEEE-754: espressioni del tipo x * y / z dove x * y produrrebbe +/- INF ma sarebbe compreso in un intervallo valido dopo la divisione, non è necessario che si verifichi un errore. Questa funzione semplifica i compilatori nell'usare una maggiore precisione nelle architetture in cui ciò porterebbe a prestazioni migliori.

Ma ho promesso un "motivo". Ecco una citazione (hai richiesto una risorsa in uno dei tuoi commenti recenti) dalla CLI di origine condivisa , in clr / src / vm / comnumber.cpp :

  

" Al fine di dare numeri che sono entrambi   amichevole da visualizzare e   round-trippable, analizziamo il numero   usando 15 cifre e quindi determinare se   fa il giro dello stesso valore. Se   lo fa, convertiamo quel NUMERO in a   stringa, altrimenti analizziamo usando 17   cifre e visualizzalo. "

In altre parole: il team di sviluppo della CLI di MS ha deciso di essere sia in grado di eseguire il round trip che di mostrare valori piuttosto che non sono così difficili da leggere. Bene o male? Vorrei un opt-in o opt-out.

Il trucco che fa per scoprire questo round-trip di un dato numero? Conversione a una struttura NUMBER generica (che ha campi separati per le proprietà di un doppio) e viceversa, quindi confrontare se il risultato è diverso. Se è diverso, viene utilizzato il valore esatto (come nel tuo valore medio con 6.9 - i ) se è lo stesso, il "grazioso valore" viene usato.

Come hai già osservato in un commento ad Andyp, 6.90 ... 00 è bit a bit uguale a 6.89 ... 9467 . E ora sai perché viene utilizzato 0.0 ... 8818 : è bitwise diverso da 0.0 .

Questa barriera di 15 cifre è codificata e può essere modificata solo ricompilando la CLI, utilizzando Mono o chiamando Microsoft e convincendoli ad aggiungere un'opzione per stampare la "precisione" completa. (non è proprio la precisione, ma la mancanza di una parola migliore). Probabilmente è più semplice calcolare da soli la precisione a 52 bit o utilizzare la libreria menzionata in precedenza.

MODIFICA: se ti piace sperimentare te stesso con i punti mobili IEE-754, considera questo strumento online , che mostra tutte le parti rilevanti di un punto mobile.

Usa

Console.WriteLine(String.Format("  {0:G17}", i));

Questo ti darà tutte le 17 cifre che ha. Per impostazione predefinita, un valore Double contiene 15 cifre decimali di precisione, sebbene al suo interno vengano mantenute al massimo 17 cifre. {0: R} non ti darà sempre 17 cifre, ne darà 15 se il numero può essere rappresentato con quella precisione.

che restituisce 15 cifre se il numero può essere rappresentato con quella precisione o 17 cifre se il numero può essere rappresentato solo con la massima precisione. Non c'è niente che tu possa fare per rendere il doppio ritorno più cifre in questo modo. Se non ti piace, fai una nuova doppia classe tu stesso ...

Il doppio di .NET non può memorizzare più cifre di 17, quindi non puoi vedere 6.89999999999999946709 nel debugger che vedresti 6.899999999999999995. Fornisci un'immagine per dimostrarci che ci sbagliamo.

La risposta a questa domanda è semplice e può essere trovata su MSDN

  

Ricorda che un numero in virgola mobile può approssimare solo un numero decimale e che la precisione di un numero in virgola mobile determina la precisione con cui quel numero si avvicina a un numero decimale. Per impostazione predefinita, un valore Double contiene 15 cifre decimali di precisione , sebbene al suo interno vengano mantenute al massimo 17 cifre.

Nel tuo esempio, il valore di i è 6.89999999999999946709 che ha il numero 9 per tutte le posizioni tra la 3a e la 16a cifra (ricorda di contare la parte intera nelle cifre). Quando si converte in stringa, il quadro arrotonda il numero alla 15a cifra.

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901

Ho provato a riprodurre i tuoi risultati, ma quando ho visto "i" nel debugger è apparso come "6.8999999999999995" non come "6.89999999999999946709" come hai scritto nella domanda. Puoi fornire passaggi per riprodurre ciò che hai visto?

Per vedere cosa ti mostra il debugger, puoi usare un DoubleConverter come nella seguente riga di codice:

Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));

Spero che questo aiuti!

Modifica: immagino di essere più stanco di quanto pensassi, ovviamente è lo stesso della formattazione sul valore di andata e ritorno (come menzionato prima).

La risposta è sì, la doppia stampa è interrotta in .NET, stanno stampando cifre di immondizia in coda.

Puoi leggere come implementarlo correttamente qui .

Ho dovuto fare lo stesso per IronScheme.

> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995

Nota: sia C che C # hanno un valore corretto, solo una stampa non funzionante.

Aggiornamento: sto ancora cercando la conversazione sulla mailing list che ho portato a questa scoperta.

Ho trovato questa soluzione rapida.

    double i = 10 * 0.69;
    System.Diagnostics.Debug.WriteLine(i);


    String s = String.Format("{0:F20}", i).Substring(0,20);
    System.Diagnostics.Debug.WriteLine(s + " " +s.Length );

Console.WriteLine (string.Format (" Il costo del corso è {0: 0.00} " ;, + cfees));

internal void DisplaycourseDetails()
        {
            Console.WriteLine("Course Code : " + cid);
            Console.WriteLine("Couse Name : " + cname);
            //Console.WriteLine("Couse Name : " + string.Format("{0:0.00}"+ cfees));
            // string s = string.Format("Course Fees is {0:0.00}", +cfees);
            // Console.WriteLine(s);
            Console.WriteLine( string.Format("Course Fees is {0:0.00}", +cfees));
        }
        static void Main(string[] args)
        {
            Course obj1 = new Course(101, "C# .net", 1100.00);
            obj1.DisplaycourseDetails();
            Course obj2 = new Course(102, "Angular", 7000.00);
            obj2.DisplaycourseDetails();
            Course obj3 = new Course(103, "MVC", 1100.00);
            obj3.DisplaycourseDetails();
            Console.ReadLine();
        }
    }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top