(patch) WIP - tests

This commit is contained in:
Pat Garrity 2026-04-14 22:17:31 -05:00
parent 80284f8013
commit 48c88e800d
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
5 changed files with 151 additions and 28 deletions

View file

@ -1,4 +1,4 @@
val scala3: String = "3.8.2"
val scala3: String = "3.8.3"
ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec")

View file

@ -6,8 +6,51 @@ import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Stack
/** Represents a _Strongly Connected Component_ (SCC).
*
* Each SCC is guaranteed to be non-empty.
*/
opaque type SCC = Vector[Vertex]
final class SCC private (val value: Set[Vertex]):
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: SCC => value.iterator.sameElements(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 SCC.
*/
def size: Int = value.size
/** @return
* True if this SCC contains a single vertex.
*/
def isSingle: Boolean = value.size == 1
/** @return
* The set of vertices in the SCC.
*/
def vertices: Set[Vertex] = value
/** Retrieve an arbitrary SCC member as the root vertex.
*/
lazy val root: Vertex = value.toList.head
end SCC
object SCC:
@ -20,32 +63,38 @@ object SCC:
* @return
* The new SCC.
*/
def apply(value: Seq[Vertex]): SCC = value.distinct.toVector
def apply(value: Vertex*): SCC =
if value.isEmpty then
throw new IllegalArgumentException(
"Empty strongly connected components (SCC) are not allowed."
)
else new SCC(value.toSet)
def of(value: Int*): SCC =
if value.isEmpty then
throw new IllegalArgumentException(
"Empty strongly connected components (SCC) are not allowed."
)
else new SCC(value.map(Vertex.apply).toSet)
/** Instantiate a new SCC that contains a single [[Vertex]].
*
* @param value
* The input vertex.
* @return
* The new SCC.
*/
def single(value: Vertex): SCC = new SCC(Set(value))
given CanEqual[SCC, SCC] = CanEqual.derived
extension (scc: SCC)
/** @return
* The underlying value of this SCC.
*/
def unwrap(): Vector[Vertex] = scc
/** @return
* The vertices contained by this SCC.
*/
def vertices: Vector[Vertex] = scc
/** @return
* The number of vertices contained by this SCC.
*/
def size: Int = scc.size
/** @return
* True if this SCC contains a single vertex.
*/
def isSingle: Boolean = scc.size == 1
/** Alias for `findAll`.
*
* Implementation of Tarjan's Algorithm for finding all strongly connected
* components for some graph.
*
* This algorithm captures directed cycles. Any vertex not part of some
* directed cycle (e.g. every node in a DAG) forms an SCC by itself.
*
* @param g
* The graph to analyze.
@ -57,6 +106,9 @@ object SCC:
/** Implementation of Tarjan's Algorithm for finding all strongly connected
* components for some graph.
*
* This algorithm captures directed cycles. Any vertex not part of some
* directed cycle (e.g. every node in a DAG) forms an SCC by itself.
*
* @param g
* The graph to analyze.
* @return
@ -143,14 +195,22 @@ object SCC:
sccVertex = s.pop()
scc.addOne(sccVertex)
output.addOne(SCC(scc.toVector))
if !scc.isEmpty then output.addOne(new SCC(scc.toSet))
else println("ERROR")
else ()
final private class Counter(var i: Int = 0):
/** Mutable state wrapper around an integer.
*
* @param i
* The integer value.
*/
final private class Counter(var i: Int):
def increment(): Unit =
i += 1
def get(): Int = i
end Counter
end SCC

View file

@ -34,7 +34,7 @@ class UndirectedGraph(
SCC.findAll(this)
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
sccs.map(_.vertices.apply(0))
sccs.map(_.root)
end UndirectedGraph

View file

@ -0,0 +1,63 @@
package gs.graph.v0
import gs.graph.v0.directed.Digraph
import munit.*
class TarjansTests extends FunSuite:
test("should provide basic SCC representation") {
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)
assertEquals(s1, s3)
assertNotEquals(s1, s2)
assertEquals(s2.value, Set(v0, v1, v2))
assertEquals(s2.vertices, Set(v0, v1, v2))
assertEquals(s2.size, 3)
assertEquals(s2.isSingle, false)
assertEquals(s1.isSingle, true)
}
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)
assertEquals(sccs, Nil)
assertEquals(alt, Nil)
}
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)
assertEquals(sccs, List(expected))
}
/** Each vertex of a DAG is an SCC.
*/
test("should identify a single SCC for a single 'line' of vertices") {
val n = Size(3)
// 0 -> 1 -> 2
val g = Digraph.fromAdjacency(
Adjacency.fromEdges(
n,
List(
Edge(0, 1),
Edge(1, 2)
)
)
)
val sccs = SCC.findAll(g)
assertEquals(g.roots, Vector(Vertex.Zero))
assertEquals(sccs.toSet, Set(SCC.of(0), SCC.of(1), SCC.of(2)))
}
end TarjansTests

View file

@ -1 +1 @@
sbt.version=1.12.8
sbt.version=1.12.9