Package

ids

Identifiers and encodings: ULID, KSUID, nanoid, Snowflake, UUID v4/v7 from bytes, base58, base62, base36, base32 and Crockford

0.1.0 MIT

Install

sspur add ids@^0.1.0

or in sspur.toml:

ids = { version = "^0.1.0" }

README

ids

Identifiers and the encodings behind them: ULID, KSUID, nanoid, Snowflake-style 64-bit ids, UUID versions 4 and 7 built from bytes, and base58, base62, base36, RFC 4648 base32 and Crockford base32 for bytes, Int and BigInt.

Every format has a pure function that builds the identifier from its parts (a time and the random bytes), so tests, fixtures and replays are deterministic, and the monotonic generators are pure state steps. A few small generators read the clock (time) and the operating system's random bytes (rand, through crypto.random). Decoders return none for text that is not exactly the format. The test suite checks the published vectors: the ULID specification's example, Bitcoin's base58 vectors, RFC 4648's base32 vectors, RFC 9562's version 7 example and the largest KSUID.

Example

use ids.{ulid_from, ulid_time, ulid_next, ulid_gen, base58, from_base58, twitter_layout, snowflake_decompose, uuid_v7_from, nanoid}

fn main() -> Unit ! log, rand
= do
  r = bytes([214, 118, 76, 97, 239, 185, 147, 2, 189, 91])
  id = ulid_from(1469918176385, r)
  log(id)
  log(ulid_time(id).str)
  log(ulid_next(ulid_gen(), 1469918176385, r).map(p => p.0).str)
  log(base58("Hello World!".to_bytes))
  log(from_base58("2NEpo7TZRRrLZSi2U").map(b => b.to_str).str)
  log(snowflake_decompose(twitter_layout(), 1541815603606036480).str)
  log(uuid_v7_from(1645557742000, bytes([12, 195, 24, 196, 220, 12, 12, 7, 57, 143])))
  log(nanoid().len.str)

prints (natively and with --interp):

01ARYZ6S41TSV4RRFFQ69G5FAV
some(1469918176385)
some("01ARYZ6S41TSV4RRFFQ69G5FAV")
2NEpo7TZRRrLZSi2U
some(some("Hello World!"))
SnowflakeParts{ms: 1656432460105, node: 378, seq: 0}
017f22e2-79b0-7cc3-98c4-dc0c0c07398f
21

API

Base-N encodings

  • base58_alphabet(), base62_alphabet(), base36_alphabet(), crockford_alphabet().
  • encode_base(b, alphabet), decode_base(s, alphabet): bytes as one big-endian number in any base; each leading zero byte is one leading copy of the alphabet's first character (Bitcoin's rule), so the bytes round-trip exactly.
  • encode_int(n, alphabet), decode_int(s, alphabet): non-negative Ints (decoding refuses values above the 64-bit maximum).
  • encode_big(n, alphabet), decode_big(s, alphabet): non-negative BigInts.
  • base58(b), from_base58(s): Bitcoin base58.
  • base62(b), from_base62(s): 0-9A-Za-z.
  • base36(b), from_base36(s): 0-9a-z (decoding takes either case).
  • base32(b, pad), from_base32(s): RFC 4648 base32, padded or not; decoding takes either case and checks the length and the unused bits.
  • crockford(n, check), from_crockford(s, check): Crockford base32 numbers with the optional mod 37 check symbol; decoding ignores hyphens and case and reads I, L as 1 and O as 0.

ULID

  • ulid_from(ms, rnd): the ULID of a millisecond time and 10 random bytes.
  • parse_ulid(s), is_ulid(s), ulid_time(s): the 16 bytes, validity, the time.
  • UlidGen, ulid_gen(), ulid_next(g, ms, rnd): the specification's monotonic generator as a pure step (the same or an earlier millisecond adds 1 to the last random part; none on overflow).
  • ulid(), ulid_next_now(g): with the clock and random bytes (! time, rand).
  • ulid_to_uuid(s), ulid_from_uuid(u): the same 128 bits in the other text form.

KSUID

  • ksuid_epoch(): 1400000000.
  • ksuid_from(secs, rnd), ksuid_from_epoch(secs, epoch, rnd): 27 base62 characters from a 32-bit time and 16 random bytes.
  • parse_ksuid(s), is_ksuid(s), ksuid_time(s), ksuid_time_epoch(s, epoch).
  • ksuid(): with the clock and random bytes.

nanoid and short ids

  • nanoid_alphabet(): nanoid's 64 URL-safe characters.
  • nanoid_from(rnd, alphabet, size): nanoid's unbiased mask-and-reject algorithm on given bytes.
  • nanoid(), nanoid_custom(alphabet, size), short_id(size): random ids (! rand).

