8

tl;dr: What's wrong with my Cur (currency) structure?

tl;dr 2: Read the rest of the question please, before giving an example with float or double. :-)


I'm aware that this question has come up numerous times before all around the internet, but I have not yet seen a convincing answer, so I thought I'd ask again.

I fail to understand why using a non-decimal data type is bad for handling money. (That refers to data types that store binary digits instead of decimal digits.)

True, it's not wise to compare two doubles with a == b. But you can easily say a - b <= EPSILON or something like that.

What is wrong with this approach?

For instance, I just made a struct in C# that I believe handles money correctly, without using any decimal-based data formats:

struct Cur { private const double EPS = 0.00005; private double val; Cur(double val) { this.val = Math.Round(val, 4); } static Cur operator +(Cur a, Cur b) { return new Cur(a.val + b.val); } static Cur operator -(Cur a, Cur b) { return new Cur(a.val - b.val); } static Cur operator *(Cur a, double factor) { return new Cur(a.val * factor); } static Cur operator *(double factor, Cur a) { return new Cur(a.val * factor); } static Cur operator /(Cur a, double factor) { return new Cur(a.val / factor); } static explicit operator double(Cur c) { return Math.Round(c.val, 4); } static implicit operator Cur(double d) { return new Cur(d); } static bool operator <(Cur a, Cur b) { return (a.val - b.val) < -EPS; } static bool operator >(Cur a, Cur b) { return (a.val - b.val) > +EPS; } static bool operator <=(Cur a, Cur b) { return (a.val - b.val) <= +EPS; } static bool operator >=(Cur a, Cur b) { return (a.val - b.val) >= -EPS; } static bool operator !=(Cur a, Cur b) { return Math.Abs(a.val - b.val) < EPS; } static bool operator ==(Cur a, Cur b) { return Math.Abs(a.val - b.val) > EPS; } bool Equals(Cur other) { return this == other; } override int GetHashCode() { return ((double)this).GetHashCode(); } override bool Equals(object o) { return o is Cur && this.Equals((Cur)o); } override string ToString() { return this.val.ToString("C4"); } } 

(Sorry for changing the name Currency to Cur, for the poor variable names, for omitting the public, and for the bad layout; I tried to fit it all onto the screen so that you could read it without scrolling.) :)

You can use it like:

Currency a = 2.50; Console.WriteLine(a * 2); 

Of course, C# has the decimal data type, but that's beside the point here -- the question is about why the above is dangerous, not why we shouldn't use decimal.

So would someone mind providing me with a real-world counterexample of a dangerous statement that would fail for this in C#? I can't think of any.

Thanks!


Note: I am not debating whether decimal is a good choice. I'm asking why a binary-based system is said to be inappropriate.

13
  • Banks track money past the second significant digit. Commented Apr 24, 2011 at 5:32
  • @SFun: How many digits do they track? Commented Apr 24, 2011 at 5:34
  • @Mehrdad At my work (manufacturing) our ERP software tracks the price of components to 4 decimal places, for what that's worth. Commented Apr 24, 2011 at 5:37
  • 1
    For what it's worth, our old accounting software (for land tenures) just uses good-ole doubles... and then rounds all payments to the nearest 5 cents. In ten years there's only been one instance that I know of where it's produced a "wrong" rounded amount. That was penalty on multimillion dollar (federal government) lease which hadn't been paid for years... and that was only 5 cents out, which nobody worried about, once we knew it was a rounding error. Commented Apr 24, 2011 at 7:11
  • 1
    @SFun28: Hm, interesting... so you mean money could get lost in the conversions, right? Commented Apr 24, 2011 at 19:03

5 Answers 5

10

