Header menu logo ananoid

How-To: Define a Custom Alphabet

The default settings for creating a NanoId (21 characters taken from a mix of letters, numbers, hyphen, and underscore) reflect a reasonable balance of entropy versus performance. Further the additional alphabets shipped with Ananoid cover a wide range of common needs. But it is possible to go further. Consumers can define their own alphabets.

Learning about alphabets

Conceptually, an 'alphabet' is a set of 'letters' (technically, single-byte characters) from which a NanoId is constituted. In practice, an Alphabet instance represents a valildated set of letters. Specifically, an Alphabet is safe to use for the generation and parsing of nano identifiers because it upholds the following invariants:

These are not the most challenging invariants, and any set of letters which conforms to them can be validated as an Alphabet. For example, one could define an alphabet consisting entirely of upper case ASCII letters:

F#
let uppercase = Alphabet.Validate("ABCDEFGHIJKLMNOPQRSTUVWXYZ")

printfn $"Is alphabet valid? %b{Result.isOk uppercase}"
VB
Dim uppercase = Alphabet.Validate("ABCDEFGHIJKLMNOPQRSTUVWXYZ")

WriteLine($"Is alphabet valid? {uppercase.IsOk}")
C#
var uppercase = Alphabet.Validate("ABCDEFGHIJKLMNOPQRSTUVWXYZ");

WriteLine($"Is alphabet valid? {uppercase.IsOk}");
OUT
> dotnet fsi ~/scratches/definecustom.fsx

Is alphabet valid? true

Dealing with failures

Not all letter sets will be valid. When validation fails, Ananoid provides the InvalidAlphabet type, which provides details about why, exactly, a given set of letters is not valid.

F#
open AlphabetPatterns // ⮜⮜⮜ contains the `(|Letters|)` active pattern

match Alphabet.ofLetters String.Empty with
| Ok valid -> printfn $"%s{valid.Letters} are valid."

| Error(Letters invalid) when 255 < String.length invalid -> printfn "Too large: '%s{invalid}'!"
| Error(Letters invalid) when String.length invalid < 1 -> printfn "Too small: '%s{invalid}'!"
VB
Dim checked = String.Empty.ToAlphabet()

If checked.IsOk Then
  Dim alphabet = checked.ResultValue
  WriteLine($"{alphabet.Letters} are valid.")
Else
  Dim [error] = checked.ErrorValue
  Select True
    Case 255 < [error].Letters.Length
      WriteLine($"Too large: '{[error].Letters}'!")

    Case [error].Letters.Length < 1
      WriteLine($"Too small: '{[error].Letters}'!")

    Case Else
      Throw New UnreachableException()
  End Select
End If
C#
var @checked = string.Empty.ToAlphabet();

var message = @checked switch
{
  { IsOk: true, ResultValue: var alphabet } => $"{alphabet.Letters} are valid.",

  { ErrorValue: { Letters: var letters} } when 255 < letters.Length => $"Too large: '{letters}'!",
  { ErrorValue: { Letters: var letters} } when letters.Length < 1 => $"Too small: '{letters}'!",

  _ => throw new UnreachableException()
};

WriteLine(message);
OUT
> dotnet fsi ~/scratches/definecustom.fsx

Too small: ''!

However, sometimes, processing failures is uncessary (or, at least, unwanted). In those cases, Ananoid has helper methods which reduce success-or-failure to a boolean condition. Consider the following:

F#
match String.Empty.TryMakeAlphabet() with
| (true, alphabet) -> printfn $"%s{alphabet.Letters} are valid."
| (false, _) -> printfn "Too small: ''!"
VB
Dim alphabet As Alphabet = Nothing
Dim isOkay = String.Empty.TryMakeAlphabet(alphabet)
If Not isOkay AndAlso alphabet Is Nothing Then
  WriteLine("Too small: ''!")
Else
  WriteLine($"{alphabet.Letters} are valid.")
End If
C#
if ("".TryMakeAlphabet(out var alphabet))
{
  Console.WriteLine($"{alphabet.Letters} are valid.");
}
else
{
  Console.WriteLine("Too small: ''!");
}
OUT
> dotnet fsi ~/scratches/definecustom.fsx

Too small: ''!

Further, Ananoid can escalate failures by raising a ArgumentOutOfRangeException, which surfaces details from an InvalidAlphabet while also halting program flow and capturing a stack trace. This is shown in the following example:

F#
try
  let alphabet = Alphabet.makeOrRaise ("$" |> String.replicate 800)
  printfn $"%s{alphabet.Letters} are valid."
with
| :? ArgumentOutOfRangeException as x -> printfn $"FAIL! %s{x.Message}"
VB
Try
  Dim letters = New String("$"c, 800)
  Dim alphabet = letters.ToAlphabetOrThrow()
  WriteLine($"{alphabet.Letters} are valid.")
Catch x As ArgumentOutOfRangeException
  WriteLine($"FAIL! {x.Message}")
End Try
C#
try
{
  var alphabet = new String('$', 300).ToAlphabetOrThrow();
  WriteLine($"{alphabet.Letters} are valid.");
}
catch (ArgumentOutOfRangeException x)
{
  WriteLine($"FAIL! {x.Message}");
}
OUT
> dotnet fsi ~/scratches/definecustom.fsx

FAIL! must be between 1 and 255 letters (Parameter 'letters')

Related Reading

Copyright

The library is available under the Mozilla Public License, Version 2.0. For more information see the project's License file.

val uppercase: Result<obj,obj>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val isOk: result: Result<'T,'Error> -> bool
module String from Microsoft.FSharp.Core
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val length: str: string -> int
val replicate: count: int -> str: string -> string

Type something to start searching.