lib/base58.ex

defmodule TUID.Base58 do
  @moduledoc """
  `Base58` provides heper functions to encode and decode base58 encoded strings

  https://en.wikipedia.org/wiki/Binary-to-text_encoding#Base58


  credit to blog post:
  https://danschultzer.com/posts/prefixed-base62-uuidv7-object-ids-with-ecto

  """

  @b58_char_list ~c"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

  for {digit, idx} <- Enum.with_index(@b58_char_list) do
    def encode(unquote(idx)), do: unquote(<<digit>>)
  end

  def encode(number) do
    encode(div(number, unquote(length(@b58_char_list)))) <>
      encode(rem(number, unquote(length(@b58_char_list))))
  end

  def decode(string) do
    string
    |> String.split("", trim: true)
    |> Enum.reverse()
    |> Enum.reduce_while({:ok, {0, 0}}, fn char, {:ok, {acc, step}} ->
      case decode_base58_char(char) do
        {:ok, number} ->
          {:cont,
           {:ok, {acc + number * Integer.pow(unquote(length(@b58_char_list)), step), step + 1}}}

        {:error, error} ->
          {:halt, {:error, error}}
      end
    end)
    |> case do
      {:ok, {number, _step}} -> {:ok, number}
      {:error, error} -> {:error, error}
    end
  end

  for {digit, idx} <- Enum.with_index(@b58_char_list) do
    defp decode_base58_char(unquote(<<digit>>)), do: {:ok, unquote(idx)}
  end

  defp decode_base58_char(char), do: {:error, "got invalid base58 character; #{inspect(char)}"}

  @base58_uuid_length 21
  @uuid_length 32

  def encode_uuid(uuid) do
    uuid
    |> String.replace("-", "")
    |> String.to_integer(16)
    |> encode()
    |> String.pad_leading(@base58_uuid_length, "1")
  end

  def decode_uuid(string) do
    with {:ok, number} <- decode(string) do
      number_to_uuid(number)
    end
  end

  defp number_to_uuid(number) do
    number
    |> Integer.to_string(16)
    |> String.downcase()
    |> String.pad_leading(@uuid_length, "0")
    |> case do
      <<g1::binary-size(8), g2::binary-size(4), g3::binary-size(4), g4::binary-size(4),
        g5::binary-size(12)>> ->
        {:ok, "#{g1}-#{g2}-#{g3}-#{g4}-#{g5}"}

      other ->
        {:error, "got invalid base58 uuid; #{inspect(other)}"}
    end
  end
end