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 214b8b1..67a825c 100644 --- a/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala +++ b/modules/core/src/main/scala/gs/graph/v0/Adjacency.scala @@ -79,7 +79,7 @@ sealed trait Adjacency: val notContained = vertices.filterNot(contains) if notContained.nonEmpty then throw GraphException.AdjacencyDoesNotContainVertices( - vertices, + notContained, numberOfVertices ) else @@ -109,16 +109,6 @@ object Adjacency: given CanEqual[Adjacency, Adjacency] = CanEqual.derived - /** Instantiate a new [[Adjacency]] list from a raw representation. - * - * @param adj - * The underlying vector of values. - * @return - * The new [[Adjacency]]. - */ - def apply(adj: Vector[Vector[Vertex]]): Adjacency = - DefaultAdjacency.apply(adj) - /** Create an empty adjacency list for some number of vertices. No vertex has * any connections to another vertex. * @@ -269,19 +259,6 @@ final class DefaultAdjacency private ( object DefaultAdjacency: - /** Instantiate a new [[Adjacency]] list from a raw representation. - * - * @param adj - * The underlying vector of values. - * @return - * The new [[Adjacency]]. - */ - def apply(adj: Vector[Vector[Vertex]]): Adjacency = - new DefaultAdjacency( - numberOfVertices = Size.fromVector(adj), - neighbors = adj - ) - /** Create an empty adjacency list for some number of vertices. No vertex has * any connections to another vertex. * @@ -462,7 +439,10 @@ object MappedAdjacency: ): Adjacency = val acc = scala.collection.mutable.Map.empty[Vertex, ListBuffer[Vertex]] edges.foreach { edge => - acc.getOrElseUpdate(edge.from, ListBuffer.empty).addOne(edge.to) + val _ = acc.getOrElseUpdate(edge.from, ListBuffer.empty).addOne(edge.to) + // Accounts for vertices with no outgoing edges. + // They should still be present as vertices in the graph. + val _ = acc.getOrElseUpdate(edge.to, ListBuffer.empty) } val neighbors = acc.map { case (v, ns) => v -> ns.toVector }.toMap new MappedAdjacency(Size.of(neighbors), neighbors) diff --git a/modules/core/src/main/scala/gs/graph/v0/GraphException.scala b/modules/core/src/main/scala/gs/graph/v0/GraphException.scala index ff4a8fb..d094be9 100644 --- a/modules/core/src/main/scala/gs/graph/v0/GraphException.scala +++ b/modules/core/src/main/scala/gs/graph/v0/GraphException.scala @@ -1,9 +1,19 @@ package gs.graph.v0 +/** Root for all exceptions in the `gs-graph` library. + */ sealed abstract class GraphException extends Throwable object GraphException: + /** Thrown when attempting to leverage a vertex with a bounded adjacency, and + * the vertex exceeds the bounds of that graph (adjacency). + * + * @param vertex + * The vertex in question. + * @param bound + * The bound (maximum vertex value) of the graph. + */ case class VertexExceedsGraphBounds( vertex: Vertex, bound: Size @@ -12,6 +22,14 @@ object GraphException: override def getMessage(): String = s"Vertex '$vertex' exceeds graph bound of '$bound' vertices." + /** Thrown when instantiating a data graph, but the wrong amount of data is + * provided. + * + * @param numberOfVertices + * The number of vertices in the graph. + * @param quantityOfData + * The number of data entries provided. + */ case class DataGraphMismatch( numberOfVertices: Size, quantityOfData: Int @@ -21,16 +39,14 @@ object GraphException: s"""Attempted to initialize a data graph with '$numberOfVertices' vertices. '$quantityOfData' data values were provided.""".stripMargin - case class RootOutOfBounds( - root: Vertex, - numberOfVertices: Size - ) extends GraphException: - - override def getMessage(): String = - s"""Attempted to instantiate a single-root digraph with root '$root', - but that root exceeds the graph bound of '$numberOfVertices' - vertices.""".stripMargin - + /** Thrown when some collection of vertices are expected within an adjacency, + * but not found. + * + * @param vertices + * The collection of expected vertices. + * @param numberOfVertices + * The size of the underlying adjacency. + */ case class AdjacencyDoesNotContainVertices( vertices: Iterable[Vertex], numberOfVertices: Size @@ -40,6 +56,14 @@ object GraphException: s"""The following vertices were not found within an Adjacency of size '$numberOfVertices': [${vertices.mkString(", ")}]""".stripMargin + /** Thrown when some candidate neighbor is defined, but not contained within + * the adjacency as a vertex. This indicates a malformed structure. + * + * @param candidate + * The candidate neighbor vertex. + * @param vertex + * The vertex that uses the neighbor. + */ case class NeighborNotContainedByAdjacency( candidate: Vertex, vertex: Vertex 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 afc3dbb..3e6dcaf 100644 --- a/modules/core/src/test/scala/gs/graph/v0/AdjacencyTests.scala +++ b/modules/core/src/test/scala/gs/graph/v0/AdjacencyTests.scala @@ -19,6 +19,9 @@ class AdjacencyTests extends munit.FunSuite: 4 -> 6 ) + private val vertices = + edges.flatMap(edge => List(edge.from, edge.to)).distinct + private val adj = Adjacency.fromEdges(size, edges) test("should provide equality") { @@ -102,3 +105,75 @@ class AdjacencyTests extends munit.FunSuite: assertEquals(a.numberOfVertices, expectedSize) assertEquals(a.numberOfEdges, Size.of(es)) } + + test( + "should fail to take a subset of vertices not contained within the source" + ) { + val subset = Set(V(0), V(1), V(5)) + val es = Edge.list(0 -> 1, 1 -> 2, 2 -> 0) + val a = Adjacency.fromEdges(es) + + Try(a.subset(subset)) match + case Failure(ex: GraphException.AdjacencyDoesNotContainVertices) => + assertEquals(ex.vertices, Set(V(5))) + assertEquals(ex.numberOfVertices, a.numberOfVertices) + case _ => fail("Expected the subset operation to fail.") + } + + test("should represent a single arbitrary vertex") { + val v = V(9) + val a = Adjacency.single(v) + assertEquals(a.numberOfVertices, Size.One) + assertEquals(a.numberOfEdges, Size.Zero) + assertEquals(a.contains(v), true) + assertEquals(a.contains(V(0)), false) + } + + test("should instantiate an adjacency from mapped edges") { + val es = Edge.list(7 -> 8, 8 -> 9, 9 -> 5) + val vs = es.flatMap(edge => List(edge.v1, edge.v2)) + val a = Adjacency.fromEdgesMapped(es) + assertEquals(a.numberOfVertices, Size(4)) + assertEquals(a.numberOfEdges, Size.of(es)) + es.foreach { edge => + assertEquals(a.contains(edge.v1), true) + assertEquals(a.contains(edge.v2), true) + } + a.vertices.foreach(vertex => assertEquals(vs.contains(vertex), true)) + assertEquals(a.toEdges(), es.toVector) + assertEquals(a.incoming(V(7)), Vector.empty) + assertEquals(a.incoming(V(8)), Vector(V(7))) + assertEquals(a.incoming(V(9)), Vector(V(8))) + assertEquals(a.incoming(V(5)), Vector(V(9))) + } + + test( + "should return an empty vector when finding a vertex that is not present" + ) { + val es = Edge.list(0 -> 1, 1 -> 2, 2 -> 3) + val a1 = Adjacency.fromEdges(es) + val a2 = Adjacency.fromEdgesMapped(es) + assertEquals(a1.at(V(9)), Vector.empty) + assertEquals(a2.at(V(9)), Vector.empty) + } + + test("should instantiate an adjacency from edges (size-based)") { + val a = Adjacency.fromEdges(edges) + a.vertices.foreach(v => assertEquals(vertices.contains(v), true)) + assertEquals(a.numberOfVertices, Size.of(vertices)) + assertEquals(a.numberOfEdges, Size.of(edges)) + } + + test( + "should fail to validate a mapped adjacency when a neighbor is not contained in the keyset" + ) { + val input = Map( + V(0) -> Vector(V(1), V(2)), + V(1) -> Vector.empty + ) + Try(MappedAdjacency.validate(input)) match + case Failure(ex: GraphException.NeighborNotContainedByAdjacency) => + assertEquals(ex.candidate, V(2)) + assertEquals(ex.vertex, V(0)) + case _ => fail("Expected adjacency validation to fail.") + } diff --git a/modules/core/src/test/scala/gs/graph/v0/GraphExceptionTests.scala b/modules/core/src/test/scala/gs/graph/v0/GraphExceptionTests.scala new file mode 100644 index 0000000..6509faf --- /dev/null +++ b/modules/core/src/test/scala/gs/graph/v0/GraphExceptionTests.scala @@ -0,0 +1,57 @@ +package gs.graph.v0 + +import gs.graph.v0.syntax.V +import munit.* + +class GraphExceptionTests extends FunSuite: + + test("should VertexExceedsGraphBounds") { + val v = V(2) + val b = Size(1) + val ex = GraphException.VertexExceedsGraphBounds(v, b) + assertEquals(ex.vertex, v) + assertEquals(ex.bound, b) + assertEquals( + ex.getMessage(), + s"Vertex '$v' exceeds graph bound of '$b' vertices." + ) + } + + test("should DataGraphMismatch") { + val n = Size(2) + val q = 1 + val ex = GraphException.DataGraphMismatch(n, q) + assertEquals(ex.numberOfVertices, n) + assertEquals(ex.quantityOfData, q) + assertEquals( + ex.getMessage(), + s"""Attempted to initialize a data graph with '$n' + vertices. '$q' data values were provided.""".stripMargin + ) + } + + test("should AdjacencyDoesNotContainVertices") { + val vs = List(V(8), V(9)) + val n = Size(4) + val ex = GraphException.AdjacencyDoesNotContainVertices(vs, n) + assertEquals(ex.vertices, vs) + assertEquals(ex.numberOfVertices, n) + assertEquals( + ex.getMessage(), + s"""The following vertices were not found within an Adjacency of size + '$n': [${vs.mkString(", ")}]""".stripMargin + ) + } + + test("should NeighborNotContainedByAdjacency") { + val c = V(9) + val v = V(0) + val ex = GraphException.NeighborNotContainedByAdjacency(c, v) + assertEquals(ex.candidate, c) + assertEquals(ex.vertex, v) + assertEquals( + ex.getMessage(), + s"""Attempted to reference neighbor '$c' of vertex '$v', but + '$c' is not a valid vertex in the adjacency.""".stripMargin + ) + }