lib/pairing_heap.ex

defmodule PairingHeap do
  @readme "README.md"
  @external_resource @readme
  @moduledoc_readme @readme
                    |> File.read!()
                    |> String.split("<!-- END HEADER -->")
                    |> Enum.fetch!(1)
                    |> String.trim()

  @moduledoc """
  #{@moduledoc_readme}
  """

  defstruct root: :empty, size: 0, mode: nil, ordered?: nil

  alias PairingHeap.Node

  @type key() :: any()
  @type value() :: any()
  @type pair() :: {key(), value()}
  @type mode() :: :min | :max | {:min, module()} | {:max, module()}

  @type t :: %PairingHeap{
          root: :empty | Node.t(),
          size: non_neg_integer(),
          mode: mode(),
          ordered?: Node.ordered_fn()
        }

  @doc """
  Return an empty heap with the given `mode`.

  The `mode` can be one of

    * `:min` - a _min-heap_, where keys are compared with `&<=/2`
    * `:max` - a _max-heap_, where keys are compared with `&>=/2`
    * `{:min | :max, module}` - a min or max heap, where the `module` must
      implement a `compare` funciton with signature `(any, any -> :lt | :eq | :gt)`

  ## Examples

      iex> PairingHeap.new(:min)
      #PairingHeap<root: :empty, size: 0, mode: :min>
  """
  @spec new(mode()) :: t()
  def new(mode) do
    %PairingHeap{
      root: :empty,
      size: 0,
      mode: mode,
      ordered?: to_ordered_fn(mode)
    }
  end

  @doc """
  Return a heap with the given `mode` and insert each key-value pair in the
  given `list`.

  The `mode` can be one of

    * `:min` - a _min-heap_, where keys are compared with `&<=/2`
    * `:max` - a _max-heap_, where keys are compared with `&>=/2`
    * `{:min | :max, module}` - a min or max heap, where the `module` must
      implement a `compare` funciton with signature `(any, any -> :lt | :eq | :gt)`

  ## Examples

      iex> PairingHeap.new(:min, [{2, :b}, {1, :a}])
      #PairingHeap<root: {1, :a}, size: 2, mode: :min>
  """
  @spec new(mode(), [pair()]) :: t
  def new(mode, []), do: new(mode)

  def new(mode, [{_, _} | _] = list) do
    Enum.reduce(list, new(mode), fn {key, value}, heap ->
      put(heap, key, value)
    end)
  end

  # Generates the `ordered?` function. If `ordered?.(key1, key2)` is true,
  # then the heap property is satisfied if a node with key `key1` is a parent of
  # a ndoe with key `key2`.
  @spec to_ordered_fn(mode()) :: Node.ordered_fn()
  defp to_ordered_fn(:min), do: to_node_fn(&<=/2)
  defp to_ordered_fn(:max), do: to_node_fn(&>=/2)

  defp to_ordered_fn({:min, module}) when is_atom(module),
    do: to_node_fn(&(module.compare(&1, &2) != :gt))

  defp to_ordered_fn({:min, module}) when is_atom(module),
    do: to_node_fn(&(module.compare(&1, &2) != :lt))

  # Lift a function of two key to a function of two Nodes.
  @spec to_node_fn((key(), key() -> boolean())) :: Node.ordered_fn()
  defp to_node_fn(key_fn) do
    fn %Node{item: {key1, _}}, %Node{item: {key2, _}} ->
      key_fn.(key1, key2)
    end
  end

  @doc """
  Return `true` if the heap is empty, and `false` otherwise.

  ## Examples

      iex> PairingHeap.new(:min) |> PairingHeap.empty?()
      true
  """
  @spec empty?(t()) :: boolean()
  def empty?(%PairingHeap{root: :empty} = _heap), do: true
  def empty?(%PairingHeap{root: %PairingHeap.Node{}} = _heap), do: false

  @doc """
  Insert a new key and value to the heap and return the updated heap.

  ## Examples

      iex> PairingHeap.new(:min) |> PairingHeap.put(1, :a)
      #PairingHeap<root: {1, :a}, size: 1, mode: :min>
  """
  @spec put(t(), key(), value()) :: t()
  def put(%PairingHeap{root: :empty, size: 0} = heap, key, value) do
    %{heap | root: Node.new({key, value}, []), size: 1}
  end

  def put(
        %PairingHeap{root: %Node{} = node, size: size, ordered?: ordered?} = heap,
        key,
        value
      ) do
    %{
      heap
      | root: Node.merge(Node.new({key, value}, []), node, ordered?),
        size: size + 1
    }
  end

  @doc """
  Return the root key-value pair in the heap without modifying the heap.

  If the heap is non-empty, this returns `{:ok, {key, value}}`. If the heap is empty,
  `:error` is returned.

  ## Examples

      iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.peek()
      {:ok, {1, :a}}
  """
  @spec peek(t()) :: {:ok, pair()} | :error
  def peek(%PairingHeap{root: :empty} = _heap), do: :error
  def peek(%PairingHeap{root: %Node{item: {key, value}}} = _heap), do: {:ok, {key, value}}

  @doc """
  Return the root key-value pair in the heap, as well as the updated heap after
  the root is removed.

  If the heap is non-empty, this returns `{:ok, {key, value}, heap}`. If the heap
  is empty, `:error` is returned.

  ## Examples

      iex> {:ok, {key, value}, heap} =
      ...>   PairingHeap.new(:min, [{1, :a}])
      ...>   |> PairingHeap.pop()
      iex> {key, value}
      {1, :a}
      iex> heap
      #PairingHeap<root: :empty, size: 0, mode: :min>
  """
  @spec pop(t()) :: {:ok, pair(), t()} | :error
  def pop(%PairingHeap{root: :empty}), do: :error

  def pop(
        %PairingHeap{
          root: %Node{item: {key, value}, children: children},
          size: size,
          ordered?: ordered?
        } = heap
      ) do
    heap =
      case children do
        [] -> %{heap | root: :empty, size: 0}
        [_ | _] -> %{heap | root: Node.merge(children, ordered?), size: size - 1}
      end

    {:ok, {key, value}, heap}
  end

  @doc """
  Return the size of the heap, the total number of nodes.

  ## Examples

      iex> PairingHeap.new(:min, [{1, :a}]) |> PairingHeap.size()
      1
  """
  @spec size(t()) :: non_neg_integer()
  def size(%PairingHeap{size: size} = _heap), do: size

  @doc """
  Return the first `n` key-value pairs from the heap and the final state of the heap.

  If the heap `size` is less than `n`, all key-value pairs are returned along with
  an empty heap.

  ## Examples

      iex> {pairs, heap} =
      ...>   PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
      ...>   |> PairingHeap.pull(2)
      iex> pairs
      [{1, :a}, {2, :b}]
      iex> heap
      #PairingHeap<root: {3, :c}, size: 1, mode: :min>
  """
  @spec pull(t(), non_neg_integer()) :: {[pair()], t()}
  def pull(heap, n) when is_integer(n) and n >= 0 do
    {data, heap} = pull(heap, n, [])
    {Enum.reverse(data), heap}
  end

  defp pull(heap, 0, acc), do: {acc, heap}

  defp pull(%PairingHeap{} = heap, n, acc) do
    case pop(heap) do
      {:ok, {key, value}, heap} ->
        pull(heap, n - 1, [{key, value} | acc])

      :error ->
        {acc, heap}
    end
  end

  @doc """
  Merge two heaps into a single heap.

  An exception will be raised if the modes of the two heaps are not identical.

  ## Examples

      iex> PairingHeap.merge(
      ...>   PairingHeap.new(:min, [{1, :a}]),
      ...>   PairingHeap.new(:min, [{2, :b}])
      ...> )
      #PairingHeap<root: {1, :a}, size: 2, mode: :min>
  """
  @spec merge(t(), t()) :: t()
  def merge(%PairingHeap{mode: m1}, %PairingHeap{mode: m2}) when m1 != m2 do
    # TODO: Improve this error message
    raise ArgumentError, message: "when merging heaps, the modes must match"
  end

  def merge(%PairingHeap{root: :empty} = heap, %PairingHeap{root: :empty}), do: heap
  def merge(%PairingHeap{root: %Node{}} = heap, %PairingHeap{root: :emtpy}), do: heap
  def merge(%PairingHeap{root: :empty}, %PairingHeap{root: %Node{}} = heap), do: heap

  def merge(
        %PairingHeap{root: node1, size: size1, ordered?: ordered?} = heap,
        %PairingHeap{root: node2, size: size2}
      ) do
    %{heap | root: Node.merge([node1, node2], ordered?), size: size1 + size2}
  end

  @doc """
  Merge a list of one or more heaps into a single heap.

  An exception will be raised if the modes of the heaps are not identical.

  ## Examples

      iex> PairingHeap.merge([
      ...>   PairingHeap.new(:min, [{1, :a}]),
      ...>   PairingHeap.new(:min, [{2, :b}])
      ...> ])
      #PairingHeap<root: {1, :a}, size: 2, mode: :min>
  """
  @spec merge([t]) :: t
  def merge([heap] = _heaps), do: heap
  def merge([heap1, heap2]), do: merge(heap1, heap2)
  def merge([heap1, heap2 | rest]), do: merge(merge(heap1, heap2), merge(rest))

  @doc """
  Retrun `true` if the `heap` contains the key-value `pair`, and `false`
  otherwise.

  ## Examples

      iex> heap = PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
      iex> heap |> PairingHeap.member?({2, :b})
      true
  """
  @spec member?(t(), pair()) :: boolean()
  def member?(%PairingHeap{root: :empty} = _heap, _pair), do: false

  def member?(
        %PairingHeap{root: %Node{} = node, ordered?: ordered?} = _heap,
        {_, _} = pair
      ) do
    Node.member?(node, pair, ordered?)
  end

  @doc """
  Return the list of items in the heap.

  While the head of the list will be the item in the root node of the heap,
  there is not guarantee that the remaining items will be in key order. This
  funciton runs in `O(n)` time and is faster than repeatedly calling `pop/1`.

  ## Examples

      iex> heap = PairingHeap.new(:min, [{3, :c}, {1, :a}, {2, :b}])
      iex> PairingHeap.dump(heap)
      [{1, :a}, {2, :b}, {3, :c}]
  """
  @spec dump(t()) :: [pair()]
  def dump(%PairingHeap{root: :empty}), do: []
  def dump(%PairingHeap{root: %Node{} = node}), do: Node.dump(node)
end