(minor) coverage, fixes, updates

This commit is contained in:
Pat Garrity 2026-03-30 23:22:12 -05:00
parent b7feb7a341
commit 95e9ec5e08
Signed by: pfm
GPG key ID: 0DC16BCA24B270C4
18 changed files with 670 additions and 98 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")
@ -38,7 +38,7 @@ val Deps = new {
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
}
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.4"
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.3.0"
}
lazy val testSettings = Seq(

View file

@ -8,7 +8,7 @@ import scala.collection.mutable.ListBuffer
* the corresponding vector is a "to" [[Vertex]] -- there are edges _from_ some
* vertex _to_ another vertex.
*/
final class Adjacency(val neighbors: Vector[Vector[Vertex]]):
final class Adjacency private (val neighbors: Vector[Vector[Vertex]]):
/** Get the vector of [[Vertex]] that receive a connection _from_ the input
* [[Vertex]].
*
@ -103,7 +103,7 @@ object Adjacency:
* @return
* Some new empty adjacency.
*/
def empty(numberOfVertices: Size): Adjacency =
def noEdges(numberOfVertices: Size): Adjacency =
new Adjacency(Vector.fill(numberOfVertices.value)(Vector.empty))
given CanEqual[Adjacency, Adjacency] = CanEqual.derived
@ -118,8 +118,7 @@ object Adjacency:
*/
final val Single: Adjacency = new Adjacency(Vector(Vector.empty))
/** Calculate an [[Adjacency]] from some collection of [[Edge]], where those
* edges are assumed to be directed.
/** Calculate an [[Adjacency]] from some collection of [[Edge]].
*
* @param numberOfVertices
* The number of [[Vertex]] (`N`) in this graph.
@ -128,14 +127,44 @@ object Adjacency:
* @return
* The calculated [[Adjacency]].
*/
def fromDirectedEdges(
def fromEdges(
numberOfVertices: Size,
edges: Iterable[Edge]
): 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}"
)
else
val _ = buffs(edge.from.ordinal).addOne(edge.to)
}
new Adjacency(buffs.map(_.distinct.toVector))
/** Calculate an [[Adjacency]] from some collection of [[Edge]].
*
* @param edges
* The collection of [[Edge]] present in this graph.
* @return
* The calculated [[Adjacency]].
*/
def fromEdges(
edges: Iterable[Edge]
): Adjacency =
val numberOfVertices = findMaximumVertex(edges).ordinal + 1
val buffs = Vector.fill(numberOfVertices)(ListBuffer.empty[Vertex])
val _ = edges.foreach { edge =>
val _ = buffs(edge.from.ordinal).addOne(edge.to)
}
new Adjacency(buffs.map(_.distinct.toVector))
private def findMaximumVertex(edges: Iterable[Edge]): Vertex =
var maximum = Vertex.Zero
edges.foreach { edge =>
if edge.max > maximum then maximum = edge.max
else ()
}
maximum
end Adjacency

View file

@ -2,7 +2,7 @@ package gs.graph.v0
import java.util.Objects
/** Represents a relationship between two [[Vertex]].
/** Represents a relationship between two distinct [[Vertex]].
*
* When used is a directed context, the edge goes _from_ `v1` _to_ `v2`.
*
@ -11,11 +11,13 @@ import java.util.Objects
* @param v2
* The second [[Vertex]].
*/
final class Edge(
final class Edge private (
val v1: Vertex,
val v2: Vertex
):
def max: Vertex = if v1 >= v2 then v1 else v2
/** When considering this edge as _directed_, this function returns the
* [[Vertex]] that is the beginning of the connection.
*
@ -54,11 +56,63 @@ object Edge:
given CanEqual[Edge, Edge] = CanEqual.derived
/** Instantiate a new Edge.
*
* Throws an exception if the given vertices are the same.
*
* @param v1
* The first [[Vertex]].
* @param v2
* The second [[Vertex]].
* @return
* The new edge.
*/
def apply(
v1: Vertex,
v2: Vertex
): Edge = new Edge(v1, v2)
): Edge =
if v1 != v2 then new Edge(v1, v2)
else
throw new IllegalArgumentException(
"Loop edges are not supported. Edges must refer to two distinct vertexes."
)
def apply(vs: (Vertex, Vertex)): Edge = new Edge(vs._1, vs._2)
/** Instantiate a new Edge.
*
* Throws an exception if the given vertices are the same.
*
* @param vs
* The pair of [[Vertex]].
* @return
* The new edge.
*/
def apply(vs: (Vertex, Vertex)): Edge = Edge(vs._1, vs._2)
/** Instantiate a new Edge.
*
* Throws an exception if the given vertices are the same or are not valid
* [[Vertex]] values.
*
* @param v1
* The first [[Vertex]].
* @param v2
* The second [[Vertex]].
* @return
* The new edge.
*/
def apply(
v1: Int,
v2: Int
): Edge = Edge(Vertex(v1), Vertex(v2))
/** Instantiate a list of Edge.
*
* @param edges
* The list of Edge.
* @return
* The captured list of Edge.
*/
def list(edges: (Int, Int)*): List[Edge] =
edges.map(e => apply(e._1, e._2)).toList
end Edge

