Math, Dates, And UUIDs

Math

import math;

The math module provides numeric helpers, rounding functions, trigonometry, roots, exponentiation, logarithms, and constants. Most functions accept int, float, or decimal and return either int or float depending on the operation.

Rounding and clamping

math.floor(n), math.ceil(n), and math.round(n) accept any numeric value and return int. math.trunc(n) truncates toward zero and returns float.

import math;

io.println(math.floor(2.9));    # 2
io.println(math.ceil(2.1));     # 3
io.println(math.round(2.5));    # 3
io.println(math.round(2.4));    # 2
io.println(math.trunc(-2.9f));  # -2.0 (float)

# floor/ceil/round also accept decimal
decimal price = 4.0 / 3.0;
io.println(math.floor(price));  # 1
io.println(math.ceil(price));   # 2
io.println(math.round(price));  # 1

math.abs(n) returns the absolute value, preserving the type of the argument:

io.println(math.abs(-5));     # 5      (int)
io.println(math.abs(-3.7f));  # 3.7    (float)
io.println(math.abs(-2.5));   # 2.5000000000  (decimal)

math.clamp(value, min, max) constrains a value to the range [min, max]:

io.println(math.clamp(12, 0, 10));   # 10
io.println(math.clamp(-5, 0, 10));   # 0
io.println(math.clamp(7, 0, 10));    # 7

math.min(a, ...) and math.max(a, ...) accept one or more numeric arguments:

io.println(math.min(3, 1, 4, 1, 5));   # 1
io.println(math.max(3, 1, 4, 1, 5));   # 5

math.sign(n) returns -1, 0, or 1 as int:

io.println(math.sign(-7));    # -1
io.println(math.sign(0));     # 0
io.println(math.sign(3.5f));  # 1

Roots, powers, and geometry

math.sqrt(n), math.cbrt(n) - square root and cube root, return float:

io.println(math.sqrt(16.0f));    # 4.0
io.println(math.cbrt(27.0f));    # 3.0

math.pow(base, exponent) - raise to a power, returns float:

io.println(math.pow(2.0f, 10.0f));   # 1024.0
io.println(math.pow(9.0f, 0.5f));    # 3.0

math.hypot(a, b) - length of the hypotenuse, returns float:

io.println(math.hypot(3.0f, 4.0f));   # 5.0

Trigonometry

All trig functions work in radians and return float:

io.println(math.sin(math.pi() / 2.0f));   # 1.0
io.println(math.cos(0.0f));               # 1.0
io.println(math.tan(math.pi() / 4.0f));   # ~1.0

io.println(math.asin(1.0f));              # ~1.5707963...  (π/2)
io.println(math.acos(1.0f));              # 0.0
io.println(math.atan(1.0f));              # ~0.7853...      (π/4)
io.println(math.atan2(1.0f, 1.0f));       # ~0.7853...      (π/4)

Convert degrees to radians and back:

let deg = 90.0f;
let rad = deg * math.pi() / 180.0f;
io.println(math.sin(rad));   # 1.0

let back = rad * 180.0f / math.pi();
io.println(back);   # 90.0

Logarithms and exponentials

math.log(n) - natural logarithm (base e), math.log2(n), math.log10(n), math.exp(n) - all accept numeric and return float:

io.println(math.log(math.e()));    # 1.0
io.println(math.log2(8.0f));       # 3.0
io.println(math.log10(1000.0f));   # 3.0
io.println(math.exp(1.0f));        # ~2.71828...

Constants

math.pi() and math.e() return the constant as float:

io.println(math.pi());   # 3.141592653589793
io.println(math.e());    # 2.718281828459045

math.inf() returns positive infinity. math.nan() returns a not-a-number value. Use math.isNaN(n) and math.isInf(n) to test:

let inf = math.inf();
let nan = math.nan();

io.println(math.isInf(inf));   # true
io.println(math.isNaN(nan));   # true
io.println(math.isNaN(42.0f)); # false
io.println(inf > 1e308f);      # true

Datetime

import datetime;

The datetime module works with two representations:

  • Unix seconds (int) - a raw integer timestamp. Lightweight and interoperable. Most module-level functions take and return Unix seconds.
  • Instant - an object with chainable methods. Useful when you need to apply several operations in sequence without passing the timestamp through every call.

Both representations are exact to the second. Sub-second precision is not currently supported.

Current time

import datetime;

let ts = datetime.nowUnix();             # int: current Unix seconds
let parts = datetime.now();              # dict with year, month, day, etc.
let instant = datetime.nowInstant();     # Instant object

io.println(ts);
io.println(parts["year"]);
io.println(instant.formatRFC3339());

datetime.now() returns a dict with these keys:

Key Type Meaning
timestamp int Unix seconds
year int Full year, e.g. 2025
month int 1-12
day int 1-31
hour int 0-23
minute int 0-59
second int 0-59
weekday int 0 (Sunday) - 6 (Saturday)
zone string IANA timezone name ("UTC" by default)

Timezone-aware now

datetime.now(zoneName) returns the same parts dict shifted into the given IANA timezone (e.g. "Europe/London", "America/New_York", "Asia/Tokyo"). The zone key reflects the requested timezone:

let london = datetime.now("Europe/London");
io.println(london["hour"], london["zone"]);

Parts at a specific timezone for a known unix timestamp

datetime.partsInZone(unixSeconds, zoneName) is the equivalent constructor when you already have a Unix timestamp:

let parts = datetime.partsInZone(1700000000, "America/New_York");
io.println(parts["hour"]);   # 17 (5pm EST)

HTTP-date / RFC1123 formatting

datetime.formatHTTP(unixSeconds) produces an RFC1123 GMT timestamp suitable for HTTP headers (Last-Modified, Date, Expires, Set-Cookie expires=):

io.println(datetime.formatHTTP(1700000000));
# Tue, 14 Nov 2023 22:13:20 GMT

Constructing timestamps

datetime.make(year, month, day) or datetime.make(year, month, day, hour, minute, second) builds a Unix timestamp from components:

let ts = datetime.make(2025, 12, 25);            # midnight UTC
let ts2 = datetime.make(2025, 12, 25, 9, 30, 0); # 09:30 UTC

datetime.unix(seconds) converts a Unix timestamp to an RFC3339 string:

io.println(datetime.unix(0));   # 1970-01-01T00:00:00Z

datetime.Instant(ts_or_string) wraps a Unix timestamp or RFC3339 string as an Instant object:

let a = datetime.Instant(1735128600);                # from Unix seconds
let b = datetime.Instant("2025-12-25T09:30:00Z");   # from RFC3339

datetime.Duration(seconds) wraps a number of seconds as a Duration object:

let oneDay = datetime.Duration(86400);
let oneHour = datetime.Duration(3600);

datetime.Zone(name) creates a Zone object from an IANA timezone name:

let london = datetime.Zone("Europe/London");
let tokyo  = datetime.Zone("Asia/Tokyo");

Parsing and formatting

datetime.parse(s) and datetime.parseRFC3339(s) both parse an RFC3339 string and return Unix seconds:

let ts = datetime.parse("2025-06-01T12:00:00Z");
let ts2 = datetime.parseRFC3339("2025-06-01T12:00:00Z");

datetime.format(unixSeconds, layout) formats a timestamp using a Go-style layout string. The reference time is 2006-01-02T15:04:05Z07:00:

io.println(datetime.format(ts, "2006-01-02"));           # 2025-06-01
io.println(datetime.format(ts, "2006-01-02 15:04:05"));  # 2025-06-01 12:00:00
io.println(datetime.format(ts, "Jan 2, 2006"));          # Jun 1, 2025
io.println(datetime.format(ts, "3:04 PM"));              # 12:00 PM

Convenience formatters:

io.println(datetime.formatRFC3339(ts));  # 2025-06-01T12:00:00Z
io.println(datetime.formatDate(ts));     # 2025-06-01
io.println(datetime.formatTime(ts));     # 12:00:00

datetime.toUtc(ts) and datetime.toLocal(ts, tz) format to RFC3339, with toLocal converting to the given timezone:

let tokyo = "Asia/Tokyo";
io.println(datetime.toLocal(ts, tokyo));   # 2025-06-01T21:00:00+09:00
io.println(datetime.toUtc(ts));            # 2025-06-01T12:00:00Z

The timezone can be a string name or a datetime.Zone object.

Arithmetic with Unix seconds

All arithmetic functions take and return Unix timestamps (int):

let now = datetime.nowUnix();
let tomorrow   = datetime.addDays(now, 1);
let nextMonth  = datetime.addMonths(now, 1);
let nextYear   = datetime.addYears(now, 1);
let inOneHour  = datetime.addSeconds(now, 3600);

datetime.diff(start, end) returns a dict:

let start = datetime.make(2025, 1, 1);
let end   = datetime.make(2025, 6, 15);
let d = datetime.diff(start, end);

io.println(d["days"]);     # 165
io.println(d["hours"]);    # 0
io.println(d["minutes"]);  # 0
io.println(d["seconds"]);  # 0

The diff result is always non-negative; the order of arguments does not matter.

The Instant class

Instant is an object-oriented interface to the same timestamp. Methods chain and always return Instant or a value - they never mutate in place.

let appt = datetime.nowInstant()
    .addDays(7)
    .addSeconds(3600);

io.println(appt.formatRFC3339());   # one week and one hour from now
io.println(appt.unix());            # as Unix seconds

Formatting methods:

let i = datetime.Instant("2025-12-25T09:30:00Z");

i.toString();           # "2025-12-25T09:30:00Z"  (same as formatRFC3339)
i.formatRFC3339();      # "2025-12-25T09:30:00Z"
i.toUtc();              # "2025-12-25T09:30:00Z"
i.format("Jan 2, 2006 15:04");          # "Dec 25, 2025 09:30"
i.toLocal("America/New_York");          # "2025-12-25T04:30:00-05:00"

Arithmetic methods (each returns a new Instant):

i.add(datetime.Duration(3600));   # add one hour
i.addSeconds(3600);               # same
i.addDays(1);
i.addMonths(1);
i.addYears(1);

Components and comparison:

i.unix();    # int: Unix seconds
i.parts();   # dict with year, month, day, hour, minute, second, weekday, timestamp

let diff = a.diff(b);   # Duration (absolute difference)

Full chained example - schedule a reminder for 9 AM next Monday:

import datetime;

func nextMonday(Instant from): Instant {
    let parts = from.parts();
    let daysUntilMonday = (8 - parts["weekday"]) % 7;
    if (daysUntilMonday == 0) { daysUntilMonday = 7; }
    return from.addDays(daysUntilMonday)
               .add(datetime.Duration(9 * 3600 - (parts["hour"] * 3600 + parts["minute"] * 60 + parts["second"])));
}

let reminder = nextMonday(datetime.nowInstant());
io.println(reminder.formatRFC3339());

The Duration class

datetime.Duration(seconds) wraps a number of seconds. Use it with Instant.add() for semantic arithmetic.

let d = datetime.Duration(90061);   # 1 day + 1 hour + 1 minute + 1 second

io.println(d.seconds());   # 90061
io.println(d.toString());  # "90061s"

let parts = d.toDict();
io.println(parts["days"]);     # 1
io.println(parts["hours"]);    # 1
io.println(parts["minutes"]);  # 1
io.println(parts["seconds"]);  # 1

Predefined durations using datetime.Duration:

let oneMinute = datetime.Duration(60);
let oneHour   = datetime.Duration(3600);
let oneDay    = datetime.Duration(86400);
let oneWeek   = datetime.Duration(604800);

The Zone class

datetime.Zone(name) validates and wraps an IANA timezone identifier:

let tz = datetime.Zone("America/Los_Angeles");

io.println(tz.name());           # "America/Los_Angeles"
io.println(tz.toString());       # "America/Los_Angeles"

let now = datetime.nowInstant();
let offset = tz.offsetAt(now);   # offset in seconds, e.g. -28800 (UTC-8)
io.println(offset / 3600);       # -8

Zone.offsetAt(instant) returns the UTC offset in seconds at the given instant (accounts for DST):

let summerTs  = datetime.Instant("2025-07-01T00:00:00Z");
let winterTs  = datetime.Instant("2025-01-01T00:00:00Z");
let la = datetime.Zone("America/Los_Angeles");

io.println(la.offsetAt(summerTs) / 3600);   # -7  (PDT)
io.println(la.offsetAt(winterTs) / 3600);   # -8  (PST)

Labels

io.println(datetime.weekdayName(0));   # Sunday  (0=Sunday, 1=Monday, …)
io.println(datetime.weekdayName(1));   # Monday
io.println(datetime.monthName(1));     # January
io.println(datetime.monthName(12));    # December

Pull the day name from a timestamp:

let parts = datetime.now();
io.println(datetime.weekdayName(parts["weekday"]));
io.println(datetime.monthName(parts["month"]));

Sleeping

datetime.sleep(ms) pauses execution for the given number of milliseconds:

datetime.sleep(1000);   # wait 1 second

Complete example

import datetime;
import io;

# Build a human-readable countdown to a deadline
func countdown(string deadline): string {
    let target = datetime.Instant(deadline);
    let now    = datetime.nowInstant();
    let diff   = now.diff(target);
    let parts  = diff.toDict();

    if (parts["days"] > 0) {
        return "${parts["days"]} days, ${parts["hours"]} hours";
    }
    if (parts["hours"] > 0) {
        return "${parts["hours"]} hours, ${parts["minutes"]} minutes";
    }
    return "${parts["minutes"]} minutes";
}

io.println(countdown("2026-01-01T00:00:00Z"));

Time - Elapsed durations

import time;

The time module exposes simple monotonic-style timing primitives for measuring elapsed wall-clock durations, throttling, debouncing and blocking sleeps. It is distinct from datetime, which models calendar-aware moments in time.

Reach for time when you want to time how long something took, profile a block, throttle a loop, or pause execution synchronously. Reach for datetime when you need a zone-aware timestamp or a formatted date.

Functions

