(minor) WIP continuing to fix, refine, refactor, test
This commit is contained in:
parent
95e9ec5e08
commit
117bff5242
15 changed files with 467 additions and 74 deletions
|
|
@ -35,6 +35,12 @@ val Deps = new {
|
|||
}
|
||||
|
||||
val Gs = new {
|
||||
val Std = new {
|
||||
private val Version: String = "0.1.3"
|
||||
|
||||
val Core: ModuleID = "gs" %% "gs-std-core-v0" % Version
|
||||
}
|
||||
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +67,7 @@ lazy val core = project
|
|||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
|
||||
.settings(libraryDependencies ++= Seq(Deps.Gs.Std.Core))
|
||||
|
||||
lazy val cats = project
|
||||
.in(file("modules/cats"))
|
||||
|
|
|
|||
|
|
@ -133,10 +133,10 @@ object Adjacency:
|
|||
): Adjacency =
|
||||
val buffs = Vector.fill(numberOfVertices.value)(ListBuffer.empty[Vertex])
|
||||
val _ = edges.foreach { edge =>
|
||||
if edge.v1 >= numberOfVertices || edge.v2 >= numberOfVertices then
|
||||
throw new IllegalArgumentException(
|
||||
s"Edge (${edge.v1}, ${edge.v2}) is out of bounds. Maximum vertex value is ${numberOfVertices.value - 1}"
|
||||
)
|
||||
if edge.v1 >= numberOfVertices then
|
||||
throw GraphException.VertexExceedsGraphBounds(edge.v1, numberOfVertices)
|
||||
else if edge.v2 >= numberOfVertices then
|
||||
throw GraphException.VertexExceedsGraphBounds(edge.v2, numberOfVertices)
|
||||
else
|
||||
val _ = buffs(edge.from.ordinal).addOne(edge.to)
|
||||
}
|
||||
|
|
|
|||
135
modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala
Normal file
135
modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.Vertex
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.mutable.Queue
|
||||
|
||||
/** Represents a _ Connected Component_.
|
||||
*
|
||||
* TODO: ADD EDGES!
|
||||
*/
|
||||
final class ConnectedComponent private (val value: Set[Vertex]):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: ConnectedComponent => value.equals(other.value)
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = value.hashCode()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String =
|
||||
val sb = StringBuilder()
|
||||
sb.append("[")
|
||||
sb.append(value.mkString(","))
|
||||
sb.append("]")
|
||||
sb.toString()
|
||||
|
||||
/** @return
|
||||
* The number of vertices contained by this component.
|
||||
*/
|
||||
def size: Int = value.size
|
||||
|
||||
/** @return
|
||||
* True if this component contains a single vertex.
|
||||
*/
|
||||
def isSingle: Boolean = value.size == 1
|
||||
|
||||
/** @return
|
||||
* The set of vertices in the Connected Component.
|
||||
*/
|
||||
def vertices: Set[Vertex] = value
|
||||
|
||||
/** Assign an arbitrary vertex as the root vertex.
|
||||
*/
|
||||
lazy val root: Vertex = value.toList.head
|
||||
|
||||
end ConnectedComponent
|
||||
|
||||
object ConnectedComponent:
|
||||
|
||||
/** Instantiate a new Connected Component from the given collection of
|
||||
* [[Vertex]].
|
||||
*
|
||||
* This constructor ensures that each [[Vertex]] only occurs once.
|
||||
*
|
||||
* @param value
|
||||
* The input collection.
|
||||
* @return
|
||||
* The new Connected Component.
|
||||
*/
|
||||
def apply(value: Vertex*): ConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty connected components are not allowed."
|
||||
)
|
||||
else new ConnectedComponent(value.toSet)
|
||||
|
||||
def of(value: Int*): ConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty connected components are not allowed."
|
||||
)
|
||||
else new ConnectedComponent(value.map(Vertex.apply).toSet)
|
||||
|
||||
/** Instantiate a new Connected Component that contains a single [[Vertex]].
|
||||
*
|
||||
* @param value
|
||||
* The input vertex.
|
||||
* @return
|
||||
* The new Connected Component.
|
||||
*/
|
||||
def single(value: Vertex): ConnectedComponent =
|
||||
new ConnectedComponent(Set(value))
|
||||
|
||||
given CanEqual[ConnectedComponent, ConnectedComponent] = CanEqual.derived
|
||||
|
||||
def findAll(g: Graph): List[ConnectedComponent] =
|
||||
if g.isEmpty then Nil
|
||||
else
|
||||
val tracker = Tracker.initialize(g)
|
||||
val ccs = ListBuffer.empty[ConnectedComponent]
|
||||
(0 until g.numberOfVertices.value).foreach { ordinal =>
|
||||
val v = Vertex(ordinal)
|
||||
if !tracker.isVisited(v) then ccs.addOne(bfsFromVertex(g, v, tracker))
|
||||
else ()
|
||||
}
|
||||
ccs.toList
|
||||
|
||||
private def bfsFromVertex(
|
||||
g: Graph,
|
||||
v: Vertex,
|
||||
t: Tracker
|
||||
): ConnectedComponent =
|
||||
val q = Queue.empty[Vertex]
|
||||
val b = ListBuffer.empty[Vertex]
|
||||
q.enqueue(v)
|
||||
|
||||
while !q.isEmpty
|
||||
do
|
||||
val current = q.dequeue()
|
||||
t.visit(current)
|
||||
b.addOne(current)
|
||||
g.neighbors(current).foreach { neighbor =>
|
||||
if !t.isVisited(neighbor) then q.enqueue(neighbor)
|
||||
else ()
|
||||
}
|
||||
|
||||
new ConnectedComponent(b.toSet)
|
||||
|
||||
private class Tracker(val data: Array[Boolean]):
|
||||
def visit(v: Vertex): Unit = data(v.ordinal) = true
|
||||
|
||||
def isVisited(v: Vertex): Boolean = data(v.ordinal)
|
||||
|
||||
private object Tracker:
|
||||
|
||||
def initialize(g: Graph): Tracker =
|
||||
new Tracker(Array.fill(g.numberOfVertices.value)(false))
|
||||
|
||||
end ConnectedComponent
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.directed.Digraph
|
||||
|
||||
/** Graph representation based on an adjacency list.
|
||||
*
|
||||
* See:
|
||||
|
|
@ -32,6 +34,11 @@ trait Graph:
|
|||
*/
|
||||
def isEmpty: Boolean = numberOfVertices == Size.Zero
|
||||
|
||||
/** @return
|
||||
* True if this graph has one vertex, false otherwise.
|
||||
*/
|
||||
def isSingle: Boolean = numberOfVertices == Size.One
|
||||
|
||||
/** @return
|
||||
* The roots that should be used for traversal operations.
|
||||
*/
|
||||
|
|
@ -95,4 +102,22 @@ object Graph:
|
|||
adj
|
||||
)
|
||||
|
||||
/** Construct a new [[Digraph]] from the given [[Edge]].
|
||||
*
|
||||
* @param numberOfVertices
|
||||
* The number of vertices in this [[Graph]].
|
||||
* @param edges
|
||||
* The [[Edge]] in this graph.
|
||||
* @return
|
||||
* The new [[Digraph]].
|
||||
*/
|
||||
def directed(
|
||||
numberOfVertices: Size,
|
||||
edges: (Int, Int)*
|
||||
): Digraph =
|
||||
Digraph.fromEdges(
|
||||
numberOfVertices,
|
||||
edges.map(pair => Edge(pair._1, pair._2))
|
||||
)
|
||||
|
||||
end Graph
|
||||
|
|
|
|||
|
|
@ -19,22 +19,20 @@ class UndirectedGraph(
|
|||
*/
|
||||
override def selectRoots(): Vector[Vertex] = roots
|
||||
|
||||
/** The roots of an undirected graph are identified by arbitrarily selecting
|
||||
* the first listed vertex from each strongly connected component in the
|
||||
* graph.
|
||||
/** The lazily calculated roots of this graph.
|
||||
*
|
||||
* In an undirected graph, the roots are calculated by selecting an arbitrary
|
||||
* vertex from each connected component in the graph.
|
||||
*/
|
||||
lazy val roots: Vector[Vertex] =
|
||||
calculateRoots()
|
||||
|
||||
private def calculateRoots(): Vector[Vertex] =
|
||||
if numberOfVertices == Size.Zero then Vector.empty
|
||||
else oneRootFromEachSCC(calculateSCCs()).toVector
|
||||
else oneFromEachConnectedComponent().toVector
|
||||
|
||||
private def calculateSCCs(): List[SCC] =
|
||||
SCC.findAll(this)
|
||||
|
||||
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
|
||||
sccs.map(_.root)
|
||||
private def oneFromEachConnectedComponent(): List[Vertex] =
|
||||
ConnectedComponent.findAll(this).map(_.root)
|
||||
|
||||
end UndirectedGraph
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,24 @@ object Vertex:
|
|||
else
|
||||
throw new IllegalArgumentException("Vertex values must be 0 or greater.")
|
||||
|
||||
/** Instantiate an immutable sequence of vertices.
|
||||
*
|
||||
* Throws an `IllegalArgumentException` if any input value is less than 0.
|
||||
*
|
||||
* @param values
|
||||
* The integer values to validate.
|
||||
* @return
|
||||
* The new vertices.
|
||||
*/
|
||||
def of(values: Int*): Vector[Vertex] =
|
||||
values.map(apply).toVector
|
||||
|
||||
def list(values: Int*): List[Vertex] =
|
||||
values.map(apply).toList
|
||||
|
||||
def set(values: Int*): Set[Vertex] =
|
||||
values.map(apply).toSet
|
||||
|
||||
given CanEqual[Vertex, Vertex] = CanEqual.derived
|
||||
|
||||
given Ordering[Vertex] with
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ class Dag protected (
|
|||
|
||||
object Dag:
|
||||
|
||||
/** The empty DAG.
|
||||
*/
|
||||
final val Empty: Dag =
|
||||
new Dag(Size.Zero, Adjacency.Empty, Vector.empty)
|
||||
|
||||
/** DAG with a single vertex.
|
||||
*/
|
||||
final val Single: Dag =
|
||||
new Dag(Size.One, Adjacency.Single, Vector(Vertex.Zero))
|
||||
|
||||
given CanEqual[Dag, Dag] = CanEqual.derived
|
||||
given CanEqual[Dag, Digraph] = CanEqual.derived
|
||||
given CanEqual[Digraph, Dag] = CanEqual.derived
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ class Digraph(
|
|||
case other: Digraph =>
|
||||
numberOfVertices == other.numberOfVertices
|
||||
&& adjacency == other.adjacency
|
||||
&& roots.sameElements(other.roots)
|
||||
case _ => false
|
||||
|
||||
/** `True` if this directed graph has a cycle, `false` otherwise.
|
||||
*/
|
||||
lazy val hasCycle: Boolean = Digraph.hasCycle(this)
|
||||
|
||||
end Digraph
|
||||
|
||||
object Digraph:
|
||||
|
|
@ -122,6 +125,7 @@ object Digraph:
|
|||
def hasCycle(digraph: Digraph): Boolean =
|
||||
digraph match
|
||||
case _ if digraph.isEmpty => false
|
||||
case _ if digraph.isSingle => false
|
||||
case _: Dag => false
|
||||
case _ => new CycleDetector(digraph).hasCycle
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package gs.graph.v0
|
||||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Vertex
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.mutable.Stack
|
||||
|
|
@ -8,14 +7,19 @@ import scala.collection.mutable.Stack
|
|||
/** Represents a _Strongly Connected Component_ (SCC).
|
||||
*
|
||||
* Each SCC is guaranteed to be non-empty.
|
||||
*
|
||||
* This class is used with [[Digraph]]. See [[gs.graph.v0.ConnectedComponent]]
|
||||
* for general connected components.
|
||||
*
|
||||
* TODO: ADD EDGES
|
||||
*/
|
||||
final class SCC private (val value: Set[Vertex]):
|
||||
final class StronglyConnectedComponent private (val value: Set[Vertex]):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: SCC => value.equals(other.value)
|
||||
case other: StronglyConnectedComponent => value.equals(other.value)
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
|
|
@ -50,9 +54,9 @@ final class SCC private (val value: Set[Vertex]):
|
|||
*/
|
||||
lazy val root: Vertex = value.toList.head
|
||||
|
||||
end SCC
|
||||
end StronglyConnectedComponent
|
||||
|
||||
object SCC:
|
||||
object StronglyConnectedComponent:
|
||||
|
||||
/** Instantiate a new SCC from the given collection of [[Vertex]].
|
||||
*
|
||||
|
|
@ -63,19 +67,19 @@ object SCC:
|
|||
* @return
|
||||
* The new SCC.
|
||||
*/
|
||||
def apply(value: Vertex*): SCC =
|
||||
def apply(value: Vertex*): StronglyConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty strongly connected components (SCC) are not allowed."
|
||||
)
|
||||
else new SCC(value.toSet)
|
||||
else new StronglyConnectedComponent(value.toSet)
|
||||
|
||||
def of(value: Int*): SCC =
|
||||
def of(value: Int*): StronglyConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty strongly connected components (SCC) are not allowed."
|
||||
)
|
||||
else new SCC(value.map(Vertex.apply).toSet)
|
||||
else new StronglyConnectedComponent(value.map(Vertex.apply).toSet)
|
||||
|
||||
/** Instantiate a new SCC that contains a single [[Vertex]].
|
||||
*
|
||||
|
|
@ -84,9 +88,11 @@ object SCC:
|
|||
* @return
|
||||
* The new SCC.
|
||||
*/
|
||||
def single(value: Vertex): SCC = new SCC(Set(value))
|
||||
def single(value: Vertex): StronglyConnectedComponent =
|
||||
new StronglyConnectedComponent(Set(value))
|
||||
|
||||
given CanEqual[SCC, SCC] = CanEqual.derived
|
||||
given CanEqual[StronglyConnectedComponent, StronglyConnectedComponent] =
|
||||
CanEqual.derived
|
||||
|
||||
/** Alias for `findAll`.
|
||||
*
|
||||
|
|
@ -101,7 +107,7 @@ object SCC:
|
|||
* @return
|
||||
* The complete list of [[SCC]] for the input graph.
|
||||
*/
|
||||
def tarjan(g: Graph): List[SCC] = findAll(g)
|
||||
def tarjan(g: Digraph): List[StronglyConnectedComponent] = findAll(g)
|
||||
|
||||
/** Implementation of Tarjan's Algorithm for finding all strongly connected
|
||||
* components for some graph.
|
||||
|
|
@ -114,7 +120,7 @@ object SCC:
|
|||
* @return
|
||||
* The complete list of [[SCC]] for the input graph.
|
||||
*/
|
||||
def findAll(g: Graph): List[SCC] =
|
||||
def findAll(g: Digraph): List[StronglyConnectedComponent] =
|
||||
val V = g.numberOfVertices.value
|
||||
|
||||
// Vertex numbers.
|
||||
|
|
@ -139,7 +145,7 @@ object SCC:
|
|||
val counter = new Counter(0)
|
||||
|
||||
// List of output SCCs.
|
||||
val sccs = ListBuffer.empty[SCC]
|
||||
val sccs = ListBuffer.empty[StronglyConnectedComponent]
|
||||
|
||||
(0 until V).foreach { v =>
|
||||
if !visited(v) then
|
||||
|
|
@ -151,7 +157,7 @@ object SCC:
|
|||
sccs.toList
|
||||
|
||||
private def tarjanDfs(
|
||||
g: Graph,
|
||||
g: Digraph,
|
||||
v: Int,
|
||||
num: Array[Int],
|
||||
lowest: Array[Int],
|
||||
|
|
@ -159,7 +165,7 @@ object SCC:
|
|||
processed: Array[Boolean],
|
||||
s: Stack[Vertex],
|
||||
counter: Counter,
|
||||
output: ListBuffer[SCC]
|
||||
output: ListBuffer[StronglyConnectedComponent]
|
||||
): Unit =
|
||||
// Convenience integer representation for array access.
|
||||
val V = Vertex(v)
|
||||
|
|
@ -195,7 +201,8 @@ object SCC:
|
|||
sccVertex = s.pop()
|
||||
|
||||
scc.addOne(sccVertex)
|
||||
if !scc.isEmpty then output.addOne(new SCC(scc.toSet))
|
||||
if !scc.isEmpty then
|
||||
output.addOne(new StronglyConnectedComponent(scc.toSet))
|
||||
else println("ERROR")
|
||||
else ()
|
||||
|
||||
|
|
@ -213,4 +220,4 @@ object SCC:
|
|||
|
||||
end Counter
|
||||
|
||||
end SCC
|
||||
end StronglyConnectedComponent
|
||||
|
|
@ -1,10 +1,26 @@
|
|||
package gs.graph.v0.syntax
|
||||
|
||||
import gs.graph.v0.Edge
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.GraphTraversal
|
||||
import gs.graph.v0.Vertex
|
||||
import gs.graph.v0.data.AnyGraphWithData
|
||||
|
||||
/** Construction helper for [[Vertex]] values.
|
||||
*
|
||||
* @param value
|
||||
* The value of the vertex.
|
||||
* @return
|
||||
* The new [[Vertex]] instance.
|
||||
*/
|
||||
def V(value: Int): Vertex = Vertex(value)
|
||||
|
||||
extension (value: (Int, Int))
|
||||
/** @return
|
||||
* The [[Edge]] represented by this tuple.
|
||||
*/
|
||||
def edge: Edge = Edge(value._1, value._2)
|
||||
|
||||
extension (graph: Graph)
|
||||
|
||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.syntax.V
|
||||
import scala.util.Failure
|
||||
import scala.util.Try
|
||||
|
||||
class AdjacencyTests extends munit.FunSuite:
|
||||
|
||||
private val size = Size(7)
|
||||
private val vs = (0 until size.value).map(Vertex(_)).toArray
|
||||
|
||||
private val edges = Edge.list(
|
||||
0 -> 1,
|
||||
|
|
@ -32,13 +32,13 @@ class AdjacencyTests extends munit.FunSuite:
|
|||
}
|
||||
|
||||
test("should provide incoming connections") {
|
||||
assertEquals(adj.incoming(vs(0)), Vector.empty)
|
||||
assertEquals(adj.incoming(vs(1)), Vector(vs(0)))
|
||||
assertEquals(adj.incoming(vs(2)), Vector(vs(0)))
|
||||
assertEquals(adj.incoming(vs(3)), Vector(vs(0)))
|
||||
assertEquals(adj.incoming(vs(4)), Vector(vs(1), vs(2), vs(3)))
|
||||
assertEquals(adj.incoming(vs(5)), Vector(vs(3)))
|
||||
assertEquals(adj.incoming(vs(6)), Vector(vs(4)))
|
||||
assertEquals(adj.incoming(V(0)), Vector.empty)
|
||||
assertEquals(adj.incoming(V(1)), Vertex.of(0))
|
||||
assertEquals(adj.incoming(V(2)), Vertex.of(0))
|
||||
assertEquals(adj.incoming(V(3)), Vertex.of(0))
|
||||
assertEquals(adj.incoming(V(4)), Vertex.of(1, 2, 3))
|
||||
assertEquals(adj.incoming(V(5)), Vertex.of(3))
|
||||
assertEquals(adj.incoming(V(6)), Vertex.of(4))
|
||||
}
|
||||
|
||||
test("should show no incoming connections for a vertex not in the graph") {
|
||||
|
|
@ -66,19 +66,32 @@ class AdjacencyTests extends munit.FunSuite:
|
|||
}
|
||||
|
||||
test(
|
||||
"should guard against out of bound values when building from edges with an explicit bound"
|
||||
"should guard against out of bound values when building from edges with an explicit bound (first vertex)"
|
||||
) {
|
||||
val n = Size(2)
|
||||
val es = Edge.list(3 -> 0)
|
||||
Try(Adjacency.fromEdges(n, es)) match
|
||||
case Failure(ex: GraphException.VertexExceedsGraphBounds) =>
|
||||
assertEquals(ex.vertex, Vertex(3))
|
||||
assertEquals(ex.bound, n)
|
||||
case _ =>
|
||||
fail(
|
||||
"Expected adjacency construction to fail with a GraphException."
|
||||
)
|
||||
}
|
||||
|
||||
test(
|
||||
"should guard against out of bound values when building from edges with an explicit bound (second vertex)"
|
||||
) {
|
||||
val n = Size(2)
|
||||
val es = Edge.list(0 -> 3)
|
||||
Try(Adjacency.fromEdges(n, es)) match
|
||||
case Failure(ex: IllegalArgumentException) =>
|
||||
assertEquals(
|
||||
ex.getMessage(),
|
||||
"Edge (0, 3) is out of bounds. Maximum vertex value is 1"
|
||||
)
|
||||
case Failure(ex: GraphException.VertexExceedsGraphBounds) =>
|
||||
assertEquals(ex.vertex, Vertex(3))
|
||||
assertEquals(ex.bound, n)
|
||||
case _ =>
|
||||
fail(
|
||||
"Expected adjacency construction to fail with an IllegalArgumentException."
|
||||
"Expected adjacency construction to fail with a GraphException."
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,48 @@ class UndirectedGraphTests extends FunSuite:
|
|||
assertEquals(g.selectRoots(), Vector.empty)
|
||||
}
|
||||
|
||||
test(
|
||||
"should calculate roots based on an arbitrary vertex from each connected component"
|
||||
) {
|
||||
// TODO: implement - current logic is wrong.
|
||||
test("should construct a graph based on # of edges and edge list") {
|
||||
val N = Size(3)
|
||||
val g = Graph.undirected(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
0 -> 2
|
||||
)
|
||||
|
||||
assertEquals(g.numberOfVertices, N)
|
||||
assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2)))
|
||||
assertEquals(g.neighbors(Vertex(4)), Vector.empty)
|
||||
assertEquals(g.selectRoots(), Vector(Vertex.Zero))
|
||||
}
|
||||
|
||||
test("should construct a graph based on edge list") {
|
||||
val N = Size(3)
|
||||
val g = Graph.undirected(
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
0 -> 2
|
||||
)
|
||||
|
||||
assertEquals(g.numberOfVertices, N)
|
||||
assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2)))
|
||||
assertEquals(g.neighbors(Vertex(4)), Vector.empty)
|
||||
assertEquals(g.selectRoots(), Vector(Vertex.Zero))
|
||||
}
|
||||
|
||||
test("should calculate roots for each connected component") {
|
||||
val N = Size(10)
|
||||
val g = Graph.undirected(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 3,
|
||||
4 -> 5,
|
||||
5 -> 6,
|
||||
4 -> 6,
|
||||
7 -> 8
|
||||
)
|
||||
val expectedRoots = Vertex.of(0, 4, 7, 9)
|
||||
|
||||
assertEquals(g.roots, expectedRoots)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ class DagTests extends FunSuite:
|
|||
fail("Expected a graph with one vertex to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should not have cycles by definition") {
|
||||
Dag.validate(Digraph.Single) match
|
||||
case Right(dag) =>
|
||||
assertEquals(Digraph.hasCycle(dag), false)
|
||||
assertEquals(dag.hasCycle, false)
|
||||
case _ =>
|
||||
fail("Expected a graph with one vertex to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should validate a single-root graph") {
|
||||
val size = Size(8)
|
||||
val digraph: Digraph = Digraph.fromAdjacency(
|
||||
|
|
@ -40,7 +49,10 @@ class DagTests extends FunSuite:
|
|||
)
|
||||
|
||||
Dag.validate(digraph) match
|
||||
case Right(dag) => assertEquals(dag, digraph)
|
||||
case Right(dag) =>
|
||||
assertEquals(dag, digraph)
|
||||
assertEquals(Digraph.hasCycle(dag), false)
|
||||
assertEquals(dag.hasCycle, false)
|
||||
case _ => fail("Expected a single-root graph to be validated as a DAG.")
|
||||
}
|
||||
|
||||
|
|
@ -65,3 +77,8 @@ class DagTests extends FunSuite:
|
|||
)
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
test("should provide empty and single graphs") {
|
||||
assertEquals(Dag.Empty, Digraph.Empty)
|
||||
assertEquals(Dag.Single, Digraph.Single)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import munit.*
|
||||
|
||||
class DigraphTests extends FunSuite:
|
||||
|
||||
test("should provide an empty graph") {
|
||||
val dg = Digraph.Empty
|
||||
assertEquals(dg.numberOfVertices, Size.Zero)
|
||||
assertEquals(dg.isSingle, false)
|
||||
assertEquals(dg.roots, Vector.empty)
|
||||
assertEquals(dg.isEmpty, true)
|
||||
assertEquals(dg.neighbors(Vertex.Zero), Vector.empty)
|
||||
assertEquals(dg.hasCycle, false)
|
||||
}
|
||||
|
||||
test("should provide a single-node graph") {
|
||||
val dg = Digraph.Single
|
||||
assertEquals(dg.numberOfVertices, Size.One)
|
||||
assertEquals(dg.isSingle, true)
|
||||
assertEquals(dg.roots, Vertex.of(0))
|
||||
assertEquals(dg.isEmpty, false)
|
||||
assertEquals(dg.neighbors(Vertex.Zero), Vector.empty)
|
||||
assertEquals(dg.hasCycle, false)
|
||||
}
|
||||
|
||||
test("should not equal non-digraph types") {
|
||||
val dg = Digraph.Empty
|
||||
assertEquals(dg.equals(null), false)
|
||||
assertEquals(dg.equals("foo"), false)
|
||||
assertEquals(dg.equals(1), false)
|
||||
}
|
||||
|
||||
test("should not equal a digraph of a different size") {
|
||||
assertNotEquals(Digraph.Empty, Digraph.Single)
|
||||
}
|
||||
|
||||
test("should not equal a digraph with different edges") {
|
||||
val N = Size(3)
|
||||
val d1 = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2
|
||||
)
|
||||
val d2 = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
0 -> 2
|
||||
)
|
||||
assertNotEquals(d1, d2)
|
||||
}
|
||||
|
||||
test(
|
||||
"should detect cycles where a single component has a cycle back to the root"
|
||||
) {
|
||||
val N = Size(3)
|
||||
val dg = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 0
|
||||
)
|
||||
assertEquals(dg.hasCycle, true)
|
||||
}
|
||||
|
||||
test("should detect cycles where one component does not have a cycle") {
|
||||
val N = Size(6)
|
||||
val dg = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
3 -> 4,
|
||||
4 -> 5,
|
||||
5 -> 3
|
||||
)
|
||||
assertEquals(dg.hasCycle, true)
|
||||
}
|
||||
|
||||
test("should detect cycles within a subset of a component") {
|
||||
val N = Size(3)
|
||||
val dg = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 1
|
||||
)
|
||||
assertEquals(dg.hasCycle, true)
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package gs.graph.v0
|
||||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.directed.Digraph
|
||||
import gs.graph.v0.Adjacency
|
||||
import gs.graph.v0.Edge
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import munit.*
|
||||
|
||||
class TarjansTests extends FunSuite:
|
||||
|
|
@ -9,9 +13,9 @@ class TarjansTests extends FunSuite:
|
|||
val v0 = Vertex.Zero
|
||||
val v1 = Vertex(1)
|
||||
val v2 = Vertex(2)
|
||||
val s1 = SCC(v0)
|
||||
val s2 = SCC(v0, v1, v2)
|
||||
val s3 = SCC(v0)
|
||||
val s1 = StronglyConnectedComponent(v0)
|
||||
val s2 = StronglyConnectedComponent(v0, v1, v2)
|
||||
val s3 = StronglyConnectedComponent(v0)
|
||||
|
||||
assertEquals(s1, s3)
|
||||
assertNotEquals(s1, s2)
|
||||
|
|
@ -23,9 +27,9 @@ class TarjansTests extends FunSuite:
|
|||
}
|
||||
|
||||
test("should produce an empty list of SCCs for an empty graph") {
|
||||
val g = UndirectedGraph.Empty
|
||||
val sccs = SCC.tarjan(g)
|
||||
val alt = SCC.findAll(g)
|
||||
val g = Digraph.Empty
|
||||
val sccs = StronglyConnectedComponent.tarjan(g)
|
||||
val alt = StronglyConnectedComponent.findAll(g)
|
||||
assertEquals(sccs, Nil)
|
||||
assertEquals(alt, Nil)
|
||||
}
|
||||
|
|
@ -33,9 +37,9 @@ class TarjansTests extends FunSuite:
|
|||
test(
|
||||
"should produce a list of once SCC with one vertex for a graph with one vertex"
|
||||
) {
|
||||
val g = Graph.undirected(Size.One)
|
||||
val sccs = SCC.findAll(g)
|
||||
val expected = SCC.single(Vertex.Zero)
|
||||
val g = Graph.directed(Size.One)
|
||||
val sccs = StronglyConnectedComponent.findAll(g)
|
||||
val expected = StronglyConnectedComponent.single(Vertex.Zero)
|
||||
assertEquals(sccs, List(expected))
|
||||
}
|
||||
|
||||
|
|
@ -55,9 +59,16 @@ class TarjansTests extends FunSuite:
|
|||
)
|
||||
)
|
||||
|
||||
val sccs = SCC.findAll(g)
|
||||
val sccs = StronglyConnectedComponent.findAll(g)
|
||||
assertEquals(g.roots, Vector(Vertex.Zero))
|
||||
assertEquals(sccs.toSet, Set(SCC.of(0), SCC.of(1), SCC.of(2)))
|
||||
assertEquals(
|
||||
sccs.toSet,
|
||||
Set(
|
||||
StronglyConnectedComponent.of(0),
|
||||
StronglyConnectedComponent.of(1),
|
||||
StronglyConnectedComponent.of(2)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
test("should handle a graph with multiple SCCs") {
|
||||
|
|
@ -87,14 +98,14 @@ class TarjansTests extends FunSuite:
|
|||
)
|
||||
)
|
||||
|
||||
val sccs = SCC.findAll(g)
|
||||
val sccs = StronglyConnectedComponent.findAll(g)
|
||||
assertEquals(g.roots, Vector.empty)
|
||||
assertEquals(
|
||||
sccs.toSet,
|
||||
Set(
|
||||
SCC.of(0, 1, 2),
|
||||
SCC.of(3, 4, 5),
|
||||
SCC.of(6, 7, 8)
|
||||
StronglyConnectedComponent.of(0, 1, 2),
|
||||
StronglyConnectedComponent.of(3, 4, 5),
|
||||
StronglyConnectedComponent.of(6, 7, 8)
|
||||
)
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue