Saturday, September 17, 2011

BigDecimal why and how ?

Hello



In this article, I present some examples how to use BigDicimal Java type.


Problems with Double type in Java



The Double type in Java stores a number in two parts : significant digits et exponent.


Significant digits × base^exponent

That is why it can store a very large number or very small number but it loses precision.

Example

double d = 999888777666555555333222111.0;
System.out.println("my double " + d);

This code prints the following result : 
my double 9.998887776665556E26

In general, when the "significant digits" part is too long, the double type loses precision.

What is why when we add a small number with a big number, the precision is lost (with small number part gets lost).

To avoid the number to be printed with significant/exponent form, we can use DecimalFormat utility.

// this format prints number with optional 2-digit fractional part
  // # = optional part
  //  ex. "12", "12,2", "12,25"  
  static DecimalFormat f = new DecimalFormat("#0.##");

  // this format prints number with 2-digit fractional part
  //  ex. "12,00", "12,20", "12,25"  
  static DecimalFormat f2 = new DecimalFormat("#0.00");


Solution : BigDecimal type

The BigDecimal type stores a number as if it is a string.
It allows us to perform add or subtract operations without losing precision.


Example

double d1 = 1000000000;
  double d2 = 0.000000003;

  double d3 = d1 + d2;

  double d4 = d3 - d1;
  System.out.println("d4=" + d4 + " or " + f.format(d4));

  BigDecimal bc1 = new BigDecimal("1000000000");
  BigDecimal bc2 = new BigDecimal("0.000000003");

  BigDecimal bc3 = bc1.add(bc2);
  BigDecimal bc4 = bc3.subtract(bc1);
  System.out.println("bc4=" + bc4 + " or " + f.format(bc4));

This code prints the following result : 

d4=0.0 or 0
bc4=3E-9 or 0,000000003




Manage precision in division


The division operation oftens make some precision lost, for example 1/3 = 0,33333...
If we truncate the number, we will lose some precision.


The BigDecimal type enables us to specify how long we want to keep the number.
The "scale" parameter specifies the number of digits in the fractional part to keep.
The RoundingMode parameter specifies what to do when the precision lost.
Usually, we can use HALF_UP
(Ex. with scale 2, HALF_UP : 0.128 -> 0,13; 0.123 -> 0.12; 0.125 -> 0.13)

For example, if we calculate 1/3 with scale 4, the result is 0,3333.
We how that the error margin is +/- 0,00005.




This is the complete example code :




import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;

public class TestBigDecimal {

 static DecimalFormat f = new DecimalFormat(
   "#0.#####################################");

 private static void ex1() {

  /*
    this example show how the Double type lose precision 
    when adding big value to small value
   */

  double d1 = 1000000000;
  double d2 = 0.000000003;

  double d3 = d1 + d2;

  double d4 = d3 - d1;
  System.out.println("d4=" + d4 + " or " + f.format(d4));

  BigDecimal bc1 = new BigDecimal("1000000000");
  BigDecimal bc2 = new BigDecimal("0.000000003");

  BigDecimal bc3 = bc1.add(bc2);
  BigDecimal bc4 = bc3.subtract(bc1);
  System.out.println("bc4=" + bc4 + " or " + f.format(bc4));
 }

 private static void ex2() {

  /*
    this example shows how to keep the desired precision when dividing numbers. 
    
   */

  BigDecimal result;

  System.out.println("test 1/2");
  result = new BigDecimal(1).divide(new BigDecimal(2));
  System.out.println("[division without precision lose] result="
    + f.format(result));

  try {
   System.out.println("test 1/3");
   result = new BigDecimal(1).divide(new BigDecimal(3));
   System.out.println("[division with precision lose] result="
     + f.format(result));
  } catch (Exception e) {
   System.out.println("division impossible " + e);
  }

  try {
   System.out.println("test 1/3 with scale 4");
   result = new BigDecimal(1).divide(new BigDecimal(3), 4,
     RoundingMode.HALF_UP);
   System.out.println("[division with scale 4] result="
     + f.format(result));
  } catch (Exception e) {
   System.out.println("division impossible " + e);
  }

  try {
   System.out.println("test 1/3 with scale 5");
   result = new BigDecimal(1).divide(new BigDecimal(3), 5,
     RoundingMode.HALF_UP);
   System.out.println("[division with scale 5] result= "
     + f.format(result));
  } catch (Exception e) {
   System.out.println("division impossible " + e);
  }

  /*
  **************
     Example of division and margin 
  **************
  */

  System.out.println("\n\n" + "Example (bc1/bc2)*bc2 ");

  BigDecimal bc1 = new BigDecimal("777555333.000123456789");
  BigDecimal bc2 = new BigDecimal("999777444");

  System.out.println("Expected result = bc1 = " + f.format(bc1));
  double d = bc1.doubleValue() * bc2.doubleValue() / bc2.doubleValue();
  System.out.println("Double calculation [lose precision] "
    + f.format(d));

  testDivisionWithScale(bc1, bc2, 5);
  testDivisionWithScale(bc1, bc2, 25);
 }

 /**
  * @param args
  */
 public static void main(final String[] args) {

  System.out.println("Example 1");
  ex1();
  System.out.println();

  System.out.println("Example 2");
  ex2();
  System.out.println();
 }

 private static void testDivisionWithScale(final BigDecimal bc1,
   final BigDecimal bc2, final int scale) {
  System.out.println("\n\n" + "Test big decimal calculation with scale "
    + scale);

  BigDecimal result;
  result = bc1.divide(bc2, scale, RoundingMode.HALF_UP);
  System.out.println("division result=" + f.format(result));
  result = bc1.divide(bc2, scale, RoundingMode.HALF_UP).multiply(bc2);
  System.out.println("multiplication result=" + f.format(result)
    + " real error=\n" + f.format(bc1.subtract(result)));

  BigDecimal margin = null;
  margin = new BigDecimal(Math.pow(10, -1 * +(scale)) * 0.5)
    .multiply(bc2);
  System.out
    .println("error margin (max error) +/- \n" + f.format(margin));
 }

}


******* result ********

Example 1
d4=0.0 or 0
bc4=3E-9 or 0,000000003

Example 2
test 1/2
[division without precision lose] result=0,5
test 1/3
division impossible java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
test 1/3 with scale 4
[division with scale 4] result=0,3333
test 1/3 with scale 5
[division with scale 5] result= 0,33333


Example (bc1/bc2)*bc2 
Expected result = bc1 = 777555333,000123456789
Double calculation [lose precision] 777555333,0001235


Test big decimal calculation with scale 5
division result=0,77773
multiplication result=777556911,52212 real error=
-1578,521996543211
error margin (max error) +/- 
4998,8872199999995620798057781308809666143


Test big decimal calculation with scale 25
division result=0,7777284211266157118753721
multiplication result=777555333,0001234567889999646869124 real error=
0,0000000000000000353130876
error margin (max error) +/- 
0,0000000000000000499888722000000019243





In this example, we see that in each calculation step, we can manage the precision.
The real result is between the calculated result +/- error margin.

By augmenting the scale parameter, the error margin reduces.

No comments: