Cómo trabajar con importes, ratios y divisas en Java

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Aún en Java 8 no tenemos una API incluida en el JDK dedicada al manejo de importes, divisas y conversiones. Si la especificación JSR-354 se incluye en alguna versión podremos hacer uso de ella sin necesidad de ninguna dependencia adicional, pero si tenemos necesidad ahora podemos usar la librería que ha producido la especificación. Usando las clases y métodos de la API evitaremos hacer y mantener una implementación nosotros mismos que además seguro no llega al nivel de esta.

Java

Las aplicaciones de comercio electrónico o que realizan operaciones financieras con importes seguro que necesitan una forma de representar un importe junto con una divisa. También si necesitan convertir importes en diferentes divisas necesitarán obtener los ratios de conversión de alguna fuente, en el artículo Servicio para obtener ratios de conversión entre divisas comentaba uno que podemos usar, Open Exchange Rates. Java incluye clases para datos numéricos y con ellos se pueden representar importes como por ejemplo BigDecimal. Para importes no debemos usar en ningún caso un tipo de dato float o double ya que estos son incapaces de representar ciertos valores de forma exacta, usando float y double tendremos errores de precisión, redondeo y representación. En vez de crear un nuevo tipo de datos (una clase) que tenga como propiedades un BigDecimal para el importe y un String o similar para representar la divisa además de implementar las varias operaciones aritméticas y de comparación entre otras muchas cosas que necesitaremos podemos usar la librería que la especificación JSR-354 proporciona una API dedicada a importes y divisas en Java. En Java 8 no se incluyó pero en una futura versión quizá si se incluya en el propio JDK. En este artículo comentaré como usando Java 8 podemos hacer uso de esta API desde ya y que ofrece.

Aunque la especificación no es parte de Java aún el grupo de trabajo encargado ha generado una dependencia que podemos usar. En el repositorio de GitHub podemos encontrar el código de la librería. Incluyéndola como dependencia de un proyecto podemos usarla, usando Gradle con:

1
2
3
4
5
6
...
dependencies {
	compile 'org.javamoney:moneta:1.0'
	...
}
...
build.gradle

La librería hace uso de lambdas, una de las novedades que introdujo de Java 8 en el lenguaje, y nos facilita varias funcionalidades. También permite usar streams. Veamos algunas de las posibilidades.

Representación de divisas e importes

Las divisas se representan con CurrencyUnit y los importes se representan usando la clase MoneyAmount, tenemos varias formas de crear instancias de estas clases.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// getting CurrencyUnit by currency code and locale
CurrencyUnit euro = Monetary.getCurrency("EUR");		
CurrencyUnit dollar = Monetary.getCurrency(Locale.US);

// getting MonetaryAmount by currency code and CurrencyUnit, without using Money (implementation class)
MonetaryAmount fiveEuro = Money.of(5, euro);
MonetaryAmount twelveEuro = Money.of(new BigDecimal("12"), euro);
MonetaryAmount tenDollar = Money.of(10, "USD");
MonetaryAmount tenPound = Monetary.getDefaultAmountFactory().setNumber(10).setCurrency("GBP").create();

System.out.println("getting MonetaryAmount by currency code and CurrencyUnit, without using Money (implementation class)");
System.out.printf("5 EUR: %s\n", fiveEuro);
System.out.printf("12 EUR: %s\n", twelveEuro);
System.out.printf("10 USD: %s\n", tenDollar);
System.out.printf("10 GBP: %s\n", tenPound);

// 5 EUR: EUR 5
// 12 EUR: EUR 12
// 10 USD: USD 10
// 10 GBP: GBP 10
Main-1.java

La API ofrece varios métodos para extraer los valores numéricos, la parte entera y decimal, que una instancia de MoneyAmount contiene así como obtener los valores en un tipo de datos más básico como BigDecimal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// getting currency, the numeric amount and precision
MonetaryAmount amount = Money.of(123.45, euro);		
		
System.out.printf("123.45 EUR (currency): %s\n", amount.getCurrency());
System.out.printf("123.45 EUR (long): %s\n", amount.getNumber().longValue());
System.out.printf("123.45 EUR (number): %s\n", amount.getNumber());
System.out.printf("123.45 EUR (fractionNumerator): %s\n", amount.getNumber().getAmountFractionNumerator());
System.out.printf("123.45 EUR (fractionDenominator): %s\n", amount.getNumber().getAmountFractionDenominator());
System.out.printf("123.45 EUR (amount, BigDecimal): %s\n", amount.getNumber().numberValue(BigDecimal.class));

// 123.45 EUR (currency): EUR
// 123.45 EUR (long): 123
// 123.45 EUR (number): 123.45
// 123.45 EUR (fractionNumerator): 45
// 123.45 EUR (fractionDenominator): 100
// 123.45 EUR (amount, BigDecimal): 123.45
Main-2.java

Operaciones aritméticas, de comparación y operaciones personalizadas

Podemos hacer operaciones aritméticas (suma, resta, multiplicación y división) entre dos importes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// aritmetic
MonetaryAmount seventeenEuros = fiveEuro.add(twelveEuro);
MonetaryAmount sevenEuros = twelveEuro.subtract(fiveEuro);
MonetaryAmount tenEuro = fiveEuro.multiply(2);
MonetaryAmount twoPointFiveEuro = fiveEuro.divide(2);
		
System.out.printf("5 EUR + 12 EUR: %s\n", seventeenEuros);
System.out.printf("12 EUR - 5 EUR: %s\n", sevenEuros);
System.out.printf("5 EUR * 2: %s\n", tenEuro);
System.out.printf("5 EUR / 2: %s\n", twoPointFiveEuro);

// 5 EUR + 12 EUR: EUR 17
// 12 EUR - 5 EUR: EUR 7
// 5 EUR * 2: EUR 10
// 5 EUR / 2: EUR 2.5
		 
// negative
MonetaryAmount minusSevenEuro = fiveEuro.subtract(twelveEuro);
		
System.out.println("negative");
System.out.printf("5 EUR - 12 EUR: %s\n", minusSevenEuro);

// 5 EUR - 12 EUR: EUR -7
Main-3.java

También podremos hacer comparaciones:

1
2
3
4
5
6
7
8
// comparing
System.out.printf("7€ < 10€: %s\n", sevenEuros.isLessThan(tenEuro));
System.out.printf("7€ > 10€: %s\n", sevenEuros.isGreaterThan(tenEuro));
System.out.printf("10 > 7€: %s\n", tenEuro.isGreaterThan(sevenEuros));

// 7€ < 10€: true
// 7€ > 10€: false
// 10 > 7: true
Main-4.java

Redondear importes

1
2
3
4
5
6
7
8
9
// rounding
MonetaryAmount euros = Money.of(12.34567, "EUR");
MonetaryAmount roundedEuros = euros.with(Monetary.getDefaultRounding());
		
System.out.println();
System.out.println("rounding");
System.out.printf("12.34567 EUR redondeados: %s\n", roundedEuros);

// 12.34567 EUR redondeados: EUR 12.35
Main-9.java

E incluso implementar operaciones más complejas y habituales personalizadas con la clase MonetaryOperator que se puede aplicar usando el método with de MonetaryAmount.

Formateado y analizado

Dependiendo de país o la moneda los importes se representan de forma diferente, por ejemplo, en Estados Unidos se usa «,» como separador de millares y «.» como separador de los decimales, en España es diferente, se usa «.» para los millares y «,» para los decimales. También hay monedas que no tienen decimales como el Yen japonés. Disponemos de métodos y clases para formatear correctamente el importe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// formating
MonetaryAmountFormat spainFormat = MonetaryFormats.getAmountFormat(new Locale("es", "ES"));
MonetaryAmountFormat usFormat = MonetaryFormats.getAmountFormat(new Locale("en", "US"));
MonetaryAmount fiveThousandEuro = Money.of(5000, euro);
		
System.out.println("formating");
System.out.printf("Formato de 5000 EUR localizado en España: %s\n", spainFormat.format(fiveThousandEuro));
System.out.printf("Formato de 5000 EUR localizado en Estados Unidos: %s\n", usFormat.format(fiveThousandEuro));

// Formato de 5000 EUR localizado en España: 5.000,00 EUR
// Formato de 5000 EUR localizado en Estados Unidos: EUR5,000.00
Main-5.java

Podemos hacer la operación contraria parseando o analizando la cadena, obtener un objeto MoneyAmount desde su representación en String.

1
2
3
4
5
6
// parsing
MonetaryAmount twelvePointFiveEuro = spanishFormat.parse("12,50 EUR");
		
System.out.printf("Analizando «12,50 EUR» es %s\n", spainFormat.format(twelvePointFiveEuro));

// Analizando «12,50 EUR» es 12,50 EUR
Main-6.java

Ratios de conversión, conversiones entre divisas

Si necesitamos convertir el importe de una moneda a otra necesitaremos el ratio de conversión entre las monedas, es decir, por cada dólar estadounidense cuántos euros son si queremos hacer una conversión de USD a euro. Se puede obtener el ratio de conversión o hacer la conversión directamente entre las dos monedas. En el siguiente código se muestra cuántos euros son 10 USD con la cotización entre las divisas en el momento de escribir el artículo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// exchange rates		
ExchangeRateProvider exchangeRateProvider = MonetaryConversions.getExchangeRateProvider("ECB");
ExchangeRate exchangeRate = exchangeRateProvider.getExchangeRate("USD", "EUR");
		
System.out.printf("Ratio de conversión de USD a EUR: %f\n", exchangeRate.getFactor().doubleValue());

// Ratio de conversión de USD a EUR: 0,921489
		
// conversion
CurrencyConversion toEuro = MonetaryConversions.getConversion("EUR");		
MonetaryAmount tenDollarToEuro = tenDollar.with(toEuro);

System.out.printf("10 USD son %s EUR\n", tenDollarToEuro);

// 10 USD son EUR 9.214891264283081 EUR
Main-7.java

La librería incluye varias fuentes para las cotizaciones de cada moneda, una de ellas es el Banco Central Europeo pero también podemos crear la implementación de una nueva fuente que por ejemplo use Open Exchange Rates.

Streams y filtros

Por si todo esto fuera poco podemos usar las características de programación funcional de Java 8 ya que la librería ofrece soporte para streams para ejemplo filtrar o para agrupar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// filter
List<MonetaryAmount> onlyDollars = amounts.stream()
	.filter(MonetaryFunctions.isCurrency(dollar))
	.collect(Collectors.toList());
		
System.out.printf("Solo USD: %s\n", onlyDollars);
		
List<MonetaryAmount> euroAndDollar = amounts.stream()
	.filter(MonetaryFunctions.isCurrency(euro, dollar))
	.collect(Collectors.toList());

// grouping
Map<CurrencyUnit, List<MonetaryAmount>> groupedByCurrency = amounts.stream()
				.collect(MonetaryFunctions.groupByCurrencyUnit());
		
System.out.printf("Agrupación por divisa: %s\n", groupedByCurrency);

// Agrupación por divisa: {EUR=[EUR 2], GBP=[GBP 13.37], USD=[USD 7, USD 18, USD 42]}

Main-8.java

El código fuente completo del ejemplo está en uno de mis repositorios de GitHub.


Comparte el artículo: