yabase/intid
Integer helpers for short URL-safe identifiers.
The byte-oriented codecs in yabase/facade are the right tool when
the input is opaque bytes (hashes, public keys, raw payloads). For
the very common short-ID case — DB autoincrement ids, sequence
numbers, hash truncations — callers want Int -> compact string
directly. Without these helpers every project re-implements the
same Int -> big-endian bytes -> trim-leading-zero shim.
encode_int_* emits canonical form: no leading zero characters
beyond what the value itself requires (encode_int_base58(0) == "1", the alphabet’s zero character; encode_int_base58(58) == "21", no leading "1").
decode_int_* is tolerant of leading zero characters
(decode_int_base58("0042") and decode_int_base58("42") both
return the same Int), so input from external sources that
zero-pads is accepted without ceremony.
decode_int_* rejects the empty string with
Error(InvalidLength(0)) rather than treating it as zero. Callers
can therefore distinguish “no ID was supplied” from “the ID is
zero” — important for URL routing, form parsing, and database
lookups. The byte-oriented decoders in yabase/facade retain the
Ok(<<>>) round-trip behavior for empty input.
Negative inputs are silently absolutized
Every encode_int_* function in this module accepts any Int
— including negatives — and normalizes the input to
int.absolute_value before encoding. The magnitude is what gets
stored. The decode side always returns a non-negative Int.
This is intentional, not a bug, but it is a footgun. Two
distinct inputs (-1 and 1) round-trip to the same Int,
breaking bijection. Code that compares a re-encoded value to its
source can match where you would expect a mismatch. If your
caller path can produce negative values (offsets that subtracted
past zero, Posix timestamps from before 1970, deliberate
-1 sentinels), the safer pattern is to validate the sign at
the boundary before reaching encode_int_*:
case n >= 0 {
True -> Ok(encode_int_base32_crockford(n))
False -> Error(MyDomainError.NegativeId(n))
}
We intentionally chose silent absolutization over Result /
panic for ergonomics — almost every realistic short-ID caller
reaches encode_int_* with a value that is already non-negative
(DB autoincrement, hash truncation, sequence number), and forcing
Result everywhere added boilerplate without preventing real
bugs. Tracked in #84.
Bounded decode
decode_int_* accepts inputs of any length, so the decoded
Int can exceed any fixed integer width — Erlang Int is a
bignum. Realistic backing stores cap IDs at 64 bits (SQLite
INTEGER, Postgres bigserial, MySQL BIGINT), so feeding an
unbounded decode_int_* result into one of those columns
crashes the driver as soon as a user supplies a slightly-too-long
string. For the same reason, JavaScript-target callers cap at
53 bits (Number.MAX_SAFE_INTEGER).
Use decode_int_*_bounded(input:, max:) whenever the decoded value
flows into a fixed-width sink. The bounded variants return
Error(Overflow) if the decoded Int exceeds max. Common caps
are exported as int64_max (signed 64-bit, 2^63 - 1) and
int53_max (JS-safe integer, 2^53 - 1).
Types
Issue #74: every decode_int_* function in this module returns
Result(Int, CodecError). Without this re-export, callers who only
import yabase/intid cannot type-annotate a wrapper around a
decode call without reaching into yabase/core/error — a module
the README does not mention. The alias keeps the type identity
(it’s the same CodecError the underlying codec functions
already use) so error values flow through unchanged.
pub type CodecError =
error.CodecError
Values
pub fn decode_int(
encoding encoding: encoding.Encoding,
value value: String,
) -> Result(Int, error.CodecError)
Decode a string back to an Int using the supplied Encoding,
dispatching to the matching decode_int_* helper.
Empty input returns Error(InvalidLength(0)) so callers can
distinguish “no ID was supplied” from “the ID is zero” — the
same contract as the per-base helpers.
Returns Error(UnsupportedForInt(name)) for encodings that have
no integer codec wired up.
pub fn decode_int_base10(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base10 (decimal) string back to an Int.
pub fn decode_int_base10_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base10 (decimal) string back to an Int, rejecting
values greater than max with Error(Overflow).
pub fn decode_int_base16(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base16 (hexadecimal) string back to an Int. Accepts
both uppercase and lowercase input via base16.decode’s
case-insensitive alphabet.
pub fn decode_int_base16_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base16 (hexadecimal) string back to an Int, rejecting
values greater than max with Error(Overflow).
pub fn decode_int_base32_crockford(
input: String,
) -> Result(Int, error.CodecError)
Decode a Crockford Base32 string back to an Int.
pub fn decode_int_base32_crockford_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Crockford Base32 string back to an Int, rejecting
values greater than max with Error(Overflow).
pub fn decode_int_base32_crockford_check(
input: String,
) -> Result(Int, error.CodecError)
Decode a checksummed Crockford Base32 string back to an Int,
verifying the trailing check symbol.
pub fn decode_int_base32_crockford_check_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a checksummed Crockford Base32 string back to an Int,
rejecting values greater than max with Error(Overflow).
pub fn decode_int_base32_rfc4648(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base32 (RFC 4648) string back to an Int.
pub fn decode_int_base32_rfc4648_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base32 (RFC 4648) string back to an Int, rejecting
values greater than max with Error(Overflow).
pub fn decode_int_base36(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base36 string back to an Int.
pub fn decode_int_base36_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base36 string back to an Int, rejecting values
greater than max with Error(Overflow).
pub fn decode_int_base58(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base58 (Bitcoin alphabet) string back to an Int.
pub fn decode_int_base58_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base58 (Bitcoin alphabet) string back to an Int,
rejecting values greater than max with Error(Overflow).
pub fn decode_int_base58_flickr(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base58 (Flickr alphabet) string back to an Int.
pub fn decode_int_base58_flickr_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base58 (Flickr alphabet) string back to an Int,
rejecting values greater than max with Error(Overflow).
pub fn decode_int_base58check(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base58Check string back to an Int, verifying the
4-byte SHA-256 checksum.
Issue #73: returns the payload as an Int, ignoring the version
byte (which encode_int_base58check always sets to 0).
Callers that need to inspect the version byte should reach for
yabase/base58check.decode/1 directly.
pub fn decode_int_base58check_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base58Check string back to an Int, rejecting payload
values greater than max with Error(Overflow). The checksum is
verified before the bounds check, so a corrupted input fails as
InvalidChecksum rather than Overflow.
pub fn decode_int_base62(
input: String,
) -> Result(Int, error.CodecError)
Decode a Base62 string back to an Int.
pub fn decode_int_base62_bounded(
input input: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a Base62 string back to an Int, rejecting values
greater than max with Error(Overflow).
pub fn decode_int_bounded(
encoding encoding: encoding.Encoding,
value value: String,
max max: Int,
) -> Result(Int, error.CodecError)
Decode a string back to an Int using the supplied Encoding,
rejecting values greater than max with Error(Overflow). The
runtime sibling of the per-base decode_int_*_bounded helpers.
pub fn encode_int(
encoding encoding: encoding.Encoding,
value value: Int,
) -> Result(String, error.CodecError)
Encode an Int to a string using the supplied Encoding,
dispatching to the matching encode_int_* helper. Negative
inputs are normalised to int.absolute_value exactly as the
per-base helpers do; see the module note on “Negative inputs
are silently absolutized” for the rationale and the
boundary-check pattern.
Returns Error(UnsupportedForInt(name)) for encodings that have
no integer codec wired up (every byte-only codec: Base2,
Base8, Base32(Hex|Clockwork|ZBase32), Base45, every
Base64 / Base85 variant, Base91, Bech32). For
Base58Check, the existing encode_int_base58check/1 helper
uses the fixed version byte 0x00; reach for the generic
facade with encoding.base58_check(version) to pin a different
version.
pub fn encode_int_base10(value: Int) -> String
Encode an Int as a Base10 (decimal) string. Negative inputs
are normalized to int.absolute_value; see the module note on
“Negative inputs are silently absolutized”.
Behaviour matches int.to_string for the typical case
(positive integers) and the rest of the intid family for the
switch-case bench harnesses described in #78. Routing through
base10.encode keeps the contract uniform with the other
encode_int_* functions: a non-negative Int in, a string
in the alphabet out, no padding.
pub fn encode_int_base16(value: Int) -> String
Encode an Int as a Base16 (uppercase hexadecimal) string.
Negative inputs are normalized to int.absolute_value; see the
module note on “Negative inputs are silently absolutized”.
Routing through base16.encode keeps the contract uniform with
the rest of the encode_int_* family. The output uses the
canonical RFC 4648 §8 uppercase alphabet (0-9 A-F) — callers
who need lowercase for interop with sha256sum-style tools can
post-process with string.lowercase or use base16.encode_lowercase
after int_to_bytes_be themselves.
pub fn encode_int_base32_crockford(value: Int) -> String
Encode an Int as a Crockford Base32 string. Negative inputs
are normalized to int.absolute_value; see the module note on
“Negative inputs are silently absolutized”.
pub fn encode_int_base32_crockford_check(value: Int) -> String
Encode a non-negative Int as a Crockford Base32 string with a
trailing checksum symbol (Douglas Crockford’s optional check
character).
Issue #73: same shape as encode_int_base32_crockford but with
the typo-resistance guard the underlying codec already supports.
Use the matching decode_int_base32_crockford_check to recover
the integer; the decoder verifies the symbol and returns
Error(InvalidChecksum) if the input was mistyped.
pub fn encode_int_base32_rfc4648(value: Int) -> String
Encode an Int as a Base32 (RFC 4648) string. Negative inputs
are normalized to int.absolute_value; see the module note on
“Negative inputs are silently absolutized” for the rationale and
the recommended boundary-check pattern.
pub fn encode_int_base36(value: Int) -> String
Encode an Int as a Base36 string. Negative inputs are
normalized to int.absolute_value; see the module note on
“Negative inputs are silently absolutized”.
pub fn encode_int_base58(value: Int) -> String
Encode an Int as a Base58 (Bitcoin alphabet) string. Negative
inputs are normalized to int.absolute_value; see the module
note on “Negative inputs are silently absolutized”.
pub fn encode_int_base58_flickr(value: Int) -> String
Encode an Int as a Base58 (Flickr alphabet) string. Negative
inputs are normalized to int.absolute_value; see the module
note on “Negative inputs are silently absolutized”.
pub fn encode_int_base58check(value: Int) -> String
Encode a non-negative Int as a Base58Check string (Bitcoin’s
double-SHA-256 checksum format).
Issue #73: this is the int-typed counterpart of
yabase/base58check.encode/2. Version is fixed at 0
(Bitcoin mainnet P2PKH) — callers that need a different version
should reach for yabase/base58check.encode/2 directly with their
own BitArray payload.
Returns the canonical Base58Check string. The underlying
yabase/base58check.encode only errors on out-of-range version
bytes (this helper hard-codes a valid one), so this signature
does not surface a Result.
pub fn encode_int_base62(value: Int) -> String
Encode an Int as a Base62 string. Negative inputs are
normalized to int.absolute_value; see the module note on
“Negative inputs are silently absolutized”.
pub const int53_max: Int
Largest value that round-trips losslessly through a JavaScript
number (2^53 - 1, Number.MAX_SAFE_INTEGER). Use as the
max argument to decode_int_*_bounded when the decoded value
is passed across a JS-target boundary or serialized as JSON for
a JavaScript consumer.