Search code examples
javabigdecimal

Does BigDecimal division have a shortcoming when the scale is known?


First of all, I should clarify that I'm asking this question only for enlightenment; I'm not desperate for an answer, but an answer might help me appreciate the BigDecimal class more.

I always had - and still have - a philosophy about decimal values guaranteed to have only a certain number of digits after the decimal point. Take, for example, dollar figures, which are guaranteed to have no more than 2 digits after the decimal point. When writing code for such applications, I obviously mustn't use a float with its rounding errors. Instead, I would use an integer (or long, or BigInteger, depending on how high the figure might get) to represent the number of cents. I would store and process a figure of $12.34 as the integer 1234, for example. Only when outputting the dollar figures (and possibly when inputting them) would I format the figures with a decimal point.

But now I'm reading a Java book that's pitching the BigDecimal class. It even uses dollar figures in some of the examples. I'm not sold on it because I feel that division could be awkward, unless you have an idea how to do it.

Let's say you're writing a program that divides a dollar figure. For instance, maybe many people want to split the money equally. And let's say, for example, that one day the program runs with the parameters such that it should divide $1.21 by 3. You would want the program to come up with the result of 40 cents with a remainder of 1 cent. In other words, the result should be 0.40 and 0.01. If the figures are stored as integers representing numbers of cents, that will work fine. But can it work with BigDecimals that represent dollars?

Obviously, this code won't work:

import java.math.BigDecimal;
    
    public class test {
    
        public static void main(String[] args) {
            BigDecimal total = new BigDecimal("1.21");
            BigDecimal divider = new BigDecimal(3);
            BigDecimal results[]= new BigDecimal[2];
            results = total.divideAndRemainder(divider);
    
            for (BigDecimal result : results) System.out.println(result);   
        }
    }

You'll get a result of 0.00 and 1.21, not 0.40 and 0.01 like you want.

The book implies that the solution is to set the scale. So I tried that:

    BigDecimal total = new BigDecimal("1.21");
    total = total.setScale(2);
    BigDecimal divider = new BigDecimal(3);
    divider = divider.setScale(2);
    BigDecimal results[]= new BigDecimal[2];
    results = total.divideAndRemainder(divider);

The result is still 0.00 and 1.21.

Maybe the result should be in a variable that already has the scale set? I tried that:

    BigDecimal total = new BigDecimal("1.21");
    total = total.setScale(2);
    BigDecimal divider = new BigDecimal(3);
    divider = divider.setScale(2);
    BigDecimal results[]= new BigDecimal[2];
    results[0] = new BigDecimal(0);
    results[0] = results[0].setScale(2);
    results[1] = new BigDecimal(0);
    results[1] = results[0].setScale(2);
    results = total.divideAndRemainder(divider);

That was very awkward! And the result is still the same.

I could, of course, multiply the $1.21 by 100, then divide by 3, and then divide by 100, but that would be a good argument for using my integer approach in the first place and skipping those steps. Is there an elegant way to divide BigDecimal values the way I want to? Or should I stick to storing cent values in integers?


Solution

  • The fundamental issue is your use of divideAndRemainder in the first place, which does not do what you want it to do; as its documentation specifies, it divides only to an integer value -- a "dollar" amount, without cents. It is analogous to divideToIntegralValue, not divide:

    Returns a two-element BigDecimal array containing the result of divideToIntegralValue followed by the result of remainder on the two operands.

    You need to use .divide instead, presumably specifying a RoundingMode, and then compute the remainder yourself.

    BigDecimal division = total.divide(divisor, RoundingMode.DOWN);
    System.out.println(division); // 0.40
    System.out.println(total.subtract(division.multiply(divisor))); // 0.01