Snowflake ids

  • Snowflake{epoch_ms, time_bits, node_bits, seq_bits}, SnowflakeParts{ms, node, seq}.
  • twitter_layout(), snowflake_layout(epoch_ms, time_bits, node_bits, seq_bits).
  • snowflake_compose(l, p), snowflake_decompose(l, id).
  • SnowflakeGen, snowflake_gen(), snowflake_next(l, g, node, at_ms): a pure generator step that never goes backwards and reports a full sequence with none.

UUIDs

  • uuid_from_bytes(b), uuid_bytes(s): 16 bytes and canonical text.
  • uuid_v4_from(rnd), uuid_v7_from(ms, rnd), uuid_v7_time(s): versions 4 and 7 from given bytes (the standard library's uuid.v4() and uuid.v7() generate them).
  • nil_uuid(), max_uuid().

Out of scope

  • UUID versions 1, 3, 5 and 6 (they need MAC addresses, MD5 or SHA-1, which the standard library does not provide); use version 4 or 7.
  • Hashids and Sqids (reversible obfuscation of integers), CUID2, XID and MongoDB ObjectIds.
  • Base64 and hex, which the standard library's Bytes already has (b.base64, from_hex), and Ascii85/Z85.
  • Coordinating Snowflake node numbers across machines, and waiting for the next millisecond: snowflake_next returns none and the caller decides.
  • Cryptographic tokens and secrets beyond random ids: use crypto.token and crypto.key.

Install

sspur add ids

MIT licensed.

API

base58_alphabet fn

fn base58_alphabet() -> Str

The base58 alphabet of Bitcoin addresses: digits and letters without 0, O, I and l.

base62_alphabet fn

fn base62_alphabet() -> Str

The base62 alphabet: 0-9, A-Z, a-z, in that order (as KSUID and most base62 libraries).

base36_alphabet fn

fn base36_alphabet() -> Str

The base36 alphabet: 0-9 then a-z.

crockford_alphabet fn

fn crockford_alphabet() -> Str

Douglas Crockford's base32 alphabet: 0-9 and the letters without I, L, O and U.

encode_base fn

fn encode_base(b: Bytes, alphabet: Str where _.len >= 2) -> Str

The bytes b written in the base of alphabet (its length), most significant digit first: the bytes are one big-endian number, and every leading zero byte becomes one leading copy of the alphabet's first character, as in Bitcoin's base58, so decode_base gives the same bytes back. Empty bytes give "".

decode_base fn

fn decode_base(s: Str, alphabet: Str where _.len >= 2) -> Opt[Bytes]

The bytes that encode_base wrote as s, or none if s has a character outside the alphabet. Each leading first character is a zero byte.

encode_int fn

fn encode_int(n: Int where _ >= 0, alphabet: Str where _.len >= 2) -> Str

The non-negative integer n in the base of alphabet, without leading zeros (0 is the alphabet's first character).

decode_int fn

fn decode_int(s: Str, alphabet: Str where _.len >= 2) -> Opt[Int]

The integer s spells in the base of alphabet, or none for "", a character outside the alphabet, or a value above the 64-bit maximum. Leading zero digits are allowed.

encode_big fn

fn encode_big(n: BigInt, alphabet: Str where _.len >= 2) -> Str

The non-negative BigInt n in the base of alphabet, without leading zeros.

decode_big fn

fn decode_big(s: Str, alphabet: Str where _.len >= 2) -> Opt[BigInt]

The BigInt s spells in the base of alphabet, or none for "" or a character outside the alphabet.

base58 fn

fn base58(b: Bytes) -> Str

Bitcoin base58 of b (encode_base with base58_alphabet).

from_base58 fn

fn from_base58(s: Str) -> Opt[Bytes]

The bytes of Bitcoin base58 text, or none.

base62 fn

fn base62(b: Bytes) -> Str

Base62 of b (encode_base with base62_alphabet).

from_base62 fn

fn from_base62(s: Str) -> Opt[Bytes]

The bytes of base62 text, or none.

base36 fn

fn base36(b: Bytes) -> Str

Base36 of b (encode_base with base36_alphabet, lower case).

from_base36 fn

fn from_base36(s: Str) -> Opt[Bytes]

The bytes of base36 text, either case, or none.

base32 fn

fn base32(b: Bytes, pad: Bool) -> Str

RFC 4648 base32 of b (A-Z, 2-7), padded with = to a multiple of 8 characters when pad.

from_base32 fn

fn from_base32(s: Str) -> Opt[Bytes]

The bytes of RFC 4648 base32 text, either case, with or without padding (padding, when present, must make the length a multiple of 8). none for other characters, an impossible length, or nonzero unused bits.

crockford fn

fn crockford(n: Int where _ >= 0, check: Bool) -> Str

The non-negative integer n in Crockford base32 (upper case, no leading zeros), followed by the mod 37 check symbol (0-9A-Z without ILOU, then *~$=U) when check.

from_crockford fn

fn from_crockford(s: Str, check: Bool) -> Opt[Int]

The integer of Crockford base32 text: either case, hyphens ignored, I and L read as 1 and O as 0; with check, the last character must be the right check symbol. none for anything else or a value above the 64-bit maximum.

ulid_from fn

fn ulid_from(ms: Int where _ >= 0 and _ <= 281474976710655, rnd: Bytes) -> Str

The ULID of a Unix time in milliseconds and 10 random bytes: 26 Crockford base32 characters, the 48-bit time first, so ULIDs sort by time as text.

parse_ulid fn

fn parse_ulid(s: Str) -> Opt[Bytes]

The 16 bytes of a ULID, or none unless s is 26 Crockford base32 characters (either case, no I, L, O or U) starting with 0 to 7 (higher values exceed 128 bits).

is_ulid fn

fn is_ulid(s: Str) -> Bool

Whether s is a ULID (parse_ulid).

ulid_time fn

fn ulid_time(s: Str) -> Opt[Int]

The Unix time in milliseconds of a ULID, or none if s is not one.

ulid fn

fn ulid() -> Str ! time, rand

A new ULID from the clock and 10 random bytes from the operating system.

UlidGen type

pub type UlidGen = {last_ms: Int, last_rand: Bytes} derive Eq, Show

The state of a monotonic ULID generator: the time and random part of the last ULID.

ulid_gen fn

fn ulid_gen() -> UlidGen

A monotonic generator that has made no ULID yet.

ulid_next fn

fn ulid_next(g: UlidGen, ms: Int where _ >= 0 and _ <= 281474976710655, rnd: Bytes) -> Opt[(Str, UlidGen)]

The next ULID of a monotonic generator and its new state, as the ULID specification describes: a later millisecond uses rnd; the same millisecond, or an earlier one (a clock that went back), keeps the last time and adds 1 to the last random part, so every ULID sorts after the one before. none when the random part would overflow (2^80 ULIDs in one millisecond).

ulid_next_now fn

fn ulid_next_now(g: UlidGen) -> Opt[(Str, UlidGen)] ! time, rand

ulid_next with the clock and fresh random bytes.

ulid_to_uuid fn

fn ulid_to_uuid(s: Str) -> Opt[Str]

The UUID text of the same 128 bits as a ULID, or none if s is not one.

ulid_from_uuid fn

fn ulid_from_uuid(u: Str) -> Opt[Str]

The ULID of the same 128 bits as a UUID, or none if u is not a canonical UUID.

ksuid_epoch fn

fn ksuid_epoch() -> Int

The KSUID epoch, 1400000000 seconds after the Unix epoch (2014-05-13T16:53:20Z).

ksuid_from_epoch fn

fn ksuid_from_epoch(secs: Int, epoch: Int, rnd: Bytes) -> Str

The KSUID of a Unix time in seconds and 16 random bytes, counting time from epoch (Unix seconds): a 32-bit time then the random bytes, as 27 base62 characters that sort by time.

ksuid_from fn

fn ksuid_from(secs: Int, rnd: Bytes) -> Str

The KSUID of a Unix time in seconds (from 2014-05-13 to 2150) and 16 random bytes, with the standard epoch.

parse_ksuid fn

fn parse_ksuid(s: Str) -> Opt[Bytes]

The 20 bytes of a KSUID, or none unless s is 27 base62 characters of at most 160 bits (aWgEPTl1tmebfsQzFP4bxwgy80V is the largest).

is_ksuid fn

fn is_ksuid(s: Str) -> Bool

Whether s is a KSUID (parse_ksuid).

ksuid_time_epoch fn

fn ksuid_time_epoch(s: Str, epoch: Int) -> Opt[Int]

The Unix time in seconds of a KSUID made with epoch, or none if s is not one.

ksuid_time fn

fn ksuid_time(s: Str) -> Opt[Int]

The Unix time in seconds of a KSUID with the standard epoch, or none if s is not one.

ksuid fn

fn ksuid() -> Str ! time, rand

A new KSUID from the clock and 16 random bytes from the operating system.

nanoid_alphabet fn

fn nanoid_alphabet() -> Str

The alphabet of nanoid's default ids: 64 URL-safe characters (A-Za-z0-9_-).

nanoid_from fn

fn nanoid_from(rnd: Bytes, alphabet: Str where _.len >= 2 and _.len <= 256, size: Int where _ >= 0) -> Opt[Str]

The id nanoid's algorithm makes from the random bytes rnd: each byte, masked to the smallest 2^k - 1 that covers the alphabet, picks a character when it falls inside the alphabet and is skipped otherwise, so every character is equally likely. none when rnd runs out before size characters. The bytes are used in order (nanoid's JavaScript reads them from the end).

nanoid_custom fn

fn nanoid_custom(alphabet: Str where _.len >= 2 and _.len <= 256, size: Int where _ >= 0) -> Str ! rand

A random id of size characters from alphabet, by nanoid's unbiased algorithm, with bytes from the operating system. It draws ceil(1.6 * mask * size / len) bytes at a time (nanoid's step) until it has enough; after 64 draws, which happens with a probability below 2^-64, it fills the rest by taking bytes modulo the alphabet length.

