Using double/long vs BigDecimal for monetary calculations

by Mikhail Vorontsov

This article discusses how to perform monetary operations in Java: what is the correct way of using double and what alternatives do we have.

Monetary operations using long/double

The easiest way to represent monetary values in financial environment is to work with the smallest currency units – for example, cents in USA, instead of normal currency unit – dollar in USA. long datatype is rather suitable for this case. Unfortunately, sometimes we have to divide such values or multiply them by decimal point values (for example, calculate how much you have earned on your savings account). This means that while we can still use long for storing cents, we need to multiply/divide using decimal point arithmetic.

Do not use float for any monetary operations unless you absolutely sure. It has too low precision (23 bits).

double calculations are not precise. Even simple one, such as addition and subtraction:

1
System.out.println( "362.2 - 362.6 = " + ( 362.2 - 362.6 ) );
System.out.println( "362.2 - 362.6 = " + ( 362.2 - 362.6 ) );

will print "362.2 - 362.6 = -0.4000000000000341". This means we should:

  1. Avoid working with non-integral values while using double (calculate in the smallest currency units).
  2. Round any multiplication/division results using Math.round/rint/ceil/floor (per your system requirements).


It is still possible to add/subtract using long as long as you stick to 2 rules described above.

Let’s see how long it will take us to calculate 1.5% from 362.2$ using double and BigDecimal. The following loops were run 100M times:

1
2
3
4
5
6
7
8
int res = 0;
final BigDecimal orig = new BigDecimal( "362.2" );
final BigDecimal mult = new BigDecimal( "0.015" ); //1.5%
for ( int i = 0; i < ITERS; ++i )
{
    final BigDecimal result = orig.multiply( mult, MathContext.DECIMAL64 );
    if ( result != null ) res++;
}
int res = 0;
final BigDecimal orig = new BigDecimal( "362.2" );
final BigDecimal mult = new BigDecimal( "0.015" ); //1.5%
for ( int i = 0; i < ITERS; ++i )
{
    final BigDecimal result = orig.multiply( mult, MathContext.DECIMAL64 );
    if ( result != null ) res++;
}

We can not exactly emulate the previous calculation using double and long. In the following code JIT will move a constant Math.round( orig * mult ) out of the loop:

1
2
3
4
5
6
7
final double orig = 36220; //362.2 in cents
final double mult = 0.015; //1.5%
for ( int i = 0; i < ITERS; ++i )
{
    final long result = Math.round( orig * mult );
    if ( result != 543 ) res++;    //543.3 cents actually
}
final double orig = 36220; //362.2 in cents
final double mult = 0.015; //1.5%
for ( int i = 0; i < ITERS; ++i )
{
    final long result = Math.round( orig * mult );
    if ( result != 543 ) res++;    //543.3 cents actually
}

So, we will use slightly different test in this case:

1
2
3
4
5
6
final double orig = 36220; //362.2 in cents
for ( long i = 0; i < ITERS; ++i )
{
    final long result = Math.round( orig * i );
    if ( result != 543 ) res++;    //compare with something
}
final double orig = 36220; //362.2 in cents
for ( long i = 0; i < ITERS; ++i )
{
    final long result = Math.round( orig * i );
    if ( result != 543 ) res++;    //compare with something
}

It took 4.899 sec for BigDecimal calculations and 0.58 sec for double calculations.

As you can see, as long as your calculations fit in 52 bits (double precision) and you stick to the 2 rules listed above, you will have a fast and precise monetary calculations in Java.

Monetary calculations using BigDecimal

If you still want to use BigDecimal for your monetary calculations (it is convenient at least!), the following text will give you some hints on the correct BigDecimal usage.

For BigDecimals you can specify both rounding mode and precision, if you need it, but there is a more convenient way - you can use MathContext instead, which contains both precision and rounding information. More important, there are a few predefined ones, which will allow you to emulate float/double/decimal_128 arithmetic operations without any rounding problems: MathContext.DECIMAL32/DECIMAL64/DECIMAL128. There is also MathContext.UNLIMITED, which is a default MathContext value.

You can use addition and subtraction without MathContext, but for multiplication and division you'd better specify one of DECIMAL* contexts. They are required because these operations require a precision to be specified in case when an operation result has an infinitely long decimal expansion; for example, 1 divided by 3. Otherwise you'll get ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. Try it:

1
2
3
4
5
6
7
8
9
final BigDecimal three = new BigDecimal( "3" );
try
{
    System.out.println( BigDecimal.ONE.divide( three ) );
}
catch ( ArithmeticException ex )
{
    System.out.println( "Got an exception while calculating 1/3 : " + ex.getMessage() );
}
final BigDecimal three = new BigDecimal( "3" );
try
{
    System.out.println( BigDecimal.ONE.divide( three ) );
}
catch ( ArithmeticException ex )
{
    System.out.println( "Got an exception while calculating 1/3 : " + ex.getMessage() );
}

How fast are BigDecimal arithmetic operations? Let's calculate a sum of 10M E*E+E, where E = Math.E:

1
2
3
4
5
6
7
8
9
BigDecimal res = BigDecimal.ZERO;
final BigDecimal a = new BigDecimal( Math.E, context );
final BigDecimal b = new BigDecimal( Math.E, context );
final BigDecimal c = new BigDecimal( Math.E, context );
for ( int i = 0; i < 10000000; ++i )
{
    final BigDecimal val = a.multiply( b, context ).add( c, context );
    res = res.add( val, context );
}
BigDecimal res = BigDecimal.ZERO;
final BigDecimal a = new BigDecimal( Math.E, context );
final BigDecimal b = new BigDecimal( Math.E, context );
final BigDecimal c = new BigDecimal( Math.E, context );
for ( int i = 0; i < 10000000; ++i )
{
    final BigDecimal val = a.multiply( b, context ).add( c, context );
    res = res.add( val, context );
}

In addition to this method, we will run another one for BigDecimal, where MathContext is not used. And the last one, just to compare, will calculate the same expression using double. The last column in the table is result of calculation in a given mode.

Type Time Result
double 0.018 sec 1.010733792587689E8
no MathContext 4.1 sec 101073379.273896945320908905278183855697464192452494578591950602844407036684515333035960793495178222656250000000
MathContext.UNLIMITED 3.9 sec 101073379.273896945320908905278183855697464192452494578591950602844407036684515333035960793495178222656250000000
MathContext.DECIMAL32 4.2 sec 100000000
MathContext.DECIMAL64 9.5 sec 101073379.2938854
MathContext.DECIMAL128 13.9 sec 101073379.2738969453209089052948157

There are a lot of cases when you can avoid using BigDecimal arithmetic operations. For example, if you have a number represented as a String and you want to multiply it by 10^N (and N is your input), when it will be faster to write some code which moves decimal point left or right for a given input.

Other example - if you have a double and want to multiply or divide it by power of 2 - in general case you should have correct results for such calculations, because exponent part of floating point numbers represent a power of 2.

Converting a number to a string

How easy is it to convert a floating point number to a String (and get a correct result)? As it turns out - rather difficult. For double conversion, for example, you need to know a binary representation of a floating point number (IEEE-754). For details take a look at the source of sun.misc.FloatingDecimal class (usually not shipped with your JDK, so google it :) ), in particular, to a very short dtoa method, which does all the magic. One may conclude from that method comments that nobody dared to touch it since 1996 :)

Java 6 has an interesting sequence of calls required to convert a Double (object!) to String:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Double
public String toString() {
    return String.valueOf(value);
}
 
String
public static String valueOf(double d) {
    return Double.toString(d);
}
 
Double
public static String toString(double d) {
    return new FloatingDecimal(d).toJavaFormatString();
}
Double
public String toString() {
    return String.valueOf(value);
}

String
public static String valueOf(double d) {
    return Double.toString(d);
}

Double
public static String toString(double d) {
    return new FloatingDecimal(d).toJavaFormatString();
}

In Java 7 they finally went the shortest road:

1
2
3
4
jdk 7 Double
public String toString() {
    return toString(value);
}
jdk 7 Double
public String toString() {
    return toString(value);
}

BigDecimal has 3 methods for converting it to a String: toString, toPlainString and toEngineeringString. toString is caching result of toEngineeringString inside a BigDecimal and using it on subsequent calls (BigDecimal value is immutable, so this trick is possible for it). Let's see how fast Math.E (for BigDecimals it is converted to BigDecimal in advance) is converted to String 10M times:

Double.toString(double) BigDecimal.toPlainString BigDecimal.toEngineeringString
4.1 sec 12.4 sec 12.5 sec

By the way, it is a bad idea to convert double to BigDecimal: double is converted to String first and then a String is converted to a BigDecimal. So, if your algorithm gets a String as an input, convert it straight to BigDecimal. As a bonus, you will avoid any rounding errors.

See also

If you want to see how to efficiently implement a Money class, take a look at Implementing a high performance Money class article.

Summary

  • If you want to implement fast and correct monetary arithmetic operations in Java, stick to the following rules:

    1. Store monetary values in the smallest currency units (for example, cents) in long variables.
    2. Avoid working with non-integral values while using double (calculate in the smallest currency units).
    3. Add/subtract using long.
    4. Round any multiplication/division results using Math.round/rint/ceil/floor (per your system requirements).
    5. Your calculations should fit into 52 bits (double precision).
  • Always use MathContext for BigDecimal multiplication and division in order to avoid ArithmeticException for infinitely long decimal results. Don't use MathContext.UNLIMITED for that reason - it is equivalent to no context at all.
  • Do not convert double to BigDecimal, instead convert String to BigDecimal when possible.

One thought on “Using double/long vs BigDecimal for monetary calculations

  1. Pingback: Implementing a high performance Money class   | Java Performance Tuning Guide

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code lang=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" extra="">