Math
Numeric utilities. integer and float are distinct types in Saule and
nothing promotes between them silently, so Math is written to respect that
rather than to paper over it.
Most of it is generic over numeric flavour, written <N> below, where
N is integer or float and nothing else. The flavour of the answer
follows from what the function does:
- the ones that pick one of their arguments —
abs,min,max,clamp,fmod— answer in the flavour they were given; - the ones that compute a real number —
sqrt,pow,log,expand the whole trigonometry family — always answerfloat; - the rounding functions always answer
integer.
-- integer in, integer out: no cast either way.local hp: integer = Math.clamp(137, 0, 100)println(hp) -- 100
-- A real number is always a float, even from integer arguments.local root: float = Math.sqrt(9)println(root) -- 3.0Flavour still cannot be mixed inside one call. Math.max(1, 2.5) is a
type error for the same reason 1 + 2.5 is — cast one side with as float
first.
Constants
Section titled “Constants”| Name | Type | Value |
|---|---|---|
Math.pi |
float |
π |
Math.e |
float |
Euler’s number |
Math.huge |
float |
positive infinity |
Math.maxinteger |
integer |
the largest integer, 2⁶³−1 |
Math.mininteger |
integer |
the smallest integer, −2⁶³ |
println(Math.pi) -- 3.141592653589793println(Math.maxinteger) -- 9223372036854775807
-- `huge` is what a comparison against "no limit yet" starts from.local best: float = Math.hugefor score in {4.5, 2.0, 9.0} do if score < best then best = score endendprintln(best) -- 2.0
-- Integer arithmetic wraps at the ends of the range rather than trapping.println(Math.maxinteger + 1 == Math.mininteger) -- trueRounding
Section titled “Rounding”Math.floor<N>(n: N) -> integer
Rounds toward negative infinity. An integer comes back unchanged.
println(Math.floor(2.9)) -- 2println(Math.floor(2.1)) -- 2println(Math.floor(-2.1)) -- -3, toward -inf, not toward zeroprintln(Math.floor(5)) -- 5, already an integerMath.ceil<N>(n: N) -> integer
Rounds toward positive infinity.
println(Math.ceil(2.1)) -- 3println(Math.ceil(-2.9)) -- -2println(Math.ceil(5)) -- 5
-- How many pages of ten a list needs.local items: integer = 23println(Math.ceil((items as float) / 10.0)) -- 3Math.round<N>(n: N) -> integer
Rounds to the nearest integer, with a half rounding away from zero —
2.5 goes to 3 and -2.5 to -3. This is not banker’s rounding: a .5
never rounds toward even.
println(Math.round(2.4)) -- 2println(Math.round(2.5)) -- 3, away from zeroprintln(Math.round(-2.5)) -- -3println(Math.round(3.5)) -- 4Math.sign<N>(n: N) -> integer
-1, 0 or 1. Always an integer, whatever flavour went in.
println(Math.sign(-8)) -- -1println(Math.sign(0)) -- 0println(Math.sign(2.5)) -- 1
-- Stepping toward a target without branching on direction.local current: integer = 3local target: integer = 10println(current + Math.sign(target - current)) -- 4Numeric kind
Section titled “Numeric kind”Math.type(v: any) -> string?
"integer" or "float" for a number, nil for anything else. The way to
ask which flavour a value that arrived through any actually has.
println(Math.type(1) ?? "?") -- integerprintln(Math.type(1.0) ?? "?") -- floatprintln(Math.type("1") == nil) -- true, not a number
-- Which flavour arrived.local value: any = 2.5if (Math.type(value) ?? "") == "float" then println("a float: " .. ((value as float) ?? 0.0)) -- a float: 2.5endArithmetic
Section titled “Arithmetic”Math.abs<N>(n: N) -> N
Magnitude, in the flavour it was given.
println(Math.abs(-8)) -- 8, an integerprintln(Math.abs(-2.5)) -- 2.5, a floatprintln(Math.abs(0)) -- 0
-- Distance between two numbers, direction discarded.println(Math.abs(3 - 10)) -- 7Math.min<N>(...N) -> N
The smallest of its arguments, which must all share one flavour.
println(Math.min(3, 1, 7)) -- 1println(Math.min(2.5, 1.5)) -- 1.5println(Math.min(5)) -- 5, one argument is fineMath.max<N>(...N) -> N
The largest of its arguments.
println(Math.max(3, 1, 7)) -- 7println(Math.max(2.5, 1.5)) -- 2.5
-- A floor under a computed value, without an `if`.local damage: integer = 2println(Math.max(damage - 5, 0)) -- 0Math.clamp<N>(n: N, lo: N, hi: N) -> N
Pins n into the inclusive range [lo, hi].
println(Math.clamp(137, 0, 100)) -- 100println(Math.clamp(-5, 0, 100)) -- 0println(Math.clamp(42, 0, 100)) -- 42, already inside
-- Keeping a value in range as it changes.local health: integer = 95health = Math.clamp(health + 20, 0, 100)println(health) -- 100Math.fmod<N>(a: N, b: N) -> N
The remainder after truncated division, in the flavour it was given. The
sign follows the left operand, which is also what the % operator does —
unlike Lua, where % floors and takes the sign of the right one.
println(Math.fmod(7, 3)) -- 1println(Math.fmod(-7, 3)) -- -1, the sign of the left operandprintln(-7 % 3) -- -1, the operator agreesprintln(Math.fmod(7.5, 2.0)) -- 1.5Math.modf<N>(n: N) -> table<float>
Splits a number into its integer and fractional parts, as a two-element table of floats.
local parts: table<float> = Math.modf(2.75)println(parts[1] ?? 0.0) -- 2.0, the integer partprintln(parts[2] ?? 0.0) -- 0.75, the fraction
-- Both parts carry the sign.local neg: table<float> = Math.modf(-2.75)println(neg[1] ?? 0.0) -- -2.0Math.pow<N>(base: N, exp: N) -> float
base raised to the power exp. Always a float, which is what lets it
answer a negative exponent — 2 ^ -1 is an error on integers, while
Math.pow(2, -1) is 0.5.
println(Math.pow(2, 10)) -- 1024.0, a float even from integersprintln(Math.pow(2, -1)) -- 0.5, where `2 ^ -1` is an errorprintln(Math.pow(9.0, 0.5)) -- 3.0, both arguments share one flavourMath.sqrt<N>(n: N) -> float
The square root.
println(Math.sqrt(9)) -- 3.0println(Math.sqrt(2)) -- 1.4142135623730951
-- The hypotenuse of a right triangle.local a: float = 3.0local b: float = 4.0println(Math.sqrt(a * a + b * b)) -- 5.0Math.exp<N>(n: N) -> float
e raised to the power n.
println(Math.exp(0)) -- 1.0println(Math.exp(1)) -- 2.718281828459045println(Math.exp(1) == Math.e) -- trueMath.log<N>(n: N, base: N?) -> float
The natural logarithm, or the logarithm in base when one is given.
println(Math.log(Math.e)) -- 1.0, natural by defaultprintln(Math.log(8, 2)) -- 3.0, base 2
-- Floating point: a logarithm that "should" be exact often is not, so-- compare with a tolerance rather than with `==`.println(Math.log(1000, 10)) -- 2.9999999999999996
-- How many bits a value needs.println(Math.ceil(Math.log(300, 2))) -- 9Trigonometry
Section titled “Trigonometry”All of these take an integer or a float and answer a float — an angle
is a real number, so there is no integer-in / integer-out form the way there
is for abs or clamp.
Math.sin<N>(n: N) -> float
Sine of n radians.
println(Math.sin(0)) -- 0.0println(Math.sin(Math.pi / 2.0)) -- 1.0
-- Degrees go through `rad` first.println(Math.sin(Math.rad(90))) -- 1.0Math.cos<N>(n: N) -> float
Cosine of n radians.
println(Math.cos(0)) -- 1.0println(Math.cos(Math.pi)) -- -1.0
-- A point on the unit circle, a third of the way round.local angle: float = Math.rad(120)println(Math.round(Math.cos(angle) * 100.0)) -- -50Math.tan<N>(n: N) -> float
Tangent of n radians.
println(Math.tan(0)) -- 0.0println(Math.round(Math.tan(Math.rad(45)))) -- 1Math.asin<N>(n: N) -> float
Arcsine, in radians. n outside [-1, 1] has no answer and gives nan.
println(Math.asin(0)) -- 0.0println(Math.asin(1) == Math.pi / 2.0) -- trueMath.acos<N>(n: N) -> float
Arccosine, in radians.
println(Math.acos(1)) -- 0.0println(Math.acos(0) == Math.pi / 2.0) -- trueMath.atan<N>(y: N, x: N?) -> float
Arctangent, in radians. With one argument it is the arctangent of y; with
two it is the atan2 form — the angle to the point (x, y), with the
quadrant resolved correctly, which a single-argument arctangent cannot do.
println(Math.atan(0)) -- 0.0println(Math.atan(1, 1) == Math.pi / 4.0) -- true, 45°
-- The quadrant is what the two-argument form is for.println(Math.round(Math.deg(Math.atan(1, -1)))) -- 135println(Math.round(Math.deg(Math.atan(-1, -1)))) -- -135Math.deg<N>(radians: N) -> float
Radians to degrees.
println(Math.deg(Math.pi)) -- 180.0println(Math.deg(Math.pi / 2.0)) -- 90.0Math.rad<N>(degrees: N) -> float
Degrees to radians.
println(Math.rad(180)) -- 3.141592653589793println(Math.rad(180) == Math.pi) -- true
-- Round tripping.println(Math.deg(Math.rad(45))) -- 45.0Randomness
Section titled “Randomness”Math.random() -> float
A uniform float in [0, 1) — zero is possible, one is not.
Math.randomseed(42)local r: float = Math.random()println(r >= 0.0 and r < 1.0) -- trueMath.random(n: integer) -> integer
A uniform integer in [1, n], both ends included.
Math.randomseed(42)local roll: integer = Math.random(6)println(roll >= 1 and roll <= 6) -- trueMath.random(lo: integer, hi: integer) -> integer
A uniform integer in [lo, hi], both ends included.
Math.randomseed(42)local n: integer = Math.random(10, 20)println(n >= 10 and n <= 20) -- true
-- Picking an element out of a table.local names: table<string> = {"ada", "grace", "alan"}println(names[Math.random(1, #names)] != nil) -- trueMath.randomseed(seed: integer) -> nil
Resets the generator, so a sequence of Math.random calls repeats exactly.
This is what makes a test or a procedurally generated world reproducible.
Math.randomseed(42)local first: integer = Math.random(1000)
Math.randomseed(42)local again: integer = Math.random(1000)
println(first == again) -- true, the same seed gives the same sequenceUnsigned comparison
Section titled “Unsigned comparison”Math.ult(a: integer, b: integer) -> boolean
Compares two integers as unsigned 64-bit values. A negative integer has its top bit set, so it reads as a very large unsigned number.
println(Math.ult(1, 2)) -- trueprintln(Math.ult(-1, 1)) -- false: -1 is the largest unsigned valueprintln(1 < 2) -- true, the signed comparison agrees hereprintln(-1 < 1) -- true, where the signed one differs