Java Money e l'API Currency

1. Panoramica

JSR 354 - "Currency and Money" riguarda la standardizzazione delle valute e degli importi monetari in Java.

Il suo obiettivo è aggiungere un'API flessibile ed estensibile all'ecosistema Java e rendere più semplice e sicuro il lavoro con importi monetari.

JSR non si è fatto strada in JDK 9 ma è un candidato per future versioni di JDK.

2. Configurazione

Per prima cosa, definiamo la dipendenza nel nostro file pom.xml :

 org.javamoney moneta 1.1  

L'ultima versione della dipendenza può essere verificata qui.

3. Caratteristiche JSR-354

Gli obiettivi dell'API "Currency and Money":

  • Fornire un'API per la gestione e il calcolo degli importi monetari
  • Per definire classi che rappresentano valute e importi monetari, nonché arrotondamenti monetari
  • Per gestire i tassi di cambio delle valute
  • Per gestire la formattazione e l'analisi di valute e importi monetari

4. Modello

Le classi principali della specifica JSR-354 sono illustrate nel diagramma seguente:

Il modello contiene due interfacce principali CurrencyUnit e MonetaryAmount, spiegate nelle sezioni seguenti.

5. CurrencyUnit

CurrencyUnit modella le proprietà minime di una valuta. Le sue istanze possono essere ottenute utilizzando il metodo Monetary.getCurrency :

@Test public void givenCurrencyCode_whenString_thanExist() { CurrencyUnit usd = Monetary.getCurrency("USD"); assertNotNull(usd); assertEquals(usd.getCurrencyCode(), "USD"); assertEquals(usd.getNumericCode(), 840); assertEquals(usd.getDefaultFractionDigits(), 2); }

Creiamo CurrencyUnit utilizzando una rappresentazione in stringa della valuta, questo potrebbe portare a una situazione in cui proviamo a creare una valuta con codice inesistente. La creazione di valute con codici inesistenti solleva un'eccezione UnknownCurrency :

@Test(expected = UnknownCurrencyException.class) public void givenCurrencyCode_whenNoExist_thanThrowsError() { Monetary.getCurrency("AAA"); } 

6. Importo monetario

MonetaryAmount è una rappresentazione numerica di un importo monetario. È sempre associato a CurrencyUnit e definisce una rappresentazione monetaria di una valuta.

L'importo può essere implementato in diversi modi, concentrandosi sul comportamento di un requisito di rappresentazione monetaria, definito da ogni caso d'uso concreto. Per esempio. Money e FastMoney sono implementazioni dell'interfaccia MonetaryAmount .

FastMoney implementa MonetaryAmount utilizzando una rappresentazione numerica long ed è più veloce di BigDecimal a scapito della precisione; può essere utilizzato quando abbiamo bisogno di prestazioni e la precisione non è un problema.

È possibile creare un'istanza generica utilizzando una factory predefinita. Mostriamo il modo diverso di ottenere istanze di MonetaryAmount :

@Test public void givenAmounts_whenStringified_thanEquals() { CurrencyUnit usd = Monetary.getCurrency("USD"); MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory() .setCurrency(usd).setNumber(200).create(); Money moneyof = Money.of(12, usd); FastMoney fastmoneyof = FastMoney.of(2, usd); assertEquals("USD", usd.toString()); assertEquals("USD 200", fstAmtUSD.toString()); assertEquals("USD 12", moneyof.toString()); assertEquals("USD 2.00000", fastmoneyof.toString()); }

7 . Aritmetica monetaria

Possiamo eseguire operazioni aritmetiche monetarie tra Money e FastMoney, ma dobbiamo fare attenzione quando combiniamo istanze di queste due classi.

Ad esempio, quando confrontiamo un'istanza in euro di FastMoney con un'istanza in euro di Money, il risultato è che non sono la stessa cosa:

@Test public void givenCurrencies_whenCompared_thanNotequal() { MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); Money oneEuro = Money.of(1, "EUR"); assertFalse(oneEuro.equals(FastMoney.of(1, "EUR"))); assertTrue(oneDolar.equals(Money.of(1, "USD"))); }

Possiamo eseguire operazioni di addizione, sottrazione, moltiplicazione, divisione e altre operazioni aritmetiche monetarie utilizzando i metodi forniti dalla classe MonetaryAmount .

Le operazioni aritmetiche dovrebbero generare un'eccezione ArithmeticException , se le operazioni aritmetiche tra importi superano le capacità del tipo di rappresentazione numerica utilizzato, ad esempio, se proviamo a dividere uno per tre, otteniamo un'eccezione ArithmeticException perché il risultato è un numero infinito:

@Test(expected = ArithmeticException.class) public void givenAmount_whenDivided_thanThrowsException() { MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); oneDolar.divide(3); }

Quando si aggiungono o sottraggono importi, è meglio utilizzare parametri che sono istanze di MonetaryAmount , poiché dobbiamo assicurarci che entrambi gli importi abbiano la stessa valuta per eseguire operazioni tra importi.

