lib/ulid.ex

defmodule Ulid do
  @moduledoc """
  This is an implementation of the ULID (Universally Unique Lexicographically Sortable Identifier) spec.
  """

  alias Ulid.Binary

  @typedoc """
  A Crockford 32 encoded ULID.
  """
  @type t() :: String.t()

  @doc """
  Generates a Crockford 32 encoded ULID for the given timestamp which must be in milliseconds.

  If no timestamp is given it uses the current system time.

  ## Examples

      iex> ulid = Ulid.generate()
      iex> <<_::size(208)>> = ulid
  """
  @spec generate(integer()) :: binary()
  def generate(timestamp \\ System.system_time(:millisecond)) do
    timestamp
    |> Binary.generate()
    |> encode()
  end

  @doc """
  Takes a 128 bit binary ULID and returns a Crockford 32 encoded ULID.

  Raises `Ulid.Error.InvalidBinary` when the input is malformed.

  ## Examples
      iex> ulid = <<1, 128, 211, 67, 45, 254, 115, 142, 105, 252, 121, 217, 228, 222, 219, 155>>
      iex> Ulid.encode(ulid)
      "01G39M6BFYEE76KZ3SV7JDXPWV"
  """
  @spec encode(Binary.t()) :: t()
  def encode(ulid) when is_binary(ulid) and byte_size(ulid) == 16 do
    encoded =
      for <<(symbol::5 <- <<0::2, ulid::bitstring>>)>> do
        encode_symbol(symbol)
      end

    to_string(encoded)
  end

  def encode(_), do: raise(Ulid.Error.InvalidBinary, "Must be a 128 bit binary")

  @doc """
  Takes a Crockford 32 encoded ULID and returns a a 128 bit binary ULID.

  Raises `Ulid.Error.InvalidUlidString` when the input is malformed.

  ## Examples

      iex> ulid = "01G3C1TW8AKE19BYDDR6996C1D"
      iex> Ulid.decode(ulid)
      <<1, 128, 216, 29, 113, 10, 155, 130, 149, 249, 173, 193, 146, 147, 48, 45>>
  """
  @spec decode(t()) :: Binary.t()
  def decode(ulid) when is_binary(ulid) and byte_size(ulid) == 26 do
    decoded =
      ulid
      |> to_charlist()
      |> Enum.reduce(<<>>, fn el, acc ->
        <<acc::bitstring, decode_symbol(el)::5>>
      end)

    <<_padding::2, decoded::binary>> = decoded
    decoded
  end

  def decode(string), do: raise(Ulid.Error.InvalidUlidString, string)

  defp encode_symbol(0), do: ?0
  defp encode_symbol(1), do: ?1
  defp encode_symbol(2), do: ?2
  defp encode_symbol(3), do: ?3
  defp encode_symbol(4), do: ?4
  defp encode_symbol(5), do: ?5
  defp encode_symbol(6), do: ?6
  defp encode_symbol(7), do: ?7
  defp encode_symbol(8), do: ?8
  defp encode_symbol(9), do: ?9
  defp encode_symbol(10), do: ?A
  defp encode_symbol(11), do: ?B
  defp encode_symbol(12), do: ?C
  defp encode_symbol(13), do: ?D
  defp encode_symbol(14), do: ?E
  defp encode_symbol(15), do: ?F
  defp encode_symbol(16), do: ?G
  defp encode_symbol(17), do: ?H
  defp encode_symbol(18), do: ?J
  defp encode_symbol(19), do: ?K
  defp encode_symbol(20), do: ?M
  defp encode_symbol(21), do: ?N
  defp encode_symbol(22), do: ?P
  defp encode_symbol(23), do: ?Q
  defp encode_symbol(24), do: ?R
  defp encode_symbol(25), do: ?S
  defp encode_symbol(26), do: ?T
  defp encode_symbol(27), do: ?V
  defp encode_symbol(28), do: ?W
  defp encode_symbol(29), do: ?X
  defp encode_symbol(30), do: ?Y
  defp encode_symbol(31), do: ?Z

  defp decode_symbol(?0), do: 0
  defp decode_symbol(?o), do: 0
  defp decode_symbol(?O), do: 0
  defp decode_symbol(?1), do: 1
  defp decode_symbol(?I), do: 1
  defp decode_symbol(?i), do: 1
  defp decode_symbol(?L), do: 1
  defp decode_symbol(?l), do: 1
  defp decode_symbol(?2), do: 2
  defp decode_symbol(?3), do: 3
  defp decode_symbol(?4), do: 4
  defp decode_symbol(?5), do: 5
  defp decode_symbol(?6), do: 6
  defp decode_symbol(?7), do: 7
  defp decode_symbol(?8), do: 8
  defp decode_symbol(?9), do: 9
  defp decode_symbol(?A), do: 10
  defp decode_symbol(?a), do: 10
  defp decode_symbol(?B), do: 11
  defp decode_symbol(?b), do: 11
  defp decode_symbol(?C), do: 12
  defp decode_symbol(?c), do: 12
  defp decode_symbol(?D), do: 13
  defp decode_symbol(?d), do: 13
  defp decode_symbol(?E), do: 14
  defp decode_symbol(?e), do: 14
  defp decode_symbol(?F), do: 15
  defp decode_symbol(?f), do: 15
  defp decode_symbol(?G), do: 16
  defp decode_symbol(?g), do: 16
  defp decode_symbol(?H), do: 17
  defp decode_symbol(?h), do: 17
  defp decode_symbol(?J), do: 18
  defp decode_symbol(?j), do: 18
  defp decode_symbol(?K), do: 19
  defp decode_symbol(?k), do: 19
  defp decode_symbol(?M), do: 20
  defp decode_symbol(?m), do: 20
  defp decode_symbol(?N), do: 21
  defp decode_symbol(?n), do: 21
  defp decode_symbol(?P), do: 22
  defp decode_symbol(?p), do: 22
  defp decode_symbol(?Q), do: 23
  defp decode_symbol(?q), do: 23
  defp decode_symbol(?R), do: 24
  defp decode_symbol(?r), do: 24
  defp decode_symbol(?S), do: 25
  defp decode_symbol(?s), do: 25
  defp decode_symbol(?T), do: 26
  defp decode_symbol(?t), do: 26
  defp decode_symbol(?V), do: 27
  defp decode_symbol(?v), do: 27
  defp decode_symbol(?W), do: 28
  defp decode_symbol(?w), do: 28
  defp decode_symbol(?X), do: 29
  defp decode_symbol(?x), do: 29
  defp decode_symbol(?Y), do: 30
  defp decode_symbol(?y), do: 30
  defp decode_symbol(?Z), do: 31
  defp decode_symbol(?z), do: 31
end