Skip to content

ULID Generator — Zig source

Generate Universally Unique Lexicographically Sortable Identifiers (ULID) - 26-character Crockford-base32 strings that sort by millisecond timestamp. Paste any ULID to decode its timestamp and randomness. Runs entirely in your browser.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// ulid-generator - 26-char Crockford-base32 sortable ID: 10 chars of 48-bit ms timestamp + 16 chars of 80-bit randomness. Language: Zig (0.12+). Port of src/lib/ulid.ts (Go twin: cli/ulid-generator) - encode/generate/decode-time core; inspectUlid (date/random-hex view) lives in this dir's javascript.js (80-line budget).
const std = @import("std");

const crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // no I, L, O, U

/// Divide the big-endian value in b (mutated in place) by 32; return the
/// remainder 0-31. Long division in base 256, exactly as in the Go twin.
fn divBy32(b: []u8) u8 {
    var rem: u16 = 0;
    for (b) |*byte| {
        const cur: u16 = (rem << 8) | byte.*;
        byte.* = @intCast(cur >> 5);
        rem = cur & 0x1F;
    }
    return @intCast(rem);
}

/// Base32-encode the 128-bit big-endian [6 time bytes | 10 random bytes] into
/// the canonical 26 chars. Because 32^16 == 2^80, the first 10 chars equal the
/// TS/Go twins' separate time encoding - the cross-language lock-step anchor.
fn encodeUlid(time_bytes: [6]u8, random_bytes: [10]u8) [26]u8 {
    var b: [16]u8 = time_bytes ++ random_bytes;
    var out: [26]u8 = undefined;
    var i: usize = 26;
    while (i > 0) {
        i -= 1;
        out[i] = crockford[divBy32(&b)];
    }
    return out;
}

/// 48-bit ms timestamp -> 6 big-endian bytes (MSB first), like TS timeBytes().
fn timeBytesOf(ms: u64) [6]u8 {
    var t: [6]u8 = undefined;
    var i: usize = 6;
    while (i > 0) {
        i -= 1;
        t[i] = @truncate(ms >> @intCast(8 * i));
    }
    return t;
}

/// Extract the 48-bit ms timestamp from the first 10 chars; error.InvalidChar
/// on a char outside the alphabet (I/L/O/U), error.BadLength on wrong length.
fn decodeUlidTime(id: []const u8) !u64 {
    if (id.len != 26) return error.BadLength;
    var ts: u64 = 0;
    for (id[0..10]) |c| {
        const idx = std.mem.indexOfScalar(u8, crockford, c) orelse return error.InvalidChar;
        ts = ts * 32 + idx;
    }
    return ts;
}

pub fn main() !void {
    var rnd: [10]u8 = undefined;
    std.crypto.random.bytes(&rnd); // stdlib CSPRNG - the analogue of Go's crypto/rand
    const ulid = encodeUlid(timeBytesOf(1722000000000), rnd);
    std.debug.print("ulid:     {s}\n", .{ulid});
    const ts = try decodeUlidTime(&ulid);
    std.debug.print("time(ms): {d}  (round-trip ok)\n", .{ts});
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →