WIP: More fixes and traversal fully tested for non-enriched graphs.
This commit is contained in:
parent
117bff5242
commit
180990c934
7 changed files with 614 additions and 93 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package gs.graph.v0
|
package gs.graph.v0
|
||||||
|
|
||||||
import gs.graph.v0.directed.Digraph
|
import gs.graph.v0.directed.Digraph
|
||||||
|
import scala.collection.IndexedSeqView
|
||||||
|
|
||||||
/** Graph representation based on an adjacency list.
|
/** Graph representation based on an adjacency list.
|
||||||
*
|
*
|
||||||
|
|
@ -29,6 +30,12 @@ trait Graph:
|
||||||
*/
|
*/
|
||||||
def numberOfVertices: Size
|
def numberOfVertices: Size
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* View over all [[Vertex]] values in this graph.
|
||||||
|
*/
|
||||||
|
def view: IndexedSeqView[Vertex] =
|
||||||
|
(0 until numberOfVertices.value).view.map(Vertex(_))
|
||||||
|
|
||||||
/** @return
|
/** @return
|
||||||
* True if this graph has no vertices, false otherwise.
|
* True if this graph has no vertices, false otherwise.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,19 @@ package gs.graph.v0
|
||||||
*/
|
*/
|
||||||
sealed abstract class GraphDisposition(val name: String):
|
sealed abstract class GraphDisposition(val name: String):
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
override def equals(that: Any): Boolean =
|
override def equals(that: Any): Boolean =
|
||||||
that match
|
that match
|
||||||
case other: GraphDisposition => name == other.name
|
case other: GraphDisposition => name == other.name
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
override def hashCode(): Int = name.hashCode()
|
override def hashCode(): Int = name.hashCode()
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
override def toString(): String = name
|
override def toString(): String = name
|
||||||
|
|
||||||
object GraphDisposition:
|
object GraphDisposition:
|
||||||
|
|
@ -28,4 +34,17 @@ object GraphDisposition:
|
||||||
*/
|
*/
|
||||||
case object Undirected extends GraphDisposition("undirected")
|
case object Undirected extends GraphDisposition("undirected")
|
||||||
|
|
||||||
|
/** Parse the given string as a [[GraphDisposition]].
|
||||||
|
*
|
||||||
|
* @param candidate
|
||||||
|
* The string to validate.
|
||||||
|
* @return
|
||||||
|
* The parsed [[GraphDisposition]], or `None` if the input does not
|
||||||
|
* represent a valid disposition.
|
||||||
|
*/
|
||||||
|
def parse(candidate: String): Option[GraphDisposition] =
|
||||||
|
if candidate.equalsIgnoreCase(Directed.name) then Some(Directed)
|
||||||
|
else if candidate.equalsIgnoreCase(Undirected.name) then Some(Undirected)
|
||||||
|
else None
|
||||||
|
|
||||||
end GraphDisposition
|
end GraphDisposition
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,6 @@ object GraphTraversal:
|
||||||
/** Depth-first search that executes a side-effecting function on each
|
/** Depth-first search that executes a side-effecting function on each
|
||||||
* [[Vertex]]. This function will operate on _any_ [[Graph]].
|
* [[Vertex]]. This function will operate on _any_ [[Graph]].
|
||||||
*
|
*
|
||||||
* This implementation selects the first [[Vertex]] as an arbitrary starting
|
|
||||||
* point.
|
|
||||||
*
|
|
||||||
* @param graph
|
* @param graph
|
||||||
* The input [[Graph]] on which to run DFS.
|
* The input [[Graph]] on which to run DFS.
|
||||||
* @param visit
|
* @param visit
|
||||||
|
|
@ -27,25 +24,23 @@ object GraphTraversal:
|
||||||
val s = Stack.empty[Vertex]
|
val s = Stack.empty[Vertex]
|
||||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
|
|
||||||
graph.selectRoots().foreach { root =>
|
graph.view.foreach { root =>
|
||||||
val _ = s.push(root)
|
if !discovered(root.ordinal) then
|
||||||
while !s.isEmpty
|
val _ = s.push(root)
|
||||||
do
|
while !s.isEmpty
|
||||||
val v = s.pop()
|
do
|
||||||
if !discovered(v.ordinal) then
|
val v = s.pop()
|
||||||
val _ = visit(v)
|
if !discovered(v.ordinal) then
|
||||||
discovered(v.ordinal) = true
|
val _ = visit(v)
|
||||||
graph.neighbors(v).foreach(w => s.push(w))
|
discovered(v.ordinal) = true
|
||||||
else ()
|
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||||
()
|
else ()
|
||||||
|
else ()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
||||||
* some output. This function will operate on _any_ [[Graph]].
|
* some output. This function will operate on _any_ [[Graph]].
|
||||||
*
|
*
|
||||||
* This implementation selects the first [[Vertex]] as an arbitrary starting
|
|
||||||
* point.
|
|
||||||
*
|
|
||||||
* @param graph
|
* @param graph
|
||||||
* The input [[Graph]] on which to run DFS.
|
* The input [[Graph]] on which to run DFS.
|
||||||
* @param visit
|
* @param visit
|
||||||
|
|
@ -61,20 +56,35 @@ object GraphTraversal:
|
||||||
val s = Stack.empty[Vertex]
|
val s = Stack.empty[Vertex]
|
||||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
|
|
||||||
graph.selectRoots().foreach { root =>
|
graph.view.foreach { root =>
|
||||||
val _ = s.push(root)
|
if !discovered(root.ordinal) then
|
||||||
while !s.isEmpty
|
val _ = s.push(root)
|
||||||
do
|
while !s.isEmpty
|
||||||
val v = s.pop()
|
do
|
||||||
if !discovered(v.ordinal) then
|
val v = s.pop()
|
||||||
val _ = output.addOne(visit(v))
|
if !discovered(v.ordinal) then
|
||||||
discovered(v.ordinal) = true
|
val _ = output.addOne(visit(v))
|
||||||
graph.neighbors(v).foreach(w => s.push(w))
|
discovered(v.ordinal) = true
|
||||||
else ()
|
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||||
|
else ()
|
||||||
|
else ()
|
||||||
}
|
}
|
||||||
|
|
||||||
output.toList
|
output.toList
|
||||||
|
|
||||||
|
/** Depth-first search that executes a function on each [[Vertex]] in the
|
||||||
|
* context of some accumulator value to produce some accumulated output. This
|
||||||
|
* function will operate on _any_ [[Graph]].
|
||||||
|
*
|
||||||
|
* @param graph
|
||||||
|
* The input [[Graph]] on which to run DFS.
|
||||||
|
* @param initial
|
||||||
|
* The initial value.
|
||||||
|
* @param f
|
||||||
|
* The function to apply to each vertex in the context of an accumulator.
|
||||||
|
* @return
|
||||||
|
* The accumulated value.
|
||||||
|
*/
|
||||||
def dfsFold[Acc](
|
def dfsFold[Acc](
|
||||||
graph: Graph,
|
graph: Graph,
|
||||||
initial: Acc,
|
initial: Acc,
|
||||||
|
|
@ -84,16 +94,18 @@ object GraphTraversal:
|
||||||
val s = Stack.empty[Vertex]
|
val s = Stack.empty[Vertex]
|
||||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
|
|
||||||
graph.selectRoots().foreach { root =>
|
graph.view.foreach { root =>
|
||||||
val _ = s.push(root)
|
if !discovered(root.ordinal) then
|
||||||
while !s.isEmpty
|
val _ = s.push(root)
|
||||||
do
|
while !s.isEmpty
|
||||||
val v = s.pop()
|
do
|
||||||
if !discovered(v.ordinal) then
|
val v = s.pop()
|
||||||
acc = f(acc, v)
|
if !discovered(v.ordinal) then
|
||||||
discovered(v.ordinal) = true
|
acc = f(acc, v)
|
||||||
graph.neighbors(v).foreach(w => s.push(w))
|
discovered(v.ordinal) = true
|
||||||
else ()
|
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||||
|
else ()
|
||||||
|
else ()
|
||||||
}
|
}
|
||||||
|
|
||||||
acc
|
acc
|
||||||
|
|
@ -101,9 +113,6 @@ object GraphTraversal:
|
||||||
/** Depth-first search that executes a side-effecting function on each
|
/** Depth-first search that executes a side-effecting function on each
|
||||||
* [[Vertex]], accepting the data stored for that [[Vertex]] as input.
|
* [[Vertex]], accepting the data stored for that [[Vertex]] as input.
|
||||||
*
|
*
|
||||||
* This implementation performs DFS for _each root_ in the input
|
|
||||||
* [[AnyGraphWithData]].
|
|
||||||
*
|
|
||||||
* @param graph
|
* @param graph
|
||||||
* The input [[Graph]] on which to run DFS.
|
* The input [[Graph]] on which to run DFS.
|
||||||
* @param visit
|
* @param visit
|
||||||
|
|
@ -116,16 +125,18 @@ object GraphTraversal:
|
||||||
val s = Stack.empty[Vertex]
|
val s = Stack.empty[Vertex]
|
||||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
|
|
||||||
graph.selectRoots().foreach { root =>
|
graph.view.foreach { root =>
|
||||||
val _ = s.push(root)
|
if !discovered(root.ordinal) then
|
||||||
while !s.isEmpty
|
val _ = s.push(root)
|
||||||
do
|
while !s.isEmpty
|
||||||
val v = s.pop()
|
do
|
||||||
if !discovered(v.ordinal) then
|
val v = s.pop()
|
||||||
val _ = visit(v, graph.data(v.ordinal))
|
if !discovered(v.ordinal) then
|
||||||
discovered(v.ordinal) = true
|
val _ = visit(v, graph.data(v.ordinal))
|
||||||
graph.neighbors(v).foreach(w => s.push(w))
|
discovered(v.ordinal) = true
|
||||||
else ()
|
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||||
|
else ()
|
||||||
|
else ()
|
||||||
}
|
}
|
||||||
|
|
||||||
()
|
()
|
||||||
|
|
@ -133,9 +144,6 @@ object GraphTraversal:
|
||||||
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
||||||
* some output, accepting the data stored for that [[Vertex]] as input.
|
* some output, accepting the data stored for that [[Vertex]] as input.
|
||||||
*
|
*
|
||||||
* This implementation performs DFS for _each root_ in the input
|
|
||||||
* [[AnyGraphWithData]].
|
|
||||||
*
|
|
||||||
* @param graph
|
* @param graph
|
||||||
* The input [[Graph]] on which to run DFS.
|
* The input [[Graph]] on which to run DFS.
|
||||||
* @param visit
|
* @param visit
|
||||||
|
|
@ -174,16 +182,18 @@ object GraphTraversal:
|
||||||
val s = Stack.empty[Vertex]
|
val s = Stack.empty[Vertex]
|
||||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
|
|
||||||
graph.selectRoots().foreach { root =>
|
graph.view.foreach { root =>
|
||||||
val _ = s.push(root)
|
if !discovered(root.ordinal) then
|
||||||
while !s.isEmpty
|
val _ = s.push(root)
|
||||||
do
|
while !s.isEmpty
|
||||||
val v = s.pop()
|
do
|
||||||
if !discovered(v.ordinal) then
|
val v = s.pop()
|
||||||
acc = f(acc, graph.data(v.ordinal))
|
if !discovered(v.ordinal) then
|
||||||
discovered(v.ordinal) = true
|
acc = f(acc, graph.data(v.ordinal))
|
||||||
graph.neighbors(v).foreach(w => s.push(w))
|
discovered(v.ordinal) = true
|
||||||
else ()
|
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||||
|
else ()
|
||||||
|
else ()
|
||||||
}
|
}
|
||||||
|
|
||||||
acc
|
acc
|
||||||
|
|
@ -194,19 +204,23 @@ object GraphTraversal:
|
||||||
): Unit =
|
): Unit =
|
||||||
val q = Queue.empty[Vertex]
|
val q = Queue.empty[Vertex]
|
||||||
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
val _ = graph.selectRoots().foreach(q.enqueue)
|
|
||||||
|
|
||||||
while !q.isEmpty
|
graph.view.foreach { root =>
|
||||||
do
|
if !visited(root.ordinal) then
|
||||||
val v = q.dequeue()
|
val _ = q.enqueue(root)
|
||||||
if !visited(v.ordinal) then
|
while !q.isEmpty
|
||||||
val _ = visit(v)
|
do
|
||||||
visited(v.ordinal) = true
|
val v = q.dequeue()
|
||||||
graph.neighbors(v).foreach { neighbor =>
|
if !visited(v.ordinal) then
|
||||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
val _ = visit(v)
|
||||||
|
visited(v.ordinal) = true
|
||||||
|
graph.neighbors(v).foreach { neighbor =>
|
||||||
|
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||||
|
else ()
|
||||||
|
}
|
||||||
else ()
|
else ()
|
||||||
}
|
|
||||||
else ()
|
else ()
|
||||||
|
}
|
||||||
|
|
||||||
def bfs[Out](
|
def bfs[Out](
|
||||||
graph: Graph,
|
graph: Graph,
|
||||||
|
|
@ -215,19 +229,23 @@ object GraphTraversal:
|
||||||
val output = ListBuffer.empty[Out]
|
val output = ListBuffer.empty[Out]
|
||||||
val q = Queue.empty[Vertex]
|
val q = Queue.empty[Vertex]
|
||||||
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
val _ = graph.selectRoots().foreach(q.enqueue)
|
|
||||||
|
|
||||||
while !q.isEmpty
|
graph.view.foreach { root =>
|
||||||
do
|
if !visited(root.ordinal) then
|
||||||
val v = q.dequeue()
|
val _ = q.enqueue(root)
|
||||||
if !visited(v.ordinal) then
|
while !q.isEmpty
|
||||||
val _ = output.addOne(visit(v))
|
do
|
||||||
visited(v.ordinal) = true
|
val v = q.dequeue()
|
||||||
graph.neighbors(v).foreach { neighbor =>
|
if !visited(v.ordinal) then
|
||||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
val _ = output.addOne(visit(v))
|
||||||
|
visited(v.ordinal) = true
|
||||||
|
graph.neighbors(v).foreach { neighbor =>
|
||||||
|
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||||
|
else ()
|
||||||
|
}
|
||||||
else ()
|
else ()
|
||||||
}
|
|
||||||
else ()
|
else ()
|
||||||
|
}
|
||||||
|
|
||||||
output.toList
|
output.toList
|
||||||
|
|
||||||
|
|
@ -239,19 +257,23 @@ object GraphTraversal:
|
||||||
var acc = initial
|
var acc = initial
|
||||||
val q = Queue.empty[Vertex]
|
val q = Queue.empty[Vertex]
|
||||||
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
||||||
val _ = graph.selectRoots().foreach(q.enqueue)
|
|
||||||
|
|
||||||
while !q.isEmpty
|
graph.view.foreach { root =>
|
||||||
do
|
if !visited(root.ordinal) then
|
||||||
val v = q.dequeue()
|
val _ = q.enqueue(root)
|
||||||
if !visited(v.ordinal) then
|
while !q.isEmpty
|
||||||
acc = f(acc, v)
|
do
|
||||||
visited(v.ordinal) = true
|
val v = q.dequeue()
|
||||||
graph.neighbors(v).foreach { neighbor =>
|
if !visited(v.ordinal) then
|
||||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
acc = f(acc, v)
|
||||||
|
visited(v.ordinal) = true
|
||||||
|
graph.neighbors(v).foreach { neighbor =>
|
||||||
|
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||||
|
else ()
|
||||||
|
}
|
||||||
else ()
|
else ()
|
||||||
}
|
|
||||||
else ()
|
else ()
|
||||||
|
}
|
||||||
|
|
||||||
acc
|
acc
|
||||||
|
|
||||||
|
|
|
||||||
200
modules/core/src/test/scala/gs/graph/v0/BfsTraversalTests.scala
Normal file
200
modules/core/src/test/scala/gs/graph/v0/BfsTraversalTests.scala
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
package gs.graph.v0
|
||||||
|
|
||||||
|
import gs.graph.v0.BfsTraversalTests.Counter
|
||||||
|
import gs.graph.v0.BfsTraversalTests.Tracker
|
||||||
|
import gs.graph.v0.directed.Digraph
|
||||||
|
import munit.*
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
|
class BfsTraversalTests extends FunSuite:
|
||||||
|
|
||||||
|
test("should perform BFS on an empty graph") {
|
||||||
|
val c1 = new Counter
|
||||||
|
val _ = GraphTraversal.bfs(graph = UndirectedGraph.Empty, visit = c1.visit)
|
||||||
|
assertEquals(c1.count, 0)
|
||||||
|
|
||||||
|
val c2 = new Counter
|
||||||
|
val _ = GraphTraversal.bfs(graph = Digraph.Empty, visit = c2.visit)
|
||||||
|
assertEquals(c2.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform BFS on a graph of size 1") {
|
||||||
|
val c1 = new Counter
|
||||||
|
val _ = GraphTraversal.bfs(graph = UndirectedGraph.Single, visit = c1.visit)
|
||||||
|
assertEquals(c1.count, 1)
|
||||||
|
|
||||||
|
val c2 = new Counter
|
||||||
|
val _ = GraphTraversal.bfs(graph = Digraph.Single, visit = c2.visit)
|
||||||
|
assertEquals(c2.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform BFS on a complex directed graph") {
|
||||||
|
val n = Size(6)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = 0 -> 1,
|
||||||
|
1 -> 2,
|
||||||
|
2 -> 1,
|
||||||
|
0 -> 3,
|
||||||
|
3 -> 2,
|
||||||
|
4 -> 5
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 3, 2, 4, 5)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.bfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.bfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform BFS on a DAG") {
|
||||||
|
val n = Size(4)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = 0 -> 1,
|
||||||
|
0 -> 2,
|
||||||
|
1 -> 3,
|
||||||
|
2 -> 3
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2, 3)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.bfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.bfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform BFS on a strongly connected graph") {
|
||||||
|
val n = Size(3)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = 0 -> 1,
|
||||||
|
1 -> 2,
|
||||||
|
2 -> 0
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.bfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.bfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform BFS on a line") {
|
||||||
|
val n = Size(4)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = 0 -> 1,
|
||||||
|
1 -> 2,
|
||||||
|
2 -> 3
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2, 3)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.bfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.bfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform BFS on a larger complex digraph") {
|
||||||
|
val n = Size(10)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = 0 -> 1,
|
||||||
|
1 -> 2,
|
||||||
|
2 -> 3,
|
||||||
|
3 -> 6,
|
||||||
|
4 -> 5,
|
||||||
|
5 -> 6,
|
||||||
|
6 -> 7,
|
||||||
|
7 -> 8,
|
||||||
|
8 -> 9,
|
||||||
|
9 -> 6,
|
||||||
|
6 -> 8
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2, 3, 6, 7, 8, 9, 4, 5)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.bfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.bfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
object BfsTraversalTests:
|
||||||
|
|
||||||
|
trait BasicVisitor:
|
||||||
|
def visit(v: Vertex): Unit
|
||||||
|
|
||||||
|
class Counter extends BasicVisitor:
|
||||||
|
|
||||||
|
var count: Int = 0
|
||||||
|
|
||||||
|
def visit(v: Vertex): Unit =
|
||||||
|
count = count + 1
|
||||||
|
()
|
||||||
|
|
||||||
|
end Counter
|
||||||
|
|
||||||
|
class Tracker extends BasicVisitor:
|
||||||
|
|
||||||
|
private val output: ListBuffer[Int] = ListBuffer.empty
|
||||||
|
|
||||||
|
def snapshot(): List[Int] = output.toList
|
||||||
|
|
||||||
|
def visit(v: Vertex): Unit =
|
||||||
|
val _ = output.addOne(v.ordinal)
|
||||||
|
()
|
||||||
|
|
||||||
|
end Tracker
|
||||||
|
|
||||||
|
end BfsTraversalTests
|
||||||
203
modules/core/src/test/scala/gs/graph/v0/DfsTraversalTests.scala
Normal file
203
modules/core/src/test/scala/gs/graph/v0/DfsTraversalTests.scala
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
package gs.graph.v0
|
||||||
|
|
||||||
|
import gs.graph.v0.DfsTraversalTests.Counter
|
||||||
|
import gs.graph.v0.DfsTraversalTests.Tracker
|
||||||
|
import gs.graph.v0.directed.Digraph
|
||||||
|
import munit.*
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
|
class DfsTraversalTests extends FunSuite:
|
||||||
|
|
||||||
|
test("should perform DFS on an empty graph") {
|
||||||
|
val c1 = new Counter
|
||||||
|
val _ = GraphTraversal.dfs(graph = UndirectedGraph.Empty, visit = c1.visit)
|
||||||
|
assertEquals(c1.count, 0)
|
||||||
|
|
||||||
|
val c2 = new Counter
|
||||||
|
val _ = GraphTraversal.dfs(graph = Digraph.Empty, visit = c2.visit)
|
||||||
|
assertEquals(c2.count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform DFS on a graph of size 1") {
|
||||||
|
val c1 = new Counter
|
||||||
|
val _ = GraphTraversal.dfs(graph = UndirectedGraph.Single, visit = c1.visit)
|
||||||
|
assertEquals(c1.count, 1)
|
||||||
|
|
||||||
|
val c2 = new Counter
|
||||||
|
val _ = GraphTraversal.dfs(graph = Digraph.Single, visit = c2.visit)
|
||||||
|
assertEquals(c2.count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform DFS on a complex directed graph") {
|
||||||
|
// This test case includes:
|
||||||
|
// - Cycles
|
||||||
|
// - Multi-Root (0 and 4 are the roots)
|
||||||
|
val n = Size(6)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = (0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 1),
|
||||||
|
(0, 3),
|
||||||
|
(3, 2),
|
||||||
|
(4, 5)
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2, 3, 4, 5)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.dfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.dfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform DFS on a DAG") {
|
||||||
|
val n = Size(4)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = (0, 1),
|
||||||
|
(0, 2),
|
||||||
|
(1, 3),
|
||||||
|
(2, 3)
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 3, 2)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.dfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.dfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform DFS on a strongly connected graph") {
|
||||||
|
val n = Size(3)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = (0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 0)
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.dfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.dfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform DFS on a line") {
|
||||||
|
val n = Size(4)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = (0, 1),
|
||||||
|
(1, 2),
|
||||||
|
(2, 3)
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2, 3)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.dfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.dfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should perform DFS on a larger complex digraph") {
|
||||||
|
val n = Size(10)
|
||||||
|
val g = Graph.directed(
|
||||||
|
numberOfVertices = n,
|
||||||
|
edges = 0 -> 1,
|
||||||
|
1 -> 2,
|
||||||
|
2 -> 3,
|
||||||
|
3 -> 6,
|
||||||
|
4 -> 5,
|
||||||
|
5 -> 6,
|
||||||
|
6 -> 7,
|
||||||
|
7 -> 8,
|
||||||
|
8 -> 9,
|
||||||
|
9 -> 6,
|
||||||
|
6 -> 8
|
||||||
|
)
|
||||||
|
val expectedVisitOrder = List(0, 1, 2, 3, 6, 7, 8, 9, 4, 5)
|
||||||
|
val t = new Tracker
|
||||||
|
val b = ListBuffer.empty[Int]
|
||||||
|
val _ = GraphTraversal.dfs(g, t.visit)
|
||||||
|
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||||
|
val o2 = GraphTraversal.dfsFold(
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
(
|
||||||
|
acc,
|
||||||
|
vx
|
||||||
|
) => acc.addOne(vx.ordinal)
|
||||||
|
)
|
||||||
|
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||||
|
assertEquals(o1, expectedVisitOrder)
|
||||||
|
assertEquals(o2.toList, expectedVisitOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
object DfsTraversalTests:
|
||||||
|
|
||||||
|
trait BasicVisitor:
|
||||||
|
def visit(v: Vertex): Unit
|
||||||
|
|
||||||
|
class Counter extends BasicVisitor:
|
||||||
|
|
||||||
|
var count: Int = 0
|
||||||
|
|
||||||
|
def visit(v: Vertex): Unit =
|
||||||
|
count = count + 1
|
||||||
|
()
|
||||||
|
|
||||||
|
end Counter
|
||||||
|
|
||||||
|
class Tracker extends BasicVisitor:
|
||||||
|
|
||||||
|
private val output: ListBuffer[Int] = ListBuffer.empty
|
||||||
|
|
||||||
|
def snapshot(): List[Int] = output.toList
|
||||||
|
|
||||||
|
def visit(v: Vertex): Unit =
|
||||||
|
val _ = output.addOne(v.ordinal)
|
||||||
|
()
|
||||||
|
|
||||||
|
end Tracker
|
||||||
|
|
||||||
|
end DfsTraversalTests
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package gs.graph.v0
|
||||||
|
|
||||||
|
import munit.*
|
||||||
|
|
||||||
|
class GraphDispositionTests extends FunSuite:
|
||||||
|
|
||||||
|
test("should parse and match all defined dispositions") {
|
||||||
|
val d1 = GraphDisposition.Directed
|
||||||
|
val d2 = GraphDisposition.Undirected
|
||||||
|
assertEquals(d1, GraphDisposition.Directed)
|
||||||
|
assertEquals(d2, GraphDisposition.Undirected)
|
||||||
|
assertEquals(d1.equals(d2), false)
|
||||||
|
assertEquals(d1.hashCode(), GraphDisposition.Directed.hashCode())
|
||||||
|
assertEquals(d2.hashCode(), GraphDisposition.Undirected.hashCode())
|
||||||
|
assertEquals(d1.toString(), GraphDisposition.Directed.name)
|
||||||
|
assertEquals(d2.toString(), GraphDisposition.Undirected.name)
|
||||||
|
assertEquals(d1.equals(null), false)
|
||||||
|
assertEquals(d1.equals("foo"), false)
|
||||||
|
assertEquals(d1.equals(1), false)
|
||||||
|
|
||||||
|
val p1 = GraphDisposition.parse(d1.name)
|
||||||
|
val p2 = GraphDisposition.parse(d2.name)
|
||||||
|
val p3 = GraphDisposition.parse("something else")
|
||||||
|
|
||||||
|
assertEquals(p3, None)
|
||||||
|
assertEquals(p1, Some(d1))
|
||||||
|
assertEquals(p2, Some(d2))
|
||||||
|
}
|
||||||
|
|
@ -17,19 +17,38 @@ class VertexTests extends FunSuite:
|
||||||
assertEquals(v1.hashCode(), v2.hashCode())
|
assertEquals(v1.hashCode(), v2.hashCode())
|
||||||
assertNotEquals(v1.hashCode(), v3.hashCode())
|
assertNotEquals(v1.hashCode(), v3.hashCode())
|
||||||
assertEquals(v1 < v3, true)
|
assertEquals(v1 < v3, true)
|
||||||
|
assertEquals(v1 <= v3, true)
|
||||||
assertEquals(v1 < v2, false)
|
assertEquals(v1 < v2, false)
|
||||||
|
assertEquals(v1 <= v2, true)
|
||||||
assertEquals(v1 > v3, false)
|
assertEquals(v1 > v3, false)
|
||||||
|
assertEquals(v1 >= v3, false)
|
||||||
assertEquals(v3 > v1, true)
|
assertEquals(v3 > v1, true)
|
||||||
|
assertEquals(v3 >= v1, true)
|
||||||
|
assertEquals(v2 >= v1, true)
|
||||||
|
assertEquals(v2 > v1, false)
|
||||||
assertEquals(v1 < 3, true)
|
assertEquals(v1 < 3, true)
|
||||||
|
assertEquals(v1 <= 3, true)
|
||||||
assertEquals(v1 < 0, false)
|
assertEquals(v1 < 0, false)
|
||||||
|
assertEquals(v1 <= 0, true)
|
||||||
assertEquals(v1 > 0, false)
|
assertEquals(v1 > 0, false)
|
||||||
|
assertEquals(v1 >= 0, true)
|
||||||
assertEquals(v3 > 0, true)
|
assertEquals(v3 > 0, true)
|
||||||
|
assertEquals(v3 >= 0, true)
|
||||||
assertEquals(v1 < Size(3), true)
|
assertEquals(v1 < Size(3), true)
|
||||||
|
assertEquals(v1 <= Size(3), true)
|
||||||
assertEquals(v1 < Size.Zero, false)
|
assertEquals(v1 < Size.Zero, false)
|
||||||
|
assertEquals(v1 <= Size.Zero, true)
|
||||||
assertEquals(v1 > Size.Zero, false)
|
assertEquals(v1 > Size.Zero, false)
|
||||||
|
assertEquals(v1 >= Size.Zero, true)
|
||||||
assertEquals(v3 > Size.Zero, true)
|
assertEquals(v3 > Size.Zero, true)
|
||||||
|
assertEquals(v3 >= Size.Zero, true)
|
||||||
assertEquals(v1.toString(), v2.toString())
|
assertEquals(v1.toString(), v2.toString())
|
||||||
assertNotEquals(v1.toString(), v3.toString())
|
assertNotEquals(v1.toString(), v3.toString())
|
||||||
|
assertEquals(v1.compare(v3), -3)
|
||||||
|
assertEquals(v1.compare(v2), 0)
|
||||||
|
assertEquals(v1.value, 0)
|
||||||
|
assertEquals(v2.value, 0)
|
||||||
|
assertEquals(v3.value, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should throw an exception if an invalid value is given") {
|
test("should throw an exception if an invalid value is given") {
|
||||||
|
|
@ -38,3 +57,26 @@ class VertexTests extends FunSuite:
|
||||||
case Some(cause) =>
|
case Some(cause) =>
|
||||||
assertEquals(cause.getMessage(), "Vertex values must be 0 or greater.")
|
assertEquals(cause.getMessage(), "Vertex values must be 0 or greater.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("should instantiate a list of vertices") {
|
||||||
|
val vs = Vertex.list(0, 1, 2, 3)
|
||||||
|
assertEquals(vs, List(0, 1, 2, 3).map(Vertex(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should instantiate a set of vertices") {
|
||||||
|
val vs = Vertex.set(0, 1, 2, 3, 0, 1, 2, 3)
|
||||||
|
assertEquals(vs, Set(0, 1, 2, 3).map(Vertex(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should support the ordering type class") {
|
||||||
|
val o = Ordering[Vertex]
|
||||||
|
val v1 = Vertex(0)
|
||||||
|
val v2 = Vertex(0)
|
||||||
|
val v3 = Vertex(1)
|
||||||
|
val v4 = Vertex(2)
|
||||||
|
assertEquals(o.compare(v1, v2), 0)
|
||||||
|
assertEquals(o.compare(v1, v3), -1)
|
||||||
|
assertEquals(o.compare(v1, v4), -2)
|
||||||
|
assertEquals(o.compare(v4, v1), 2)
|
||||||
|
assertEquals(o.compare(v4, v3), 1)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue