Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

BigDecimal not keeping actual value when being returned from Java method

I am making a Currency conversion app in Java. Some other awesome StackOverflowians gave me advice to read up on BigDecimal for the purpose of replacing double to fix any precision issues.

I have a two method system; where it converts from the starting currency into USD, then converts the USD value to the destination currency.

Note, my conversion rates are stored like this:

// Conversion Rates - START (as of October 30, 2018 @ 3:19 AM)
// Rates obtained from exchange-rates.org

//Convert to United States Dollar rates
private final BigDecimal CAD_TO_USD = new BigDecimal(0.76135);
private final BigDecimal EUR_TO_USD = new BigDecimal(1.1345);
private final BigDecimal YEN_TO_USD = new BigDecimal(0.008853);
// Conversion Rates - END

After I replaced my doubles with their respective BigDecimals - I decided to test it and see how it all works out.

My tester class runs the following method to start the conversion process.

public BigDecimal convert()
{
    BigDecimal value;

    value = convertToUSD(); //Converts the current currency into USD 
    value = convertFromUSD(value);  //Converts the previous USD currency value into the destination currency

    return value;
}

When I enter my sample variables (which are converting 2.78 YEN to Canadian Dollars), I stepped through the process and found that everything functions up until I return a value.

From the earlier mentioned method, convertToUSD() is run and is coded as follows

private BigDecimal convertToUSD()
{
    switch (fromCurrency)
    {
        case "USD":
            return fromQuantity.multiply(new BigDecimal(1));

        case "CAD":
            return fromQuantity.multiply(CAD_TO_USD);

        case "EUR":
            return fromQuantity.multiply(EUR_TO_USD);

        case "YEN":
            return fromQuantity.multiply(YEN_TO_USD);
    }

    return new BigDecimal(0);
}

ALl the values are passed in correctly, it steps through down to the proper case ("YEN"), and the variable pane shows that the "fromQuantity" BigDecimal has a intCompact value of 278 (which makes sense to me)

enter image description here

As soon as the breakpoint returns back to the "convert" method, it gets all messed up. Instead of returning the 2.78 * 0.008853 = 0.0246, it returns -9223372036854775808.

enter image description here

This causes all other calculations to produce and error.

I am new to using BigDecimal, so I may be making a complete obvious mistake; but I am happy to learn, so I sought advice from you guys :)

Any assistance is appreciated.

like image 687
Bryan Douglas Avatar asked Jan 27 '23 05:01

Bryan Douglas


2 Answers

tl;dr

Use String, not double literals.

new BigDecimal( "2.78" )           // Pass "2.78" not 2.78
.multiply(
    new BigDecimal( "0.008853" )   // Pass "0.008853" not 0.008853
)
.toString()

0.02461134

Do not pass floating-point types

The point of the BigDecimal class is to avoid the inherent inaccuracies found in floating-point technology. Floating-point types such as float/Float and double/Double trade away accuracy for speed of execution. In contrast, BigDecimal is slow but accurate.

Your code:

new BigDecimal( 0.76135 )
new BigDecimal( 1.1345 )
new BigDecimal( 0.008853 )

…is passing a double primitive literal. During compilation, the text you typed 0.76135 is parsed as a number, specifically as a double (a 64-bit floating-point value). At that point you have introduced inaccuracies inherent with this type. In other words, the double produced from 0.76135 may no longer be exactly 0.76135.

Let’s dump your BigDecimal instances immediately after instantiating.

System.out.println( new BigDecimal( 0.76135 ) );    // Passing a `double` primitive.
System.out.println( new BigDecimal( 1.1345 ) );
System.out.println( new BigDecimal( 0.008853 ) );

0.7613499999999999712230192017159424722194671630859375

1.13450000000000006394884621840901672840118408203125

0.0088529999999999997584154698415659368038177490234375

So, by creating double number values, you invoked floating-point technology, and introduced inaccuracies.

Use strings

The solution? Work with strings, avoiding the double type entirely.

Put some double-quote marks around those inputs, and voilà.

System.out.println( new BigDecimal( "0.76135" ) );  // Passing a `String` object.
System.out.println( new BigDecimal( "1.1345" ) );
System.out.println( new BigDecimal( "0.008853" ) );

0.76135

1.1345

0.008853

Example

You expected 2.78 * 0.008853 = 0.0246. Let’s try it.

BigDecimal x = new BigDecimal( "2.78" );
BigDecimal y = new BigDecimal( "0.008853" );
BigDecimal z = x.multiply( y );
System.out.println( x + " * " + y + " = " + z );

2.78 * 0.008853 = 0.02461134

Next you should study up on rounding & truncating with BigDecimal. Already covered many times on Stack Overflow.

like image 193
Basil Bourque Avatar answered Jan 31 '23 09:01

Basil Bourque


The problem is that you are assuming intCompact represents a non-floating point version of your float. In some cases, it does, but in most cases it will not.

For example I tried to reduce your problem to the bare minimum.

import java.math.BigDecimal;

class Main {
  public static void main(String[] args) {
    final BigDecimal YEN_TO_USD = new BigDecimal(0.008853);
    BigDecimal value = new BigDecimal(2.78);

    value = value.multiply(YEN_TO_USD);
    System.out.println(value);
  }
}

Placing a break on the line with the println I get the following:

break point

You see the intCompact is the same as yours (-9223372036854775808). But the stringCache in this case is the expected value.

Refer to the answer by Basil as to why you should construct BigDecimal using strings and not doubles.

like image 43
Dan N Avatar answered Jan 31 '23 07:01

Dan N