Skip to content

ULID Generator — C++ 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 C++ 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: C++ (C++17, stdlib only). 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).
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <random>
#include <stdexcept>
#include <string>

inline const std::string 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.
int div_by_32(std::array<uint8_t, 16>& b) {
    int rem = 0;
    for (auto& byte : b) {
        int cur = (rem << 8) | byte;
        byte = static_cast<uint8_t>(cur >> 5);
        rem = cur & 0x1F;
    }
    return 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.
std::string encode_ulid(const std::array<uint8_t, 6>& time_b, const std::array<uint8_t, 10>& rnd_b) {
    std::array<uint8_t, 16> b;
    std::copy(time_b.begin(), time_b.end(), b.begin());
    std::copy(rnd_b.begin(), rnd_b.end(), b.begin() + 6);
    std::string out(26, ' ');
    for (int i = 25; i >= 0; --i) out[i] = CROCKFORD[div_by_32(b)];
    return out;
}

// 48-bit ms timestamp -> 6 big-endian bytes (MSB first), like TS timeBytes().
std::array<uint8_t, 6> time_bytes(uint64_t ms) {
    std::array<uint8_t, 6> out{};
    for (int i = 5; i >= 0; --i, ms >>= 8) out[i] = ms & 0xFF;
    return out;
}

// New ULID for the epoch-ms. std::random_device is the OS entropy source -
// the C++ analogue of Go's crypto/rand.
std::string generate_ulid(uint64_t ms) {
    std::array<uint8_t, 10> rnd{};
    for (auto& byte : rnd) byte = static_cast<uint8_t>(std::random_device{}());
    return encode_ulid(time_bytes(ms), rnd);
}

// Extract the 48-bit ms timestamp from the first 10 chars; throws on
// malformed input (wrong length, or a char outside the alphabet such as I/L/O/U).
uint64_t decode_ulid_time(const std::string& id) {
    if (id.size() != 26) throw std::invalid_argument("ulid: malformed id: want 26 chars");
    uint64_t ts = 0;
    for (size_t i = 0; i < 10; ++i) {
        size_t v = CROCKFORD.find(id[i]);
        if (v == std::string::npos)
            throw std::invalid_argument(std::string("ulid: invalid character ") + id[i]);
        ts = ts * 32 + v;
    }
    return ts;
}

int main() {
    const std::string ulid = generate_ulid(1722000000000ULL);
    std::cout << "ulid:     " << ulid << '\n';
    std::cout << "time(ms): " << decode_ulid_time(ulid) << "  (round-trip ok)\n";
}

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 →