base58_alphabet fn
fn base58_alphabet() -> StrThe base58 alphabet of Bitcoin addresses: digits and letters without 0, O, I and l.
Package
Identifiers and encodings: ULID, KSUID, nanoid, Snowflake, UUID v4/v7 from bytes, base58, base62, base36, base32 and Crockford
0.1.0 MIT
sspur add ids@^0.1.0or in sspur.toml:
ids = { version = "^0.1.0" }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.
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
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_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_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_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{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.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().Bytes already has (b.base64, from_hex), and Ascii85/Z85.snowflake_next returns none and the caller decides.crypto.token and crypto.key.sspur add ids
MIT licensed.
fn base58_alphabet() -> StrThe base58 alphabet of Bitcoin addresses: digits and letters without 0, O, I and l.
fn base62_alphabet() -> StrThe base62 alphabet: 0-9, A-Z, a-z, in that order (as KSUID and most base62 libraries).
fn base36_alphabet() -> StrThe base36 alphabet: 0-9 then a-z.
fn crockford_alphabet() -> StrDouglas Crockford's base32 alphabet: 0-9 and the letters without I, L, O and U.
fn encode_base(b: Bytes, alphabet: Str where _.len >= 2) -> StrThe 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 "".
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.
fn encode_int(n: Int where _ >= 0, alphabet: Str where _.len >= 2) -> StrThe non-negative integer n in the base of alphabet, without leading zeros (0 is the alphabet's first character).
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.
fn encode_big(n: BigInt, alphabet: Str where _.len >= 2) -> StrThe non-negative BigInt n in the base of alphabet, without leading zeros.
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.
fn base58(b: Bytes) -> StrBitcoin base58 of b (encode_base with base58_alphabet).
fn from_base58(s: Str) -> Opt[Bytes]The bytes of Bitcoin base58 text, or none.
fn base62(b: Bytes) -> StrBase62 of b (encode_base with base62_alphabet).
fn from_base62(s: Str) -> Opt[Bytes]The bytes of base62 text, or none.
fn base36(b: Bytes) -> StrBase36 of b (encode_base with base36_alphabet, lower case).
fn from_base36(s: Str) -> Opt[Bytes]The bytes of base36 text, either case, or none.
fn base32(b: Bytes, pad: Bool) -> StrRFC 4648 base32 of b (A-Z, 2-7), padded with = to a multiple of 8 characters when pad.
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.
fn crockford(n: Int where _ >= 0, check: Bool) -> StrThe 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.
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.
fn ulid_from(ms: Int where _ >= 0 and _ <= 281474976710655, rnd: Bytes) -> StrThe 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.
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).
fn is_ulid(s: Str) -> BoolWhether s is a ULID (parse_ulid).
fn ulid_time(s: Str) -> Opt[Int]The Unix time in milliseconds of a ULID, or none if s is not one.
fn ulid() -> Str ! time, randA new ULID from the clock and 10 random bytes from the operating system.
pub type UlidGen = {last_ms: Int, last_rand: Bytes} derive Eq, ShowThe state of a monotonic ULID generator: the time and random part of the last ULID.
fn ulid_gen() -> UlidGenA monotonic generator that has made no ULID yet.
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).
fn ulid_next_now(g: UlidGen) -> Opt[(Str, UlidGen)] ! time, randulid_next with the clock and fresh random bytes.
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.
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.
fn ksuid_epoch() -> IntThe KSUID epoch, 1400000000 seconds after the Unix epoch (2014-05-13T16:53:20Z).
fn ksuid_from_epoch(secs: Int, epoch: Int, rnd: Bytes) -> StrThe 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.
fn ksuid_from(secs: Int, rnd: Bytes) -> StrThe KSUID of a Unix time in seconds (from 2014-05-13 to 2150) and 16 random bytes, with the standard epoch.
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).
fn is_ksuid(s: Str) -> BoolWhether s is a KSUID (parse_ksuid).
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.
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.
fn ksuid() -> Str ! time, randA new KSUID from the clock and 16 random bytes from the operating system.
fn nanoid_alphabet() -> StrThe alphabet of nanoid's default ids: 64 URL-safe characters (A-Za-z0-9_-).
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).
fn nanoid_custom(alphabet: Str where _.len >= 2 and _.len <= 256, size: Int where _ >= 0) -> Str ! randA 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.
fn nanoid() -> Str ! randA nanoid: 21 characters from nanoid_alphabet, about 126 random bits.
fn short_id(size: Int where _ >= 0) -> Str ! randA 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.
pub type Snowflake = {epoch_ms: Int, time_bits: Int, node_bits: Int, seq_bits: Int} derive Eq, ShowThe 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.
pub type SnowflakeParts = {ms: Int, node: Int, seq: Int} derive Eq, ShowThe parts of a Snowflake id: Unix time in milliseconds, node and sequence.
fn twitter_layout() -> SnowflakeTwitter's layout: epoch 1288834974657 (2010-11-04), 41 time bits, 10 node bits, 12 sequence bits.
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.
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.
fn snowflake_decompose(l: Snowflake, id: Int where _ >= 0) -> SnowflakePartsThe parts of the id id in layout l (the time as Unix milliseconds).
pub type SnowflakeGen = {last_ms: Int, last_seq: Int} derive Eq, ShowThe state of a Snowflake generator: the millisecond and sequence of its last id.
fn snowflake_gen() -> SnowflakeGenA Snowflake generator that has made no id yet.
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.
fn uuid_from_bytes(b: Bytes) -> Opt[Str]The canonical lower-case text of 16 bytes as a UUID, or none for another length.
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.
fn uuid_v4_from(rnd: Bytes) -> StrThe 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.
fn uuid_v7_from(ms: Int where _ >= 0 and _ <= 281474976710655, rnd: Bytes) -> StrThe 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.
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.
fn nil_uuid() -> StrThe nil UUID, all zeros.
fn max_uuid() -> StrThe max UUID, all ones.
| Version | Published | Commit | Source | Effects |
|---|---|---|---|---|
| 0.1.0 | 2026-10-16 | 645997df14 | snapshot |