dimhold.by
← Writing

Money in a double: 4.35 * 100 comes out 434

I had a price of 4.35. I needed it in kopecks:

$ cat Price.java
public class Price {
    public static void main(String[] args) {
        System.out.println(4.35 * 100);
        System.out.println((long) (4.35 * 100));
    }
}

$ javac Price.java && java Price
434.99999999999994
434

The cast truncates. A kopeck is gone. Avoiding double for money is advice I took without ever measuring what it costs, so I measured it.

new BigDecimal(double) prints the exact decimal value of the bits with no rounding on the way out. That is the whole diagnostic tool here:

4.35 -> 4.3499999999999996447286321199499070644378662109375
0.1  -> 0.1000000000000000055511151231257827021181583404541015625
0.2  -> 0.200000000000000011102230246251565404236316680908203125
0.3  -> 0.299999999999999988897769753748434595763683319091796875
0.5  -> 0.5

4.35 is held a little below 4.35. Times 100 it is still a little below 435 and the cast to long drops everything after the point. Math.round gives 435 here. It does not help when the rate itself is inexact.

4 exact prices in every 100

I ran every 2 decimal value from 0.01 to 100.00 through the same comparison, the double against the decimal it stands for:

for (long c = 1; c <= 10000; c++) {
    double v = c / 100.0;
    if (new BigDecimal(v).compareTo(BigDecimal.valueOf(c, 2)) == 0) exactCount++;
}
exact: 400 of 10000
first hundred: 0.25 0.50 0.75 1.00

4 percent. A double is a sum of powers of 2 so a hundredth fits only when it reduces to a fourth. Every other price is already an approximation when it is parsed.

prices 0.01 to 1.00 .25 .50 .75 .00
Every 2 decimal price from 0.01 to 1.00. 4 of the 100 are stored exactly.
The other 96 are approximations before any arithmetic happens.

10 additions of 0.10

The famous one first:

double acc = 0.0;
for (int i = 0; i < 10; i++) acc += 0.10;
sum of ten 0.10 = 0.9999999999999999
acc == 1.0 ? false
exact         = 0.99999999999999988897769753748434595763683319091796875

I expected accumulation to be the expensive part, so I ran a million additions of 0.01 next to a BigDecimal doing the same:

after 1000000 additions, double = 10000.000000171856
exact                           = 10000.00
difference in rubles            = 1.71856299857608973979949951171875E-7

Then I rounded the double total to kopecks after every one of those million steps and compared it with the exact total. They never disagreed. Not once in a million steps. The drift is real at the seventh decimal. Rounding to 2 decimals eats all of it.

The kopeck leaves on an exact half

Take the Russian VAT rate of 18 percent and compute the tax 2 ways for every amount from 0.01 to 10000.00:

long viaDouble = Math.round(cents / 100.0 * 0.18 * 100.0);

long exact = BigDecimal.valueOf(cents, 2)
        .multiply(new BigDecimal("0.18"))
        .setScale(2, RoundingMode.HALF_UP)
        .movePointRight(2).longValueExact();
18%: mismatches = 3656 of 1000000, first: 1.25(exact 23 vs 22) 5.75(exact 104 vs 103)
6.75(exact 122 vs 121) 10.75(exact 194 vs 193) 11.75(exact 212 vs 211)

The smallest one takes it apart:

1.25 as a double   1.25
0.18 as a double   0.179999999999999993338661852249060757458209991455078125
product            0.22499999999999997779553950749686919152736663818359375
times 100          22.499999999999996447286321199499070644378662109375
Math.round         22

1.25 is one of the 400 exact prices while 0.18 is stored below its decimal value. The true product is 0.225. That sits exactly between 2 kopecks so HALF_UP takes it up to 0.23. The double sits 3.55e-15 under the halfway point and falls to 0.22.

22 kopecks halfway 23 kopecks exact 0.225 HALF_UP double 22.499999999999996 Math.round
The exact product sits on the halfway mark. HALF_UP sends it to 23.
The double lands below it. Math.round sends it to 22. The gap is drawn far wider than 3.55e-15.

I repeated the scan at 6 rates, with the mismatches counted separately depending on whether the exact product landed on a half:

ratehalfway caseswrong on a halfwrong anywhere else
3%1000020300
5%500008180
7%1000000
18%2000036560
20%000
25%250000164050

The column I did not expect is the last one. 6 rates, a million amounts each. That counter never moved off zero.

20 percent has no halves to get wrong. The tax in kopecks is the price in kopecks times two, divided by ten. An even numerator never leaves a remainder of five. That one is arithmetic. It holds outside the scan. 7 percent does have halves. It has 10000 of them in the million. It loses none. I widened that scan to 10 million amounts to be sure. 100000 halves, still zero.

I wanted a rule out of the direction each rate is stored in. 0.05 and 0.07 and 0.20 sit above their decimal value, 0.03 and 0.18 sit below, 0.25 is exact. Sorted by the share of halves lost it almost lines up. The 2 stored below lose the most: 20.3 halves in every 100 at 3 percent and 18.3 at 18 percent. 0.25 is stored exactly and loses 6.6. Then 0.05 sits above and still loses 1.6, where the direction on its own says zero. The price has been through cents / 100.0 before the rate ever touches it, so the rate is only half of what is going on. I do not have the rule.

BigDecimal has its own edges

Changing the type does not switch the problem off:

new BigDecimal(0.1)     = 0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(0.1) = 0.1
new BigDecimal("0.1")   = 0.1

The constructor that takes a double copies the error straight in. valueOf goes through Double.toString and gets the short form back. The string constructor never sees a double.

1.0 equals 1.00    = false
1.0 compareTo 1.00 = 0
hash 1.0  = 311
hash 1.00 = 3102

Scale is part of identity, so a HashSet keeps 1.0 and 1.00 as 2 different prices. Those 2 hash values are what the implementation does and not what the javadoc promises. Division refuses to guess at all:

1/3 -> ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

I like that it throws. A double would have handed back 0.3333333333333333 and let me carry it into the next line.

Long kopecks did the same job

The same tax over the same million amounts, in long kopecks with the rounding written by hand:

long ints = (cents * 18 + 50) / 100;
(cents*18 + 50)/100 vs BigDecimal HALF_UP: mismatches=0

Zero, against setScale(2, HALF_UP) on every one of them. A long holds 9223372036854775807 kopecks. Even a double counts whole kopecks exactly up to 9007199254740992 of them. That is ninety trillion rubles, so the size of the number was never the problem. The cost is that the rounding rule is now in my code and keeping it correct is on me.

What I have not checked

HALF_EVEN roughly halves it and does not remove it. Through Math.rint the same 18 percent scan gives 1830 wrong out of a million. Half of 3656 is 1828, which is about what I expect when half the ties were rounding down anyway. I have not chased the other two.

Still open: currencies with 3 decimals. Splitting one amount across several invoice lines so the parts add back up to the whole. And the JDBC driver with a NUMERIC column, which I have not looked at yet.