Skip to content

Commit

Permalink
Merge branch 'release/0.7.0'. fixes #20.
Browse files Browse the repository at this point in the history
  • Loading branch information
moaxcp committed Mar 21, 2017
2 parents 4defe08 + 5e46b2c commit fb67ea6
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 18 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ Contributions are welcome. Please submit a pull request to the develop branch in

# Releases

## 0.7.0

* Added support for classifying edges. This can be used to detect cycles in a graph.
* Added test coverage.
* Added javadoc.

## 0.6.0

* fixed issue with logging when optimizations are turned off
Expand Down
5 changes: 5 additions & 0 deletions src/main/groovy/graph/DefaultVertexFactory.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ package graph
* base class Vertex.
*/
class DefaultVertexFactory implements VertexFactory {
/**
* Creates a new Vertex with the provided name.
* @param name
* @return
*/
@Override
Vertex newVertex(String name) {
new Vertex(name:name)
Expand Down
18 changes: 17 additions & 1 deletion src/main/groovy/graph/DepthFirstTraversalSpec.groovy
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package graph

/**
* Specification for a DepthFirstTraversal. Contains actions that are called when an
* event happens during the traversal.
*/
class DepthFirstTraversalSpec extends TraversalSpec {
private Closure preorderClosure
private Closure postorderClosure
private Closure classifyEdgeClosure

/**
* returns the preorder event.
Expand All @@ -24,6 +24,14 @@ class DepthFirstTraversalSpec extends TraversalSpec {
postorderClosure
}

/**
* returns the classifyEdge event.
* @return
*/
Closure getClassifyEdge() {
classifyEdgeClosure
}

/**
* method to set the preorder event
* @param preorderClosure
Expand All @@ -41,4 +49,12 @@ class DepthFirstTraversalSpec extends TraversalSpec {
void postorder(Closure postorderClosure) {
this.postorderClosure = postorderClosure
}

/**
* method to set the classifyEdge event
* @param classifyEdgeClosure
*/
void classifyEdge(Closure classifyEdgeClosure) {
this.classifyEdgeClosure = classifyEdgeClosure
}
}
78 changes: 78 additions & 0 deletions src/main/groovy/graph/EdgeClassification.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package graph

import groovy.transform.PackageScope

/**
* The results from classifyEdges in Graph.
*/
class EdgeClassification {
Graph forrest = []
List<Edge> backEdges = []
List<Edge> treeEdges = []
List<Edge> forwardEdges = []
List<Edge> crossEdges = []

/**
* The resulting edge type. Used in addEdge to notify the action closure.
*/
enum EdgeType {
/**
* When the followed edge is GREY
*/
BACK_EDGE,
/**
* When the followed edge is WHITE
*/
TREE_EDGE,
/**
* When the followed edge is BLACK and the connecting vertex is not in the forrest
*/
FORWARD_EDGE,
/**
* When the followed edge is BLACK and the connecting vertex is in the forreest
*/
CROSS_EDGE
}

/**
* Adds an edge calling action with the classification.
* @param graph
* @param edge
* @param from
* @param to
* @param toColor
* @param action
*/
@PackageScope
void addEdge(Graph graph, Edge edge, String from, String to, Graph.TraversalColor toColor, Closure action) {
def edgeType
switch(toColor) {
case Graph.TraversalColor.WHITE:
forrest.addVertex(graph.vertex(from))
forrest.addVertex(graph.vertex(to))
forrest.addEdge(edge)
treeEdges << edge
edgeType = EdgeClassification.EdgeType.TREE_EDGE
break

case Graph.TraversalColor.GREY:
backEdges << edge
edgeType = EdgeClassification.EdgeType.BACK_EDGE
break

case Graph.TraversalColor.BLACK:
if(forrest.vertices[to]) {
crossEdges << edge
edgeType = EdgeClassification.EdgeType.CROSS_EDGE
} else {
forwardEdges << edge
edgeType = EdgeClassification.EdgeType.FORWARD_EDGE
}
break

default:
throw new IllegalStateException("Edge from $from to $to needs to be WHITE, GREY, or BLACK.")
}
action(from, to, edgeType)
}
}
47 changes: 46 additions & 1 deletion src/main/groovy/graph/Graph.groovy
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package graph

import groovy.transform.PackageScope

/**
* Implementation of a Graph. Vertices are represented as key/value pairs in a map. The edges connect the keys in
* the map to form a graph. The values in the map are the contents of the vertices. This makes it easy to represent
Expand All @@ -26,7 +28,7 @@ class Graph {
/**
* Defines the color for a vertex when traversing.
*/
def enum TraversalColor {
enum TraversalColor {
/**
* an undiscovered vertex
*/
Expand Down Expand Up @@ -93,6 +95,16 @@ class Graph {
plugin.apply(this)
}

/**
* Adds a vertex object directly.
* @param vertex
* @return true if add was successful.
*/
@PackageScope
boolean addVertex(Vertex vertex) {
vertices[vertex.name] = vertex
}

/**
* Creates a map with the name key set to the name param. The map
* and closure are passed to vertex(Map, Clousre)
Expand Down Expand Up @@ -139,6 +151,16 @@ class Graph {
vertex
}

/**
* Adds an edge object directly.
* @param edge
* @return true if add was successful.
*/
@PackageScope
boolean addEdge(Edge edge) {
edges << edge
}

/**
* Creates a map with the entries one and two set to the params one and two.
* This map is then passed to edge(map, closure = null).
Expand Down Expand Up @@ -360,6 +382,12 @@ class Graph {
for (int index = 0; index < adjacentEdges.size(); index++) { //cannot stop and each() call on adjacentEdges
Edge edge = adjacentEdges[index]
String connectedName = root == edge.one ? edge.two : edge.one
//if white tree edge
//if grey back edge
//if black forward or cross edge. must keep track of trees to say cross edge.
if(spec.classifyEdge && spec.classifyEdge(edge, root, connectedName, spec.colors[connectedName]) == Traversal.STOP) {
return Traversal.STOP
}
if (spec.colors[connectedName] == TraversalColor.WHITE) {
if (Traversal.STOP == depthFirstTraversalConnected(connectedName, spec)) {
return Traversal.STOP
Expand Down Expand Up @@ -408,6 +436,7 @@ class Graph {
}
def traversal = spec.visit(vertices[root])
if (traversal == Traversal.STOP) {
spec.colors[root] = TraversalColor.GREY
return traversal
}
spec.colors[root] = TraversalColor.GREY
Expand All @@ -422,6 +451,7 @@ class Graph {
if (spec.colors[connected] == TraversalColor.WHITE) {
traversal = spec.visit(vertices[connected])
if (traversal == Traversal.STOP) {
spec.colors[connected] = TraversalColor.GREY
return traversal
}
spec.colors[connected] = TraversalColor.GREY
Expand All @@ -432,4 +462,19 @@ class Graph {
}
null
}

/**
* Classifies edges in a depthFirstTraversal returning the results.
* @param action passed into EdgeClassification.addEdge
* @return the resulting EdgeClassification
*/
EdgeClassification classifyEdges(Closure action) {
EdgeClassification ec = new EdgeClassification()
depthFirstTraversal {
classifyEdge { Edge edge, String from, String to, TraversalColor toColor ->
ec.addEdge(this, edge, from, to, toColor, action)
}
}
ec
}
}
15 changes: 0 additions & 15 deletions src/test/groovy/graph/DirectedGraphEdgeSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,4 @@ class DirectedGraphEdgeSpec extends Specification {
it.one == 'step2' && it.two == 'step3'
}
}

def 'can modify existing edge'() {
setup:
def edge = graph.edge 'step1', 'step2'

when:
def testValue = false
def testEdge = graph.edge 'step1', 'step2', {
testValue = delegate == edge
}

then:
testValue
edge == testEdge
}
}
115 changes: 115 additions & 0 deletions src/test/groovy/graph/EdgeClassificationSpec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package graph

import spock.lang.Specification

import static graph.Graph.TraversalColor.*
import static graph.EdgeClassification.EdgeType.*

class EdgeClassificationSpec extends Specification {
def graph = new Graph()
def edgeClassification = new EdgeClassification()

def setup() {
graph.with {
vertex 'A'
vertex 'B'
edge 'A', 'B'
}
}

def 'test from and to in addEdge'() {
setup:
def fromCheck
def toCheck

when:
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', WHITE) { from, to, type ->
fromCheck = from
toCheck = to
}

then:
fromCheck == 'A'
toCheck == 'B'
}

def 'test WHITE edge in addEdge'() {
setup:
def typeCheck

when:
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', WHITE) { from, to, type ->
typeCheck = type
}

then:
typeCheck == TREE_EDGE
edgeClassification.forrest.vertices['A'] == graph.vertex('A')
edgeClassification.forrest.vertices['B'] == graph.vertex('B')
edgeClassification.forrest.vertices.size() == 2
edgeClassification.forrest.edge('A', 'B') == graph.edge('A', 'B')
edgeClassification.forrest.edges.size() == 1
edgeClassification.treeEdges.contains(graph.edge('A', 'B'))
}

def 'test GREY edge in addEdge'() {
setup:
def fromCheck
def toCheck
def typeCheck

when:
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', GREY) { from, to, type ->
fromCheck = from
toCheck = to
typeCheck = type
}

then:
fromCheck == 'A'
toCheck == 'B'
typeCheck == BACK_EDGE
edgeClassification.backEdges.contains(graph.edge('A', 'B'))
}

def 'test BLACK forward edge in addEdge'() {
setup:
def fromCheck
def toCheck
def typeCheck

when:
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', BLACK) { from, to, type ->
fromCheck = from
toCheck = to
typeCheck = type
}

then:
fromCheck == 'A'
toCheck == 'B'
typeCheck == FORWARD_EDGE
edgeClassification.forwardEdges.contains(graph.edge('A', 'B'))
}

def 'test BLACK cross edge in addEdge'() {
setup:
def fromCheck
def toCheck
def typeCheck
edgeClassification.forrest.addVertex(graph.vertex('B'))

when:
edgeClassification.addEdge(graph, graph.edge('A', 'B'), 'A', 'B', BLACK) { from, to, type ->
fromCheck = from
toCheck = to
typeCheck = type
}

then:
fromCheck == 'A'
toCheck == 'B'
typeCheck == CROSS_EDGE
edgeClassification.crossEdges.contains(graph.edge('A', 'B'))
}
}
Loading

0 comments on commit fb67ea6

Please sign in to comment.