(minor) coverage, fixes, updates
This commit is contained in:
parent
b7feb7a341
commit
95e9ec5e08
18 changed files with 670 additions and 98 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]].
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
36
modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala
Normal file
36
modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala
Normal 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")
|
||||
}
|
||||
53
modules/core/src/test/scala/gs/graph/v0/SizeTests.scala
Normal file
53
modules/core/src/test/scala/gs/graph/v0/SizeTests.scala
Normal 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)
|
||||
}
|
||||
102
modules/core/src/test/scala/gs/graph/v0/TarjansTests.scala
Normal file
102
modules/core/src/test/scala/gs/graph/v0/TarjansTests.scala
Normal 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
|
||||
|
|
@ -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.
|
||||
}
|
||||
40
modules/core/src/test/scala/gs/graph/v0/VertexTests.scala
Normal file
40
modules/core/src/test/scala/gs/graph/v0/VertexTests.scala
Normal 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.")
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
sbt.version=1.12.8
|
||||
sbt.version=1.12.11
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue