Monday, August 8, 2011

BigDecimal, Groovy, and the Non-terminating Decimal Expansion

Not all Java developers need the precision that BigDecimal offers. However, those who do usually don't have to use BigDecimal for very long before running into the java.lang.ArithmeticException with message "Non-terminating decimal expansion; no exact representable decimal result." Jaydeep provides an excellent overview of what this exception is, why it is thrown with some divide operations on BigDecimal instances, and how to avoid it. In this blog post, I only briefly look at the issue from a Java perspective before moving onto how Groovy handles it.

The following simple code snippet demonstrates how easy it is to encounter this ArithmeticException when dividing BigDecimal instances. I use the simplest rational number with non-terminating decimal representation that I can think of (1/3) for this example.

Main.java Demonstrating Arithmetic Exception on BigDecimal.divide
package dustin.examples;

import java.math.BigDecimal;

import static java.lang.System.out;

public class Main
{
   public static void main(final String[] arguments)
   {
      final BigDecimal dividend = new BigDecimal("1");
      final BigDecimal divisor = new BigDecimal("3");
      out.println("1/3 = " + dividend.divide(divisor));
   }
}

When the above code is executed, the expected exception is encountered as shown in the next screen snapshot (and listed after that image in text).


The text version of the above screen snapshot is shown next.

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
 at java.math.BigDecimal.divide(BigDecimal.java:1616)
 at dustin.examples.Main.main(Main.java:13)

It is well-known among Groovy developers that Groovy automatically and implicitly often uses BigDecimal for any floating-point numbers. So, does Groovy run into this same problem? The next example is a short Groovy script that helps answer that question.

demoBigDecimalDivide.groovy
#!/usr/bin/env groovy
def dividend = 1.0
def divisor = 3.0
def quotient = dividend / divisor
println "1/3 = ${quotient}"

Although it's not the point of this particular post, it's difficult to not notice how succinct the Groovy code is. I don't show it here, but if I printed out the .getClass() results on the three defined numerals in this script, they'd all come back as java.math.BigDecimal. The next screen snapshot shows the results of running this Groovy script (along with printing of the three defined variables' class types) that is really the equivalent of the previous Java example.


The Groovy script does not throw an exception! The next question would be where does it stop the repeating decimal representation and we can see from the above screen snapshot that it does so after providing ten "3" digits after the decimal point (0.3333333333).

There are often times when not using Groovy that it is advantageous to have a rounded representation of a quotient rather than an ArithmeticException when the quotient cannot be represented exactly. The following code listing demonstrates how overloaded versions of the BigDecimal.divide methods can be used to obtain a rounded answer.

Main.java Demonstrating Overloaded Versions of BigDecimal.divide
package dustin.examples;

import java.math.BigDecimal;
import java.math.RoundingMode;
import static java.lang.System.out;

/**
 * Demonstrates use of some of the overloaded BigDecimal.divide() methods with
 * specific focus on using scale and rounding mode to avoid ArithmeticException
 * when an exact quotient representation is not available.
 */
public class Main
{
   /**
    * Perform BigDecimal division without specifying scale or rounding.
    *
    * @param dividend Dividend in division.
    * @param divisor Divisor in division.
    * @return Quotient from dividing dividend by divisor; returns null if the
    *    requested division results in a non-terminating representation.
    */
   public static BigDecimal performBigDecimalDivisionWithoutScaleOrRounding(
      final BigDecimal dividend, final BigDecimal divisor)
   {
      BigDecimal quotient = null;
      try
      {
         quotient = dividend.divide(divisor);
      }
      catch(ArithmeticException arithEx)
      {
         out.print(dividend.toPlainString() + "/" + divisor.toPlainString() + ": ");
         out.println("leads to this exception: ");
         out.println("\t" + arithEx.toString());
      }
      return quotient;
   }

   /**
    * Perform BigDecimal division of specified divisor by specified dividend and
    * using provided RoundingMode. Note that a RoundingMode of UNNECESSARY could
    * potentially lead to an ArithmeticException if the quotient cannot be
    * represented exactly or if the provided divisor is zero.
    *
    * @param dividend Dividend in division.
    * @param divisor Divisor in division.
    * @param rounding Rounding Mode to be used in division if exact representation
    *    cannot be obtained with provided scale (UNNECESSARY could lead to an
    *    ArithmeticException being thrown).
    * @return Quotient from dividing dividend by divisor; returns null if the
    *    requested division results in a non-terminating representation.
    * @throw ArithmeticException Thrown if the divisor is zero or if the quotient
    *    cannot be represented exactly and the provided RoundingMode is
    *    UNNECESSARY.
    */
   public static BigDecimal performBigDecimalDivisionWithSpecifiedRounding(
      final BigDecimal dividend, final BigDecimal divisor, final RoundingMode rounding)
   {
      return dividend.divide(divisor, rounding);
   }

   /**
    * Perform BigDecimal division of specified divisor by specified dividend and
    * using provided RoundingMode and provided scale. Note that a RoundingMode
    * of UNNECESSARY could potentially lead to an ArithmeticException if the
    * quotient cannot be represented exactly or if the provided divisor is zero.
    *
    * @param dividend Dividend in division.
    * @param divisor Divisor in division.
    * @param scale Scale of quotient.
    * @param rounding Rounding Mode to be used in division if exact representation
    *    cannot be obtained with provided scale (UNNECESSARY could lead to an
    *    ArithmeticException being thrown).
    * @return Quotient from dividing dividend by divisor; returns null if the
    *    requested division results in a non-terminating representation.
    * @throw ArithmeticException Thrown if the divisor is zero or if the quotient
    *    cannot be represented exactly and the provided RoundingMode is
    *    UNNECESSARY.
    */
   public static BigDecimal performBigDecimalDivisionWithSpecifiedRoundingAndScale(
      final BigDecimal dividend, final BigDecimal divisor,
      final int scale, final RoundingMode rounding)
   {
      return dividend.divide(divisor, scale, rounding);
   }

   /**
    * Main driver of demonstration of overloaded BigDecimal.divide methods.
    *
    * @param arguments Command line arguments: none expected.
    */
   public static void main(final String[] arguments)
   {
      final BigDecimal one = new BigDecimal("1");
      final BigDecimal two = new BigDecimal("2");
      final BigDecimal three = new BigDecimal("3");
      final BigDecimal zero = new BigDecimal("0");
      out.println("1/3 = " + performBigDecimalDivisionWithoutScaleOrRounding(one, three));
      out.println("2/0 = " + performBigDecimalDivisionWithoutScaleOrRounding(two, zero));
      out.println("1/3 = " + performBigDecimalDivisionWithSpecifiedRounding(one, three, RoundingMode.HALF_UP));
      for (int i = 0 ; i < 15; i++)
      {
         out.println(  "1/3 [scale " + i + "] = "
                     + performBigDecimalDivisionWithSpecifiedRoundingAndScale(
                           one, three, i, RoundingMode.HALF_UP));
      }
      for (int i = 0 ; i < 15; i++)
      {
         out.println(  "2/3 [scale " + i + "] = "
                     + performBigDecimalDivisionWithSpecifiedRoundingAndScale(
                           two, three, i, RoundingMode.HALF_UP));
      }
   }
}
The output from running the above is shown next.
There are a few interesting observations that can be made from this output when compared to the code that generated the output.
  • The ArithmeticException is thrown when BigDecimal.divide cannot generate an exact representation and there is no rounding used or rounding type of UNNECESSARY is specified.
  • An ArithmeticException is also thrown by the overloaded versions of BigDecimal.divide when the divisor is zero (cannot divide by zero).
  • The effects of RoundingMode are shown for quotients that cannot be represented exactly. Although only RoundingMode.HALF_UP is used, it does demonstrate those effects by representing both 1/3 and 2/3.
  • The effects of scale are dramatically demonstrated via iterations of 15 different scales from 0 to 14. From this, we can see that Groovy implicitly used a scale of 10 rather than throwing an ArithmeticException.
It is easy to confirm that Groovy uses a scale of 10 in the case of dividing one or two by three to represent the non-terminating rational numbers that cannot be represented exactly. The revised Groovy script does this.
#!/usr/bin/env groovy
def one = 1.0
def two = 2.0
def three = 3.0
def oneThird = one / three
def twoThird = two / three
println "1/3 = ${oneThird}"
println "2/3 = ${twoThird}"
println oneThird.dump()
println twoThird.dump()
The output from running this script (shown in the next screen snapshot) proves that the scale is 10 and that the rounding mode is one that allows for 1/3 to end with a "3" and for 2/3 to end with a "7."
Groovy uses BigDecimal implicitly, so it is generally desirable that it provides overloaded operators and implicit rounding and scaling. However, there may be cases where one needs a different level of scaling. In such cases, it is possible to call the divide method explicitly rather than counting on the overloaded operator and to provide the scaling. This is demonstrated in the next Groovy code listing.
#!/usr/bin/env groovy
import static java.math.RoundingMode.HALF_UP
def one = 1.0
def two = 2.0
def three = 3.0
def oneThird = one.divide(three, 15, HALF_UP)
def twoThird = two.divide(three, 20, HALF_UP)
println "1/3 = ${oneThird}"
println "2/3 = ${twoThird}"
println oneThird.dump()
println twoThird.dump()
The output from the above is shown in the next screen snapshot, which includes the output from the previous run as well for easy comparison.
There might be a situation in which the developer actually wants an ArithmeticException to be thrown to indicate that an exact representation is not available for a division call. This can be enforced in Groovy by making an explicit divide call (via method name rather than overloaded / operator) and providing a rounding mode of UNNECESSARY as shown in the next code listing (assumes local variables one and three as defined above).
def exception = one.divide(three, 10, RoundingMode.UNNECESSARY)
When executed, the ArithmeticException is encountered with a message simply stating "Rounding necessary." This is shown in the next screen snapshot.
Conclusion Java's BigDecimal is handy when one desires better precision than double or float can support. However, it must throw an ArithmeticException when a quotient result of one of the overloaded BigDecimal.divide calls cannot be represented exactly so that client code does not presume that the quotient provided is an exactly representation. Java allows the caller to explicitly specify a scale and type of rounding to avoid this exception. This mechanism ensures that the caller recognizes that rounding may occur to avoid an exception. The caller gets to specify the rules of that rounding and of the scale.

Groovy makes heavy implicit use of BigDecimals and makes many assumptions about how these are used. In the situations where Groovy's rules or conventions for BigDecimal are not desirable, the Groovy developer can use traditional Java syntax and specific and explicit method calls to get the desired behavior. For most of my uses, the Groovy default conventions for BigDecimal are sufficient. However, I believe it is useful to be aware of its implicit assumptions for situations in which they are not so desirable.

No comments: