A .NET library for sequential, sortable base-N (radix) encoding over a custom alphabet. It maps non-negative integers to left-padded, monotonically increasing codes and back - for serial numbers, license keys, and sortable short IDs.
Targets net10.0 and netstandard2.0. The net10.0 build adds a
FrozenDictionary lookup, AOT/trim compatibility, and a zero-allocation
TryEncode(Span<char>).
NOTE: Codes are predictable and reversible: the next code is trivially guessable and any code decodes straight back to its number. Don't use this where you need unguessable or non-enumerable IDs (use a GUID) or obfuscated IDs that hide the underlying sequence (use Hashids/Sqids). It also doesn't generate uniqueness; feed it unique numbers (e.g. a database sequence) and the codes are unique; feed it duplicates and they will collide.
dotnet add package SequentialRadixCodec
| Codec | Alphabet | Min width |
|---|---|---|
RadixCodec.Base10 |
0-9 |
5 |
RadixCodec.Base26 |
A-Z |
5 |
RadixCodec.Base36 |
0-9A-Z |
5 |
using SequentialRadixCodec;
string code = RadixCodec.Base26.Encode(1001); // "AABMN"
// Pick the return width you want (no BigInteger required):
int i = RadixCodec.Base26.DecodeInt32("AABMN"); // 1001
long l = RadixCodec.Base26.DecodeInt64("AABMN"); // 1001
BigInteger b = RadixCodec.Base26.DecodeBigInteger("AABMN"); // 1001
// Non-throwing decode (false on null/empty/invalid/out-of-range):
if (RadixCodec.Base26.TryDecodeInt64(userInput, out long value)) { }DecodeInt32 / DecodeInt64 throw OverflowException if the decoded value exceeds
int.MaxValue / long.MaxValue; the TryDecode* variants return false instead.
DecodeBigInteger never overflows, so values beyond int/long round-trip cleanly.
var codec = new RadixCodec("02357ABC"); // first char '0' is the pad digit
string code = codec.Encode(1001); // "2CA2"
BigInteger n = codec.DecodeBigInteger(code); // 1001Custom codecs default to minLength: 1 (no padding). Pass a minLength to pad.
The minimum width left-pads with alphabet[0]; a longer natural encoding is never
truncated. Override per call:
var codec = new RadixCodec("ABCDEFGHIJKLMNOPQRSTUVWXYZ", minLength: 4);
codec.Encode(1001); // "ABMN"
// Override call
codec.Encode(1001, 8); // "AAAAABMN"// Next 100 Base26 codes after a stored value:
BigInteger last = RadixCodec.Base26.DecodeBigInteger("AABMM");
foreach (var code in RadixSequence.From(RadixCodec.Base26, start: last + 1, count: 100))
Console.Write($"{code} ");
// Unbounded + LINQ:
var page = RadixSequence.From(RadixCodec.Base10).Skip(1000).Take(50);Stream consecutive codes into a reused buffer — no per-item string allocation, no
BigInteger. Current is valid only until the next iteration.
// Caller owns the buffer (zero heap allocation):
Span<char> buf = stackalloc char[16];
foreach (ReadOnlySpan<char> code in RadixSequence.EnumerateInto(RadixCodec.Base26, buf, start: 1001, count: 100))
{
// Do something with "code"
}
// Pooled buffer (no sizing needed; foreach returns it to the pool):
foreach (ReadOnlySpan<char> code in RadixSequence.Enumerate(RadixCodec.Base36, start: 10_000_000_000))
{
// Do Something with "code"
}start/count are long (count omitted = unbounded). For EnumerateInto, a bounded
range whose largest code cannot fit the buffer throws ArgumentException up front; an
unbounded range stops when the next code no longer fits.
Span<char> buffer = stackalloc char[16];
if (RadixCodec.Base36.TryEncode(value, buffer, out int written))
{
// Do something with "buffer[..written];"
}