Newsgroup: it.comp.www.php
Titolo: Far di conto in Euro col PHP (v. 0)

Far di conto in Euro col PHP
----------------------------
Far di conto con gli Euro sembra facile, ma non lo e'.
Le soluzioni che presento qui sono il risultato di un fruttuoso scambio
di idee avvenuto sul newsgroup it.comp.www.php. Commenti, critiche,
precisazioni, soluzioni alternative, non solo sono benvenute, ma
necessarie.
Il problema
-----------
Un bel giorno mi e' capitato di fare questo semplice calcolo: la differenza
tra 57 centesimi e 56 centesimi fa un centesimo, giusto? sbagliato!
    echo(0.57-0.56); ==> 0.0099999999999999
Dopo un attimo di sconcerto, mi sono accorto che in realta' non c'e'
nulla di strano in questo risultato: PHP ha convertito i due numeri
decimali in float, ha eseguito la differenza e ha mostrato il risultato,
approssimato nei limiti di precisione dei float (in effetti il risultato
e' mooooolto vicino a quel 0.01 che avrei voluto vedere). Se l'espressione
aritmetica fosse stata appena piu' complicata, avremmo ottenuto risultati
ancora piu' sorprendenti.
Il problema sta nel fatto che certi numeri frazionari che espressi in base
dieci hanno un numero finito di cifre decimali, in base due non hanno una
rappresentazione finita di cifre binarie, e viceversa: in altri termini,
ogni volta che convertiamo un numero decimale frazionario in un float
e viceversa abbiamo una PERDITA IRREVERSIBILE DI INFORMAZIONE.
La morale: non e' possibile eseguire calcoli monetari in Euro usando i
float di PHP, almeno non e' possibile farlo senza alcuni accorgimenti.
Nelle discussioni su questo newsgroup avvenute qualche tempo fa, grazie
al contributo di vari partecipanti, siamo pervenuti a due soluzioni
possibili:
Soluzione 1: usando la libreria BC Math
---------------------------------------
Esiste un porting su PHP della famosa libreria di calcolo matematico
in precisione arbitraria che si chiama BC Math. Essenzialmente,
questa libreria rappresenta i numeri come strighe di cifre decimali,
e permette di definire un numero fisso di decimali coi quali eseguire
i calcoli. Visto che in questo caso i numeri sono stringhe, dovremo
eseguire TUTTI i calcoli sfruttando le apposite funzioni della libreria:
bcadd() per eseguire le somme, bcmul() per eseguire le moltiplicazioni,
bccomp() per confrontare valori, ecc. C'e' poi anche la funzione bcscale()
che permette di impostare il numero di decimali da usare: due decimali
potrebbero bastare per i calcoli in Euro, ma alcuni riferimenti normativi
raccomandano di usare 5 decimali per i calcoli intermedi, arrotondando
poi il risultato finale a due decimali. Ad esempio:
    $imponibile = "1234.56";
    # calcola l'IVA 20% (e' un calcolo intermedio: uso 5 decimali):
    bcscale(5);
    $iva = bcadd( bcmul($imponibile, "0.20"), "0.005" );
    # tronca i decimali dal terzo in poi:
    bcscale(2);
    $iva = bcadd($iva, "0");
    # calcola il totale (bisogna usare i valori a due decimali):
    $totale = bcadd($imponibile, $iva);
    # stampa la fattura:
    echo("IMPONIBILE = $imponibile\n");
    echo("IVA 20% = $iva\n");
    echo("TOTALE FATTURA = $totale\n");
Nel DB dovremo salvare l'imponibile, l'iva e il totale con due decimali.
I vari DB conformi ANSI/ISO SQL92 hanno un tipo dedicato per i valori monetari. Ad esempio, in PostgreSQL e in MySQL potremo usare:
    imponibile numeric(11,2)
che rappresenta importi fino a 1 migliardo di Euro. Al livello del DB
la rappresentazione di questi numeri e' come stringhe di caratteri,
per cui quando si vanno a leggere e scrivere con il PHP vanno trattati
come stringhe.
Soluzione 2: usando i float del PHP e facendo i conti in centesimi
------------------------------------------------------------------
Due osservazioni:
- il manuale del PHP assicura che i numeri float offrono almeno 14 cifre
significative in tutte le implementazioni di questo linguaggio;
- fintanto che i numeri da rappresentare sono interi, non avviene la
nefasta conversione tra numeri frazionari tra la rappresentazione dei
numeri frazionari in base 10 (quella che usiamo noi) e la base 2 (quella
che usa il computer), e quindi non si ha perdita di precisione.
Quindi, ad esempio, l'imponibile di 1234,56 Euro lo possiamo rappresentare
come 123456 Centesimi: in questo caso il PHP non sta neppure a scomodare
la precisione float, e usa dei semplici numeri interi (possiamo contare
su almeno 32 bit con segno). Per importi superiori ai 40 milioni di Euro,
PHP attiva la rappresentazione float che ci permette fino a 14 cifre,
con le quali possiamo rappresentare fino a 1 migliardo di Euro (Euro piu',
Euro meno).
Difetto 1: per numeri superiori a 1 migliardo di Euro si attiva la
notazione scientifica dei float, e si perdono alcune cifre significative
(saltano prima i centesimi, poi i decimi di Euro, poi gli Euro, poi le
decine di Euro, ecc.).
Difetto 2: la precisione dei calcoli intermedi e' fissata dal numero
di cifre significative dei float, e quindi non puo' essere controllata;
rimane comunque una precisione elevatissima.
Dunque, riassumiamo: nel PHP e nel DB spariscono gli Euro, e ci sono
solo Centesimi; solo a livello della interfaccia utente avremo cura di
presentare gli importi monetari posizionando la virgola come si conviene.
Per eseguire queste piccole conversioni Euro <--> Centesimi ho scritto
alcune funzioncine apposite, che forse postero'.
Vediamo come si presenta il calcolo della fattura di prima:
    $imponibile = 123456;
    $iva = floor( $imponibile * 0.20 + 0.005 );
    # calcola il totale (bisogna usare i valori a due decimali):
    $totale = $imponibile + $iva;
    # stampa la fattura:
    echo("IMPONIBILE = $imponibile Centesimi di Euro\n");
    echo("IVA 20% = $iva Centesimi di Euro\n");
    echo("TOTALE FATTURA = $totale Centesimi di Euro\n");
Se gli importi monetari espressi in centesimi di Euro non vi piacciono :-)
basta convertirli in Euro con una funzioncina come questa:
    function Cent2Euro($c)
    {
        $intero = floor( $c / 100 );
        $frazione = $c - 100 * $intero;
        if( $frazione <= 9 )  $frazione = "0$frazione";
        return "$intero,$frazione";
    }
