Why is using a NON-decimal data type bad for money?

前端 未结 5 1628
执念已碎
执念已碎 2021-02-08 23:13

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

tl;dr 2: Read the rest of the question please, before giving an

相关标签:
5条回答
  • 2021-02-08 23:37

    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?

    0 讨论(0)
  • 2021-02-08 23:38

    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.

    0 讨论(0)
  • 2021-02-08 23:44

    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.

    0 讨论(0)
  • 2021-02-08 23:47

    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?

    0 讨论(0)
  • 2021-02-08 23:57
    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.

    0 讨论(0)
提交回复
热议问题