jumoku 0.1.3 → 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/README.md +0 -2
- data/lib/jumoku.rb +36 -8
- data/lib/jumoku/builders/arborescence.rb +11 -0
- data/lib/jumoku/builders/extended.rb +327 -0
- data/lib/jumoku/builders/raw_directed_tree.rb +33 -0
- data/lib/jumoku/builders/raw_undirected_tree.rb +37 -0
- data/lib/jumoku/builders/{raw_tree.rb → shared.rb} +38 -63
- data/lib/jumoku/builders/tree.rb +5 -330
- data/lib/jumoku/classes/tree_classes.rb +6 -4
- data/lib/jumoku/ext/ext.rb +8 -2
- data/lib/jumoku/raw_tree_node.rb +5 -5
- data/lib/jumoku/support/branch.rb +9 -1
- data/lib/jumoku/support/support.rb +6 -13
- data/lib/jumoku/tree_api.rb +7 -7
- data/lib/jumoku/version.rb +2 -2
- data/spec/{raw_tree_spec.rb → raw_undirected_tree_spec.rb} +48 -86
- data/spec/tree_spec.rb +36 -48
- metadata +8 -4
data/README.md
CHANGED
data/lib/jumoku.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
libpath = File.expand_path(File.dirname(__FILE__))
|
2
2
|
$:.unshift File.dirname(__FILE__) unless $:.include?(File.dirname(__FILE__)) || $:.include?(libpath)
|
3
3
|
$: << libpath + '/../vendor/git/plexus/lib'
|
4
|
-
puts $:.inspect
|
5
4
|
|
6
5
|
require 'pathname'
|
7
6
|
require 'pp'
|
@@ -9,15 +8,44 @@ require 'active_support'
|
|
9
8
|
require 'facets'
|
10
9
|
require 'plexus'
|
11
10
|
|
11
|
+
# Jumoku provides you with several modules and classes to build and manipulate
|
12
|
+
# tree graphs. Two basic implementations are available: undirected trees
|
13
|
+
# ({RawUndirectedTree}) and directed trees ({RawDirectedTree}).
|
14
|
+
#
|
15
|
+
# {Tree} is derived from the undirected flavour and sticks to the mathematical
|
16
|
+
# tree definition. {Arborescence} is derived from the directed flavour and is
|
17
|
+
# likely to be used as the basis to modelize hierarchy structures, such as a
|
18
|
+
# family tree, a file browser…
|
19
|
+
#
|
20
|
+
# Note that a node can be any Object. There is no "node type", therefore
|
21
|
+
# arguments which re expected to be nodes are simply labelled as "`node`"
|
22
|
+
# within this documentation. A nice object type to use as a node may be an
|
23
|
+
# OpenStruct or an [OpenObject](http://facets.rubyforge.org/apidoc/api/more/classes/OpenObject.html)
|
24
|
+
# (from the Facets library), both turning nodes into versatile handlers.
|
25
|
+
#
|
12
26
|
module Jumoku
|
13
|
-
#
|
14
|
-
autoload :TreeAPI,
|
15
|
-
autoload :
|
16
|
-
autoload :
|
17
|
-
autoload :RawTree, 'jumoku/classes/tree_classes'
|
18
|
-
autoload :Tree, 'jumoku/classes/tree_classes'
|
19
|
-
autoload :Branch, 'jumoku/support/branch'
|
27
|
+
# API, core implementations
|
28
|
+
autoload :TreeAPI, 'jumoku/tree_api'
|
29
|
+
autoload :Shared, 'jumoku/builders/shared'
|
30
|
+
autoload :Extended, 'jumoku/builders/extended'
|
20
31
|
|
32
|
+
# branch types
|
33
|
+
autoload :UndirectedBranch, 'jumoku/support/branch'
|
34
|
+
autoload :DirectedBranch, 'jumoku/support/branch'
|
35
|
+
|
36
|
+
# tree builders
|
37
|
+
autoload :RawUndirectedTreeBuilder, 'jumoku/builders/raw_undirected_tree'
|
38
|
+
autoload :RawDirectedTreeBuilder, 'jumoku/builders/raw_directed_tree'
|
39
|
+
autoload :TreeBuilder, 'jumoku/builders/tree'
|
40
|
+
autoload :ArborescenceBuilder, 'jumoku/builders/arborescence'
|
41
|
+
|
42
|
+
# tree classes
|
43
|
+
autoload :RawDirectedTree, 'jumoku/classes/tree_classes'
|
44
|
+
autoload :RawUndirectedTree, 'jumoku/classes/tree_classes'
|
45
|
+
autoload :Tree, 'jumoku/classes/tree_classes'
|
46
|
+
autoload :Arborescence, 'jumoku/classes/tree_classes'
|
47
|
+
|
48
|
+
# support
|
21
49
|
require 'jumoku/support/support'
|
22
50
|
require 'jumoku/ext/ext'
|
23
51
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Jumoku
|
2
|
+
# This builder extends {RawDirectedTreeBuilder} implementation.
|
3
|
+
#
|
4
|
+
# It provides a directed tree which acts as a hierarchical structure, known
|
5
|
+
# as an arborescence.
|
6
|
+
#
|
7
|
+
module ArborescenceBuilder
|
8
|
+
include RawDirectedTreeBuilder
|
9
|
+
include Extended
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
module Jumoku
|
2
|
+
# This module provides methods extending the core TreeAPI. Most of those
|
3
|
+
# methods are cloning the receiver tree and perform their operation on the
|
4
|
+
# duplicated object. Some other are helpers adding reflexivity to trees.
|
5
|
+
# There are also a few in-place methods.
|
6
|
+
#
|
7
|
+
module Extended
|
8
|
+
# Non destructive version of {RawTreeBuilder#add_node!} (works on a copy of the tree).
|
9
|
+
#
|
10
|
+
# @overload add_node!(n)
|
11
|
+
# @param [node] n
|
12
|
+
# @overload add_node!(b)
|
13
|
+
# @param [Branch] b Branch[node i, node j, label l = nil]; if i (j) already exists, then j (i) must not exist
|
14
|
+
# @return [Tree] a modified copy of `self`
|
15
|
+
# @see RawTreeBuilder#add_node!
|
16
|
+
#
|
17
|
+
def add_node(n, l = nil)
|
18
|
+
_clone.add_node!(n, l)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Non destructive version {RawTreeBuilder#add_branch!} (works on a copy of the tree).
|
22
|
+
#
|
23
|
+
# @overload add_branch!(i, j, l = nil)
|
24
|
+
# @param [node] i
|
25
|
+
# @param [node] j
|
26
|
+
# @param [Label] l
|
27
|
+
# @overload add_branch!(b)
|
28
|
+
# @param [Branch] b Branch[node i, node j, label l = nil]; if i (j) already exists, then j (i) must not exist
|
29
|
+
# @return [Tree] a modified copy of `self`
|
30
|
+
# @see RawTreeBuilder#add_branch!
|
31
|
+
#
|
32
|
+
def add_branch(i, j = nil, l = nil)
|
33
|
+
_clone.add_branch!(i, j, l)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Non destructive version of {RawTreeBuilder#remove_node!} (works on a copy of the tree).
|
37
|
+
#
|
38
|
+
# @param [node] n
|
39
|
+
# @return [Tree] a modified copy of `self`
|
40
|
+
# @see RawTreeBuilder#remove_node!
|
41
|
+
#
|
42
|
+
def remove_node(n)
|
43
|
+
_clone.remove_node!(n)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Non destructive version {RawTreeBuilder#remove_branch!} (works on a copy of the tree).
|
47
|
+
#
|
48
|
+
# You cannot remove non terminal branches as it would break the connectivity constraint
|
49
|
+
# of the tree.
|
50
|
+
#
|
51
|
+
# @param [node] i
|
52
|
+
# @param [node] j
|
53
|
+
# @return [Tree] a modified copy of `self`
|
54
|
+
# @see RawTreeBuilder#remove_branch!
|
55
|
+
#
|
56
|
+
def remove_branch(i, j = nil, *params)
|
57
|
+
_clone.remove_branch!(i, j)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Adds all specified nodes to the node set.
|
61
|
+
#
|
62
|
+
# The nodes defines implicit branches, that is it is mandatory to provide
|
63
|
+
# an odd number of connected nodes which do not form cycles. You may specify
|
64
|
+
# Branch objects within the list, though.
|
65
|
+
#
|
66
|
+
# Valid usage:
|
67
|
+
#
|
68
|
+
# tree = Tree.new # an empty tree
|
69
|
+
# tree.add_nodes!(1,2, 2,3, 3,4) # tree.nodes => [1, 2, 3, 4]
|
70
|
+
# # branches are (1-2), (2-3), (3-4)
|
71
|
+
# tree.add_nodes!(1,2, 2,3, Branch.new(3,4), 10,1)
|
72
|
+
# tree.add_nodes! [1,2, 2,3, 3,4]
|
73
|
+
#
|
74
|
+
# Invalid usages:
|
75
|
+
#
|
76
|
+
# tree = Tree.new # an empty tree
|
77
|
+
# tree.add_nodes!(1, 2, 3) # even number of nodes
|
78
|
+
# tree.add_nodes!(1,2, 2,3, 3,1) # cycle
|
79
|
+
# tree.add_nodes!(1,2, 4,5) # not connected
|
80
|
+
#
|
81
|
+
# A helper exist to make it easy to add nodes in a suite.
|
82
|
+
# See {TreeBuilder#add_consecutive_nodes! add_consecutive_nodes!}.
|
83
|
+
#
|
84
|
+
# @param [#each] *a an Enumerable nodes set
|
85
|
+
# @return [Tree] `self`
|
86
|
+
#
|
87
|
+
def add_nodes!(*a)
|
88
|
+
a = a.flatten.expand_branches!
|
89
|
+
if a.size == 1 # This allows for using << to add the root as well,
|
90
|
+
if empty? # so that you may write:
|
91
|
+
add_node! a.only # Tree.new << :root << [:root, 1] << [1,2, 2,3, Branch.new(3,4)]...
|
92
|
+
else
|
93
|
+
raise RawTreeError, "Cannot add a node without a neighbour."
|
94
|
+
end
|
95
|
+
else
|
96
|
+
a.expand_branches!.each_nodes_pair { |pair| add_branch! pair[0], pair[1] }
|
97
|
+
end
|
98
|
+
self
|
99
|
+
end
|
100
|
+
alias << add_nodes!
|
101
|
+
|
102
|
+
# Same as {TreeBuilder#add_nodes! add_nodes!} but works on a copy of the receiver.
|
103
|
+
#
|
104
|
+
# @param [#each] *a an Enumerable nodes set
|
105
|
+
# @return [Tree] a modified copy of `self`
|
106
|
+
#
|
107
|
+
def add_nodes(*a)
|
108
|
+
_clone.add_nodes!(*a)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Allows for adding consecutive nodes, each nodes being connected to their previous and
|
112
|
+
# next neighbours.
|
113
|
+
#
|
114
|
+
# You may pass {Branch branches} within the nodes list.
|
115
|
+
#
|
116
|
+
# Tree.new.add_consecutive_nodes!(1, 2, 3, :four, Branch.new(:foo, "bar")).branches
|
117
|
+
# # => (1=2, 2=3, 3=:four, :four=:foo, :foo="bar")
|
118
|
+
#
|
119
|
+
# @param [Array(nodes)] *a flat array of unique nodes
|
120
|
+
# @return [Tree] `self`
|
121
|
+
#
|
122
|
+
def add_consecutive_nodes!(*a)
|
123
|
+
# FIXME: really this may not be as efficient as it could be.
|
124
|
+
# We may get rid of nodes duplication (no expand_branches!)
|
125
|
+
# and add nodes by pair, shifting one node at a time from the list.
|
126
|
+
add_nodes!(a.expand_branches!.create_nodes_pairs_list)
|
127
|
+
self
|
128
|
+
end
|
129
|
+
|
130
|
+
# Same as {TreeBuilder#add_consecutive_nodes! add_consecutive_nodes!} but
|
131
|
+
# works on a copy of the receiver.
|
132
|
+
#
|
133
|
+
# @param [Array(nodes)] *a flat array of unique nodes
|
134
|
+
# @return [Tree] a modified copy of `self`
|
135
|
+
#
|
136
|
+
def add_consecutive_nodes(*a)
|
137
|
+
_clone.add_consecutive_nodes!(*a)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Adds all branches mentionned in the specified Enumerable to the branch set.
|
141
|
+
#
|
142
|
+
# Elements of the Enumerable can be either two-element arrays or instances of
|
143
|
+
# {Branch}.
|
144
|
+
#
|
145
|
+
# @param [#each] *a an Enumerable branches set
|
146
|
+
# @return [Tree] `self`
|
147
|
+
#
|
148
|
+
def add_branches!(*a)
|
149
|
+
a.expand_branches!.each_nodes_pair { |pair| add_branch! pair[0], pair[1] }
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
# Same as {TreeBuilder#add_branches! add_branches!} but works on a copy of the receiver.
|
154
|
+
#
|
155
|
+
# @param [#each] *a an Enumerable branches set
|
156
|
+
# @return [Tree] a modified copy of `self`
|
157
|
+
#
|
158
|
+
def add_branches(*a)
|
159
|
+
_clone.add_branches!(*a)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Removes all nodes mentionned in the specified Enumerable from the tree.
|
163
|
+
#
|
164
|
+
# The process relies on {RawTreeBuilder#remove_node! remove_node!}.
|
165
|
+
#
|
166
|
+
# @param [#each] *a an Enumerable nodes set
|
167
|
+
# @return [Tree] `self`
|
168
|
+
#
|
169
|
+
def remove_nodes!(*a)
|
170
|
+
a.flatten.each { |v| remove_node! v }
|
171
|
+
self
|
172
|
+
end
|
173
|
+
alias delete_nodes! remove_nodes!
|
174
|
+
|
175
|
+
# Same as {TreeBuilder#remove_nodes! remove_nodes!} but works on a copy of the receiver.
|
176
|
+
#
|
177
|
+
# @param [#each] *a a node Enumerable set
|
178
|
+
# @return [Tree] a modified copy of `self`
|
179
|
+
#
|
180
|
+
def remove_nodes(*a)
|
181
|
+
_clone.remove_nodes!(*a)
|
182
|
+
end
|
183
|
+
alias delete_nodes remove_nodes
|
184
|
+
|
185
|
+
# Removes all branches mentionned in the specified Enumerable from the tree.
|
186
|
+
#
|
187
|
+
# @param [#each] *a an Enumerable branches set
|
188
|
+
# @return [Tree] `self`
|
189
|
+
#
|
190
|
+
def remove_branches!(*a)
|
191
|
+
a.expand_branches!.each_nodes_pair { |pair| remove_branch! pair[0], pair[1] }
|
192
|
+
self
|
193
|
+
end
|
194
|
+
alias delete_branches! remove_branches!
|
195
|
+
|
196
|
+
# Same as {TreeBuilder#remove_branches! remove_branches!} but works on a copy of the receiver.
|
197
|
+
#
|
198
|
+
# @param [#each] *a an Enumerable branches set
|
199
|
+
# @return [Tree] a modified copy of `self`
|
200
|
+
# @see RawTreeBuilder#remove_branch!
|
201
|
+
#
|
202
|
+
def remove_branches(*a)
|
203
|
+
_clone.remove_branches!(*a)
|
204
|
+
end
|
205
|
+
alias delete_branches remove_branches
|
206
|
+
|
207
|
+
# Returns true if the specified node belongs to the tree.
|
208
|
+
#
|
209
|
+
# This is a default implementation that is of O(n) average complexity.
|
210
|
+
# If a subclass uses a hash to store nodes, then this can be
|
211
|
+
# made into an O(1) average complexity operation.
|
212
|
+
#
|
213
|
+
# @param [node] n
|
214
|
+
# @return [Boolean]
|
215
|
+
#
|
216
|
+
def node?(n)
|
217
|
+
nodes.include?(n)
|
218
|
+
end
|
219
|
+
alias has_node? node?
|
220
|
+
|
221
|
+
# Returns true if all specified nodes belong to the tree.
|
222
|
+
#
|
223
|
+
# @param [nodes]
|
224
|
+
# @return [Boolean]
|
225
|
+
#
|
226
|
+
def nodes?(*maybe_nodes)
|
227
|
+
maybe_nodes.all? { |node| nodes.include? node }
|
228
|
+
end
|
229
|
+
alias has_nodes? nodes?
|
230
|
+
|
231
|
+
# Returns true if any of the specified nodes belongs to the tree.
|
232
|
+
#
|
233
|
+
# @param [nodes]
|
234
|
+
# @return [Boolean]
|
235
|
+
#
|
236
|
+
def nodes_among?(*maybe_nodes)
|
237
|
+
maybe_nodes.any? { |node| nodes.include? node }
|
238
|
+
end
|
239
|
+
alias has_nodes_among? nodes_among?
|
240
|
+
alias has_node_among? nodes_among?
|
241
|
+
# FIXME: these helpers could be backported into Plexus.
|
242
|
+
|
243
|
+
# Returns true if i or (i,j) is a {Branch branch} of the tree.
|
244
|
+
#
|
245
|
+
# @overload branch?(a)
|
246
|
+
# @param [Branch] a
|
247
|
+
# @overload branch?(i, j)
|
248
|
+
# @param [node] i
|
249
|
+
# @param [node] j
|
250
|
+
# @return [Boolean]
|
251
|
+
#
|
252
|
+
def branch?(*args)
|
253
|
+
branches.include?(edge_convert(*args))
|
254
|
+
end
|
255
|
+
alias has_branch? branch?
|
256
|
+
|
257
|
+
# Returns true if the specified branches are a subset of or match exactly the
|
258
|
+
# branches set of the tree (no ordering criterion).
|
259
|
+
#
|
260
|
+
# Labels are not part of the matching constraint: only connected nodes
|
261
|
+
# matter in defining branch equality.
|
262
|
+
#
|
263
|
+
# @param [*Branch, *nodes] *maybe_branches a list of branches, either as Branch
|
264
|
+
# objects or as nodes pairs
|
265
|
+
# @return [Boolean]
|
266
|
+
#
|
267
|
+
def branches?(*maybe_branches)
|
268
|
+
list = maybe_branches.create_branches_list(_branch_type)
|
269
|
+
all = true
|
270
|
+
|
271
|
+
# Branch objects are really Edge objects within Plexus, therefore
|
272
|
+
# cannot rely on #eql? to compare those structures and must drop
|
273
|
+
# down to the attributes.
|
274
|
+
list.each do |e| # Jumoku::Branch structs
|
275
|
+
all = branches.any? do |b| # Plexus::Edge structs
|
276
|
+
(b[:source] == e[:source]) and (b[:target] == e[:target])
|
277
|
+
end
|
278
|
+
end
|
279
|
+
all
|
280
|
+
end
|
281
|
+
alias has_branches? branches?
|
282
|
+
|
283
|
+
# Returns true if actual branches are included in the specified set of branches
|
284
|
+
# (no ordering criterion).
|
285
|
+
#
|
286
|
+
# Labels are not part of the matching constraint: only connected nodes
|
287
|
+
# matter in defining equality.
|
288
|
+
#
|
289
|
+
# @param [*Branch, *nodes] *maybe_branches a list of branches, either as Branch
|
290
|
+
# objects or as nodes pairs
|
291
|
+
# @return [Boolean]
|
292
|
+
#
|
293
|
+
def branches_among?(*maybe_branches)
|
294
|
+
list = maybe_branches.create_branches_list(_branch_type)
|
295
|
+
all = true
|
296
|
+
|
297
|
+
# Branch objects are really Edge objects within Plexus, therefore
|
298
|
+
# cannot rely on #eql? to compare those structures and must drop
|
299
|
+
# down to the attributes.
|
300
|
+
branches.each do |e| # Plexus::Edge structs
|
301
|
+
all = list.any? do |b| # Jumoku::Branch structs
|
302
|
+
(b[:source] == e[:source]) and (b[:target] == e[:target])
|
303
|
+
end
|
304
|
+
end
|
305
|
+
all
|
306
|
+
end
|
307
|
+
alias has_branches_among? branches_among?
|
308
|
+
|
309
|
+
# Number of nodes.
|
310
|
+
#
|
311
|
+
# @return [Integer]
|
312
|
+
#
|
313
|
+
def num_nodes
|
314
|
+
nodes.size
|
315
|
+
end
|
316
|
+
alias number_of_nodes num_nodes
|
317
|
+
|
318
|
+
# Number of branches.
|
319
|
+
#
|
320
|
+
# @return [Integer]
|
321
|
+
#
|
322
|
+
def num_branches
|
323
|
+
branches.size
|
324
|
+
end
|
325
|
+
alias number_of_branches num_branches
|
326
|
+
end
|
327
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Jumoku
|
2
|
+
# A {RawDirectedTree} relax the undirected constraint of trees as defined in
|
3
|
+
# Graph Theory. They remain connected and acyclic.
|
4
|
+
#
|
5
|
+
# It thus uses Plexus::DirectedGraphBuilder as its backend.
|
6
|
+
#
|
7
|
+
# It offers limited functionalities, therefore the main tree structure you'll likely to
|
8
|
+
# use is its extended version, {Arborescence}.
|
9
|
+
module RawDirectedTreeBuilder
|
10
|
+
include Plexus::DirectedGraphBuilder
|
11
|
+
include Shared
|
12
|
+
|
13
|
+
# This method is called by the specialized implementations upon tree creation.
|
14
|
+
#
|
15
|
+
# Initialization parameters can include:
|
16
|
+
#
|
17
|
+
# * an array of branches to add
|
18
|
+
# * one or several trees to copy (will be merged if multiple)
|
19
|
+
#
|
20
|
+
# @param *params [Hash] the initialization parameters
|
21
|
+
# @return enhanced Plexus::DirectedGraph
|
22
|
+
#
|
23
|
+
def initialize(*params)
|
24
|
+
super(*params) # Delegates to Plexus.
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def _branch_type
|
30
|
+
DirectedBranch
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Jumoku
|
2
|
+
# A {RawUndirectedTree} sticks to the standard definition of trees in Graph Theory:
|
3
|
+
# * undirected,
|
4
|
+
# * connected,
|
5
|
+
# * acyclic.
|
6
|
+
# It thus uses Plexus::UndirectedGraphBuilder as its backend, which ensure the first
|
7
|
+
# constraint.
|
8
|
+
#
|
9
|
+
# {RawUndirectedTreeBuilder} ensures the two remaining constraints are satisfied.
|
10
|
+
# It offers limited functionalities, therefore the main tree structure you'll likely to
|
11
|
+
# use is its extended version, {TreeBuilder}.
|
12
|
+
#
|
13
|
+
module RawUndirectedTreeBuilder
|
14
|
+
include Plexus::UndirectedGraphBuilder
|
15
|
+
include Shared
|
16
|
+
|
17
|
+
# This method is called by the specialized implementations upon tree creation.
|
18
|
+
#
|
19
|
+
# Initialization parameters can include:
|
20
|
+
#
|
21
|
+
# * an array of branches to add
|
22
|
+
# * one or several trees to copy (will be merged if multiple)
|
23
|
+
#
|
24
|
+
# @param *params [Hash] the initialization parameters
|
25
|
+
# @return enhanced Plexus::UndirectedGraph
|
26
|
+
#
|
27
|
+
def initialize(*params)
|
28
|
+
super(*params) # Delegates to Plexus.
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def _branch_type
|
34
|
+
UndirectedBranch
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|