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 double
s 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.
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?
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With