nanoid fn

fn nanoid() -> Str ! rand

A nanoid: 21 characters from nanoid_alphabet, about 126 random bits.

short_id fn

fn short_id(size: Int where _ >= 0) -> Str ! rand

A random id of size letters and digits (base62_alphabet), for ids that must not contain - or _. Eight characters carry about 47 bits; use 22 or more for unguessable ids.

Snowflake type

pub type Snowflake = {epoch_ms: Int, time_bits: Int, node_bits: Int, seq_bits: Int} derive Eq, Show

The layout of Snowflake-style 64-bit ids: milliseconds since epoch_ms in the top time_bits, then a node (machine or shard) number in node_bits, then a per-millisecond sequence in seq_bits; the three add up to 63 so ids are positive Ints.

SnowflakeParts type

pub type SnowflakeParts = {ms: Int, node: Int, seq: Int} derive Eq, Show

The parts of a Snowflake id: Unix time in milliseconds, node and sequence.

twitter_layout fn

fn twitter_layout() -> Snowflake

Twitter's layout: epoch 1288834974657 (2010-11-04), 41 time bits, 10 node bits, 12 sequence bits.

snowflake_layout fn

fn snowflake_layout(epoch_ms: Int, time_bits: Int, node_bits: Int, seq_bits: Int) -> Opt[Snowflake]

A layout with an epoch of your own and the usual 41, 10 and 12 bits; none unless the bit counts are non-negative and add up to 63.

snowflake_compose fn

fn snowflake_compose(l: Snowflake, p: SnowflakeParts) -> Opt[Int]

The id of p in layout l, or none when the time is before the epoch or any part does not fit its bits.

snowflake_decompose fn

fn snowflake_decompose(l: Snowflake, id: Int where _ >= 0) -> SnowflakeParts

The parts of the id id in layout l (the time as Unix milliseconds).

SnowflakeGen type

pub type SnowflakeGen = {last_ms: Int, last_seq: Int} derive Eq, Show

The state of a Snowflake generator: the millisecond and sequence of its last id.

snowflake_gen fn

fn snowflake_gen() -> SnowflakeGen

A Snowflake generator that has made no id yet.

snowflake_next fn

fn snowflake_next(l: Snowflake, g: SnowflakeGen, node: Int, at_ms: Int) -> Opt[(Int, SnowflakeGen)]

The next id of node node at Unix time at_ms, and the new state: a later millisecond starts the sequence at 0; the same one, or an earlier one (a clock that went back), keeps the last time and adds 1 to the sequence, so ids only grow. none when the sequence is full (wait for the next millisecond and call again) or a part does not fit the layout.

uuid_from_bytes fn

fn uuid_from_bytes(b: Bytes) -> Opt[Str]

The canonical lower-case text of 16 bytes as a UUID, or none for another length.

uuid_bytes fn

fn uuid_bytes(s: Str) -> Opt[Bytes]

The 16 bytes of a canonical UUID (8-4-4-4-12 hex digits, either case), or none.

uuid_v4_from fn

fn uuid_v4_from(rnd: Bytes) -> Str

The version 4 (random) UUID of 16 random bytes: their version bits set to 4 and variant bits to RFC 9562's, as uuid.v4() does with bytes from the operating system.

uuid_v7_from fn

fn uuid_v7_from(ms: Int where _ >= 0 and _ <= 281474976710655, rnd: Bytes) -> Str

The version 7 (time-ordered) UUID of a Unix time in milliseconds and 10 random bytes: the 48-bit time, version 7, 12 random bits, the variant, 62 random bits (RFC 9562 section 5.7). The first random byte gives its low 4 bits and the third its low 6.

uuid_v7_time fn

fn uuid_v7_time(s: Str) -> Opt[Int]

The Unix time in milliseconds of a version 7 UUID, or none if s is not one.

nil_uuid fn

fn nil_uuid() -> Str

The nil UUID, all zeros.

max_uuid fn

fn max_uuid() -> Str

The max UUID, all ones.

Versions

VersionPublishedCommitSourceEffects
0.1.0 2026-10-16645997df14snapshot