Summary#
bigdecimal is decimal with more room: 38 significant digits instead of ~28. Everything else — that scale is
part of the value, how arithmetic changes it, how rounding breaks a tie — is identical, on purpose. One set of
rules, two widths.
Reach for it when a number genuinely does not fit in a decimal: a Snowflake NUMBER(38,0) (which is Snowflake's
default integer), a Databricks DECIMAL(38,s), a ledger keyed by a very large identifier. For money, keep using
decimal — it is narrower, cheaper, and wide enough.
Signature#
bigdecimal units = 99999999999999999999999999999999999999n; // 38 nines — the widest value it holds
bigdecimal rate = 1.10n; // scale 2, and it stays scale 2Description#
The rules, and why each one is what it is#
These are the answers to every question the type can be asked. They are decimal's answers, and they are written
out here rather than left to be discovered, because a rounding rule you find out about from a reconciliation report
is a rule that was not documented.
Scale is part of the value. 1.10n has scale 2 and prints "1.10" — not "1.1". Scale is data, not
formatting; a total that renders as 1.1 on an invoice is wrong even though the number is right.
…but equality ignores it. 1.1n == 1.10n is true. Those two rules sound contradictory and both are C#'s:
arithmetic preserves scale, comparison does not consider it.
Arithmetic changes scale by fixed rules, so a result's precision is predictable rather than a side effect:
| operation | the result's scale |
|---|---|
a + b, a - b, a % b | the larger of the two scales |
a * b | the two scales added — 1.5n * 1.5n is 2.25n, not 2.3n |
a / b | computed to 39 decimal places (truncating), rounded to 38 places half-even, then trailing zeros removed |
-a, Math.Abs(a) | unchanged — Math.Abs(-2.50n) is 2.50n |
Math.Floor, Math.Ceiling, Math.Truncate | zero |
Math.Round(a, places) | places |
Division is the one that trims, and only division. 1n / 2n is 0.5n — not 0.5000… padded to 38 places —
while 1n / 3n keeps its full 38-digit tail. Addition and multiplication never trim: 1.10n + 2.20n is 3.30n.
Math.Round breaks a tie HALF TO EVEN, as C# does. Math.Round(2.5n) is 2 and Math.Round(-2.5n) is -2.
That is the rule for every numeric type, so Math.Round means the same thing whichever kind of number you hand
it. To round a half away from zero, say so: Math.Round(2.5n, MidpointRounding.AwayFromZero) is 3.
⚠ Division rounds differently, and only division. Its last digit — the 38th — is settled half to even,
because that step is not a rounding you asked for but the point at which an endless quotient has to stop. It is the
one place the two rules differ, and it is below anything Math.Round can reach.
Remainder takes the sign of the left operand. -7n % 3n is -1n, not 2n. Same as C#, and as JavaScript.
Floor, Ceiling and Truncate differ on negatives, so pick deliberately:
-2.5n | 2.5n | |
|---|---|---|
Math.Floor — toward −∞ | -3 | 2 |
Math.Ceiling — toward +∞ | -2 | 3 |
Math.Truncate — toward zero | -2 | 2 |
Too big is an error, never a wrap. A result past 38 digits raises rather than silently truncating or wrapping. Too many decimal places is different and not fatal: a scale past 38 is rounded (to even) down to 38 first. Only the magnitude is fatal.
A value that is not a number is an error, never zero. Parsing "abc" raises. Exponent form (1.5e3, 1e-3)
is accepted, because both the wire and every warehouse use it.
Choosing between it and decimal#
They do not mix implicitly, for the reason decimal and double do not: a conversion has to lose something, and
only you know which loss is acceptable. Widening is exact and explicit; narrowing can raise.
decimal money = 19.99m;
bigdecimal wide = (bigdecimal)money; // ✓ exact — every decimal fits
decimal back = (decimal)wide; // ✓ explicit, and raises if it no longer fits
bigdecimal mixed = money * wide; // ✗ refused — say which type you meanIt is the same number on both sides#
Like decimal, bigdecimal gives identical answers in a server function and in a browser action — same digits,
same scale, same rounding. That is not a promise made in prose: both runtimes are held to one shared table of
cases generated by a third implementation, so neither can drift toward the other's bugs.
Examples#
// A Snowflake NUMBER(38,0) — its DEFAULT integer column — which a decimal cannot hold.
bigdecimal Units(bigdecimal a, bigdecimal b) {
return a + b;
}// Scale survives arithmetic, which is what makes a rendered total trustworthy.
bigdecimal Line(bigdecimal unitPrice, bigdecimal qty) {
return unitPrice * qty; // 1.10n * 3n → 3.30n, and it prints "3.30"
}See also#
- decimal — narrower, cheaper, and the right choice for money
- long — an exact 64-bit integer, when 19 digits is enough and you want no scale at all
- Crossing the network — why a type behaving identically on both sides matters