Home / Indice sezione | www.icosaedro.it | ![]() |
Ultimo aggiornamento: 2009-01-17
Far di conto con gli Euro sembra facile, ma non lo è. Dopo l'introduzione dell'Euro anche nel nostro paese, ci siamo dovuti confrontare con importi monetari con la virgola. Molti programmatori, in modo più o meno consapevole, ricorrono semplicemente ai numeri in virgola mobile float. Qui dimostriamo come questa scelta sia sbagliata perché può portare a risultati inattesi, e inoltre illustriamo alcune soluzioni per risolvere il problema in modo corretto.
Il problema
Il lungo viaggio da 0.1 a binario e ritorno
Soluzione 1: usando la libreria BC Math
Soluzione 1b: usando la classe BigFloat
Soluzione 2: usando i float del PHP e facendo i conti in centesimi
Soluzione 2b: usando la classe BigInt
Appendice
Ringraziamenti
Riferimenti
Un bel giorno mi è capitato di fare questo semplice calcolo: la differenza tra 57 centesimi e 56 centesimi fa un centesimo, giusto? sbagliato! Verifichiamo:
echo 0.57 - 0.56; ==> 0.0099999999999999
Dopo un attimo di sconcerto, mi sono accorto che in realtà non c'è 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 è molto vicino a quel 0.01 che avrei voluto vedere). Se l'espressione aritmetica fosse stata appena più complicata, avremmo ottenuto risultati ancora più sorprendenti. Vediamo a quanto ammonta l'errore:
echo 0.57 - 0.56 - 0.01; ==> -1.0234868508263E-16
Questa espressione avrebbe dovuto dare esattamente zero, e di fatto 1E-16 è estremamente piccolo, ma non abbastanza piccolo da non fare sorgere qualche preoccupazione. Si possono trovare infiniti altri esempi che danno errori in più o in meno. Per esempio:
echo 12.3 - 12.27; ==> 0.030000000000001
L'errore cresce se la parte intera a sinistra del punto decimale è più grande. Si potrebbe obiettare che in una lunga serie di calcoli, tutti questi errori in più e in meno tendono a compensarsi, sicché l'errore finale dovrebbe mantenersi piccolo. Tuttavia l'uso del condizionale in questa frase non mi rassicura per niente...
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. Ad esempio, il numero 0.1 (un decimo) se rappresentato in base binaria prende infinite cifre (per l'esattezza, è anche periodico):
0.1dec = 0.000110011001100110011001100110011...bin
Ma un numero di infinite cifre non può stare nel registro del processore, per cui deve essere troncato o arrotondato. Tipicamente ci dovremo accontentare di solo 53 cifre binarie (è il caso della rappresentazione IEEE 754 in doppia precisione).
L'uso dei float riserva qualche sorpresa anche quando si vanno a stampare espressioni algebricamente identiche. Ad esempio, queste espressioni differiscono per l'ordine dei fattori e dovrebbero dare zero per via dell'ultimo fattore, che appunto vale zero:
echo -1 * 0.01 * 0, "\n"; ==> 0 echo 0.01 * (-1) * 0, "\n"; ==> -0
Ovviamente 0 e -0 sono identici ai fini scientifici e ingegneristici, ma se cose del genere appaiono in un programma gestionale non fanno una bella figura.
Un'altra fonte di possibili tranelli sono i confronti tra numeri float. Troncamenti, arrotondamenti, lievi differenze dovute all'ordine di esecuzione dei calcoli, possono far sì che valori che dovrebbero essere uguali risultino di fatto diversi:
$entrate = una certa espressione; $uscite = un'altra espressione; echo "Quadratura del bilancio: "; if( $entrate == $uscite ) echo "ok"; else echo "ERRATA";
Anche se il bilancio viene arrotondato all'unità di Euro (D.L. 24 giugno 1998 n. 213) siamo sicuri che gli errori accumulati portino comunque alla quadratura?
Nel calcolo scientifico e tecnico si risolve eseguendo il test di uguaglianza entro un certo errore stabilito, per esempio
if( abs($x - $y) < 1e-6 ) echo "sono uguali";
ma questa strategia non è soddisfacente nel calcolo con importi monetari dove anche il più piccolo resto deve essere tenuto da conto.
Ultimo "incidente" che mi è capitato realmente: senza riflettere troppo, pensavo che i numeri interi come 1e23 potessero, almeno loro, essere memorizzati in modo esatto grazie alla rappresentazione esponenziale dei numeri in virgola mobile, per cui stampando questo numero mi aspettavo di vedere un "1" seguito da 23 zeri, giusto? Sbagliato! Ecco cosa succede:
$f = 1e23; printf("%.0F", $f); ==> 99999999999999991611392
Cosa è successo? forse la funzione printf() fa i capricci? Se uso il descrittore di formato "%g" viene stampato giusto, ma si tratta di un arrotondamento cosmetico. Il problema nasce quando il numero 1e23 viene convertito in float dal parser di PHP. Infatti 1e23 richiede ben più di 53 bit e inoltre, ed è questo il punto, mentre in base dieci abbiamo 23 zeri, in base due ci sono un miscuglio di bit e non degli zeri:
1e23dec =
10101001011010000001011000111111000010100101011110110100000000000000000000000bin
La parte che ho sottolineato eccede il 53-esimo bit e perciò deve essere troncata, così portandosi via le 8388608 unità che mancano per ottenere da printf() il risultato giusto. L'artefatto finisce memorizzato nella variabile $f, e quando quest'ultima viene stampata, la conversione in decimale porta al risultato "inatteso".
Abbiamo visto che anche i più semplici numeri decimali come 0.1 non si possono rappresentare in modo esatto nella forma binaria, e che pertanto dentro alla variabile di tipo float viene messo un valore arrotondato o troncato in modo più o meno prevedibile. La conversione inversa, cioè dalla forma binaria alla forma decimale, è invece sempre possibile in modo esatto. Questo almeno in teoria. In pratica non ho trovato modo di stampare esattamente fino all'ultimo bit il contenuto di un float. Neanche con printf("%.99F", 0.1) si riescono a stampare tutte le cifre decimali che dovrebbero apparire. Infatti nonostante la richiesta di 99 decimali, il risultato presentato ne mostra solo 30:
printf("%.99F", 0.1);
==> 0.1000000000000000055511151231257827021182
invece di mostrare quello che il valore float effettivamente contiene
rappresentato in base dieci, che dovrebbe invece essere:
==> 0.1000000000000000055511151231257827021181583404541015625
A causa di ciò, neppure la conversione da float a stringa può essere eseguita in modo esatto. NOTA: per completezza, in linguaggio C la funzione printf("%.90f", 0.1) stampa proprio il valore qui sopra, per cui il problema sembra legato al solo PHP che implementa male il descrittore di formato.
Conversione esatta da float a string.
L'unico modo che ho trovato per convertire un numero float in una stringa che
rappresenti in modo esatto il valore float è quello di sfruttare la funzione
/*. string .*/ function float2string(/*. float .*/ $f) { $old_precision = ini_set("serialize_precision", "100"); $s = serialize($f); ini_set("serialize_precision", $old_precision); return substr($s, 2, strlen($s) - 3); }
Questa funzione ritorna la rappresentazione decimale del numero float con
precisione fino all'ultimo bit disponibile. Per esempio
|
La morale: non è possibile eseguire calcoli monetari in Euro usando i float perché ci sono troppe insidie che è difficile evitare. Nelle discussioni sul newsgroup it.comp.www.php avvenute qualche tempo fa, grazie al contributo di vari partecipanti, siamo pervenuti ad alcune soluzioni possibili che ora esaminiamo.
Cerco ora di riassumere il discorso di prima illustrando un esempio concreto. Ecco cosa succede realmente quando il PHP esegue una istruzione semplice semplice come questa:
printf("%.99F", 0.1);
Come tutti sappiamo, l'interprete PHP esegue prima il parsing del sorgente e
poi ne esegue il codice. Sebbene il PHP sia un linguaggio debolmente tipizzato,
tuttavia internamente ogni valore e ogni variabile ha in realtà un tipo. Nel
caso del valore 0.1 esso viene quindi convertito in un numero in virgola mobile
di tipo float, che per chi mastica di linguaggio C è in realtà un
double
. Sulla maggior parte delle macchine questo tipo di dato usa
la rappresentazione IEEE 754 dei numeri in virgola mobile. La rappresentazione
IEEE 754 prevede 53 bit di mantissa. Ora, il numero decimale 0.1 ha una
rappresentazione binaria non finita e periodica:
0.1dec = 0.0001100110011001100110011001100...bin
Come si vede, la sequenza di bit 1100 si ripete all'infinito da un certo punto in poi. Siccome in un float sono disponibili solo 53 bit, bisogna decidere cosa fare dei bit in eccesso. Siccome dopo il 53-esimo bit c'è un altro 1, il PHP sceglie la strada dell'arrotondamento in eccesso:
Posizione dei bit della mantissa: 0 0 1 1 2 2 3 3 4 4 5 5 0 5 0 5 0 5 0 5 0 5 0 2 ----------------------------------------------------- esatto: 0.0001100110011001100110011001100110011001100110011001100110011... float : 0.00011001100110011001100110011001100110011001100110011010
L'arrotondamento in eccesso ha prodotto un numero che è la migliore
approssimazione possibile del valore 0.1. Tuttavia si tratta di un valore
diverso e leggermente superiore a 0.1. Ce ne accorgiamo quando andiamo
a stampare il numero con printf()
ottenendo un sorprendente
0.1000000000000000055511151231257827021182
Questo output è doppiamente sorprendente. Primo, perché non abbiamo ottenuto
il valore 0.1 che avevamo inserito nel sorgente. Secondo, perché i numeri
float binari si possono sempre convertire in modo esatto nella rappresentazione
decimale, ma la rappresentazione decimale del numero binario che abbiamo detto
non è quella data da printf()
, ma dovrebbe essere invece
0.1000000000000000055511151231257827021181583404541015625
Questo lunghissimo numero l'ho ottenuto (faticosamente...) in diversi modi, sia
direttamente da PHP estraendo con cura i bit uno a uno dal numero float, sia
dal linguaggio C, che invece stampa il numero atteso, sia operando con un
programma calcolatore a precisione arbitraria (BigFloat). Quindi, all'errore
introdotto dal parser PHP, che è previsto, si aggiunge un altro oscuro errore
introdotto nella printf()
che stampa un numero minore di decimali
rispetto ai 99 richiesti.
Quadro riassuntivo. Il numero decimale x≡0.1 ha un numero infinito di cifre nella rappresentazione binaria. Con la rappresentazione IEEE 754 in doppia precisione, che usa 53 bit, i due numeri più vicini ad x sono a,b indicati qui sotto: a ≡ 0.00011001100110011001100110011001100110011001100110011001 (bin) ≡ 0.09999999999999999167332731531132594682276248931884765625 (dec) x ≡ 0.000110011001100(1100)... (bin) ≡ 0.1 (dec) b ≡ 0.00011001100110011001100110011001100110011001100110011010 (bin) ≡ 0.10000000000000000555111512312578270211815834045410156250 (dec) Si può osservare che b approssima meglio x, ed è quindi il valore arrotondato più adatto a rappresentare x. |
Pertanto, quando si desiderano calcoli esatti, oppure quando occorre tenere sotto controllo la precisione dei risultati, i numeri float del PHP vanno usati con estrema cautela, oppure vanno abbandonati del tutto.
I numeri in precisione float si possono invece usare quando sono coinvolti calcoli tecnici e scientifici, ma si tratta di casi in cui in generale si preferisce usare altri linguaggi più adatti. In tutti gli altri casi, anche se si tratta solo di riportare, per esempio, la distanza chilometrica tra due città o le dimensioni di un tavolo, usare i float espone al richio di trovare qualche sorpresa.
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'è 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. La scelta della precisione dei calcoli e della modalità di arrotondamento dipende dai requisiti stabiliti per la nostra applicazione. Un esempio elementare dove uso due decimali:
$imponibile = "1234.56"; # Euro # calcola l'IVA 20% con 3 decimali: bcscale(3); $iva = bcmul($imponibile, "0.20"); # arrotondo l'IVA a 2 decimali: bcscale(2); $iva = bcadd( $iva, "0.005" ); # calcola il totale a 2 decimali: bcscale(2); $totale = bcadd($imponibile, $iva); # stampa la fattura: echo "Imponibile = $imponibile\n"; echo "IVA 20% = $iva\n"; echo "TOTALE = $totale\n";
che produce in output:
Imponibile = 1234.56 IVA 20% = 246.91 TOTALE = 1481.47
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 999'999'999,99 Euro (un miliardo di Euro meno un centesimo). Al livello del DB la rappresentazione di questi numeri è come stringhe di caratteri o comunque in forma decimale (non binaria), per cui non avviene mai la conversione da decimale a binario. Gli importi vengono poi restituiti al programma come stringhe.
Non sempre l'estensione BCMath è disponibile nel server dove il nostro programma dovrà girare, oppure decidiamo da subito di rendere il nostro programma indipendente dalla sua presenza. La classe BigFloat (http://www.icosaedro.it/phplint/phplint2/doc/stdlib/it/icosaedro/bignumbers/BigFloat.html) fa al caso nostro. BigFloat, come il nome suggerisce, permette di eseguire i calcoli con numeri a virgola mobile senza limiti di lunghezza né di precisione. Ecco come l'esempio di prima si trasforma usando questa classe:
require_once "BigFloat.php"; $imponibile = new BigFloat("1234.56"); # Euro # calcola l'IVA 20%: $iva = $imponibile ->mul( new BigFloat("0.20") ) # applica 20% ->round(-2); # arrotonda a due decimali # calcola il totale: $totale = $imponibile->add($iva); # stampa la fattura: echo "Imponibile = ", $imponibile->format(2), "\n"; echo "IVA 20% = ", $iva->format(2), "\n"; echo "TOTALE = ", $totale->format(2), "\n";
che produce in output:
Imponibile = 1,234.56 IVA 20% = 246.91 TOTALE = 1,481.47
Ricordare che il DB arrotonda silentemente i decimali a quelli previsti
(2 nel nostro esempio con numeric(11,2)
). Meglio controllare
da programma che i numeri abbiano la precisione necessaria come abbiamo
fatto qui, e scrivere i numeri nel DB già troncati o arrotondati secondo
il criterio scelto.
Infine, ricordare che il DB dà un errore se si tenta di scrivere un numero
più grande del massimo previsto. Ad esempio, se il il campo è di tipo
numeric(11,2)
allora dovremo controllare il range con un codice
simile a questo:
if( $n->abs()->cmp( new BigFloat("999999999.99") ) > 0 ){ echo "$n è troppo grande per il mio DB!"; }
Due osservazioni:
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 a 2^31-1 = 2147483647 centesimi ovvero oltre 21 milioni di Euro, il PHP attiva la rappresentazione float che ci permette fino a 14 cifre, con le quali possiamo rappresentare in modo esatto fino a mille miliardi di Euro. Per i computer che usano la rappresentazione IEEE 754 in doppia precisione il massimo è 2^53-1 = 9007199254740991 centesimi ovvero oltre 90 mila miliardi di Euro.
Difetto 1: per numeri superiori a questo limite 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 è fissata dal numero di cifre significative dei float, e quindi non può essere controllata; rimane comunque una precisione elevatissima. Dunque dobbiamo stare attenti che nei calcoli intermedi di una espressione non venga mai superato il limite che abbiamo detto, qualunque sia il valore degli operandi coinvolti.
Difetto 3: la conversione da numero float a stringa dipende dal parametro di configurazione "precision" impostato nel file di configurazione di PHP, che di norma si chiama php.ini. Questa conversione avviene sia esplicitamente che implicitamente, a volte in modo inatteso:
echo $f
oppure con print $f
(string) $f
"Totale:
$f"
strlen($f)
serialize()
un oggetto o un array che
contengono dei float, e quando, alla fine dello script, viene salvata la
sessione e abbiamo messo dei float dentro a $_SESSION[]
NOTA. A partire da PHP 4.3.2 esiste un nuovo parametroserialize_precision
specifico per il processo di serializzazione impostato a 100 per default che garantisce la perfetta conversione del numero float nella sua rappresentazione decimale equivalente. Per esempio,serialize(0.1)
produce esattamente l'atteso"d:0.1000000000000000055511151231257827021181583404541015625;"
. Quindi, almeno con le versioni recenti di PHP, la funzioneunserialize(serialize($f))
è una identità perfetta anche per i numeri float.
In tutti questi casi avviene la conversione da float a stringa usando
il numero di cifre di precisione indicato dal parametro di configurazione
precision
.
Se la conversione di un float in stringa contiene un numero di cifre
maggiore di questo valore, si attiva la rappresentazione scientifica
x.xxxxE+xx
che di norma non ha molto senso per i calcoli
monetari.
Il valore default di questo parametro è solo 14, mentre l'IEEE 754 ci
permette di arrivare a 16. Conviene impostare questo parametro a 16 nel
php.ini, oppure a run-time usare ini_set()
come nell'esempio
qui sotto.
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 si scrivono alcune funzioncine apposite.
Vediamo come si presenta il calcolo della fattura di prima:
ini_set("precision", "16"); $imponibile = 123456; # = 1'234,56 Euro $iva = floor( $imponibile * 0.20 + 0.5 ); $totale = $imponibile + $iva; # stampa la fattura: echo "Imponibile = $imponibile centesimi di Euro\n"; echo "IVA 20% = $iva centesimi di Euro\n"; echo "TOTALE = $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
number_format2()
riportata in appendice.
Per la validazione dell'input dell'utente scriveremo un'altra funzioncina
ValidaEuro()
, e per la conversione del valore inserito
dall'utente (espresso in Euro) nella nostra rappresentazione in centesimi
useremo un'altra funzioncina ancora Euro2Cent()
. 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 perché i numeri sono rappresentati in forma binaria.
Questa è una variante della soluzione 2 dove ancora rappresentiamo
gli importi monetari in centesimi.
La classe BigInt (http://www.icosaedro.it/phplint/phplint2/doc/stdlib/it/icosaedro/bignumbers/BigInt.html)
permette calcoli con numeri interi di arbitraria lunghezza e quindi
può sostituire in modo affidabile i nostri float
. Con BigInt
possiamo eseguire calcoli con numeri molto lunghi senza paura di perdere
precisione e senza timore di sforare un limite massimo, per quando grande
esso sia.
Riprendiamo il calcolo della nostra fattura ancora una volta:
require_once "BigInt.php"; $imponibile = new BigInt("123456"); # 1'234.56 Euro $iva = $imponibile ->mul( new BigInt(20) ) # applica IVA 20% ->add( new BigInt(50) ) # arrotonda ->div( new BigInt(100) ); # riscala $totale = $imponibile->add($iva); echo "Imponibile = ", $imponibile->format(2), "\n"; echo "IVA 20% = ", $iva->format(2), "\n"; echo "TOTALE = ", $totale->format(2), "\n";
Sembra un po' complicato, ma bisogna tenere conto che in un tipico programma gestionale non ci sono mai formule molto lunghe, ci si limita a qualche somma e moltiplicazione, rare sottrazioni e divisioni. Inoltre la frazione di codice che si occupa del calcolo numerico è tipicamente infinitesimale rispetto alla gran mole del codice circostante, che si occupa dell'interfaccia utente, della validazione, dell'accesso alla sessione e al data base, ecc.
Determinare la precisione del proprio server.
Le versioni recenti di PHP definiscono le costanti PHP_INT_SIZE
(tipicamente 4) che dà il numero di byte per int
e
PHP_INT_MAX
(tipicamente 2147483647) che dà il massimo intero
positivo (il massimo intero negativo è -PHP_INT_MAX - 1
),
ma nulla è previsto per determinare la precisione dei float
.
Il programmino che segue permette di determinare "sperimentalmente" la
precisione della rappresentazione int
e float
del proprio interprete PHP. Il risultato tipico sarà 32 bit nel primo caso
e 53 bit nel secondo. Quindi il massimo numero positivo intero è 2^31-1 =
2'147'483'647 mentre il massimo numero positivo senza perdita di precisione
in rappresentazione floating-point è 2^53-1 = 9'007'199'254'740'991.
<?php /*. int .*/ function int_size() { $i = 1; $n = 1; while( $i > 0 ){ $i = $i << 1; $n++; } return $n; } /*. int .*/ function float_mantissa_size() { $n = 1; $eps = 0.5; while( 1.0 + $eps > 1.0 ){ $n++; $eps *= 0.5; } return $n; } echo "Size of int: ", int_size(), " bits\n"; echo "Mantissa float: ", float_mantissa_size(), " bits\n"; ?>
Formattare i numeri lunghi.
Sia che adottiamo la soluzione 1 o la soluzione 2, i numeri risultanti
sono sempre delle stringhe che dovremo formattare adeguatamente prima di
presentarle all'utente. Di solito occorre mettere la virgola per separare
i decimali dalla parte intera e separare in qualche modo le migliaia
per rendere più leggibili i numeri grandi. Allo scopo sembra utile
la funzione number_format()
, ma questa accetta solo numeri
float "convenzionali" e quindi non va bene né per la soluzione 1 (che usa
stringhe di arbitraria lunghezza), né per la 2 (che usa sì float, ma si
tratta di centesimi).
Esiste anche la funzione money_format()
, ma questa non è
disponibile su Windows. Non rimane che scrivere una funzione apposita. Quella
qui sotto formatta numeri passati come stringa di cifre e privi di punto
decimale, per cui è adatta alla soluzione 2:
/*. string .*/ function number_format2( /*. string .*/ $num, $decimals = 2, $dec_point = ".", $thousands_sep = ",") { $l = strlen($num); if( $l <= $decimals ) return "0" . $dec_point . str_repeat("0", $decimals - $l) . $num; $d = $l - $decimals; # offset of the decimal point if( empty($thousands_sep) ) return substr($num, 0, $d) . $dec_point . substr($num, $d); $s = ""; $i = $d; do { $j = $i - 3; if( $j < 0 ) $j = 0; $s = substr($num, $j, $i - $j) . (empty($s)? "" : ($thousands_sep . $s)); $i = $j; } while( $i > 0 ); return $s . $dec_point . substr($num, $d, $l - $d); }
Gli argomenti della funzione sono simili a quelli di
number_format()
. $num è una stringa che contiene una lista di
cifre, almeno una cifra è richiesta. $decimals è il numero di cifre finali
che sono decimali, e deve valere almeno 1. $dec_point e $thousands_sep
sono il simbolo di punto decimale e il simbolo per separare le migliaia.
Ad esempio, per formattare gli importi alla maniera italiana useremo
due decimali, la virgola e un apicetto:
echo number_format2("123400", 2, ",", "'"); ==> 1'234,00
Notare che l'argomento $num deve proprio essere una stringa: se gli diamo un numero float la funzione dovrà eseguire una conversione a stringa OGNI volta che nell'algoritmo viene usato il suo valore! Questo non sarebbe molto efficiente. Se si adotta la soluzione 2, ricordare di impostare la precisione a 16 cifre, se supportata dal proprio sistema.
Le soluzioni che ho originano da un fruttuoso scambio di idee avvenuto sul newsgroup it.comp.www.php (v. articolo con subject "Far di conto in Euro col PHP (v. 1)" del marzo 2003). Questo documento è una versione riveduta ed estesa di quell'articolo.
Grazie a Marco Natoni (soluzione 1) e a Jurgen Schwietering (soluzione 2) per avere suggerito queste soluzioni. Io avevo posto solo il problema!
Umberto Salsi | Commenti | Contatto | Mappa | Home / Indice sezione |
An abstract of the latest comments from the visitors of this page follows. Please, use the Comments link above to read all the messages or to add your contribute.
2007-04-22
by Umberto Salsi
Re: formattazione di valute
Ho aggiornato l'articolo, ecco i cambiamenti: * Ho aggiunto la funzione number_format2() adatta per formattare i numeri lunghi. * Ho riportato il valore massimo intero dell'IEEE 754. * Massimo intero IEEE 754 è 2^53-1 e non 2^52-1 come era scritto prima. * Per la soluzione 2, richiamo su ini_set("precision", 16).[more...]
2006-07-07
by Umberto Salsi
Re: formattazione di valute
Anonymous wrote:
[...]
number_format accetta come argomento un float che rappresenta l'importo in Euro, quindi non lo si puo' usare ne' con la libreria BCMath, ne' con la seconda soluzione che usa i centesimi. Pero' sarebbe effettivamente utile realizzare una funzione analoga con gli stessi parametri da usare nelle due soluzioni esaminate nell'articolo. Volontari?[more...]
2006-07-04
by Guest
formattazione di valute
e' molto comodo usare number_format($numero, $numeroCifre, $separatoreCentesimi, $seperatoreMigliaia);
un esempio per formattare le valute in italia
echo number_format(63.745, 2, ',', "");[more...]
2005-03-10
by Umberto Salsi
Euro e dollari
Ovviamente, le considerazioni qui fatte sull'Euro valgono anche per il Dollaro e le altre monete che usano numeri frazionari, che hanno lo stesso problema.[more...]