Summary#
Exact base-10 arithmetic, for money and anything else where a fraction of a cent matters. decimal is not a binary
float: 0.1 + 0.2 is exactly 0.3, and it behaves identically whether your code runs on the server or in the browser.
Signature#
decimal Total = 19.99m; // the `m` suffix makes a literal a decimalDescription#
Use decimal for money, quantities, tax rates, percentages — anything where the answer has to be the answer rather
than a very close one. Use double for measurements and scientific values, where the extra range matters more than the
last digit.
The difference is not cosmetic. A binary float cannot represent 0.1 exactly, so 0.1 + 0.2 lands a hair above 0.3
and 1.005 * 100 lands a hair below 100.50 — which then rounds to 100.49. A decimal stores the digits you
wrote, so it gives the answer you would give.
decimal price = 1.005m;
decimal total = price * 100m; // exactly 100.500 — a double would say 100.49999999999999They never mix, and that is on purpose#
There is no conversion between decimal and double in either direction, so no operator over the pair has a
type — exactly as in C#. Every other numeric combination widens (int → long → decimal, and int → long →
double); only the rung between these two is missing, because crossing it would have to throw away either the exact
digits or the range, and only you know which.
The one that catches people is the literal: a bare 1.5 is a double, so total * 1.5 on a decimal is a
mix and is refused. Write the literal in the type you mean, or convert explicitly.
decimal total = 100.50m;
decimal a = total * 1.5m; // ✓ decimal × decimal
decimal b = total * 1.5; // ✗ refused — a bare 1.5 is a double
decimal c = total * (decimal)ratio; // ✓ explicit, and you can see where the conversion happens
double d = (double)total * ratio; // ✓ the other direction, equally explicitComparisons follow the same rule: total == ratio and total > ratio are refused too, for the same reason — one
side would have to be converted to answer, and neither conversion is lossless.
The same answer everywhere#
An Osy# expression means one thing. A decimal is exact wherever the function runs — server-side, or in-process in
the browser (see Crossing the network). A comparison that is true on one side is true on the other; a total that
prints "1.10" on one side prints "1.10" on the other. You do not need to know, or care, where a piece of code
executes in order to trust its arithmetic.
Rounding goes half to even, as in C##
Math.Round breaks a tie to the even neighbour, which is C#'s own default: 2.5 rounds to 2, 3.5 to 4 and
-2.5 to -2. That is the rule for every numeric type here — decimal, bigdecimal and
double alike — on the server, in the browser and inside a query.
Math.Round(2.5m) // 2
Math.Round(3.5m) // 4
Math.Round(-2.5m) // -2
Math.Round(1.005m, 2) // 1
Math.Round(1.005m, 2, MidpointRounding.AwayFromZero) // 1.01⚠ Money often wants the other rule. Most invoices and tax rules round a half UP (away from zero), so a price,
a VAT amount or a converted total says so: Math.Round(amount, 2, MidpointRounding.AwayFromZero). Every mode is
listed at [[function-math#round]].
Round explicitly when you present a value. Rounding as you go accumulates error just as surely as a float would.
Trailing zeros are part of the value#
1.10m keeps its two decimal places and prints as "1.10" — a currency total keeps its cents column. But equality is
numeric, so 1.1m == 1.10m is true. Arithmetic follows the same rules you would use on paper: addition takes the
wider of the two scales, and multiplication adds them.
1.10m + 2.20m // 3.30
1.5m * 1.5m // 2.25
1.1m == 1.10m // trueExamples#
class Line {
decimal Price;
int Quantity;
decimal Total() {
return Price * Quantity;
}
}decimal WithTax(decimal subtotal, decimal rate) {
return Math.Round(subtotal * (1m + rate), 2);
}See also#
- Crossing the network — where a function runs; a decimal is exact on either side