graphr 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +18 -0
- data/README.md +46 -0
- data/RELEASE +46 -0
- data/lib/graph/Rakefile +6 -0
- data/lib/graph/base_extensions.rb +70 -0
- data/lib/graph/directed_graph.rb +476 -0
- data/lib/graph/graphviz_dot.rb +182 -0
- data/lib/graph/version.rb +3 -0
- data/tests/directed_graph.rb +215 -0
- data/tests/graphviz_dot.rb +140 -0
- data/tests/test.rb +3 -0
- metadata +59 -0
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Graph-related Ruby classes written by Robert Feld in 2001.
|
2
|
+
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
4
|
+
it under the terms of the GNU General Public License as published by
|
5
|
+
the Free Software Foundation, either version 3 of the License, or
|
6
|
+
(at your option) any later version.
|
7
|
+
|
8
|
+
This program is distributed in the hope that it will be useful,
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
GNU General Public License for more details.
|
12
|
+
|
13
|
+
This license also applies to the included Stanford CoreNLP files.
|
14
|
+
|
15
|
+
You should have received a copy of the GNU General Public License
|
16
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
Author: Robert Feldt, feldt@ce.chalmers.se. All rights reserved.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
##graphr
|
2
|
+
|
3
|
+
###About
|
4
|
+
|
5
|
+
Graph-related Ruby classes [written by Robert Feld](http://rockit.sourceforge.net/subprojects/graphr/) in 2001, but never released as a gem.
|
6
|
+
|
7
|
+
* DirectedGraph - fairly extensive directed graph class.
|
8
|
+
* DotGraphPrinter - output a graph in GraphViz's dot format and invoke dot.
|
9
|
+
|
10
|
+
###Example Usage
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# Lets crank out a simple graph...
|
14
|
+
require 'graph/graphviz_dot'
|
15
|
+
|
16
|
+
# In this simple example we don't even have a "real" graph
|
17
|
+
# just an Array with the links. The optional third element
|
18
|
+
# of a link is link information. The nodes in this graph are
|
19
|
+
# implicit in the links. If we had additional nodes that were
|
20
|
+
# not linked we would supply them in an array as 2nd parameter.
|
21
|
+
links = [[:start, 1, "*"], [1, 1, "a"], [1, 2, "~a"], [2, :stop, "*"]]
|
22
|
+
dgp = DotGraphPrinter.new(links)
|
23
|
+
|
24
|
+
# We specialize the printer to change the shape of nodes
|
25
|
+
# based on their names.
|
26
|
+
dgp.node_shaper = proc do |n|
|
27
|
+
["start", "stop"].include?(n.to_s) ? "doublecircle" : "box"
|
28
|
+
end
|
29
|
+
|
30
|
+
# We can also set the attributes on individual nodes and edges.
|
31
|
+
# These settings override the default shapers and labelers.
|
32
|
+
dgp.set_node_attributes(2, :shape => "diamond")
|
33
|
+
|
34
|
+
# Add URL link from node (this only work in some output formats?)
|
35
|
+
# Note the extra quotes needed!
|
36
|
+
dgp.set_node_attributes(2, :URL => '"node2.html"')
|
37
|
+
|
38
|
+
# And now output to files
|
39
|
+
dgp.write_to_file("g.png", "png") # Generate png file
|
40
|
+
dgp.orientation = "landscape" # Dot problem with PS orientation
|
41
|
+
dgp.write_to_file("g.ps") # Generate postscript file
|
42
|
+
```
|
43
|
+
|
44
|
+
###License
|
45
|
+
|
46
|
+
Copyright (c) 2001 Robert Feldt, feldt@ce.chalmers.se. All rights reserved. Distributed under GPL. See LICENSE.
|
data/RELEASE
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
2012-07-23, LM
|
2
|
+
* Released as 0.2.0
|
3
|
+
* First version published as a gem.
|
4
|
+
|
5
|
+
2001-11-16, RF
|
6
|
+
* Released 0.1.9
|
7
|
+
* Added DirectedGraph#strongly_connected_components and
|
8
|
+
DirectedGraph#num_vertices with tests, all written by Tanaka Akira.
|
9
|
+
Thanks Tanaka-san! (Or is it Akira-san?)
|
10
|
+
* Released 0.1.8
|
11
|
+
* Added test that negative hash values are never used in
|
12
|
+
node names. Dot can't handle them.
|
13
|
+
* Released 0.1.7
|
14
|
+
* Added "n" char in front of the node names so that they can
|
15
|
+
be used in well-formed XML docs (nums not allowed in first char).
|
16
|
+
Thanks to Tobias Reif for this one.
|
17
|
+
* Extended example in README to show how URL's can be added to nodes.
|
18
|
+
|
19
|
+
2001-11-01, RF
|
20
|
+
* Use hash value as node number in dot files instead of id value,
|
21
|
+
since the latter is not the same for strings even though they
|
22
|
+
are equal.
|
23
|
+
* Added node attributes in the same way as edge attributes.
|
24
|
+
|
25
|
+
2001-10-26, RF
|
26
|
+
* Released as 0.1.3.
|
27
|
+
* Redesign. Skipped DotGraph and DotGraphFormatter class and moved
|
28
|
+
everything to new class DotGRaphPrinter.
|
29
|
+
* Size does not have a default value so dot uses its default.
|
30
|
+
* Default value for orientation is "porttrait" which fits most
|
31
|
+
image output formats (except postscript!).
|
32
|
+
|
33
|
+
2001-10-26, RF
|
34
|
+
* Release 0.1.2
|
35
|
+
* Added DotGraphFormatter#set_edge_attributes
|
36
|
+
* Added unit tests for DotGraphFormatter
|
37
|
+
* Added accessors to DotGraphFormatter for orientation and size
|
38
|
+
* Added this Changelog
|
39
|
+
|
40
|
+
2001-10-23, RF
|
41
|
+
* Fixed minor bugs relating to file structure.
|
42
|
+
|
43
|
+
2001-10-23, Robert Feldt (RF)
|
44
|
+
* First version 0.1.0 released.
|
45
|
+
* Created from files in rockit. Extracted since people asked for
|
46
|
+
support for GraphViz's dot.
|
data/lib/graph/Rakefile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
class Array
|
2
|
+
def equality_uniq
|
3
|
+
uniq_elements = []
|
4
|
+
self.each {|e| uniq_elements.push(e) unless uniq_elements.index(e)}
|
5
|
+
uniq_elements
|
6
|
+
end
|
7
|
+
|
8
|
+
def delete_at_indices(indices = [])
|
9
|
+
not_deleted = Array.new
|
10
|
+
self.each_with_index {|e,i| not_deleted.push(e) if !indices.include?(i)}
|
11
|
+
not_deleted
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class DefaultInitArray < Array
|
16
|
+
def initialize(*args, &initblock)
|
17
|
+
super(*args)
|
18
|
+
@initblock = initblock
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](index)
|
22
|
+
super(index) || (self[index] = @initblock.call(index))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ArrayOfArrays < DefaultInitArray
|
27
|
+
@@create_array = proc{|i| Array.new}
|
28
|
+
def initialize(*args)
|
29
|
+
super(*args, &@@create_array)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ArrayOfHashes < DefaultInitArray
|
34
|
+
@@create_hash = proc{|i| Hash.new}
|
35
|
+
def initialize(*args)
|
36
|
+
super(*args, &@@create_hash)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Hash which takes a block that is called to give a default value when a key
|
41
|
+
# has the value nil in the hash.
|
42
|
+
class DefaultInitHash < Hash
|
43
|
+
def initialize(*args, &initblock)
|
44
|
+
super(*args)
|
45
|
+
@initblock = initblock
|
46
|
+
end
|
47
|
+
|
48
|
+
def [](key)
|
49
|
+
super(key) || (self[key] = @initblock.call(key))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
unless Object.constants.include?("TimesClass")
|
54
|
+
TimesClass = (RUBY_VERSION < "1.7") ? Time : Process
|
55
|
+
end
|
56
|
+
|
57
|
+
def time_and_puts(string, &block)
|
58
|
+
if $TIME_AND_PUTS_VERBOSE
|
59
|
+
print string; STDOUT.flush
|
60
|
+
end
|
61
|
+
starttime = [Time.new, TimesClass.times]
|
62
|
+
block.call
|
63
|
+
endtime = [Time.new, TimesClass.times]
|
64
|
+
duration = endtime[0] - starttime[0]
|
65
|
+
begin
|
66
|
+
load = [((endtime[1].utime+endtime[1].stime)-(starttime[1].utime+starttime[1].stime))/duration*100.0, 100.0].min
|
67
|
+
puts " (%.2f s %.2f%%)" % [duration, load] if $TIME_AND_PUTS_VERBOSE
|
68
|
+
rescue FloatDomainError
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,476 @@
|
|
1
|
+
require 'graph/base_extensions'
|
2
|
+
require 'graph/graphviz_dot'
|
3
|
+
|
4
|
+
class HashOfHash < DefaultInitHash
|
5
|
+
def initialize(&initBlock)
|
6
|
+
super do
|
7
|
+
if initBlock
|
8
|
+
DefaultInitHash.new(&initBlock)
|
9
|
+
else
|
10
|
+
Hash.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
GraphLink = Struct.new("GraphLink", :from, :to, :info)
|
17
|
+
class GraphLink
|
18
|
+
def inspect
|
19
|
+
info_str = info ? info.inspect + "-" : ""
|
20
|
+
"#{from.inspect}-#{info_str}>#{to.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class GraphTraversalException < Exception
|
25
|
+
attr_reader :node, :links, :link_info
|
26
|
+
def initialize(node, links, linkInfo)
|
27
|
+
@node, @links, @link_info = node, links, linkInfo
|
28
|
+
super(message)
|
29
|
+
end
|
30
|
+
|
31
|
+
def message
|
32
|
+
"There is no link from #{@node.inspect} having info #{@link_info.inspect} (valid links are #{@links.inspect})"
|
33
|
+
end
|
34
|
+
alias inspect message
|
35
|
+
end
|
36
|
+
|
37
|
+
class DirectedGraph
|
38
|
+
# This is a memory expensive variant that manages several additional
|
39
|
+
# information data structures to cut down on processing when the graph
|
40
|
+
# has been built.
|
41
|
+
|
42
|
+
attr_reader :links
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@link_map = HashOfHash.new {Array.new} # [from][to] -> array of links
|
46
|
+
@links = Array.new # All links in one array
|
47
|
+
@is_root = Hash.new # true iff root node
|
48
|
+
@is_leaf = Hash.new # true iff leaf node
|
49
|
+
end
|
50
|
+
|
51
|
+
def nodes
|
52
|
+
@is_root.keys
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_node(node)
|
56
|
+
unless include_node?(node)
|
57
|
+
@is_root[node] = @is_leaf[node] = true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def root?(node)
|
62
|
+
@is_root[node]
|
63
|
+
end
|
64
|
+
|
65
|
+
def leaf?(node)
|
66
|
+
@is_leaf[node]
|
67
|
+
end
|
68
|
+
|
69
|
+
def include_node?(node)
|
70
|
+
@is_root.has_key?(node)
|
71
|
+
end
|
72
|
+
|
73
|
+
def links_from_to(from, to)
|
74
|
+
@link_map[from][to]
|
75
|
+
end
|
76
|
+
|
77
|
+
def links_from(node)
|
78
|
+
@link_map[node].map {|to, links| links}.flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
def children(node)
|
82
|
+
@link_map[node].keys.select {|k| @link_map[node][k].length > 0}
|
83
|
+
end
|
84
|
+
|
85
|
+
# (Forced) add link will always add link even if there are already links
|
86
|
+
# between the nodes.
|
87
|
+
def add_link(from, to, informationOnLink = nil)
|
88
|
+
add_link_nodes(from, to)
|
89
|
+
link = GraphLink.new(from, to, informationOnLink)
|
90
|
+
links_from_to(from, to).push link
|
91
|
+
add_to_links(link)
|
92
|
+
link
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_link_nodes(from, to)
|
96
|
+
add_node(from)
|
97
|
+
add_node(to)
|
98
|
+
@is_leaf[from] = @is_root[to] = false
|
99
|
+
end
|
100
|
+
|
101
|
+
# Add link if not already linked
|
102
|
+
def link_nodes(from, to, info = nil)
|
103
|
+
links_from_to?(from, to) ? nil : add_link(from, to, info)
|
104
|
+
end
|
105
|
+
|
106
|
+
def links_from_to?(from, to)
|
107
|
+
not links_from_to(from, to).empty?
|
108
|
+
end
|
109
|
+
alias linked? links_from_to?
|
110
|
+
|
111
|
+
def add_to_links(link)
|
112
|
+
@links.push link
|
113
|
+
end
|
114
|
+
private :add_to_links
|
115
|
+
|
116
|
+
def each_reachable_node_once_depth_first(node, inclusive = true, &block)
|
117
|
+
children(node).each do |c|
|
118
|
+
recurse_each_reachable_depth_first_visited(c, Hash.new, &block)
|
119
|
+
end
|
120
|
+
block.call(node) if inclusive
|
121
|
+
end
|
122
|
+
alias each_reachable_node each_reachable_node_once_depth_first
|
123
|
+
|
124
|
+
def recurse_each_reachable_depth_first_visited(node, visited, &block)
|
125
|
+
visited[node] = true
|
126
|
+
children(node).each do |c|
|
127
|
+
unless visited[c]
|
128
|
+
recurse_each_reachable_depth_first_visited(c, visited, &block)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
block.call(node)
|
132
|
+
end
|
133
|
+
|
134
|
+
def each_reachable_node_once_breadth_first(node, inclusive = true, &block)
|
135
|
+
block.call(node) if inclusive
|
136
|
+
children(node).each do |c|
|
137
|
+
recurse_each_reachable_breadth_first_visited(c, Hash.new, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
alias each_reachable_node each_reachable_node_once_depth_first
|
141
|
+
|
142
|
+
def recurse_each_reachable_breadth_first_visited(node, visited, &block)
|
143
|
+
visited[node] = true
|
144
|
+
block.call(node)
|
145
|
+
children(node).each do |c|
|
146
|
+
unless visited[c]
|
147
|
+
recurse_each_reachable_breadth_first_visited(c, visited, &block)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def root_nodes
|
153
|
+
@is_root.reject {|key,val| val == false}.keys
|
154
|
+
end
|
155
|
+
alias_method :roots, :root_nodes
|
156
|
+
|
157
|
+
def leaf_nodes
|
158
|
+
@is_leaf.reject {|key,val| val == false}.keys
|
159
|
+
end
|
160
|
+
alias_method :leafs, :leaf_nodes
|
161
|
+
|
162
|
+
def internal_node?(node)
|
163
|
+
!root?(node) and !leaf?(node)
|
164
|
+
end
|
165
|
+
|
166
|
+
def internal_nodes
|
167
|
+
nodes.reject {|n| root?(n) or leaf?(n)}
|
168
|
+
end
|
169
|
+
|
170
|
+
def recurse_cyclic?(node, visited)
|
171
|
+
visited[node] = true
|
172
|
+
children(node).each do |c|
|
173
|
+
return true if visited[c] || recurse_cyclic?(c, visited)
|
174
|
+
end
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
def cyclic?
|
179
|
+
visited = Hash.new
|
180
|
+
root_nodes.each {|root| return true if recurse_cyclic?(root, visited)}
|
181
|
+
false
|
182
|
+
end
|
183
|
+
|
184
|
+
def acyclic?
|
185
|
+
not cyclic?
|
186
|
+
end
|
187
|
+
|
188
|
+
def transition(state, linkInfo)
|
189
|
+
link = links_from(state).detect {|l| l.info == linkInfo}
|
190
|
+
begin
|
191
|
+
link.to
|
192
|
+
rescue Exception
|
193
|
+
raise GraphTraversalException.new(state, links_from(state), linkInfo)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def traverse(fromState, alongLinksWithInfo = [])
|
198
|
+
state, len = fromState, alongLinksWithInfo.length
|
199
|
+
alongLinksWithInfo = alongLinksWithInfo.clone
|
200
|
+
while len > 0
|
201
|
+
state = transition(state, alongLinksWithInfo.shift)
|
202
|
+
len -= 1
|
203
|
+
end
|
204
|
+
state
|
205
|
+
end
|
206
|
+
|
207
|
+
def to_dot(nodeShaper = nil, nodeLabeler = nil, linkLabeler = nil)
|
208
|
+
dgp = DotGraphPrinter.new(links, nodes)
|
209
|
+
dgp.node_shaper = nodeShaper if nodeShaper
|
210
|
+
dgp.node_labeler = nodeLabeler if nodeLabeler
|
211
|
+
dgp.link_labeler = linkLabeler if linkLabeler
|
212
|
+
dgp
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_postscript_file(filename, nodeShaper = nil, nodeLabeler = nil,
|
216
|
+
linkLabeler = nil)
|
217
|
+
to_dot(nodeShaper, nodeLabeler, linkLabeler).write_to_file(filename)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Floyd-Warshal algorithm which should be O(n^3) where n is the number of
|
221
|
+
# nodes. We can probably work a bit on the constant factors!
|
222
|
+
def transitive_closure_floyd_warshal
|
223
|
+
vertices = nodes
|
224
|
+
tcg = DirectedGraph.new
|
225
|
+
num_nodes = vertices.length
|
226
|
+
|
227
|
+
# Direct links
|
228
|
+
for k in (0...num_nodes)
|
229
|
+
for s in (0...num_nodes)
|
230
|
+
vk, vs = vertices[k], vertices[s]
|
231
|
+
if vk == vs
|
232
|
+
tcg.link_nodes(vk,vs)
|
233
|
+
elsif linked?(vk, vs)
|
234
|
+
tcg.link_nodes(vk,vs)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Indirect links
|
240
|
+
for i in (0...num_nodes)
|
241
|
+
for j in (0...num_nodes)
|
242
|
+
for k in (0...num_nodes)
|
243
|
+
vi, vj, vk = vertices[i], vertices[j], vertices[k]
|
244
|
+
if not tcg.linked?(vi,vj)
|
245
|
+
tcg.link_nodes(vi, vj) if linked?(vi,vk) and linked?(vk,vj)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
tcg
|
251
|
+
end
|
252
|
+
alias_method :transitive_closure, :transitive_closure_floyd_warshal
|
253
|
+
|
254
|
+
def num_vertices
|
255
|
+
@is_root.size
|
256
|
+
end
|
257
|
+
alias num_nodes num_vertices
|
258
|
+
|
259
|
+
# strongly_connected_components uses the algorithm described in
|
260
|
+
# following paper.
|
261
|
+
# @Article{Tarjan:1972:DFS,
|
262
|
+
# author = "R. E. Tarjan",
|
263
|
+
# key = "Tarjan",
|
264
|
+
# title = "Depth First Search and Linear Graph Algorithms",
|
265
|
+
# journal = "SIAM Journal on Computing",
|
266
|
+
# volume = "1",
|
267
|
+
# number = "2",
|
268
|
+
# pages = "146--160",
|
269
|
+
# month = jun,
|
270
|
+
# year = "1972",
|
271
|
+
# CODEN = "SMJCAT",
|
272
|
+
# ISSN = "0097-5397 (print), 1095-7111 (electronic)",
|
273
|
+
# bibdate = "Thu Jan 23 09:56:44 1997",
|
274
|
+
# bibsource = "Parallel/Multi.bib, Misc/Reverse.eng.bib",
|
275
|
+
# }
|
276
|
+
def strongly_connected_components
|
277
|
+
order_cell = [0]
|
278
|
+
order_hash = {}
|
279
|
+
node_stack = []
|
280
|
+
components = []
|
281
|
+
|
282
|
+
order_hash.default = -1
|
283
|
+
|
284
|
+
nodes.each {|node|
|
285
|
+
if order_hash[node] == -1
|
286
|
+
recurse_strongly_connected_components(node, order_cell, order_hash, node_stack, components)
|
287
|
+
end
|
288
|
+
}
|
289
|
+
|
290
|
+
components
|
291
|
+
end
|
292
|
+
|
293
|
+
def recurse_strongly_connected_components(node, order_cell, order_hash, node_stack, components)
|
294
|
+
order = (order_cell[0] += 1)
|
295
|
+
reachable_minimum_order = order
|
296
|
+
order_hash[node] = order
|
297
|
+
stack_length = node_stack.length
|
298
|
+
node_stack << node
|
299
|
+
|
300
|
+
links_from(node).each {|link|
|
301
|
+
nextnode = link.to
|
302
|
+
nextorder = order_hash[nextnode]
|
303
|
+
if nextorder != -1
|
304
|
+
if nextorder < reachable_minimum_order
|
305
|
+
reachable_minimum_order = nextorder
|
306
|
+
end
|
307
|
+
else
|
308
|
+
sub_minimum_order = recurse_strongly_connected_components(nextnode, order_cell, order_hash, node_stack, components)
|
309
|
+
if sub_minimum_order < reachable_minimum_order
|
310
|
+
reachable_minimum_order = sub_minimum_order
|
311
|
+
end
|
312
|
+
end
|
313
|
+
}
|
314
|
+
|
315
|
+
if order == reachable_minimum_order
|
316
|
+
scc = node_stack[stack_length .. -1]
|
317
|
+
node_stack[stack_length .. -1] = []
|
318
|
+
components << scc
|
319
|
+
scc.each {|n|
|
320
|
+
order_hash[n] = num_vertices
|
321
|
+
}
|
322
|
+
end
|
323
|
+
return reachable_minimum_order;
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Parallel propagation in directed acyclic graphs. Should be faster than
|
328
|
+
# traversing all links from each start node if the graph is dense so that
|
329
|
+
# many traversals can be merged.
|
330
|
+
class DagPropagator
|
331
|
+
def initialize(directedGraph, startNodes, &propagationBlock)
|
332
|
+
@graph, @block = directedGraph, propagationBlock
|
333
|
+
init_start_nodes(startNodes)
|
334
|
+
@visited = Hash.new
|
335
|
+
end
|
336
|
+
|
337
|
+
def init_start_nodes(startNodes)
|
338
|
+
@startnodes = startNodes
|
339
|
+
end
|
340
|
+
|
341
|
+
def propagate
|
342
|
+
@visited.clear
|
343
|
+
propagate_recursive
|
344
|
+
end
|
345
|
+
|
346
|
+
def propagate_recursive
|
347
|
+
next_start_nodes = Array.new
|
348
|
+
@startnodes.each do |parent|
|
349
|
+
@visited[parent] = true
|
350
|
+
@graph.children(parent).each do |child|
|
351
|
+
@block.call(parent, child)
|
352
|
+
unless @visited[child] or next_start_nodes.include?(child)
|
353
|
+
next_start_nodes.push(child)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
if next_start_nodes.length > 0
|
358
|
+
@startnodes = next_start_nodes
|
359
|
+
propagate_recursive
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# Directed graph with fast traversal from children to parents (back)
|
365
|
+
class BackLinkedDirectedGraph < DirectedGraph
|
366
|
+
def initialize(*args)
|
367
|
+
super
|
368
|
+
@back_link_map = HashOfHash.new {Array.new} # [to][from] -> array of links
|
369
|
+
@incoming_links_info = DefaultInitHash.new {Array.new}
|
370
|
+
end
|
371
|
+
|
372
|
+
def add_link(from, to, informationOnLink = nil)
|
373
|
+
link = super
|
374
|
+
links_to_from(to, from).push link
|
375
|
+
if informationOnLink and
|
376
|
+
!@incoming_links_info[to].include?(informationOnLink)
|
377
|
+
@incoming_links_info[to].push informationOnLink
|
378
|
+
end
|
379
|
+
link
|
380
|
+
end
|
381
|
+
|
382
|
+
def incoming_links_info(node)
|
383
|
+
@incoming_links_info[node]
|
384
|
+
end
|
385
|
+
|
386
|
+
def back_transition(node, backLinkInfo)
|
387
|
+
link = links_to(node).detect {|l| l.info == backLinkInfo}
|
388
|
+
begin
|
389
|
+
link.from
|
390
|
+
rescue Exception
|
391
|
+
raise GraphTraversalException.new(node, links_to(node), backLinkInfo)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def back_traverse(state, alongLinksWithInfo = [])
|
396
|
+
len = alongLinksWithInfo.length
|
397
|
+
alongLinksWithInfo = alongLinksWithInfo.clone
|
398
|
+
while len > 0
|
399
|
+
state = back_transition(state, alongLinksWithInfo.pop)
|
400
|
+
len -= 1
|
401
|
+
end
|
402
|
+
state
|
403
|
+
end
|
404
|
+
|
405
|
+
def links_to(node)
|
406
|
+
@back_link_map[node].map {|from, links| links}.flatten
|
407
|
+
end
|
408
|
+
|
409
|
+
protected
|
410
|
+
|
411
|
+
def links_to_from(to, from)
|
412
|
+
@back_link_map[to][from]
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def calc_masks(start, stop, masks = Array.new)
|
417
|
+
mask = 1 << start
|
418
|
+
(start..stop).each {|i| masks[i] = mask; mask <<= 1}
|
419
|
+
masks
|
420
|
+
end
|
421
|
+
|
422
|
+
class BooleanMatrix
|
423
|
+
def initialize(objects)
|
424
|
+
@index, @objects, @matrix = Hash.new, objects, Array.new
|
425
|
+
cnt = 0
|
426
|
+
objects.each do |o|
|
427
|
+
@index[o] = cnt
|
428
|
+
@matrix[cnt] = 0 # Use Integers to represent the booleans
|
429
|
+
cnt += 1
|
430
|
+
end
|
431
|
+
@num_obects = cnt
|
432
|
+
end
|
433
|
+
|
434
|
+
@@masks_max = 1000
|
435
|
+
@@masks = calc_masks(0,@@masks_max)
|
436
|
+
|
437
|
+
def mask(index)
|
438
|
+
mask = @@masks[index]
|
439
|
+
unless mask
|
440
|
+
calc_masks(@@masks_max+1, index, @@masks)
|
441
|
+
mask = @masks[index]
|
442
|
+
end
|
443
|
+
mask
|
444
|
+
end
|
445
|
+
|
446
|
+
def or(index1, index2)
|
447
|
+
@matrix[index1] |= @matrix[index2]
|
448
|
+
end
|
449
|
+
|
450
|
+
def indices(anInteger)
|
451
|
+
index = 0
|
452
|
+
while anInteger > 0
|
453
|
+
yeild(index) if anInteger & 1
|
454
|
+
anInteger >>= 1
|
455
|
+
index += 1
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def directed_graph
|
460
|
+
dg = Directedgraph.new
|
461
|
+
@matrix.each_with_index do |v,i|
|
462
|
+
indices(v) do |index|
|
463
|
+
dg.link_nodes(@objects[i], @objects[index])
|
464
|
+
end
|
465
|
+
end
|
466
|
+
dg
|
467
|
+
end
|
468
|
+
|
469
|
+
def transitive_closure
|
470
|
+
for i in (0..@num_obects)
|
471
|
+
for j in (0..@num_obects)
|
472
|
+
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
class DotGraphPrinter
|
2
|
+
attr_accessor :orientation, :size, :color
|
3
|
+
|
4
|
+
# The following can be set to blocks of code that gives a default
|
5
|
+
# value for the node shapes, node labels and link labels, respectively.
|
6
|
+
attr_accessor :node_shaper, :node_labeler, :link_labeler
|
7
|
+
|
8
|
+
# A node shaper maps each node to a string describing its shape.
|
9
|
+
# Valid shapes are:
|
10
|
+
# "ellipse" (default)
|
11
|
+
# "box"
|
12
|
+
# "circle"
|
13
|
+
# "plaintext" (no outline)
|
14
|
+
# "doublecircle"
|
15
|
+
# "diamond"
|
16
|
+
# Not yet supported or untested once are:
|
17
|
+
# "polygon", "record", "epsf"
|
18
|
+
@@default_node_shaper = proc{|n| "box"}
|
19
|
+
|
20
|
+
@@default_node_labeler = proc{|n|
|
21
|
+
if Symbol===n
|
22
|
+
n.id2name
|
23
|
+
elsif String===n
|
24
|
+
n
|
25
|
+
else
|
26
|
+
n.inspect
|
27
|
+
end
|
28
|
+
}
|
29
|
+
|
30
|
+
@@default_link_labeler = proc{|info| info ? info.inspect : nil}
|
31
|
+
|
32
|
+
# links is either array of
|
33
|
+
# arrays [fromNode, toNode [, infoOnLink]], or
|
34
|
+
# objects with attributes :from, :to, :info
|
35
|
+
# nodes is array of node objects
|
36
|
+
# All nodes used in the links are used as nodes even if they are not
|
37
|
+
# in the "nodes" parameters.
|
38
|
+
def initialize(links = [], nodes = [])
|
39
|
+
@links, @nodes = links, add_nodes_in_links(links, nodes)
|
40
|
+
@node_attributes, @edge_attributes = Hash.new, Hash.new
|
41
|
+
set_default_values
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_default_values
|
45
|
+
@color = "black"
|
46
|
+
@size = "9,11"
|
47
|
+
@orientation = "portrait"
|
48
|
+
@node_shaper = @@default_node_shaper
|
49
|
+
@node_labeler = @@default_node_labeler
|
50
|
+
@link_labeler = @@default_link_labeler
|
51
|
+
end
|
52
|
+
|
53
|
+
def write_to_file(filename, fileType = "ps")
|
54
|
+
dotfile = temp_filename(filename)
|
55
|
+
File.open(dotfile, "w") {|f| f.write to_dot_specification}
|
56
|
+
system "dot -T#{fileType} -o #{filename} #{dotfile}"
|
57
|
+
File.delete(dotfile)
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_edge_attributes(anEdge, aHash)
|
61
|
+
# TODO check if attributes are valid dot edge attributes
|
62
|
+
edge = find_edge(anEdge)
|
63
|
+
set_attributes(edge, @edge_attributes, true, aHash)
|
64
|
+
end
|
65
|
+
|
66
|
+
def set_node_attributes(aNode, aHash)
|
67
|
+
# TODO check if attributes are valid dot node attributes
|
68
|
+
set_attributes(aNode, @node_attributes, true, aHash)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_dot_specification
|
72
|
+
set_edge_labels(@links)
|
73
|
+
set_node_labels_and_shape(@nodes)
|
74
|
+
"digraph G {\n" +
|
75
|
+
graph_parameters_to_dot_specification +
|
76
|
+
@nodes.uniq.map {|n| format_node(n)}.join(";\n") + ";\n" +
|
77
|
+
@links.uniq.map {|l| format_link(l)}.join(";\n") + ";\n" +
|
78
|
+
"}"
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
def find_edge(anEdge)
|
84
|
+
@links.each do |link|
|
85
|
+
return link if source_and_dest(link) == source_and_dest(anEdge)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_attributes(key, hash, override, newAttributeHash)
|
90
|
+
h = hash[key] || Hash.new
|
91
|
+
newAttributeHash = all_keys_to_s(newAttributeHash)
|
92
|
+
newAttributeHash.each do |k, value|
|
93
|
+
h[k] = value unless h[k] and !override
|
94
|
+
end
|
95
|
+
hash[key] = h
|
96
|
+
end
|
97
|
+
|
98
|
+
def graph_parameters_to_dot_specification
|
99
|
+
"graph [\n" +
|
100
|
+
(self.size ? " size = #{@size.inspect},\n" : "") +
|
101
|
+
(self.orientation ? " orientation = #{@orientation},\n" : "") +
|
102
|
+
(self.color ? " color = #{@color}\n" : "") +
|
103
|
+
"]\n"
|
104
|
+
end
|
105
|
+
|
106
|
+
def each_node_in_links(links)
|
107
|
+
links.each do |l|
|
108
|
+
src, dest = source_and_dest(l)
|
109
|
+
yield src
|
110
|
+
yield dest
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_nodes_in_links(links, nodes)
|
115
|
+
new_nodes = []
|
116
|
+
each_node_in_links(links) {|node| new_nodes.push node}
|
117
|
+
(nodes + new_nodes).uniq
|
118
|
+
end
|
119
|
+
|
120
|
+
def all_keys_to_s(aHash)
|
121
|
+
# MAYBE reuse existing hash?
|
122
|
+
Hash[*(aHash.map{|p| p[0] = p[0].to_s; p}.flatten)]
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_edge_labels(edges)
|
126
|
+
edges.each do |edge|
|
127
|
+
src, dest, info = get_link_data(edge)
|
128
|
+
if info
|
129
|
+
label = @link_labeler.call(info)
|
130
|
+
set_attributes(edge, @edge_attributes, false, :label =>label) if label
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_node_labels_and_shape(nodes)
|
136
|
+
nodes.each do |node|
|
137
|
+
set_attributes(node, @node_attributes, false,
|
138
|
+
:label => @node_labeler.call(node).inspect,
|
139
|
+
:shape => @node_shaper.call(node).inspect)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_link_data(link)
|
144
|
+
begin
|
145
|
+
return link.from, link.to, link.info
|
146
|
+
rescue Exception
|
147
|
+
return link[0], link[1], link[2]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def source_and_dest(link)
|
152
|
+
get_link_data(link)[0,2]
|
153
|
+
end
|
154
|
+
|
155
|
+
def format_attributes(attributes)
|
156
|
+
return "" unless attributes
|
157
|
+
strings = attributes.map {|a, v| "#{a}=#{v}"}
|
158
|
+
strings.length > 0 ? (" [" + strings.join(", ") + "]") : ("")
|
159
|
+
end
|
160
|
+
|
161
|
+
def mangle_node_name(node)
|
162
|
+
"n" + node.hash.abs.inspect
|
163
|
+
end
|
164
|
+
|
165
|
+
def format_link(link)
|
166
|
+
from, to, info = get_link_data(link)
|
167
|
+
mangle_node_name(from) + " -> " + mangle_node_name(to) +
|
168
|
+
format_attributes(@edge_attributes[link])
|
169
|
+
end
|
170
|
+
|
171
|
+
def format_node(node)
|
172
|
+
mangle_node_name(node) + format_attributes(@node_attributes[node])
|
173
|
+
end
|
174
|
+
|
175
|
+
def temp_filename(base = "tmp")
|
176
|
+
tmpfile = base + rand(100000).inspect
|
177
|
+
while test(?f, tmpfile)
|
178
|
+
tmpfile = base + rand(100000).inspect
|
179
|
+
end
|
180
|
+
tmpfile
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'graph/directed_graph'
|
2
|
+
|
3
|
+
class TestDirectedGraph < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@dg = DirectedGraph.new
|
6
|
+
[[1,2],[2,3],[3,2],[2,4]].each_with_index do |(src,dest),i|
|
7
|
+
@dg.add_link(src, dest, i)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_initialize
|
12
|
+
assert_kind_of(DirectedGraph, @dg)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_links
|
16
|
+
assert_equals(4, @dg.links.length)
|
17
|
+
assert_equals([1,2,2,3], @dg.links.map {|l| l.from}.sort)
|
18
|
+
assert_equals([2,2,3,4], @dg.links.map {|l| l.to}.sort)
|
19
|
+
assert_equals([0,1,2,3], @dg.links.map {|l| l.info}.sort)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_nodes
|
23
|
+
assert_equals([1,2,3,4], @dg.nodes.sort)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_root?
|
27
|
+
assert_equals(true, @dg.root?(1))
|
28
|
+
assert_equals(false, @dg.root?(2))
|
29
|
+
assert_equals(false, @dg.root?(3))
|
30
|
+
assert_equals(false, @dg.root?(4))
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_roots
|
34
|
+
assert_equals([1], @dg.roots)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_leaf?
|
38
|
+
assert_equals(false, @dg.leaf?(1))
|
39
|
+
assert_equals(false, @dg.leaf?(2))
|
40
|
+
assert_equals(false, @dg.leaf?(3))
|
41
|
+
assert_equals(true, @dg.leaf?(4))
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_leafs
|
45
|
+
assert_equals([4], @dg.leafs)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_internal_nodes
|
49
|
+
assert_equals([2,3], @dg.internal_nodes.sort)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_acyclic?
|
53
|
+
assert_equals(false, @dg.acyclic?)
|
54
|
+
|
55
|
+
dg = DirectedGraph.new
|
56
|
+
[[1,2],[2,3],[2,4]].each {|f,t| dg.add_link(f,t)}
|
57
|
+
assert_equals(true, dg.acyclic?)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_cyclic?
|
61
|
+
assert_equals(true, @dg.cyclic?)
|
62
|
+
|
63
|
+
dg = DirectedGraph.new
|
64
|
+
[[1,2],[2,3],[2,4]].each {|f,t| dg.add_link(f,t)}
|
65
|
+
assert_equals(false, dg.cyclic?)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_each_reachable_node_once_depth_first
|
69
|
+
# Test inclusive visit
|
70
|
+
visited_nodes = Array.new
|
71
|
+
@dg.each_reachable_node_once_depth_first(1) {|n| visited_nodes.push(n)}
|
72
|
+
assert_equals(4, visited_nodes.length)
|
73
|
+
assert_equals([3,4], visited_nodes[0..1].sort)
|
74
|
+
assert_equals([2,1], visited_nodes[2..3])
|
75
|
+
|
76
|
+
# Test exclusive visit
|
77
|
+
visited_nodes.clear
|
78
|
+
@dg.each_reachable_node_once_depth_first(1, false) do |n|
|
79
|
+
visited_nodes.push(n)
|
80
|
+
end
|
81
|
+
assert_equals(3, visited_nodes.length)
|
82
|
+
assert_equals([3,4], visited_nodes[0..1].sort)
|
83
|
+
assert_equals([2], visited_nodes[2..2])
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_each_reachable_node_once_breadth_first
|
87
|
+
# Test inclusive visit
|
88
|
+
visited_nodes = Array.new
|
89
|
+
@dg.each_reachable_node_once_breadth_first(1) {|n| visited_nodes.push(n)}
|
90
|
+
assert_equals(4, visited_nodes.length)
|
91
|
+
assert_equals([1,2], visited_nodes[0..1])
|
92
|
+
assert_equals([3,4], visited_nodes[2..3].sort)
|
93
|
+
|
94
|
+
# Test exclusive visit
|
95
|
+
visited_nodes.clear
|
96
|
+
@dg.each_reachable_node_once_breadth_first(1, false) do |n|
|
97
|
+
visited_nodes.push(n)
|
98
|
+
end
|
99
|
+
assert_equals(3, visited_nodes.length)
|
100
|
+
assert_equals([3,4], visited_nodes[1..2].sort)
|
101
|
+
assert_equals([2], visited_nodes[0..0])
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_link_from_to
|
105
|
+
assert_equals(1, @dg.links_from_to(1,2).length)
|
106
|
+
assert_equals(1, @dg.links_from_to(2,3).length)
|
107
|
+
assert_equals(1, @dg.links_from_to(3,2).length)
|
108
|
+
assert_equals(1, @dg.links_from_to(2,4).length)
|
109
|
+
assert_equals(0, @dg.links_from_to(4,2).length)
|
110
|
+
assert_equals(0, @dg.links_from_to(4,1).length)
|
111
|
+
assert_equals(0, @dg.links_from_to(2,1).length)
|
112
|
+
assert_equals(0, @dg.links_from_to(3,1).length)
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_link_from_to?
|
116
|
+
assert_equals(true, @dg.links_from_to?(1,2))
|
117
|
+
assert_equals(true, @dg.links_from_to?(2,3))
|
118
|
+
assert_equals(true, @dg.links_from_to?(3,2))
|
119
|
+
assert_equals(true, @dg.links_from_to?(2,4))
|
120
|
+
assert_equals(false, @dg.links_from_to?(2,1))
|
121
|
+
assert_equals(false, @dg.links_from_to?(3,1))
|
122
|
+
assert_equals(false, @dg.links_from_to?(4,1))
|
123
|
+
assert_equals(false, @dg.links_from_to?(4,2))
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_link_nodes
|
127
|
+
len = @dg.links.length
|
128
|
+
@dg.link_nodes(1,2)
|
129
|
+
assert_equals(len, @dg.links.length)
|
130
|
+
@dg.link_nodes(1,4)
|
131
|
+
assert_equals(len+1, @dg.links.length)
|
132
|
+
end
|
133
|
+
|
134
|
+
def temp_filename(suffix = "", prefix = "tmp")
|
135
|
+
filename = prefix + rand(100000).inspect + suffix
|
136
|
+
while test(?f, filename)
|
137
|
+
filename = prefix + rand(100000).inspect + suffix
|
138
|
+
end
|
139
|
+
filename
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_to_dot
|
143
|
+
d = @dg.to_dot
|
144
|
+
assert_kind_of(DotGraphPrinter, d)
|
145
|
+
d.write_to_file(filename = temp_filename(".ps"))
|
146
|
+
assert(test(?f, filename))
|
147
|
+
File.delete(filename)
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_to_postscript_file
|
151
|
+
filename = temp_filename(".ps")
|
152
|
+
d = @dg.to_postscript_file(filename)
|
153
|
+
assert(test(?f, filename))
|
154
|
+
File.delete(filename)
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_transition
|
158
|
+
assert_equals(2, @dg.transition(1, 0))
|
159
|
+
assert_equals(3, @dg.transition(2, 1))
|
160
|
+
assert_equals(2, @dg.transition(3, 2))
|
161
|
+
assert_equals(4, @dg.transition(2, 3))
|
162
|
+
assert_exception(GraphTraversalException) {@dg.transition(2,-1)}
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_traverse
|
166
|
+
assert_equals(4, @dg.traverse(1,[0,3]))
|
167
|
+
assert_equals(3, @dg.traverse(1,[0,1]))
|
168
|
+
assert_equals(2, @dg.traverse(1,[0,1,2]))
|
169
|
+
assert_equals(4, @dg.traverse(1,a = [0,1,2,3]))
|
170
|
+
assert_equals([0,1,2,3], a)
|
171
|
+
assert_equals(1, @dg.traverse(1,[]))
|
172
|
+
assert_exception(GraphTraversalException) {@dg.traverse(1,[0,1,2,-1])}
|
173
|
+
end
|
174
|
+
|
175
|
+
DecoratedNode = Struct.new("DecoratedNode", :num, :set)
|
176
|
+
|
177
|
+
def test_propagation
|
178
|
+
dg = DirectedGraph.new
|
179
|
+
nodes = Hash.new
|
180
|
+
[1,2,3,4].each {|n| nodes[n] = DecoratedNode.new(n,[n])}
|
181
|
+
[[1,2],[2,3],[3,2],[2,4]].each_with_index do |ft,i|
|
182
|
+
dg.add_link(nodes[ft[0]], nodes[ft[1]], i)
|
183
|
+
end
|
184
|
+
prop = DagPropagator.new(dg, dg.roots) {|p, c| c.set |= p.set}
|
185
|
+
prop.propagate
|
186
|
+
nodes = dg.nodes.sort {|n1, n2| n1.num <=> n2.num}
|
187
|
+
assert_equals([1], nodes[0].set)
|
188
|
+
assert_equals([1,2,3], nodes[1].set.sort)
|
189
|
+
assert_equals([1,2,3], nodes[2].set.sort)
|
190
|
+
# Note that cycles are not correctly handled! If they were the set below
|
191
|
+
# should be [1,2,3,4]!
|
192
|
+
assert_equals([1,2,4], nodes[3].set.sort)
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_transitive_closure
|
196
|
+
tcg = @dg.transitive_closure
|
197
|
+
assert_kind_of(DirectedGraph, tcg)
|
198
|
+
assert_equals([1,2,3,4], tcg.children(1).sort)
|
199
|
+
assert_equals([2,3,4], tcg.children(2).sort)
|
200
|
+
assert_equals([2,3,4], tcg.children(3).sort)
|
201
|
+
assert_equals([4], tcg.children(4).sort)
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_num_vertices
|
205
|
+
assert_equals(4, @dg.num_vertices)
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_strongly_connected_components
|
209
|
+
components = @dg.strongly_connected_components
|
210
|
+
assert_equal(3, components.length)
|
211
|
+
assert_equal([[4], [2, 3], [1]], components.map {|ns| ns.sort})
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
RUNIT::CUI::TestRunner.run(TestDirectedGraph.suite) if $0 == __FILE__
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'graph/graphviz_dot'
|
2
|
+
|
3
|
+
class TestDotGraphPrinter < Test::Unit::TestCase
|
4
|
+
def test_initialize
|
5
|
+
dgf = DotGraphPrinter.new
|
6
|
+
assert_kind_of(DotGraphPrinter, dgf)
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO refactor
|
10
|
+
def assert_dotgraph(dotGraph,
|
11
|
+
expectedNodeAttributes = {},
|
12
|
+
expectedEdgeAttributes = {},
|
13
|
+
expectedParameters = nil)
|
14
|
+
if expectedNodeAttributes
|
15
|
+
expectedNodeAttributes =
|
16
|
+
replace_nodes_with_their_mangled_names(expectedNodeAttributes)
|
17
|
+
end
|
18
|
+
if expectedEdgeAttributes
|
19
|
+
expectedEdgeAttributes =
|
20
|
+
replace_nodes_with_their_mangled_names(expectedEdgeAttributes, true)
|
21
|
+
end
|
22
|
+
lines = dotGraph.to_dot_specification.split("\n")
|
23
|
+
assert_equals("digraph G {", lines[0])
|
24
|
+
assert_equals("}", lines[-1])
|
25
|
+
lines[1..-2].each do |line|
|
26
|
+
if line =~ /(\w+)\s*->\s*(\w+)\s*(\[.*\])?/ && expectedEdgeAttributes
|
27
|
+
source, dest = $1, $2
|
28
|
+
expected = expectedEdgeAttributes[[source, dest]]
|
29
|
+
if expected
|
30
|
+
if $3
|
31
|
+
attributes = $3[1..-2].split(/\s*,\s*/)
|
32
|
+
assert_edge_attributes(expected, attributes)
|
33
|
+
else
|
34
|
+
assert(false, "There are no attributes but we expected #{expected.inspect}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
elsif line =~ /(\w+)\s+(\[.*\])?$/ && expectedNodeAttributes
|
38
|
+
nodeid, attributes = $1, $2[1..-2].split(", ")
|
39
|
+
assert_node_attributes(expectedNodeAttributes[nodeid], attributes)
|
40
|
+
elsif line =~ /(\w+)\s*=\s*([\w\"]+)/ && expectedParameters
|
41
|
+
parameter, value = $1, $2
|
42
|
+
expected = expectedParameters[parameter]
|
43
|
+
assert_equals(expected, $2) if expected
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def assert_edge_attributes(expected, actualAttributes)
|
49
|
+
actualAttributes.each do |attr|
|
50
|
+
attr =~ /(\w+)\s*=\s*(\w+)/
|
51
|
+
assert_equals(expected[$1], $2) if expected[$1]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def mangle_node_name(node)
|
56
|
+
"n" + node.hash.inspect
|
57
|
+
end
|
58
|
+
|
59
|
+
# The objects are referred to with their ids in the DotGraph, so we
|
60
|
+
# must map them to ids.
|
61
|
+
def replace_nodes_with_their_mangled_names(aHash, edges = false)
|
62
|
+
h = Hash.new
|
63
|
+
if edges
|
64
|
+
aHash.each do |k, val|
|
65
|
+
k[0] = mangle_node_name(k[0])
|
66
|
+
k[1] = mangle_node_name(k[1])
|
67
|
+
h[k] = val
|
68
|
+
end
|
69
|
+
else
|
70
|
+
aHash.each {|key, val| h[mangle_node_name(key)] = val}
|
71
|
+
end
|
72
|
+
h
|
73
|
+
end
|
74
|
+
|
75
|
+
def assert_node_attributes(expected, attributes)
|
76
|
+
return unless expected
|
77
|
+
attributes.each do |attr|
|
78
|
+
attr =~ /(\w+)=([\w\"]+)/
|
79
|
+
assert_equals(expected[$1], $2) if expected[$1]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_format
|
84
|
+
dgp = DotGraphPrinter.new([[1,2]])
|
85
|
+
assert_dotgraph(dgp, {1 => {"shape" => '"box"', "label" => '"1"'},
|
86
|
+
2 => {"shape" => '"box"', "label" => '"2"'}})
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_custom_node_shape
|
90
|
+
dgp = DotGraphPrinter.new([[1,2]])
|
91
|
+
dgp.node_shaper = proc{|n| n < 2 ? "doublecircle" : "ellipse"}
|
92
|
+
assert_dotgraph(dgp, {1 => {"shape" => '"doublecircle"', "label" => '"1"'},
|
93
|
+
2 => {"shape" => '"ellipse"', "label" => '"2"'}})
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_changing_orientation
|
97
|
+
dgp = DotGraphPrinter.new([[1,2]])
|
98
|
+
dgp.orientation = "portrait"
|
99
|
+
assert_dotgraph(dgp, nil, nil, {"orientation" => "portrait"})
|
100
|
+
|
101
|
+
dgp.orientation = "landscape"
|
102
|
+
assert_dotgraph(dgp, nil, nil, {"orientation" => "landscape"})
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_setting_edge_attributes
|
106
|
+
# If we want a graph with arrows going up we can achieve this by
|
107
|
+
# * reversing all edges, and
|
108
|
+
# * adding an edge attribute "dir=back", which changes the direction
|
109
|
+
# of the arrow.
|
110
|
+
#
|
111
|
+
# This is the only way to acheive this according to the dot user manual
|
112
|
+
# in section 2.4 on page 9.
|
113
|
+
#
|
114
|
+
# Below is an example.
|
115
|
+
#
|
116
|
+
edges = [[1,2]]
|
117
|
+
reversed_edges = edges.map {|e| e.reverse}
|
118
|
+
dgp = DotGraphPrinter.new
|
119
|
+
reversed_edges.each do |edge|
|
120
|
+
dgp.set_edge_attributes(edge, "dir" => "back")
|
121
|
+
end
|
122
|
+
|
123
|
+
assert_dotgraph(dgp, nil, {[2,1] => {"dir" => "back"}})
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_node_url_attribute
|
127
|
+
dgp = DotGraphPrinter.new [[1,2]]
|
128
|
+
dgp.set_node_attributes(1, :URL => "node1.html")
|
129
|
+
assert_dotgraph(dgp, {1 => {"URL" => '"node1.html"'}})
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_handles_negative_hash_values
|
133
|
+
dgp = DotGraphPrinter.new [[-1,-2]]
|
134
|
+
dgp.set_node_attributes(-11, :URL => "node1.html")
|
135
|
+
assert_dotgraph(dgp, {-11 => {"URL" => '"node1.html"'}})
|
136
|
+
assert_equals(nil, dgp.to_dot_specification =~ /n-\d+/)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
RUNIT::CUI::TestRunner.run(TestDotGraphPrinter.suite) if $0 == __FILE__
|
data/tests/test.rb
ADDED
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Feld
|
9
|
+
- Louis Mullie
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-07-24 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description: ! ' Graph-related Ruby classes, including a fairly extensive directed
|
16
|
+
graph class, and an interface to Graphviz'' DOT graph generator. '
|
17
|
+
email:
|
18
|
+
- feldt@ce.chalmers.se
|
19
|
+
- louis.mullie@gmail.com
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- lib/graph/base_extensions.rb
|
25
|
+
- lib/graph/directed_graph.rb
|
26
|
+
- lib/graph/graphviz_dot.rb
|
27
|
+
- lib/graph/Rakefile
|
28
|
+
- lib/graph/version.rb
|
29
|
+
- tests/directed_graph.rb
|
30
|
+
- tests/graphviz_dot.rb
|
31
|
+
- tests/test.rb
|
32
|
+
- README.md
|
33
|
+
- LICENSE
|
34
|
+
- RELEASE
|
35
|
+
homepage: https://github.com/louismullie/graphr
|
36
|
+
licenses: []
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.8.24
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Graph-related Ruby classes.
|
59
|
+
test_files: []
|