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 (Not a doctest - delegates to Erlang)
# 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.shortest_path(
in: graph,
from: home_id,
to: gym_id,
zero: 0,
add: &Kernel.+/2,
compare: &Integer.compare/2
) do
{:some, path} -> path
:none -> :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)
"""
@typedoc "Opaque builder type"
@type builder :: term()
@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_tuple(builder)
true
"""
@spec directed() :: builder()
defdelegate directed(), to: :yog@builder@labeled
@doc """
Creates a new labeled undirected graph builder.
## Examples
iex> builder = Yog.Builder.Labeled.undirected()
iex> is_tuple(builder)
true
"""
@spec undirected() :: builder()
defdelegate undirected(), to: :yog@builder@labeled
@doc """
Creates a new labeled graph builder of the specified type.
## Examples
iex> builder = Yog.Builder.Labeled.new(:directed)
iex> is_tuple(builder)
true
"""
@spec new(Yog.graph_type()) :: builder()
defdelegate new(graph_type), to: :yog@builder@labeled
# ============= 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(builder(), label()) :: builder()
defdelegate add_node(builder, label), to: :yog@builder@labeled
@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(builder(), label()) :: {builder(), Yog.node_id()}
defdelegate ensure_node(builder, label), to: :yog@builder@labeled
# ============= 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(builder(), label(), label(), term()) :: builder()
defdelegate add_edge(builder, from, to, weight), to: :yog@builder@labeled
@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(builder(), label(), label()) :: builder()
defdelegate add_unweighted_edge(builder, from, to), to: :yog@builder@labeled
@doc """
Adds a simple edge with no weight data between two labeled nodes.
Unlike `add_unweighted_edge/3` which stores weight as 1, this stores no weight.
## Examples
iex> Yog.Builder.Labeled.directed()
...> |> Yog.Builder.Labeled.add_simple_edge("A", "B")
...> |> is_tuple()
true
"""
@spec add_simple_edge(builder(), label(), label()) :: builder()
defdelegate add_simple_edge(builder, from, to), to: :yog@builder@labeled
# ============= 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(Yog.graph_type(), [{label(), label(), term()}]) :: builder()
defdelegate from_list(graph_type, edges), to: :yog@builder@labeled
@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(Yog.graph_type(), [{label(), label()}]) :: builder()
defdelegate from_unweighted_list(graph_type, edges), to: :yog@builder@labeled
# ============= 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(builder()) :: Yog.graph()
defdelegate to_graph(builder), to: :yog@builder@labeled
@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(builder()) :: %{label() => Yog.node_id()}
defdelegate to_registry(builder), to: :yog@builder@labeled
# ============= 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(builder(), label()) :: {:ok, Yog.node_id()} | {:error, nil}
def get_id(builder, label) do
case :yog@builder@labeled.get_id(builder, label) do
{:ok, id} -> {:ok, id}
{:error, _} -> {: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(builder()) :: [label()]
defdelegate all_labels(builder), to: :yog@builder@labeled
@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(builder()) :: Yog.node_id()
defdelegate next_id(builder), to: :yog@builder@labeled
@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(builder(), label()) :: {:ok, [{label(), term()}]} | {:error, nil}
def successors(builder, label) do
case :yog@builder@labeled.successors(builder, label) do
{:ok, edges} -> {:ok, 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(builder(), label()) :: {:ok, [{label(), term()}]} | {:error, nil}
def predecessors(builder, label) do
case :yog@builder@labeled.predecessors(builder, label) do
{:ok, edges} -> {:ok, edges}
{:error, _} -> {:error, nil}
end
end
end