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 Optionally configure the alphabet to be used for encoding in `config.exs`: config :shortuuid, alphabet: "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" Note that for this to take effect currently requires recompiling the dependency. The default alphabet (above) 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 For compatibility with the python version we also pad values to the length it takes to store a UUID in the chosen alphabet. Any UUID will be padded to length 22 in case of the default base 57 alphabet. The pad character is always the first character of the alphabet which represents zero. In the default alphabet that character is 2 since it doesn't include 0 and 1. 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}` Letter case is not relevant. ## Acknowledgments This project was inspired by [skorokithakis/shortuuid](https://github.com/skorokithakis/shortuuid). """ @alphabet (Application.get_env(:shortuuid, :alphabet) || "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") |> String.graphemes |> Enum.sort |> Enum.dedup |> List.to_string @alphabet_graphemes String.graphemes(@alphabet) @alphabet_length String.length(@alphabet) # Support default padding for python lib compatibility # Calculate the necessary length to fit the entire UUID given the current alphabet. @encode_padding_min_length round(Float.ceil(:math.log(:math.pow(2, 128)) / :math.log(@alphabet_length))) @only_alphabet ~r/^[#{@alphabet}]*$/ @doc """ Encode a UUID using the chosen alphabet. ## 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 |> add_encode_padding {:ok, encoded_uuid} _anything_else -> {:error, "Invalid UUID"} end end @doc """ Encode a UUID using the chosen alphabet. 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 shortened UUID. ## Examples iex> ShortUUID.decode!("keATfB8JP2ggT7U9JZrpV9") "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 |> add_decode_padding |> format_uuid_string false -> {:error, "Alphabet doesn't match string #{string}"} end end @doc """ Decode a shortened UUID. Similar to `decode/1` but raises an ArgumentError if the encoded UUID is invalid. """ @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 | String.at(@alphabet, 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.reverse(string) |> String.graphemes |> Enum.reduce(0, fn(char, acc) -> acc * @alphabet_length + Enum.find_index(@alphabet_graphemes, &(&1 == char)) end) end defp to_hex_string(number) do number |> Integer.to_string(16) end defp add_encode_padding(encoded_uuid) do encoded_uuid |> String.pad_trailing(@encode_padding_min_length, List.first(@alphabet_graphemes)) end defp add_decode_padding(string) do string |> String.pad_leading(32, "0") end defp format_uuid_string(<>) do {:ok, <>} end defp format_uuid_string(_invalid), do: {:error, "Decoded string is not a UUID"} end