Table of Contents
Overview
Working with currency data in Java involves several key aspects: conversion rates, formatting, rounding, and arithmetic operations. This article will guide you through handling these aspects effectively in Java, ensuring accurate and reliable financial applications.
Conversion Rates
To handle currency conversion, you can use external APIs like the ExchangeRate-API. This API allows you to fetch exchange rates easily and integrate them into your Java project. Here’s a simple example using the ExchangeRate-API and Google’s GSON library to fetch exchange rates:
public record Rate (String code, Double rate) { } public record ExchangeResponse( String result, String documentation, String terms_of_use, long time_last_update_unix, String time_last_update_utc, long time_next_update_unix, String time_next_update_utc, String base_code, Map<String, Double> conversion_rates ) { } public static List<Rate> getRates(String currency) throws IOException, InterruptedException { var url = String.format("https://v6.exchangerate-api.com/v6/%s/latest/%s", Secrets.EXCHANGE_RATE_API_KEY, currency); //send GET request to obtain the exchange rate HttpClient client = HttpClient.newHttpClient(); var response = client .send( HttpRequest.newBuilder() .uri(URI.create(url)) .build(), HttpResponse.BodyHandlers.ofString() ); //parse the conversion_rates in the response using gson and return var gson = new GsonBuilder().create(); var exchangeResponse = gson.fromJson(response.body(), ExchangeResponse.class); return exchangeResponse.conversion_rates().entrySet().stream().map(e -> new Rate(e.getKey(), e.getValue())).toList(); }
Now, we can run the function and get the response back:
public static void main(String[] args) throws IOException, InterruptedException { var rates = getRates("USD"); System.out.println(rates); }
Formatting
Formatting currency values according to locale is crucial for readability and correctness. Java provides the NumberFormat
class for this purpose. Here’s how you can format a currency value for a specific locale:
private static void printCurrencyWithLocales() { System.out.println("Currency with locales"); var localeUs = new Locale.Builder().setLanguage("en").setRegion("US").build(); var localeFr = new Locale.Builder().setLanguage("fr").setRegion("FR").build(); var localeDe = new Locale.Builder().setLanguage("de").setRegion("DE").build(); var localeCa = new Locale.Builder().setLanguage("fr").setRegion("CA").build(); var localeList = List.of( localeUs, localeFr, localeDe, localeCa ); double amount = 2500.01; for (var locale : localeList) { var currencyFormatter = NumberFormat.getCurrencyInstance(locale); System.out.println(currencyFormatter.format(amount)); } }
This code when run will output the following:
Rounding
Rounding currency values is essential to avoid precision loss, especially when dealing with financial calculations. Java’s BigDecimal
class provides methods for rounding values according to various rounding modes.
In Java, the scale
of a BigDecimal
represents the number of digits to the right of the decimal point. The scale can be zero or positive, and if it’s negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale. For example, a BigDecimal
with a scale of 3 means the number has three digits after the decimal point.
Let’s consider some rounding examples:
public static void roundingWithBigDecimal() { var moneyValue1 = "1.6362"; //demonstrate the rounding modes for BigDecimal //rounding up var v1 = new BigDecimal(moneyValue1); var v1RoundedHalfUp = v1.setScale(3, RoundingMode.HALF_UP); var v1RoundedUp = v1.setScale(3, RoundingMode.UP); var v1RoundedCeiling = v1.setScale(3, RoundingMode.CEILING); System.out.printf("original value %s with rounding mode %s is %s%n", moneyValue1, RoundingMode.HALF_UP, v1RoundedHalfUp); System.out.printf("original value %s with rounding mode %s is %s%n", moneyValue1, RoundingMode.UP, v1RoundedUp); System.out.printf("original value %s with rounding mode %s is %s%n", moneyValue1, RoundingMode.CEILING, v1RoundedCeiling); }
This code outputs:
original value 1.6362 with rounding mode HALF_UP is 1.636
original value 1.6362 with rounding mode UP is 1.637
original value 1.6362 with rounding mode CEILING is 1.637
CEILING vs UP
- RoundingMode.UP: This mode rounds away from zero. For positive numbers, it behaves like rounding towards the higher number, and for negative numbers, it rounds towards a more negative number (i.e., away from zero in both cases).
- RoundingMode.CEILING: This mode rounds towards positive infinity. For positive numbers, it behaves similarly to
RoundingMode.UP
, rounding towards the higher number. However, for negative numbers, it rounds towards zero, which is different fromRoundingMode.UP
.
Let’s consider the following example:
private static void ceilingVsUp () { BigDecimal positiveValue = new BigDecimal("2.3"); BigDecimal negativeValue = new BigDecimal("-2.3"); // Rounding up BigDecimal roundedUpPositive = positiveValue.setScale(0, RoundingMode.UP); BigDecimal roundedUpNegative = negativeValue.setScale(0, RoundingMode.UP); // Rounding to the ceiling BigDecimal roundedCeilingPositive = positiveValue.setScale(0, RoundingMode.CEILING); BigDecimal roundedCeilingNegative = negativeValue.setScale(0, RoundingMode.CEILING); System.out.println("Original: " + positiveValue); System.out.println("Rounded UP (Positive): " + roundedUpPositive); System.out.println("Rounded CEILING (Positive): " + roundedCeilingPositive); System.out.println("\nOriginal: " + negativeValue); System.out.println("Rounded UP (Negative): " + roundedUpNegative); System.out.println("Rounded CEILING (Negative): " + roundedCeilingNegative); }
This will output:
Original: 2.3
Rounded UP (Positive): 3
Rounded CEILING (Positive): 3
Original: -2.3
Rounded UP (Negative): -3
Rounded CEILING (Negative): -2
Arithmetic Operations
For accurate arithmetic operations with currency values, it’s recommended to use the BigDecimal
class, which provides methods for addition, subtraction, multiplication, and division without losing precision. Here’s an example:
private static void arithmeticWithBigDecimal() { BigDecimal value1 = new BigDecimal("100.05"); BigDecimal value2 = new BigDecimal("50.06"); // Addition BigDecimal sum = value1.add(value2); // Subtraction BigDecimal difference = value1.subtract(value2); // Multiplication BigDecimal product = value1.multiply(value2); // Division BigDecimal quotient = value1.divide(value2, 20, RoundingMode.HALF_UP); System.out.println("Sum: " + sum); System.out.println("Difference: " + difference); System.out.println("Product: " + product); System.out.println("Quotient: " + quotient); } private static void arithmeticWithoutBigDecimal() { double value1 = Double.parseDouble("100.05"); double value2 = Double.parseDouble("50.06"); System.out.println("Sum: " + (value1 + value2)); System.out.println("Difference: " + (value1 - value2)); System.out.println("Product: " + (value1 * value2)); System.out.println("Quotient: " + (value1 / value2)); }
Here is the output:
=== Arithmetic with BigDecimal
Sum: 150.11
Difference: 49.99
Product: 5008.5030
Quotient: 1.99860167798641630044
=== Arithmetic without BigDecimal
Sum: 150.11
Difference: 49.989999999999995
Product: 5008.503
Quotient: 1.9986016779864162
As you can see, without using BigDecimal, there is an unexpected result in the subtract operation.
Conclusion
In conclusion, handling currency data in Java requires careful consideration of conversion rates, formatting, rounding, and arithmetic operations. By leveraging APIs like ExchangeRate-API for conversion rates, utilizing the NumberFormat
class for formatting, employing the BigDecimal
class for rounding and arithmetic operations, you can ensure your Java applications handle currency data accurately and efficiently.
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.