Floats aren't stable for accumulating and decrementing funds. Here's your actual example:

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace BadFloat { class Program { static void Main(string[] args) { Currency yourMoneyAccumulator = 0.0d; int count = 200000; double increment = 20000.01d; //1 cent for (int i = 0; i < count; i++) yourMoneyAccumulator += increment; Console.WriteLine(yourMoneyAccumulator + " accumulated vs. " + increment * count + " expected"); } } struct Currency { private const double EPSILON = 0.00005; public Currency(double value) { this.value = value; } private double value; public static Currency operator +(Currency a, Currency b) { return new Currency(a.value + b.value); } public static Currency operator -(Currency a, Currency b) { return new Currency(a.value - b.value); } public static Currency operator *(Currency a, double factor) { return new Currency(a.value * factor); } public static Currency operator *(double factor, Currency a) { return new Currency(a.value * factor); } public static Currency operator /(Currency a, double factor) { return new Currency(a.value / factor); } public static Currency operator /(double factor, Currency a) { return new Currency(a.value / factor); } public static explicit operator double(Currency c) { return System.Math.Round(c.value, 4); } public static implicit operator Currency(double d) { return new Currency(d); } public static bool operator <(Currency a, Currency b) { return (a.value - b.value) < -EPSILON; } public static bool operator >(Currency a, Currency b) { return (a.value - b.value) > +EPSILON; } public static bool operator <=(Currency a, Currency b) { return (a.value - b.value) <= +EPSILON; } public static bool operator >=(Currency a, Currency b) { return (a.value - b.value) >= -EPSILON; } public static bool operator !=(Currency a, Currency b) { return Math.Abs(a.value - b.value) <= EPSILON; } public static bool operator ==(Currency a, Currency b) { return Math.Abs(a.value - b.value) > EPSILON; } public bool Equals(Currency other) { return this == other; } public override int GetHashCode() { return ((double)this).GetHashCode(); } public override bool Equals(object other) { return other is Currency && this.Equals((Currency)other); } public override string ToString() { return this.value.ToString("C4"); } } } 

On my box this gives $4,000,002,000.0203 accumulated vs. 4000002000 expected in C#. It's a bad deal if this gets lost over many transactions in a bank - it doesn't have to be large ones, just many. Does that help?

Sign up to request clarification or add additional context in comments.

10 Comments

I don't see the word Currency anywhere... did you read the question? -1
You've missed the point of the question. If you use OP's struct, you wouldn't have that problem.
Your example might have more substance if it used double as Mehrdad did in his Currency class. ;)
Happy? The concept is the same.
Yep, that's it. Think about it this way: there are way more possible numbers that a double tries to represent than it can actually represent. The folks making the spec behind floats and doubles chose to make it more accurate around 0, so small amounts will affect it less - try plugging in 1 cent in this example and you'll be ok. But the "holes" (inaccuracies in representation) get bigger the farther away from 0 you get, so the bigger the number, the more inaccurate it is and that's why I picked a bigger number here. If you start dealing in millions, you're going to be hurting.
|
4

Usually monetary calculations require exact results, not just accurate results. float and double types cannot accurately represent the whole range of base 10 real numbers. For instance, 0.1 cannot be represented by a floating-point variable. What will be stored is the nearest representable value, which may be a number such as 0.0999999999999999996. Try it out for yourself by unit testing your struct - for example, attempt 2.00 - 1.10.

5 Comments

Does any real bank store even close to that many digits? (i.e. is this a real-world problem, or just a theoretical one that never happens?)
Of course not, but that's the exact point. Even if an operation is off by 0.01, that imprecision represents money in the real world. Scale that example up to billions of dollars worth of bank operations per day - that results in quite a bit of money gained or lost by the respective parties.
The U.S. Treasury Department does.
@Legs: I'd love to see code example(s) of what you're talking about. @dan04: How many digits do they store?
I don't have a citation, but if they're keeping track of our national debt, they'd need 16 digits (including the cents).
4

I'm not sure why you're shrugging off J Trana's answer as irrelevant. Why don't you try it yourself? The same example works with your struct too. You just need to add a couple extra iterations because you're using a double instead of a float, which gives you a bit more precision. Just delays the problem, doesn't get rid of it.

Proof:

class Program { static void Main(string[] args) { Currency currencyAccumulator = new Currency(0.00); double doubleAccumulator = 0.00f; float floatAccumulator = 0.01f; Currency currencyIncrement = new Currency(0.01); double doubleIncrement = 0.01; float floatIncrement = 0.01f; for(int i=0; i<100000000; ++i) { currencyAccumulator += currencyIncrement; doubleAccumulator += doubleIncrement; floatAccumulator += floatIncrement; } Console.WriteLine("Currency: {0}", currencyAccumulator); Console.WriteLine("Double: {0}", doubleAccumulator); Console.WriteLine("Float: {0}", floatAccumulator); Console.ReadLine(); } } struct Currency { private const double EPSILON = 0.00005; public Currency(double value) { this.value = value; } private double value; public static Currency operator +(Currency a, Currency b) { return new Currency(a.value + b.value); } public static Currency operator -(Currency a, Currency b) { return new Currency(a.value - b.value); } public static Currency operator *(Currency a, double factor) { return new Currency(a.value * factor); } public static Currency operator *(double factor, Currency a) { return new Currency(a.value * factor); } public static Currency operator /(Currency a, double factor) { return new Currency(a.value / factor); } public static Currency operator /(double factor, Currency a) { return new Currency(a.value / factor); } public static explicit operator double(Currency c) { return System.Math.Round(c.value, 4); } public static implicit operator Currency(double d) { return new Currency(d); } public static bool operator <(Currency a, Currency b) { return (a.value - b.value) < -EPSILON; } public static bool operator >(Currency a, Currency b) { return (a.value - b.value) > +EPSILON; } public static bool operator <=(Currency a, Currency b) { return (a.value - b.value) <= +EPSILON; } public static bool operator >=(Currency a, Currency b) { return (a.value - b.value) >= -EPSILON; } public static bool operator !=(Currency a, Currency b) { return Math.Abs(a.value - b.value) <= EPSILON; } public static bool operator ==(Currency a, Currency b) { return Math.Abs(a.value - b.value) > EPSILON; } public bool Equals(Currency other) { return this == other; } public override int GetHashCode() { return ((double)this).GetHashCode(); } public override bool Equals(object other) { return other is Currency && this.Equals((Currency)other); } public override string ToString() { return this.value.ToString("C4"); } } 

Result:

Currency: $1,000,000.0008 Double: 1000000.00077928 Float: 262144 

We're only up to .08 cents, but eventually that'll add up.


Your edit:

 static void Main(string[] args) { Currency c = 1.00; c /= 100000; c *= 100000; Console.WriteLine(c); Console.ReadLine(); } } struct Currency { private const double EPS = 0.00005; private double val; public Currency(double val) { this.val = Math.Round(val, 4); } public static Currency operator +(Currency a, Currency b) { return new Currency(a.val + b.val); } public static Currency operator -(Currency a, Currency b) { return new Currency(a.val - b.val); } public static Currency operator *(Currency a, double factor) { return new Currency(a.val * factor); } public static Currency operator *(double factor, Currency a) { return new Currency(a.val * factor); } public static Currency operator /(Currency a, double factor) { return new Currency(a.val / factor); } public static Currency operator /(double factor, Currency a) { return new Currency(a.val / factor); } public static explicit operator double(Currency c) { return Math.Round(c.val, 4); } public static implicit operator Currency(double d) { return new Currency(d); } public static bool operator <(Currency a, Currency b) { return (a.val - b.val) < -EPS; } public static bool operator >(Currency a, Currency b) { return (a.val - b.val) > +EPS; } public static bool operator <=(Currency a, Currency b) { return (a.val - b.val) <= +EPS; } public static bool operator >=(Currency a, Currency b) { return (a.val - b.val) >= -EPS; } public static bool operator !=(Currency a, Currency b) { return Math.Abs(a.val - b.val) < EPS; } public static bool operator ==(Currency a, Currency b) { return Math.Abs(a.val - b.val) > EPS; } public bool Equals(Currency other) { return this == other; } public override int GetHashCode() { return ((double)this).GetHashCode(); } public override bool Equals(object o) { return o is Currency && this.Equals((Currency)o); } public override string ToString() { return this.val.ToString("C4"); } } 

Prints $0.

7 Comments

@Mark: His answer was irrelevant originally, but after the edit, it was right on the mark, as I commented. :)
@Mehrdad: Even before he used your class, the point was exactly the same. If you're using a finite-precision number internally, you'll always run into the same problem.
@Mark: Actually, I just made an edit to the constructor that seems to work fine with this example. Could you provide another example that breaks it?
Why'd you make the methods non-public? Now it won't compile. Also, now you're just rounding off the error to make it "correct" again. See my edit.
@Mark: Yeah, see @J Trana's answer, I just found a counterexample myself too. :) Thanks a lot for the answer! +1
|
2

