jumoku 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,21 +2,22 @@
2
2
 
3
3
  ## Synopsis
4
4
 
5
- Jumoku provides you with tree structures and related tools to perform manipulation and computation the easy way. Trees are a subset of graphs, which are used in a whole slew of computer science and mathematical problems: network modelization, datasets storage, scientific computation, load balancing, games and AI designs, … Trees are frequently used to mimic hierarchal structures such as filesystems, or modelize decisionnal patterns, for instance.
5
+ Jumoku provides you with tree structures and related tools to perform manipulation and computation, the easy way. Trees are frequently used to mimic hierarchal structures such as filesystems, or modelize decisionnal patterns, for instance.
6
6
 
7
7
  Jumoku is built upon [Plexus](http://github.com/chikamichi/plexus "Plexus on Github"), a ruby-powered Graph Theory library, and aims at being a fully-fledged, pithy solution for tree-like structures managment. See below for additionnal information about graphs, trees, arborescences, why they're different and how to make good use of them.
8
8
 
9
9
  ## A few words about *trees*
10
10
 
11
- A Tree is a graph subject to three basic constraints: nodes are all connected, they must not form any loop, and the branches binding nodes have no preferential direction. A tree is not compelled to have a root node and leaves as you may think *prima facie*. Trees with such features are called arborescences and Jumoku has support for them, too.
11
+ A Tree is a graph subject to three basic constraints: nodes are all connected, they must not form any loop, and the branches binding nodes have no preferential direction. Trees are an important subset of graphs used in a whole slew of computer science and mathematical problems: network modelization, datasets storage, scientific computation, load balancing, games, AI designs, etc. They however are not limited to directed patterns: a tree is not compelled to have a "root" node, nor to have "leaves" as you may think *prima facie*. Trees with such features are called arborescences and Jumoku has support for them, too.
12
12
 
13
13
  Jumoku (*currently under early development stage*) provides you with the following structures:
14
14
 
15
- * RawTree: a tree-graph with only basic features
16
- * **Tree**: a tree-graph with extended features built upon RawTree. That's what you'd want to use as a basic tree structure
15
+ * RawUndirectedTree: a pure tree-graph with only basic features
16
+ * RawDirectedTree: a tree that's an arborescence, that is, a tree with a flow from its root to its leaf nodes
17
+ * **Tree**: a tree-graph with extended features built upon RawUndirectedTree. That's what you'd want to use as a fully-fledged tree structure
18
+ * **Arborescence**: an arbo-graph with extended features built upon RawDirectedTree. That's what you'd want to use as a fully-fledged arborescence structure
17
19
  * AVLTree (*not yet*)
18
20
  * RedBlackTree (*not yet*)
19
- * **Arborescence** (*not yet*): the structure everybody thinks of when asked about "trees", with a root, internal nodes and leaves
20
21
 
21
22
  You can also extend those structures with hybrid behaviors (not Graph Theory compliant but may be useful):
22
23
 
@@ -24,6 +25,10 @@ You can also extend those structures with hybrid behaviors (not Graph Theory com
24
25
  * Loopy (*not yet*): relax the *acyclic* constraint
25
26
  * Atomic (*not yet*): relax the *connected* constraint
26
27
 
28
+ There are also strategies one may enable:
29
+
30
+ * a simple edge labeling scheme (increasing integer indexes), providing edges and nodes sorting facilities
31
+
27
32
  ## Basic usage
28
33
 
29
34
  To create an instance of a tree, you may use either inheritance or mixins.
@@ -42,7 +47,7 @@ tree = Jumoku::Tree.new
42
47
 
43
48
  ``` ruby
44
49
  class MyTree
45
- include Jumoku::RawTreeBuilder
50
+ include Jumoku::TreeBuilder
46
51
  end
47
52
  tree = MyTree.new
48
53
  ```
@@ -51,7 +56,7 @@ The RawTree class is actually implemented this way.
51
56
 
52
57
  ### What you get
53
58
 
54
- `tree` is now a Tree object, shipping with some default options:
59
+ Following the previous code example, `tree` is now a Tree object, shipping with some default options:
55
60
 
56
61
  * it is a valid tree *per se* (an undirected, acyclic, connected graph)
57
62
  * Jumoku API's methods will ensure it remains so as you manipulate it
@@ -8,16 +8,173 @@ module Jumoku
8
8
  include RawDirectedTreeBuilder
9
9
  include Extended
10
10
 
11
+ def add_branch!(u, v = nil, l = nil)
12
+ return super(u,v,l) if @_options[:free_flow]
13
+ return add_branch!(u.source, u.target, l) if u.is_a? _branch_type
14
+ return super(u,v,l) if nodes.size < 2
15
+
16
+ nodes.include?(u) ? super(u,v,l) : super(v,u,l)
17
+ end
18
+
19
+ # Find the root node.
20
+ #
21
+ # @return [Node]
22
+ #
23
+ def root
24
+ return nodes.first if nodes.size == 1
25
+ nodes.find { |node| root?(node) }
26
+ end
27
+
28
+ # Return the list of arcs branched out from the root node.
29
+ #
30
+ # @return [Array<Plexus::Arc>]
31
+ #
32
+ def root_edges
33
+ adjacent(root, :type => :edges)
34
+ end
35
+
36
+ # Check whether a node is the root.
37
+ #
38
+ # @return [true, false]
39
+ #
40
+ def root?(node)
41
+ in_degree(node) == 0 && out_degree(node) > 0
42
+ end
43
+
44
+ # Return the list of a node's children.
45
+ #
46
+ # @param [Node] node
47
+ # @return [Array<Node>]
48
+ #
49
+ def children(parent)
50
+ adjacent(parent)
51
+ end
52
+ alias children_of children
53
+
54
+ # Return the parent node of another.
55
+ #
56
+ # @param [Node] node
57
+ # @raise if the node has more than one parent (should never occur!)
58
+ # @return [Node, nil] nil if the node is root
59
+ #
60
+ def parent(node)
61
+ parent = adjacent(node, :direction => :in)
62
+ raise JumokuError, "Inconsistent directed tree (more than one parent for the node!)" if parent.size > 1
63
+ parent.empty? ? nil : parent.first
64
+ end
65
+ alias parent_of parent
66
+
67
+ # Check whether a node is a parent. If another node is provided as
68
+ # second parameter, check whether the former node is the parent of the
69
+ # latter node.
70
+ #
71
+ # @overload parent?(node)
72
+ # @param [Node] node
73
+ # @return [true, false]
74
+ # @overload parent?(node, maybe_child)
75
+ # @param [Node] node
76
+ # @param [Node] maybe_child
77
+ # @return [true, false]
78
+ #
79
+ def parent?(node, maybe_child = nil)
80
+ if maybe_child.nil?
81
+ !children(node).empty?
82
+ else
83
+ children(node).include? maybe_child
84
+ end
85
+ end
86
+
87
+ # Return the siblings for a node. Siblings are other node's parent children.
88
+ #
89
+ # @param [Node] node
90
+ # @return [Array<Node>] empty list if the node is root
91
+ #
92
+ def siblings(node)
93
+ return [] if root?(node)
94
+ siblings = children(parent(node))
95
+ siblings.delete(node)
96
+ siblings
97
+ end
98
+ alias siblings_of siblings
99
+
100
+ # Check whether two nodes are siblings.
101
+ #
102
+ # @param [Node] node1
103
+ # @param [Node] node2
104
+ # @return [true, false]
105
+ #
106
+ def siblings?(node1, node2)
107
+ siblings(node1).include?(node2)
108
+ end
109
+
110
+ # Return the list of a node's neighbours, that is, children of node's
111
+ # parent siblings (cousins).
112
+ #
113
+ # @param [Node] node
114
+ # @param [Hash] options
115
+ # @option options [true, false] :siblings whether to include the node's
116
+ # siblings in the list
117
+ # @return [Array<Node>]
118
+ #
119
+ def neighbours(node, options = {:siblings => false})
120
+ # special case when the node is the root
121
+ return [] if root?(node)
122
+
123
+ # special case when the node is a root's child
124
+ if root?(parent(node))
125
+ nghb = children(parent(node))
126
+ nghb.delete_if { |child| child == node } unless options[:siblings]
127
+ return nghb.map { |child| [child] }
128
+ end
129
+
130
+ # general case
131
+ nghb = siblings(parent(node)).map do |sibling|
132
+ children(sibling)
133
+ end
134
+ nghb << siblings(node) if options[:siblings]
135
+ nghb
136
+ end
137
+ alias cousins neighbours
138
+
139
+ # Check whether two nodes are neighbours. To include the node's siblings
140
+ # in the matching candidates, pass the :siblings option to true.
141
+ #
142
+ # @param [Node] node1
143
+ # @param [Node] node2
144
+ # @param [Hash] options
145
+ # @option options [true, false] :siblings whether to include the node's
146
+ # siblings in the list
147
+ # @return [true, false]
148
+ #
149
+ def neighbours?(node1, node2, options = {:siblings => false})
150
+ neighbours(node1, options).any? { |set| set.include? node2 }
151
+ end
152
+ alias cousins? neighbours?
153
+
154
+ # Return the list of leaf nodes.
155
+ #
156
+ # @return [Array<Node>]
157
+ #
11
158
  def leaves
12
159
  terminal_nodes.delete_if do |node|
13
160
  out_degree(node) > 0
14
161
  end
15
162
  end
16
163
 
164
+ # Check whether a node is a leaf.
165
+ #
166
+ # @param [Node]
167
+ # @return [true, false]
168
+ #
17
169
  def leaf?(node)
18
170
  terminal?(node) && out_degree(node) == 0
19
171
  end
20
172
 
173
+ # Check whether all nodes from a list are leaves.
174
+ #
175
+ # @param [#to_a] nodes
176
+ # @return [true, false]
177
+ #
21
178
  def leaves?(*nodes)
22
179
  nodes.to_a.flatten.all? { |node| leaf?(node) }
23
180
  end
@@ -21,11 +21,11 @@ module Jumoku
21
21
  # @return enhanced Plexus::DirectedGraph
22
22
  #
23
23
  def initialize(*params)
24
+ super(*params) # Delegates to Plexus.
24
25
  args = (params.pop if params.last.is_a? Hash) || {}
26
+ @_options = args
25
27
  strategies = _extract_strategies(args)
26
28
 
27
- super(*params) # Delegates to Plexus.
28
-
29
29
  class << self; self; end.module_eval do
30
30
  strategies.each { |strategy| include strategy }
31
31
  alias has_branch? has_arc?
@@ -25,7 +25,9 @@ module Jumoku
25
25
  # @return enhanced Plexus::UndirectedGraph
26
26
  #
27
27
  def initialize(*params)
28
+ super(*params) # Delegates to Plexus.
28
29
  args = (params.pop if params.last.is_a? Hash) || {}
30
+ @_options = args
29
31
  strategies = _extract_strategies(args)
30
32
 
31
33
  super(*params) # Delegates to Plexus.
@@ -3,6 +3,8 @@ module Jumoku
3
3
  # builders: {UndirectedTreeBuilder} and {DirectedTreeBuilder}.
4
4
  #
5
5
  module Shared
6
+ STRATEGIES = [:edge_labeling, :node_labeling]
7
+
6
8
  def self.included(base)
7
9
  base.class_eval do
8
10
  # Late aliasing as it references methods provided by Plexus modules.
@@ -10,6 +12,8 @@ module Jumoku
10
12
  end
11
13
  end
12
14
 
15
+ attr_accessor :_options
16
+
13
17
  # Adds the node to the tree.
14
18
  #
15
19
  # For convenience, you may pass a branch as the parameter,
@@ -208,6 +212,7 @@ module Jumoku
208
212
  end
209
213
 
210
214
  def _extract_strategies(options)
215
+ options = options.dup.select! { |k,v| STRATEGIES.include?(k) } || options.dup
211
216
  options.inject([]) do |strategies, (k,v)|
212
217
  begin
213
218
  strategies << Jumoku.const_get(k.to_s.constantize).const_get(v.to_s.constantize)
@@ -7,7 +7,7 @@ module Jumoku
7
7
  #
8
8
  # @return [Array]
9
9
  #
10
- def sort_edges(&block)
10
+ def sorted_edges(&block)
11
11
  return edges.sort_by { |edge| block.call(edge) } if block_given?
12
12
  edges
13
13
  end
@@ -31,9 +31,18 @@ module Jumoku
31
31
  #
32
32
  # @return [Array]
33
33
  #
34
- def sort_edges(&block)
34
+ def sorted_edges(&block)
35
35
  return super(&block) if block_given?
36
- edges.sort { |a,b| a.label._weight <=> b.label._weight }
36
+ _sort_edges(edges)
37
+ end
38
+
39
+ # Sort a set of edges.
40
+ #
41
+ # @param [Array]
42
+ # @return [Array]
43
+ #
44
+ def sort_edges(set)
45
+ _sort_edges(set)
37
46
  end
38
47
 
39
48
  # Only for directed trees.
@@ -70,6 +79,10 @@ module Jumoku
70
79
  self.next_simple_edge_label_number += 1
71
80
  super
72
81
  end
82
+
83
+ def _sort_edges(set)
84
+ set.sort { |a,b| a.label._weight <=> b.label._weight }
85
+ end
73
86
  end
74
87
  end
75
88
  end
@@ -1,6 +1,6 @@
1
1
  module Jumoku
2
2
  MAJOR = 0
3
3
  MINOR = 2
4
- PATCH = 3
4
+ PATCH = 4
5
5
  VERSION = [MAJOR, MINOR, PATCH].join('.')
6
6
  end
@@ -14,6 +14,147 @@ describe Arborescence do
14
14
 
15
15
  it_should_behave_like "a tree with extended features"
16
16
 
17
+ context "arcs flow" do
18
+ it "should by default be forced to match direction of the first arc added" do
19
+ tree.add_branch! 1, 2, 0
20
+ tree.add_branch! 2, 3, 1
21
+ tree.add_branch! 4, 3, 2 # will be reversed actually
22
+ tree.edges.sort_by { |edge| edge.label }.map { |edge| edge.target }.should == [2, 3, 4]
23
+ end
24
+
25
+ it "should be possible to relax this constraint" do
26
+ tree = Arborescence.new(:free_flow => true)
27
+ tree.add_branch! 1, 2, 0
28
+ tree.add_branch! 2, 3, 1
29
+ tree.add_branch! 4, 3, 2 # will *not* be reversed
30
+ tree.edges.sort_by { |edge| edge.label }.map { |edge| edge.target }.should == [2, 3, 3]
31
+ end
32
+ end
33
+
34
+ context "root helpers" do
35
+ before :each do
36
+ tree.add_branches! 1,2, 2,3, 1,4, 4,5, 4,6
37
+ end
38
+
39
+ describe "#root" do
40
+ it "should return the root node" do
41
+ tree.root.should == 1
42
+ end
43
+ end
44
+
45
+ describe "#root_edges" do
46
+ it "should return the edge(s) branched from the root" do
47
+ tree.root_edges.size.should == 2
48
+ end
49
+ end
50
+
51
+ describe "#root?" do
52
+ it "should report whether a node is the root" do
53
+ tree.root?(1).should be_true
54
+ (3..6).all? { |i| !tree.root?(i) }.should be_true
55
+ end
56
+ end
57
+ end
58
+
59
+ context "children helpers" do
60
+ before :each do
61
+ tree.add_branches! 1,2, 2,3, 1,4, 4,5, 4,6
62
+ end
63
+
64
+ describe "#children" do
65
+ it "should return the list of children nodes for a node" do
66
+ tree.children(1).sort.should == [2,4]
67
+ tree.children_of(4).sort.should == [5,6]
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "parent helpers" do
73
+ before :each do
74
+ tree.add_branches! 1,2, 2,3, 1,4, 4,5, 4,6
75
+ end
76
+
77
+ describe "#parent" do
78
+ it "should return the parent of a node" do
79
+ tree.parent(1).should be_nil
80
+ tree.parent(2).should == 1
81
+ tree.parent_of(6).should == 4
82
+ end
83
+ end
84
+
85
+ describe "parent?" do
86
+ it "should check whether a node is the parent of another (which may be specified)" do
87
+ tree.parent?(1).should be_true
88
+ tree.parent?(6).should be_false
89
+ tree.parent?(1,2).should be_true
90
+ end
91
+ end
92
+ end
93
+
94
+ context "siblings helpers" do
95
+ before :each do
96
+ tree.add_branches! 1,2, 2,3, 1,4, 4,5, 4,6, 1,7
97
+ end
98
+
99
+ describe "#siblings" do
100
+ it "should return the siblings for a node" do
101
+ tree.siblings(1).should be_empty
102
+ tree.siblings_of(2).sort.should == [4,7]
103
+ end
104
+ end
105
+
106
+ describe "#siblings?" do
107
+ it "should check whether two nodes are siblings" do
108
+ tree.siblings?(1,2).should be_false
109
+ tree.siblings?(2,3).should be_false
110
+ tree.siblings?(2,4).should be_true
111
+ end
112
+ end
113
+
114
+ describe "#neighbours" do
115
+ before :each do
116
+ tree.add_node! 2, 8
117
+ tree.add_node! 7, 9
118
+ end
119
+
120
+ it "should return the grand-siblings for a node" do
121
+ tree.neighbours(1).should be_empty
122
+ tree.neighbours(2).size.should == 2
123
+ [4,7].each { |node| tree.neighbours(2).should include([node]) }
124
+ neighbours = tree.neighbours(8)
125
+ neighbours.size.should == 2
126
+ neighbours.should include [5,6]
127
+ neighbours.should include [9]
128
+ end
129
+
130
+ it "should be able to include siblings as well" do
131
+ neighbours = tree.neighbours(8, :siblings => true)
132
+ neighbours.size.should == 3
133
+ neighbours.should include [3]
134
+ neighbours.should include [5,6]
135
+ neighbours.should include [9]
136
+ end
137
+ end
138
+
139
+ describe "#neighbours?" do
140
+ before :each do
141
+ tree.add_node! 2, 8
142
+ tree.add_node! 7, 9
143
+ end
144
+
145
+ it "should check whether two nodes are neighbours" do
146
+ tree.neighbours?(1,2).should be_false
147
+ tree.neighbours?(2,8).should be_false
148
+ tree.neighbours?(3,8).should be_false
149
+ tree.neighbours?(5,8).should be_true
150
+ end
151
+
152
+ it "should be possible to include siblings in the match" do
153
+ tree.neighbours?(3,8, :siblings => true).should be_true
154
+ end
155
+ end
156
+ end
157
+
17
158
  context "leaf nodes inspection" do
18
159
  before :each do
19
160
  tree.add_branches! 1,2, 2,3, 1,4, 1,5
@@ -7,6 +7,7 @@ describe RawDirectedTree do
7
7
  let(:branch_type) { subject.send :_branch_type }
8
8
 
9
9
  it_should_behave_like "a legacy tree"
10
+
10
11
  it "should not allow to add a reverse arc" do
11
12
  tree.add_branch! 1, 2
12
13
  lambda { tree.add_branch! 2, 1 }.should raise_error ForbiddenCycle
@@ -25,21 +25,28 @@ describe EdgeLabeling::Simple do
25
25
  tree.edges.map { |e| e.label._weight }.sort.should == [1,2,3,5]
26
26
  end
27
27
 
28
- describe "#sort_edges" do
28
+ describe "#sorted_edges" do
29
29
  it "should by default return the list of edges in (increasing) order" do
30
30
  tree.add_branches! 1,2, 1,3, 2,4, 1,5, 3,6, 1,7
31
- tree.sort_edges.map { |e| e.label._weight }.should == (0..5).to_a
31
+ tree.sorted_edges.map { |e| e.label._weight }.should == (0..5).to_a
32
32
  end
33
33
 
34
34
  it "should accept a block to sort edges" do
35
35
  # for the sake of this specs, one'd actually #reverse the default sorting ;)
36
36
  tree.add_branches! 1,2, 1,3, 2,4, 1,5, 3,6, 1,7
37
- tree.sort_edges do |edge|
37
+ tree.sorted_edges do |edge|
38
38
  -edge.label._weight
39
39
  end.map { |e| e.label._weight }.should == (0..5).to_a.reverse
40
40
  end
41
41
  end
42
42
 
43
+ describe "#sort_edges" do
44
+ it "should sort a set of edges using the simple scheme" do
45
+ tree.add_branches! 1,2, 1,3, 2,4, 1,5, 3,6, 1,7
46
+ tree.sort_edges(tree.adjacent(1, :type => :edges)).map { |e| e.target }.should == [2,3,5,7]
47
+ end
48
+ end
49
+
43
50
  it "should thus allow for local edge and children ordering (directed trees only)" do
44
51
  arbo.add_branches! 1,2, 1,3, 2,4, 1,5, 3,6, 1,7
45
52
  arbo.sorted_arcs_from(1).map { |e| e.target }.should == [2,3,5,7]
@@ -7,8 +7,10 @@ describe Tree do
7
7
  let(:branch_type) { subject.send :_branch_type }
8
8
 
9
9
  it_should_behave_like "a legacy tree"
10
+
10
11
  it "should be a undirected graph" do
11
12
  tree.class.ancestors.should include RawUndirectedTreeBuilder
12
13
  end
14
+
13
15
  it_should_behave_like "a tree with extended features"
14
16
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: jumoku
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.3
5
+ version: 0.2.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Jean-Denis Vauguet <jd@vauguet.fr>
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-07-11 00:00:00 +02:00
13
+ date: 2011-07-12 00:00:00 +02:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency