» Home and Blogs

  » Essays

  » Software

  » Prime Brokerage

  » Web Page Optimisation

  » Interviewing

  » Publications

  » Resources

  » Book Reviews

  » About Me




.

..

BigDecimal, double and Rounding

BigDecimal, double and Rounding

 

This subject came about thanks to my involvement with the Intelligent Pricing project which introduced me to the client side pricing functionality found in:

 

          com.barcap.fox.client.pricing

 

and it’s sub packages. This pricing code makes heavy use of classes from:

 

          com.barcap.fox.client.dataobjects

 

and it’s sub packages.

 

Browsing through the ClearQuest cases raised against the Intelligent Pricing project, I noticed that almost all the client side cases were to do with rounding issues.

 

I smelled a rat but had no remit to follow it up.

 

However, I can say that floating point arithmetic is being undertaken in the client side pricing code and may be impacted by the well known problem that not all floating point numbers can be represents as a binary fraction.

 

Take this example: Start with a relatively large floating point number (a price, for instance) and add a few, relatively small, floating point numbers (spreads, say) to it.

 

After adding each spread we round the resulting price to two decimal places.

 

Are we expecting any problems?

 

The answer (in Java) depends on whether or not you are using double or BigDecimal to hold your prices and spreads.

 

Take the simple case where we add 0.0001 to 1.2345 10 times. In the real world, this equates to a sequence like so:

 

Original Value         Rounded Value

1.2345

1.2346

1.2347

1.2348

1.2349

1.2350

1.2351

1.2352

1.2353

1.2354 1.23

1.23

1.23

1.23

1.23

1.24

1.24

1.24

1.24

1.24

 

 

Ok, using a simple “half round up” algorithm, do we get this in a Java Program?

         

    public static void main(String[] argv) {

 

        int places= 2;

        double value= 1.2345;

 

        for (int i= 0; i < 10; i++) {

            System.err.println(

                "+0.000" +i +" rounded= " +round(value, places));

            value += 0.0001;

        }   

    }

 

    static double round(double value, int places) {

        double power= 1;

        while (places-- > 0)

            power *= 10.0;

        return Math.round(value*power)/power;

    }

 

 

Nope. This produces a rounded sequence like so.

 

1.23

1.23

1.23

1.23

1.23

1.23

1.24

1.24

1.24

1.24

 

The item in bold is wrong!

 

Now, I said this problem had a lot to do with java.math.BigDecimal, so lets’ replace the basic Math round() method with this one:

 

    static double round(double value, int places) {

        BigDecimal decimal= new BigDecimal(value);

        decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);

        return decimal.doubleValue();

    }

 

Did that fix it?

 

Well, we get this rounded sequence:

 

1.23

1.23

1.23

1.23

1.23

1.23

1.24

1.24

1.24

1.24

 

The item in bold is still wrong!

 

This may be the origin of a rather frightening belief I’ve heard bandied about here that java.math.BigDecimal is “broken”.

 

In reality, the aspect of this that is truly broken is the use of double to store the price.

 

Consider this from the java.math.BigDecimal javadoc.

 

 

public BigDecimal(double val)

 

Translates a double into a BigDecimal. The scale of the BigDecimal is the smallest value such that (10scale * val) is an integer.

Note: the results of this constructor can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625. This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to .1, appearances notwithstanding.

The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one.

 

I don’t think anyone could argue with that.

 

So, let’s try again with the price held as a string:

 

   public static void main(String[] argv) {

 

        int places= 2;

        int integerPart= 1;

        int decimalPart= 2345;

 

        for (int i= 0; i < 10; i++) {

            String value= integerPart +"." +decimalPart;

            System.err.println(

                "+0.000" +i +" rounded= " +round(value, places));

            decimalPart += 1;

        }

    }

 

 

    static double round(String value, int places) {

        BigDecimal decimal= new BigDecimal(value);

        decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);

        return decimal.doubleValue();

    }

 

We now get this rounded sequence:

 

1.23

1.23

1.23

1.23

1.23