I DB supportano tipi interi in precisione multipla, capaci di
rappresentare facilmente numeri interi a 64 bit con segno (bigint o int8
per PostgreSQL e MySQL) che occupano solo 8 Byte e sono molto efficienti
da gestire perche' i numeri sono rappresentati in forma binaria.
Ringraziamenti
--------------
Grazie a Jurgen Schwietering e a Marco Natoni per avere suggerito queste
soluzioni. Io avevo posto solo il problema!
Ciao,
 ___ 
/_|_\  Umberto Salsi
\/_\/  
www.icosaedro.it

 Far di conto in Euro col PHP
[cut]
Dato l'importanza: metto l'articolo nella FAQ?

 Difetto 1: per numeri superiori a 1 migliardo
Se però scrivi ancora una volta migliardo... la punizione consiste nel
doverlo scrivere per 200 volte sulla lavagna... Ovviamente senza l'uso di
str_repeat...

Grazie Umberto
E' usanza rara quanto ammirevole scrivere nel NG il risultato di un
dibattito e la conseguente risoluzione raggiunta.
Mi son salvato il testo, benché attualmente non sia un argomento di stretta
necessità per me, lo sarà sicuramente presto.
Grazie ancora e ciao
Marcello Verona
"Umberto Salsi" <sa...@icosaedro.italia> ha scritto nel messaggio
news:b3qhn6$8r4$1...@news.ngi.it...


Chiedo scusa se dico quello che penso senza aver
letto tutto il dibattito (non ho avuto tempo) e spero mi
perdoniate se dico una cosa già eviscerata:
Non era più facile moltiplicare per 100 tutte le valute,
prima del conto e ridividere per cento il risltato?
In questo modo si trasforma tutte le valute da float ad interi...
Spero di non aver detto una banalità...
Ninzo




    echo(0.57-0.56); ==> 0.0099999999999999
IMHO 0.0099999999999999 e' diverso da 0.01
mi spieghi dove finisce quel cent ???
quarda che c'e un errore nella libreria matematica del php che stai usando
ho fatto (scusa che cpu hai? c'e un intel e un amd che sono buggati
quali non lo so pero' controlla)(il tuo linux ha bug ?)
<?PHP
echo(0.57-0.56);
?>
a video 0.01
(lo fanno anche alcune calcolatrici con la divisione e li ammetto i
problemi:
perndere la calcolatrice e sbatterla al muro)
nel db mysql ci metti come campo double 10.2 (10 e' un numero di 10 cifre,
99 cent)
e poi con number_format($variabile,2) lo fai vedere in caso di 2.1 euri fa
vedere 2.10
e 'c'e  anche la funzione per arrotondare
un caffe costa 0.83 cent pero non sono 1600 lire di fatti sono 0,826 (il 6
e' maggiore di 5)
e si arrotonda all'unita superiore 0.83 con valori al di sotto del
5(compreso) si arrotonda
all'unita inferiore 0.825 diventa 0.82
se al bar consumate 10 caffe(lire) pagavate 16000 ma in euro 8.30
anche nei calcoli si usa sto metodo (ottimo per le banche)
spero di non aver detto stronzate :-)) (se lavorate avrete un commercialita
chiedeteci)

 float di PHP
