2011-12-30

Some hints for calculations with floating point numbers

This is an old posting from 2009 which I am publishing again.

Here are some (slightly simplifying) hints or rules for the usage of BigDecimal.
  1. Use BigDecimal.valueOf(double) or new BigDecimal(String) to create BigDecimal objects.
    Never use new BigDecimal(double), unless you know what you are doing. Be aware that 0.1 is an infinite decimal number, 0.5 is not, so new BigDecimal(0.5) is fine, but not new BigDecimal(0.1). And if you do not want to think about the difference every time, do not use new BigDecimal(...) at all.
  2. If you want to compare BigDecimal objects, be aware: equals() compares the value and the scale, that means 1.001 is not equal to 1.00100. Use signum() and compareTo().
  3. Use setScale to round the BigDecimal value.
  4. Use a Context for rounding.
  5. Be aware that setScale or a division may throw an ArithmeticException, so always use a RoundingMode or a RoundingContext.



package testdrive;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.math.BigDecimal;

import org.junit.Test;



public class DoubleCalcTest
{

    @Test
    public void addDouble()
    {
        /**
         * This is actually a very old example which shows that calculations with double values can lead to unexpected
         * results.
         */
        double a = 0.1;
        double sum = 0;
        for ( int i = 0; i < 10; i++ )
        {
            sum += a;
        }
        // sum should be 1, but it isn't
        assertTrue( sum != 1 );
        /**
         * There is no exact binary representation for 0.1, so repeated calculations with 0.1 will probably have wrong
         * results.
         */
    }



    @Test
    public void createBigDecimalCorrectly()
    {
        /**
         * This shows the usage of the static valueOf method. In such a way, a BigDecimal object can be created from a
         * double value. Another possibility is the constructor with the String which is shown here just for fun. I would
         * recommend to always use the static valueOf method.
         */
        BigDecimal a = BigDecimal.valueOf( 0.1 );
        BigDecimal sum = new BigDecimal( "0" ); // Just to show this possibility.
        // OR: BigDecimal sum = BigDecimal.ZERO;

        for ( int i = 0; i < 10; i++ )
        {
            sum = sum.add( a );
        }
        BigDecimal expected = BigDecimal.valueOf( 1.0 );

        /**
         * If the two BigDecimal objects may have different scale, then use signum() and compareTo().
         */
        assertTrue( expected.signum() == sum.signum() );
        assertTrue( expected.compareTo( sum ) == 0 ); // == 0 means, they are equal.
    }



    @Test
    public void createBigDecimalInAWrongWay()
    {
        /**
         * Never use the constructor with double values, unless the binary representation of the double values is what you
         * want. So actually it is better to use the constructor with a String or the static value of method.

         * This example shows the problem with the constructor which has a double as parameter.
         */
        BigDecimal a = new BigDecimal( 0.1 );
        BigDecimal sum = new BigDecimal( 0.0 );
        for ( int i = 0; i < 10; i++ )
        {
            sum = sum.add( a );
        }
        BigDecimal expected = new BigDecimal( 1.0 );

        /**
         * The value of the expected variable (i.e. 1.0) is not the same as the value of the sum variable. The reason is
         * that there is no exact representation of 0.1 in the binary system, so the BigDecimal does not represent 0.1 but
         * something similar.
         */
        assertFalse( expected.equals( sum ) );

        /**
         * If the two BigDecimal objects may have different scale, then use signum() and compareTo().
         */
        assertTrue( expected.signum() == sum.signum() );
        assertTrue( expected.compareTo( sum ) != 0 ); // != 0 means, they are different.

        /**
         * A hint to JUnit:

         * 

         * If you use JUnit, then be aware that assertEquals does use the Number.longValue() method to compare BigDecimal
         * objects, so the result is often wrong. So please use the assertTrue or assertFalse methods.
         */
        assertEquals( expected, sum );
    }

}


No comments:

Post a Comment