(minor) WIP continuing to fix, refine, refactor, test

This commit is contained in:
Pat Garrity 2026-05-28 23:11:40 -05:00
parent 95e9ec5e08
commit 117bff5242
Signed by: pfm
GPG key ID: 0DC16BCA24B270C4
15 changed files with 467 additions and 74 deletions

View file

@ -35,6 +35,12 @@ val Deps = new {
} }
val Gs = 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" val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
} }
@ -61,6 +67,7 @@ lazy val core = project
.settings(sharedSettings) .settings(sharedSettings)
.settings(testSettings) .settings(testSettings)
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}") .settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
.settings(libraryDependencies ++= Seq(Deps.Gs.Std.Core))
lazy val cats = project lazy val cats = project
.in(file("modules/cats")) .in(file("modules/cats"))

View file

@ -133,10 +133,10 @@ object Adjacency:
): Adjacency = ): Adjacency =
val buffs = Vector.fill(numberOfVertices.value)(ListBuffer.empty[Vertex]) val buffs = Vector.fill(numberOfVertices.value)(ListBuffer.empty[Vertex])
val _ = edges.foreach { edge => val _ = edges.foreach { edge =>
if edge.v1 >= numberOfVertices || edge.v2 >= numberOfVertices then if edge.v1 >= numberOfVertices then
throw new IllegalArgumentException( throw GraphException.VertexExceedsGraphBounds(edge.v1, numberOfVertices)
s"Edge (${edge.v1}, ${edge.v2}) is out of bounds. Maximum vertex value is ${numberOfVertices.value - 1}" else if edge.v2 >= numberOfVertices then
) throw GraphException.VertexExceedsGraphBounds(edge.v2, numberOfVertices)
else else
val _ = buffs(edge.from.ordinal).addOne(edge.to) val _ = buffs(edge.from.ordinal).addOne(edge.to)
} }

View 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

View file

@ -1,5 +1,7 @@
package gs.graph.v0 package gs.graph.v0
import gs.graph.v0.directed.Digraph
/** Graph representation based on an adjacency list. /** Graph representation based on an adjacency list.
* *
* See: * See:
@ -32,6 +34,11 @@ trait Graph:
*/ */
def isEmpty: Boolean = numberOfVertices == Size.Zero def isEmpty: Boolean = numberOfVertices == Size.Zero
/** @return
* True if this graph has one vertex, false otherwise.
*/
def isSingle: Boolean = numberOfVertices == Size.One
/** @return /** @return
* The roots that should be used for traversal operations. * The roots that should be used for traversal operations.
*/ */
@ -95,4 +102,22 @@ object Graph:
adj 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 end Graph

View file

@ -19,22 +19,20 @@ class UndirectedGraph(
*/ */
override def selectRoots(): Vector[Vertex] = roots override def selectRoots(): Vector[Vertex] = roots
/** The roots of an undirected graph are identified by arbitrarily selecting /** The lazily calculated roots of this graph.
* the first listed vertex from each strongly connected component in the *
* 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] = lazy val roots: Vector[Vertex] =
calculateRoots() calculateRoots()
private def calculateRoots(): Vector[Vertex] = private def calculateRoots(): Vector[Vertex] =
if numberOfVertices == Size.Zero then Vector.empty if numberOfVertices == Size.Zero then Vector.empty
else oneRootFromEachSCC(calculateSCCs()).toVector else oneFromEachConnectedComponent().toVector
private def calculateSCCs(): List[SCC] = private def oneFromEachConnectedComponent(): List[Vertex] =
SCC.findAll(this) ConnectedComponent.findAll(this).map(_.root)
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
sccs.map(_.root)
end UndirectedGraph end UndirectedGraph

View file

@ -130,6 +130,24 @@ object Vertex:
else else
throw new IllegalArgumentException("Vertex values must be 0 or greater.") 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 CanEqual[Vertex, Vertex] = CanEqual.derived
given Ordering[Vertex] with given Ordering[Vertex] with

View file

@ -18,6 +18,16 @@ class Dag protected (
object Dag: 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, Dag] = CanEqual.derived
given CanEqual[Dag, Digraph] = CanEqual.derived given CanEqual[Dag, Digraph] = CanEqual.derived
given CanEqual[Digraph, Dag] = CanEqual.derived given CanEqual[Digraph, Dag] = CanEqual.derived

View file

@ -36,9 +36,12 @@ class Digraph(
case other: Digraph => case other: Digraph =>
numberOfVertices == other.numberOfVertices numberOfVertices == other.numberOfVertices
&& adjacency == other.adjacency && adjacency == other.adjacency
&& roots.sameElements(other.roots)
case _ => false case _ => false
/** `True` if this directed graph has a cycle, `false` otherwise.
*/
lazy val hasCycle: Boolean = Digraph.hasCycle(this)
end Digraph end Digraph
object Digraph: object Digraph:
@ -121,9 +124,10 @@ object Digraph:
*/ */
def hasCycle(digraph: Digraph): Boolean = def hasCycle(digraph: Digraph): Boolean =
digraph match digraph match
case _ if digraph.isEmpty => false case _ if digraph.isEmpty => false
case _: Dag => false case _ if digraph.isSingle => false
case _ => new CycleDetector(digraph).hasCycle case _: Dag => false
case _ => new CycleDetector(digraph).hasCycle
// Internal class for finding cycles. // Internal class for finding cycles.
// This implementation comes (roughly) from Algorithms (Fourth Edition) by // This implementation comes (roughly) from Algorithms (Fourth Edition) by

View file

@ -1,6 +1,5 @@
package gs.graph.v0 package gs.graph.v0.directed
import gs.graph.v0.Graph
import gs.graph.v0.Vertex import gs.graph.v0.Vertex
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Stack import scala.collection.mutable.Stack
@ -8,15 +7,20 @@ import scala.collection.mutable.Stack
/** Represents a _Strongly Connected Component_ (SCC). /** Represents a _Strongly Connected Component_ (SCC).
* *
* Each SCC is guaranteed to be non-empty. * 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 /** @inheritDocs
*/ */
override def equals(obj: Any): Boolean = override def equals(obj: Any): Boolean =
obj match obj match
case other: SCC => value.equals(other.value) case other: StronglyConnectedComponent => value.equals(other.value)
case _ => false case _ => false
/** @inheritDocs /** @inheritDocs
*/ */
@ -50,9 +54,9 @@ final class SCC private (val value: Set[Vertex]):
*/ */
lazy val root: Vertex = value.toList.head 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]]. /** Instantiate a new SCC from the given collection of [[Vertex]].
* *
@ -63,19 +67,19 @@ object SCC:
* @return * @return
* The new SCC. * The new SCC.
*/ */
def apply(value: Vertex*): SCC = def apply(value: Vertex*): StronglyConnectedComponent =
if value.isEmpty then if value.isEmpty then
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Empty strongly connected components (SCC) are not allowed." "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 if value.isEmpty then
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Empty strongly connected components (SCC) are not allowed." "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]]. /** Instantiate a new SCC that contains a single [[Vertex]].
* *
@ -84,9 +88,11 @@ object SCC:
* @return * @return
* The new SCC. * 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`. /** Alias for `findAll`.
* *
@ -101,7 +107,7 @@ object SCC:
* @return * @return
* The complete list of [[SCC]] for the input graph. * 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 /** Implementation of Tarjan's Algorithm for finding all strongly connected
* components for some graph. * components for some graph.
@ -114,7 +120,7 @@ object SCC:
* @return * @return
* The complete list of [[SCC]] for the input graph. * 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 val V = g.numberOfVertices.value
// Vertex numbers. // Vertex numbers.
@ -139,7 +145,7 @@ object SCC:
val counter = new Counter(0) val counter = new Counter(0)
// List of output SCCs. // List of output SCCs.
val sccs = ListBuffer.empty[SCC] val sccs = ListBuffer.empty[StronglyConnectedComponent]
(0 until V).foreach { v => (0 until V).foreach { v =>
if !visited(v) then if !visited(v) then
@ -151,7 +157,7 @@ object SCC:
sccs.toList sccs.toList
private def tarjanDfs( private def tarjanDfs(
g: Graph, g: Digraph,
v: Int, v: Int,
num: Array[Int], num: Array[Int],
lowest: Array[Int], lowest: Array[Int],
@ -159,7 +165,7 @@ object SCC:
processed: Array[Boolean], processed: Array[Boolean],
s: Stack[Vertex], s: Stack[Vertex],
counter: Counter, counter: Counter,
output: ListBuffer[SCC] output: ListBuffer[StronglyConnectedComponent]
): Unit = ): Unit =
// Convenience integer representation for array access. // Convenience integer representation for array access.
val V = Vertex(v) val V = Vertex(v)
@ -195,7 +201,8 @@ object SCC:
sccVertex = s.pop() sccVertex = s.pop()
scc.addOne(sccVertex) 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 println("ERROR")
else () else ()
@ -213,4 +220,4 @@ object SCC:
end Counter end Counter
end SCC end StronglyConnectedComponent

View file

@ -1,10 +1,26 @@
package gs.graph.v0.syntax package gs.graph.v0.syntax
import gs.graph.v0.Edge
import gs.graph.v0.Graph import gs.graph.v0.Graph
import gs.graph.v0.GraphTraversal import gs.graph.v0.GraphTraversal
import gs.graph.v0.Vertex import gs.graph.v0.Vertex
import gs.graph.v0.data.AnyGraphWithData 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) extension (graph: Graph)
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the /** Visit vertex node in this graph, using Depth-First Search (DFS) as the

View file

@ -1,12 +1,12 @@
package gs.graph.v0 package gs.graph.v0
import gs.graph.v0.syntax.V
import scala.util.Failure import scala.util.Failure
import scala.util.Try import scala.util.Try
class AdjacencyTests extends munit.FunSuite: class AdjacencyTests extends munit.FunSuite:
private val size = Size(7) private val size = Size(7)
private val vs = (0 until size.value).map(Vertex(_)).toArray
private val edges = Edge.list( private val edges = Edge.list(
0 -> 1, 0 -> 1,
@ -32,13 +32,13 @@ class AdjacencyTests extends munit.FunSuite:
} }
test("should provide incoming connections") { test("should provide incoming connections") {
assertEquals(adj.incoming(vs(0)), Vector.empty) assertEquals(adj.incoming(V(0)), Vector.empty)
assertEquals(adj.incoming(vs(1)), Vector(vs(0))) assertEquals(adj.incoming(V(1)), Vertex.of(0))
assertEquals(adj.incoming(vs(2)), Vector(vs(0))) assertEquals(adj.incoming(V(2)), Vertex.of(0))
assertEquals(adj.incoming(vs(3)), Vector(vs(0))) assertEquals(adj.incoming(V(3)), Vertex.of(0))
assertEquals(adj.incoming(vs(4)), Vector(vs(1), vs(2), vs(3))) assertEquals(adj.incoming(V(4)), Vertex.of(1, 2, 3))
assertEquals(adj.incoming(vs(5)), Vector(vs(3))) assertEquals(adj.incoming(V(5)), Vertex.of(3))
assertEquals(adj.incoming(vs(6)), Vector(vs(4))) assertEquals(adj.incoming(V(6)), Vertex.of(4))
} }
test("should show no incoming connections for a vertex not in the graph") { test("should show no incoming connections for a vertex not in the graph") {
@ -66,19 +66,32 @@ class AdjacencyTests extends munit.FunSuite:
} }
test( 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 n = Size(2)
val es = Edge.list(0 -> 3) val es = Edge.list(0 -> 3)
Try(Adjacency.fromEdges(n, es)) match Try(Adjacency.fromEdges(n, es)) match
case Failure(ex: IllegalArgumentException) => case Failure(ex: GraphException.VertexExceedsGraphBounds) =>
assertEquals( assertEquals(ex.vertex, Vertex(3))
ex.getMessage(), assertEquals(ex.bound, n)
"Edge (0, 3) is out of bounds. Maximum vertex value is 1"
)
case _ => case _ =>
fail( fail(
"Expected adjacency construction to fail with an IllegalArgumentException." "Expected adjacency construction to fail with a GraphException."
) )
} }

View file

@ -12,8 +12,48 @@ class UndirectedGraphTests extends FunSuite:
assertEquals(g.selectRoots(), Vector.empty) assertEquals(g.selectRoots(), Vector.empty)
} }
test( test("should construct a graph based on # of edges and edge list") {
"should calculate roots based on an arbitrary vertex from each connected component" val N = Size(3)
) { val g = Graph.undirected(
// TODO: implement - current logic is wrong. 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)
} }

View file

@ -20,6 +20,15 @@ class DagTests extends FunSuite:
fail("Expected a graph with one vertex to be validated as a DAG.") 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") { test("should validate a single-root graph") {
val size = Size(8) val size = Size(8)
val digraph: Digraph = Digraph.fromAdjacency( val digraph: Digraph = Digraph.fromAdjacency(
@ -40,7 +49,10 @@ class DagTests extends FunSuite:
) )
Dag.validate(digraph) match 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.") case _ => fail("Expected a single-root graph to be validated as a DAG.")
} }
@ -65,3 +77,8 @@ class DagTests extends FunSuite:
) )
case _ => () case _ => ()
} }
test("should provide empty and single graphs") {
assertEquals(Dag.Empty, Digraph.Empty)
assertEquals(Dag.Single, Digraph.Single)
}

View file

@ -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)
}

View file

@ -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.* import munit.*
class TarjansTests extends FunSuite: class TarjansTests extends FunSuite:
@ -9,9 +13,9 @@ class TarjansTests extends FunSuite:
val v0 = Vertex.Zero val v0 = Vertex.Zero
val v1 = Vertex(1) val v1 = Vertex(1)
val v2 = Vertex(2) val v2 = Vertex(2)
val s1 = SCC(v0) val s1 = StronglyConnectedComponent(v0)
val s2 = SCC(v0, v1, v2) val s2 = StronglyConnectedComponent(v0, v1, v2)
val s3 = SCC(v0) val s3 = StronglyConnectedComponent(v0)
assertEquals(s1, s3) assertEquals(s1, s3)
assertNotEquals(s1, s2) assertNotEquals(s1, s2)
@ -23,9 +27,9 @@ class TarjansTests extends FunSuite:
} }
test("should produce an empty list of SCCs for an empty graph") { test("should produce an empty list of SCCs for an empty graph") {
val g = UndirectedGraph.Empty val g = Digraph.Empty
val sccs = SCC.tarjan(g) val sccs = StronglyConnectedComponent.tarjan(g)
val alt = SCC.findAll(g) val alt = StronglyConnectedComponent.findAll(g)
assertEquals(sccs, Nil) assertEquals(sccs, Nil)
assertEquals(alt, Nil) assertEquals(alt, Nil)
} }
@ -33,9 +37,9 @@ class TarjansTests extends FunSuite:
test( test(
"should produce a list of once SCC with one vertex for a graph with one vertex" "should produce a list of once SCC with one vertex for a graph with one vertex"
) { ) {
val g = Graph.undirected(Size.One) val g = Graph.directed(Size.One)
val sccs = SCC.findAll(g) val sccs = StronglyConnectedComponent.findAll(g)
val expected = SCC.single(Vertex.Zero) val expected = StronglyConnectedComponent.single(Vertex.Zero)
assertEquals(sccs, List(expected)) 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(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") { 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(g.roots, Vector.empty)
assertEquals( assertEquals(
sccs.toSet, sccs.toSet,
Set( Set(
SCC.of(0, 1, 2), StronglyConnectedComponent.of(0, 1, 2),
SCC.of(3, 4, 5), StronglyConnectedComponent.of(3, 4, 5),
SCC.of(6, 7, 8) StronglyConnectedComponent.of(6, 7, 8)
) )
) )
} }