3

I've always heard that you should use a money class due to floating point inaccuracy. However, it is astonishingly hard to find any example where floating point inaccuracy actually leads to a wrong result.

My programming language of choice is Python. To test if the result is different from the expected, I use:

expected = '1.23'
result = '{:0.2f}'.format(result)
assert expected == result

So while the following is a good example for floating point inaccuracy, it is NOT an example for the need of a money class using a rational number class (like Pythons fractions) for most use-cases:

a = 10.0
b = 1.2
assert a + b - a == b

The best thing I could come up with is

result = (a + b - a) * 10**14 - b * 10**14
expected = 0

but multiplying something money-related by 10**14 seems really made-up.

Now I wonder if there are any realistic examples showing the need for a money class or if everything is "captured" by simply rounding to two digits.

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • 1
    If you're going to round everything to two digits then you already have your money type; it is "integer", and it counts the number of cents. – Eric Lippert Jun 12 '18 at 18:41
  • No, I'm not rounding everything. I'm rounding the end result after various calculations. – Martin Thoma Jun 12 '18 at 18:42
  • In other words: Is it valid to just use float for money all the time (with the usual things one should watch out four when using floats, especially checking for equality) and just rounding whatever is shown to the user? – Martin Thoma Jun 12 '18 at 18:44
  • 1
    What you're saying then is "I know when it is necessary to round in order to make it work". If you know that and you never make a mistake, then by definition, the tool meets your needs. **Most people don't know enough about floating point to know when they need to round and when they don't**, and so you design a type that does it for you. Most people do not know "the usual things one should watch out for", and that list of "usual things" is longer than you might think! Floating point numbers are designed for physics calculations. Use them for that. – Eric Lippert Jun 12 '18 at 18:45
  • Hm... ok. I thought there might be some operations where the error quickly adds up. Or some other use-cases about which I didn't think. I'll wait a bit and see. – Martin Thoma Jun 12 '18 at 18:47
  • 3
    Personally I would never, ever use floating point numbers to represent precise decimal quantities, particularly if doing a computation where **real money is on the line if I am wrong**. I would use a type carefully designed and tested to accurately model the real-world transaction that I'm executing. Your question is basically "can I cut corners when doing financial transaction coding?" I wouldn't! – Eric Lippert Jun 12 '18 at 18:48
  • 3
    Well let's think about a few. Does every chip your code runs on support denormals? Is your algorithm correct in the face of denormals? Some floating point chips use 80 bit precision and some use 64, and some switch back and forth basically at random; does that have any implication to the correctness, testability, and repeatability of your algorithm? Does your system handle infinities and nans? Does it correctly deal with the fact that there are both positive and negative zeros in floats? These are all things I would want to know the answers to before I used floats for money. – Eric Lippert Jun 12 '18 at 18:51
  • 2
    An example of one of the things people typically don't know about when using floating point numbers is when to use Kahan Summation and when not to. Now, if you're going to say "but I never add up so many numbers that it makes a difference after I round which summation algorithm to use", then again, what you are doing is saying that you know something about the problem space that we do not, and that you are solving a *simpler* problem than the general problem of summing money. So if your question is: in simple cases where using floats and rounding is safe, is it safe? Yes, by definition. – Eric Lippert Jun 12 '18 at 18:58
  • 1
    So then the question becomes: *are you a good judge of when you're in a simple case and when you are not?* Most people are not a good judge of that because their intuitions lead them astray. Floating point numbers do not behave like algebraic numbers *sometimes*. – Eric Lippert Jun 12 '18 at 18:59
  • 4
    I would still avoid floating point values even if you know the inherent dangers unless you're the only one that's ever going to be looking at this code. Using a money type tells the reader something about your intentions. It also avoids the problem of future enhancements not being safe with floating point values and having bugs creep in. – itsme86 Jun 12 '18 at 19:43
  • 1
    @itsme86: That is a *great* point. Code defensively to prevent your future bugs and the future bugs of others. I very often see code that was right but a little weird, and then someone makes an assumption that it is not weird, modifies it, and it suddenly becomes not right in a subtle way. (This also happens in physical infrastructure; my favourite example is shared-neutral circuits in home wiring. Easy to do right, very easy to accidentally modify to make wrong and you don't know you made a mistake until the house burns down.) – Eric Lippert Jun 12 '18 at 19:53
  • 1
    If you use binary floating point, you'll need to tell management/coworkers/clients/engineers: Every time the user tells me tells me to represent $0.10, I'm going to write down `0.1000000000000000055511151231257827021181583404541015625` (Thanks, [DoubleConverter](http://jonskeet.uk/csharp/DoubleConverter.cs)) instead. But don't worry, it's only off by `0.0000000000000000055511151231257827021181583404541015625`. That's barely noticeable, and I'll round the number before I display it and after each calculation. I promise everyone will be very careful; it will never make a difference. – Brian Jun 12 '18 at 21:08
  • 1
    The question is rather: why would you ever represent money using a type that is **not** exact by design, when every computer has a native type that **is** exact by design (until you overflow it, of course), and can represent exact minor amounts (cents) in any currency? Note that even though I designed a [Money class](https://github.com/brick/money) myself, I still always store my monies as `int`, perform simple arithmetic operations as `int`, and only resort to using a Money instance for more complicated use cases (division, rounding, formatting, currency conversion, custom scale...) – BenMorel Jun 13 '18 at 09:33
  • @Benjamin No, that is not my question. – Martin Thoma Jun 13 '18 at 10:03
  • C# has `decimal` for this sort of reason – James Parsons Jun 13 '18 at 18:34
  • @james: while I think a decimal data type certainly is required for a money class, I don't think it is enough. I think a money class should also (1) prevent adding non-money numbers (2) prevent adding two different currencies (3) not allow taking the power / roots. But that is just my gut feeling. The fact there seem not to be real world examples where something broke due to not having a money class is an indicator that it might actually not be that important – Martin Thoma Jun 13 '18 at 18:53
  • @MartinThoma I remember asking an Ada question a while back, and one of the given answers shows an example that can address your points (1) and (2) [here](https://stackoverflow.com/a/23225197/1938826), it's a feature of Ada I think is rather cool. – James Parsons Jun 13 '18 at 18:56
  • Your point about units of measure is well-taken; this isn't just a float problem. I'll add some thoughts to my answer. – Eric Lippert Jun 13 '18 at 19:28

1 Answers1

11

it is astonishingly hard to find any example where floating point inaccuracy actually leads to a wrong result.

I would not say it is astonishingly hard. A famous real-world example, albeit not involving money was that the Patriot missile system code accumulated a floating point rounding error of 0.000000095 seconds per second; if the system was not rebooted every five days, it would be off by a fraction of a second. Since the missiles it intercepts can move several thousand meters per second, it would miss.

At least 28 people died as a result of this floating point error.

We can demonstrate the Patriot error without putting more lives at risk. Here's a little C# program. Suppose we are adding up dimes; how many do we have to add before we get a significant error?

    double sum = 0.0;
    long k = 0;
    long report = 1;
    while (true) {
        k += 1;
        sum += 0.1;
        if (k == report) {
            Console.WriteLine($"{k} {k / 10.0 - sum}");
            report *= 10;
        }
    }

Let it run as long as you like. The output on my machine started:

1 0
10 1.11022302462516E-16
100 1.95399252334028E-14
1000 1.406874616805E-12
10000 -1.58820512297098E-10
100000 -1.88483681995422E-08
1000000 -1.33288267534226E-06
10000000 0.00016102462541312
100000000 0.0188705492764711
1000000000 1.25458218157291
10000000000 -163.12445807457

After only a hundred million computations -- so, $10M -- we are already off by two cents. By ten billion computations we are off by $163.12. Sure, that's a tiny error per transaction, and maybe $163.12 is not a lot of money in the grand scheme of things compared to a billion dollars, but if we cannot correctly compute 100 million times 0.1 then we have no reason to have confidence in any computation that comes out of this system.

The error could be guaranteed to be zero; why would you not want the error to be zero?

Exercise: You imply that you know where to put the roundings in to ensure that this error is eliminated. So: where do they go?


Some additional thoughts, inspired by your comment:

while I think a decimal data type certainly is required for a money class, I don't think it is enough. I think a money class should also (1) prevent adding non-money numbers (2) prevent adding two different currencies (3) not allow taking the power / roots.

If what you want is real-world examples of money errors involving units of measure not being trapped by the type system, there are many, many such examples.

I used to work at a company which writes software that detects software defects. One of the most magical defect checkers is the "cut and paste error" detector, and it found a defect in real world code like

dollarTot = (euros1 + euros2) * dollarEuroRate;
pesoTot = (euros3 + euros4) * pesoEuroRate;
... dozens more like this...

And then later on in the code

dollarTot = (yen1 + yen2) * yenDollarRate;
pesoTot = (yen3 + yen4) * pesoEuroRate;
...

Oops.

The major international trading house that had that defect called us up and said that the beer was on them next time we were in Switzerland.

Examples like these show why financial houses are so interested in languages like F# that make it super easy to track properties in the type system.

I did a series on my blog a few years ago about using the ML type system to find bugs when implementing virtual machines where an integer could mean an address of a dozen different data structures, or an offset into those structures. It finds bugs fast, and the runtime overhead is minimal. Units of measure types are awesome, even for simple problems like making sure you don't mix up dollars with yen.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067