C# & .NET (C# 14, .NET 10 LTS) Module 2 – Types, variables and operators
double, float and decimal: money and rounding
Why 0.1 + 0.2 is not 0.3 in a C# double, why decimal gets it right, how Math.Round treats halves, and which type fits money, measurements and counts.
What you will learn
- Explain why 0.1 + 0.2 is not exactly 0.3 in a double, and when that matters
- Choose between float, double, decimal and a whole count of paise for a kind of value
- Use Math.Round with a MidpointRounding mode chosen on purpose to round amounts
- Build a repayment schedule whose rounded instalments add up to the loan exactly
- Compare doubles with a tolerance, and handle NaN and infinity
Before you start
What you’ll learn · Before you start
What you will learn
- Explain why 0.1 + 0.2 is not exactly 0.3 in a double, and when that matters
- Choose between float, double, decimal and a whole count of paise for a kind of value
- Use Math.Round with a MidpointRounding mode chosen on purpose to round amounts
- Build a repayment schedule whose rounded instalments add up to the loan exactly
- Compare doubles with a tolerance, and handle NaN and infinity
Before you start
On this page
In a C# double, 0.1 + 0.2 is 0.30000000000000004, and 0.1 + 0.2 == 0.3 is false. The same sum in decimal
is exactly 0.3, and the choice between the two decides whether a bill adds up to the last paisa.
You will see what double and float really store, why decimal gets 0.1 right, how Math.Round treats a value
that lies exactly halfway, and which type fits money, measurements and counts.
double and float store binary fractions, so 0.1 is never exact
double and float are binary floating point: A way of storing a number as a binary fraction times a power of two, used by float and double. Tenths and other decimal fractions have no exact binary form, so they are stored as the nearest value that fits. types of 64 and 32 bits. Halves,
quarters and eighths have exact binary forms; a tenth has none, just as a third has no exact decimal one. So the
literal 0.1 becomes the nearest double instead. This program prints what is stored, to 27 decimals:
using System.Globalization;
// Every number prints with the invariant culture, the same on every computer.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
double a = 0.1;
double b = 0.2;
double sum = a + b;
Console.WriteLine($"double: 0.1 + 0.2 = {sum}");
Console.WriteLine($" equal to 0.3: {sum == 0.3}");
Console.WriteLine($" 0.1 is stored as {a:F27}");
Console.WriteLine($" 0.2 is stored as {b:F27}");
// The two stored values added exactly, in decimal, from the 27 decimals printed above.
decimal exact = decimal.Parse(a.ToString("F27")) + decimal.Parse(b.ToString("F27"));
Console.WriteLine($" their exact sum {exact}");
Console.WriteLine($" the sum is {sum:F27}");
Console.WriteLine($" 0.3 is stored as {0.3:F27}");
float x = 0.1f;
float y = 0.2f;
Console.WriteLine($"float: 0.1 + 0.2 = {x + y}, equal to 0.3: {x + y == 0.3f}");
decimal m = 0.1m;
decimal n = 0.2m;
Console.WriteLine($"decimal: 0.1 + 0.2 = {m + n}, equal to 0.3: {m + n == 0.3m}"); Output
double: 0.1 + 0.2 = 0.30000000000000004
equal to 0.3: False
0.1 is stored as 0.100000000000000005551115123
0.2 is stored as 0.200000000000000011102230246
their exact sum 0.300000000000000016653345369
the sum is 0.300000000000000044408920985
0.3 is stored as 0.299999999999999988897769754
float: 0.1 + 0.2 = 0.3, equal to 0.3: True
decimal: 0.1 + 0.2 = 0.3, equal to 0.3: True
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run sum.cs
0.1 and 0.2 are stored a little above their true values and 0.3 a little below. Step through the addition:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 0.1 in binary. A double holds a binary fraction. 0.1 has no exact binary form, the way 1/3 has no exact decimal one, so the literal 0.1 is stored as the nearest double, a little above 0.1.
- 0.2 in binary. 0.2 is stored the same way, also a little above its true value. Both errors are tiny, in the 17th digit and beyond.
- The exact sum. The processor adds the two stored values. Their exact sum, 0.300000000000000016653345369 to 27 decimals, has more binary digits than a double can keep.
- Rounded once more. The sum is rounded to the nearest double, 0.300000000000000044408920985. That is not the double the literal 0.3 becomes: 0.3 is stored as 0.299999999999999988897769754, just below 0.3.
- Printed and compared. Printing a double shows the shortest text that turns back into the same double, 0.30000000000000004. The == operator compares the stored values, which differ, so 0.1 + 0.2 == 0.3 is false.
The default text of a double, 0.30000000000000004, is the shortest text that reads back as the same stored value,
which is why the error shows. In a float the same sum printed 0.3 and even compared equal, because with the 24
significant bits of a float both sides happened to round to the same value: luck, not exactness.
Comparing doubles with ==
0.1 + 0.2 == 0.3 is false, so a test like if (total == 0.3) silently fails. Compare doubles with a tolerance
chosen for the size of the values, such as Math.Abs(a - b) <= 1e-9, or keep the values in decimal.
decimal stores decimal digits, so 0.1 and 0.2 add up to 0.3
decimal is a 128-bit type that stores a whole number of up to 96 bits and a power of ten, from 0 to 28, to divide
it by. 0.1m is 1 divided by 10, which is exact, so the same sum printed 0.3 and compared equal. The m suffix makes
a literal a decimal, and C# refuses to mix it with a double silently:
// A price in decimal and a rate in double, mixed without a conversion.
decimal price = 99.99m;
double rate = 0.18;
decimal tax = price * rate;
decimal discount = 0.05;
Console.WriteLine(tax - discount); from session import run
# mix multiplies a decimal by a double and stores a double literal in a decimal; it is built once.
run("""
cd mix
!dotnet build -v q
""") Output
$ cd mix
$ dotnet build -v q
mix/Program.cs(4,15): error CS0019: Operator '*' cannot be applied to operands of type 'decimal' and 'double' [mix/App.csproj]
mix/Program.cs(5,20): error CS0664: Literal of type double cannot be implicitly converted to type 'decimal'; use an 'M' suffix to create a literal of this type [mix/App.csproj]
Build FAILED.
mix/Program.cs(4,15): error CS0019: Operator '*' cannot be applied to operands of type 'decimal' and 'double' [mix/App.csproj]
mix/Program.cs(5,20): error CS0664: Literal of type double cannot be implicitly converted to type 'decimal'; use an 'M' suffix to create a literal of this type [mix/App.csproj]
0 Warning(s)
2 Error(s)
Time Elapsed …
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mix_session.py
CS0019 refuses decimal * double, and CS0664 refuses the double literal 0.05 in a decimal. Write 0.18m and
0.05m, or convert with an explicit cast where a double really arrives from elsewhere. How many digits does each
type keep? One third, then two whole numbers just past what float and double can count exactly:
using System.Globalization;
// Every number prints with the invariant culture, the same on every computer.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("One third in each type:");
Console.WriteLine($"Half {(Half)(1.0 / 3)}");
Console.WriteLine($"float {1f / 3}");
Console.WriteLine($"double {1.0 / 3}");
Console.WriteLine($"decimal {1m / 3}");
Console.WriteLine();
Console.WriteLine("Whole numbers past the digits a type holds:");
Console.WriteLine($"16,777,217 as a float: {(float)16_777_217:F0}");
Console.WriteLine($"9,007,199,254,740,993 as a double: {(double)9_007_199_254_740_993:F0}");
Console.WriteLine($"9,007,199,254,740,993 as decimal: {9_007_199_254_740_993m}"); Output
One third in each type: Half 0.3333 float 0.33333334 double 0.3333333333333333 decimal 0.3333333333333333333333333333 Whole numbers past the digits a type holds: 16,777,217 as a float: 16777216 9,007,199,254,740,993 as a double: 9007199254740992 9,007,199,254,740,993 as decimal: 9007199254740993
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run digits.cs
| Type | Size | Digits kept | Exact for 0.1 |
|---|---|---|---|
float |
4 bytes | about 6 to 9 | no |
double |
8 bytes | about 15 to 17 | no |
decimal |
16 bytes | 28 to 29 | yes |
A float turned 16,777,217 into 16,777,216, and a double lost the last 1 of 9,007,199,254,740,993, which a decimal
kept. The 16-bit binary type Half kept only four digits of one third. Answer three questions about the values you
store, and the table rules out the types that would get them wrong:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Type | Exact for 0.10 | Digits and range | Size and speed |
|---|---|---|---|
| float | Poor no: binary fractions, and 16,777,217 became 16,777,216 | Poor about 6 to 9 digits | Good 4 bytes a value |
| double | Poor no: 0.1 + 0.2 printed 0.30000000000000004 | Good about 15 to 17 digits, up to about 1.7 × 10^308 | Good 8 bytes, and the usual choice where speed matters |
| decimal | Good yes: 0.1m + 0.2m printed 0.3 | Mixed 28 to 29 digits, but only up to about 7.9 × 10^28 | Mixed 16 bytes, and slower than double |
| long of paise or cents | Good yes: 10 paise is the whole number 10 | Mixed whole units only, no fraction of a paisa | Good 8 bytes, integer arithmetic |
How the answers decide
- “Are the values money, or other amounts that people write with decimal fractions?” Yes, money or decimal amounts: rules out float and double. A binary fraction cannot hold 0.10 exactly, so amounts drift: 0.1 + 0.2 printed 0.30000000000000004.
- “Are the values money, or other amounts that people write with decimal fractions?” No, measurements or scientific values: adds a caveat to decimal and long of paise or cents. A measurement is not exact to begin with; double keeps 15 to 17 digits of it in 8 bytes.
- “Do you need fractions of the smallest unit, such as interest rates or currency conversions?” Yes, fractions of a paisa or a cent: rules out long of paise or cents. A long of paise has no room for part of a paisa; decimal keeps 28 to 29 digits.
- “Do you store millions of values, where the size of each value matters?” Yes, millions of values: adds a caveat to decimal. decimal takes 16 bytes a value, twice a double and four times a float.
In short: decimal, or a whole count of paise in a long, for money; double for measurements and science; float
only when millions of values must stay small.
Checkpoint
Check: Binary fractions in double and float · decimal and choosing a type
Math.Round sends halves to the even neighbour unless you choose otherwise
Math.Round without a mode uses midpoint rounding: The rule for a value that lies exactly halfway between two results, such as 2.5 rounded to a whole number. MidpointRounding.ToEven, the default of Math.Round, picks the even neighbour; AwayFromZero picks the one further from zero. to the even neighbour,
MidpointRounding.ToEven, which Microsoft’s documentation also calls banker’s rounding:
using System.Globalization;
// Every number prints with the invariant culture, the same on every computer.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine($"Math.Round(2.5m) = {Math.Round(2.5m)}");
Console.WriteLine($"Math.Round(3.5m) = {Math.Round(3.5m)}");
Console.WriteLine($"Math.Round(2.5m, AwayFromZero) = {Math.Round(2.5m, MidpointRounding.AwayFromZero)}");
Console.WriteLine($"Math.Round(10.125m, 2) = {Math.Round(10.125m, 2)}");
Console.WriteLine($"Math.Round(10.125m, 2, AwayFromZero) = {Math.Round(10.125m, 2, MidpointRounding.AwayFromZero)}");
// 11.0 plus 0.1 five times is 11.5, a midpoint, in exact arithmetic.
double total = 11.0;
decimal exact = 11.0m;
for (int i = 0; i < 5; i++)
{
total += 0.1;
exact += 0.1m;
}
Console.WriteLine($"double: {total} rounds to {Math.Round(total, MidpointRounding.AwayFromZero)}");
Console.WriteLine($"decimal: {exact} rounds to {Math.Round(exact, MidpointRounding.AwayFromZero)}"); Output
Math.Round(2.5m) = 2 Math.Round(3.5m) = 4 Math.Round(2.5m, AwayFromZero) = 3 Math.Round(10.125m, 2) = 10.12 Math.Round(10.125m, 2, AwayFromZero) = 10.13 double: 11.499999999999998 rounds to 11 decimal: 11.5 rounds to 12
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run round.cs
2.5 became 2, 3.5 became 4, and to two places 10.125 became 10.12. A bill that should say 10.13 needs
MidpointRounding.AwayFromZero, passed on every call. The last two lines show that the type matters here too: adding
0.1 five times to 11.0 gave the double 11.499999999999998, just below the midpoint, which rounded to 11, while the
decimal sum was exactly 11.5 and rounded to 12. MidpointRounding has five modes:
ToEvensends a value that lies exactly halfway to the even neighbour, and it is the default.AwayFromZerosends a halfway value away from zero, the most widely known way to round.ToZerodrops the extra digits, like truncation.ToNegativeInfinityalways rounds down, likeMath.Floor.ToPositiveInfinityalways rounds up, likeMath.Ceiling.
Key idea
Keep money in decimal or in whole paise, round it with a mode you chose on purpose, and leave double for
measurements, where a tiny error is part of the value anyway.
Checkpoint
Check: Math.Round and midpoint rounding
Rounding money so that every total adds up, with the numbers Pro: opens with a Pro, Premium or Ultimate pass
Rounding one amount is easy; making many rounded amounts add up to the right total is where real bills go wrong. The runs below compare the five modes on the same values, build a loan schedule that reconciles to the paisa and round tax two ways.
In this part:
- All five modes on the same values, and the bias of rounding halves up
- An EMI schedule whose instalments add up to the loan
- Rounding tax on each line or once on the invoice
- Amounts that arrive as doubles, and the scale a decimal keeps
4 runnable examples (C#) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Comparing doubles: tolerances, NaN and infinity, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Floating-point errors grow as values are combined, and a few special values break the rules of ordinary comparison. The runs below measure the drift, compare three ways of testing two doubles for equality and walk through NaN, infinity and negative zero.
In this part:
- Errors add up: 0.1 summed ten times and a million times
- Equal enough: absolute, relative and both
- NaN, infinity and negative zero
2 runnable examples (C#) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: make the double round like the decimal
Round the double sum to 12
Copy round.cs to your computer. Before the last rounding, round the double total to 10 decimal places with
Math.Round(total, 10), then round that to a whole number away from zero. Predict what it prints, then run it with
dotnet run round.cs.
The questions below come one at a time, each picked from the idea your answers so far leave weakest:
Practice that adapts to you
Practise this lesson
9 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · Why does 0.1 + 0.2 == 0.3 come out false for C# doubles?
Because a double holds a binary fraction, and each of the literals 0.1, 0.2 and 0.3 lies between two such
fractions. Each one becomes the nearest double: 0.1 and 0.2 a little above their true values, 0.3 a little below. The sum of the two stored values
is rounded to the nearest double, 0.30000000000000004, which is not the double that 0.3 becomes, so == is false.
It matters wherever values must be exact, above all money. Use decimal, whose 0.1m is exactly one tenth, or whole
paise in a long; when doubles must be compared, use a tolerance instead of ==.
-
Mid to senior
An invoicing service keeps prices and totals as double, and finance reports totals that are off by a paisa now and then. Explain the cause, and how you would move the service to exact amounts without breaking the stored data or the API.
-
Fresher to mid
A customer's bill shows 10.12 where the customer worked out 10.13 from an amount of 10.125. The code calls Math.Round(amount, 2). Explain what happened, decide which rule the service should follow, and how you make that rule hold in every part of the code.
-
Fresher to mid
A unit test asserts that a computed interest of type double equals 1234.56, and it fails because the value is 1234.5600000000002. How do you write the assertion, and when is an exact comparison of doubles still right?
Key takeaways
doubleandfloatstore binary fractions, so 0.1 + 0.2 is 0.30000000000000004 in adoubleand==against 0.3 is false.decimalstores decimal digits and a power of ten, so 0.1m + 0.2m is exactly 0.3; it does not mix withdoublewithout a cast.- Money belongs in
decimalor in whole paise, and measurements indouble. Math.Roundsends halves to the even neighbour by default, so passMidpointRounding.AwayFromZerowhen a bill needs 10.125 to become 10.13.
Terms in this lesson
- binary floating point
- A way of storing a number as a binary fraction times a power of two, used by float and double. Tenths and other decimal fractions have no exact binary form, so they are stored as the nearest value that fits.
- midpoint rounding
- The rule for a value that lies exactly halfway between two results, such as 2.5 rounded to a whole number. MidpointRounding.ToEven, the default of Math.Round, picks the even neighbour; AwayFromZero picks the one further from zero.
References
- Floating-point numeric types (Microsoft Learn)
- Decimal struct (Microsoft Learn)
- Math.Round method (Microsoft Learn)
- MidpointRounding enum (Microsoft Learn)
- Standard numeric format strings (round-trip and G17) (Microsoft Learn)
- Arithmetic operators (floating-point overflow and round-off errors) (Microsoft Learn)
- Double.IsNaN method (Microsoft Learn)
- Convert.ToDecimal method (15 significant digits from a Double) (Microsoft Learn)
- Decimal.Scale property (Microsoft Learn)
- Further remarks on reducing truncation errors (W. Kahan) (Communications of the ACM)
- IEEE 754-2019: Standard for Floating-Point Arithmetic (IEEE Standards Association)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress