Skip to content

nicolaou-dev/ulid.zig

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ULID

ULID for Zig

CI Zig License: MIT

A fast, allocation-free implementation of ULID (Universally Unique Lexicographically Sortable Identifier) in Zig 0.15.1.
Lexicographically sortable, 128-bit compatible, with monotonic + atomic generators.

Generated by AI according to the ULID specification
Comprehensive compliance tests can be found in spec_compliance_test.zig

const ulid = @import("ulid");
const id = ulid.new().toString(); // "01HQXW5P7R8ZYFG9K3NMVBCXSD"

Features

  • Fast & allocation-free — stack-allocated [26]u8 strings, no heap needed
  • 128-bit compatible — drop-in replacement for UUID columns
  • Crockford's Base32 — case-insensitive, avoids ambiguous I/L/O/U
  • Monotonic generation — strictly increasing within the same millisecond
  • Thread-safe — lock-free AtomicGenerator for high-concurrency scenarios
  • Pure Zig — zero dependencies, works out of the box

Installation

Add to your build.zig.zon:

zig fetch --save https://github.com/nicolaou-dev/ulid.zig/archive/refs/heads/main.tar.gz

In your build.zig:

const ulid_dep = b.dependency("ulid", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("ulid", ulid_dep.module("ulid"));

Usage

Basic generation

const ulid = @import("ulid");

// Generate new ULID
const id = ulid.new();
const str = id.toString(); // [26]u8

Generate string directly:

const id_str = ulid.newString(); // [26]u8

Monotonic generation

Guarantees strictly increasing values within the same millisecond:

const id1 = try ulid.newMonotonic();
const id2 = try ulid.newMonotonic();
std.debug.assert(ulid.Ulid.lessThan(id1, id2));

Parsing & validation

const parsed = try ulid.parse("01HQXW5P7R8ZYFG9K3NMVBCXSD");
const ts = parsed.timestamp(); // ms since epoch

// Case-insensitive parsing (output always canonical uppercase)
const lower = try ulid.parse("01hqxw5p7r8zyfg9k3nmvbcxsd");

// Validation without parsing
if (ulid.isValid("01HQXW5P7R8ZYFG9K3NMVBCXSD")) {
    const timestamp = try ulid.extractTimestamp("01HQXW5P7R8ZYFG9K3NMVBCXSD");
}

// Compile-time parsing (no error handling needed)
const known = ulid.parseComptime("01HQXW5P7R8ZYFG9K3NMVBCXSD");

Zero-allocation API

For hot paths, encode directly into caller's buffer:

var buffer: [26]u8 = undefined;
ulid.newInto(&buffer);

Advanced usage

Custom generator with internal state

var gen = ulid.Generator.init();
const id = try gen.next();

Time-based helpers

const min_ulid = ulid.minForTime(ts_ms); // All zeros entropy
const max_ulid = ulid.maxForTime(ts_ms); // All ones entropy

// Check if ULID is within time range
if (ulid.isInTimeRange(id, start_ms, end_ms)) {
    // ...
}

Builder pattern

Create ULIDs with specific components:

var builder = ulid.Builder{};
const id = builder.withTimestamp(1234567890).build();

// Or with custom entropy
const entropy = [_]u8{1,2,3,4,5,6,7,8,9,10};
const id2 = builder
    .withTimestamp(ts)
    .withEntropy(entropy)
    .build();

Sorting

var ulids: [100]ulid.Ulid = ...;
ulid.sort(&ulids);

// Or sort string representations
var strings: [100][]const u8 = ...;
ulid.sortStrings(&strings);

Concurrent atomic generator

For high-concurrency scenarios:

var gen = ulid.AtomicGenerator.init();
const id1 = try gen.next(); // Thread-safe
const id2 = try gen.next();

Comparison

if (ulid.Ulid.lessThan(id1, id2)) {
    // id1 was created before id2
}

if (ulid.Ulid.equals(id1, id2)) {
    // Same ULID
}

Error handling

// Parsing errors
ulid.parse("invalid") catch |err| switch (err) {
    error.InvalidUlid => // Wrong format/length
};

// Monotonic overflow (extremely rare)
ulid.newMonotonic() catch |err| switch (err) {
    error.MonotonicOverflow => // All 80 entropy bits exhausted in 1ms
};

Specification

 01AN4Z07BY      79KA1307SR9X4MV3

|----------|    |----------------|
 Timestamp          Randomness
   48 bits            80 bits
  • 48-bit timestamp — ms precision (~8,919 years)
  • 80-bit randomness — 1.21e+24 unique IDs per ms
  • Encoding — Crockford's Base32 (no I, L, O, U)
  • Length — 26 chars, fixed width

Performance

  • Generation: < 200ns per ULID
  • No heap allocations for basic operations
  • Thread-safe atomic generator available
  • Optimized for both single and high-volume generation

Run the benchmark tests yourself:

zig build test

Why ULID instead of UUID?

  • Shorter strings (26 vs 36 chars)
  • Sortable by creation time
  • Easier to read, avoids ambiguous characters
  • Still 128-bit compatible with UUID storage columns

Comparison with UUID

Feature ULID UUID v4
Size 128 bits 128 bits
Sortable ✓ (by time) ✗ Random
String length 26 chars 36 chars
Timestamp ✓ (48-bit ms) ✗ None
Monotonic

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages