Compare commits

...
Sign in to create a new pull request.

3 commits

28 changed files with 1800 additions and 314 deletions

View file

@ -1,4 +1,4 @@
val scala3: String = "3.8.2" val scala3: String = "3.8.3"
ThisBuild / scalaVersion := scala3 ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec") ThisBuild / versionScheme := Some("semver-spec")
@ -35,10 +35,16 @@ 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"
} }
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.4" val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.3.0"
} }
lazy val testSettings = Seq( lazy val testSettings = Seq(
@ -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

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

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

@ -2,7 +2,7 @@ package gs.graph.v0
import java.util.Objects 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`. * When used is a directed context, the edge goes _from_ `v1` _to_ `v2`.
* *
@ -11,11 +11,13 @@ import java.util.Objects
* @param v2 * @param v2
* The second [[Vertex]]. * The second [[Vertex]].
*/ */
final class Edge( final class Edge private (
val v1: Vertex, val v1: Vertex,
val v2: Vertex val v2: Vertex
): ):
def max: Vertex = if v1 >= v2 then v1 else v2
/** When considering this edge as _directed_, this function returns the /** When considering this edge as _directed_, this function returns the
* [[Vertex]] that is the beginning of the connection. * [[Vertex]] that is the beginning of the connection.
* *
@ -54,11 +56,63 @@ object Edge:
given CanEqual[Edge, Edge] = CanEqual.derived 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( def apply(
v1: Vertex, v1: Vertex,
v2: 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 end Edge

View file

@ -1,5 +1,8 @@
package gs.graph.v0 package gs.graph.v0
import gs.graph.v0.directed.Digraph
import scala.collection.IndexedSeqView
/** Graph representation based on an adjacency list. /** Graph representation based on an adjacency list.
* *
* See: * See:
@ -27,11 +30,22 @@ trait Graph:
*/ */
def numberOfVertices: Size def numberOfVertices: Size
/** @return
* View over all [[Vertex]] values in this graph.
*/
def view: IndexedSeqView[Vertex] =
(0 until numberOfVertices.value).view.map(Vertex(_))
/** @return /** @return
* True if this graph has no vertices, false otherwise. * True if this graph has no vertices, false otherwise.
*/ */
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.
*/ */
@ -58,3 +72,59 @@ trait Graph:
case _ => false case _ => false
end Graph 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
)
/** 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

View file

@ -7,13 +7,19 @@ package gs.graph.v0
*/ */
sealed abstract class GraphDisposition(val name: String): sealed abstract class GraphDisposition(val name: String):
/** @inheritDocs
*/
override def equals(that: Any): Boolean = override def equals(that: Any): Boolean =
that match that match
case other: GraphDisposition => name == other.name case other: GraphDisposition => name == other.name
case _ => false case _ => false
/** @inheritDocs
*/
override def hashCode(): Int = name.hashCode() override def hashCode(): Int = name.hashCode()
/** @inheritDocs
*/
override def toString(): String = name override def toString(): String = name
object GraphDisposition: object GraphDisposition:
@ -28,4 +34,17 @@ object GraphDisposition:
*/ */
case object Undirected extends GraphDisposition("undirected") case object Undirected extends GraphDisposition("undirected")
/** Parse the given string as a [[GraphDisposition]].
*
* @param candidate
* The string to validate.
* @return
* The parsed [[GraphDisposition]], or `None` if the input does not
* represent a valid disposition.
*/
def parse(candidate: String): Option[GraphDisposition] =
if candidate.equalsIgnoreCase(Directed.name) then Some(Directed)
else if candidate.equalsIgnoreCase(Undirected.name) then Some(Undirected)
else None
end GraphDisposition end GraphDisposition

View file

@ -12,9 +12,6 @@ object GraphTraversal:
/** Depth-first search that executes a side-effecting function on each /** Depth-first search that executes a side-effecting function on each
* [[Vertex]]. This function will operate on _any_ [[Graph]]. * [[Vertex]]. This function will operate on _any_ [[Graph]].
* *
* This implementation selects the first [[Vertex]] as an arbitrary starting
* point.
*
* @param graph * @param graph
* The input [[Graph]] on which to run DFS. * The input [[Graph]] on which to run DFS.
* @param visit * @param visit
@ -27,7 +24,8 @@ object GraphTraversal:
val s = Stack.empty[Vertex] val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false) val discovered = Array.fill(graph.numberOfVertices.value)(false)
graph.selectRoots().foreach { root => graph.view.foreach { root =>
if !discovered(root.ordinal) then
val _ = s.push(root) val _ = s.push(root)
while !s.isEmpty while !s.isEmpty
do do
@ -35,17 +33,14 @@ object GraphTraversal:
if !discovered(v.ordinal) then if !discovered(v.ordinal) then
val _ = visit(v) val _ = visit(v)
discovered(v.ordinal) = true discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w)) val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
else ()
else () else ()
()
} }
/** Depth-first search that executes a function on each [[Vertex]] to produce /** Depth-first search that executes a function on each [[Vertex]] to produce
* some output. This function will operate on _any_ [[Graph]]. * some output. This function will operate on _any_ [[Graph]].
* *
* This implementation selects the first [[Vertex]] as an arbitrary starting
* point.
*
* @param graph * @param graph
* The input [[Graph]] on which to run DFS. * The input [[Graph]] on which to run DFS.
* @param visit * @param visit
@ -61,7 +56,8 @@ object GraphTraversal:
val s = Stack.empty[Vertex] val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false) val discovered = Array.fill(graph.numberOfVertices.value)(false)
graph.selectRoots().foreach { root => graph.view.foreach { root =>
if !discovered(root.ordinal) then
val _ = s.push(root) val _ = s.push(root)
while !s.isEmpty while !s.isEmpty
do do
@ -69,12 +65,26 @@ object GraphTraversal:
if !discovered(v.ordinal) then if !discovered(v.ordinal) then
val _ = output.addOne(visit(v)) val _ = output.addOne(visit(v))
discovered(v.ordinal) = true discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w)) val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
else ()
else () else ()
} }
output.toList output.toList
/** Depth-first search that executes a function on each [[Vertex]] in the
* context of some accumulator value to produce some accumulated output. This
* function will operate on _any_ [[Graph]].
*
* @param graph
* The input [[Graph]] on which to run DFS.
* @param initial
* The initial value.
* @param f
* The function to apply to each vertex in the context of an accumulator.
* @return
* The accumulated value.
*/
def dfsFold[Acc]( def dfsFold[Acc](
graph: Graph, graph: Graph,
initial: Acc, initial: Acc,
@ -84,7 +94,8 @@ object GraphTraversal:
val s = Stack.empty[Vertex] val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false) val discovered = Array.fill(graph.numberOfVertices.value)(false)
graph.selectRoots().foreach { root => graph.view.foreach { root =>
if !discovered(root.ordinal) then
val _ = s.push(root) val _ = s.push(root)
while !s.isEmpty while !s.isEmpty
do do
@ -92,7 +103,8 @@ object GraphTraversal:
if !discovered(v.ordinal) then if !discovered(v.ordinal) then
acc = f(acc, v) acc = f(acc, v)
discovered(v.ordinal) = true discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w)) val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
else ()
else () else ()
} }
@ -101,9 +113,6 @@ object GraphTraversal:
/** Depth-first search that executes a side-effecting function on each /** Depth-first search that executes a side-effecting function on each
* [[Vertex]], accepting the data stored for that [[Vertex]] as input. * [[Vertex]], accepting the data stored for that [[Vertex]] as input.
* *
* This implementation performs DFS for _each root_ in the input
* [[AnyGraphWithData]].
*
* @param graph * @param graph
* The input [[Graph]] on which to run DFS. * The input [[Graph]] on which to run DFS.
* @param visit * @param visit
@ -116,7 +125,8 @@ object GraphTraversal:
val s = Stack.empty[Vertex] val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false) val discovered = Array.fill(graph.numberOfVertices.value)(false)
graph.selectRoots().foreach { root => graph.view.foreach { root =>
if !discovered(root.ordinal) then
val _ = s.push(root) val _ = s.push(root)
while !s.isEmpty while !s.isEmpty
do do
@ -124,7 +134,8 @@ object GraphTraversal:
if !discovered(v.ordinal) then if !discovered(v.ordinal) then
val _ = visit(v, graph.data(v.ordinal)) val _ = visit(v, graph.data(v.ordinal))
discovered(v.ordinal) = true discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w)) val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
else ()
else () else ()
} }
@ -133,9 +144,6 @@ object GraphTraversal:
/** Depth-first search that executes a function on each [[Vertex]] to produce /** Depth-first search that executes a function on each [[Vertex]] to produce
* some output, accepting the data stored for that [[Vertex]] as input. * some output, accepting the data stored for that [[Vertex]] as input.
* *
* This implementation performs DFS for _each root_ in the input
* [[AnyGraphWithData]].
*
* @param graph * @param graph
* The input [[Graph]] on which to run DFS. * The input [[Graph]] on which to run DFS.
* @param visit * @param visit
@ -174,7 +182,8 @@ object GraphTraversal:
val s = Stack.empty[Vertex] val s = Stack.empty[Vertex]
val discovered = Array.fill(graph.numberOfVertices.value)(false) val discovered = Array.fill(graph.numberOfVertices.value)(false)
graph.selectRoots().foreach { root => graph.view.foreach { root =>
if !discovered(root.ordinal) then
val _ = s.push(root) val _ = s.push(root)
while !s.isEmpty while !s.isEmpty
do do
@ -182,7 +191,8 @@ object GraphTraversal:
if !discovered(v.ordinal) then if !discovered(v.ordinal) then
acc = f(acc, graph.data(v.ordinal)) acc = f(acc, graph.data(v.ordinal))
discovered(v.ordinal) = true discovered(v.ordinal) = true
graph.neighbors(v).foreach(w => s.push(w)) val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
else ()
else () else ()
} }
@ -194,8 +204,10 @@ object GraphTraversal:
): Unit = ): Unit =
val q = Queue.empty[Vertex] val q = Queue.empty[Vertex]
val visited = Array.fill(graph.numberOfVertices.value)(false) val visited = Array.fill(graph.numberOfVertices.value)(false)
val _ = graph.selectRoots().foreach(q.enqueue)
graph.view.foreach { root =>
if !visited(root.ordinal) then
val _ = q.enqueue(root)
while !q.isEmpty while !q.isEmpty
do do
val v = q.dequeue() val v = q.dequeue()
@ -207,6 +219,8 @@ object GraphTraversal:
else () else ()
} }
else () else ()
else ()
}
def bfs[Out]( def bfs[Out](
graph: Graph, graph: Graph,
@ -215,8 +229,10 @@ object GraphTraversal:
val output = ListBuffer.empty[Out] val output = ListBuffer.empty[Out]
val q = Queue.empty[Vertex] val q = Queue.empty[Vertex]
val visited = Array.fill(graph.numberOfVertices.value)(false) val visited = Array.fill(graph.numberOfVertices.value)(false)
val _ = graph.selectRoots().foreach(q.enqueue)
graph.view.foreach { root =>
if !visited(root.ordinal) then
val _ = q.enqueue(root)
while !q.isEmpty while !q.isEmpty
do do
val v = q.dequeue() val v = q.dequeue()
@ -228,6 +244,8 @@ object GraphTraversal:
else () else ()
} }
else () else ()
else ()
}
output.toList output.toList
@ -239,8 +257,10 @@ object GraphTraversal:
var acc = initial var acc = initial
val q = Queue.empty[Vertex] val q = Queue.empty[Vertex]
val visited = Array.fill(graph.numberOfVertices.value)(false) val visited = Array.fill(graph.numberOfVertices.value)(false)
val _ = graph.selectRoots().foreach(q.enqueue)
graph.view.foreach { root =>
if !visited(root.ordinal) then
val _ = q.enqueue(root)
while !q.isEmpty while !q.isEmpty
do do
val v = q.dequeue() val v = q.dequeue()
@ -252,6 +272,8 @@ object GraphTraversal:
else () else ()
} }
else () else ()
else ()
}
acc acc

View file

@ -83,17 +83,6 @@ object Size:
if candidate >= 0 then new Size(candidate) if candidate >= 0 then new Size(candidate)
else throw new IllegalArgumentException("Size values must be 0 or greater.") 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. /** Instantiate the size of some array.
* *
* @param arr * @param arr
@ -112,4 +101,24 @@ object Size:
*/ */
def fromVector(vec: Vector[?]): Size = new Size(vec.length) 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 end Size

View file

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

View file

@ -8,6 +8,11 @@ package gs.graph.v0
*/ */
final class Vertex private (val ordinal: Int) extends Ordered[Vertex]: final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
/** @return
* The value (ordinal) of the Vertex.
*/
def value: Int = ordinal
/** @inheritDocs /** @inheritDocs
*/ */
override def compare(that: Vertex): Int = 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 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? /** Is the ordinal of this vertex greater than some integer value?
* *
* @param value * @param value
@ -46,6 +61,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
*/ */
infix def >(value: Int): Boolean = ordinal > value 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? /** Is the ordinal of this vertex less than some [[Size]] value?
* *
* @param 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 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? /** Is the ordinal of this vertex greater than some [[Size]] value?
* *
* @param 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 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: object Vertex:
/** The fixed value 0 expressed as a [[Vertex]]. /** The fixed value 0 expressed as a [[Vertex]].
@ -84,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:
@ -63,7 +66,7 @@ object Digraph:
): Digraph = ): Digraph =
new Digraph( new Digraph(
numberOfVertices = numberOfVertices, numberOfVertices = numberOfVertices,
adjacency = Adjacency.fromDirectedEdges(numberOfVertices, edges), adjacency = Adjacency.fromEdges(numberOfVertices, edges),
roots = findRootsForDirectedEdges(numberOfVertices, edges) roots = findRootsForDirectedEdges(numberOfVertices, edges)
) )
@ -122,6 +125,7 @@ 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 _ if digraph.isSingle => false
case _: Dag => false case _: Dag => false
case _ => new CycleDetector(digraph).hasCycle case _ => new CycleDetector(digraph).hasCycle

View file

@ -1,147 +0,0 @@
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
/** Represents a _Strongly Connected Component_ (SCC).
*/
opaque type SCC = Vector[Vertex]
object SCC:
/** Instantiate a new SCC from the given collection of [[Vertex]].
*
* This constructor ensures that each [[Vertex]] only occurs once.
*
* @param value
* The input collection.
* @return
* The new SCC.
*/
def apply(value: Seq[Vertex]): SCC = value.distinct.toVector
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.
*
* @param g
* The directed graph to analyze.
* @return
* The complete list of [[SCC]] for the input graph.
*/
def findAll(g: Graph): List[SCC] =
val V = g.numberOfVertices.value
// Vertex numbers.
val num = Array.fill(V)(-1)
// Array holding the minimum reachable vertex numbers. `lowest(v)` is the
// minimum number of a vertex reachable from `v`.
val lowest = Array.fill(V)(-1)
// For each vertex, indicates whether that vertex has been visited.
val visited = Array.fill(V)(false)
// For each vertex, indicates whether that vertex has been processed.
val processed = Array.fill(V)(false)
// Stack that holds the working set of vertices. S holds all vertices
// reachable from the starting vertex. When an SCC is found, the stack is
// unwound to get each element of that SCC.
val s = Stack.empty[Vertex]
// Counter -- provides sequential numbers to the vertices.
val counter = new Counter(0)
// List of output SCCs.
val sccs = ListBuffer.empty[SCC]
(0 until V).foreach { v =>
if !visited(v) then
tarjanDfs(g, v, num, lowest, visited, processed, s, counter, sccs)
()
else ()
}
sccs.toList
private def tarjanDfs(
g: Graph,
v: Int,
num: Array[Int],
lowest: Array[Int],
visited: Array[Boolean],
processed: Array[Boolean],
s: Stack[Vertex],
counter: Counter,
output: ListBuffer[SCC]
): Unit =
// Convenience integer representation for array access.
val V = Vertex(v)
num(v) = counter.get()
lowest(v) = num(v)
val _ = counter.increment()
visited(v) = true
s.push(V)
// Traverse the neighbors of the current vertex.
g.neighbors(V).foreach { neighbor =>
// Convenience integer representation for array access.
val n = neighbor.ordinal
if !visited(n) then
tarjanDfs(g, n, num, lowest, visited, processed, s, counter, output)
lowest(v) = Math.min(lowest(v), lowest(n))
else if !processed(n) then lowest(v) = Math.min(lowest(v), num(n))
else ()
}
// The current vertex is now processed.
processed(v) = true
if lowest(v) == num(v) then
val scc = ListBuffer.empty[Vertex]
var sccVertex = s.pop()
while sccVertex != V
do
scc.addOne(sccVertex)
sccVertex = s.pop()
scc.addOne(sccVertex)
output.addOne(SCC(scc.toVector))
else ()
final private class Counter(var i: Int = 0):
def increment(): Unit =
i += 1
def get(): Int = i
end SCC

View file

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

View file

@ -0,0 +1,223 @@
package gs.graph.v0.directed
import gs.graph.v0.Vertex
import scala.collection.mutable.ListBuffer
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 StronglyConnectedComponent private (val value: Set[Vertex]):
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: StronglyConnectedComponent => 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 StronglyConnectedComponent
object StronglyConnectedComponent:
/** Instantiate a new SCC from the given collection of [[Vertex]].
*
* This constructor ensures that each [[Vertex]] only occurs once.
*
* @param value
* The input collection.
* @return
* The new SCC.
*/
def apply(value: Vertex*): StronglyConnectedComponent =
if value.isEmpty then
throw new IllegalArgumentException(
"Empty strongly connected components (SCC) are not allowed."
)
else new StronglyConnectedComponent(value.toSet)
def of(value: Int*): StronglyConnectedComponent =
if value.isEmpty then
throw new IllegalArgumentException(
"Empty strongly connected components (SCC) are not allowed."
)
else new StronglyConnectedComponent(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): StronglyConnectedComponent =
new StronglyConnectedComponent(Set(value))
given CanEqual[StronglyConnectedComponent, StronglyConnectedComponent] =
CanEqual.derived
/** 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.
* @return
* The complete list of [[SCC]] for the input graph.
*/
def tarjan(g: Digraph): List[StronglyConnectedComponent] = 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.
*/
def findAll(g: Digraph): List[StronglyConnectedComponent] =
val V = g.numberOfVertices.value
// Vertex numbers.
val num = Array.fill(V)(-1)
// Array holding the minimum reachable vertex numbers. `lowest(v)` is the
// minimum number of a vertex reachable from `v`.
val lowest = Array.fill(V)(-1)
// For each vertex, indicates whether that vertex has been visited.
val visited = Array.fill(V)(false)
// For each vertex, indicates whether that vertex has been processed.
val processed = Array.fill(V)(false)
// Stack that holds the working set of vertices. S holds all vertices
// reachable from the starting vertex. When an SCC is found, the stack is
// unwound to get each element of that SCC.
val s = Stack.empty[Vertex]
// Counter -- provides sequential numbers to the vertices.
val counter = new Counter(0)
// List of output SCCs.
val sccs = ListBuffer.empty[StronglyConnectedComponent]
(0 until V).foreach { v =>
if !visited(v) then
tarjanDfs(g, v, num, lowest, visited, processed, s, counter, sccs)
()
else ()
}
sccs.toList
private def tarjanDfs(
g: Digraph,
v: Int,
num: Array[Int],
lowest: Array[Int],
visited: Array[Boolean],
processed: Array[Boolean],
s: Stack[Vertex],
counter: Counter,
output: ListBuffer[StronglyConnectedComponent]
): Unit =
// Convenience integer representation for array access.
val V = Vertex(v)
num(v) = counter.get()
lowest(v) = num(v)
val _ = counter.increment()
visited(v) = true
s.push(V)
// Traverse the neighbors of the current vertex.
g.neighbors(V).foreach { neighbor =>
// Convenience integer representation for array access.
val n = neighbor.ordinal
if !visited(n) then
tarjanDfs(g, n, num, lowest, visited, processed, s, counter, output)
lowest(v) = Math.min(lowest(v), lowest(n))
else if !processed(n) then lowest(v) = Math.min(lowest(v), num(n))
else ()
}
// The current vertex is now processed.
processed(v) = true
if lowest(v) == num(v) then
val scc = ListBuffer.empty[Vertex]
var sccVertex = s.pop()
while sccVertex != V
do
scc.addOne(sccVertex)
sccVertex = s.pop()
scc.addOne(sccVertex)
if !scc.isEmpty then
output.addOne(new StronglyConnectedComponent(scc.toSet))
else println("ERROR")
else ()
/** 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 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,27 +1,104 @@
package gs.graph.v0 package gs.graph.v0
import gs.graph.v0.syntax.V
import scala.util.Failure
import scala.util.Try
class AdjacencyTests extends munit.FunSuite: class AdjacencyTests extends munit.FunSuite:
test("should provide incoming connections") { private val size = Size(7)
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)
assertEquals(A.incoming(vs(0)), Vector.empty) private val edges = Edge.list(
assertEquals(A.incoming(vs(1)), Vector(vs(0))) 0 -> 1,
assertEquals(A.incoming(vs(2)), Vector(vs(0))) 0 -> 2,
assertEquals(A.incoming(vs(3)), Vector(vs(0))) 0 -> 3,
assertEquals(A.incoming(vs(4)), Vector(vs(1), vs(2), vs(3))) 1 -> 4,
assertEquals(A.incoming(vs(5)), Vector(vs(3))) 2 -> 4,
assertEquals(A.incoming(vs(6)), Vector(vs(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(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") {
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 (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: GraphException.VertexExceedsGraphBounds) =>
assertEquals(ex.vertex, Vertex(3))
assertEquals(ex.bound, n)
case _ =>
fail(
"Expected adjacency construction to fail with a GraphException."
)
}
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,200 @@
package gs.graph.v0
import gs.graph.v0.BfsTraversalTests.Counter
import gs.graph.v0.BfsTraversalTests.Tracker
import gs.graph.v0.directed.Digraph
import munit.*
import scala.collection.mutable.ListBuffer
class BfsTraversalTests extends FunSuite:
test("should perform BFS on an empty graph") {
val c1 = new Counter
val _ = GraphTraversal.bfs(graph = UndirectedGraph.Empty, visit = c1.visit)
assertEquals(c1.count, 0)
val c2 = new Counter
val _ = GraphTraversal.bfs(graph = Digraph.Empty, visit = c2.visit)
assertEquals(c2.count, 0)
}
test("should perform BFS on a graph of size 1") {
val c1 = new Counter
val _ = GraphTraversal.bfs(graph = UndirectedGraph.Single, visit = c1.visit)
assertEquals(c1.count, 1)
val c2 = new Counter
val _ = GraphTraversal.bfs(graph = Digraph.Single, visit = c2.visit)
assertEquals(c2.count, 1)
}
test("should perform BFS on a complex directed graph") {
val n = Size(6)
val g = Graph.directed(
numberOfVertices = n,
edges = 0 -> 1,
1 -> 2,
2 -> 1,
0 -> 3,
3 -> 2,
4 -> 5
)
val expectedVisitOrder = List(0, 1, 3, 2, 4, 5)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.bfs(g, t.visit)
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
val o2 = GraphTraversal.bfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform BFS on a DAG") {
val n = Size(4)
val g = Graph.directed(
numberOfVertices = n,
edges = 0 -> 1,
0 -> 2,
1 -> 3,
2 -> 3
)
val expectedVisitOrder = List(0, 1, 2, 3)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.bfs(g, t.visit)
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
val o2 = GraphTraversal.bfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform BFS on a strongly connected graph") {
val n = Size(3)
val g = Graph.directed(
numberOfVertices = n,
edges = 0 -> 1,
1 -> 2,
2 -> 0
)
val expectedVisitOrder = List(0, 1, 2)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.bfs(g, t.visit)
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
val o2 = GraphTraversal.bfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform BFS on a line") {
val n = Size(4)
val g = Graph.directed(
numberOfVertices = n,
edges = 0 -> 1,
1 -> 2,
2 -> 3
)
val expectedVisitOrder = List(0, 1, 2, 3)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.bfs(g, t.visit)
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
val o2 = GraphTraversal.bfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform BFS on a larger complex digraph") {
val n = Size(10)
val g = Graph.directed(
numberOfVertices = n,
edges = 0 -> 1,
1 -> 2,
2 -> 3,
3 -> 6,
4 -> 5,
5 -> 6,
6 -> 7,
7 -> 8,
8 -> 9,
9 -> 6,
6 -> 8
)
val expectedVisitOrder = List(0, 1, 2, 3, 6, 7, 8, 9, 4, 5)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.bfs(g, t.visit)
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
val o2 = GraphTraversal.bfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
object BfsTraversalTests:
trait BasicVisitor:
def visit(v: Vertex): Unit
class Counter extends BasicVisitor:
var count: Int = 0
def visit(v: Vertex): Unit =
count = count + 1
()
end Counter
class Tracker extends BasicVisitor:
private val output: ListBuffer[Int] = ListBuffer.empty
def snapshot(): List[Int] = output.toList
def visit(v: Vertex): Unit =
val _ = output.addOne(v.ordinal)
()
end Tracker
end BfsTraversalTests

View file

@ -0,0 +1,203 @@
package gs.graph.v0
import gs.graph.v0.DfsTraversalTests.Counter
import gs.graph.v0.DfsTraversalTests.Tracker
import gs.graph.v0.directed.Digraph
import munit.*
import scala.collection.mutable.ListBuffer
class DfsTraversalTests extends FunSuite:
test("should perform DFS on an empty graph") {
val c1 = new Counter
val _ = GraphTraversal.dfs(graph = UndirectedGraph.Empty, visit = c1.visit)
assertEquals(c1.count, 0)
val c2 = new Counter
val _ = GraphTraversal.dfs(graph = Digraph.Empty, visit = c2.visit)
assertEquals(c2.count, 0)
}
test("should perform DFS on a graph of size 1") {
val c1 = new Counter
val _ = GraphTraversal.dfs(graph = UndirectedGraph.Single, visit = c1.visit)
assertEquals(c1.count, 1)
val c2 = new Counter
val _ = GraphTraversal.dfs(graph = Digraph.Single, visit = c2.visit)
assertEquals(c2.count, 1)
}
test("should perform DFS on a complex directed graph") {
// This test case includes:
// - Cycles
// - Multi-Root (0 and 4 are the roots)
val n = Size(6)
val g = Graph.directed(
numberOfVertices = n,
edges = (0, 1),
(1, 2),
(2, 1),
(0, 3),
(3, 2),
(4, 5)
)
val expectedVisitOrder = List(0, 1, 2, 3, 4, 5)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.dfs(g, t.visit)
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
val o2 = GraphTraversal.dfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform DFS on a DAG") {
val n = Size(4)
val g = Graph.directed(
numberOfVertices = n,
edges = (0, 1),
(0, 2),
(1, 3),
(2, 3)
)
val expectedVisitOrder = List(0, 1, 3, 2)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.dfs(g, t.visit)
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
val o2 = GraphTraversal.dfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform DFS on a strongly connected graph") {
val n = Size(3)
val g = Graph.directed(
numberOfVertices = n,
edges = (0, 1),
(1, 2),
(2, 0)
)
val expectedVisitOrder = List(0, 1, 2)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.dfs(g, t.visit)
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
val o2 = GraphTraversal.dfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform DFS on a line") {
val n = Size(4)
val g = Graph.directed(
numberOfVertices = n,
edges = (0, 1),
(1, 2),
(2, 3)
)
val expectedVisitOrder = List(0, 1, 2, 3)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.dfs(g, t.visit)
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
val o2 = GraphTraversal.dfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
test("should perform DFS on a larger complex digraph") {
val n = Size(10)
val g = Graph.directed(
numberOfVertices = n,
edges = 0 -> 1,
1 -> 2,
2 -> 3,
3 -> 6,
4 -> 5,
5 -> 6,
6 -> 7,
7 -> 8,
8 -> 9,
9 -> 6,
6 -> 8
)
val expectedVisitOrder = List(0, 1, 2, 3, 6, 7, 8, 9, 4, 5)
val t = new Tracker
val b = ListBuffer.empty[Int]
val _ = GraphTraversal.dfs(g, t.visit)
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
val o2 = GraphTraversal.dfsFold(
g,
b,
(
acc,
vx
) => acc.addOne(vx.ordinal)
)
assertEquals(t.snapshot(), expectedVisitOrder)
assertEquals(o1, expectedVisitOrder)
assertEquals(o2.toList, expectedVisitOrder)
}
object DfsTraversalTests:
trait BasicVisitor:
def visit(v: Vertex): Unit
class Counter extends BasicVisitor:
var count: Int = 0
def visit(v: Vertex): Unit =
count = count + 1
()
end Counter
class Tracker extends BasicVisitor:
private val output: ListBuffer[Int] = ListBuffer.empty
def snapshot(): List[Int] = output.toList
def visit(v: Vertex): Unit =
val _ = output.addOne(v.ordinal)
()
end Tracker
end DfsTraversalTests

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,28 @@
package gs.graph.v0
import munit.*
class GraphDispositionTests extends FunSuite:
test("should parse and match all defined dispositions") {
val d1 = GraphDisposition.Directed
val d2 = GraphDisposition.Undirected
assertEquals(d1, GraphDisposition.Directed)
assertEquals(d2, GraphDisposition.Undirected)
assertEquals(d1.equals(d2), false)
assertEquals(d1.hashCode(), GraphDisposition.Directed.hashCode())
assertEquals(d2.hashCode(), GraphDisposition.Undirected.hashCode())
assertEquals(d1.toString(), GraphDisposition.Directed.name)
assertEquals(d2.toString(), GraphDisposition.Undirected.name)
assertEquals(d1.equals(null), false)
assertEquals(d1.equals("foo"), false)
assertEquals(d1.equals(1), false)
val p1 = GraphDisposition.parse(d1.name)
val p2 = GraphDisposition.parse(d2.name)
val p3 = GraphDisposition.parse("something else")
assertEquals(p3, None)
assertEquals(p1, Some(d1))
assertEquals(p2, Some(d2))
}

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

View file

@ -0,0 +1,82 @@
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 <= v3, true)
assertEquals(v1 < v2, false)
assertEquals(v1 <= v2, true)
assertEquals(v1 > v3, false)
assertEquals(v1 >= v3, false)
assertEquals(v3 > v1, true)
assertEquals(v3 >= v1, true)
assertEquals(v2 >= v1, true)
assertEquals(v2 > v1, false)
assertEquals(v1 < 3, true)
assertEquals(v1 <= 3, true)
assertEquals(v1 < 0, false)
assertEquals(v1 <= 0, true)
assertEquals(v1 > 0, false)
assertEquals(v1 >= 0, true)
assertEquals(v3 > 0, true)
assertEquals(v3 >= 0, true)
assertEquals(v1 < Size(3), true)
assertEquals(v1 <= Size(3), true)
assertEquals(v1 < Size.Zero, false)
assertEquals(v1 <= Size.Zero, true)
assertEquals(v1 > Size.Zero, false)
assertEquals(v1 >= Size.Zero, true)
assertEquals(v3 > Size.Zero, true)
assertEquals(v3 >= Size.Zero, true)
assertEquals(v1.toString(), v2.toString())
assertNotEquals(v1.toString(), v3.toString())
assertEquals(v1.compare(v3), -3)
assertEquals(v1.compare(v2), 0)
assertEquals(v1.value, 0)
assertEquals(v2.value, 0)
assertEquals(v3.value, 3)
}
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.")
}
test("should instantiate a list of vertices") {
val vs = Vertex.list(0, 1, 2, 3)
assertEquals(vs, List(0, 1, 2, 3).map(Vertex(_)))
}
test("should instantiate a set of vertices") {
val vs = Vertex.set(0, 1, 2, 3, 0, 1, 2, 3)
assertEquals(vs, Set(0, 1, 2, 3).map(Vertex(_)))
}
test("should support the ordering type class") {
val o = Ordering[Vertex]
val v1 = Vertex(0)
val v2 = Vertex(0)
val v3 = Vertex(1)
val v4 = Vertex(2)
assertEquals(o.compare(v1, v2), 0)
assertEquals(o.compare(v1, v3), -1)
assertEquals(o.compare(v1, v4), -2)
assertEquals(o.compare(v4, v1), 2)
assertEquals(o.compare(v4, v3), 1)
}

View file

@ -3,7 +3,6 @@ package gs.graph.v0.directed
import gs.graph.v0.Adjacency import gs.graph.v0.Adjacency
import gs.graph.v0.Edge import gs.graph.v0.Edge
import gs.graph.v0.Size import gs.graph.v0.Size
import gs.graph.v0.Vertex
import munit.* import munit.*
class DagTests extends FunSuite: class DagTests extends FunSuite:
@ -21,42 +20,52 @@ 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 vs = (0 until size.value).map(Vertex(_))
val digraph: Digraph = Digraph.fromAdjacency( val digraph: Digraph = Digraph.fromAdjacency(
Adjacency.fromDirectedEdges( Adjacency.fromEdges(
numberOfVertices = size, numberOfVertices = size,
edges = Seq( edges = Edge.list(
Edge(vs(0) -> vs(1)), 0 -> 1,
Edge(vs(0) -> vs(2)), 0 -> 2,
Edge(vs(0) -> vs(3)), 0 -> 3,
Edge(vs(1) -> vs(4)), 1 -> 4,
Edge(vs(2) -> vs(4)), 2 -> 4,
Edge(vs(3) -> vs(5)), 3 -> 5,
Edge(vs(4) -> vs(6)), 4 -> 6,
Edge(vs(5) -> vs(6)), 5 -> 6,
Edge(vs(6) -> vs(7)) 6 -> 7
) )
) )
) )
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.")
} }
test("should NOT validate a single-root digraph with a cycle") { test("should NOT validate a single-root digraph with a cycle") {
val size = Size(4) val size = Size(4)
val vs = (0 until size.value).map(Vertex(_))
val digraph: Digraph = Digraph.fromAdjacency( val digraph: Digraph = Digraph.fromAdjacency(
Adjacency.fromDirectedEdges( Adjacency.fromEdges(
numberOfVertices = size, numberOfVertices = size,
edges = Seq( edges = Edge.list(
Edge(vs(0) -> vs(1)), 0 -> 1,
Edge(vs(1) -> vs(2)), 1 -> 2,
Edge(vs(2) -> vs(3)), 2 -> 3,
Edge(vs(3) -> vs(1)) 3 -> 1
) )
) )
) )
@ -68,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

@ -0,0 +1,113 @@
package gs.graph.v0.directed
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:
test("should provide basic SCC representation") {
val v0 = Vertex.Zero
val v1 = Vertex(1)
val v2 = Vertex(2)
val s1 = StronglyConnectedComponent(v0)
val s2 = StronglyConnectedComponent(v0, v1, v2)
val s3 = StronglyConnectedComponent(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 = Digraph.Empty
val sccs = StronglyConnectedComponent.tarjan(g)
val alt = StronglyConnectedComponent.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.directed(Size.One)
val sccs = StronglyConnectedComponent.findAll(g)
val expected = StronglyConnectedComponent.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 = StronglyConnectedComponent.findAll(g)
assertEquals(g.roots, Vector(Vertex.Zero))
assertEquals(
sccs.toSet,
Set(
StronglyConnectedComponent.of(0),
StronglyConnectedComponent.of(1),
StronglyConnectedComponent.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 = StronglyConnectedComponent.findAll(g)
assertEquals(g.roots, Vector.empty)
assertEquals(
sccs.toSet,
Set(
StronglyConnectedComponent.of(0, 1, 2),
StronglyConnectedComponent.of(3, 4, 5),
StronglyConnectedComponent.of(6, 7, 8)
)
)
}
end TarjansTests

View file

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