[ 2020-January-24 08:08 ]
One piece of popular programming wisdom is “never using floating-point numbers for money.” This has never made sense to me. A – bit floating-point number can represent decimal digits , which is all balances less than (9 trillion) () 2017 ), with two digits after the decimal place. Excel does all computation with – bit floats , and it is used for tons of financial calculations. After investigating, my conclusion is that the common wisdom is good advice but overly simplified. If you round the result of every computation, then you can get exactly correct financial calculations using floating-point numbers, for realistic ranges of values. However, it can be tricky to make sure you round in the right places, so using a money-specific data type is an easy way to avoid these errors, and is still good advice. The challenge is that you need to carefully manage rounding when computing with money, no matter how you represent it. Floating-point numbers seem like they should “just work,” and in most cases, they will. This means rounding bugs will be hidden until the right (wrong?) Input is processed.
Let’s start by discussing how to get the “right” answer for financial calculations (I have not worked in banking, so please correct me if I get this wrong). Financial calculations are typically written by humans in contracts in decimal (base ). That is, we use numbers such as CAD $ 1. (2 decimal places), and interest rates like 1. (%) 0. 4 decimal places . Payments can only be made with some fixed precision, such as two decimal places in Canadian dollars. When we compute fractional values that may have more decimal places than the payment system, which happens for tax or interest, we have to decide how to round. The exact rule to be used will vary. In many cases, such as US banking interest payments, there is not a required standard, which I find surprising since banking has a reputation for punishingly exact regulation. I use round half to even in this article, since it is less biased and is the default rounding mode for IEEE floating-point numbers. No matter what rule we choose, the correct results are what we would compute “by hand,” using the decimal math we learned in school.
(0.1 0.2) : Produces 0. but should be exactly 0.3. This means
is false when it should be true. See Bruce Dawson's explanation for details .
: produces but should be exactly 675. This can cause errors when computing the total price for a purchase order, particularly if we truncate the output to 2 digits, instead of rounding. Round half to nearest even to 2 decimal places (eg Python 3): (round) 2. 728, (2)
2. but should produce 2. 99, since 2. (is halfway between 2.) and 2 . , and it should round to the even choice. This is because 2. (is actually 2.) , which is slightly below half. The opposite error occurs with round (2. 728, (2) which produces 2. 85 But should produce 2. .
1. % of $ 0. 64. This is equivalent to
0.0 0. 68 which should produce 0 . , which if using round to even rounds to $ 0. 08. However, in floating-point numbers, it is above the halfway point (0. ), so it rounds up. Solution : Round after every operation
We can solve these problems by rounding after every operation. The "intuitive" reasoning is if we were computing numbers by hand, we need a fixed number of decimal places: e.g. 2 for basic totals. The floating-point numbers approximate the decimal numbers with a tiny bit of error. Since we "know" the exact answers have a finite number of decimal digits, we can just round off the lower part of the numbers, which will produce the nearest float with that number of digits. For this to work, the results must have at most decimal digits, which is less than 19 trillion for 2 digits after the decimal, or less than 1 billion for 6 digits (eg interest calculations). As shown by Excel, you probably don't need to round after every operation: the error may accumulate, but the number of operations you would need to cause a 1 cent error is pretty huge. However, in this case you will need to "double round" before using the final result. You need to first round to the number of significant figures from your calculation, then you need to apply your rounding rule down to cents. Otherwise, you will get some rounding edge cases wrong (eg 3.6% of $ 3. (=0. 0185, which should round to $ 0. 23, but in floating-point it is 0. 2020 ... which rounds to $ 0. 24).
I am not a theoretician and have not proven that this is actually correct. Let me know if you find a counter-example. I did, however, try every interest calculation in the range of [0.00%-4.00%] with all two-digit values [0.00-4.00], and they were equivalent to a precise decimal math library, so it does work at least for that limited domain ( Go test program
- Manual fixed-point calculations
Another common suggested solution is to use an integer value that represents the largest precision you need. For example, CAD $ 1. 24 could be stored as the integer 125. This is great for integer multiplication, addition and subtraction, which is sufficient for basic order accounting. However, as soon as you need to deal with interest or taxes, you need to track the number of decimal places and manage rounding, which is just as much of a pain as using floating point. For example, 1. (% of $ 0.) can be expressed as 67=. However, we need to remember this has 6 digits after the decimal point, so is equivalent to 0. 13 (4 decimal places times 2 decimal places produces a result with 6 decimal places). To convert to cents, we need to divide by 10000 and decide how to round. The primary advantage of using an integer is that it forces you to think about the conversions, rather than blindly applying the calculations and forgetting about rounding.
My summary: if you are doing some financial math that does not need to be accurate to the penny, just use floating point numbers. If it is good enough for Excel, it will be good enough for most applications. This is the approach I took when building a cloud cost model that breaks down a cloud bill by software component. I was able to get it to exactly equal the bill by applying rounding rules in the right places, but simplified the code by not doing it. However, if you are writing software that needs to get it exactly right, use a specialized package. Not only will the APIs force you to get the math right, but they also provide tools to make it easier, such as rounding rules or currency support.
(What Every Programmer Should Know About Floating-Point Arithmetic : A simpler guide to floating point numbers than the often cited What Every Computer Scientist Should Know About Floating-Point Arithmetic
) which is far too math-heavy for me.
IEEE Standard (Floating Point Numbers : Another readable overview of how floating point numbers work.
Sometimes floating-point math is perfect : Bruce Dawson digs into the common misconception that floating-point math is erroneous. It isn't always! His other articles on floating-point numbers are also entertaining and educational.
Stack Overflow on Why Not Use Double or Float to Represent Currency: The answers here are ... mostly correct, but some are not completely correct.