1.24

1.24

1.24

1.24

1.24

 

Our bold item is now correct!

 

However, carrying around the integer and decimal part of a price is a bit cumbersome, so let’s delegate that responsibility to BigDecimal and try again:

 

    public static void main(String[] argv) {

 

        int places= 2;

        BigDecimal value= new BigDecimal("1.2345");

        BigDecimal spread= new BigDecimal("0.0001");

 

        for (int i= 0; i < 10; i++) {

            System.err.println(

                "+0.000" +i +" rounded= " +round(value, places));

            value= value.add(spread);

        }

    }

 

    static double round(BigDecimal value, int places) {

        return value.setScale(places,

            BigDecimal.ROUND_HALF_UP).doubleValue();

    }

 

What do we get now?

 

1.23

1.23

1.23

1.23

1.23

1.24

1.24

1.24

1.24

1.24

 

Bingo.

 

Here’s a composition of all the methods to see who gets what:

 

import java.math.BigDecimal;

 

public class Rounding {

 

    public static void main(String[] argv) {

 

        int places= 2;

        double value= 1.2345;

        int integerPart= 1;

        int decimalPart= 2345;

        BigDecimal bdv= new BigDecimal("1.2345");

        BigDecimal spread= new BigDecimal("0.0001");

 

        for (int i= 0; i < 10; i++) {

            String szValue= integerPart +"." +decimalPart;

            double math= math(value, places);

            double decimal= decimal(value, places);

            double string= string(szValue, places);

            double round= round(bdv, places);

            boolean match=

                (math == decimal) && (decimal == string) &&

                (decimal == round);

            System.err.println(

                "places= " +places +", szValue= " +szValue

                +", value= " +value

                + " => " + math +", " +decimal +", " +string +", " +round

                + (match ? "" : "  whoops!"));

            value += 0.0001;

            decimalPart += 1;

            bdv= bdv.add(spread);

        }

    }

 

    static double math(double value, int places) {

        double power= 1;

        while (places-- > 0)

            power *= 10.0;

        return Math.round(value*power)/power;

    }

 

    static double decimal(double value, int places) {

        BigDecimal decimal= new BigDecimal(value);

        decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);

        return decimal.doubleValue();

    }

 

    static double string(String value, int places) {

        BigDecimal decimal= new BigDecimal(value);

        decimal= decimal.setScale(places, BigDecimal.ROUND_HALF_UP);

        return decimal.doubleValue();

    }

 

    static double round(BigDecimal value, int places) {

        return value.setScale(places,

           BigDecimal.ROUND_HALF_UP).doubleValue();

    }

}

 

Which produces the following:

 

places= 2, szValue= 1.2345, value= 1.2345 => 1.23, 1.23, 1.23, 1.23

places= 2, szValue= 1.2346, value= 1.2346 => 1.23, 1.23, 1.23, 1.23

places= 2, szValue= 1.2347, value= 1.2347 => 1.23, 1.23, 1.23, 1.23

places= 2, szValue= 1.2348, value= 1.2348 => 1.23, 1.23, 1.23, 1.23

places= 2, szValue= 1.2349, value= 1.2348999999999999 => 1.23, 1.23, 1.23, 1.23

places= 2, szValue= 1.2350, value= 1.2349999999999999 => 1.23, 1.23, 1.24, 1.24  whoops!

places= 2, szValue= 1.2351, value= 1.2350999999999999 => 1.24, 1.24, 1.24, 1.24

places= 2, szValue= 1.2352, value= 1.2351999999999999 => 1.24, 1.24, 1.24, 1.24

places= 2, szValue= 1.2353, value= 1.2352999999999998 => 1.24, 1.24, 1.24, 1.24

places= 2, szValue= 1.2354, value= 1.2353999999999998 => 1.24, 1.24, 1.24, 1.24

 

Which clearly demonstrates that the problem is with floating point arithmetic (adding double to double) not with java.math.BigDecimal.

 















This site is © Copyright BenStopford.com 2003-2005, All Rights Reserved