View file

@ -58,3 +58,41 @@ trait Graph:
case _ => false
end Graph
object Graph:
/** Construct a new [[UndirectedGraph]] from the given [[Edge]].
*
* @param numberOfVertices
* The number of vertices in this [[Graph]].
* @param edges
* The [[Edge]] in this graph.
* @return
* The new [[UndirectedGraph]].
*/
def undirected(
numberOfVertices: Size,
edges: (Int, Int)*
): UndirectedGraph =
new UndirectedGraph(
numberOfVertices,
Adjacency.fromEdges(numberOfVertices, Edge.list(edges*))
)
/** Construct a new [[UndirectedGraph]] from the given [[Edge]].
*
* @param edges
* The [[Edge]] in this graph.
* @return
* The new [[UndirectedGraph]].
*/
def undirected(
edges: (Int, Int)*
): UndirectedGraph =
val adj = Adjacency.fromEdges(Edge.list(edges*))
new UndirectedGraph(
adj.numberOfVertices,
adj
)
end Graph

View file

@ -1,4 +1,4 @@
package gs.graph.v0.directed
package gs.graph.v0
import gs.graph.v0.Graph
import gs.graph.v0.Vertex
@ -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.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 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,36 +63,54 @@ 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
/** Implementation of Tarjan's Algorithm for finding all strongly connected
* components for some directed graph.
/** 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 directed graph to analyze.
* The graph to analyze.
* @return
* The complete list of [[SCC]] for the input graph.
*/
def tarjan(g: Graph): List[SCC] = findAll(g)
/** 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
* The complete list of [[SCC]] for the input graph.
*/
@ -134,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

@ -83,17 +83,6 @@ object Size:
if candidate >= 0 then new Size(candidate)
else throw new IllegalArgumentException("Size values must be 0 or greater.")
given CanEqual[Size, Size] = CanEqual.derived
given Ordering[Size] with
/** @inheritDocs
*/
def compare(
x: Size,
y: Size
): Int = x.value - y.value
/** Instantiate the size of some array.
*
* @param arr
@ -112,4 +101,24 @@ object Size:
*/
def fromVector(vec: Vector[?]): Size = new Size(vec.length)
/** Instantiate the size of any iterable.
*
* @param iter
* The iterable.
* @return
* Size representing the total length of the iterable.
*/
def of(iter: Iterable[?]): Size = new Size(iter.size)
given CanEqual[Size, Size] = CanEqual.derived
given Ordering[Size] with
/** @inheritDocs
*/
def compare(
x: Size,
y: Size
): Int = x.value - y.value
end Size

View file

