Torna al Blog

Regole di arrotondamento valutario per sviluppatori: decimali, unità minori e conversioni sicure

V
Vlado Grigirov
August 21, 2026
Currency API Exchange Rates Currency Rounding Minor Units ISO 4217 Finexly Developer Guide

Un cliente a Tokyo acquista un abbonamento da 19,99 $. Il tuo codice moltiplica per il tasso USD/JPY, ottiene 2942,82785, lo scrive nel database e lo invia al tuo processore di pagamento. Il processore lo rifiuta — o peggio, lo accetta e addebita 100 volte tanto. Lo yen non ha decimali, e il tuo codice non l'ha mai chiesto.

L'arrotondamento valutario è uno di quei problemi che sembrano banali finché non arrivano in produzione. Non è una questione di formattazione, è una questione di correttezza. Ogni valuta ha il proprio numero di cifre decimali, l'aritmetica in virgola mobile corrompe silenziosamente gli importi, e nel momento in cui converti tra valute introduci una decisione di arrotondamento che va presa deliberatamente. Questa guida copre le regole che contano davvero: quanti decimali ha ogni valuta, perché conservare gli importi come interi, quale modalità di arrotondamento scegliere e come arrotondare una conversione FX perché il tuo libro mastro torni a fine mese.

Unità minori: quanti decimali ha ogni valuta?

L'unità minore di una valuta è la sua più piccola suddivisione transazionabile. Per il dollaro statunitense è il cent, quindi USD ha due decimali e 19,99 $ sono 1999 cent. È la rappresentazione che i processori di pagamento si aspettano, ed è quella che il tuo database dovrebbe usare.

ISO 4217 — lo stesso standard che fornisce i codici a tre lettere come USD e JPY — assegna a ogni valuta anche un esponente di unità minore. La maggior parte degli sviluppatori dà per scontato che quell'esponente sia sempre 2. Non lo è, e questa assunzione è il bug più costoso della categoria.

Valute senza decimali

Queste valute non hanno una sottounità in circolazione, quindi l'importo che invii è il numero intero di unità:

  • JPY — yen giapponese
  • KRW — won sudcoreano
  • VND — dong vietnamita
  • CLP — peso cileno
  • ISK — corona islandese
  • XAF / XOF / XPF — franchi CFA e CFP
  • UGX — scellino ugandese
  • PYG — guaraní paraguaiano
  • RWF, GNF, KMF, DJF, VUV — e diverse altre valute di piccolo taglio

Se tratti lo JPY come valuta a due decimali e moltiplichi per 100 prima di inviarlo al processore, hai appena addebitato al cliente 100 volte l'importo previsto.

Valute con tre decimali

Sette valute si suddividono in millesimi anziché in centesimi:

  • KWD — dinaro kuwaitiano (1000 fils)
  • BHD — dinaro del Bahrein (1000 fils)
  • OMR — rial omanita (1000 baisa)
  • JOD — dinaro giordano (1000 fils)
  • TND — dinaro tunisino (1000 millesimi)
  • IQD — dinaro iracheno (1000 fils)
  • LYD — dinaro libico (1000 dirham)

Qui l'errore va nella direzione opposta: tratta il KWD come due decimali e addebiti un decimo di quanto volevi. Una fattura da KWD 12,500 diventa KWD 1,250.

In ISO 4217 esistono persino voci a quattro decimali — l'unidad de fomento cilena (CLF) e l'unidad previsional uruguaiana (UYW). Sono unità contabili indicizzate più che contante, ma se il tuo sistema accetta codici ISO arbitrari deve sopravvivere anche a queste.

Quando lo standard e il tuo processore divergono

È la trappola che frega i team che hanno fatto tutto il resto correttamente. I provider di pagamento a volte si discostano da ISO 4217 per ragioni operative. Adyen, per esempio, documenta che CLP, CVE, IDR e ISK usano nella sua API un numero di decimali diverso da quello dello standard: ISK ha zero decimali secondo ISO 4217, ma va inviata con due decimali ad Adyen.

La regola: la tua tabella di arrotondamento è una proprietà del sistema con cui stai parlando, non una costante universale. Tieni una tabella per integrazione, inizializzala da ISO 4217 e sovrascrivila per provider dove la loro documentazione lo richiede. Non scrivere mai 100 in modo fisso.