Mehrdad, I don't think I could convince you if I brought in the entire SEC. Now, your entire class basically implements BigInteger arithmetic with an implied shift of 2 decimal places. (It should be at least 4 for accounting purposes, but we can change 2 to 4 easily enough.)

What advantage do we have backing this class with double instead of BigDecimal (or longlong if something like that is available)? For the advantage of a primitive type I pay with expensive rounding operations. And I also pay with inaccuracies. [Example from here 1]

import java.text.*; public class CantAdd { public static void main(String[] args) { float a = 8250325.12f; float b = 4321456.31f; float c = a + b; System.out.println(NumberFormat.getCurrencyInstance().format(c)); } } 

OK, here we backed with a float instead of a double, but shouldn't that be a BIG warning flag that the whole concept is wrong and that we may get in trouble if we have to make millions of calculations?

Every professional who works in finance believes that floating-point representation of money are a bad idea. (See, among dozens of hits, http://discuss.joelonsoftware.com/default.asp?design.4.346343.29.) Which is more likely: they are all stupid, or floating-point money is indeed a bad idea?

4 Comments

Well it's quite all right if you can't (lots of others couldn't either!), but it seems like @J Trauna could. :) I was looking for an example with my structure, which was pretty convincing. :)
Using a 7-digit (actually, 24-bit) data type for 9-digit values is going to cause problems regardless of the radix.
To be fair, Andrew, "everyone doing it" isn't a sufficient reason to do something. It may be a reason to halt production until you've researched what others do, but if you don't understand why they do what they do, eventually you're going to screw it up. A best practice is worthless without the problem it solves.
Historically, double was sometimes the best type to represent money, if one stored a value which was scaled so it always represented whole numbers. Modern languages all have 64-bit integer types, but 20 years ago the largest integer type was limited values of 4 billion or less, while double could handle whole numbers two million times larger with perfect precision. Nowadays, however, there's seldom any reason to use doubles for whole-number arithmetic.
0
Cur c = 0.00015; System.Console.WriteLine(c); // rounds to 0.0001 instead of the expected 0.0002 

The problem is that 0.00015 in binary is really 0.00014999999999999998685946966947568625982967205345630645751953125, which rounds down, but the exact decimal value rounds up.

4 Comments

Doesn't illustrate the problem with my currency structure. :\
See my edit. (It rounds to 4 digits rather than 2, but same concept.)
You'd get the same problem with 0.00015 rounded to 4 decimal places.
It's not quite the issue you're talking about, but I saw a different example in the accepted answer; see that one for an example.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.