diff --git a/modules/core/src/main/scala/gs/graph/v0/Edge.scala b/modules/core/src/main/scala/gs/graph/v0/Edge.scala index 71d2435..ef91269 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Edge.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Edge.scala @@ -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,7 +11,7 @@ import java.util.Objects * @param v2 * The second [[Vertex]]. */ -final class Edge( +final class Edge private ( val v1: Vertex, val v2: Vertex ): @@ -54,11 +54,53 @@ 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)) end Edge diff --git a/modules/core/src/main/scala/gs/graph/v0/directed/SCC.scala b/modules/core/src/main/scala/gs/graph/v0/SCC.scala similarity index 92% rename from modules/core/src/main/scala/gs/graph/v0/directed/SCC.scala rename to modules/core/src/main/scala/gs/graph/v0/SCC.scala index ce46957..c0669f4 100644 --- a/modules/core/src/main/scala/gs/graph/v0/directed/SCC.scala +++ b/modules/core/src/main/scala/gs/graph/v0/SCC.scala @@ -1,4 +1,4 @@ -package gs.graph.v0.directed +package gs.graph.v0 import gs.graph.v0.Graph import gs.graph.v0.Vertex @@ -45,11 +45,20 @@ object SCC: */ def isSingle: Boolean = scc.size == 1 - /** Implementation of Tarjan's Algorithm for finding all strongly connected - * components for some directed graph. + /** Alias for `findAll`. * * @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 * The complete list of [[SCC]] for the input graph. */ diff --git a/modules/core/src/main/scala/gs/graph/v0/Size.scala b/modules/core/src/main/scala/gs/graph/v0/Size.scala index e98c192..6c47402 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Size.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Size.scala @@ -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,15 @@ object Size: */ 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 diff --git a/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala b/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala index 4795f5d..d7e9618 100644 --- a/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala +++ b/modules/core/src/main/scala/gs/graph/v0/UndirectedGraph.scala @@ -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(_.vertices.apply(0)) end UndirectedGraph diff --git a/modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala b/modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala new file mode 100644 index 0000000..94b1e81 --- /dev/null +++ b/modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala @@ -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) + } diff --git a/modules/core/src/test/scala/gs/graph/v0/SizeTests.scala b/modules/core/src/test/scala/gs/graph/v0/SizeTests.scala new file mode 100644 index 0000000..981ff85 --- /dev/null +++ b/modules/core/src/test/scala/gs/graph/v0/SizeTests.scala @@ -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) + } diff --git a/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala b/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala new file mode 100644 index 0000000..dda6fdb --- /dev/null +++ b/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala @@ -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. + } diff --git a/modules/core/src/test/scala/gs/graph/v0/VertexTests.scala b/modules/core/src/test/scala/gs/graph/v0/VertexTests.scala new file mode 100644 index 0000000..305d2d0 --- /dev/null +++ b/modules/core/src/test/scala/gs/graph/v0/VertexTests.scala @@ -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.") + }