Non conservare mai il denaro come float

Prima di qualsiasi discussione sull'arrotondamento, le fondamenta. La virgola mobile binaria non può rappresentare esattamente la maggior parte delle frazioni decimali:

0.1 + 0.2              // 0.30000000000000004
1.005 * 100            // 100.49999999999999
19.99 * 147.2150       // 2942.8278499999997

Quelle cifre finali non sono estetica. Passale in Math.round() nel momento sbagliato e ottieni un importo sbagliato di un'unità minore, il che basta a far fallire la riconciliazione.

Due regole coprono quasi ogni caso:

  1. Conserva gli importi come interi in unità minori. Una colonna amount_minor BIGINT più una colonna currency CHAR(3). { amount_minor: 1999, currency: "USD" } è inequivocabile e corrisponde a ciò che Stripe, Adyen e la maggior parte dei processori già si aspettano.
  2. Fai l'aritmetica con interi o con un tipo decimale. decimal.Decimal di Python, BigDecimal di Java, NUMERIC di PostgreSQL, oppure una libreria monetaria JavaScript che incapsula aritmetica intera. Riserva i float al tasso di cambio stesso, e anche allora solo fino al punto della moltiplicazione.

Se stai progettando questo strato da zero, la nostra guida al design di un libro mastro multivaluta approfondisce le decisioni di schema.

La pipeline di conversione: unità minori in ingresso, unità minori in uscita

La conversione valutaria ha esattamente quattro passi, e l'arrotondamento appartiene al passo tre — una sola volta, alla fine.

  1. Converti l'importo di partenza da unità minori a un valore decimale.
  2. Moltiplica per il tasso di cambio a precisione piena.
  3. Arrotonda all'esponente di unità minore della valuta di destinazione.
  4. Riconverti in unità minori intere.

Ecco la versione JavaScript, dove è la tabella per valuta a fare il lavoro:

// Minor unit exponents. Seed from ISO 4217, override per payment provider.
const MINOR_UNITS = {
  USD: 2, EUR: 2, GBP: 2, CHF: 2, CAD: 2, AUD: 2, CNY: 2, INR: 2,
  JPY: 0, KRW: 0, VND: 0, CLP: 0, ISK: 0, XAF: 0, XOF: 0, XPF: 0,
  KWD: 3, BHD: 3, OMR: 3, JOD: 3, TND: 3, IQD: 3, LYD: 3,
};

function exponentFor(currency) {
  const e = MINOR_UNITS[currency];
  if (e === undefined) throw new Error(`Unknown minor unit for ${currency}`);
  return e;
}

/**
 * Convert an integer minor-unit amount from one currency to another.
 * Returns an integer in the target currency's minor units.
 */
function convertMinor(amountMinor, from, to, rate) {
  const fromExp = exponentFor(from);
  const toExp = exponentFor(to);

  const decimalAmount = amountMinor / 10 ** fromExp;   // 1999 -> 19.99
  const converted = decimalAmount * rate;              // full precision, no rounding yet
  return Math.round(converted * 10 ** toExp);          // single rounding step
}

convertMinor(1999, "USD", "JPY", 147.2150);   // 2943      (¥2,943)
convertMinor(1999, "USD", "KWD", 0.30590);    // 6115      (KWD 6.115)
convertMinor(1999, "USD", "EUR", 0.9241);     // 1847      (€18.47)

Nota cosa la funzione non fa: non arrotonda mai il tasso, non arrotonda mai un valore intermedio e non presume mai due decimali. Math.round qui è half-up sui positivi — va bene per un checkout, ma leggi la sezione successiva prima di usarlo per qualcosa di regolamentato.

Scegliere una modalità di arrotondamento

«Arrotondare a due decimali» non è una specifica. Ci sono almeno cinque modi difendibili di sciogliere un pareggio, e ai sistemi finanziari importa quale scegli.

Modalità2,5 →3,5 →−2,5 →Uso tipico
Half up34−3Prezzi al consumo, totali di checkout
Half even (del banchiere)24−2Contabilità, interessi, imposte, reportistica
Half down23−2Raro; talvolta in codice finanziario legacy
Ceiling (per eccesso)34−2Commissioni che non devi mai incassare in difetto
Floor (per difetto / troncamento)23−3Pagamenti che non devi mai erogare in eccesso
Half up è ciò che la maggior parte delle persone intende per «arrotondare» ed è quello che Math.round() fa sui numeri positivi. È intuitivo e adatto a un prezzo che il cliente sta per vedere.

