lib/ordered_nary_tree.ex

defmodule OrderedNaryTree do
  @moduledoc """
  A struct based implementation of a pure Elixir ordered n-ary tree
  """

  @type tree_node :: OrderedNaryTree.Node.t()
  @type tree_node_id :: OrderedNaryTree.Node.id()
  @type t :: %OrderedNaryTree{root: tree_node | nil}

  defstruct ~w[root]a

  @spec new(tree_node | nil) :: t
  def new(root \\ nil) do
    %OrderedNaryTree{root: root}
  end

  @spec root(t) :: {:ok, tree_node} | {:error, :empty_root}
  def root(tree) do
    case tree.root do
      %OrderedNaryTree.Node{} = n -> {:ok, n}
      nil -> {:error, :empty_root}
    end
  end

  @spec children(t) :: {:ok, [tree_node]} | {:error, :empty_root}
  def children(tree) do
    case OrderedNaryTree.root(tree) do
      {:ok, r} -> {:ok, r.children}
      {:error, :empty_root} = error -> error
    end
  end

  @spec children(t, tree_node_id) :: {:ok, [tree_node]} | {:error, :empty_root | :not_found}
  def children(tree, node_id) do
    case find_node_by_id(tree.root, node_id) do
      {:ok, n} -> {:ok, n.children}
      {:error, _} = error -> error
    end
  end

  @spec add_child(t, tree_node_id, tree_node) :: {:ok, t} | {:error, :empty_root | :not_found}
  def add_child(tree, parent_node_id, child_node) do
    tree.root
    |> replace_node(parent_node_id, & OrderedNaryTree.Node.add_child(&1, child_node))
    |> case do
      {:ok, root} -> {:ok, OrderedNaryTree.new(root)}
      {:error, _} = error -> error
    end
  end

  @spec add_child(t, tree_node) :: {:ok, t} | {:error, :empty_root}
  def add_child(tree, child_node) do
    case OrderedNaryTree.root(tree) do
      {:ok, root} -> OrderedNaryTree.add_child(tree, root.id, child_node)
      {:error, :empty_root} = error -> error
    end
  end

  @spec find(t, function) :: {:ok, tree_node} | {:error, :empty_root | :not_found}
  def find(tree, func) do
    find_node(tree.root, func)
  end

  @spec parent(t, tree_node_id) :: {:ok, tree_node} | {:error, :empty_root | :not_found}
  def parent(tree, node_id) do
    find_node(tree.root, fn p ->
      Enum.find(p.children, & &1.id == node_id) != nil
    end)
  end

  defp find_node([], _func), do: {:error, :not_found}
  defp find_node(nil, _func), do: {:error, :empty_root}
  defp find_node(%OrderedNaryTree.Node{} = n, func) do
    case func.(n) do
      true -> {:ok, n}
      false -> find_node(n.children, func)
    end
  end
  defp find_node([n | nodes], func) do
    case find_node(n, func) do
      {:ok, _n} = result -> result
      {:error, :not_found} -> find_node(nodes, func)
    end
  end

  defp find_node_by_id([], _search_id), do: {:error, :not_found}
  defp find_node_by_id(nil, _search_id), do: {:error, :empty_root}
  defp find_node_by_id(%OrderedNaryTree.Node{id: id} = n, search_id) when id == search_id, do: {:ok, n}
  defp find_node_by_id(%OrderedNaryTree.Node{children: c}, search_id), do: find_node_by_id(c, search_id)
  defp find_node_by_id([n | nodes], search_id) do
    case find_node_by_id(n, search_id) do
      {:ok, _n} = result -> result
      {:error, :not_found} -> find_node_by_id(nodes, search_id)
    end
  end

  defp replace_node(n, search_id, func, parent_and_older_sibling_nodes \\ nil) do
    case n do
      nil ->
        {:error, :empty_root}

      [] ->
        {:error, :not_found}

      %OrderedNaryTree.Node{id: id} = n when id == search_id ->
        {:ok, func.(n)}

      %OrderedNaryTree.Node{children: c} = n ->
        replace_node(c, search_id, func, {n, []})

      [n | nodes] ->
        {parent_node, older_sibling_nodes} = parent_and_older_sibling_nodes
        case replace_node(n, search_id, func, {parent_node, older_sibling_nodes}) do
          {:ok, n} ->
            {:ok, %{parent_node | children: older_sibling_nodes ++ [n] ++ nodes}}

          {:error, :not_found} ->
            replace_node(nodes, search_id, func, {parent_node, older_sibling_nodes ++ [n]})
        end
    end
  end
end