Numeric and time
Math helpers, random numbers, dates and durations — backed by std/numeric and std/time.
Math basics
Numeric operations are methods on the primitive types themselves — x.sqrt(), n.abs(), a.max(b). No import is needed for those. The
handful of things that are not operations on a value — the constants,
and the two-argument functions with no natural receiver — are free
functions in std/numeric/math, reached by binding the module with use std::numeric::math;.
A numeric literal receiver needs parentheses: (2.0).sqrt(), not 2.0.sqrt().
use std::numeric::math;
fn main() : i32 {
let pi : f64 = math::pi(); // 3.141592653589793
let e : f64 = math::e(); // 2.718281828459045
let s : f64 = (2.0).sqrt(); // 1.4142…
let p : f64 = (2.0).pow(10.0); // 1024.0
let c : f64 = (27.0).cbrt(); // 3.0
let h : f64 = math::hypot(3.0, 4.0); // 5.0
println((pi + e + s + p + c + h).toString());
return 0;
} The full helper set, as methods on f64 :
| Group | Methods on an f64 |
|---|---|
| Absolute | abs |
| Min / Max | min, max |
| Powers / roots | pow, sqrt, cbrt, exp |
| Logs | ln, log2, log10 |
| Trig | sin, cos, tan, asin, acos, atan |
| Hyperbolic | sinh, cosh, tanh |
Rounding (→ f64) | floor, ceil, trunc |
Rounding (→ i64) | round, truncToInt, floorToInt, ceilToInt |
| Sign / classify | sign, isNaN, isInfinite, isFinite |
On i32 and i64 : abs, absChecked, min, max, toHex, toBin.
On i32 only : toOct.
And in std/numeric/math, as free functions :
| Group | Free functions |
|---|---|
| Constants | pi(), e(), tau() |
| Two-argument | atan2(y, x), hypot(x, y), sinCos(x) |
| Type bounds | i32Min(), i32Max(), i64Min(), i64Max(), f64Min(), f64Max() |
| Special values | f64PositiveInfinity(), f64NegativeInfinity(), f64NotANumber() |
i32Max() is the largest value an i32 can hold; a.max(b) is the
larger of two numbers. They are unrelated despite the shared word.
Calling style
An operation on a value is a method on that value :
fn main() : i32 {
let r : f64 = (2.0).sqrt();
println(r.toString());
return 0;
} Methods on primitives need no use line at all.
Integer absolute value, two flavours
fn main() : i32 ! ArithmeticException {
let a : i32 = (-5).abs(); // 5 — wraps on the i32 minimum
let b : i32 = (-5).absChecked(); // 5 — throws on the i32 minimum
println((a + b).toString());
return 0;
} abs() of the minimum i32 returns it unchanged (the
two’s-complement edge case). absChecked() throws ArithmeticException instead — use it when you can’t tolerate
silent overflow. Both exist on i64 as well.
Mind the width when the receiver is a literal: (1).max(big) reads 1 as an i32 and truncates an i64 argument. Write (1 as i64).max(big), or make the receiver an i64 local.
NaN / Infinity handling
fn main() : i32 {
let x : f64 = (-1.0).sqrt(); // NaN
if (x.isNaN()) {
// NaN propagates through every arithmetic op
}
if ((1000.0).exp().isInfinite()) { /* … */ }
return 0;
} A literal zero divisor (0.0 / 0.0, 1 / 0) is rejected at
compile time, so NaN / Infinity enter a program through
operations like these rather than literal division.
NaN-aware code should always classify before comparing — x == x is false for NaN, so the usual == operator can mislead.
sign(x) is float-typed
fn main() : i32 {
let s : f64 = (-3.5).sign(); // -1.0
let z : f64 = (0.0).sign(); // 0.0
let n : f64 = (-1.0).sqrt().sign(); // NaN in, NaN out
println((s + z + n).toString());
return 0;
} The float result lets you fold the sign into computations without a branch.
Random numbers
use std::numeric::random::Random;
fn main() : i32 {
Random::seed(42); // deterministic from here on
let a : i32 = Random::nextI32(); // uniform i32
let b : i64 = Random::nextI64();
let f : f64 = Random::nextF64(); // [0.0, 1.0)
let p : bool = Random::nextBool();
let dice : i32 = Random::nextRangeI32(1, 7); // [1, 7) — i.e. 1..6
let big : i64 = Random::nextRangeI64(0, 1000000);
return dice;
} All draws come from one process-wide xoshiro256** stream — Random is a static facade over it, and the module’s free functions
(bind the module with use std::numeric::random; and call random::nextI64()) reach the same stream. For deterministic tests, seed it with a fixed value ; left unseeded, the stream seeds itself
from the monotonic clock on first use.
Picking a random list element
use std::collections::ArrayList;
fn pickRandom(items : ArrayList<string>) : string
! IndexOutOfBoundsException
{
if (items.isEmpty()) {
throw IndexOutOfBoundsException("pickRandom: empty list");
}
let i : i32 = Random::nextRangeI32(0, items.size());
return items.get(i) ?? ""; // in range — never null here
} Time — wall clock
fn main() : i32 {
let nowMs : i64 = now(); // Unix epoch milliseconds
let nowNs : i64 = nanoTime(); // monotonic nanoseconds — not wall-clock !
println(nowMs.toString() + " " + nowNs.toString());
return 0;
} now() and nanoTime() are in every file’s prelude — no use line.
Use now() for “what’s the date” and nanoTime() for “how long did
this take” — they’re different counters with different guarantees.
Measuring elapsed time
fn doWork() : void {
}
fn timeIt() : i64 {
let start : i64 = nanoTime();
doWork();
let end : i64 = nanoTime();
return end - start; // nanoseconds elapsed
} nanoTime is monotonic — never goes backward, immune to NTP
adjustments. Always use it for benchmarks.
Dates — LocalDateTime
use std::time::datetime::LocalDateTime;
fn main() : i32 {
let dt : LocalDateTime = LocalDateTime::now();
let y : i32 = dt.getYear();
let m : i32 = dt.getMonth(); // 1..12
let d : i32 = dt.getDay(); // 1..31
let h : i32 = dt.getHour(); // 0..23
return y;
} From a timestamp
use std::time::datetime::LocalDateTime;
fn main() : i32 {
let dt : LocalDateTime = LocalDateTime::fromMillis(1715342400000);
println(dt.formatIso());
return 0;
} Building explicitly
use std::time::datetime::LocalDateTime;
fn main() : i32 {
let dt : LocalDateTime = LocalDateTime::fromComponents(
2026, 5, 10, // year, month, day
14, 23, 45 // hour, minute, second
);
println(dt.formatIso());
return 0;
} Formatting
use std::time::datetime;
fn main() : i32 {
let ms : i64 = now();
let iso : string = datetime::formatIso8601(ms); // "2026-05-10T14:23:45.123Z"
let d : string = datetime::formatDate(ms); // "2026-05-10"
let t : string = datetime::formatTime(ms); // "14:23:45"
let http: string = datetime::formatHttpDate(ms); // "Sun, 10 May 2026 14:23:45 GMT"
println(iso + d + t + http);
return 0;
} Parsing ISO-8601
use std::time::datetime;
use std::lang::ParseException;
fn parseTimestamp(s : string) : i64 ! ParseException {
return datetime::parseIso8601(s); // returns epoch millis
} Date arithmetic
A LocalDateTime is immutable — the plus* methods return a new value, they don’t mutate the receiver, so they chain:
use std::time::datetime::LocalDateTime;
fn main() : i32 {
let start : LocalDateTime = LocalDateTime::fromComponents(2026, 5, 10, 14, 0, 0);
let later : LocalDateTime = start.plusDays(3).plusHours(6); // 2026-05-13 20:00
println(later.formatIso()); // "2026-05-13T20:00:00.000Z"
return 0;
} | Method | Effect (returns a new LocalDateTime) |
|---|---|
plusDays(n) | add n days |
plusHours(n) / plusMinutes(n) / plusSeconds(n) | add a time offset |
toMillis() | epoch milliseconds for this instant |
To measure a span, subtract the two toMillis() values and convert the
raw millisecond delta with std/time — the module binds as use std::time; (its free functions are top-level, not under a nested time::time):
use std::time;
use std::time::datetime::LocalDateTime;
fn hoursBetween(a : LocalDateTime, b : LocalDateTime) : i64 {
let deltaMs : i64 = b.toMillis() - a.toMillis();
return time::durationToHours(deltaMs); // truncated toward zero
} std/time also builds durations from a unit, for adding to an epoch
timestamp without a LocalDateTime:
use std::time;
fn main() : i32 {
let twoHours : i64 = time::durationFromHours(2); // 7_200_000 ms
let secs : i64 = time::durationToSeconds(twoHours); // 7200
println(twoHours.toString() + " " + secs.toString());
return 0;
} | Function | Effect |
|---|---|
durationFromSeconds/Minutes/Hours(n) | a duration of n units, in milliseconds |
durationToSeconds/Minutes/Hours(ms) | a millisecond duration, in that unit |
instantPlusMillis(t, ms) / instantMinusMillis(t, ms) | shift an epoch-millis instant |
instantUntil(start, end) | millis from start to end |
Decomposing an epoch timestamp
use std::time::datetime;
let ms : i64 = 1715342400000;
let y : i32 = datetime::yearOf(ms);
let m : i32 = datetime::monthOf(ms);
let d : i32 = datetime::dayOf(ms);
let dow: i32 = datetime::dayOfWeek(ms); // 0 = Sunday … 6 = Saturday For one-off field extraction, the free fns are cheaper than
constructing a LocalDateTime.
Durations / sleep
fn doWork() : void {
}
fn rateLimited() : void {
doWork();
sleep(1000); // 1 second
} sleep(ms) is millisecond-resolution. For sub-millisecond
delays use nanoTime busy-wait (rare ; usually a sign the
algorithm is wrong).
Timeout pattern
fn check() : bool {
return true;
}
fn pollUntil(deadlineMs : i64) : bool {
while (now() < deadlineMs) {
if (check()) { return true; }
sleep(50);
}
return false;
}
fn main() : i32 {
let deadline : i64 = now() + 5000; // 5-second deadline
let ok : bool = pollUntil(deadline);
if (ok) { return 0; }
return 1;
} Numeric conversions
fn main() : i32 {
let a : i32 = 42;
let b : i64 = a as i64; // widening — always safe
let c : f64 = a as f64; // i32 → f64 — exact for |a| < 2^53
let d : i32 = (3.9).truncToInt() as i32; // 3 — truncate toward zero
let e : i32 = (-3.9).truncToInt() as i32; // -3
println(b.toString() + c.toString() + d.toString() + e.toString());
return 0;
} as converts between integer widths and to f64, but not from a
float to an integer — that rounding decision must be explicit. Use truncToInt() to truncate toward zero, or round() / floorToInt() / ceilToInt() to round :
fn main() : i32 {
let t : i32 = (3.9).truncToInt() as i32; // 3 — toward zero
let r : i64 = (3.5).round(); // 4
let q : i64 = (2.5).round(); // 3 — halves round away from zero
println(t.toString() + r.toString() + q.toString());
return 0;
} Overflow semantics
| Type | Behaviour |
|---|---|
i32, i64 arithmetic | wraps silently on overflow (two’s-complement) ; a constant-foldable overflow is rejected at compile time |
u32, u64 arithmetic | wraps the same way — 0 - 5 on a u32 is 4294967291, and adding 1 to the largest u32 value (4294967295) gives 0 |
| mixing a signed and an unsigned operand | rejected at compile time (E0037) — write the cast that says which reading you meant |
| division by a literal zero | rejected at compile time |
| division by a runtime zero | aborts with a runtime error — guard the divisor yourself |
f64 arithmetic | follows IEEE-754 (Infinity, NaN) |
truncToInt(x) for x outside i64 range | saturates to the nearest representable i64 |
fn add(a : i32, b : i32) : i32 {
return a + b;
}
fn main() : i32 {
let x : i32 = add(2_147_483_647, 1); // wraps to -2_147_483_648, no exception
println(x.toString());
return 0;
} If you need checked arithmetic, write the check yourself :
fn checkedAdd(a : i32, b : i32) : i32 ! ArithmeticException {
let r : i32 = a + b;
// Overflow iff signs of a and b match but differ from sign of r.
if ((a > 0) == (b > 0) && (a > 0) != (r > 0)) {
throw ArithmeticException("integer overflow");
}
return r;
} Patterns
Clamping
fn clamp(x : f64, lo : f64, hi : f64) : f64 {
return x.max(lo).min(hi);
}
fn main() : i32 {
let input : f64 = 1.75;
let v : f64 = clamp(input, 0.0, 1.0);
println(v.toString());
return 0;
} Modulo handling negatives
// Axle `%` is "remainder" — keeps the sign of the dividend.
// True modulo (always non-negative for a positive divisor) :
fn modPos(a : i32, n : i32) : i32 {
let r : i32 = a % n;
if (r < 0) { return r + n; }
return r;
}
fn main() : i32 {
let rem : i32 = (-7) % 3; // -1, not 2
let m : i32 = modPos(-7, 3); // 2
println(rem.toString() + " " + m.toString());
return 0;
} Linear interpolation
fn lerp(a : f64, b : f64, t : f64) : f64 {
return a + (b - a) * t;
} Power-of-two rounding
fn nextPow2(n : i32) : i32 {
let p : i32 = 1;
while (p < n) { p = p * 2; }
return p;
} See also
std/numericreferencestd/numeric/randomreferencestd/time/datetimereferencestd/sysreference —now,sleep,nanoTime- Concept index — every numeric / time concept cross-linked