Half even, detto anche arrotondamento del banchiere, manda le metà esatte alla cifra pari più vicina. Su molte transazioni annulla la distorsione sistematica verso l'alto introdotta dall'half-up, motivo per cui è il default nei sistemi contabili, nel modulo decimal di Python e nello stesso IEEE 754. Se aggreghi migliaia di importi convertiti in un report di ricavi, l'half-up gonfierà silenziosamente il totale; l'half-even no.

Ceiling e floor esistono per rischi asimmetrici. Un marketplace che paga i venditori può arrotondare per difetto ogni pagamento per non distribuire mai più di quanto detiene; la differenza finisce su un conto arrotondamenti.

Python rende la scelta esplicita, il che è l'ergonomia corretta:

from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP

MINOR_UNITS = {"USD": 2, "EUR": 2, "JPY": 0, "KWD": 3}

def convert_minor(amount_minor: int, src: str, dst: str,
                  rate: str, mode=ROUND_HALF_EVEN) -> int:
    """Convert integer minor units to integer minor units, exactly once."""
    src_exp, dst_exp = MINOR_UNITS[src], MINOR_UNITS[dst]

    amount = Decimal(amount_minor) / (Decimal(10) ** src_exp)
    converted = amount * Decimal(rate)          # rate passed as a string, not a float

    quantum = Decimal(1).scaleb(-dst_exp)       # 0.01, 1, or 0.001
    rounded = converted.quantize(quantum, rounding=mode)
    return int(rounded.scaleb(dst_exp))

convert_minor(1999, "USD", "JPY", "147.2150")   # 2943
convert_minor(1999, "USD", "KWD", "0.30590")    # 6115

Passare il tasso a Decimal come stringa conta. Decimal(0.9241) eredita l'errore del float; Decimal("0.9241") no.

Tre bug di arrotondamento che costano soldi veri

1. Arrotondare il tasso prima di moltiplicare

I tassi di cambio portano abitualmente da quattro a sei decimali significativi, e troncarli non è innocuo. Prendi USD/JPY a 147,2150 e un bonifico da 10.000 $:

  • Tasso completo: 10000 × 147.2150 = ¥1.472.150
  • Tasso arrotondato a due decimali (147,21): 10000 × 147.21 = ¥1.472.100

Una discrepanza di ¥50 su una singola transazione, solo per aver formattato il tasso prima di usarlo. Conserva il tasso alla precisione che il tuo provider restituisce, arrotonda solo l'importo risultante e persisti il tasso esatto usato insieme alla transazione per l'audit. La nostra guida su da dove le API di cambio prendono i dati spiega perché quella precisione ha senso in partenza.

2. Arrotondare due volte in una conversione a più tratte

Se instradi USD → EUR → JPY e arrotondi al passaggio EUR, hai buttato via precisione che la seconda moltiplicazione poi amplifica. Convertendo 12,34 $ con USD/EUR a 0,9241 ed EUR/JPY a 159,3063:

  • Diretto: 12.34 × 147.2150 = 1816.63¥1.817
  • Tramite una tratta EUR arrotondata: 12.34 × 0.9241 = 11.4034 → arrotondato a 11,40 € → 11.40 × 159.3063 = 1816.09¥1.816

Uno yen, per un passaggio di arrotondamento superfluo. Su un ciclo di pagamenti da cinquantamila transazioni, quello diventa un ticket di riconciliazione. Quando esiste una coppia diretta, usala; quando devi triangolare, mantieni l'intermedio a precisione piena. Vedi i tassi incrociati spiegati per la meccanica.

3. Righe che non fanno il totale

Arrotonda ogni riga di una fattura in modo indipendente e le parti non sempre daranno il totale arrotondato. Il caso classico è una divisione:

$10.00 split three ways
  10.00 / 3 = 3.3333...
  → 3.33 + 3.33 + 3.33 = 9.99   ✗ one cent missing

