Math (std::math)
std::math brings together the essential numeric operations — rounding, trigonometry,
interpolation, random-number generation, constants and arbitrary-precision decimal
types. Import with use std::math.
Directed rounding
math.floor rounds toward −∞; math.ceil rounds toward +∞.
An exact integer has floor == ceil.
pages(total, per_page) uses ceil to calculate the required number of pages.
// Feature: math.floor / math.ceil — directed rounding
// When to use: round down/up to the nearest integer.
use std::math
// floor — rounds down (toward -inf).
print(math.floor(3.2)) // expected: 3
print(math.floor(3.9)) // expected: 3
print(math.floor(-1.2)) // expected: -2
// ceil — rounds up (toward +inf).
print(math.ceil(3.1)) // expected: 4
print(math.ceil(3.0)) // expected: 3
print(math.ceil(-1.7)) // expected: -1
// Exact integer — floor == ceil.
print(math.floor(5.0)) // expected: 5
print(math.ceil(5.0)) // expected: 5
// Application: split N items across fixed-size pages.
fn pages(total: int, per_page: int) -> int {
return math.ceil(total / (per_page * 1.0))
}
print(pages(100, 10)) // expected: 10
print(pages(101, 10)) // expected: 11
print(pages(1, 10)) // expected: 1
Square root and exponentiation
math.sqrt and math.pow cover most geometric formulas.
2-D Euclidean distance built with sqrt and direct multiplication.
// Feature: math.sqrt / math.pow — square root and exponentiation
// When to use: distances, vector magnitudes, math formulas.
use std::math
print(math.sqrt(9.0)) // expected: 3.0
print(math.sqrt(2.0)) // ~1.4142...
print(math.sqrt(0.0)) // expected: 0.0
// pow — base ^ exp.
print(math.pow(2.0, 10.0)) // expected: 1024.0
print(math.pow(3.0, 2.0)) // expected: 9.0
print(math.pow(10.0, 0.0)) // expected: 1.0
// 2D Euclidean distance.
fn dist(x1: float, y1: float, x2: float, y2: float) -> float {
let dx = x2 - x1
let dy = y2 - y1
return math.sqrt(dx * dx + dy * dy)
}
print(dist(0.0, 0.0, 3.0, 4.0)) // expected: 5.0
print(dist(1.0, 1.0, 4.0, 5.0)) // expected: 5.0
Trigonometry
Trigonometric functions operate in radians. Use math.rad and math.deg
to convert between degrees and radians.
Notable values of sin/cos/tan; built-in degree converter.
// Feature: math.sin / math.cos / math.tan — trigonometry (radians)
// When to use: rotations, animations, geometry, physics.
use std::math
const PI = 3.14159265358979
// Notable values.
print("{math.sin(0.0):.2f}") // expected: 0.00
print("{math.cos(0.0):.2f}") // expected: 1.00
print("{math.sin(PI / 2.0):.2f}") // expected: 1.00
print("{math.cos(PI):.2f}") // expected: -1.00
// Degrees -> radians conversion.
fn rad(deg: float) -> float {
return deg * (PI / 180.0)
}
// 30 degrees.
print("{math.sin(rad(30.0)):.2f}") // expected: 0.50
print("{math.cos(rad(60.0)):.2f}") // expected: 0.50
// math.deg / math.rad — built-in converters.
print("{math.rad(180.0):.2f}") // expected: 3.14
print("{math.deg(PI):.0f}") // expected: 180
// tan(45 degrees) ~ 1.
print("{math.tan(rad(45.0)):.2f}") // expected: 1.00
Random numbers
math.randomseed fixes the seed for deterministic results — useful in tests.
math.random() returns a float in [0, 1); math.random(N) returns an integer in [1, N].
Fixed seed + random element selection from an array.
// Feature: math.random / math.randomseed — pseudo-random numbers
// When to use: pick elements, simple simulations, games.
use std::math
// A fixed seed makes output deterministic — useful for tests.
math.randomseed(42)
// random() with no args — float in [0.0, 1.0).
let x = math.random()
print(x >= 0.0 && x < 1.0) // expected: true
let y = math.random()
print(y >= 0.0 && y < 1.0) // expected: true
// random(N) — integer in [1, N].
let die = math.random(6)
print(die >= 1 && die <= 6) // expected: true
// random(lo, hi) — integer in [lo, hi].
let between = math.random(10, 20)
print(between >= 10 && between <= 20)
// expected: true
// Pick a random element from an array.
let colors = ["red", "green", "blue", "yellow"]
let i = math.random(1, colors.len()) - 1 // 0..len-1
let chosen = colors[i]
print(chosen.len() > 0) // expected: true
Absolute value, minimum and maximum
abs, min, max; manual clamp composed with min(max(v, lo), hi).
// Feature: math.abs / math.min / math.max — numeric utilities
// When to use: scalar distance, clamp, comparisons.
use std::math
// abs — absolute value.
print(math.abs(5)) // expected: 5
print(math.abs(-5)) // expected: 5
print(math.abs(-3.14)) // expected: 3.14
print(math.abs(0)) // expected: 0
// min / max — between two values.
print(math.min(3, 7)) // expected: 3
print(math.max(3, 7)) // expected: 7
print(math.min(-1, -5)) // expected: -5
print(math.max(-1, -5)) // expected: -1
// Pattern: manual clamp.
fn clamp(v: int, lo: int, hi: int) -> int {
return math.min(math.max(v, lo), hi)
}
print(clamp(15, 0, 10)) // expected: 10
print(clamp(-3, 0, 10)) // expected: 0
print(clamp(5, 0, 10)) // expected: 5
// Absolute difference.
fn diff(a: int, b: int) -> int {
return math.abs(a - b)
}
print(diff(10, 3)) // expected: 7
print(diff(3, 10)) // expected: 7
Interpolation and smoothing
math.mix / math.lerp interpolate linearly; math.remap converts between
ranges; math.step is a binary threshold; math.smoothstep smooths the transition.
clamp, mix/lerp, inverse_lerp, remap, step, smoothstep — animation and shader primitives.
// Feature: math.clamp / mix / step / smoothstep / remap — blending helpers
// When to use: animation, easing, color blending, creative coding,
// graphics — wherever you'd reach for GLSL's interpolation primitives.
use std::math
// clamp — limit a value to [lo, hi].
print(math.clamp(15, 0, 10)) // expected: 10
print(math.clamp(-5, 0, 10)) // expected: 0
print(math.clamp(5, 0, 10)) // expected: 5
// mix / lerp — linear interpolation.
// `mix(a, b, t)` = a + (b - a) * t. Both names are aliases.
print(math.mix(0.0, 100.0, 0.5)) // expected: 50
print(math.lerp(0.0, 100.0, 0.25)) // expected: 25
print(math.mix(0.0, 100.0, 1.5)) // expected: 150 (extrapolates — does NOT clamp t)
// inverse_lerp — given v in [a, b], return t such that mix(a, b, t) == v.
print(math.inverse_lerp(0.0, 100.0, 50.0)) // expected: 0.5
print(math.inverse_lerp(0.0, 100.0, 25.0)) // expected: 0.25
print(math.inverse_lerp(10.0, 20.0, 25.0)) // expected: 1.5 (extrapolates)
// remap — convert a value from one range to another.
// remap(0.5, 0, 1, 0, 100) ≡ 50
print(math.remap(0.5, 0.0, 1.0, 0.0, 100.0)) // expected: 50
print(math.remap(75.0, 0.0, 100.0, -1.0, 1.0)) // expected: 0.5
// step — hard threshold: 0 if x < edge, 1 otherwise.
print(math.step(0.5, 0.3)) // expected: 0
print(math.step(0.5, 0.5)) // expected: 1
print(math.step(0.5, 0.7)) // expected: 1
// smoothstep — Hermite smooth interpolation. Like step, but smooth.
// Useful for soft transitions, antialiasing, eased fades.
print(math.smoothstep(0.0, 1.0, -0.5)) // expected: 0 (below edge0)
print(math.smoothstep(0.0, 1.0, 0.5)) // expected: 0.5 (center)
print(math.smoothstep(0.0, 1.0, 1.5)) // expected: 1 (above edge1)
Sign, fraction, truncation and powers of two
math.sign, math.fract and math.trunc follow GLSL semantics.
math.exp2/math.log2 are useful in audio and mip-maps; math.is_power_of_two
and math.next_power_of_two help with circular-buffer sizing.
fract(-0.3) returns 0.7 (GLSL semantics, not C); next_power_of_two(63) returns 64.
// Feature: math.sign / fract / trunc / exp2 / log2 / power-of-two helpers
// When to use: signal processing, packing, hash table sizing, fast
// power-of-two arithmetic, GLSL-style numeric primitives.
use std::math
// sign — returns -1, 0, or 1. Matches GLSL: sign(0) == 0.
print(math.sign(-5.0)) // expected: -1
print(math.sign(0.0)) // expected: 0
print(math.sign(3.0)) // expected: 1
// fract — fractional part, always in [0, 1). Differs from `x % 1`
// for negatives: GLSL semantics, not C semantics.
print(math.fract(3.7)) // expected: 0.7
print(math.fract(-0.3)) // expected: 0.7 (not -0.3!)
print(math.fract(2.0)) // expected: 0
// trunc — drop fractional part toward zero. Differs from floor for
// negatives.
print(math.trunc(3.7)) // expected: 3
print(math.trunc(-3.7)) // expected: -3 (floor(-3.7) would be -4)
print(math.trunc(2.0)) // expected: 2
// exp2 / log2 — base-2 power and logarithm. Useful for octave-based
// audio, dB conversions, mip-map levels.
print(math.exp2(3.0)) // expected: 8
print(math.log2(1024.0)) // expected: 10
print(math.log2(0.5)) // expected: -1
// is_power_of_two — handy for asserting hash table sizes, ring buffers.
print(math.is_power_of_two(64)) // expected: true
print(math.is_power_of_two(63)) // expected: false
print(math.is_power_of_two(1)) // expected: true
print(math.is_power_of_two(0)) // expected: false (0 is NOT a power of two)
// next_power_of_two — smallest 2^k >= n. Saturates rather than
// overflowing — `next_power_of_two(1e100)` returns `Inf`, not garbage.
print(math.next_power_of_two(63)) // expected: 64
print(math.next_power_of_two(64)) // expected: 64
print(math.next_power_of_two(65)) // expected: 128
print(math.next_power_of_two(1)) // expected: 1
Constants
math.pi, math.tau, math.e, math.phi, math.epsilon, math.infinity and
math.nan avoid hardcoding. math.tau (= 2π) is preferred for full-turn angles.
Using math.tau for turn fractions; math.epsilon for manual tolerance.
// Feature: math constants — pi, tau, e, phi, epsilon, infinity, nan
// When to use: any code that needs these named values without
// hardcoding them. Tau (2π) is preferred for full-turn angles;
// epsilon for "is this close enough" comparisons; infinity/nan for
// sentinel checks.
use std::math
// Already-existing: pi.
print(math.pi) // expected: 3.14...
// New constants:
print(math.tau) // expected: 6.28... (= 2 * pi)
print(math.e) // expected: 2.718...
print(math.phi) // expected: 1.618... (golden ratio)
print(math.epsilon) // expected: 2.22e-16 (f64 machine epsilon)
print(math.infinity) // expected: inf
print(math.nan) // expected: nan
// Tau is sometimes more natural than pi for full rotations:
// full circle = 1 * tau = tau radians.
// half circle = 0.5 * tau = pi radians.
fn full_turn(fraction: float) -> float {
return math.tau * fraction
}
print(full_turn(0.5)) // expected: 3.14... (half turn)
print(full_turn(0.25)) // expected: 1.57... (quarter turn)
// Epsilon for approximate equality (sketch — proper FP comparison is
// trickier; this works for values near 1.0).
fn approx_eq(a: float, b: float) -> bool {
return math.abs(a - b) < math.epsilon * 16.0
}
print(approx_eq(0.1 + 0.2, 0.3)) // expected: true (despite FP noise)
Approximate float comparison — the ~= operator
Comparing floats directly with == triggers the float-equality warning because
0.1 + 0.2 ≠ 0.3 in IEEE 754. The ~= operator uses adaptive tolerance by
default; the within tol, within tol relative and within n ulps variants
give precise control.
Plain ~=, absolute tolerance, relative tolerance and within; math.is_nan and math.is_infinite; safe_divide.
// Feature: approximate float equality via ~= operator or math.approx_eq* functions.
// When to use: any comparison between floats produced by arithmetic.
// Direct `==` triggers the `float-equality` lint warning because
// `0.1 + 0.2 ≠ 0.3` in IEEE 754. The ~= operator solves that for default tolerance.
// Default adaptive tolerance — combines absolute (near zero) and
// relative (large magnitude). Good for ~80% of cases.
use std::math
print(0.1 + 0.2 ~= 0.3) // expected: true
print(1.0 + 0.1 - 0.1 ~= 1.0) // expected: true (FP rearrangement noise)
print(1.0 ~= 2.0) // expected: false
// Explicit absolute tolerance — for near-zero checks or known scale.
fn velocity_sample() -> float {
return 0.0000001
}
print(velocity_sample() ~= 0.0 within 1e-6) // expected: true
// Explicit relative tolerance — for values that span orders of
// magnitude. `0.001` ≈ 0.1% relative tolerance.
print(1e10 ~= 1.0000001e10 within 0.001 relative) // expected: true
print(1.0 ~= 1.5 within 0.001 relative) // expected: false
// NaN semantics — `~=` returns false for any NaN comparison.
// Use `math.is_nan` to check explicitly.
let nan = 0.0 / 0.0
print(nan ~= nan) // expected: false
print(math.is_nan(nan)) // expected: true
// Infinity semantics — same sign approx-equal, opposite signs not.
print(math.huge ~= math.huge) // expected: true
print(math.huge ~= -math.huge) // expected: false
print(math.is_infinite(math.huge)) // expected: true
// Typical usage in a guard clause:
fn safe_divide(a: float, b: float) -> float? {
if b ~= 0.0 within 1e-12 { return nil }
return a / b
}
print(safe_divide(10.0, 2.0)) // expected: 5
print(safe_divide(10.0, 0.0)) // expected: nil
The complementary operator !~= inverts the result — ideal for guards and
convergence loops:
Newton loop with !~= to detect non-convergence; assert_close function.
// Feature: `!~=` operator — approximate "not equal" between floats.
// When to use: guard clauses where you want to *act* when two floats
// have drifted apart, or assertion failures when convergence didn't happen.
//
// Semantically: `a !~= b` is exactly `not (a ~= b)`. Same tolerance rules.
// Use `!~=` for direct readability — `if x !~= y { ... }` reads better
// than `if not (x ~= y) { ... }` in conditionals.
use std::math
// Bare form (default adaptive tolerance)
print(1.0 !~= 2.0) // expected: true (clearly different)
print(1.0 !~= 1.0) // expected: false (exactly equal)
print((0.1 + 0.2) !~= 0.3) // expected: false (FP noise still within default)
// With absolute tolerance
print(1.0 !~= 1.0001 within 1e-9) // expected: true (diff exceeds 1e-9)
print(1.0 !~= 1.0001 within 0.001) // expected: false (within tolerance, so NOT not-eq)
// With relative tolerance
print(100.0 !~= 101.0 within 0.001 relative) // expected: true (1% > 0.1% rtol)
print(100.0 !~= 100.05 within 0.001 relative) // expected: false (0.05% < 0.1% rtol)
// Use case 1: detect when iterative process hasn't converged yet
var x = 1.0
var prev = 0.0
var iter = 0
while prev !~= x within 1e-10 {
prev = x
x = (x + 2.0 / x) * 0.5 // Newton step toward sqrt(2)
iter = iter + 1
}
print("sqrt(2) ≈ {x} after {iter} iterations") // expected: ≈ 1.4142... after a few iters
// Use case 2: assert convergence in a test/sanity check
fn assert_close(a: float, b: float, label: str) {
if a !~= b within 1e-9 {
print("FAIL {label}: {a} differs from {b}")
} else {
print("OK {label}")
}
}
assert_close(0.1 + 0.2, 0.3, "fp_noise") // expected: OK fp_noise
assert_close(math.sqrt(2.0) * math.sqrt(2.0), 2.0, "sqrt_roundtrip") // expected: OK
When the comparison needs to be passed as a first-class value (e.g. to
filter), use the equivalent functions:
math.approx_eq, math.approx_eq_abs, math.approx_eq_rel as arguments to filter.
// Feature: `math.approx_eq*` function family — the function-call form
// of approximate float equality, parallel to the `~=` operator.
//
// When to use functions instead of the operator:
// - **Higher-order usage**: pass the function as a value to map/filter/reduce.
// - **Explicit naming in code review**: `math.approx_eq_rel(...)` makes the
// tolerance mode obvious at the call site.
// - **Compatibility / migration**: code coming from Python's `math.isclose`
// or Julia's `isapprox` feels familiar.
//
// The operator (`~=`) is preferred for inline conditional checks. The functions
// are preferred when the comparison is a "first-class" operation in your code.
use std::math
use std::Array
// Equivalence: every operator form has a function form.
print((0.1 + 0.2) ~= 0.3) // operator
print(math.approx_eq(0.1 + 0.2, 0.3)) // function — same result
print(1.0 ~= 1.0001 within 0.001) // operator
print(math.approx_eq_abs(1.0, 1.0001, 0.001)) // function — same result
print(100.0 ~= 100.05 within 0.001 relative) // operator
print(math.approx_eq_rel(100.0, 100.05, 0.001)) // function — same result
// First-class usage: filter a list using the function form
let measurements = [0.301, 0.299, 0.5, 0.30000001, 0.7]
let near_target = measurements.filter(|m| math.approx_eq_abs(m, 0.3, 0.01))
print(near_target) // expected: [0.301, 0.299, 0.30000001]
// Reduce / find: detect the first sample that matches a target within tolerance
fn first_matching(samples: [float], target: float, tol: float) -> float? {
for s in samples {
if math.approx_eq_abs(s, target, tol) { return s }
}
return nil
}
print(first_matching([1.0, 1.5, 1.999, 2.0001, 3.0], 2.0, 0.01)) // expected: 1.999
// Side-by-side semantic note: NaN behavior is identical between operator and function.
let nan = 0.0 / 0.0
print(nan ~= nan) // false
print(math.approx_eq(nan, nan)) // false
print(math.is_nan(nan)) // true (use this for NaN checks)
Special float predicates — NaN, infinite, finite
Never test x == x for NaN or x == math.huge for infinity — use the
dedicated predicates:
is_nan, is_infinite, is_finite; defensive pattern in area_of_circle.
// Feature: float predicates — `math.is_nan`, `math.is_finite`, `math.is_infinite`.
// When to use: classify a float without falling into the IEEE 754 traps:
// - `x == x` is `false` when `x` is NaN — that's the only reliable NaN test.
// - `x == math.huge` works for +infinity but you'd miss -infinity.
// - `x !~= y within tol` doesn't tell you if `x` is special (NaN/Inf).
//
// These predicates encapsulate the right check for each case.
use std::math
// ── NaN detection ───────────────────────────────────────────────────
// NaN is produced by indeterminate operations: 0/0, sqrt(-1), inf - inf.
let nan_from_div = 0.0 / 0.0
let nan_from_sqrt = math.sqrt(-1.0)
let nan_from_inf = math.huge - math.huge
print(math.is_nan(nan_from_div)) // expected: true
print(math.is_nan(nan_from_sqrt)) // expected: true
print(math.is_nan(nan_from_inf)) // expected: true
print(math.is_nan(42.0)) // expected: false
print(math.is_nan(math.huge)) // expected: false (infinity is NOT NaN)
// NaN propagates through every arithmetic operation:
print(math.is_nan(nan_from_div + 1.0)) // expected: true
print(math.is_nan(nan_from_div * 0.0)) // expected: true
// ── Infinity detection ──────────────────────────────────────────────
// `math.huge` is +infinity. `-math.huge` is -infinity.
// `math.is_infinite` matches both signs; `math.is_inf` is an alias kept
// for backward compatibility (introduced before the canonical name).
print(math.is_infinite(math.huge)) // expected: true
print(math.is_infinite(-math.huge)) // expected: true
print(math.is_inf(math.huge)) // expected: true (alias)
print(math.is_infinite(42.0)) // expected: false
print(math.is_infinite(nan_from_div)) // expected: false (NaN is not Inf)
// Operations that produce infinity:
print(math.is_infinite(1.0 / 0.0)) // expected: true (+inf)
print(math.is_infinite(-1.0 / 0.0)) // expected: true (-inf)
// ── Finite detection (the "everything is well-behaved" check) ───────
// `is_finite(x)` ≡ `not is_nan(x) and not is_infinite(x)`.
print(math.is_finite(42.0)) // expected: true
print(math.is_finite(0.0)) // expected: true
print(math.is_finite(-1e308)) // expected: true (large but representable)
print(math.is_finite(math.huge)) // expected: false
print(math.is_finite(nan_from_div)) // expected: false
// ── Typical defensive-coding pattern ────────────────────────────────
// A function that should never receive special values: validate up front.
fn area_of_circle(radius: float) -> float? {
if !math.is_finite(radius) { return nil } // reject NaN, ±inf
if radius < 0.0 { return nil }
return math.pi * radius * radius
}
print(area_of_circle(2.0)) // expected: 12.566...
print(area_of_circle(-1.0)) // expected: nil
print(area_of_circle(math.huge)) // expected: nil (would overflow)
print(area_of_circle(0.0 / 0.0)) // expected: nil (NaN input)
// ── Constants for direct comparison/return ──────────────────────────
// `math.nan` and `math.huge` are the constants. They're often useful
// as sentinel return values or "unset" markers in numeric APIs.
let unset: float = math.nan
print(math.is_nan(unset)) // expected: true (sentinel detected)
ULP comparison
For testing math implementations (sqrt, sin…) where the exact bit-distance matters:
within n ulps — strictest mode; 0.0 ~= -0.0 within 0 ulps returns true (IEEE 754).
// Feature: `~= within <n> ulps` — ULP-based approximate equality.
// When to use: when testing implementations of math operations
// (sqrt, sin, etc.) where you want bit-exact-modulo-N-ULPs comparison.
// "ULP" = "units in the last place" = bit-pattern distance for same-sign
// floats. This is the strictest of the three tolerance modes:
// - within tol (absolute: |a - b| <= tol)
// - within tol relative (relative: |a - b| <= tol * max(|a|, |b|))
// - within n ulps (bit-pattern: differ by at most n in their f64 bits)
use std::math
// ── Exact match: differs by 0 ULPs ──────────────────────────────────
print(1.0 ~= 1.0 within 0 ulps) // expected: true
print(2.5 ~= 2.5 within 0 ulps) // expected: true
// ── 1 ULP away ──────────────────────────────────────────────────────
// (0.1 + 0.2) is the classic FP noise case. The result differs from
// 0.3 by 1 ULP (~5.55e-17 at that magnitude). `within 0 ulps` fails,
// `within 1 ulps` (or more) succeeds.
print((0.1 + 0.2) ~= 0.3 within 0 ulps) // expected: false (NOT bit-exact)
print((0.1 + 0.2) ~= 0.3 within 1 ulps) // expected: true
print((0.1 + 0.2) ~= 0.3 within 4 ulps) // expected: true (4 ulps is plenty)
// ── Distinctly different values ─────────────────────────────────────
print(1.0 ~= 2.0 within 4 ulps) // expected: false
print(1.0 ~= 1.001 within 100 ulps) // expected: false (1.0 vs 1.001 is ~4.5e12 ULPs away)
print(1.0 ~= 1.0 + math.epsilon within 1 ulps) // expected: true (1 ULP step at magnitude 1.0)
// ── NaN: always false ───────────────────────────────────────────────
let nan = 0.0 / 0.0
print(nan ~= nan within 1000 ulps) // expected: false
print(nan ~= 1.0 within 1000 ulps) // expected: false
// ── Opposite signs: never within ULPs ───────────────────────────────
// Bit patterns of negative floats are far from positive ones in u64 space,
// so opposite signs never match. (Exception: -0.0 == 0.0 is caught by the
// early `a == b` short-circuit.)
print(1.0 ~= -1.0 within 1000 ulps) // expected: false
print(0.0 ~= -0.0 within 0 ulps) // expected: true (0 and -0 are equal in IEEE 754)
// ── Negation (!~=) ──────────────────────────────────────────────────
print(1.0 !~= 2.0 within 4 ulps) // expected: true (distinctly unequal)
print((0.1 + 0.2) !~= 0.3 within 4 ulps) // expected: false (within tol → NOT not-eq)
// ── Comparison vs other tolerance modes ─────────────────────────────
// The three modes can give different verdicts on the same comparison.
// Example: 0.1 + 0.2 vs 0.3 (diff ≈ 5.55e-17 ≈ 1 ULP near 0.3):
print((0.1 + 0.2) ~= 0.3 within 1e-10) // true (very loose absolute)
print((0.1 + 0.2) ~= 0.3 within 1e-18) // false (tighter than the diff)
print((0.1 + 0.2) ~= 0.3 within 0.001 relative) // true (very loose relative)
print((0.1 + 0.2) ~= 0.3 within 1 ulps) // true (bit-exact 1 ULP)
print((0.1 + 0.2) ~= 0.3 within 0 ulps) // false (strictest)
// ── Use case: sqrt round-trip accuracy test ─────────────────────────
// sqrt(2)^2 should equal 2.0 to within a handful of ULPs.
let two = math.sqrt(2.0) * math.sqrt(2.0)
print(two ~= 2.0 within 4 ulps) // expected: true
print(two ~= 2.0 within 0 ulps) // expected: probably false
// (the round-trip introduces tiny error)
decimal type — exact decimal arithmetic (~28 digits)
decimal (suffix d) is backed by rust_decimal. Exact arithmetic for
money and accounting — no binary floating-point noise.
0.1d + 0.2d == 0.3d returns true; conversions with as; invoice total with decimal.
// Feature: `decimal` primitive type — exact decimal arithmetic.
// When to use: any context where binary float precision is wrong:
// - money / financial calculations
// - bookkeeping
// - any code where `0.1 + 0.2 == 0.3` matters
//
// Decimal is backed by `rust_decimal` (~96-bit fixed-point). Arithmetic
// is EXACT for representable decimal values (up to ~28 significant digits).
// ── Literals: `d` suffix ────────────────────────────────────────────
let a: decimal = 0.1d
let b: decimal = 0.2d
print(a + b) // expected: "0.3" (NOT 0.30000000000000004 like float!)
print(a + b == 0.3d) // expected: true (no float-equality lint warning)
// Integer-form decimal literal
let price: decimal = 100d
print(price) // expected: "100"
// ── Arithmetic operators ────────────────────────────────────────────
let x: decimal = 2.5d
let y: decimal = 1.5d
print(x + y) // expected: "4.0"
print(x - y) // expected: "1.0"
print(x * y) // expected: "3.75"
print(x / y) // expected: "1.6666..." (default precision)
// ── Comparison operators (exact, no ~= needed) ──────────────────────
print(0.1d + 0.2d == 0.3d) // expected: true (EXACT)
print(0.1d + 0.2d != 0.3d) // expected: false
print(0.5d < 1.0d) // expected: true
print(0.5d <= 0.5d) // expected: true
print(1.0d > 0.5d) // expected: true
// ── Conversions via `as` ────────────────────────────────────────────
// Decimal → float: precision may be lost (decimal has more digits than f64)
let from_d: float = 0.3d as float
print(from_d) // expected: 0.3
// Float → decimal: REVEALS the binary FP noise
// (0.1 in f64 isn't actually 0.1 — it's 0.10000000000000000555...)
let from_f: decimal = 0.1 as decimal
print(from_f) // expected: 0.1000000000000000055511151231
// Int → decimal: always exact
let from_i: decimal = 42 as decimal
print(from_i) // expected: "42"
// Decimal → int: truncates (no rounding)
let to_i: int = 99.9d as int
print(to_i) // expected: 99 (truncated, not rounded)
// ── Real-world example: simple invoice total ────────────────────────
fn invoice_total(items: [decimal], tax_rate: decimal) -> decimal {
var subtotal: decimal = 0d
for item in items {
subtotal = subtotal + item
}
return subtotal + subtotal * tax_rate
}
let items: [decimal] = [19.99d, 4.50d, 12.00d]
let total = invoice_total(items, 0.08d)
print(total) // expected: exact arithmetic, e.g., "39.7892"
bigdecimal type — arbitrary precision
bigdecimal (suffix bd) has no fixed digit limit — only memory.
Suitable for values beyond the ~28 digits of decimal.
40-digit integers; as bigdecimal reveals the binary noise of float.
// Feature: `bigdecimal` primitive type — arbitrary-precision decimal arithmetic.
// When to use: when you need MORE than rust_decimal's ~28 significant digits:
// - cryptographic / scientific computations
// - financial calculations involving enormous sums (> 10^28)
// - any code that needs to represent values like
// 1234567890123456789012345678901234567890 exactly
//
// BigDecimal is backed by the `bigdecimal` crate (arbitrary precision, heap-allocated).
// Unlike `decimal` (rust_decimal, ~96-bit / 28 digits), `bigdecimal` has no fixed
// upper limit — just heap memory.
// ── Literals: `bd` suffix ───────────────────────────────────────────
let a: bigdecimal = 0.1bd
let b: bigdecimal = 0.2bd
print(a + b) // expected: "0.3" (exact, no floating-point noise)
print(a + b == 0.3bd) // expected: true
// Integer-form bigdecimal literal
let price: bigdecimal = 100bd
print(price) // expected: "100"
// ── Arbitrary precision — beyond rust_decimal's 28-digit limit ──────
// rust_decimal would panic or lose precision here. bigdecimal handles it.
let big: bigdecimal = 1234567890123456789012345678901234567890bd
let one: bigdecimal = 1bd
print(big + one) // expected: "1234567890123456789012345678901234567891"
// ── Arithmetic operators ─────────────────────────────────────────────
let x: bigdecimal = 2.5bd
let y: bigdecimal = 1.5bd
print(x + y) // expected: "4"
print(x - y) // expected: "1"
print(x * y) // expected: "3.75"
print(x / y) // expected: "1.6666..." (extended precision)
// ── Comparison operators (exact, no ~= needed) ───────────────────────
print(0.1bd + 0.2bd == 0.3bd) // expected: true (EXACT)
print(0.1bd + 0.2bd != 0.3bd) // expected: false
print(0.5bd < 1.0bd) // expected: true
print(0.5bd <= 0.5bd) // expected: true
print(1.0bd > 0.5bd) // expected: true
// ── Conversions via `as` ─────────────────────────────────────────────
// BigDecimal → float: precision may be lost
let from_bd: float = 0.3bd as float
print(from_bd) // expected: 0.3
// Float → bigdecimal: REVEALS the binary FP noise
let from_f: bigdecimal = 0.1 as bigdecimal
print(from_f) // expected: something like 0.1000000000000000055511...
// Int → bigdecimal: always exact
let from_i: bigdecimal = 42 as bigdecimal
print(from_i) // expected: "42"
// BigDecimal → int: truncates (no rounding)
let to_i: int = 99.9bd as int
print(to_i) // expected: 99 (truncated)
// BigDecimal ↔ Decimal cross-cast
let d: decimal = 3.14d
let bd_from_d: bigdecimal = d as bigdecimal
print(bd_from_d) // expected: "3.14"
Methods on decimal and bigdecimal
Both types expose .round(n), .ceil(), .floor(), .trunc() and
.scale() directly on the value:
d.round(2), neg.ceil(), bd.floor() — controlled rounding on both types.
// Feature: decimal/bigdecimal methods — round, ceil, floor, trunc, scale.
let d: decimal = 3.14159d
print(d.round(2)) // expected: "3.14"
print(d.round(4)) // expected: "3.1416"
print(d.ceil()) // expected: "4"
print(d.floor()) // expected: "3"
print(d.trunc()) // expected: "3"
print(d.scale()) // expected: 5
let neg: decimal = -2.7d
print(neg.ceil()) // expected: "-2"
print(neg.floor()) // expected: "-3"
print(neg.trunc()) // expected: "-2"
let bd: bigdecimal = 99.999999bd
print(bd.round(2)) // expected: "100.00" or "100"
print(bd.ceil()) // expected: "100"
print(bd.floor()) // expected: "99"
print(bd.trunc()) // expected: "99"
print(bd.scale()) // expected: 6
let neg_bd: bigdecimal = -1.5bd
print(neg_bd.ceil()) // expected: "-1"
print(neg_bd.floor()) // expected: "-2"
print(neg_bd.trunc()) // expected: "-1"
Method syntax on the float type
The predicates and comparison functions from the math module are also available
as methods directly on a float value:
x.is_nan(), (-3.14).abs(), (0.1 + 0.2).approx_eq(0.3) — chained form.
// Feature: methods on float type (Phase 3 reduced).
// Same semantics as math.* stdlib functions; method syntax for readability.
// x.is_nan() → math.is_nan(x)
// x.is_finite() → math.is_finite(x)
// x.is_infinite() → math.is_infinite(x)
// x.abs() → math.abs(x)
// x.abs_diff(y) → math.abs_diff(x, y)
// x.approx_eq(y) → math.approx_eq(x, y)
// x.approx_eq_abs(y, tol) → math.approx_eq_abs(x, y, tol)
// x.approx_eq_rel(y, rtol) → math.approx_eq_rel(x, y, rtol)
// x.approx_eq_ulps(y, n) → math.approx_eq_ulps(x, y, n)
use std::math
let x: float = 1.0
let nan: float = 0.0 / 0.0
// Predicates
print(x.is_nan()) // false
print(nan.is_nan()) // true
print(x.is_finite()) // true
print(nan.is_finite()) // false
print(math.huge.is_infinite()) // true
// Arithmetic
print((-3.14).abs()) // 3.14
print(x.abs_diff(2.0)) // 1.0
// Approximate equality
print((0.1 + 0.2).approx_eq(0.3)) // true (default tol)
print((0.1 + 0.2).approx_eq_abs(0.3, 1e-9)) // true
print(1.0.approx_eq_rel(1.01, 0.001)) // false (1% > 0.1%)
print((0.1 + 0.2).approx_eq_ulps(0.3, 4)) // true
Challenge
Implement a function normalize_score(raw: float) -> float that maps raw
from [0, 100] to [0, 1] using math.remap, then verify with ~= that
normalize_score(50.0) ~= 0.5.