Uno degli errori più comuni nella progettazione di un database è salvare prezzi e importi in una colonna di tipo FLOAT o DOUBLE. Il problema non si vede subito: i valori sembrano giusti, i report quadrano quasi sempre. Poi una fattura risulta sbagliata di un centesimo, un controllo di uguaglianza fallisce senza motivo apparente, una riconciliazione bancaria non torna. La causa è il modo in cui i computer rappresentano i numeri con la virgola.
Il problema della virgola mobile
I tipi FLOAT e DOUBLE seguono lo standard IEEE 754 e rappresentano i numeri in base 2. Molte frazioni che in base 10 sono semplici, come 0,1 o 0,2, in base 2 diventano periodiche, come 1/3 in base 10 diventa 0,333… Il computer le tronca a un numero finito di cifre binarie, e il valore memorizzato è un'approssimazione molto vicina, ma non uguale.
Un esempio che si può provare in quasi qualunque linguaggio:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Per i calcoli scientifici questa precisione è più che sufficiente. Per i soldi no: gli importi devono essere esatti al centesimo, le somme devono essere ripetibili e un totale deve essere uguale alla somma delle sue righe. Su migliaia di operazioni, i piccoli errori si accumulano e producono differenze visibili.
La soluzione: DECIMAL o NUMERIC
I tipi DECIMAL e NUMERIC, equivalenti nella maggior parte dei database, memorizzano i numeri in modo esatto in base 10. Si dichiarano con due parametri: la precisione, cioè il numero totale di cifre, e la scala, cioè le cifre dopo la virgola.
CREATE TABLE righe_fattura (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
descrizione TEXT NOT NULL,
quantita NUMERIC(10,3) NOT NULL,
prezzo_unitario NUMERIC(12,4) NOT NULL,
aliquota_iva NUMERIC(5,2) NOT NULL,
imponibile NUMERIC(12,2) NOT NULL
);
NUMERIC(12,2) accetta numeri fino a 10 cifre intere e 2 decimali. Le somme sono esatte: 0,10 più 0,20 fa esattamente 0,30.
Per il prezzo unitario può servire una scala maggiore di 2. Molti listini, soprattutto all'ingrosso o per carburanti e materiali venduti a peso, indicano prezzi con tre o quattro decimali. Il totale della riga, invece, si arrotonda al centesimo.
L'alternativa: centesimi interi
Un'altra tecnica diffusa è salvare gli importi come numeri interi di centesimi: 12,50 euro diventa 1250. Gli interi sono esatti, veloci e supportati ovunque, anche in linguaggi o formati che non hanno un tipo decimale.
Ha però alcuni svantaggi:
- bisogna ricordarsi sempre di dividere per 100 in visualizzazione e moltiplicare in inserimento;
- non tutte le valute hanno due decimali: lo yen giapponese non ne ha, alcune valute ne hanno tre;
- non si presta a prezzi unitari con più decimali.
Per un database gestionale NUMERIC è di solito la scelta più chiara. I centesimi interi sono comodi nelle interfacce tra sistemi e nelle applicazioni che trattano una sola valuta.
Non basta il database: anche il codice
Salvare correttamente non serve se l'applicazione legge il valore e lo converte in un float per fare i calcoli. Nei linguaggi di programmazione esistono tipi decimali appositi: in Python il modulo decimal, in Java BigDecimal, in C# decimal.
from decimal import Decimal, ROUND_HALF_UP
imponibile = Decimal("19.99") * 3
iva = (imponibile * Decimal("0.22")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(imponibile, iva) # 59.97 13.19
Si noti che il valore è creato da una stringa, Decimal("19.99"), e non da un numero, Decimal(19.99): in quel caso si erediterebbe l'approssimazione binaria del letterale in virgola mobile.
Arrotondamenti: decidere una regola
Esistono diverse regole di arrotondamento, e scegliere quella sbagliata produce differenze di un centesimo:
| Regola | 2,345 diventa | 2,355 diventa |
|---|---|---|
| Metà per eccesso (half up) | 2,35 | 2,36 |
| Metà al pari (half even, detto anche «del banchiere») | 2,34 | 2,36 |
| Troncamento | 2,34 | 2,35 |
Molti linguaggi, compreso Python con la funzione round, usano per impostazione predefinita l'arrotondamento al pari, mentre nelle fatture si applica di solito l'arrotondamento commerciale per eccesso. La regola va scelta, documentata e applicata in un unico punto del codice.
Riga per riga o sul totale?
Calcolare l'IVA su ogni riga e poi sommare può dare un risultato diverso di qualche centesimo rispetto al calcolo sull'imponibile totale. Entrambi gli approcci sono usati; l'importante è che il sistema segua sempre lo stesso metodo previsto dalle regole che l'azienda applica, e che il totale salvato corrisponda esattamente alla somma che compare sul documento.
Più valute
Se il sistema tratta più valute, l'importo da solo non basta: va salvato insieme al codice della valuta, per esempio con i codici ISO 4217 come EUR o USD. I tassi di cambio vanno conservati con la data a cui si riferiscono, e l'importo convertito va salvato al momento dell'operazione, non ricalcolato a posteriori con il tasso di oggi.
In sintesi
FLOATeDOUBLEapprossimano i decimali e non vanno usati per i soldi.DECIMAL/NUMERICsono esatti: si sceglie la scala in base all'uso, anche oltre due decimali per i prezzi unitari.- I centesimi interi sono un'alternativa valida, soprattutto con una sola valuta.
- Anche il codice applicativo deve usare tipi decimali, creati da stringhe.
- Regola di arrotondamento e metodo di calcolo dell'IVA vanno decisi e applicati in modo uniforme.