@ -17,8 +17,24 @@ class UndirectedGraph(
/** @inheritDocs
*/
override def selectRoots(): Vector[Vertex] =
if numberOfVertices == Size.Zero then Vector.empty else Vector(Vertex.Zero)
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.
*/
lazy val roots: Vector[Vertex] =
calculateRoots()
private def calculateRoots(): Vector[Vertex] =
if numberOfVertices == Size.Zero then Vector.empty
else oneRootFromEachSCC(calculateSCCs()).toVector
private def calculateSCCs(): List[SCC] =
SCC.findAll(this)
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
sccs.map(_.root)
end UndirectedGraph

View file

@ -8,6 +8,11 @@ package gs.graph.v0
*/
final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
/** @return
* The value (ordinal) of the Vertex.
*/
def value: Int = ordinal
/** @inheritDocs
*/
override def compare(that: Vertex): Int =
@ -37,6 +42,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
*/
infix def <(value: Int): Boolean = ordinal < value
/** Is the ordinal of this vertex less than or equal to some integer value?
*
* @param value
* The integer value.
* @return
* True if the ordinal is less than or equal to the integer value. False
* otherwise.
*/
infix def <=(value: Int): Boolean = ordinal <= value
/** Is the ordinal of this vertex greater than some integer value?
*
* @param value
@ -46,6 +61,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
*/
infix def >(value: Int): Boolean = ordinal > value
/** Is the ordinal of this vertex greater than or equal to some integer value?
*
* @param value
* The integer value.
* @return
* True if the ordinal is greater than or equal to the integer value. False
* otherwise.
*/
infix def >=(value: Int): Boolean = ordinal >= value
/** Is the ordinal of this vertex less than some [[Size]] value?
*
* @param value
@ -55,6 +80,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
*/
infix def <(size: Size): Boolean = ordinal < size.value
/** Is the ordinal of this vertex less than or equal to some [[Size]] value?
*
* @param value
* The [[Size]] value.
* @return
* True if the ordinal is less than or equal to the [[Size]] value. False
* otherwise.
*/
infix def <=(size: Size): Boolean = ordinal <= size.value
/** Is the ordinal of this vertex greater than some [[Size]] value?
*
* @param value
@ -64,6 +99,17 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
*/
infix def >(size: Size): Boolean = ordinal > size.value
/** Is the ordinal of this vertex greater than or equal to some [[Size]]
* value?
*
* @param value
* The [[Size]] value.
* @return
* True if the ordinal is greater than or equal to the [[Size]] value.
* False otherwise.
*/
infix def >=(size: Size): Boolean = ordinal >= size.value
object Vertex:
/** The fixed value 0 expressed as a [[Vertex]].

View file

@ -63,7 +63,7 @@ object Digraph:
): Digraph =
new Digraph(
numberOfVertices = numberOfVertices,
adjacency = Adjacency.fromDirectedEdges(numberOfVertices, edges),
adjacency = Adjacency.fromEdges(numberOfVertices, edges),
roots = findRootsForDirectedEdges(numberOfVertices, edges)
)

View file

@ -61,7 +61,7 @@ object SingleRootDigraph:
if root < numberOfVertices then
new SingleRootDigraph(
numberOfVertices,
Adjacency.fromDirectedEdges(numberOfVertices, edges),
Adjacency.fromEdges(numberOfVertices, edges),
root
)
else throw GraphException.RootOutOfBounds(root, numberOfVertices)
@ -86,7 +86,7 @@ object SingleRootDigraph:
Some(
new SingleRootDigraph(
numberOfVertices,
Adjacency.fromDirectedEdges(numberOfVertices, edges),
Adjacency.fromEdges(numberOfVertices, edges),
roots(0)
)
)

View file

@ -1,27 +1,91 @@
package gs.graph.v0
import scala.util.Failure
import scala.util.Try
class AdjacencyTests extends munit.FunSuite:
test("should provide incoming connections") {
val N = Size(7)
val vs = (0 until N.value).map(Vertex(_)).toArray
val E = List(
Edge(vs(0), vs(1)),
Edge(vs(0), vs(2)),
Edge(vs(0), vs(3)),
Edge(vs(1), vs(4)),
Edge(vs(2), vs(4)),
Edge(vs(3), vs(4)),
Edge(vs(3), vs(5)),
Edge(vs(4), vs(6))
)
val A = Adjacency.fromDirectedEdges(N, E)
private val size = Size(7)
private val vs = (0 until size.value).map(Vertex(_)).toArray
assertEquals(A.incoming(vs(0)), Vector.empty)
assertEquals(A.incoming(vs(1)), Vector(vs(0)))
assertEquals(A.incoming(vs(2)), Vector(vs(0)))
assertEquals(A.incoming(vs(3)), Vector(vs(0)))
assertEquals(A.incoming(vs(4)), Vector(vs(1), vs(2), vs(3)))
assertEquals(A.incoming(vs(5)), Vector(vs(3)))
assertEquals(A.incoming(vs(6)), Vector(vs(4)))
private val edges = Edge.list(
0 -> 1,
0 -> 2,
0 -> 3,
1 -> 4,
2 -> 4,
3 -> 4,
3 -> 5,
4 -> 6
)
private val adj = Adjacency.fromEdges(size, edges)
test("should provide equality") {
val a2 = Adjacency(adj.neighbors)
val a3 = Adjacency.Single
assertEquals(adj, a2)
assertNotEquals(adj, a3)
assert(!adj.equals(null))
assert(!adj.equals("anything"))
}
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)))
}
test("should show no incoming connections for a vertex not in the graph") {
assertEquals(adj.incoming(Vertex(999)), Vector.empty)
}
test("should express an adjacency as edges") {
val es = adj.toEdges()
assertEquals(es, edges.toVector)
}
test("should track the number of vertices in the graph") {
assertEquals(adj.numberOfVertices, size)
}
test("should track the number of edges in the graph") {
assertEquals(adj.numberOfEdges, Size.of(edges))
}
test("should construct an adjacency with no edges") {
val sz = Size(10)
val a = Adjacency.noEdges(sz)
assertEquals(a.numberOfVertices, sz)
assertEquals(a.numberOfEdges, Size.Zero)
}
test(
"should guard against out of bound values when building from edges with an explicit bound"
) {
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 _ =>
fail(
"Expected adjacency construction to fail with an IllegalArgumentException."
)
}
test("should construct from edges, finding the largest known vertex") {
val expectedSize = Size(10)
val es = Edge.list(0 -> 1, 1 -> 2, 0 -> 9, 3 -> 1)
val a = Adjacency.fromEdges(es)
assertEquals(a.numberOfVertices, expectedSize)
assertEquals(a.numberOfEdges, Size.of(es))
}

View file

@ -0,0 +1,36 @@
package gs.graph.v0
import munit.*
import scala.util.Failure
import scala.util.Try
class EdgeTests extends FunSuite:
test("should represent an edge with two vertexes") {
val v1 = Vertex.Zero
val v2 = Vertex(1)
val v3 = Vertex(3)
val edge1 = Edge(v1, v2)
val edge2 = Edge(v1 -> v2)
val edge3 = Edge(v3, v1)
assertEquals(edge1, edge2)
assertNotEquals(edge1, edge3)
assertEquals(edge1.toString(), s"($v1, $v2)")
assertEquals(edge1.toString(), edge2.toString())
assertEquals(edge1.hashCode(), edge2.hashCode())
assertNotEquals(edge1.toString(), edge3.toString())
assertNotEquals(edge1.hashCode(), edge3.hashCode())
assertEquals(edge1.from, v1)
assertEquals(edge1.to, v2)
assertEquals(edge1.equals("unrelated-type"), false)
}
test("should forbid loop edges") {
Try(Edge(1, 1)) match
case Failure(ex: IllegalArgumentException) =>
assertEquals(
ex.getMessage(),
"Loop edges are not supported. Edges must refer to two distinct vertexes."
)
case _ => fail("Expected to fail with an IllegalArgumentException")
}

View file

@ -0,0 +1,53 @@
package gs.graph.v0
import munit.*
import scala.util.Try
class SizeTests extends FunSuite:
test("should represent a vertex - some integer >= 0") {
val s1 = Size.Zero
val s2 = Size(0)
val s3 = Size(3)
assertEquals(s1, s2)
assertNotEquals(s1, s3)
assertEquals(s1.compare(s2), 0)
assertEquals(s1.compare(s3), -3)
assertEquals(s1.equals("unrelated-type"), false)
assertEquals(s1.hashCode(), s2.hashCode())
assertNotEquals(s1.hashCode(), s3.hashCode())
assertEquals(s1 < s3, true)
assertEquals(s1 < s2, false)
assertEquals(s1 > s3, false)
assertEquals(s3 > s1, true)
assertEquals(s1 < 3, true)
assertEquals(s1 < 0, false)
assertEquals(s1 > 0, false)
assertEquals(s3 > 0, true)
assertEquals(s1 < Vertex(3), true)
assertEquals(s1 < Vertex.Zero, false)
assertEquals(s1 > Vertex.Zero, false)
assertEquals(s3 > Vertex.Zero, true)
assertEquals(s1.toString(), s2.toString())
assertNotEquals(s1.toString(), s3.toString())
assertEquals(Size.Zero.value, 0)
assertEquals(Size.One.value, 1)
}
test("should throw an exception if an invalid value is given") {
Try(Size(-1)).toEither.left.toOption match
case None => fail("Size instantiation should have failed.")
case Some(cause) =>
assertEquals(cause.getMessage(), "Size values must be 0 or greater.")
}
test("should instantiate from arrays based on the array length") {
val n1 = 3
val n2 = 5
val a1 = Array.fill(n1)(0)
val a2 = Array.fill(n2)(0)
val s1 = Size.fromArray(a1)
val s2 = Size.fromArray(a2)
assertEquals(s1.value, n1)
assertEquals(s2.value, n2)
}

View file

@ -0,0 +1,102 @@
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)))
}
test("should handle a graph with multiple SCCs") {
val n = Size(9)
// Establish discrete cycles
// 0 -> 1 -> 2 -> 0
// 3 -> 4 -> 5 -> 3
// 6 -> 7 -> 8 -> 6
val g = Digraph.fromAdjacency(
Adjacency.fromEdges(
n,
List(
// Cycle 1
Edge(0, 1),
Edge(1, 2),
Edge(2, 0),
// Cycle 2
Edge(3, 4),
Edge(4, 5),
Edge(5, 3),
// Cycle 3
Edge(6, 7),
Edge(7, 8),
Edge(8, 6)
)
)
)
val sccs = SCC.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)
)
)
}
end TarjansTests

View file

@ -0,0 +1,19 @@
package gs.graph.v0
import munit.*
class UndirectedGraphTests extends FunSuite:
test("should support an empty graph") {
val g = UndirectedGraph.Empty
assertEquals(g.numberOfVertices, Size.Zero)
assertEquals(g.adjacency, Adjacency.Empty)
assertEquals(g.disposition, GraphDisposition.Undirected)
assertEquals(g.selectRoots(), Vector.empty)
}
test(
"should calculate roots based on an arbitrary vertex from each connected component"
) {
// TODO: implement - current logic is wrong.
}

View file

@ -0,0 +1,40 @@
package gs.graph.v0
import munit.*
import scala.util.Try
class VertexTests extends FunSuite:
test("should represent a vertex - some integer >= 0") {
val v1 = Vertex.Zero
val v2 = Vertex(0)
val v3 = Vertex(3)
assertEquals(v1, v2)
assertNotEquals(v1, v3)
assertEquals(v1.compare(v2), 0)
assertEquals(v1.compare(v3), -3)
assertEquals(v1.equals("unrelated-type"), false)
assertEquals(v1.hashCode(), v2.hashCode())
assertNotEquals(v1.hashCode(), v3.hashCode())
assertEquals(v1 < v3, true)
assertEquals(v1 < v2, false)
assertEquals(v1 > v3, false)
assertEquals(v3 > v1, true)
assertEquals(v1 < 3, true)
assertEquals(v1 < 0, false)
assertEquals(v1 > 0, false)
assertEquals(v3 > 0, true)
assertEquals(v1 < Size(3), true)
assertEquals(v1 < Size.Zero, false)
assertEquals(v1 > Size.Zero, false)
assertEquals(v3 > Size.Zero, true)
assertEquals(v1.toString(), v2.toString())
assertNotEquals(v1.toString(), v3.toString())
}
test("should throw an exception if an invalid value is given") {
Try(Vertex(-1)).toEither.left.toOption match
case None => fail("Vertex instantiation should have failed.")
case Some(cause) =>
assertEquals(cause.getMessage(), "Vertex values must be 0 or greater.")
}

View file

@ -3,7 +3,6 @@ package gs.graph.v0.directed
import gs.graph.v0.Adjacency
import gs.graph.v0.Edge
import gs.graph.v0.Size
import gs.graph.v0.Vertex
import munit.*
class DagTests extends FunSuite:
@ -23,20 +22,19 @@ class DagTests extends FunSuite:
test("should validate a single-root graph") {
val size = Size(8)
val vs = (0 until size.value).map(Vertex(_))
val digraph: Digraph = Digraph.fromAdjacency(
Adjacency.fromDirectedEdges(
Adjacency.fromEdges(
numberOfVertices = size,
edges = Seq(
Edge(vs(0) -> vs(1)),
Edge(vs(0) -> vs(2)),
Edge(vs(0) -> vs(3)),
Edge(vs(1) -> vs(4)),
Edge(vs(2) -> vs(4)),
Edge(vs(3) -> vs(5)),
Edge(vs(4) -> vs(6)),
Edge(vs(5) -> vs(6)),
Edge(vs(6) -> vs(7))
edges = Edge.list(
0 -> 1,
0 -> 2,
0 -> 3,
1 -> 4,
2 -> 4,
3 -> 5,
4 -> 6,
5 -> 6,
6 -> 7
)
)
)
@ -48,15 +46,14 @@ class DagTests extends FunSuite:
test("should NOT validate a single-root digraph with a cycle") {
val size = Size(4)
val vs = (0 until size.value).map(Vertex(_))
val digraph: Digraph = Digraph.fromAdjacency(
Adjacency.fromDirectedEdges(
Adjacency.fromEdges(
numberOfVertices = size,
edges = Seq(
Edge(vs(0) -> vs(1)),
Edge(vs(1) -> vs(2)),
Edge(vs(2) -> vs(3)),
Edge(vs(3) -> vs(1))
edges = Edge.list(
0 -> 1,
1 -> 2,
2 -> 3,
3 -> 1
)
)
)

View file

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