4

We are working with financial calculations. I found this post about storing money values as decimals: decimal vs double! - Which one should I use and when?

So I'm storing amount as decimals.

I have the following calculation: 12.000 * (1/12) = 1.000

If I use a decimal data type for storing the amount and the result amount I not get the expected result

// First approach:    
decimal ratio = 1m / 12m;
decimal amount = 12000;
decimal ratioAmount = amount * ratio;
ratioAmount = 999.9999999999999

// Second approach:
double ratio = 1d / 12d;
decimal amount = 12000;
decimal ratioAmount = (decimal)((double)amount * ratio);
ratioAmount = 1.000

// Third approach:
double ratio = 1d / 12d;
double amount = 12000;
double ratioAmount = amount * ratio;
ratioAmount = 1.000

What is the best way? Everyone is talking about that amounts/money must be stored as decimals.

Marcel
  • 215
  • 3
  • 7

4 Answers4

10

Never, ever, ever, ever store financial amounts in a double. Here's an example from my blog that shows why double shouldn't be used:

var lineValues = new List<double> { 1675.89, 2600.21, 5879.79, 5367.51, 8090.30, 492.97, 7888.60 };
double dblAvailable = 31995.27d;
double dblTotal = 0d;

foreach (var lineValue in lineValues)
{
    dblTotal += lineValue;
}

if (dblAvailable < dblTotal)
{
    Console.WriteLine("They don't add up!");
}

You'll see that the Console.WriteLine will be hit because the doubles actually add up to 31995.270000000004. As you may be able to guess from the names of the variables, this code example was based on some actual code in a finance system - this issue caused users to not be able to correctly allocate amounts to transactions.

Adding the numbers up as decimals with this additional code:

decimal decAvailable = (decimal)dblAvailable;
decimal decTotal = (decimal)dblTotal;

if (decAvailable < decTotal)
{
    Console.WriteLine("They still don't add up!");
}

Won't hit the Console.WriteLine. The moral of the story: use decimal for financial calculations!

The very first part of the language reference for the decimal keyword states:

Compared to other floating-point types, the decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.

It's also worthy of note that for a numeric literal to be treated as a decimal, the suffix m (for money) should be used, further pointing towards the appropriateness of the type for financial data.

Rob
  • 45,296
  • 24
  • 122
  • 150
  • 1
    no doubt you're completely right, it just might be good explicitly state which type should be used. implicitly, its clear, but for people who don't know might be useful ;) (just like you have *the moral of the story* on your blog) – dee zg Feb 16 '19 at 16:52
  • 1
    @deezg, thanks & good point! Added =) – Rob Feb 16 '19 at 16:58
  • 1
    All of the information in this answer is correct, but it doesn't address the OP's concerns--it doesn't explain why they didn't get the results they expected. – xander Feb 16 '19 at 18:36
10

It seems that all those posts get close, but don't quite explain the crux of the issue. It's not that decimal stores values more precisely or that double has more digits or something like that. They each store values differently.

The decimal type stores values in a decimal form. Like 1234.567. The double (and float) stores values in a binary form, like 1101010.0011001. (They also do have limits of how many digits they can store, but that's not relevant here - or ever. If you feel like you're running out of digits for precision, you're probably doing something wrong)

Note that there are certain values that cannot be stored precisely in either notation because they would require an infinite amount of digits after the decimal point. Like 1/3 or 1/12. Such values get rounded a bit when stored, which is what you're seeing here.

The advantage of decimal in financial calculations is that it can store decimal fractions precisely whereas double can't. For example 0.1 can be stored precisely in decimal but not in double. Those are the kinds of values that money amounts usually take. You never need to store 2/3 of a dollar, you need 0.66 dollars exactly. Human currencies are decimal-based, so the decimal type can store them well.

In addition, adding and subtracting decimal values works flawlessly with the decimal type too. And that's the most common operation in financial calculations, so it's easier to program that way.

Multiplying decimal values works pretty well too, although it can increase the number of decimal places used to ensure an exact value.

But dividing is very risky because most values that you obtain by dividing won't be storable precisely and a rounding error will occur.

At the end of the day both double and decimal can be used to store monetary values, you just need to be very careful about their limitations. For a double type you need to round the result after every calculation, even addition and subtraction. And whenever you display values to the user, you need to explicitly format them to have a certain number of decimal digits. In addition, when comparing numbers, take care that you compare only the first X decimal digits (usually 2 or 4).

For a decimal type some of these restrictions can be relaxed since you know that your monetary value is stored precisely. You can usually skip rounding after addition and subtraction. If you only store X decimal digits in the first place, you don't need to worry about explicit display formatting and comparison. It does make things considerably easier. But you still need to round after multiplication and division.

There is one more elegant approach not discussed here. Change your monetary units. Instead of storing dollar values, store cent values. Or if you work with 4 decimal digits, store 1/100ths of a cent.

Then you can use int or long for everything!

This has most of the same advantages of a decimal (values stored precisely, addition/subtraction works precisely), but the places you need to round things will become even more obvious. A slight drawback however is that formatting such values for display becomes a bit more complicated. On the other hand, if you forget to do it, that too will be obvious. This is my preferred approach so far.

Vilx-
  • 104,512
  • 87
  • 279
  • 422
4

Everyone telling you to use decimal is correct. Even the official docs say that decimal is the thing to use:

Compared to other floating-point types, the decimal type has more precision and a smaller range, which makes it appropriate for financial and monetary calculations.

The seemingly-incorrect behavior you've observed comes from the fact that 1/12 can't be perfectly expressed as a decimal.

I've modified your examples slightly, and presented them as xUnit tests. All of the assertions in the examples pass.

This is the example that's giving you trouble...

[Fact]
public void FirstApproach()
{
    // First approach:    
    decimal ratio = 1m / 12m;
    decimal amount = 12.000m;

    decimal ratioAmount = amount * ratio;

    Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}

Clearly, 12 * (1/12) should be 1, so this seems wrong.

With a slight modification, we can get the "correct" answer...

[Fact]
public void ModifiedFirstApproach()
{
    // Values from first approach,
    // but with intermediate variables removed
    decimal ratioAmount = 12.000m * 1m / 12m;

    Assert.Equal(1.000m, ratioAmount);
}

The problem, then appears to be the intermediate variable ratio, although it's more accurate to think of it as an order-of-operations problem. The addition of parentheses re-introduces the error from the original code...

[Fact]
public void AnotherModifiedFirstApproach()
{
    // Values from first approach,
    // but with intermediate variables removed
    decimal ratioAmount = 12.000m * (1m / 12m);

    Assert.Equal(0.9999999999999999999999999996m, ratioAmount);
}

The core problem can be illustrated in a single line...

[Fact]
public void OneTwelfthAsDecimal()
{
    Assert.Equal(0.0833333333333333333333333333m, 1m / 12m);
}

The fraction 1/12 can only be expressed as a repeating decimal, which makes it imprecise. This isn't C#'s fault--it's just a fact of working in a decimal (base-10) number system.

xander
  • 1,689
  • 10
  • 18
  • How do you store 1/12 precisely as a binary fraction? I could be mistaken, but it seems to me that it should be an infinitely repeating number. – Vilx- Feb 16 '19 at 17:53
  • Actually, I'm certain you can't. Think about it. If you could, that would mean that there exist such integers `x` and `y` that `x/(2^y)=1/12`. This means that `12x=2^y`. But that's impossible since `12` is divisable by `3`, yet `2^y` isn't. – Vilx- Feb 16 '19 at 18:05
  • I may have gotten ahead of myself with that claim. I'll remove it from my answer while I reason through it myself. – xander Feb 16 '19 at 18:08
  • @Vilx- You are correct, and (now that I've reasoned through it) that' a very clear explanation. Thanks. – xander Feb 16 '19 at 18:26
  • You're welcome. :) – Vilx- Feb 16 '19 at 18:28
3

decimal stores 28-29 significant digits whereas double stores ~15-17 digits

when you divide 1m to 12m (1m/12m) its result is 0.0833333333333333333333333333.....3 where 3s are infinite. float and double rounds it to nearest 0.083333333333333329.

when 0.0833333333333333333333333333.....3 is multiplied by 12000 the result is 999.9999999999999999...999999996 but since Decimal has 28-29 significant digint places it does not evaluate 0.0833333333333333333333333333 more than this. and when 0.0833333333333333333333333333 is multiplied by 12000 the overall result is 999.9999999999999999999999996


Mathematically

1/12 = 0.0833333333333333333333333333.....3
(1/12) x 12000 = 999.9999999999999999...999999996

Mathematically Decimal evaluates

1m/12m = 0.0833333333333333333333333333
(1m/12m) * 12000 = 999.9999999999999999999999996

Mathematically Double evaluates

1d/12d = 0.083333333333333329 // looses precision
(1d/12d) * 12000 = 1000 // rounded
Derviş Kayımbaşıoğlu
  • 28,492
  • 4
  • 50
  • 72
  • 1
    Besides these roundings also the parentheses should be taken into consideration as the `amount * (1m/12m) != amount * 1m/12m`. – Samvel Petrosov Feb 16 '19 at 17:10