Manejo de Monedas y Valores Monetarios en Java

JSR 354: Manejo de Monedas y Valores Monetarios en Java

Introducción

La programación en Java ha evolucionado para abarcar una amplia gama de aplicaciones, y una parte fundamental de muchos proyectos es el manejo efectivo de monedas y valores monetarios. En este artículo, abordaremos el JSR 354, que se enfoca en la estandarización de divisas y montos monetarios en Java. Su objetivo es ofrecer una API flexible y extensible para simplificar y hacer más seguro el trabajo con montos monetarios. Aunque el JSR no se incluyó en el JDK 9, está preparado para futuras versiones, lo que lo convierte en tema relevante para desarrolladores que buscan implementar esta funcionalidad en sus aplicaciones Java.

1. Overview

El JSR 354, titulado “Currency and Money”, aborda la estandarización de divisas y montos monetarios en Java. La finalidad de este estándar es añadir una API que permita a los desarrolladores manejar y calcular montos monetarios de manera más sencilla y segura. Con esto, se busca reducir los errores comunes que ocurren en aplicaciones que requieren operaciones monetarias.

2. Setup

Para comenzar a utilizar el JSR 354, primero debemos definir la dependencia necesaria en nuestro archivo pom.xml. A continuación, se muestra cómo agregar la biblioteca moneta, que implementa las API de esta especificación:

<dependency>
    <groupId>org.javamoney</groupId>
    <artifactId>moneta</artifactId>
    <version>1.1</version>
</dependency>

La última versión de la dependencia puede ser consultada aquí.

3. JSR-354 Features

  • Proporcionar una API para manejar y calcular montos monetarios.
  • Definir clases que representen divisas y montos monetarios, así como el redondeo monetario.
  • Manejar tipos de cambio de divisas.
  • Facilitar el formateo y análisis de divisas y montos monetarios.

4. Model

Las principales clases de la especificación JSR-354 se centran en dos interfaces: CurrencyUnit y MonetaryAmount. Estas clases son esenciales para entender cómo trabajar con monedas y montos en Java. Se detalla su funcionamiento a continuación.

5. CurrencyUnit

La clase CurrencyUnit modela las propiedades mínimas de una moneda. Las instancias pueden crearse utilizando el método Monetary.getCurrency. A continuación, presentamos un ejemplo de cómo crear una instancia de CurrencyUnit:

@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);
}

Es importante mencionar que al crear una CurrencyUnit con un código de moneda inexistente, se genera una excepción UnknownCurrency. A continuación, se muestra un ejemplo:

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

6. MonetaryAmount

MonetaryAmount representa numéricamente un monto monetario y siempre está asociado a una CurrencyUnit. Puedes implementar el monto de diferentes maneras, según los requisitos de tu caso de uso. Por ejemplo, Money y FastMoney son implementaciones de la interfaz MonetaryAmount.

Aquí un ejemplo que ilustra cómo crear 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. Monetary Arithmetic

Se puede realizar aritmética monetaria entre Money y FastMoney, pero hay que tener precauciones al combinar instancias de estas dos clases. Por ejemplo, cuando comparamos una instancia de FastMoney de un Euro con una instancia de Money, la comparación no será igual:

@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")));
}

Las operaciones aritméticas deben lanzar una ArithmeticException si se realizan operaciones que exceden las capacidades del tipo de representación numérica utilizada. Por ejemplo:

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

Cuando se suman o restan montos, es recomendable asegurarse de que ambos montos tengan la misma divisa.

7.1. Calculando Montos

Se pueden calcular totales de montos de múltiples maneras. Una forma simple es encadenar los montos:

@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());
}

También se pueden aplicar operaciones de resta, multiplicación o división en montos monetarios de forma similar:

Money calcAmtUSD = Money.of(1, "USD").subtract(fstAmtUSD);
MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);
MonetaryAmount divideAmount = oneDolar.divide(0.25);

Las comparaciones de resultados aritméticos se pueden verificar utilizando sus representaciones tipo String:

@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

El redondeo monetario es simplemente la conversión de un monto con una precisión indeterminada a un monto redondeado. Utilizaremos la API getDefaultRounding proporcionada por la clase Monetary para realizar esta conversión.

@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

La conversión de divisas es un aspecto crucial al tratar con dinero. Aunque estas conversiones tienen una variedad de implementaciones diferentes, el API se enfoca en los aspectos comunes de la conversión de divisas basándose en la moneda de origen, la moneda de destino y el tipo de cambio.

Aquí tienes un ejemplo de cómo parametrizar la conversión de divisas:

@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);
}

10. Currency Formatting

El formateo permite acceder a formatos basados en java.util.Locale. A diferencia de la JDK, los formateadores definidos por esta API son seguros para hilos.

@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);
}

También podemos crear formatos personalizados para nuestras divisas:

@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. Summary

En este artículo, hemos cubierto los aspectos básicos de la especificación de Java Money & Currency JSR 354. Los valores monetarios se utilizan en diversas aplicaciones, y Java está comenzando a ofrecer soporte para manejar de manera adecuada valores monetarios, aritmética y convertibilidad de monedas. Al aplicar estos conceptos, los programadores pueden crear aplicaciones más robustas y responsables que manejan correctamente las finanzas.

Consejos Prácticos

  • Asegúrate de manejar adecuadamente las divisas y montos utilizando las clases CurrencyUnit y MonetaryAmount para evitar errores en cálculos monetarios.
  • Utiliza FastMoney para mejorar el rendimiento en aplicaciones donde la precisión no es una preocupación significativa.
  • Implementa redondeos de forma correcta para que los montos sean precisos al presentarlos y procesarlos.
  • Recuerda manejar las excepciones al trabajar con códigos de moneda y operaciones aritméticas.

Con esta base, puedes comenzar a integrar la API JSR 354 en tus proyectos de Java, asegurando que las operaciones con dinero se manejen de manera adecuada y profesional.