Decimals and Floating Point

double and float are binary approximations, decimal is base-10 and exact for cents, which is why money belongs in decimal.

Syntaxdouble d = 1.5; float f = 1.5f; decimal m = 1.5m;

These are the types for numbers with a fractional part: 2.5, 0.001, 19.99. There are three of them, and the difference between them is not size. It is how they store the fraction.

Binary fractions

double and float store a number as a sum of halves, quarters, eighths and so on. Any fraction that can be built that way is held exactly, which is why 0.5 and 0.75 cause no trouble. 0.1 cannot be built that way, so what gets stored is the closest value that can be. The error is tiny, but it is real, and it accumulates. 0.1 + 0.2 as a double is 0.30000000000000004, not 0.3.

decimal stores the digits in base 10, the way you would write them on paper. 0.1 is held exactly. 19.99 times 3 is exactly 59.97. That is why money goes in decimal.

The three types

TypeSizePrecisionSuffix
float4 bytesabout 6 to 9 digitsf
double8 bytesabout 15 to 17 digitsnone, or d
decimal16 bytes28 to 29 digitsm

A number written with a point and no suffix is a double. The keywords are shorthands for .NET type names and mean the same thing: double is System.Double, decimal is System.Decimal, and float is System.Single.

What trips people up

Comparing two doubles with == asks whether every bit matches. After any arithmetic it usually does not. Compare the size of the difference against a tolerance you choose instead.

Example

Example · csharp
// double stores numbers in binary, so most decimal fractions are approximate.
double a = 0.1;
double b = 0.2;
Console.WriteLine(a + b);
Console.WriteLine((a + b) == 0.3);

// decimal stores numbers in base 10, so decimal fractions are exact.
decimal c = 0.1m;
decimal d = 0.2m;
Console.WriteLine(c + d);
Console.WriteLine((c + d) == 0.3m);

// Money belongs in decimal. Three items at 19.99 is exactly 59.97.
decimal unitPrice = 19.99m;
int quantity = 3;
Console.WriteLine(unitPrice * quantity);

// Halves, quarters and eighths are exact in binary, so these are fine.
Console.WriteLine(0.5 + 0.25);

// Output:
// 0.30000000000000004
// False
// 0.3
// True
// 59.97
// 0.75

When to use it

  • Store a price, a tax rate or an account balance as decimal so the cents never drift.
  • Store a sensor reading, a distance or a physics calculation as double, where speed matters and a tiny error does not.
  • Assert on a computed double in a test by comparing the difference against a tolerance.
  • Check a divided double with double.IsNaN before using the result.

More examples

Comparing doubles safely

The pattern for every double comparison: subtract, take the absolute value, and test it against a tolerance that suits the quantity you are measuring. The tolerance is a decision about your data, not a constant you can look up. Use this in any test that asserts on a computed double, which is where the problem usually surfaces first.

Example · csharp
double computed = 0.1 + 0.2;
double expected = 0.3;

// Equality on doubles asks for an exact match, which rarely survives arithmetic.
Console.WriteLine(computed == expected);

// Compare the size of the difference against a tolerance you choose.
double difference = Math.Abs(computed - expected);
Console.WriteLine(difference < 0.000001);

// Rounding is for display, and takes the number of decimal places to keep.
Console.WriteLine(Math.Round(computed, 2));

// Output:
// False
// True
// 0.3

A cart total in decimal

Every amount is a decimal literal with the m suffix, and the tax is rounded on purpose rather than left to accumulate. The last two lines show something people file bugs about: decimal keeps the scale you gave it, so 10.00m prints as 10.00 while still comparing equal to 10m. Reach for this shape for any price, rate or balance.

Example · csharp
// A cart total, computed in decimal so the cents stay exact.
decimal lineOne = 19.99m;
decimal lineTwo = 4.95m;
decimal subtotal = lineOne * 3 + lineTwo;
decimal tax = Math.Round(subtotal * 0.2m, 2);
Console.WriteLine(subtotal);
Console.WriteLine(tax);
Console.WriteLine(subtotal + tax);

// decimal remembers the trailing zeros you wrote, and that shows when printed.
decimal padded = 10.00m;
Console.WriteLine(padded);
Console.WriteLine(padded == 10m);

// Output:
// 64.92
// 12.98
// 77.90
// 10.00
// True

Limits, and not-a-number

decimal has by far the most digits of precision but the smallest range, which is the trade it makes. The last part matters when you divide: dividing a double by zero does not throw, it produces NaN or an infinity, and NaN is not equal to anything including itself. Test for it with double.IsNaN rather than with ==.

Example · csharp
// The three types trade range, precision and size against each other.
Console.WriteLine(float.MaxValue);
Console.WriteLine(double.MaxValue);
Console.WriteLine(decimal.MaxValue);

// double and float carry a value for "not a number". decimal does not.
double undefined = 0.0 / 0.0;
Console.WriteLine(undefined);
Console.WriteLine(double.IsNaN(undefined));

// Output:
// 3.4028235E+38
// 1.7976931348623157E+308
// 79228162514264337593543950335
// NaN
// True

Discussion

  • Be the first to comment on this lesson.