Function Returns Description
time.now() int Milliseconds since the Unix epoch. Two now() values can be subtracted to get an elapsed duration in ms.
time.elapsed(start) int Convenience for time.now() - start. Returns milliseconds elapsed since the start value (also in ms).
time.sleep(ms) null Pauses the current thread for ms milliseconds. Use async.sleep instead inside async tasks where you want cooperative scheduling.
import time;
import io;

let start = time.now();
# ... work ...
io.println("took " + (time.elapsed(start) as string) + " ms");

A stopwatch is just two values:

let started = time.now();
let lap     = started;
# ... work block A ...
io.println("lap A: " + (time.elapsed(lap) as string) + " ms");
lap = time.now();
# ... work block B ...
io.println("lap B: " + (time.elapsed(lap) as string) + " ms");
io.println("total: " + (time.elapsed(started) as string) + " ms");

UUID

import uuid;

The uuid module generates and validates UUIDs (Universally Unique Identifiers). All functions return standard lowercase hyphenated strings (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

Generating random UUIDs

uuid.v4() - random UUID, no inherent ordering:

let id = uuid.v4();
io.println(id);   # e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479

uuid.v7() - random UUID with a millisecond-precision timestamp prefix. Use this when you want UUIDs that sort chronologically:

let id = uuid.v7();
io.println(id);   # e.g. 018f9ca2-3c40-7b4d-9e1a-5bfce2a58841

uuid.v1() - time-based UUID using the current time and MAC address (or a random node identifier). Less portable than v7 for new designs:

let id = uuid.v1();

Namespace UUIDs (v3 and v5)

uuid.v3(namespace, name) produces a deterministic UUID using MD5 hashing. uuid.v5(namespace, name) uses SHA-1. Both accept a namespace UUID string and a name string:

let ns  = uuid.namespaceDNS();
let id3 = uuid.v3(ns, "example.com");
let id5 = uuid.v5(ns, "example.com");

io.println(id3);   # 9073926b-929f-31c2-abc9-fad77ae3e8eb  (always the same)
io.println(id5);   # 2ed6657d-e927-568b-95e3-af9a23cb4481  (always the same)

The same namespace and name always produce the same UUID. Use v5 in new code - SHA-1 produces better distribution than MD5.

Predefined namespaces:

uuid.namespaceDNS();    # 6ba7b810-9dad-11d1-80b4-00c04fd430c8
uuid.namespaceURL();    # 6ba7b811-9dad-11d1-80b4-00c04fd430c8
uuid.namespaceOID();    # 6ba7b812-9dad-11d1-80b4-00c04fd430c8
uuid.namespaceX500();   # 6ba7b814-9dad-11d1-80b4-00c04fd430c8

Custom namespaces can be any UUID string:

let myNs  = uuid.v4();   # generate once and persist it
let docId = uuid.v5(myNs, "invoice-2025-001");

Parsing and validation

uuid.parse(s) normalises a UUID string (lowercase, hyphenated). Throws if the input is not a valid UUID:

let id = uuid.parse("F47AC10B-58CC-4372-A567-0E02B2C3D479");
io.println(id);   # f47ac10b-58cc-4372-a567-0e02b2c3d479

uuid.isValid(s) returns true if the string is a valid UUID:

io.println(uuid.isValid("f47ac10b-58cc-4372-a567-0e02b2c3d479"));   # true
io.println(uuid.isValid("not-a-uuid"));                              # false

uuid.nil() returns the nil UUID (all zeros):

io.println(uuid.nil());   # 00000000-0000-0000-0000-000000000000

Binary conversion

uuid.toBytes(s) converts a UUID string to a 16-byte bytes value. uuid.fromBytes(b) does the reverse:

let id    = uuid.v4();
let raw   = uuid.toBytes(id);      # 16-byte bytes value
let back  = uuid.fromBytes(raw);   # round-trips to the same string
io.println(id == back);   # true

This is useful when storing UUIDs as binary in databases or passing them as binary extension payloads.

ULID

uuid.ulid() generates a ULID (Universally Unique Lexicographically Sortable Identifier). A ULID encodes a 48-bit millisecond timestamp and 80 bits of randomness as a 26-character Crockford base32 string:

let id = uuid.ulid();
io.println(id);   # e.g. 01ARZ3NDEKTSV4RRFFQ69G5FAV

ULIDs sort correctly as plain strings, are case-insensitive, and carry no dashes. Two ULIDs generated in the same millisecond have the same timestamp prefix but differ in the random suffix.

Choosing a UUID version

Version Use when
v4 You need a random ID with no ordering or determinism requirements
v7 You need random IDs that sort chronologically (primary keys, event streams)
v5 You need the same ID every time for the same input (content addressing, idempotent inserts)
v3 Legacy systems that require MD5-based namespace UUIDs
v1 Interoperability with systems that require time-and-MAC UUIDs
ulid You want sortable, URL-safe, human-readable IDs without dashes