Current section
Files
Jump to
Current section
Files
lib/yog/builder/labeled.ex
defmodule Yog.Builder.Labeled do
@moduledoc """
Build graphs using arbitrary labels instead of integer IDs.
This module provides a convenient way to build graphs when your nodes are
naturally identified by strings, atoms, or other types, rather than integers.
## Example Usage
# Build a graph with string labels
builder = Yog.Builder.Labeled.directed()
|> Yog.Builder.Labeled.add_edge("home", "work", 10)
|> Yog.Builder.Labeled.add_edge("work", "gym", 5)
# Convert to a Graph to use with algorithms
graph = Yog.Builder.Labeled.to_graph(builder)
# Get the node ID for a label
{:ok, home_id} = Yog.Builder.Labeled.get_id(builder, "home")
# Use with pathfinding
case Yog.Pathfinding.Dijkstra.shortest_path(
in: graph,
from: home_id,
to: gym_id,
zero: 0,
add: &Kernel.+/2,
compare: &Integer.compare/2
) do
{:ok, path} -> path
_ -> :no_path
end
## Batch Construction
For building from existing data, use the `from_list` functions:
edges = [{"A", "B", 5}, {"B", "C", 3}, {"A", "C", 10}]
builder = Yog.Builder.Labeled.from_list(:directed, edges)
graph = Yog.Builder.Labeled.to_graph(builder)
"""
alias Yog.Model
@enforce_keys [:graph]
defstruct [:kind, :graph, label_to_id: %{}, next_id: 0]
@typedoc "Labeled builder struct"
@type t :: %__MODULE__{
kind: Model.graph_type(),
graph: Yog.graph(),
label_to_id: %{label() => Yog.node_id()},
next_id: integer()
}
@typedoc "Any type can be used as a label"
@type label :: term()
# ============= Constructors =============
@doc """
Creates a new labeled directed graph builder.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
iex> is_struct(builder, Yog.Builder.Labeled)
true
"""
@spec directed() :: t()
def directed, do: new(:directed)
@doc """
Creates a new labeled undirected graph builder.
## Examples
iex> builder = Yog.Builder.Labeled.undirected()
iex> is_struct(builder, Yog.Builder.Labeled)
true
"""
@spec undirected() :: t()
def undirected, do: new(:undirected)
@doc """
Creates a new labeled graph builder of the specified type.
## Examples
iex> builder = Yog.Builder.Labeled.new(:directed)
iex> is_struct(builder, Yog.Builder.Labeled)
true
"""
@spec new(Model.graph_type()) :: t()
def new(graph_type) do
%__MODULE__{
kind: graph_type,
graph: Model.new(graph_type),
label_to_id: %{},
next_id: 0
}
end
# ============= Node Operations =============
@doc """
Adds a node with the given label.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("Node A")
iex> Yog.Builder.Labeled.all_labels(builder)
["Node A"]
"""
@spec add_node(t(), label()) :: t()
def add_node(builder, label) do
{new_builder, _id} = ensure_node(builder, label)
new_builder
end
@doc """
Gets or creates a node for the given label.
If a node with this label already exists, returns its existing ID.
If it doesn't exist, creates a new node and returns the new ID.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
iex> {_builder, id} = Yog.Builder.Labeled.ensure_node(builder, "A")
iex> is_integer(id)
true
"""
@spec ensure_node(t(), label()) :: {t(), Yog.node_id()}
def ensure_node(
%__MODULE__{graph: graph, label_to_id: label_to_id, next_id: next_id} = builder,
label
) do
case Map.fetch(label_to_id, label) do
{:ok, id} ->
{builder, id}
:error ->
id = next_id
new_graph = Model.add_node(graph, id, label)
new_mapping = Map.put(label_to_id, label, id)
new_builder = %{builder | graph: new_graph, label_to_id: new_mapping, next_id: id + 1}
{new_builder, id}
end
end
# ============= Edge Operations =============
@doc """
Adds an edge between two labeled nodes with a weight.
If either node doesn't exist, it will be created automatically.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 10)
iex> {:ok, successors} = Yog.Builder.Labeled.successors(builder, "A")
iex> successors
[{"B", 10}]
"""
@spec add_edge(t(), label(), label(), term()) :: t()
def add_edge(builder, from, to, weight) do
{builder_with_src, src_id} = ensure_node(builder, from)
{builder_with_both, dst_id} = ensure_node(builder_with_src, to)
%__MODULE__{graph: graph} = builder_with_both
{:ok, new_graph} = Model.add_edge(graph, src_id, dst_id, weight)
%{builder_with_both | graph: new_graph}
end
@doc """
Adds an unweighted edge between two labeled nodes.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_unweighted_edge("A", "B")
iex> {:ok, [{"B", nil}]} = Yog.Builder.Labeled.successors(builder, "A")
"""
@spec add_unweighted_edge(t(), label(), label()) :: t()
def add_unweighted_edge(builder, from, to) do
add_edge(builder, from, to, nil)
end
@doc """
Adds a simple edge with weight 1 between two labeled nodes.
Unlike `add_unweighted_edge/3` which stores weight as nil, this stores weight as 1.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_simple_edge("A", "B")
iex> is_struct(builder, Yog.Builder.Labeled)
true
"""
@spec add_simple_edge(t(), label(), label()) :: t()
def add_simple_edge(builder, from, to) do
add_edge(builder, from, to, 1)
end
# ============= Batch Construction =============
@doc """
Creates a builder from a list of labeled edges.
## Examples
iex> edges = [{"A", "B", 5}, {"B", "C", 3}]
iex> builder = Yog.Builder.Labeled.from_list(:directed, edges)
iex> {:ok, [{"B", 5}]} = Yog.Builder.Labeled.successors(builder, "A")
"""
@spec from_list(Model.graph_type(), [{label(), label(), term()}]) :: t()
def from_list(graph_type, edges) do
Enum.reduce(edges, new(graph_type), fn {src, dst, weight}, builder ->
add_edge(builder, src, dst, weight)
end)
end
@doc """
Creates a builder from a list of unweighted labeled edges.
## Examples
iex> edges = [{"A", "B"}, {"B", "C"}]
iex> builder = Yog.Builder.Labeled.from_unweighted_list(:directed, edges)
iex> {:ok, [{"B", nil}]} = Yog.Builder.Labeled.successors(builder, "A")
"""
@spec from_unweighted_list(Model.graph_type(), [{label(), label()}]) :: t()
def from_unweighted_list(graph_type, edges) do
Enum.reduce(edges, new(graph_type), fn {src, dst}, builder ->
add_unweighted_edge(builder, src, dst)
end)
end
# ============= Conversion =============
@doc """
Converts the builder to a standard Graph.
The resulting graph can be used with all Yog algorithms.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 5)
iex> graph = Yog.Builder.Labeled.to_graph(builder)
iex> Yog.graph?(graph)
true
"""
@spec to_graph(t()) :: Yog.graph()
def to_graph(%__MODULE__{graph: graph}), do: graph
@doc """
Gets the label-to-ID registry as a map.
Returns a map where keys are labels and values are node IDs.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("A")
iex> registry = Yog.Builder.Labeled.to_registry(builder)
iex> Map.get(registry, "A")
0
"""
@spec to_registry(t()) :: %{label() => Yog.node_id()}
def to_registry(%__MODULE__{label_to_id: label_to_id}), do: label_to_id
# ============= Queries =============
@doc """
Looks up the internal node ID for a given label.
Returns `{:ok, id}` if the label exists, `{:error, nil}` otherwise.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("A")
iex> Yog.Builder.Labeled.get_id(builder, "A")
{:ok, 0}
iex> builder = Yog.Builder.Labeled.directed()
iex> Yog.Builder.Labeled.get_id(builder, "NonExistent")
{:error, nil}
"""
@spec get_id(t(), label()) :: {:ok, Yog.node_id()} | {:error, nil}
def get_id(%__MODULE__{label_to_id: label_to_id}, label) do
do_get_id(label_to_id, label)
end
defp do_get_id(label_to_id, label) do
case Map.fetch(label_to_id, label) do
{:ok, id} -> {:ok, id}
:error -> {:error, nil}
end
end
@doc """
Looks up the label for a given internal node ID.
Returns `{:ok, label}` if the ID exists, `{:error, nil}` otherwise.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("A")
iex> {:ok, id} = Yog.Builder.Labeled.get_id(builder, "A")
iex> Yog.Builder.Labeled.get_label(builder, id)
{:ok, "A"}
iex> builder = Yog.Builder.Labeled.directed()
iex> Yog.Builder.Labeled.get_label(builder, 999)
{:error, nil}
"""
@spec get_label(t(), Yog.node_id()) :: {:ok, label()} | {:error, nil}
def get_label(%__MODULE__{graph: graph}, id) do
if Model.has_node?(graph, id) do
{:ok, Model.node(graph, id)}
else
{:error, nil}
end
end
@doc """
Returns all labels that have been added to the builder.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("A")
...> |> Yog.Builder.Labeled.add_node("B")
iex> Yog.Builder.Labeled.all_labels(builder)
["A", "B"]
"""
@spec all_labels(t()) :: [label()]
def all_labels(%__MODULE__{label_to_id: label_to_id}), do: Map.keys(label_to_id)
@doc """
Checks if a label has been registered in the builder.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("A")
iex> Yog.Builder.Labeled.has_label?(builder, "A")
true
iex> Yog.Builder.Labeled.has_label?(builder, "B")
false
"""
@spec has_label?(t(), label()) :: boolean()
def has_label?(%__MODULE__{label_to_id: label_to_id}, label) do
Map.has_key?(label_to_id, label)
end
@doc """
Checks if an edge exists between two labeled nodes.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 10)
iex> Yog.Builder.Labeled.has_edge?(builder, "A", "B")
true
iex> Yog.Builder.Labeled.has_edge?(builder, "B", "A")
false
"""
@spec has_edge?(t(), label(), label()) :: boolean()
def has_edge?(%__MODULE__{graph: graph, label_to_id: label_to_id}, from, to) do
case {Map.fetch(label_to_id, from), Map.fetch(label_to_id, to)} do
{{:ok, from_id}, {:ok, to_id}} -> Model.has_edge?(graph, from_id, to_id)
_ -> false
end
end
@doc """
Returns the number of registered nodes.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_node("A")
...> |> Yog.Builder.Labeled.add_node("B")
iex> Yog.Builder.Labeled.node_count(builder)
2
"""
@spec node_count(t()) :: non_neg_integer()
def node_count(%__MODULE__{label_to_id: label_to_id}), do: map_size(label_to_id)
@doc """
Returns the number of edges in the graph.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 10)
...> |> Yog.Builder.Labeled.add_edge("B", "C", 5)
iex> Yog.Builder.Labeled.edge_count(builder)
2
"""
@spec edge_count(t()) :: non_neg_integer()
def edge_count(%__MODULE__{graph: graph}), do: Model.edge_count(graph)
@doc """
Gets the next available node ID.
This is the ID that would be assigned to the next new node.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
iex> Yog.Builder.Labeled.next_id(builder)
0
iex> builder = Yog.Builder.Labeled.add_node(builder, "A")
iex> Yog.Builder.Labeled.next_id(builder)
1
"""
@spec next_id(t()) :: Yog.node_id()
def next_id(%__MODULE__{next_id: next_id}), do: next_id
@doc """
Gets the successors of a node by its label.
Returns `{:ok, edges}` where edges is a list of `{label, weight}` tuples,
or `{:error, nil}` if the label doesn't exist.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 10)
iex> Yog.Builder.Labeled.successors(builder, "A")
{:ok, [{"B", 10}]}
"""
@spec successors(t(), label()) :: {:ok, [{label(), term()}]} | {:error, nil}
def successors(%__MODULE__{graph: graph, label_to_id: label_to_id}, label) do
do_successors(graph, label_to_id, label)
end
defp do_successors(graph, label_to_id, label) do
case Map.fetch(label_to_id, label) do
{:ok, id} ->
successor_edges = Model.successors(graph, id)
labeled_edges = map_ids_to_labels(successor_edges, graph)
{:ok, labeled_edges}
:error ->
{:error, nil}
end
end
@doc """
Gets the predecessors of a node by its label.
Returns `{:ok, edges}` where edges is a list of `{label, weight}` tuples,
or `{:error, nil}` if the label doesn't exist.
## Examples
iex> builder = Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_edge("A", "B", 5)
iex> Yog.Builder.Labeled.predecessors(builder, "B")
{:ok, [{"A", 5}]}
"""
@spec predecessors(t(), label()) :: {:ok, [{label(), term()}]} | {:error, nil}
def predecessors(%__MODULE__{graph: graph, label_to_id: label_to_id}, label) do
do_predecessors(graph, label_to_id, label)
end
defp do_predecessors(graph, label_to_id, label) do
case Map.fetch(label_to_id, label) do
{:ok, id} ->
predecessor_edges = Model.predecessors(graph, id)
labeled_edges = map_ids_to_labels(predecessor_edges, graph)
{:ok, labeled_edges}
:error ->
{:error, nil}
end
end
defp map_ids_to_labels(edges, graph) do
Enum.flat_map(edges, fn {node_id, edge_data} ->
case Model.node(graph, node_id) do
nil -> []
label -> [{label, edge_data}]
end
end)
end
end