Don't use floats for money
Filed under java,
Did you ever try this?
|
float total = 0.0f; Line line = new Line(0.3f, 1); // custom line object for (int i = 0; i < 100; i++) { total += line.getPrice() * line.getCount(); } // 0.3 * 100 = 30 System.out.println(total); // 29.999971 |
When this code sums 100 order lines with every line having one 0.30€ item, the resulting total is calculated to exactly 29.999971. The developer notices the strange behaviour and changes the float to the more precise double, only to get the result of 30.000001192092896.
The somewhat surprising result is of course due to the difference in representation of numbers by humans (in decimal format) and computers (in binary format IEEE 754). It always occurs in its most annoying form when you add fractional amounts of money or calculate the VAT.
|
double a = 1.14 * 75; // 85.5 represented as 85.4999... System.out.println(Math.round(a)); // surprising output: 85 |
There are business cases where you can not afford to lose precision. You lose precision when converting between decimal and binary and when rounding happens in not a well-defined manner or at indeterminate points. To avoid losing precision you must use fixed point or integer arithmetic's. This does not only apply to monetary values, but it is a frequent source of annoyance in business applications. Consequently a non-integer amount of money should never be stored in a floating point data type. Even a simple multiplication with an integer can already yield an inexact result. If you see a float or double in your financial code base, the code will most likely yield inexact results. Instead either a string or fixed point representation should be chosen. Both representations must define the precision (number of digits before and after the decimal point) that is stored. For calculations the class BigDecimal provides an excellent facility. The class can be used such that it throws runtime exceptions if precision is unexpectedly lost in an operation. This is very helpful to uproot subtle numerical bugs and enables the developer to correct the calculation. Once the calculations and rounding are done, you can safely use the NumberFormat objects and methods to format the BigDecimal objects for display.
But there are some pitfalls...
The results of constructor that takes a double can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625 This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to 0.1 The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one. BigDecimal supports the use of the compareTo method for comparing values. In most cases you will want to use the compareTo even for equals comparisons because the equals method considers two numbers with the exact same value but different scales to be not equal. In other words, the equals method considers 2, 2.0, 2.00, and 2.000 to be different numbers. The compareTo will consider all of them to be equal!
|
BigDecimal total = new BigDecimal("0"); // BigDecimal.ZERO exists since JAVA 1.5 for (int i = 0; i < 100; i++) { BigDecimal price = new BigDecimal(line.getPrice()); BigDecimal count = new BigDecimal(line.getCount()); total = total.add(price.multiply(count)); // BigDecimal is immutable! } total = total.setScale(2, BigDecimal.ROUND_HALF_UP); System.out.println(total); |
|
// remark if you use 1.14 as a type float as input the answer will still be // bad one, using the String type resolves this! BigDecimal a = (new BigDecimal("1.14")).multiply(new BigDecimal(75)); // 85.5 exact a = a.setScale(0, BigDecimal.ROUND_HALF_UP); System.out.println(a); // correct output: 86 |
Jan20