Skip to content

Commit fb67ea6

Browse files
committed
Merge branch 'release/0.7.0'. fixes #20.
2 parents 4defe08 + 5e46b2c commit fb67ea6

10 files changed

Lines changed: 400 additions & 18 deletions

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ Contributions are welcome. Please submit a pull request to the develop branch in
8686

8787
# Releases
8888

89+
## 0.7.0
90+
91+
* Added support for classifying edges. This can be used to detect cycles in a graph.
92+
* Added test coverage.
93+
* Added javadoc.
94+
8995
## 0.6.0
9096

9197
* fixed issue with logging when optimizations are turned off

src/main/groovy/graph/DefaultVertexFactory.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ package graph
55
* base class Vertex.
66
*/
77
class DefaultVertexFactory implements VertexFactory {
8+
/**
9+
* Creates a new Vertex with the provided name.
10+
* @param name
11+
* @return
12+
*/
813
@Override
914
Vertex newVertex(String name) {
1015
new Vertex(name:name)

src/main/groovy/graph/DepthFirstTraversalSpec.groovy

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package graph
2-
32
/**
43
* Specification for a DepthFirstTraversal. Contains actions that are called when an
54
* event happens during the traversal.
65
*/
76
class DepthFirstTraversalSpec extends TraversalSpec {
87
private Closure preorderClosure
98
private Closure postorderClosure
9+
private Closure classifyEdgeClosure
1010

1111
/**
1212
* returns the preorder event.
@@ -24,6 +24,14 @@ class DepthFirstTraversalSpec extends TraversalSpec {
2424
postorderClosure
2525
}
2626

27+
/**
28+
* returns the classifyEdge event.
29+
* @return
30+
*/
31+
Closure getClassifyEdge() {
32+
classifyEdgeClosure
33+
}
34+
2735
/**
2836
* method to set the preorder event
2937
* @param preorderClosure
@@ -41,4 +49,12 @@ class DepthFirstTraversalSpec extends TraversalSpec {
4149
void postorder(Closure postorderClosure) {
4250
this.postorderClosure = postorderClosure
4351
}
52+
53+
/**
54+
* method to set the classifyEdge event
55+
* @param classifyEdgeClosure
56+
*/
57+
void classifyEdge(Closure classifyEdgeClosure) {
58+
this.classifyEdgeClosure = classifyEdgeClosure
59+
}
4460
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package graph
2+
3+
import groovy.transform.PackageScope
4+
5+
/**
6+
* The results from classifyEdges in Graph.
7+
*/
8+
class EdgeClassification {
9+
Graph forrest = []
10+
List<Edge> backEdges = []
11+
List<Edge> treeEdges = []
12+
List<Edge> forwardEdges = []
13+
List<Edge> crossEdges = []
14+
15+
/**
16+
* The resulting edge type. Used in addEdge to notify the action closure.
17+
*/
18+
enum EdgeType {
19+
/**
20+
* When the followed edge is GREY
21+
*/
22+
BACK_EDGE,
23+
/**
24+
* When the followed edge is WHITE
25+
*/
26+
TREE_EDGE,
27+
/**
28+
* When the followed edge is BLACK and the connecting vertex is not in the forrest
29+
*/
30+
FORWARD_EDGE,
31+
/**
32+
* When the followed edge is BLACK and the connecting vertex is in the forreest
33+
*/
34+
CROSS_EDGE
35+
}
36+
37+
/**
38+
* Adds an edge calling action with the classification.
39+
* @param graph
40+
* @param edge
41+
* @param from
42+
* @param to
43+
* @param toColor
44+
* @param action
45+
*/
46+
@PackageScope
47+
void addEdge(Graph graph, Edge edge, String from, String to, Graph.TraversalColor toColor, Closure action) {
48+
def edgeType
49+
switch(toColor) {
50+
case Graph.TraversalColor.WHITE:
51+
forrest.addVertex(graph.vertex(from))
52+
forrest.addVertex(graph.vertex(to))
53+
forrest.addEdge(edge)
54+
treeEdges << edge
55+
edgeType = EdgeClassification.EdgeType.TREE_EDGE
56+
break
57+
58+
case Graph.TraversalColor.GREY:
59+
backEdges << edge
60+
edgeType = EdgeClassification.EdgeType.BACK_EDGE
61+
break
62+
63+
case Graph.TraversalColor.BLACK:
64+
if(forrest.vertices[to]) {
65+
crossEdges << edge
66+
edgeType = EdgeClassification.EdgeType.CROSS_EDGE
67+
} else {
68+
forwardEdges << edge
69+
edgeType = EdgeClassification.EdgeType.FORWARD_EDGE
70+
}
71+
break
72+
73+
default:
74+
throw new IllegalStateException("Edge from $from to $to needs to be WHITE, GREY, or BLACK.")
75+
}
76+
action(from, to, edgeType)
77+
}
78+
}

src/main/groovy/graph/Graph.groovy

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package graph
22

3+
import groovy.transform.PackageScope
4+
35
/**
46
* Implementation of a Graph. Vertices are represented as key/value pairs in a map. The edges connect the keys in
57
* the map to form a graph. The values in the map are the contents of the vertices. This makes it easy to represent
@@ -26,7 +28,7 @@ class Graph {
2628
/**
2729
* Defines the color for a vertex when traversing.
2830
*/
29-
def enum TraversalColor {
31+
enum TraversalColor {
3032
/**
3133
* an undiscovered vertex
3234
*/
@@ -93,6 +95,16 @@ class Graph {
9395
plugin.apply(this)
9496
}
9597

98+
/**
99+
* Adds a vertex object directly.
100+
* @param vertex
101+
* @return true if add was successful.
102+
*/
103+
@PackageScope
104+
boolean addVertex(Vertex vertex) {
105+
vertices[vertex.name] = vertex
106+
}
107+
96108
/**
97109
* Creates a map with the name key set to the name param. The map
98110
* and closure are passed to vertex(Map, Clousre)
@@ -139,6 +151,16 @@ class Graph {
139151
vertex
140152
}
141153

154+
/**
155+
* Adds an edge object directly.
156+
* @param edge
157+
* @return true if add was successful.
158+
*/
159+
@PackageScope
160+
boolean addEdge(Edge edge) {
161+
edges << edge
162+
}
163+
142164
/**
143165
* Creates a map with the entries one and two set to the params one and two.
144166
* This map is then passed to edge(map, closure = null).
@@ -360,6 +382,12 @@ class Graph {
360382
for (int index = 0; index < adjacentEdges.size(); index++) { //cannot stop and each() call on adjacentEdges
361383
Edge edge = adjacentEdges[index]
362384
String connectedName = root == edge.one ? edge.two : edge.one
385+
//if white tree edge
386+
//if grey back edge
387+
//if black forward or cross edge. must keep track of trees to say cross edge.
388+
if(spec.classifyEdge && spec.classifyEdge(edge, root, connectedName, spec.colors[connectedName]) == Traversal.STOP) {
389+
return Traversal.STOP
390+
}
363391
if (spec.colors[connectedName] == TraversalColor.WHITE) {
364392
if (Traversal.STOP == depthFirstTraversalConnected(connectedName, spec)) {
365393
return Traversal.STOP
@@ -408,6 +436,7 @@ class Graph {
408436
}
409437
def traversal = spec.visit(vertices[root])
410438
if (traversal == Traversal.STOP) {
439+
spec.colors[root] = TraversalColor.GREY
411440
return traversal
412441
}
413442
spec.colors[root] = TraversalColor.GREY
@@ -422,6 +451,7 @@ class Graph {
422451
if (spec.colors[connected] == TraversalColor.WHITE) {
423452
traversal = spec.visit(vertices[connected])
424453
if (traversal == Traversal.STOP) {
454+
spec.colors[connected] = TraversalColor.GREY
425455
return traversal
426456
}
427457
spec.colors[connected] = TraversalColor.GREY
@@ -432,4 +462,19 @@ class Graph {
432462
}
433463
null
434464
}
465+
466+
/**
467+
* Classifies edges in a depthFirstTraversal returning the results.
468+
* @param action passed into EdgeClassification.addEdge
469+
* @return the resulting EdgeClassification
470+
*/
471+
EdgeClassification classifyEdges(Closure action) {
472+
EdgeClassification ec = new EdgeClassification()
473+
depthFirstTraversal {
474+
classifyEdge { Edge edge, String from, String to, TraversalColor toColor ->
475+
ec.addEdge(this, edge, from, to, toColor, action)
476+
}
477+
}
478+
ec
479+
}
435480
}

src/test/groovy/graph/DirectedGraphEdgeSpec.groovy

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,4 @@ class DirectedGraphEdgeSpec extends Specification {
5656
it.one == 'step2' && it.two == 'step3'
5757
}
5858
}
59-
60-
def 'can modify existing edge'() {
61-
setup:
62-
def edge = graph.edge 'step1', 'step2'
63-
64-
when:
65-
def testValue = false
66-
def testEdge = graph.edge 'step1', 'step2', {
67-
testValue = delegate == edge
68-
}
69-
70-
then:
71-
testValue
72-
edge == testEdge
73-
}
7459
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package graph
2+
3+
import spock.lang.Specification
4+
5+
import static graph.Graph.TraversalColor.*
6+
import static graph.EdgeClassification.EdgeType.*
7+
8+
class EdgeClassificationSpec extends Specification {
9+
def graph = new Graph()
10+
def edgeClassification = new EdgeClassification()
11+
12+
def setup() {
13+
graph.with {
14+
vertex 'A'
15+
vertex 'B'
16+
edge 'A', 'B'
17+
}
18+
}
19+
20+
def 'test from and to in addEdge'() {
21+
setup:
22+
def fromCheck
23+
def toCheck
24+
25+
when:
26+
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', WHITE) { from, to, type ->
27+
fromCheck = from
28+
toCheck = to
29+
}
30+
31+
then:
32+
fromCheck == 'A'
33+
toCheck == 'B'
34+
}
35+
36+
def 'test WHITE edge in addEdge'() {
37+
setup:
38+
def typeCheck
39+
40+
when:
41+
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', WHITE) { from, to, type ->
42+
typeCheck = type
43+
}
44+
45+
then:
46+
typeCheck == TREE_EDGE
47+
edgeClassification.forrest.vertices['A'] == graph.vertex('A')
48+
edgeClassification.forrest.vertices['B'] == graph.vertex('B')
49+
edgeClassification.forrest.vertices.size() == 2
50+
edgeClassification.forrest.edge('A', 'B') == graph.edge('A', 'B')
51+
edgeClassification.forrest.edges.size() == 1
52+
edgeClassification.treeEdges.contains(graph.edge('A', 'B'))
53+
}
54+
55+
def 'test GREY edge in addEdge'() {
56+
setup:
57+
def fromCheck
58+
def toCheck
59+
def typeCheck
60+
61+
when:
62+
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', GREY) { from, to, type ->
63+
fromCheck = from
64+
toCheck = to
65+
typeCheck = type
66+
}
67+
68+
then:
69+
fromCheck == 'A'
70+
toCheck == 'B'
71+
typeCheck == BACK_EDGE
72+
edgeClassification.backEdges.contains(graph.edge('A', 'B'))
73+
}
74+
75+
def 'test BLACK forward edge in addEdge'() {
76+
setup:
77+
def fromCheck
78+
def toCheck
79+
def typeCheck
80+
81+
when:
82+
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', BLACK) { from, to, type ->
83+
fromCheck = from
84+
toCheck = to
85+
typeCheck = type
86+
}
87+
88+
then:
89+
fromCheck == 'A'
90+
toCheck == 'B'
91+
typeCheck == FORWARD_EDGE
92+
edgeClassification.forwardEdges.contains(graph.edge('A', 'B'))
93+
}
94+
95+
def 'test BLACK cross edge in addEdge'() {
96+
setup:
97+
def fromCheck
98+
def toCheck
99+
def typeCheck
100+
edgeClassification.forrest.addVertex(graph.vertex('B'))
101+
102+
when:
103+
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', BLACK) { from, to, type ->
104+
fromCheck = from
105+
toCheck = to
106+
typeCheck = type
107+
}
108+
109+
then:
110+
fromCheck == 'A'
111+
toCheck == 'B'
112+
typeCheck == CROSS_EDGE
113+
edgeClassification.crossEdges.contains(graph.edge('A', 'B'))
114+
}
115+
}

0 commit comments

Comments
 (0)