Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/graph.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ defmodule Graph do
NOTE: Currently this function assumes graphs are directed graphs, but in the future
it will support undirected graphs as well.

NOTE 2: To avoid to overwrite vertices with the same label, output is
NOTE 2: To avoid to overwrite vertices with the same label, output is
generated using the internal numeric ID as vertex label.
Original label is expressed as `id[label="<label>"]`.

Expand Down Expand Up @@ -1547,7 +1547,7 @@ defmodule Graph do
[:d, :c, :b, :a]
"""
@spec reachable(t, [vertex]) :: [[vertex]]
defdelegate reachable(g, vs), to: Graph.Directed
defdelegate reachable(g, vs), to: Graph.Common

@doc """
Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`),
Expand All @@ -1563,7 +1563,7 @@ defmodule Graph do
[:d, :c, :b]
"""
@spec reachable_neighbors(t, [vertex]) :: [[vertex]]
defdelegate reachable_neighbors(g, vs), to: Graph.Directed
defdelegate reachable_neighbors(g, vs), to: Graph.Common

@doc """
Returns an unsorted list of vertices from the graph, such that for each vertex in the list (call it `v`),
Expand Down
17 changes: 17 additions & 0 deletions lib/graph/common.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Graph.Common do
def reachable(%Graph{type: :directed} = g, vs) when is_struct(g) and is_list(vs) do
Graph.Directed.reachable(g, vs)
end

def reachable(%Graph{type: :undirected} = g, vs) when is_struct(g) and is_list(vs) do
Graph.Undirected.reachable(g, vs)
end

def reachable_neighbors(%Graph{type: :directed} = g, vs) when is_struct(g) and is_list(vs) do
Graph.Directed.reachable_neighbors(g, vs)
end

def reachable_neighbors(%Graph{type: :undirected} = g, vs) when is_struct(g) and is_list(vs) do
Graph.Undirected.reachable_neighbors(g, vs)
end
end
90 changes: 90 additions & 0 deletions lib/graph/undirected.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
defmodule Graph.Undirected do
@compile {:inline}

def reachable(%Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g, vs)
when is_list(vs) do
vs = Enum.map(vs, vertex_identifier)
for id <- :lists.append(forest(g, &neighbors/3, vs, :first)), do: Map.get(vertices, id)
end

def reachable_neighbors(
%Graph{vertices: vertices, vertex_identifier: vertex_identifier} = g,
vs
)
when is_list(vs) do
vs = Enum.map(vs, vertex_identifier)

for id <- :lists.append(forest(g, &neighbors/3, vs, :not_first)),
do: Map.get(vertices, id)
end

def neighbors(%Graph{} = g, v, []) do
neighbors(g, v)
end

def neighbors(%Graph{out_edges: oe, in_edges: ie}, v, vs) do
case {Map.get(ie, v), Map.get(oe, v)} do
{nil, nil} ->
vs

{v_in, nil} ->
MapSet.to_list(v_in) ++ vs

{nil, v_out} ->
MapSet.to_list(v_out) ++ vs

{v_in, v_out} ->
s = MapSet.union(v_in, v_out)
MapSet.to_list(s) ++ vs
end
end

def neighbors(%Graph{out_edges: oe, in_edges: ie}, v) do
v_in = Map.get(ie, v, MapSet.new())
v_out = Map.get(oe, v, MapSet.new())

MapSet.union(v_in, v_out)
|> MapSet.to_list()
end

defp forest(%Graph{vertices: vs} = g, fun) do
forest(g, fun, Map.keys(vs))
end

defp forest(g, fun, vs) do
forest(g, fun, vs, :first)
end

defp forest(g, fun, vs, handle_first) do
{_, acc} =
List.foldl(vs, {MapSet.new(), []}, fn v, {visited, acc} ->
pretraverse(handle_first, v, fun, g, visited, acc)
end)

acc
end

defp pretraverse(:first, v, fun, g, visited, acc) do
ptraverse([v], fun, g, visited, [], acc)
end

defp pretraverse(:not_first, v, fun, g, visited, acc) do
if MapSet.member?(visited, v) do
{visited, acc}
else
ptraverse(fun.(g, v, []), fun, g, visited, [], acc)
end
end

defp ptraverse([v | vs], fun, g, visited, results, acc) do
if MapSet.member?(visited, v) do
ptraverse(vs, fun, g, visited, results, acc)
else
visited = MapSet.put(visited, v)
ptraverse(fun.(g, v, vs), fun, g, visited, [v | results], acc)
end
end

defp ptraverse([], _fun, _g, visited, [], acc), do: {visited, acc}
defp ptraverse([], _fun, _g, visited, results, acc), do: {visited, [results | acc]}
end
73 changes: 73 additions & 0 deletions test/graph/undirected_test.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Graph.UndirectedTest do
use ExUnit.Case, async: true

describe "Graph.reachable/2" do
test "reachable" do
g =
Graph.new(type: :undirected)
|> Graph.add_edges([{:a, :b}, {:b, :c}])

assert [:a, :b, :c] = Graph.reachable(g, [:c])
assert [:c, :b, :a] = Graph.reachable(g, [:a])
end

test "parts reachable" do
g =
Graph.new(type: :undirected)
|> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}])

assert [:d, :e] = Graph.reachable(g, [:e])
assert [:c, :a, :b] = Graph.reachable(g, [:b])
end

test "nothing reachable" do
g =
Graph.new(type: :undirected)
|> Graph.add_edges([{:a, :b}, {:b, :d}])
|> Graph.add_vertex(:c)

assert [:c] = Graph.reachable(g, [:c])
end

test "unknown vertex" do
g = Graph.new(type: :undirected)

assert [nil] = Graph.reachable(g, [:a])
end
end

describe "Graph.reachable_neighbours/2" do
test "reachable" do
g =
Graph.new(type: :undirected)
|> Graph.add_edges([{:a, :b}, {:b, :c}])

assert [:a, :b] = Graph.reachable_neighbors(g, [:c])
end

@tag :only
test "parts reachable" do
g =
Graph.new(type: :undirected)
|> Graph.add_edges([{:a, :b}, {:b, :c}, {:d, :e}, {:e, :f}])

assert [:d, :e] = Graph.reachable_neighbors(g, [:f])
assert [] = Graph.reachable_neighbors(g, [:b])
end

test "nothing reachable" do
g =
Graph.new(type: :undirected)
|> Graph.add_edges([{:a, :b}, {:b, :d}])
|> Graph.add_vertex(:c)

assert [] = Graph.reachable_neighbors(g, [:c])
end

test "unknown vertex" do
g = Graph.new(type: :undirected)

assert [] = Graph.reachable_neighbors(g, [:a])
end
end
end