Monetary Calculation Pitfalls
0 CommentsLast Updated on June 16, 2019 by Simanta
People expect computer programs to be accurate and precise when calculating numbers, and especially programs that perform monetary calculations and as a Java developer, sooner or later you will need to deal with monetary calculations.
You may be tempted to use the primitive types of float
or double
for non-integer numbers. Both of these types support numbers with decimals. As we will see below, there are some significant pitfalls in using these primitive types for monetary calculations.
Monetary Calculations: Bad Examples
You have 1.55 $ and you bought a candy worth 35 ¢. How much is left with you?
Let’s do the calculation in Java.
. . . float pocketMoney=1.55f; float price=.35f; System.out.println(pocketMoney - price); . . .
If you said 1.20 $, you are in for a surprise. The output is:
1.1999999
Let us look at another example, this time using double
. A product is priced 10 $. What is the price after adding 8.25 % tax to it?
. . . double amount = 10.00; double tax = .0825; double taxAmount = amount * tax; double amountAfterTax = amount + taxAmount; System.out.println("Tax: "+ taxAmount); System.out.println("Amount after tax: " + amountAfterTax); . . .
The output is:
Tax: 0.8250000000000001 Amount after tax: 10.825
We again have some unexpected numbers. A simple multiplication “10.00 X .0825” is not giving us what we expected “0.825”.
So what’s the problem? Why can’t Java perform such simple calculations? The problem is not with Java but how floating point is defined in the IEEE 754 standard based on which Java implements float
and double
. Floating points are designed to provide accurate approximations (but not exact results) quickly and it is impossible to exactly represent 0.1 (or any other negative power of ten) as a Java float
or double
.
Without going into more details of binary floating point arithmetic, let’s find out how to perform monetary calculations accurately in Java. A solution is to do everything using integral types (int
and long
) and I have met several programmers advocating it. But then you will need to remember that “325” in your program is really “3.25” dollar. Also, how will you do percent calculations rounding to the nearest cent? This is when you should turn to the BigDecimal class.
Monetary Calculations Using BigDecimal
The BigDecimal
class is a part of the java.math
package. For a decimal number, BigDecimal
internally stores the unscaled value in a BigInteger
and the decimal scale (digits to the right of the decimal point) in an integer
. So, the internally used BigInteger
allows BigDecimal
to represent any number, however big it is (only limited to physical memory) and the integer allows accurate handling of the decimal scale.
During addition and subtraction operations, BigDecimal
expands the number with the smallest scale before performing the operation. This guarantees that the sum or difference is exact to the last digit. During multiplication, BigDecimal
computes the sum of the numbers scale and based on it expands its decimal scale. For division, BigDecimal
expects that the result can be represented with a scale that is the difference between the scale of the dividend and divisor.
To perform the arithmetic calculations, BigDecimal
provides the add()
, subtract()
, multiply()
, and divide()
methods. Before we use these methods, we need to represent the numbers as BigDecimal
objects. The BigDecimal
class contains 16 overloaded constructors but the one that you will use to represent a monetary value is BigDecimal(String val)
. This is important because if you mistakenly use the one that accepts double
, you will face same issues as you do while using float
and double
. This happens because the float
or double
parameter value will undergo loss of precision before you pass them to the constructor. On the other hand, when you use the String
constructor, BigDecimal
will represent exactly the number you pass to it.
Let’s now perform some BigDecimal
calculations.
. . . BigDecimal num1 = new BigDecimal("2.5"); BigDecimal num2 = new BigDecimal("3.5"); System.out.println("BigDecimal Addition: "+ num1.add(num2)); System.out.println("BigDecimal Subtraction: "+ num1.subtract(num2)); System.out.println("BigDecimal Multiplication: "+ num1.multiply(num2)); System.out.println("BigDecimal Division: "+ num1.divide(num2)); . . .
In the example above, we created two BigDecimal
numbers and called the the add()
, subtract()
, multiply()
, and divide()
methods to perform arithmetic calculations.
The output is:
BigDecimal Addition: 6.0 BigDecimal Subtraction: -1.0 BigDecimal Multiplication: 8.75 Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. at java.math.BigDecimal.divide(BigDecimal.java:1690) at prototype.CloneExample.main(CloneExample.java:24)
As we can see the addition, subtraction, and multiplication in lines 4-6 were done as expected but the division in line 7 resulted in an exception. This happened because what we have is a non-terminating decimal expansion “2.5/3.5 = 0.7142857142857. . . . .”. As mentioned earlier, for division BigDecimal
expects that the result can be represented with a scale that is the difference between the scale of the dividend and divisor. Else, the JavaDoc says “. . .if the exact quotient cannot be represented (because it has a non-terminating decimal expansion) an ArithmeticException is thrown”– and hence the exception thrown in line 7.
To avoid such exceptions, always set a resulting scale and a rounding mode during division by using the overloaded divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
method. In this method, the first parameter is the BigDecimal
divisor. The second parameter specifies the decimal scale and the third an enumeration value of the RoundingMode
enum. This enum, introduced in Java SE 6, provides eight different types of rounding modes whose descriptions with examples are available here.
Note: BigDecimal
itself have integer fields to represent rounding modes, but they are now obsolete. Ensure that you instead use the RoundingMode
enum for rounding modes in calculations.
To display BigDecimal
numbers, you can set the scale and rounding mode with the BigDecimal.setScale(scale, roundingMode)
method.
Now, the question is- Which rounding mode should I use for monetary calculations? The answer is, there is “no specific” mode. It depends on the application requirements and any legal contracts that your application must conform with. For example, it is common to use RoundingMode.HALF_UP
for tax calculations. If you are selling a product and want to round in favor of the customer, use RoundingMode.CEILING
. If you are not sure, go for RoundingMode.HALF_EVEN
. This rounding mode, also known as “Banker’s rounding”, when applied repeatedly over a sequence of calculations statistically minimizes cumulative error.
Let’s now write some code to perform some monetary calculations using BigDecimal
.
package guru.springframework.blog.monetarycalculations; import java.math.BigDecimal; import java.math.RoundingMode; public class BigDecimalCalc { public void calculate(String param1, String param2){ System.out.println("--------------------calculate-----------------------"); BigDecimal num1=new BigDecimal(param1); BigDecimal num2=new BigDecimal(param2); System.out.println("num1: "+num1+" num2: "+ num2); System.out.println("BigDecimal Addition: "+num1.add(num2)); System.out.println("BigDecimal Subtraction: " + num1.subtract(num2)); System.out.println("BigDecimal Multiplication: "+num1.multiply(num2)); } public void divideWithScaleRounding(String param1, String param2){ System.out.println("--------------------divisionWithScaleRounding-----------------------"); /*Setting scale and rounding mode for division using overloaded divide(BigDecimal divisor, int scale, RoundingMode roundingMode) */ BigDecimal num1=new BigDecimal(param1); BigDecimal num2=new BigDecimal(param2); System.out.println("num1: "+num1+" num2: "+ num2); System.out.println("BigDecimal Division with overloaded divide(): " + num1.divide(num2, 4, RoundingMode.HALF_EVEN)); } public void calculateTax(String amount, String tax){ System.out.println("--------------------calculateTax-----------------------"); BigDecimal bdAmount = new BigDecimal(amount); BigDecimal bdTax = new BigDecimal(tax); BigDecimal taxAmount = bdAmount.multiply(bdTax); /*Setting scale and rounding mode using setScale() */ taxAmount = taxAmount.setScale(2, RoundingMode.HALF_UP); BigDecimal finalAmount = bdAmount.add(taxAmount); finalAmount = finalAmount.setScale(2, RoundingMode.HALF_UP); System.out.println("Amount : " + bdAmount); System.out.println("Tax : " + taxAmount); System.out.println("Amount after tax: " + finalAmount); } }
In the example above, we first wrote a calculate()
method that accepts two String
parameters. In lines 13-14 we converted them into BigDecimal
. In lines 16-18 we performed addition, subtraction, and multiplication operations on the numbers. Next, we wrote a divideWithScaleRounding()
method that also accepts two String
parameters that we converted to BigDecimal
in line 26-27. In line 29, we performed a division with a scale 4 and a rounding mode, RoundingMode.HALF_EVEN
. Then, we wrote a calculateTax()
method that accepts a monetary amount and a tax as String
objects. After converting the parameters to BigDecimal
, we calculated the tax amount in line 36. To display the tax amount with a scale 2 and a rounding mode RoundingMode.HALF_UP
, we called the setScale()
method in line 38. Similarly, we calculated the final amount and set its scale and rounding mode in lines 39-40. To test our example, let’s write this unit test.
package guru.springframework.blog.monetarycalculations; import org.junit.Test; public class BigDecimalCalcTest { @Test public void testCalculate() throws Exception { new BigDecimalCalc().calculate("4.0", "2.0"); } @Test public void testDivideWithScaleRounding() throws Exception { new BigDecimalCalc().divideWithScaleRounding("2.5", "3.5"); } @Test public void testCalculateTax() throws Exception { new BigDecimalCalc().calculateTax("10.00", ".0825"); } }
The output is:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.3.RELEASE) Running guru.springframework.blog.monetarycalculations.BigDecimalCalcTest --------------------divisionWithScaleRounding----------------------- num1: 2.5 num2: 3.5 BigDecimal Division with overloaded divide(): 0.7143 --------------------calculate----------------------- num1: 4.0 num2: 2.0 BigDecimal Addition: 6.0 BigDecimal Subtraction: 2.0 BigDecimal Multiplication: 8.00 --------------------calculateTax----------------------- Amount : 10.00 Tax : 0.83 Amount after tax: 10.83 Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 sec - in guru.springframework.blog.monetarycalculations.BigDecimalCalcTest
In the output above, observe how BigDecimal
performed the division and displayed monetary amounts with the specified scale and rounding mode. Using this example, go ahead and experiment with different sets of monetary values, scales, and rounding modes.
Summary
While float and double are natural choices for scientific, statistical and engineering calculations, BigDecimal
is the ideal choice when it comes to monetary calculations. But, the precision and accuracy of BigDecimal
comes with a price- and it is performance. However, if we require completely accurate monetary calculations, sacrifice of some performance is acceptable.
On an ending note, keep an eye on JSR 354: Money and Currency API, which is planned to be included in Java 9. A part of this API is designed to support complex monetary calculation rules including calculation and display precisions. It will be interesting to see if this API could change how we count our money in future.
Get The Code
I’ve committed the source code for this post to GitHub. It is a Maven project which you can download and build. If you wish to learn more about the Spring Framework, I have a free introduction to the Spring tutorial. You can sign up for this tutorial in the section below.