1. Información general
JSR 354 - "Moneda y dinero" aborda la estandarización de monedas y cantidades monetarias en Java.
Su objetivo es agregar una API flexible y extensible al ecosistema de Java y hacer que trabajar con cantidades monetarias sea más simple y seguro.
El JSR no se abrió camino en JDK 9 pero es un candidato para futuras versiones de JDK.
2. Configuración
Primero, definamos la dependencia en nuestro archivo pom.xml :
org.javamoney moneta 1.1
La última versión de la dependencia se puede consultar aquí.
3. Características JSR-354
Los objetivos de la API "Moneda y dinero":
- Proporcionar una API para manejar y calcular cantidades monetarias.
- Para definir clases que representen monedas e importes monetarios, así como el redondeo monetario.
- Para lidiar con los tipos de cambio de moneda
- Para lidiar con el formato y el análisis de monedas y cantidades monetarias
4. Modelo
Las clases principales de la especificación JSR-354 se muestran en el siguiente diagrama:

El modelo tiene dos interfaces principales, CurrencyUnit y MonetaryAmount, que se explican en las siguientes secciones.
5. CurrencyUnit
CurrencyUnit modela las propiedades mínimas de una moneda. Sus instancias se pueden obtener utilizando el método 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); }
Creamos CurrencyUnit usando una representación de cadena de la moneda, esto podría llevar a una situación en la que intentemos crear una moneda con un código inexistente. La creación de monedas con códigos inexistentes genera una excepción UnknownCurrency :
@Test(expected = UnknownCurrencyException.class) public void givenCurrencyCode_whenNoExist_thanThrowsError() { Monetary.getCurrency("AAA"); }
6. MonetaryAmount
MonetaryAmount es una representación numérica de una cantidad monetaria. Siempre está asociado con CurrencyUnit y define una representación monetaria de una moneda.
El monto se puede implementar de diferentes maneras, enfocándose en el comportamiento de los requisitos de representación monetaria, definidos por cada caso de uso concreto. Por ejemplo. Money y FastMoney son implementaciones de la interfaz MonetaryAmount .
FastMoney implementa MonetaryAmount utilizando long como representación numérica y es más rápido que BigDecimal a costa de precisión; se puede utilizar cuando necesitamos rendimiento y la precisión no es un problema.
Se puede crear una instancia genérica utilizando una fábrica predeterminada. Vamos a mostrar las diferentes formas de obtener instancias de 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 . Aritmética monetaria
Podemos realizar operaciones aritméticas monetarias entre Money y FastMoney, pero debemos tener cuidado cuando combinamos instancias de estas dos clases.
Por ejemplo, cuando comparamos una instancia en euros de FastMoney con una instancia en euros de Money, el resultado es que no son lo mismo:
@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"))); }
Podemos realizar sumar, restar, multiplicar, dividir y otras operaciones aritméticas monetarias utilizando los métodos proporcionados por la clase MonetaryAmount .
Las operaciones aritméticas deberían arrojar una ArithmeticException , si las operaciones aritméticas entre cantidades superan las capacidades del tipo de representación numérica utilizada, por ejemplo, si intentamos dividir uno por tres, obtenemos una ArithmeticException porque el resultado es un número infinito:
@Test(expected = ArithmeticException.class) public void givenAmount_whenDivided_thanThrowsException() { MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); oneDolar.divide(3); }
Al sumar o restar cantidades, es mejor usar parámetros que sean instancias de MonetaryAmount , ya que debemos asegurarnos de que ambas cantidades tengan la misma moneda para realizar operaciones entre cantidades.
7.1. Cálculo de importes
Se puede calcular un total de cantidades de varias formas, una forma es simplemente encadenar las cantidades 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()); }
El encadenamiento también se puede aplicar a restar:
Money calcAmtUSD = Money.of(1, "USD").subtract(fstAmtUSD);
Multiplicar:
MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);
O dividiendo:
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. Resumen
En este artículo rápido, hemos cubierto los conceptos básicos de Java Money & Currency JSR.
Los valores monetarios se utilizan en todas partes, y Java proporciona está comenzando a admitir y manejar valores monetarios, aritmética o conversión de moneda.
Como siempre, puede encontrar el código del artículo en Github.