Summary#
Math.Abs, Math.Sign, Math.Min, Math.Max, Math.Clamp, Math.Truncate, Math.Pow and Math.Sqrt
are the numeric helpers you reach for in a calculation. They join the rounding functions (Math.Round,
Math.Floor, Math.Ceiling), and like those they are pinned to the exact answer this platform's server
produces — which is not always what a bare double would give.
Signature#
Math.Abs(<number> x) -> decimal // magnitude
Math.Sign(<number> x) -> int // -1, 0, or 1
Math.Min(<number> a, <number> b) -> decimal
Math.Max(<number> a, <number> b) -> decimal
Math.Clamp(<number> x, <number> min, <number> max) -> decimal
Math.Truncate(<number> x) -> decimal // drop the fraction, toward zero
Math.Pow(<number> x, <number> y) -> decimal
Math.Sqrt(<number> x) -> decimal
Math.Sin(<number> radians) -> double // the trig family is ALWAYS double
Math.Cos(<number> radians) -> double
Math.Tan(<number> radians) -> double
Math.Asin(<number> value) -> double // the inverses, answering RADIANS
Math.Acos(<number> value) -> double
Math.Atan(<number> value) -> double
Math.Atan2(<number> y, <number> x) -> double // note the order: y first
Math.Log(<number> value) -> double // natural log, base e
Math.Log(<number> value, <number> newBase) -> double // note the order: VALUE first
Math.Log10(<number> value) -> double
Math.Log2(<number> value) -> double
Math.Exp(<number> value) -> double // e raised to the power — Log's inverse
Math.PI -> double // 3.141592653589793 — a CONSTANT, no parentheses
Math.E -> double // 2.718281828459045Description#
The result type follows the argument, as it does in C##
There are three rules, and each is the one C#'s own overloads give:
Abs,Min,Max,Clampanswer in the WIDEST of their arguments.Math.Abs(-5)is anint;Math.Abs(-2.5)is adouble;Math.Abs(-2.50m)is adecimal, keeping its digits and its scale. So a chain of decimal arithmetic stays exact (decimal) and a chain of double arithmetic stays a double.Round,Floor,Ceiling,Truncateanswer adecimalfor a decimal, and adoubleotherwise. C# has both overloads; theintcase is genuinely ambiguous in C# (it will not compile without a cast) and resolves todoublehere.PowandSqrtare alwaysdouble, andSignis always anint— a sign is not a quantity.- The logarithms —
Log,Log10,Log2— andExpare alwaysdouble, for the same reason: C# gives them no decimal overload, and a logarithm feeds straight back into arithmetic, where one decimal operand would promote the whole expression. - The trig family —
Sin,Cos,Tan,Asin,Acos,Atan,Atan2— is alwaysdouble, whatever you pass. See below for why that one is not a free choice.
The trig family is double, and that is load-bearing#
Math.Sin and friends take an angle in radians and answer a double; the inverses (Math.Asin, Math.Acos,
Math.Atan, Math.Atan2) take a ratio and answer an angle in radians. Degrees are never implied anywhere — convert
them yourself (degrees * Math.PI / 180) if that is what you have.
They cannot answer a decimal, and the reason is arithmetic rather than taste. A trig result goes straight back into
arithmetic — dirX * cos - dirY * sin, every frame — and a single decimal operand promotes the whole expression to
decimal. A decimal has a bounded exponent, so a chain of reciprocals over promoted values fails outright with "value
was either too large or too small for a Decimal". Exactness is the right default for money and the wrong tool for a
rotation matrix.
⚠ Math.Atan2 takes (y, x), in that order — C#'s own, and the reverse of what the name suggests on first
reading. It is the one that answers "what bearing is this vector" correctly in all four quadrants, which
Math.Atan(y / x) cannot: dividing first throws away the sign information and folds two quadrants onto two others.
The two constants#
Math.PI and Math.E are constants, written without parentheses — the C# spelling. They are double, like
everything they feed.
They matter most for the trig family above, which takes radians: an angle is a multiple of π, so before these existed
a degrees-to-radians conversion had to paste the literal 3.141592653589793 at every call site. (This page told you
to do exactly that.)
The logarithms#
Math.Log(x) is the natural log (base e), matching C#. Math.Log10 and Math.Log2 are the two bases with
their own methods — prefer them to Log(x) / Log(10), which is one rounding step worse. Math.Exp is Log's
inverse, so Math.Log(Math.Exp(x)) is x to within floating-point tolerance.
⚠ Math.Log(value, newBase) takes the VALUE first — Math.Log(8, 2) is 3, not ⅓. Both arguments are bare
numbers, so a transposition produces a number rather than an error, and nothing downstream will look wrong.
Every one of these runs on the client as well as the server, and lowers into a compiled on frame body — a
log-scale axis recomputes one per tick per render, so a round trip for it would be absurd.
⚑ Comparing these in a test needs Assert.Equal(expected, actual, precision) — the third argument is the number
of decimal places both sides are rounded to. Math.Sin(Math.PI) is not exactly 0 and Math.Log(Math.Exp(2)) is not
exactly 2 in any language, because π and e are not representable; the tolerance is not sloppiness, it is the only
correct way to assert a transcendental.
⚠ This changed. These used to return a decimal whatever you passed. It was a bug rather than a policy:
the type system already declared the rules above and only the runtimes disagreed. If you were relying on
Math.Floor(someInt) handing back a decimal, it now hands back a double of the same value.
Why it mattered. A decimal operand promotes its whole expression, so a single Math.Abs inside otherwise
double arithmetic quietly turned the rest of the calculation into decimal — and a decimal has a bounded
exponent, so a chain of reciprocals eventually failed with "value was either too large or too small for a
Decimal". Exactness is what you want for money and the wrong tool for geometry.
Abs, Sign, and Truncate#
Math.Abs(x) is the magnitude; Math.Sign(x) is -1/0/1. Math.Truncate(x) drops the fraction
toward zero, so Math.Truncate(-2.9) is -2 — this is the difference from Math.Floor, which goes
toward negative infinity and gives -3. For a value already positive the two agree.
Min, Max, and Clamp#
Math.Min/Math.Max return the smaller/larger of two numbers. Math.Clamp(x, min, max) pins x into the
[min, max] range — below min it returns min, above max it returns max. If you invert the bounds so
min > max, Math.Clamp throws: there is no range to clamp into, and returning a silent wrong value
would be worse. (When two values are numerically equal but differ in scale — 1.5 vs 1.50 — which one
Min/Max returns is fixed but rarely matters; use a format specifier if the displayed scale is important.)
Pow and Sqrt are doubles, and are not exact#
Math.Pow(x, y) and Math.Sqrt(x) have no exact decimal form for most inputs — C# gives them no decimal
overload at all — so they are computed in binary floating point and stay there. Math.Sqrt(2) is
1.4142135623730951, the full float64 answer, identical in the browser and on the server.
Do not use them where you need the last cent to be provably right: a square root of a price is not a price. Convert deliberately if you need to come back to money.
These are pure functions of their arguments, so they run in the browser with no round trip where a UI action needs them (execution side).
Examples#
// Clamp a raw score into range, then keep two decimals for display.
decimal Score(decimal raw) {
return Math.Round(Math.Clamp(raw, 0m, 100m), 2);
}[Test]
void Math_answers() {
Assert.Equal(87.35m, Score(87.347m));
Assert.Equal(100m, Score(140m)); // clamped to the ceiling
Assert.Equal(0m, Score(-5m)); // clamped to the floor
Assert.Equal(5, Math.Abs(-5)); // int in, INT out — the widest argument wins
Assert.Equal(2.50m, Math.Abs(-2.50m)); // decimal in, decimal out — scale kept
Assert.Equal(1, Math.Sign(2.5m)); // Sign returns an int
Assert.Equal(0, Math.Sign(0m));
Assert.Equal(-1, Math.Sign(-0.0001m));
Assert.Equal(-2m, Math.Truncate(-2.9m)); // toward zero…
Assert.Equal(-3m, Math.Floor(-2.9m)); // …unlike Floor
Assert.Equal(3, Math.Min(3, 7));
Assert.Equal(7, Math.Max(3, 7));
Assert.Equal(3, Math.Clamp(5, 1, 3));
Assert.Equal(1024.0, Math.Pow(2, 10)); // Pow and Sqrt are DOUBLES, always
Assert.Equal(4.0, Math.Sqrt(16));
Assert.Equal(3.0, Math.Log10(1000)); // the logs are doubles too
Assert.Equal(3.0, Math.Log2(8));
Assert.Equal(3.0, Math.Log(8, 2)); // VALUE first — Log(2, 8) would be 0.333…
Assert.Equal(1.0, Math.Exp(0));
// The constants — no parentheses.
Assert.Equal(3.141592653589793, Math.PI);
Assert.Equal(2.718281828459045, Math.E);
// …and the functions, which are never EXACTLY their mathematical answer: the third argument is the number of
// decimal places both sides are rounded to before comparing.
Assert.Equal(0.0, Math.Sin(Math.PI), 12);
Assert.Equal(2.0, Math.Log(Math.Exp(2)), 12);
}Can I pass a string to Math.*?#
Math.* will coerce a string argument, and it reads it with the platform's one numeric-coercion grammar —
the same one Convert documents, including its 0 for text it cannot read. Math.Abs("-1,000") is
1000; Math.Abs("(5)") is 0, not 5. If the value came from a form field, convert it deliberately first
(Convert.ToDecimal) so the failure is visible where it happens rather than inside the arithmetic.
See also#
- decimal — what a decimal argument buys you, and where exactness matters
- format specifiers — rounding for DISPLAY (
F2,N0), distinct fromMath.Round - Convert — coercing between numeric types