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