jugbd-logo
Cancel

Never Trust Float & Double with Currency in Java

Photo by cottonbro from Pexels

Original Post: https://www.ayonkhan.me/posts/2021/08/never-trust-float-and-double-with-currency-in-java/

The float and double types are the worst candidates for monetary calculations. In this post, you will see why and how you can remedy the problem.


Introduction

The float and double are probably your go-to data types for floating-point numbers, especially if you are new to Java. It’s fine unless you’re using these types for storing precise values such as currency. Even the Java documentation advises against using these types for such values. You are welcome to look it up.

Understanding the Problem

95% of the folks out there are completely clueless about floating-point.

James Gosling, Sun Fellow, Java Inventor (February 28, 1998)1

To understand the problem, let’s look at an example first.

1
2
3
4
double x = 0.1;
double y = 0.2;

System.out.println(x + y); // 0.30000000000000004

Weirdly, the output is not 0.3, but 0.30000000000000004.

You may have come across this example before. Anyway, let’s look at another one.

1
2
3
4
5
6
7
double x = 0.2;

for (int i = 0; i < 100; i++) {
    x += 0.2;
}

System.out.println(x); // 20.19999999999996

Again, we are not getting our expected output of 20.2. One last example.

1
2
3
double x = 4.35;

System.out.println(x * 100); // 434.99999999999994

In this case, we were expecting 435.0 but ended up getting 434.99999999999994.

These are some common examples used to demonstrate the loss of significance in floating-point arithmetic. I’m sure you can imagine this behavior can lead to some unexpected errors.

The float and double types are particularly ill-suited for monetary calculations because it is impossible to represent 0.1 (or any other negative power of ten) as a float or double exactly.

Joshua Bloch, Effective Java, Third Edition2

Now I’m not going to go into details but know this, floating-point numbers are relatively complex for computers to express as they store floating-point numbers as base-2 fractions. Many programming languages, including Java, adopted IEEE 754 standard for their floating-point numbers. Even though this standard addresses many problems, we still cannot reliably use floating-point numbers to represent currency.

comic xkcd (#217): e to the pi Minus pi

Head over to this site for the explanation if you didn’t get it. You can also run it on your machine Math.pow(Math.E, Math.PI) - Math.PI.

As a rule of thumb, we should avoid these types for monetary calculations.

The Solution: BigDecimal

Java platform provides a BigDecimal immutable class that can deal with high-precision floating-point numbers.

There are many ways we can initialize a BigDecimal instance. To use BigDecimal, we need to import java.math.BigDecimal class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// String as an argument (high-precision)
BigDecimal a = new BigDecimal("0.1");
System.out.println(a); // 0.1

// Double as an argument (should be avoided)
BigDecimal b = new BigDecimal(0.1); // Unpredictable 'new BigDecimal()' call
System.out.println(b); // 0.1000000000000000055511151231257827021181583404541015625

// If we need to pass double as an argument
// Beware, it has limited precision than the String constructor
BigDecimal c = new BigDecimal(Double.toString(0.1));
System.out.println(c); // 0.1

// Alternatively, we can do this (this too has limited precision)
BigDecimal d = BigDecimal.valueOf(0.1);
System.out.println(d); // 0.1

As we can see, BigDecimal(double val) can give an unpredictable result. Therefore, BigDecimal(String val) is preferred over the one that accepts a double.

Arithmetic Operations

Another thing we need to know is that we cannot perform arithmetic operations on BigDecimal instances using the +, -, *, / operators. Instead, we need to call add(), subtract(), multiply(), divide() methods of BigDecimal objects.

BigDecimal in Action

Now let’s tackle the problems we discussed in the earlier examples with BigDecimal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Example 1
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.2);

System.out.println(a.add(b)); // 0.3

// Example 2
BigDecimal c = new BigDecimal(Double.toString(0.2));

for (int i = 0; i < 100; i++) {
    // BigDecimal objects are immutable as mentioned before
    // So, calling add() on "c" will not update the value of "c"
    // We need to assign the result of the arithmetic operation
    c = c.add(new BigDecimal("0.2"));
}

System.out.println(c); // 20.2

// Example 3
BigDecimal d = new BigDecimal(Double.toString(4.35));

System.out.println(d.multiply(new BigDecimal("100"))); // 435.00

That’s awesome, right? Well, sort of, cause now you might be wondering why we got 435.00 and not 435.0 from the multiplication like the other ones. For this, we need to understand something called scale.

Scale & Precision

Scale is the number of digits right to the decimal point. So, if the number is 3.141, then the scale is 3. Meanwhile, the precision is 4 as it indicates the total number of digits.

Preferred Scales for Arithmetic Operations

By default, each arithmetic operation uses its preferred scale for representing a result. For example, when multiplying two numbers, the scale is – the sum of the scales of both numbers. Let me borrow a table from the Java documentation for better illustration.

Table: Preferred Scales for Results of Arithmetic Operations3