La soluzione è l'allocazione, non l'arrotondamento. Arrotonda il totale una volta, poi distribuiscilo tra le parti assegnando il resto un'unità minore alla volta:

/**
 * Split an integer minor-unit total into `n` parts whose sum is exactly the total.
 * Remainder units are distributed to the earliest parts (largest-remainder method).
 */
function allocate(totalMinor, ratios) {
  const sum = ratios.reduce((a, b) => a + b, 0);
  const shares = ratios.map(r => Math.floor((totalMinor * r) / sum));
  let remainder = totalMinor - shares.reduce((a, b) => a + b, 0);

  for (let i = 0; remainder > 0; i = (i + 1) % shares.length, remainder--) {
    shares[i] += 1;
  }
  return shares;
}

allocate(1000, [1, 1, 1]);   // [334, 333, 333]        → sums to exactly 1000
allocate(9247, [3, 2, 1]);   // [4624, 3082, 1541]     → sums to exactly 9247

Applica lo stesso schema dopo una conversione FX: converti e arrotonda il totale fattura, poi alloca quel totale tra le righe. Le righe torneranno sempre, perché derivano dal totale invece di essere calcolate in modo indipendente. Conta soprattutto nella fatturazione multivaluta e nel billing SaaS con rateo, dove uno scarto di un centesimo finisce su un PDF che il cliente vede.

L'arrotondamento del contante è una regola a parte

L'unità minore di una valuta indica l'importo più piccolo che può essere registrato. Non sempre indica l'importo più piccolo che può essere pagato in contanti. Diversi paesi hanno ritirato le monete più piccole e arrotondano i pagamenti in contanti alla cassa:

  • Svizzera — il contante si arrotonda ai 0,05 CHF più vicini
  • Canada — la moneta da un cent è stata ritirata nel 2013; il contante si arrotonda ai 5 cent più vicini
  • Svezia — il contante si arrotonda alla corona intera più vicina
  • Paesi Bassi — il contante si arrotonda ai 5 centesimi più vicini

Fondamentale: vale per il pagamento in contanti, non per la fattura. Una fattura svizzera da CHF 12,32 resta registrata a 12,32; solo il regolamento in contanti arrotonda a 12,30, e la differenza di 0,02 viene contabilizzata come rettifica di arrotondamento. Se sviluppi software per punti vendita, modella l'arrotondamento del contante come un passaggio separato e successivo applicato al pagamento — non incorporarlo mai nell'importo memorizzato, o le transazioni elettroniche e quelle in contanti divergeranno.

La formattazione è l'ultimo passo, non il calcolo

Terminata l'aritmetica, affida la visualizzazione a un formattatore consapevole del locale. Intl.NumberFormat conosce già decimali, posizione del simbolo e separatori di ogni valuta:

function formatMinor(amountMinor, currency, locale = "en-US") {
  const exp = exponentFor(currency);
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(amountMinor / 10 ** exp);
}

formatMinor(1999, "USD");            // "$19.99"
formatMinor(2943, "JPY", "ja-JP");   // "¥2,943"
formatMinor(6115, "KWD");            // "KWD 6.115"
formatMinor(1847, "EUR", "de-DE");   // "18,47 €"

Due note pratiche. Primo, le istanze di Intl.NumberFormat sono costose da costruire — tienine una in cache per coppia locale-valuta invece di crearne una per riga. Secondo, la divisione per 10 ** exp nell'ultima riga è l'unico punto in cui un float dovrebbe toccare un valore monetario, e solo perché il risultato diventa immediatamente una stringa.

Mettere tutto insieme con l'API Finexly

Recupera il tasso a precisione piena, converti una volta, arrotonda una volta e salva il tasso usato:

curl "https://api.finexly.com/v1/latest?base=USD&symbols=JPY,KWD,EUR" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "success": true,
  "base": "USD",
  "timestamp": 1755244800,
  "rates": {
    "JPY": 147.2150,
    "KWD": 0.30590,
    "EUR": 0.9241
  }
}
async function quote(amountMinor, from, to) {
  const res = await fetch(
    `https://api.finexly.com/v1/latest?base=${from}&symbols=${to}`,
    { headers: { Authorization: `Bearer ${process.env.FINEXLY_API_KEY}` } }
  );
  const data = await res.json();
  const rate = data.rates[to];

  return {
    amount_minor: convertMinor(amountMinor, from, to, rate),
    currency: to,
    rate,                              // persist the exact rate used
    rate_timestamp: data.timestamp,    // and when it was captured
  };
}