7.1. Calcolo degli importi

Un totale di importi può essere calcolato in più modi, un modo è semplicemente quello di concatenare gli importi con:

@Test public void givenAmounts_whenSummed_thanCorrect() { MonetaryAmount[] monetaryAmounts = new MonetaryAmount[] { Money.of(100, "CHF"), Money.of(10.20, "CHF"), Money.of(1.15, "CHF")}; Money sumAmtCHF = Money.of(0, "CHF"); for (MonetaryAmount monetaryAmount : monetaryAmounts) { sumAmtCHF = sumAmtCHF.add(monetaryAmount); } assertEquals("CHF 111.35", sumAmtCHF.toString()); }

Il concatenamento può essere applicato anche alla sottrazione:

Money calcAmtUSD = Money.of(1, "USD").subtract(fstAmtUSD); 

Moltiplicare:

MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);

O dividendo:

MonetaryAmount divideAmount = oneDolar.divide(0.25);

Let's compare our arithmetic results using Strings, given that with Strings because the result also contains the currency:

@Test public void givenArithmetic_whenStringified_thanEqualsAmount() { CurrencyUnit usd = Monetary.getCurrency("USD"); Money moneyof = Money.of(12, usd); MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory() .setCurrency(usd).setNumber(200.50).create(); MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); Money subtractedAmount = Money.of(1, "USD").subtract(fstAmtUSD); MonetaryAmount multiplyAmount = oneDolar.multiply(0.25); MonetaryAmount divideAmount = oneDolar.divide(0.25); assertEquals("USD", usd.toString()); assertEquals("USD 1", oneDolar.toString()); assertEquals("USD 200.5", fstAmtUSD.toString()); assertEquals("USD 12", moneyof.toString()); assertEquals("USD -199.5", subtractedAmount.toString()); assertEquals("USD 0.25", multiplyAmount.toString()); assertEquals("USD 4", divideAmount.toString()); }

8. Monetary Rounding

Monetary rounding is nothing else than a conversion from an amount with an undetermined precision to a rounded amount.

We'll use the getDefaultRounding API provided by the Monetary class to make the conversion. The default rounding values are provided by the currency:

@Test public void givenAmount_whenRounded_thanEquals() { MonetaryAmount fstAmtEUR = Monetary.getDefaultAmountFactory() .setCurrency("EUR").setNumber(1.30473908).create(); MonetaryAmount roundEUR = fstAmtEUR.with(Monetary.getDefaultRounding()); assertEquals("EUR 1.30473908", fstAmtEUR.toString()); assertEquals("EUR 1.3", roundEUR.toString()); }

9. Currency Conversion

Currency conversion is an important aspect of dealing with money. Unfortunately, these conversions have a great variety of different implementations and use cases.

The API focuses on the common aspects of currency conversion based on the source, target currency, and exchange rate.

Currency conversion or the access of exchange rates can be parametrized:

@Test public void givenAmount_whenConversion_thenNotNull() { MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory().setCurrency("USD") .setNumber(1).create(); CurrencyConversion conversionEUR = MonetaryConversions.getConversion("EUR"); MonetaryAmount convertedAmountUSDtoEUR = oneDollar.with(conversionEUR); assertEquals("USD 1", oneDollar.toString()); assertNotNull(convertedAmountUSDtoEUR); }

A conversion is always bound to currency. MonetaryAmount can simply be converted by passing a CurrencyConversion to the amount’s with method.

10. Currency Formatting

The formatting allows the access of formats based on java.util.Locale. Contrary to the JDK, the formatters defined by this API are thread-safe:

@Test public void givenLocale_whenFormatted_thanEquals() { MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); MonetaryAmountFormat formatUSD = MonetaryFormats.getAmountFormat(Locale.US); String usFormatted = formatUSD.format(oneDollar); assertEquals("USD 1", oneDollar.toString()); assertNotNull(formatUSD); assertEquals("USD1.00", usFormatted); }

Here we're using the predefined format and creating a custom format for our currencies. The use of the standard format is straightforward using the method format of the MonetaryFormats class. We defined our custom format setting the pattern property of the format query builder.

As before because the currency is included in the result we test our results using Strings:

@Test public void givenAmount_whenCustomFormat_thanEquals() { MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(AmountFormatQueryBuilder. of(Locale.US).set(CurrencyStyle.NAME).set("pattern", "00000.00 ¤").build()); String customFormatted = customFormat.format(oneDollar); assertNotNull(customFormat); assertEquals("USD 1", oneDollar.toString()); assertEquals("00001.00 US Dollar", customFormatted); }

11. Riepilogo

In questo rapido articolo, abbiamo trattato le basi di Java Money & Currency JSR.

I valori monetari vengono utilizzati ovunque e Java fornisce sta iniziando a supportare e gestire valori monetari, aritmetica o conversione di valuta.

Come sempre, puoi trovare il codice dall'articolo su Github.