OperationPreferred Scale of Result
Addmax(addend.scale(), augend.scale())
Subtractmax(minuend.scale(), subtrahend.scale())
Multiplymultiplier.scale() + multiplicand.scale()
Dividedividend.scale() - divisor.scale()
Square rootradicand.scale()/2

Setting the Scale & Rounding Mode

We can use setScale(int newScale, RoundingMode roundingMode) method to explicitly set the scale. We need to provide the rounding mode along with the scale so that it knows how to round the number. There are 8 different rounding modes available in the java.math.RoundingMode enum. Refer to this documentation for details on the rounding modes.

1
2
3
4
5
6
7
// import java.math.RoundingMode

BigDecimal x = BigDecimal.valueOf(32.128);

x = x.setScale(2, RoundingMode.HALF_UP);

System.out.println(x); // 32.13

Comparing BigDecimal Objects: equals() vs compareTo()

Knowing how to perform arithmetic operations is not enough. It’s equally (no pun intended) important to understand how to compare two BigDecimal objects. You may be tempted to use the equals(Object x) method, but bear in mind, this method only returns true if both numbers are equal in value and scale.

1
2
3
4
5
6
BigDecimal x = BigDecimal.valueOf(128);
BigDecimal y = BigDecimal.valueOf(128.00);
BigDecimal z = BigDecimal.valueOf(128);

System.out.println(x.equals(y)); // false
System.out.println(x.equals(z)); // true

I hope it’s clear that we simply cannot depend on equals(Object x) method for value comparison. For that, we have compareTo(BigDecimal val) method.

1
2
3
4
5
6
BigDecimal x = BigDecimal.valueOf(128);
BigDecimal y = BigDecimal.valueOf(128.00);
BigDecimal z = BigDecimal.valueOf(128);

System.out.println(x.compareTo(y)); // 0
System.out.println(x.compareTo(z)); // 0

Unlike equals(Object x), this method doesn’t return a boolean. Why? Well, that’s because this method comes from the Comparable<T> interface implementation, and it needs to return an int as declared in the contract. Now, this could be a positive/negative number or zero. The following example shows what these different values represent and how we can leverage them for comparisons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
BigDecimal x = BigDecimal.valueOf(64.96);
BigDecimal y = BigDecimal.valueOf(16.32);

// Positive number (1) indicates the value of "x" is greater than the value of "y"
// Zero (0) indicates the value of "x" is equal to the value of "y"
// Negative number (-1) indicates the value of "x" is less than the value of "y"
System.out.println(x.compareTo(y)); // 1

// The value of "x" is not less than the value of "y"
System.out.println(x.compareTo(y) < 0); // false

// The value of "x" is not equal to the value of "y"
System.out.println(x.compareTo(y) == 0); // false

// The value of "x" is greater than the value of "y"
System.out.println(x.compareTo(y) > 0); // true

// The value of "x" is greater than or equal to the value of "y"
System.out.println(x.compareTo(y) >= 0); // true

// The value of "x" is not equal to the value of "y"
System.out.println(x.compareTo(y) != 0); // true

// The value of "x" is not less than or equal to the value of "y"
System.out.println(x.compareTo(y) <= 0); // false

Formatting Numbers as Currency

Before finishing this post, I want to address one more thing: formatting numbers as currency. There’s a NumberFormat class that we can use for this.

First, we need to import the following classes before proceeding with the examples.

  • java.text.NumberFormat
  • java.util.Locale
1
2
3
4
5
BigDecimal amount = new BigDecimal("1024.64");

String cadAmount = NumberFormat.getCurrencyInstance(Locale.CANADA).format(amount);

System.out.println(cadAmount); // $1,024.64

If a built-in locale is not available for a country, one can be constructed very easily. Let’s see how.

1
2
3
4
5
6
7
BigDecimal amount = new BigDecimal("1024.64");

// Locale(@NotNull String language, @NotNull String country)
Locale bangladesh = new Locale("en", "bd"); // or "bn" instead of "en"
String bdtAmount = NumberFormat.getCurrencyInstance(bangladesh).format(amount);

System.out.println(bdtAmount); // BDT1,024.64

Now I’ll leave it up to you to figure out how you can apply rounding mode here. It shouldn’t be too difficult for you at this point.

Conclusion

BigDecimal by no means is the only way to deal with money and currency in Java. Also, it has some caveats. It’s not as performant as a float or a double and takes a more significant toll on memory. It’s merely a class designed to be used in areas that require a high degree of precision. There are some monetary APIs that can represent money and perform extensive calculations.4 Over the years, people have also come up with different techniques or patterns, such as storing the currency’s smallest unit, which can suffer from memory overflow issues.5

We need to consider a lot of edge cases when working with currency. Take converting a currency to a different one (USD to CAD), for instance. In cases like this, we could still fall into the trap of rounding errors if we don’t consider it before assigning the values.

To sum it up, if BigDecimal is used correctly, it can solve many issues that we encounter with monetary calculations.

Data types in Java

Securing and Exploiting Java Applications

Comments powered by Disqus.