4

I'm writing a bank program with a variable long balance to store cents in an account. When users inputs an amount I have a method to do the conversion from USD to cents:

public static long convertFromUsd (double amountUsd) {
   if(amountUsd <= maxValue || amountUsd >= minValue) {
      return (long) (amountUsd * 100.0)
   } else {
      //no conversion (throws an exception, but I'm not including that part of the code)
   }
}

In my actual code I also check that amountUsd does not have more than 2 decimals, to avoid inputs that cannot be accurately be converted (e.g 20.001 dollars is not exactly 2000 cents). For this example code, assume that all inputs has 0, 1 or 2 decimals.

At first I looked at Long.MAX_VALUE (9223372036854775807 cents) and assumed that double maxValue = 92233720368547758.07 would be correct, but it gave me rounding errors for big amounts:

convertFromUsd(92233720368547758.07) gives output 9223372036854775807

convertFromUsd(92233720368547758.00) gives the same output 9223372036854775807

What should I set double maxValue and double minValue to always get accurate return values?

5 Answers5

3

You could use BigDecimal as a temp holder

If you have a very large double (something between Double.MAX_VALUE / 100.0 + 1 and Double.MAX_VALUE) the calculation of usd * 100.0 would result in an overflow of your double.

But since you know that every possible result of <any double> * 100 will fit in a long you could use a BigDecimal as a temporary holder for your calculation. Also, the BigDecimal class defines two methods which come in handy for this purpose:

By using a BigDecimal you don't have to bother about specifying a max-value at all -> any given double representing USD can be converted to a long value representing cents (assuming you don't have to handle cent-fractions).

double usd = 123.45;
long cents = BigDecimal.valueOf(usd).movePointRight(2).setScale(0).longValueExact();

Attention: Keep in mind that a double is not able to store the exact USD information in the first place. It is not possible to restore the information that has been lost by converting the double to a BigDecimal. The only advantage a temporary BigDecimal gives you is that the calculation of usd * 100 won't overflow.

Felix
  • 2,256
  • 2
  • 15
  • 35
  • Thanks for taking the time to answer my question! This solution was really helpful because it also made input validation easier, since longValueExact() throws ArithmeticException whenever rounding occurred. – Alexander Wiklund Nov 08 '20 at 15:30
3

First of all, using double for monetary amounts is risky.

TL;DR

I'd recommend to stay below $17,592,186,044,416.

The floating-point representation of numbers (double type) doesn't use decimal fractions (1/10, 1/100, 1/1000, ...), but binary ones (e.g. 1/128, 1/256). So, the double number will never exactly hit something like $1.99. It will be off by some fraction most of the time.

Hopefully, the conversion from decimal digit input ("1.99") to a double number will end up with the closest binary approximation, being a tiny fraction higher or lower than the exact decimal value.

To be able to correctly represent the 100 different cent values from $xxx.00 to $xxx.99, you need a binary resolution where you can at least represent 128 different values for the fractional part, meaning that the least significant bit corresponds to 1/128 (or better), meaning that at least 7 trailing bits have to be dedicated to the fractional dollars.

The double format effectively has 53 bits for the mantissa. If you need 7 bits for the fraction, you can devote at most 46 bits to the integral part, meaning that you have to stay below 2^46 dollars ($70,368,744,177,664.00, 70 trillions) as the absolute limit.

As a precaution, I wouldn't trust the best-rounding property of converting from decimal digits to double too much, so I'd spend two more bits for the fractional part, resulting in a limit of 2^44 dollars, $17,592,186,044,416.

Code Warning

There's a flaw in your code:

return (long) (amountUsd * 100.0);

This will truncate down to the next-lower cent if the double value lies between two exact cents, meaning that e.g. "123456789.23" might become 123456789.229... as a double and getting truncated down to 12345678922 cents as a long.

You should better use

return Math.round(amountUsd * 100.0);

This will end up with the nearest cent value, most probably being the "correct" one.

EDIT:

Remarks on "Precision"

You often read statements that floating-point numbers aren't precise, and then in the next sentence the authors advocate BigDecimal or similar representations as being precise.

The validity of such a statement depends on the type of number you want to represent.

All the number representation systems in use in today's computing are precise for some types of numbers and imprecise for others. Let's take a few example numbers from mathematics and see how well they fit into some typical data types:

  • 42: A small integer can be represented exactly in virtually all types.
  • 1/3: All the typical data types (including double and BigDecimal) fail to represent 1/3 exactly. They can only do a (more or less close) approximation. The result is that multiplication with 3 does not exactly give the integer 1. Few languages offer a "ratio" type, capable to represent numbers by numerator and denominator, thus giving exact results.
  • 1/1024: Because of the power-of-two denominator, float and double can easily do an exact representation. BigDecimal can do as well, but needs 10 fractional digits.
  • 14.99: Because of the decimal fraction (can be rewritten as 1499/100), BigDecimal does it easily (that's what it's made for), float and double can only give an approximation.
  • PI: I don't know of any language with support for irrational numbers - I even have no idea how this could be possible (aside from treating popular irrationals like PI and E symbolically).
  • 123456789123456789123456789: BigInteger and BigDecimal can do it exactly, double can do an approximation (with the last 13 digits or so being garbage), int and long fail completely.

Let's face it: Each data type has a class of numbers that it can represent exactly, where computations deliver precise results, and other classes where it can at best deliver approximations.

So the questions should be:

  • What's the type and range of numbers to be represented here?
  • Is an approximation okay, and if yes, how close should it be?
  • What's the data type that matches my requirements?
Ralf Kleberhoff
  • 6,990
  • 1
  • 13
  • 7
1

You looked at the largest possible long number, while the largest possible double is smaller. Calculating (amountUsd * 100.0) results in a double (and afterwards gets casted into a long).

You should ensure that (amountUsd * 100.0) can never be bigger than the largest double, which is 9007199254740992.

PaulS
  • 850
  • 3
  • 17
1

Using a double, the biggest, in Java, would be: 70368744177663.99.

What you have in a double is 64 bit (8 byte) to represent:

  1. Decimals and integers
  2. +/-

Problem is to get it to not round of 0.99 so you get 46 bit for the integer part and the rest need to be used for the decimals.

You can test with the following code:

double biggestPossitiveNumberInDouble = 70368744177663.99;

for(int i=0;i<130;i++){
    System.out.printf("%.2f\n", biggestPossitiveNumberInDouble);
    biggestPossitiveNumberInDouble=biggestPossitiveNumberInDouble-0.01;
}

If you add 1 to biggestPossitiveNumberInDouble you will see it starting to round off and lose precision. Also note the round off error when subtracting 0.01.

First iterations

70368744177663.99
70368744177663.98
70368744177663.98
70368744177663.97
70368744177663.96
...

The best way in this case would not to parse to double:

System.out.println("Enter amount:");
String input = new Scanner(System.in).nextLine();

int indexOfDot = input.indexOf('.');
if (indexOfDot == -1) indexOfDot = input.length();
int validInputLength = indexOfDot + 3;
if (validInputLength > input.length()) validInputLength = input.length();
String validInput = input.substring(0,validInputLength);
long amout = Integer.parseInt(validInput.replace(".", ""));

System.out.println("Converted: " + amout);

This way you don't run into the limits of double and just have the limits of long.

But ultimately would be to go with a datatype made for currency.

Jon
  • 1,060
  • 8
  • 15
  • Although your reasoning is okay, your test doesn't really show the property that the OP needs: that strings from "70368744177663.00" to "70368744177663.99" really end up in longs from 7036874417766300 to 7036874417766399. – Ralf Kleberhoff Nov 03 '20 at 12:44
  • 1
    While all comments were helpful in understanding the issue I was dealing with and methods to avoid that issue, I believe this is the answer to my actual question. – Alexander Wiklund Nov 08 '20 at 15:26
1

Floating values (float, double) are stored differently than integer values (int, long) and while double can store very large values, it is not good for storing money amounts as they get less accurate the bigger or more decimal places the number has.

Check out How many significant digits do floats and doubles have in java? for more information about floating point significant digits

A double is 15 significant digits, the significant digit count is the total number of digits from the first non-zero digit. (For a better explanation see https://en.wikipedia.org/wiki/Significant_figures Significant figures rules explained)

Therefor in your equation to include cents and make sure you are accurate you would want the maximum number to have no more than 13 whole number places and 2 decimal places.

As you are dealing with money it would be better not to use floating point values. Check out this article on using BigDecimal for storing currency: https://medium.com/@cancerian0684/which-data-type-would-you-choose-for-storing-currency-values-like-trading-price-dd7489e7a439

As you mentioned users are inputting an amount, you could read it in as a String rather than a floating point value and pass that into a BigDecimal.

Hayd
  • 361
  • 2
  • 7
  • The "significant digits" wording hides the basic fact that `double` numbers aren't based on decimal digits internally. So, "15 significant digits" is nothing more than a rough estimate. – Ralf Kleberhoff Nov 03 '20 at 12:52
  • Interesting thought. I am of the opinion that the words "significant digits" and "significant figures" enforce the fact that floating point numbers are not stored as decimal digits under the hood as they specify the amount of digits that can be rounded to accurately. – Hayd Nov 04 '20 at 02:07
  • I base the 15 significant digits on the research done: https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/. Having said that, I would advise against using floating point numbers for dealing with anything that requires complete accuracy as the significant digits are what can be displayed correctly when rounded to, not the exact value stored under the hood – Hayd Nov 04 '20 at 02:07
  • Interesting reading! But not exactly an answer to the OP's question, as the "15 digits" statement implies the answer to be `$9,999,999,999,999.99`, which is on the safe side, but somewhat arbitrary. If `$9,999,999,999,999.99` is exact (according to the OP's requirements), then `$17,592,186,044,415.99` is as well, as it uses the same binary exponent. – Ralf Kleberhoff Nov 04 '20 at 09:10
  • Thanks for taking the time to answer my question! – Alexander Wiklund Nov 08 '20 at 15:37
  • 1
    In a later stage of the program I realized that realized that reading the input as a double caused the same rounding issues, so I changed the input to a String as you suggested. – Alexander Wiklund Nov 12 '20 at 10:59