Thursday, September 30, 2010

Double Trouble

You've heard it said: the cardinal rule of Java programming... "Never use a double to hold currency". Ok, well it might not be the cardinal rule, but its pretty important. But every once in a while you think to yourself, "aww... just this once. It won't hurt anything". Let me save you the headache and explain why this is a terrible idea. I'll also show you some alternatives for all your money problems (programmatically speaking, of course).

Everyone knows that we humans use the base 10 numbering system. It makes sense, ten fingers... base 10... viola. But your computer was unfortunately created with only two fingers. For those less familiar with the topic of bases in numbering systems, you can think of it as the 'roll-over' point when you're counting. For base 10 (decimal), counting rolls over after you get to nine. For base 2 (binary), counting rolls over after 1. For example...
00 (0)
01 (1)
10 (2)
11 (3) 

Now, I'm not going to get into the nitty gritty of how your computer stores numbers for each of the common Java data types (browse some wikipedia articles if you're interested). As a general rule, any whole number can be exactly represented using binary notation. Any fractional number is not guaranteed to have an exact representation in binary. Just as 1/3 cannot be precisely represented in decimal format, many fractional numbers also cannot be precisely represented in binary format. Here's a good test to run if you aren't convinced:
  0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f
//Result: 0.8000001

  0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d + 0.1d
//Result: 0.7999999999999999
You would expect adding 0.1 eight times would give you exacly 0.8, but the results can sometimes surprise you. You can imagine the havoc this could cause if you are attempting to perform calculations on money stored as Doubles or Floats.

Solution #1:
Only deal with cents. This means multiplying your decimal number by 100 and storing it as a Long. This is a safe operation on currency because, even though your initial decimal value is not exactly represented in binary format, it will be properly rounded by the JVM when converting it to a Long. However, be sure to do this before any other calculations are performed on it. If not, you could will introduce rounding errors.
double amount = 100.01d;
long amountInPennies = (long)(amount * 100);

Solution #2:
Use BigDecimal. This type can safely handle currency represented as dollars and cents because it essentially employs solution #1 for us behind the scenes. It either detects, or you can provide it, the precision that you require. The easiest (and safest) way to construct a BigDecimal is with the String constructor.

BigDecimal num = new BigDecimal("100.01");

It will detect a number with two decimal places (hundredths) and will internally multiply that number by 100, storing the result as a whole number. The BigDecimal class provides methods for all common arithmetic operations you may need to perform. This guarantees that you won't be plagued by the rounding errors and inaccuracies inherent in doubles and floats.

1 comment: