defmodule ShortUUID do @moduledoc """ ShortUUID is a simple UUID shortener library. ## Installation Add ShortUUID to your list of dependencies in `mix.exs`: def deps do [{:shortuuid, "~> #{ShortUUID.Mixfile.project()[:version]}"}] end ShortUUID.encode/1 will translate UUIDs to base57 using lowercase and uppercase letters and digits while avoiding similar-looking characters such as l, 1, I, O and 0. ## Typical usage Use ShortUUID to shorten UUID strings generated by other libraries such as [Ecto](https://hexdocs.pm/ecto/Ecto.UUID.html), [Elixir UUID](https://github.com/zyro/elixir-uuid) and [Erlang UUID](https://github.com/okeuday/uuid). ## Notes The output is padded to a length of 22 characters, the padding character is "2", the first character of the alphabet. iex> ShortUUID.encode!("00000000-0000-0000-0000-000000000000") "2222222222222222222222" iex> ShortUUID.encode!("00000001-0001-0001-0001-000000000001") "UD6ibhr3V4YXvriP822222" iex> ShortUUID.decode!("UD6ibhr3V4YXvriP822222") "00000001-0001-0001-0001-000000000001" If we didn't do the padding the nil UUID for example would simply encode into an empty string since it's int value is 0. Some supported UUID input formats are: * `2a162ee5-02f4-4701-9e87-72762cbce5e2` * `2a162ee502f447019e8772762cbce5e2` * `{2a162ee5-02f4-4701-9e87-72762cbce5e2}` * `{2a162ee502f447019e8772762cbce5e2}` Letter case is not relevant. ## Acknowledgments This project was inspired by [skorokithakis/shortuuid](https://github.com/skorokithakis/shortuuid). """ @alphabet "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" @alphabet_tuple @alphabet |> String.split("", trim: true) |> List.to_tuple() @char_to_int_map @alphabet |> String.split("", trim: true) |> Enum.with_index() |> Enum.reduce(%{}, fn {char, idx}, acc -> Map.put_new(acc, char, idx) end) @alphabet_length 57 @padding_length 22 @padding_char "2" @only_alphabet ~r/^[#{@alphabet}]*$/ @doc """ Encode a UUID to ShortUUID. ## Examples iex> ShortUUID.encode("2a162ee5-02f4-4701-9e87-72762cbce5e2") {:ok, "keATfB8JP2ggT7U9JZrpV9"} """ @spec encode(String.t()) :: {:ok, String.t()} | {:error, String.t()} def encode(uuid) do stripped_uuid = strip_uuid(uuid) case stripped_uuid |> String.length() == 32 do true -> encoded_uuid = stripped_uuid |> String.to_integer(16) |> int_to_string |> pad_shortuuid {:ok, encoded_uuid} _anything_else -> {:error, "Invalid UUID"} end end @doc """ Encode a UUID to ShortUUID. Similar to `encode/1` but raises an ArgumentError if it cannot process the UUID. ## Examples iex> ShortUUID.encode!("2a162ee5-02f4-4701-9e87-72762cbce5e2") "keATfB8JP2ggT7U9JZrpV9" """ @spec encode!(String.t()) :: String.t() def encode!(uuid) do case encode(uuid) do {:ok, encoded_uuid} -> encoded_uuid {:error, msg} -> raise ArgumentError, message: msg end end @doc """ Decode a ShortUUID. ## Examples iex> ShortUUID.decode("keATfB8JP2ggT7U9JZrpV9") {:ok, "2a162ee5-02f4-4701-9e87-72762cbce5e2"} """ @spec decode(String.t()) :: {:ok, String.t()} | {:error, String.t()} def decode(string) when is_binary(string) do case string |> matches_alphabet? do true -> string_to_int(string) |> int_to_hex_string |> pad_uuid |> format_uuid_string false -> {:error, "Invalid input"} end end @doc """ Decode a ShortUUID. Similar to `decode/1` but raises an ArgumentError if the encoded UUID is invalid. ## Examples iex> ShortUUID.decode!("keATfB8JP2ggT7U9JZrpV9") "2a162ee5-02f4-4701-9e87-72762cbce5e2" """ @spec decode!(String.t()) :: String.t() def decode!(string) do case decode(string) do {:ok, uuid} -> uuid {:error, message} -> raise ArgumentError, message: message end end defp int_to_hex_string(number) do number |> to_hex_string |> String.downcase() end @spec divmod(Integer.t(), Integer.t()) :: [Integer.t()] defp divmod(dividend, divisor) do [div(dividend, divisor), rem(dividend, divisor)] end @spec int_to_string(Integer.t(), list()) :: [String.t()] defp int_to_string(number, acc \\ []) defp int_to_string(number, acc) when number > 0 do [result, remainder] = divmod(number, @alphabet_length) int_to_string(result, [acc | elem(@alphabet_tuple, remainder)]) end defp int_to_string(0, acc), do: acc |> to_string defp strip_uuid(uuid) do uuid |> String.downcase() |> String.replace(~r/[^0-9a-f]/, "") end defp matches_alphabet?(string) do Regex.match?(@only_alphabet, string) end defp string_to_int(string) do string |> String.split("", trim: true) |> Enum.reverse() |> Enum.reduce(0, fn char, acc -> %{^char => index} = @char_to_int_map acc * @alphabet_length + index end) end defp to_hex_string(number) do number |> Integer.to_string(16) end defp pad_shortuuid(encoded_uuid) do encoded_uuid |> String.pad_trailing(@padding_length, @padding_char) end defp pad_uuid(string) do string |> String.pad_leading(32, "0") end defp format_uuid_string(<>) do {:ok, <>} end defp format_uuid_string(_invalid), do: {:error, "Invalid UUID"} end