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

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: in altri termini,
ogni volta che convertiamo un numero decimale frazionario in un float
abbiamo una PERDITA IRREVERSIBILE DI INFORMAZIONE.
La morale: non e' possibile eseguire calcoli monetari in Euro usando i
float di PHP, o 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. Siccome in tal caso non ha mai luogo la nefasta conversione
decimale -> binario, non troveremo risultati strani.
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.004" );
    # 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:
    numeric(11,2)
che rappresenta importi fino a 1 miliardo di Euro, oltre alle due cifre
decimali. 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 20 milioni di Euro,
PHP attiva la rappresentazione float che ci permette fino a 14 cifre,
con le quali possiamo rappresentare fino a 1 miliardo di Euro (Euro piu',
Euro meno).
Difetto 1: per numeri superiori a 1 miliardo 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.
Vediamo come si presenta il calcolo della fattura di prima:
    $imponibile = 123456;
    $iva = floor( $imponibile * 0.20 + 0.004 );
    $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.0 );
        $frazione = $c - 100.0 * $intero;
        if( $frazione <= 9 )  $frazione = "0$frazione";
        return "$intero,$frazione";
    }
Per la validazione dell'input dell'utente scriveremo un'altra
funzioncina, e per la conversione del valore inserito dall'utente
(espresso in Euro) nella nostra rappresentazione in centesimi useremo
un'altra funzioncina ancora. Queste tre funzioni ci permetteranno di
astrarre tra la rappresentazione interna del programma (in Centesimi)
e la rappresentazione a livello della interfaccia utente (in Euro).
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, ci possono stare fino a
92 milioni di miliardi di Euro 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



    echo(0.57-0.56); ==> 0.0099999999999999





    $imponibile = "1234.56";




    numeric(11,2)









    $imponibile = 123456;
    $iva = floor( $imponibile * 0.20 + 0.4 );


 poi il risultato finale a due decimali.
Ecco la vera domanda, cosa che non ho mai capito: per legge i risultati
delle operazioni vanno espressi con due decimali, ovvero, se ad esempio ho
11,11 euro e devo aggiungerci il 10% di iva il risultato sarà 11,11 + 1,111
= 12,221, e si arrotonda a 12,22 euro. Ma perché per i calcoli intermedi è
concesso l'uso di un numero maggiore di cifre decimali? Dopo ogni calcolo,
il risultato non dovrebbe contenere due cifre decimali? Ad esempio, se sommo
il 10% di iva a 11,11 euro, non dovrei forse prima calcolare il valore da
aggiungere (1,11) e poi sommarlo? In questo caso il risultato è identico, ma
ci sono sicuramente casi in cui non è così, soprattutto se poi su questo
risultato si effettuano altre operazioni.

 concesso l'uso di un numero maggiore di cifre decimali? 
   A parte che ci sono casi (quotazioni di divise o titoli azionarii, 
tariffazioni telefoniche) che anche nella presentazione sono necessarie 
piu` di due cifre decimali, la necessita` di usare piu` cifre nel 
calcolo e` dovuta al fatto che solo l'ultimo valore ottenuto puo` essere 
arrotondato, altrimenti si rischiano "creste" notevoli sia in positivo 
che in negativo.
   Pensa, per esempio, ad una parcella professionale: all'imponibile 
netto va assommata prima la ritenuta d'acconto, poi il contributo 
previdenziale, poi l'IVA, e cosi` via: se ad ogni somma arrotondi, 
rischi di raggiungere risultati diversi da quelli che il cliente si 
attende, con sgradevoli conseguenze sul piano dell'immagine.
-- 
Alla prossima,
Marco

 queste cose. Se cosě fosse, č inutile scomodare la fantasia.
   Infatti non e` stata scomodata...
-- 
Alla prossima,
Marco

 queste cose. Se cosě fosse, č inutile scomodare la fantasia.
Solo per convertire lire in euro (azioni, capitale societa', ecc.)
Il decreto:
http://www.agenziaentrate.it/documentazione/euro/normativa/nazionali/dlgs_20
6_del_15061999.htm
Esempi pratici:
 
http://www.ermesimprese.it/euro/regole/default.html

= 5 si aggiunge un centesimo. Esempio:
     100,124... --> 100,12
     100,125... --> 100,13
     100,126... --> 100,13
Nel caso del nostro rivenditore di rondelle che potrebbe avere a che fare
con importi molto piccoli, si considerano invece due decimali, cioe'
il terzo e il quarto (non approfondisco, spero si capisca).
- I dettagli su come si eseguono i calcoli intermedi e su come si
riportano i risultati intermedi e finali, ad esempio in una fattura,
dipendono dal tipo di societa' e quindi dal tipo di contabilita' cui deve
sottostare. Ad esempio, un grossista di rondelle (merce di piccolo valore
unitario) potrebbe indicare nei listini prezzi tre o piu' decimali che
riporterebbe nelle varie righe della fattura (voce "prezzo unitario"),
ed eseguire i calcoli intermedi con precisione ancora superiore.
- In tutti i casi, in una fattura elementare, devono apparire
l'imponibile, l'iva e il totale con due decimali; inoltre e'
indispensabile che la somma imponibile+iva=totale: l'algoritmo che ho
proposto nella mia mini-faq dovrebbe garantire questa cosa. Notare bene:
il totale fattura va calcolato sommando l'imponibile e l'iva CON 2 SOLE
CIFRE DECIMALI.
- Curiosita': quando si va a fare il totale tra gli imponibili di tutte le
fatture emesse (imponibile_tot), il totale dell'iva emessa (iva_tot) e il
totale dei totali fattura (tot_tot) segue naturalmente che:
     imponibile_tot + iva_tot = tot_tot
pero' si avra' anche
     imponibile_tot * 0,2 != iva_tot
per via degli arrotondamenti: questo e' un fatto ovvio e perfettamente
regolare.
