WIP have roots on to construction of graphs

This commit is contained in:
Pat Garrity 2026-03-30 23:22:12 -05:00
parent b7feb7a341
commit eeb8a7400b
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
8 changed files with 224 additions and 21 deletions

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,7 +11,7 @@ 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
): ):
@ -54,11 +54,53 @@ 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))
end Edge end Edge

View file

@ -1,4 +1,4 @@
package gs.graph.v0.directed package gs.graph.v0
import gs.graph.v0.Graph import gs.graph.v0.Graph
import gs.graph.v0.Vertex import gs.graph.v0.Vertex
@ -45,11 +45,20 @@ object SCC:
*/ */
def isSingle: Boolean = scc.size == 1 def isSingle: Boolean = scc.size == 1
/** Implementation of Tarjan's Algorithm for finding all strongly connected /** Alias for `findAll`.
* components for some directed graph.
* *
* @param g * @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.
*
* @param g
* The graph to analyze.
* @return * @return
* The complete list of [[SCC]] for the input graph. * The complete list of [[SCC]] for the input graph.
*/ */

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,15 @@ object Size:
*/ */
def fromVector(vec: Vector[?]): Size = new Size(vec.length) def fromVector(vec: Vector[?]): Size = new Size(vec.length)
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,24 @@ 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 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(_.vertices.apply(0))
end UndirectedGraph end UndirectedGraph

View file

@ -0,0 +1,24 @@
package gs.graph.v0
import munit.*
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 = new 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)
}

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

View file

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