lib/unrooted_polytree.ex

defmodule UnrootedPolytree do
  @moduledoc """
  A data type and related functions to support an unrooted (multiple starting nodes) polytree (a tree-like graph with edges).
  """

  alias UnrootedPolytree.{Edges, Node}

  @type nodes_by_id :: %{Node.id() => Node.t()}
  @type edges_by_id :: %{Node.id() => Edges.t()}

  @type t :: %__MODULE__{
          by_id: nodes_by_id(),
          edges: edges_by_id(),
          starting_nodes: [Node.id()]
        }

  @type id_and_value :: {Node.id(), any()}

  defstruct by_id: %{}, edges: %{}, starting_nodes: []

  @doc """
  Retrieve a node by its ID.
  """
  @spec node_for_id(t(), Node.id()) :: {:ok, Node.t()} | :error
  def node_for_id(%__MODULE__{by_id: by_id}, id), do: Map.fetch(by_id, id)

  @doc """
  Retrieve the edges for a node given its ID.
  """
  @spec edges_for_id(t(), Node.id()) :: Edges.t()
  def edges_for_id(%__MODULE__{edges: edges}, id), do: Map.get(edges, id, %Edges{})

  @doc """
  Construct an UnrootedPolytree from a list of lists of values and associated ID strings
  """
  @spec from_lists([[id_and_value()]]) :: t()
  def from_lists(list_of_lists),
    do:
      Enum.reduce(list_of_lists, %UnrootedPolytree{}, fn list, tree -> from_list(list, tree) end)

  @doc """
  Construct an UnrootedPolytree from a list of values and associated ID strings
  """
  @spec from_list([id_and_value()]) :: t()
  @spec from_list([id_and_value()], t()) :: t()
  @spec from_list([id_and_value()], t(), Node.id()) :: t()
  def from_list(list), do: from_list(list, %UnrootedPolytree{})

  defp from_list([], tree), do: tree

  defp from_list(
         [{id, value} | tail],
         %__MODULE__{by_id: by_id, edges: edges, starting_nodes: starting_nodes}
       ) do
    new_tree = %__MODULE__{
      by_id: add_by_id(by_id, id, value),
      edges: edges,
      starting_nodes: Enum.uniq([id | starting_nodes])
    }

    from_list(tail, new_tree, id)
  end

  defp from_list([], tree, _previous_id), do: tree

  defp from_list(
         [{id, value} | tail],
         %__MODULE__{by_id: by_id, starting_nodes: starting_nodes} = tree,
         previous_id
       ) do
    new_tree = %__MODULE__{
      by_id: add_by_id(by_id, id, value),
      edges: add_next_and_previous_edges(tree, id, previous_id),
      starting_nodes: starting_nodes
    }

    from_list(tail, new_tree, id)
  end

  @spec add_by_id(nodes_by_id(), Node.id(), any()) :: nodes_by_id()
  defp add_by_id(by_id, id, value),
    do: Map.put_new(by_id, id, Node.from_id_and_value(id, value))

  @spec add_next_and_previous_edges(t(), Node.id(), Node.id()) :: edges_by_id()
  defp add_next_and_previous_edges(%UnrootedPolytree{edges: edges} = tree, next_id, previous_id) do
    Map.merge(
      edges,
      %{
        next_id => tree |> edges_for_id(next_id) |> Edges.add_previous(previous_id),
        previous_id => tree |> edges_for_id(previous_id) |> Edges.add_next(next_id)
      }
    )
  end
end