await quote(1999, "USD", "JPY");
// { amount_minor: 2943, currency: "JPY", rate: 147.215, rate_timestamp: 1755244800 }

Salvare rate e rate_timestamp sulla riga della transazione è ciò che rende una contestazione rispondibile sei mesi dopo. Tutti i dettagli su endpoint e parametri sono nella documentazione dell'API Finexly, e se metti in cache i tassi tra le richieste le nostre note su caching e gestione degli errori coprono i compromessi sulla freschezza.

Una checklist di test

I bug sul denaro si nascondono nei casi per cui nessuno scrive test. Come minimo, copri:

  1. Una destinazione senza decimali — converti in JPY o KRW e verifica che il risultato non abbia parte frazionaria.
  2. Una destinazione a tre decimali — converti in KWD o BHD e verifica che i tre decimali sopravvivano.
  3. Metà esatte — verifica la modalità di arrotondamento scelta, in entrambe le direzioni, negativi compresi.
  4. Deriva di andata e ritorno — converti USD → EUR → USD e verifica che il risultato sia entro un'unità minore, non che sia uguale.
  5. Invarianza dell'allocazione — verifica che le parti divise sommino sempre esattamente il totale, da 1 a 100 parti.
  6. Codici valuta sconosciuti — verifica che il codice sollevi un errore invece di ripiegare silenziosamente su due decimali.
  7. Importi molto grandi — verifica che non ci sia perdita di precisione oltre Number.MAX_SAFE_INTEGER in JavaScript; usa BigInt se gestisci IDR o VND su larga scala.

Domande frequenti

Quanti decimali ha ogni valuta?

La maggior parte ne ha due. Una ventina non ne ha nessuno — tra cui JPY, KRW, VND, CLP e ISK — e sette ne hanno tre: KWD, BHD, OMR, JOD, TND, IQD e LYD. ISO 4217 è la fonte autorevole, ma controlla anche la tabella del tuo provider di pagamento, perché alcuni se ne discostano per ragioni operative.

Devo arrotondare i tassi di cambio o gli importi convertiti?

Solo gli importi convertiti. Mantieni il tasso alla precisione piena restituita dal provider, moltiplica e poi arrotonda il risultato una sola volta all'unità minore della valuta di destinazione. Arrotondare un tasso prima di moltiplicare introduce un errore proporzionale alla dimensione della transazione.

Qual è la differenza tra half-up e arrotondamento del banchiere?

L'half-up allontana sempre da zero una metà esatta (2,5 → 3). L'arrotondamento del banchiere — metà al pari — la manda alla cifra pari più vicina (2,5 → 2, 3,5 → 4), il che elimina la distorsione sistematica verso l'alto quando aggreghi molti importi. Usa half-up per i prezzi mostrati ai clienti, half-even per contabilità e reportistica.

Perché le mie righe convertite non fanno il totale convertito?

Perché ogni riga è stata arrotondata in modo indipendente e gli errori si accumulano. Arrotonda il totale una volta, poi allocalo tra le righe con una ripartizione a resto maggiore. Le parti sommeranno il tutto per costruzione.

Posso semplicemente conservare il denaro come float con due decimali?

No. La virgola mobile binaria non rappresenta esattamente valori come 0,1, quindi gli errori si accumulano tra addizioni e moltiplicazioni e prima o poi ribaltano una decisione di arrotondamento. Conserva interi in unità minori, o usa un tipo decimale esatto. Non è una preoccupazione teorica: è la causa più comune dei fallimenti di riconciliazione da un centesimo.

Ottieni tassi a precisione piena

Un arrotondamento corretto parte da un tasso di cui ti puoi fidare e da una precisione che non hai buttato via. Ottieni la tua chiave API Finexly gratuita — senza carta di credito. Parti con 1.000 richieste gratuite al mese su oltre 170 valute, prova il convertitore di valuta per verificare i tuoi conti e guarda i piani tariffari quando il volume cresce.

Vlado Grigirov

Senior Currency Markets Analyst & Financial Strategist

Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...

View full profile →

Condividi questo articolo