From 44a3bb6a226dc61d5149c66e8cc15c70a7d0f6c7 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Fri, 5 Jun 2026 21:51:33 -0500 Subject: [PATCH] Back to passing tests --- .../main/scala/gs/graph/v0/Adjacency.scala | 32 +++++++++++++ .../gs/graph/v0/ConnectedComponent.scala | 48 ++++++------------- .../scala/gs/graph/v0/AdjacencyTests.scala | 2 +- .../gs/graph/v0/UndirectedGraphTests.scala | 17 ------- .../gs/graph/v0/directed/DigraphTests.scala | 2 - .../gs/graph/v0/directed/TarjansTests.scala | 23 +++++---- 6 files changed, 59 insertions(+), 65 deletions(-) diff --git a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala index 471322c..214b8b1 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala @@ -93,12 +93,16 @@ sealed trait Adjacency: override def equals(that: Any): Boolean = val tuple = (this, that) tuple match + // Optimize when two default implementations are present. case (me: DefaultAdjacency, other: DefaultAdjacency) => me.neighbors.length == other.neighbors.length && me.neighbors .zip(other.neighbors) .map { case (x, y) => x.sameElements(y) } .forall(result => result) + // Any two adjcency lists. + case (me: Adjacency, other: Adjacency) => + me.entries.equals(other.entries) case _ => false object Adjacency: @@ -136,6 +140,15 @@ object Adjacency: */ final val Single: Adjacency = DefaultAdjacency.Single + /** Instantiate a single-vertex adjacency list. + * + * @param vertex + * The vertex. + * @return + * The new [[Adjacency]]. + */ + def single(vertex: Vertex): Adjacency = MappedAdjacency.single(vertex) + /** Calculate an [[Adjacency]] from some collection of [[Edge]]. * * @param numberOfVertices @@ -152,6 +165,13 @@ object Adjacency: DefaultAdjacency.fromEdges(numberOfVertices, edges) /** Calculate an [[Adjacency]] from some collection of [[Edge]]. + * + * This function uses the maximum vertex value as an upper bound and assumes + * that there are vertices with no neighbors within that bound -- it creates + * some [[DefaultAdjacency]]. + * + * Used the mapped version if you want to ensure that _only_ the vertices + * referenced are contained in the result. * * @param edges * The collection of [[Edge]] present in this graph. @@ -163,6 +183,18 @@ object Adjacency: ): Adjacency = DefaultAdjacency.fromEdges(edges) + /** Calculate an [[Adjacency]] from some collection of [[Edge]]. + * + * @param edges + * The collection of [[Edge]] present in this graph. + * @return + * The calculated [[Adjacency]]. + */ + def fromEdgesMapped( + edges: Iterable[Edge] + ): Adjacency = + MappedAdjacency.fromEdges(edges) + end Adjacency /** An immutable adjacency list representation. diff --git a/modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala b/modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala index 2185943..81424b1 100644 --- a/modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala +++ b/modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala @@ -4,11 +4,12 @@ import gs.graph.v0.Vertex import scala.collection.mutable.ListBuffer import scala.collection.mutable.Queue -/** Represents a _ Connected Component_. - * - * TODO: ADD EDGES! +/** Represents a _Connected Component_. */ -final class ConnectedComponent private (val value: Set[Vertex]): +final class ConnectedComponent private ( + val value: Set[Vertex], + val adjacency: Adjacency +): /** @inheritDocs */ @@ -25,9 +26,13 @@ final class ConnectedComponent private (val value: Set[Vertex]): */ override def toString(): String = val sb = StringBuilder() - sb.append("[") - sb.append(value.mkString(",")) - sb.append("]") + sb.append("entries:[\n") + sb.append( + adjacency.entries + .map { case (v, ns) => s"$v -> ${ns.mkString(",")}" } + .mkString("\n") + ) + sb.append("\n]") sb.toString() /** @return @@ -53,30 +58,6 @@ 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 @@ -85,7 +66,7 @@ object ConnectedComponent: * The new Connected Component. */ def single(value: Vertex): ConnectedComponent = - new ConnectedComponent(Set(value)) + new ConnectedComponent(Set(value), Adjacency.single(value)) given CanEqual[ConnectedComponent, ConnectedComponent] = CanEqual.derived @@ -120,7 +101,8 @@ object ConnectedComponent: else () } - new ConnectedComponent(b.toSet) + val vs = b.toSet + new ConnectedComponent(vs, g.adjacency.subset(vs)) private class Tracker(val data: Array[Boolean]): def visit(v: Vertex): Unit = data(v.ordinal) = true diff --git a/modules/core/src/test/scala/gs/graph/v0/AdjacencyTests.scala b/modules/core/src/test/scala/gs/graph/v0/AdjacencyTests.scala index 7fe634e..afc3dbb 100644 --- a/modules/core/src/test/scala/gs/graph/v0/AdjacencyTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/AdjacencyTests.scala @@ -22,7 +22,7 @@ class AdjacencyTests extends munit.FunSuite: private val adj = Adjacency.fromEdges(size, edges) test("should provide equality") { - val a2 = Adjacency(adj.neighbors) + val a2 = MappedAdjacency.validate(adj.entries) val a3 = Adjacency.Single assertEquals(adj, a2) diff --git a/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala b/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala index 635a51d..91540f1 100644 --- a/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/UndirectedGraphTests.scala @@ -37,20 +37,3 @@ class UndirectedGraphTests extends FunSuite: assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2))) assertEquals(g.neighbors(Vertex(4)), Vector.empty) } - - 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) - } diff --git a/modules/core/src/test/scala/gs/graph/v0/directed/DigraphTests.scala b/modules/core/src/test/scala/gs/graph/v0/directed/DigraphTests.scala index c2979bd..de6bee5 100644 --- a/modules/core/src/test/scala/gs/graph/v0/directed/DigraphTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/directed/DigraphTests.scala @@ -11,7 +11,6 @@ class DigraphTests extends FunSuite: 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) @@ -21,7 +20,6 @@ class DigraphTests extends FunSuite: 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) diff --git a/modules/core/src/test/scala/gs/graph/v0/directed/TarjansTests.scala b/modules/core/src/test/scala/gs/graph/v0/directed/TarjansTests.scala index 6b49a8f..4307bc4 100644 --- a/modules/core/src/test/scala/gs/graph/v0/directed/TarjansTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/directed/TarjansTests.scala @@ -5,6 +5,7 @@ import gs.graph.v0.Edge import gs.graph.v0.Graph import gs.graph.v0.Size import gs.graph.v0.Vertex +import gs.graph.v0.syntax.V import munit.* class TarjansTests extends FunSuite: @@ -13,13 +14,13 @@ class TarjansTests extends FunSuite: 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) + val s1 = StronglyConnectedComponent.single(v0) + val s2 = StronglyConnectedComponent.of(0 -> 1, 1 -> 2, 2 -> 0) + val s3 = StronglyConnectedComponent.single(v0) assertEquals(s1, s3) assertNotEquals(s1, s2) - assertEquals(s2.value, Set(v0, v1, v2)) + assertEquals(s2.vertices, Set(v0, v1, v2)) assertEquals(s2.vertices, Set(v0, v1, v2)) assertEquals(s2.size, 3) assertEquals(s2.isSingle, false) @@ -60,13 +61,12 @@ class TarjansTests extends FunSuite: ) val sccs = StronglyConnectedComponent.findAll(g) - assertEquals(g.roots, Vector(Vertex.Zero)) assertEquals( sccs.toSet, Set( - StronglyConnectedComponent.of(0), - StronglyConnectedComponent.of(1), - StronglyConnectedComponent.of(2) + StronglyConnectedComponent.single(V(0)), + StronglyConnectedComponent.single(V(1)), + StronglyConnectedComponent.single(V(2)) ) ) } @@ -99,13 +99,12 @@ class TarjansTests extends FunSuite: ) 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) + StronglyConnectedComponent.of(0 -> 1, 1 -> 2, 2 -> 0), + StronglyConnectedComponent.of(3 -> 4, 4 -> 5, 5 -> 3), + StronglyConnectedComponent.of(6 -> 7, 7 -> 8, 8 -> 6) ) ) }