WIP: More fixes and traversal fully tested for non-enriched graphs.

This commit is contained in:
Pat Garrity 2026-05-31 16:09:42 -05:00
parent 117bff5242
commit 180990c934
Signed by: pfm
GPG key ID: 0DC16BCA24B270C4
7 changed files with 614 additions and 93 deletions

View file

@ -1,6 +1,7 @@
package gs.graph.v0 package gs.graph.v0
import gs.graph.v0.directed.Digraph import gs.graph.v0.directed.Digraph
import scala.collection.IndexedSeqView
/** Graph representation based on an adjacency list. /** Graph representation based on an adjacency list.
* *
@ -29,6 +30,12 @@ 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.
*/ */

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

@ -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,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

@ -17,19 +17,38 @@ class VertexTests extends FunSuite:
assertEquals(v1.hashCode(), v2.hashCode()) assertEquals(v1.hashCode(), v2.hashCode())
assertNotEquals(v1.hashCode(), v3.hashCode()) assertNotEquals(v1.hashCode(), v3.hashCode())
assertEquals(v1 < v3, true) assertEquals(v1 < v3, true)
assertEquals(v1 <= v3, true)
assertEquals(v1 < v2, false) assertEquals(v1 < v2, false)
assertEquals(v1 <= v2, true)
assertEquals(v1 > v3, false) assertEquals(v1 > v3, false)
assertEquals(v1 >= v3, false)
assertEquals(v3 > v1, true) 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 <= 3, true)
assertEquals(v1 < 0, false) assertEquals(v1 < 0, false)
assertEquals(v1 <= 0, true)
assertEquals(v1 > 0, false) assertEquals(v1 > 0, false)
assertEquals(v1 >= 0, true)
assertEquals(v3 > 0, true) assertEquals(v3 > 0, true)
assertEquals(v3 >= 0, true)
assertEquals(v1 < Size(3), true) assertEquals(v1 < Size(3), true)
assertEquals(v1 <= Size(3), true)
assertEquals(v1 < Size.Zero, false) assertEquals(v1 < Size.Zero, false)
assertEquals(v1 <= Size.Zero, true)
assertEquals(v1 > Size.Zero, false) assertEquals(v1 > Size.Zero, false)
assertEquals(v1 >= Size.Zero, true)
assertEquals(v3 > Size.Zero, true) assertEquals(v3 > Size.Zero, true)
assertEquals(v3 >= Size.Zero, true)
assertEquals(v1.toString(), v2.toString()) assertEquals(v1.toString(), v2.toString())
assertNotEquals(v1.toString(), v3.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") { test("should throw an exception if an invalid value is given") {
@ -38,3 +57,26 @@ class VertexTests extends FunSuite:
case Some(cause) => case Some(cause) =>
assertEquals(cause.getMessage(), "Vertex values must be 0 or greater.") 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)
}