Precision Error while converting dollars/gallon to dollars/liter and back

66 Views Asked by At

The following code has a precision error but I'm at a loss as to how to resolve it. Given an initial value set in dollar/gal of $3.95, when change the ChoiceBox to "dollars/liter" I get $1.04, but when I change it back to dollar/gal I'm getting $3.94 instead of $3.95

if ("dollar/gal".equals(newValue)) {
   double pricePerLiter = Double.parseDouble(txtFields.get(1).getText());
   BigDecimal pricePerGallon =  BigDecimal.valueOf(pricePerLiter * 3.785);                        
   pricePerGallon = pricePerGallon.setScale(2, RoundingMode.HALF_UP);                        
   txtFields.get(1).setText(pricePerGallon.toString());

} else if ("dollars/liter".equals(newValue)) {

  double pricePerGallon = Double.parseDouble(txtFields.get(1).getText());                        
  BigDecimal pricePerLiter = BigDecimal.valueOf(pricePerGallon * 0.264);                        
  pricePerLiter = pricePerLiter.setScale(2, RoundingMode.HALF_UP);
  txtFields.get(1).setText(pricePerLiter.toString());
}

I've attempted this with both Math.Round() and BigDecimal and RoundingMode (which is the version above) but cannot resolve this issue.

2

There are 2 best solutions below

0
On

This is not a floating-point representation issue; it's a rounding issue. When you round, your value is no longer accurate. In addition, the two values you multiply with don't themselves multiply to be 1; they're not quite inverses.

When you take 3.95 and multiply it by 0.264, the value is 1.0428, which you round down to 1.04. When you multiply 1.04 by 3.785, you get 3.9364, which is rounded up to 3.94.

But you represent the inverse of 3.785 as 0.264, which is close, but not quite equal to the actual inverse of about 0.2642008. But even multiplying the true inverse value by 3.95 you get 1.04359316, which you would still round down to 1.04.

If you must convert back to the same value, then store the un-rounded value for multiplying later, even if you display the rounded value. Also, instead of multiplying by 0.264, just divide by the same number that you multiplied by in the other case, 3.785.

0
On

As soon as you are using floating point (double, float) you lost your precision after the period, the so called scale (10-3 here for 3.785).

So do not use the BigDecimal constructor with a double but with a String. Then the scale, digits after the period, will be specified.

BigDecimal pricePerLiter = new BigDecimal(txtFields.get(1).getText());
BigDecimal pricePerGallon =  pricePerLiter.multiply(new BigDecimal("3.785"));    

So "1.00" * "3.785" will become "3.78500". This cannot be achieved by a double 3.785 as that is an approximation, maybe equal to 3.7849999998.

So either use only BigDecimal or use the imprecise double and print a rounded text: Sytem.out.printf("%5.2f", 3.785); // 3.79.