Osy#betaRun it on Osyrin: osyrin.com

Reference / Types

bigdecimal

bigdecimal Units = 99999999999999999999999999999999999999n;

Exact base-10 arithmetic with 38 significant digits — for warehouse columns and anything else too wide for `decimal`. Every rule it follows is `decimal`'s rule; the only difference is how many digits fit. Like `decimal`, it behaves identically on the server and in the browser.

stable2 examples compiled by CItypesnumbersmoneywarehouse

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 2

Description#

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:

operationthe result's scale
a + b, a - b, a % bthe larger of the two scales
a * bthe two scales added — 1.5n * 1.5n is 2.25n, not 2.3n
a / bcomputed 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.Truncatezero
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.5n2.5n
Math.Floor — toward −∞-32
Math.Ceiling — toward +∞-23
Math.Truncate — toward zero-22

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 mean

It 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

Related

decimal

Exact base-10 arithmetic, for money and anything else where a fraction of a cent matters. It behaves identically…

long

A 64-bit whole number, exact to its full range — ids, sequence numbers, row versions, byte offsets. It holds the same…

Every type, in one list

The complete vocabulary of built-in types — the scalars you can store, the collections, the two callable spellings…

Crossing the network

An Osy# app runs in two places — the server, which holds the data, and the browser, which holds the screen — and the…