Axle v0.14.1

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 :

GroupMethods on an f64
Absoluteabs
Min / Maxmin, max
Powers / rootspow, sqrt, cbrt, exp
Logsln, log2, log10
Trigsin, cos, tan, asin, acos, atan
Hyperbolicsinh, cosh, tanh
Rounding (→ f64)floor, ceil, trunc
Rounding (→ i64)round, truncToInt, floorToInt, ceilToInt
Sign / classifysign, 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 :

GroupFree functions
Constantspi(), e(), tau()
Two-argumentatan2(y, x), hypot(x, y), sinCos(x)
Type boundsi32Min(), i32Max(), i64Min(), i64Max(), f64Min(), f64Max()
Special valuesf64PositiveInfinity(), 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;
}
MethodEffect (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;
}
FunctionEffect
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

TypeBehaviour
i32, i64 arithmeticwraps silently on overflow (two’s-complement) ; a constant-foldable overflow is rejected at compile time
u32, u64 arithmeticwraps 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 operandrejected at compile time (E0037) — write the cast that says which reading you meant
division by a literal zerorejected at compile time
division by a runtime zeroaborts with a runtime error — guard the divisor yourself
f64 arithmeticfollows IEEE-754 (Infinity, NaN)
truncToInt(x) for x outside i64 rangesaturates 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

numericmathrandomtime