Floating point numbers and their java representation - Part 2

If you aren't familiar with IEEE 754 standard or inner bit-wise representation of float and double values, please read https://kaustubhsaha.postach.io/post/floating-point-numbers-and-their-java-representation-part-1 first

From the examples in https://kaustubhsaha.postach.io/post/floating-point-numbers-and-their-java-representation-part-1 it is pretty clear that not every number can be exactly represented in binary32 or binary64 formats and for numbers that cant be exactly represented in binary32 or binary64 format, the level of accuracy is exactly determined by number of bits in the mantissa (which is 23 and 52 for float and double datatypes respectively). This is where BigDecimal class comes handy. The java.math.BigDecimal class provides operations for arithmetic, scale manipulation, rounding, comparison, hashing, and format conversion and is also an immutable representation of a number.

Typically math operations in Java are done in double datatype which offer no way to control how the number is rounded or to limit the precision in computation. This is where BigDecimal class comes handy especially in financial/currency-based calculations

Currency calculations require precision to a specific degree, such as two digits after the decimal for most currencies. They also require a specific type of rounding behavior, such as always rounding up in the case of taxes.

For example, suppose we have a product which costs 10.00 in a given currency and the local sales tax is 0.0825, or 8.25%. If we work it out on paper, the tax amount is, 10.00 * 0.0825 = 0.825

Because our precision for the currency is two digits after the decimal, we need to round the 0.825 figure. Also, because this is a tax, it is good practice to always round up to the next highest cent. That way when the accounts are balanced at the end of the day, we never find ourselves underpaying taxes.
0.825 -> 0.83

And so the total we charge to the customer is 10.83 in the local currency and pay 0.83 to the tax collector. Note that if we sold 1000 of these, we would have overpaid the collector by this much,
1000 * (0.83 - 0.825) = 5.00

Another important issue is where to do the rounding in a given computation. Suppose we sold Liquid Nitrogen at 0.528361 per liter. A customer comes in and buys 100.00 liters, so we write out the total price,
100.0 * 0.528361 = 52.8361

Because this isn't a tax, we can round this either up or down at our discretion. Suppose we round according to standard rounding rules: If the next significant digit is less than 5, then round down. Otherwise round up. This gives us a figure of 52.84 for the final price.

Now suppose we want to give a promotional discount of 5% off the entire purchase. Do we apply this discount on the 52.8361 figure or the 52.84 figure? What's the difference?

Calculation 1: 52.8361 * 0.95 = 50.194295 = 50.19

Calculation 2: 52.84 * 0.95 = 50.198 = 50.20

Note that we rounded the final figure by using the standard rounding rule.

See how there's a difference of one cent between the two figures? The old code never bothered to consider rounding, so it always did computations as in Calculation 1. But in the new code we always round before applying promotions, taxes, and so on, just like in Calculation 2. This is one of the main reasons for the one cent error.

From the examples in the previous section, it should be clear that we need two things:
  • Ability to specify a scale, which represents the number of digits after the decimal place
  • Ability to specify a rounding method
The java.math.BigDecimal class handles both of these considerations. Note that this is not a matter of accuracy, nor is it a matter of precision. It is a matter of meeting the expectations of humans who use base 10 for calculations instead of base 2

BigDecimal has three different notions for limiting numbers
intVal is the unscaled digit representation (e.g 12.34 and 1.234 have the same intVal - 1234)
Precision is the overall limit of, well, precision, in a number. E.g. 1234 and 12.34 have the same precision (4 decimal digits)
Scale is the limit of the digits after the decimal point 3.34 and 234.25 have the same scale (2 digits after the decimal point)
So, clearly BigDecimal is more centered towards the digits of a number when expressed in base 10 instead of the numerical value

A java.math.BigDecimal object can be created in multiple ways :
1. by passing a double value :
public BigDecimal(double val)

First the double value is translated into sign, exponent and significand as per IEEE 754 standard such that :
val = sign * significand * 2^ (exponent - 127)
Note that most numbers cannot be exactly represented in this format and hence there are some approximations made

Once sign,significand and exponent are calculated, the integer representation (sans decimal point), precision and scale are calculated from those values
So if there were approximations made in the calculation of sign, significand and exponent - those approximations got propagated to the integer representation as well
Hence, its not a good idea to use the double constructor of BigDecimal

2. by passing a String value :
public BigDecimal (String val)


First the String val is converted to a char array. Now if val is a legitimate String representation of a number, it can have just '+'/'-' (at beginning) and '.' characters apart from numeric values
Next, the code iterates over the charArray and computes the integer representation, scale and precision

Clearly as long as the input String representation was perfect, no approximations can creep into the value representation of the BigDecimal. Hence its a much safer option than the double constructor

Note that BigDecimal has its own set of quirks and pitfalls too.
1. Since BigDecimal uses the same object state for both data accuracy as well as data presentation. states that represent the same value but are presented in different ways are not 'equal'
For example, both the equals invocations will evaluate to false here:
BigDecimal b1 = new BigDecimal("13");
BigDecimal b2 = new BigDecimal("13.0");
BigDecimal b3 = new BigDecimal("13.00");
System.out.println(b1.equals(b2));
System.out.println(b2.equals(b3));
Always use compareTo() for comparing BigDecimal objects (instead of equals()). For the same reason, never use BigDecimal objects as keys in a HashMap

2. Big Decimals are never interned. So == operator used on two BigDecimal objects will always return false unless they point to the same object on heap ( for certain values though 0-10 there's an internal cache but you really don't want to write a piece of code that wont work if the range goes above 10)

3. Because BigDecimal encapsulates data with presentation, they won’t be able to run any calculation that wouldn’t result in a representable answer. Take the following code snippet as an example:
BigDecimal value = new BigDecimal(1);
value = value.divide(new BigDecimal(3));
Primitive numeric types would just swallow this and represent the result as best they could, while a BigDecimal throws an Arithmetic Exception instead of attempting to represent a recurring number unless a rounding mode is specified

4. BigDecimals don't play nice with databases. According to the JDBC spec database drivers must implement a getBigDecimal, setBigDecimal and updateBigDecimal functions. They seem like a great idea, until you ponder that your database may not have a suitable storage type for these values. When storing a BigDecimal in a database, it’s common to type the column as a DECIMAL or REAL SQL type. These are both standard floating-point types, with all the rounding errors that implies. The only practical solution which will keep all the BigDecimal functionality and accuracy in a database is to type the amounts a BLOB columns

comments powered by Disqus