2010-10-12 10 views
9

Come posso risolvere questo codice in modo che 1.1 + 2.2 == 3.3? Cosa sta realmente accadendo qui che sta causando questo comportamento? Ho vagamente familiarità con problemi di arrotondamento e matematica in virgola mobile, ma ho pensato che fosse applicato solo alla divisione e alla moltiplicazione e sarebbe visibile nell'output.Come posso correggere questo codice Perl in modo che 1.1 + 2.2 == 3.3?

[[email protected]:~/perltests]> cat testmathsimple.pl 
#!/usr/bin/perl 

use strict; 
use warnings; 

check_math(1, 2, 3); 
check_math(1.1, 2.2, 3.3); 

sub check_math { 
     my $one = shift; 
     my $two = shift; 
     my $three = shift; 

     if ($one + $two == $three) { 
       print "$one + $two == $three\n"; 
     } else { 
       print "$one + $two != $three\n"; 
     } 
} 

[[email protected]:~/perltests]> perl testmathsimple.pl 
1 + 2 == 3 
1.1 + 2.2 != 3.3 

Edit:

maggior parte delle risposte finora sono sulla falsariga di "si tratta di un problema in virgola mobile, duh" e stanno fornendo soluzioni per esso. Ho già il sospetto che questo sia il problema. Come lo dimostro? Come ottengo Perl per produrre la forma lunga delle variabili? Memorizzare il $ 1 + $ due di calcolo in una variabile temporanea e stamparlo non dimostra il problema.

Edit:

Utilizzando la tecnica sprintf dimostrata da aschepler, io sono ora in grado di "vedere" il problema. Inoltre, l'utilizzo di bignum, come raccomandato da mscha e rafl, risolve il problema del confronto non essendo uguale. Tuttavia, l'output di sprint indica ancora che i numeri non sono "corretti". Questo lascia un minimo di dubbio su questa soluzione.

Bignum è un buon modo per risolvere questo problema? Ci sono alcuni possibili effetti collaterali di bignum che dovremmo considerare quando li integriamo in un programma più grande ed esistente?

+0

Ahh, OK. Bene, puoi farlo: stampa "$ uno + $ due! = $ Tre \ n"; stampa ($ tre - ($ uno + $ due)). "\ N"; e vedrai qualcosa del tipo: "1.1 + 2.2! = 3.3 \ n-4.44089209850063e-16 \ n". Quindi puoi manipolare le stringhe per metterle insieme in una variabile. –

+0

Stampa '$ uno + $ due - $ tre' per dimostrarlo. 'perl -e '$ one = 1.1; $ two = 2.2; $ three = 3.3; print $ one + $ two- $ three' ==> 4.44089209850063e-16' YMMV. – mob

+0

Se vuoi usare 'bignum', usa un metodo di' Math :: BigFloat', non 'sprintf'. Ad esempio, 'stampa $ num-> bstr();'. Passare un numero a 'sprintf' probabilmente lo converte in un normale numero a virgola mobile (' double') prima di convertirlo in stringa. – aschepler

risposta

17

Vedi What Every Computer Scientist Should Know About Floating-Point Arithmetic.

Nessuno di ciò è specifico Perl: esiste un numero infinitamente infinito di numeri reali e, ovviamente, tutti non possono essere rappresentati utilizzando solo un numero finito di bit.

La "soluzione" specifica da utilizzare dipende dal problema specifico. Stai cercando di tracciare importi monetari? In tal caso, utilizzare i numeri di precisione arbitrari (utilizzare più memoria e più CPU, ottenere risultati più precisi) forniti da bignum. Stai facendo analisi numerica? Quindi, decidere la precisione che si desidera utilizzare e utilizzare sprintf (come illustrato di seguito) e eq da confrontare.

È sempre possibile utilizzare:

use strict; use warnings; 

check_summation(1, $_) for [1, 2, 3], [1.1, 2.2, 3.3]; 

sub check_summation { 
    my $precision = shift; 
    my ($x, $y, $expected) = @{ $_[0] }; 
    my $result = $x + $y; 

    for my $n ($x, $y, $expected, $result) { 
     $n = sprintf('%.*f', $precision, $n); 
    } 

    if ($expected eq $result) { 
     printf "%s + %s = %s\n", $x, $y, $expected; 
    } 
    else { 
     printf "%s + %s != %s\n", $x, $y, $expected; 
    } 
    return; 
} 

uscita:

1.0 + 2.0 = 3.0 
1.1 + 2.2 = 3.3
+0

ho intenzione di accettare questa come la risposta, perché è il più completo finora. Tuttavia, ho trovato anche il materiale specifico Perl che brian d foy ha fornito in un commento per essere utile. – user255205

1

Utilizzare sprintf per convertire la variabile in una stringa formattata e quindi confrontare la stringa risultante.

# equal($x, $y, $d); 
# compare the equality of $x and $y with precision of $d digits below the decimal point. 
sub equal { 
    my ($x, $y, $d) = @_; 
    return sprintf("%.${d}g", $x) eq sprintf("%.${d}g", $y); 
} 

Questo tipo di problema si verifica perché non esiste una perfetta rappresentazione in virgola fissa per le tue frazioni (0,1, 0,2, ecc.). Quindi il valore 1.1 e 2.2 sono effettivamente memorizzati come qualcosa come 1.10000000000000...1 e 2.2000000....1, rispettivamente (non sono sicuro se diventa leggermente più grande o leggermente più piccolo. Nel mio esempio presumo siano leggermente più grandi). Quando li aggiungi, diventa 3.300000000...3, che è maggiore di 3.3 che viene convertito in 3.300000...1.

+1

In alternativa, fare matematica con preci- sione più alta, come previsto dalla classe 'Math :: Big *' dei moduli ('Math :: BigInt',' Math :: BigFloat', 'Math: : BigRat', 'bignum',' bigint', 'bigrat', ...) – rafl

+0

Non collegare a copie non autorizzate di libri. –

+0

Siamo spiacenti. Non ne ero a conoscenza. Link rimosso. – YYC

2

abs($three - ($one + $two)) < $some_Very_small_number

6

"What Every Computer Scientist Should Know About Floating-Point Arithmetic"

In sostanza, Perl si occupa di numeri in virgola mobile, mentre si sono probabilmente aspettava da usare fisso -punto. Il modo più semplice per gestire questa situazione è modificare il codice in modo da utilizzare interi interi ovunque, tranne, forse, in una routine di visualizzazione finale. Ad esempio, se hai a che fare con valuta USD, memorizza tutti gli importi in dollari in pochi centesimi. 123 dollari e 45 centesimi diventano "12345". In questo modo non vi è alcuna ambiguità di virgola mobile durante le operazioni di aggiunta e sottrazione.

Se questo non è un'opzione, considerano Matt Kane's comment. Trova un buon valore epsilon e usalo ogni volta che devi confrontare i valori.

Mi piacerebbe indovinare che la maggior parte delle attività non è in realtà necessario virgola mobile, tuttavia, e consiglio caldamente di considerare se sia o meno lo strumento giusto per l'attività.

+1

Il problema è * non * virgola mobile rispetto al punto fisso. Il problema è binario vs decimale e precisione limitata. –

+1

Ci sono un numero infinitamente infinito di numeri reali. Chiaramente, non puoi rappresentarli tutti esattamente con un numero finito di bit. –

4

Da The Floating-Point Guide:

Perché i miei numeri, come 0,1 + 0,2 aggiungere fino a un bel giro 0.3, e invece ho un risultato strano come ,30000000000000004?

Poiché internamente, computer utilizzano un formato (binario a virgola mobile) che non possono rappresentare accuratamente un numero come 0,1, 0,2 o 0,3 affatto.

Quando il codice viene compilato o interpretato , proprio “0,1” è già arrotondato al numero più vicino in quel formato , che si traduce in un piccolo errore di arrotondamento ancor prima del calcolo succede.

Cosa posso fare per evitare questo problema?

Dipende dal tipo di calcolo che stai facendo.

  • Se si ha realmente bisogno i risultati per aggiungere esattamente, soprattutto quando si lavoro con i soldi: utilizza una speciale decimal datatype.
  • Se non vuoi vedere tutte quelle cifre decimali aggiuntive: semplicemente il formato il tuo risultato viene arrotondato a un numero fisso di cifre decimali quando lo visualizza .
  • Se non avete tipo di dati decimale disponibili, in alternativa è quello di lavorare con gli interi, per esempio fare soldi calcoli interamente in centesimi. Ma questo è più lavoro e ha alcuni svantaggi .

Youz potrebbe anche usare un "fuzzy compare" per determinare se due numeri sono abbastanza vicino a supporre che sarebbero stati lo stesso utilizzando la matematica esatta.

4

per visualizzare i valori precisi per le scalari in virgola mobile, dare un grande precisione per sprintf:

print sprintf("%.60f", 1.1), $/; 
print sprintf("%.60f", 2.2), $/; 
print sprintf("%.60f", 3.3), $/; 

ottengo:

1.100000000000000088817841970012523233890533447265625000000000 
2.200000000000000177635683940025046467781066894531250000000000 
3.299999999999999822364316059974953532218933105468750000000000 

Purtroppo% di C99 una conversione non sembra funzionare . perlvar menziona una variabile obsoleta $# che modifica il formato predefinito per la stampa di un numero, ma si interrompe se gli viene dato un% f e% g rifiuta di stampare cifre "non significative".

5

Un modo rapido per correggere i punti mobili è utilizzare bignum. Basta aggiungere una riga

use bignum; 

all'inizio del copione. Ci sono ovviamente implicazioni sul rendimento, quindi questa potrebbe non essere una buona soluzione per te.

Una soluzione più localizzata è utilizzare Math::BigFloat esplicitamente dove è necessaria una maggiore precisione.

1

Number::Fraction consente di lavorare con numeri razionali (frazioni) invece di decimali, qualcosa di simile (':' costanti viene importata per convertire automaticamente le stringhe come '11/10' in oggetti Numero :: Fraction):

use strict; 
use warnings; 
use Number::Fraction ':constants'; 

check_math(1, 2, 3); 
check_math('11/10', '22/10', '33/10'); 

sub check_math { 
     my $one = shift; 
     my $two = shift; 
     my $three = shift; 

     if ($one + $two == $three) { 
       print "$one + $two == $three\n"; 
     } else { 
       print "$one + $two != $three\n"; 
     } 
} 

che stampa:

1 + 2 == 3 
11/10 + 11/5 == 33/10 
Problemi correlati