Home / Indice sezione
 www.icosaedro.it 

 Calcoli monetari con numeri frazionari in PHP

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.

Indice

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

Il problema

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 serialize(). Il risultato è questa funzioncina di utilità:

/*. 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 float2string(0.1) dà esattamente la stringa "0.1000000000000000055511151231257827021181583404541015625" come abbiamo detto.



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.

Il lungo viaggio da 0.1 a binario e ritorno

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.

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'è 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.

Soluzione 1b: usando la classe BigFloat

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!";
}

Soluzione 2: usando i float del PHP e facendo i conti in centesimi

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:

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.

Soluzione 2b: usando la classe BigInt

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.

Appendice

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.

Ringraziamenti

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!

Riferimenti


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...]