E neppure nelle altre valute dotate di centesimi, immagino! E queste sono
in uso da molto piu' tempo dell'euro.
Mi pare strano che in PHP non ci siano funzioni "furbe" per la gestione
valutaria: ma si sa che agli hacker il denaro non interessa :-)))
Io personalmente vado in centesimi.
MySql prevede la colonna DECIMAL per memorizzare i numeri come stringa,
senza perdere precisione.
Jurgen, tu che hai nozione di causa, come si faceva in Germania quando
c'erano ancora i Pfennig?
ciao
Marco
-- 
bernama -tornado- libero -puntino- it
Salvate gli alberi: mangiate termiti.
M.B.
+- PHPRndSig 0.02 --------------------------------------------39/74-+

 [...]
Ti ringrazio molto per il riconoscimento, in fondo sto solo cercando
di dare il mio piccolo contributo (pensa solo a tutti quegli anonimi
che hanno sviluppato il PHP, tutte le sue librerie, la documentazione,
ecc. ecc.).
La prossima volta che posti un articolo, cerca di fare il quoting come si
deve: partecipa anche tu alla campagna contro il rumore sui newsgroup.


 Dato l'importanza: metto l'articolo nella FAQ?
Aspetta: e' gia' in preparazione la v. 1 ;-)


 anche nei calcoli si usa sto metodo (ottimo per le banche)
Suppongo che nelle banche non usino il PHP, ma semmai linguaggi specifici
per questo tipo di applicazioni che dispongono di tipi numerici in
rappresentazione decimale (COBOL?).


Far di conto in Euro col PHP
Please FAQ it! ;-)
Grazie.
Ciao

 tratta, ovviamente, di una soluzione molto inefficiente.
Si tratta di una stronzata. Dai, su...


 Non ci si IMPROVVISA programmatori, imho... si studia.
non so se il senso di questa frase è che IYHO Umberto non ha studiato 
programmazione.
se non vuoi che la gente pensi lo stesso di te, perché non rivedi come 
meglio credi questo documento e non ci esponi le tue conoscenze reali?
IMHO non va criticato un lavoro come quello di Umberto, ma va al limite 
corretto e soprattutto sostenuto a gran voce, o troveremo sempre più 
domande e sempre meno risposte sui NG.
ciao
-- 
punkarruso

 domande e sempre meno risposte sui NG.
Ripeto: capisco il punto. Io non sono una di quelle persone che premia 
"lo sforzo". Premio il risultato. E, contrariamente a ciò che possa 
sembrare, accetto molto volentieri le critiche degli altri.
Tutto qui.


 base 10).
Guarda che la famiglia 68k della Motorola ha delle istruzioni assembly
per trattare i numeri in base 10...
Questo non per dire che le calcolatrici funzionino cosi`, ma per esempio
per farti notare che e` vero che in alcune codifiche dei numeri floating
point i numeri decimali a cifre finite hanno corrispondenti binari a
cifre infinite. Non c'e` solo la rappresentazione IEEE, ce ne sono
altre.

 cifre infinite. 
Puoi postarmi - per favore - qualche link dove mi possa documentare 
sulla cosa ?
grazie, ciao



Certo che ci sono altri, ma PHP double si basa su IEEE, almeno su
piattaforme win,linux, altri potrebbe avere una rappresentazione diversa,
dipende dalla implementazione 'C'.
BTW: Motorola e (molto importante) IBM Mainframes usano altri formati (che
non importa molto qui). Cerca in rete EBCDIC ABCD SBCD, BCD numbers)

 "lo sforzo". Premio il risultato.
Concordo ;-)

 sulla cosa ?
Onestamente no.
Ricordo solo che al primo anno di Universita`, quando ci hanno spiegato
i vari sistemi di conversione decimale-binario ci e` capitato piu` di
una volta che un numero finito in decimale venisse convertito con
infinite cifre in binario.
Non so se la codifica IEEE 754 che tu citi abbia risolto questo problema
che, spesso, dipende anche dal numero di cifre significative, anzi,
grazie per il riferimento, mi sto documentando come posso :)
Per quanto ricordo dei tempi dell'Amiga, c'erano 2 codifiche: la IEEE e
la FP. La FP aveva circa 16 cifre significative, la IEEE qualcosa come
108, quindi con numeri cosi` "grandi" come 0.001 non dovrebbero avere
problemi, ma credo che molto dipenda anche dalla rappresentazione
interna e dai vari passaggi e conversioni effettuati dal parser.
E magari anche da qualche bug... :)
Bye.

 Onestamente no.
[supersnip]
Grazie lo stesso, cercherò un po'...
