nobrainer-tree 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/Gemfile +12 -0
- data/LICENSE +20 -0
- data/README.md +220 -0
- data/Rakefile +25 -0
- data/lib/nobrainer/tree/ordering.rb +238 -0
- data/lib/nobrainer/tree/traversal.rb +122 -0
- data/lib/nobrainer/tree.rb +442 -0
- data/spec/nobrainer/tree/ordering_spec.rb +344 -0
- data/spec/nobrainer/tree/traversal_spec.rb +178 -0
- data/spec/nobrainer/tree_spec.rb +460 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/macros/tree_macros.rb +54 -0
- data/spec/support/models/node.rb +34 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0a7bca7cff2679bd8fbd69bc4ccdbeba8e59f50d
|
4
|
+
data.tar.gz: 75eae734d058953c9e63840e07176b1e325defde
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3149e67069144dfd3bf675501c7d8ce41f7eb33e152396e07ff14b8cdce77d722f258a3212a6106b536b9ddbf3d34177062916c985b2ae4c0c78d3a030dd8799
|
7
|
+
data.tar.gz: 3eb4a08d9067691283368b9cc04e04f1a64c947366ad7902ae5ffb11e6a4a721ee7a5b40d3a7051e186c2f0e46902eea6b345b96ff34b8c58563a25ae2b54b03
|
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'guard-rspec', '>= 0.6.0'
|
6
|
+
gem 'ruby_gntp', '>= 0.3.4'
|
7
|
+
gem 'rb-fsevent' if RUBY_PLATFORM =~ /darwin/
|
8
|
+
gem 'nobrainer', :github => 'nviennot/nobrainer'
|
9
|
+
|
10
|
+
platforms :rbx do
|
11
|
+
gem 'rubysl-rake', '~> 2.0'
|
12
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010-2013 Benedikt Deicke
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# nobrainer-tree ![Build Status](https://travis-ci.org/secondimpression/nobrainer-tree.svg?branch=nobrainer) [![Dependency Status](https://gemnasium.com/secondimpression/nobrainer-tree.png)](https://gemnasium.com/secondimpression/nobrainer-tree)
|
2
|
+
|
3
|
+
A tree structure for NoBrainer documents using the materialized path pattern
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
* nobrainer (~> 0.20.0)
|
8
|
+
|
9
|
+
|
10
|
+
## Install
|
11
|
+
|
12
|
+
To install nobrainer-tree, simply add it to your Gemfile:
|
13
|
+
|
14
|
+
gem 'nobrainer-tree', :require => 'nobrainer/tree'
|
15
|
+
|
16
|
+
In order to get the latest development version of nobrainer-tree:
|
17
|
+
|
18
|
+
gem 'nobrainer-tree', :git => 'git://github.com/secondimpression/nobrainer-tree'
|
19
|
+
|
20
|
+
You might want to add `:require => nil` option and explicitly `require 'nobrainer/tree'` where needed and finally run
|
21
|
+
|
22
|
+
bundle install
|
23
|
+
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Read the API documentation at https://github.com/secondimpression/nobrainer-tree and take a look at the `NoBrainer::Tree` module
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class Node
|
31
|
+
include NoBrainer::Document
|
32
|
+
include NoBrainer::Tree
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
|
37
|
+
### Utility methods
|
38
|
+
|
39
|
+
There are several utility methods that help getting to other related documents in the tree:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
Node.root
|
43
|
+
Node.roots
|
44
|
+
Node.leaves
|
45
|
+
|
46
|
+
node.root
|
47
|
+
node.parent
|
48
|
+
node.children
|
49
|
+
node.ancestors
|
50
|
+
node.ancestors_and_self
|
51
|
+
node.descendants
|
52
|
+
node.descendants_and_self
|
53
|
+
node.siblings
|
54
|
+
node.siblings_and_self
|
55
|
+
node.leaves
|
56
|
+
```
|
57
|
+
|
58
|
+
In addition it's possible to check certain aspects of the document's position in the tree:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
node.root?
|
62
|
+
node.leaf?
|
63
|
+
node.depth
|
64
|
+
node.ancestor_of?(other)
|
65
|
+
node.descendant_of?(other)
|
66
|
+
node.sibling_of?(other)
|
67
|
+
```
|
68
|
+
|
69
|
+
See `NoBrainer::Tree` for more information on these methods.
|
70
|
+
|
71
|
+
|
72
|
+
### Ordering
|
73
|
+
|
74
|
+
`NoBrainer::Tree` doesn't order children by default. To enable ordering of tree nodes include the `NoBrainer::Tree::Ordering` module. This will add a `position` field to your document and provide additional utility methods:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
node.lower_siblings
|
78
|
+
node.higher_siblings
|
79
|
+
node.first_sibling_in_list
|
80
|
+
node.last_sibling_in_list
|
81
|
+
|
82
|
+
node.move_up
|
83
|
+
node.move_down
|
84
|
+
node.move_to_top
|
85
|
+
node.move_to_bottom
|
86
|
+
node.move_above(other)
|
87
|
+
node.move_below(other)
|
88
|
+
|
89
|
+
node.at_top?
|
90
|
+
node.at_bottom?
|
91
|
+
```
|
92
|
+
|
93
|
+
Example:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class Node
|
97
|
+
include NoBrainer::Document
|
98
|
+
include NoBrainer::Tree
|
99
|
+
include NoBrainer::Tree::Ordering
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
See `NoBrainer::Tree::Ordering` for more information on these methods.
|
104
|
+
|
105
|
+
|
106
|
+
### Traversal
|
107
|
+
|
108
|
+
It's possible to traverse the tree using different traversal methods using the `NoBrainer::Tree::Traversal` module.
|
109
|
+
|
110
|
+
Example:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class Node
|
114
|
+
include NoBrainer::Document
|
115
|
+
include NoBrainer::Tree
|
116
|
+
include NoBrainer::Tree::Traversal
|
117
|
+
end
|
118
|
+
|
119
|
+
node.traverse(:breadth_first) do |n|
|
120
|
+
# Do something with Node n
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
|
125
|
+
### Destroying
|
126
|
+
|
127
|
+
`NoBrainer::Tree` does not handle destroying of nodes by default. However it provides several strategies that help you to deal with children of deleted documents. You can simply add them as `before_destroy` callbacks.
|
128
|
+
|
129
|
+
Available strategies are:
|
130
|
+
|
131
|
+
* `:nullify_children` -- Sets the children's parent_id to null
|
132
|
+
* `:move_children_to_parent` -- Moves the children to the current document's parent
|
133
|
+
* `:destroy_children` -- Destroys all children by calling their `#destroy` method (invokes callbacks)
|
134
|
+
* `:delete_descendants` -- Deletes all descendants using a database query (doesn't invoke callbacks)
|
135
|
+
|
136
|
+
Example:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class Node
|
140
|
+
include NoBrainer::Document
|
141
|
+
include NoBrainer::Tree
|
142
|
+
|
143
|
+
before_destroy :nullify_children
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
|
148
|
+
### Callbacks
|
149
|
+
|
150
|
+
There are two callbacks that are called before and after the rearranging process. This enables you to do additional computations after the documents position in the tree is updated. See `NoBrainer::Tree` for details.
|
151
|
+
|
152
|
+
Example:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class Page
|
156
|
+
include NoBrainer::Document
|
157
|
+
include NoBrainer::Tree
|
158
|
+
|
159
|
+
after_rearrange :rebuild_path
|
160
|
+
|
161
|
+
field :slug
|
162
|
+
field :path
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def rebuild_path
|
167
|
+
self.path = self.ancestors_and_self.collect(&:slug).join('/')
|
168
|
+
end
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
|
173
|
+
### Validations
|
174
|
+
|
175
|
+
`NoBrainer::Tree` currently does not validate the document's children or parent associations by default. To explicitly enable validation for children and parent documents it's required to add a `validates_associated` validation.
|
176
|
+
|
177
|
+
Example:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class Node
|
181
|
+
include NoBrainer::Document
|
182
|
+
include NoBrainer::Tree
|
183
|
+
|
184
|
+
validates_associated :parent, :children
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
|
189
|
+
## Build Status
|
190
|
+
|
191
|
+
nobrainer-tree is on [Travis CI](http://travis-ci.org/secondimpression/nobrainer-tree) running the specs on Ruby Head, Ruby 1.9.3, Ruby 2.0 and Ruby 2.1
|
192
|
+
|
193
|
+
|
194
|
+
## Known issues
|
195
|
+
|
196
|
+
See [https://github.com/secondimpression/nobrainer-tree/issues](https://github.com/secondimpression/nobrainer-tree/issues)
|
197
|
+
|
198
|
+
|
199
|
+
## Repository
|
200
|
+
|
201
|
+
See [https://github.com/secondimpression/nobrainer-tree](https://github.com/secondimpression/nobrainer-tree) and feel free to fork it!
|
202
|
+
|
203
|
+
|
204
|
+
## Contributors
|
205
|
+
|
206
|
+
See a list of all contributors at [https://github.com/benedikt/mongoid-tree/contributors](https://github.com/benedikt/mongoid-tree/contributors) and [https://github.com/secondimpression/nobrainer-tree/contributors](https://github.com/secondimpression/nobrainer-tree/contributors). Thanks a lot everyone!
|
207
|
+
|
208
|
+
|
209
|
+
## Support
|
210
|
+
|
211
|
+
If you like nobrainer-tree and want to support the development, the original author would appreciate a small donation:
|
212
|
+
|
213
|
+
[![Pledgie](http://www.pledgie.com/campaigns/12137.png?skin_name=chrome)](http://www.pledgie.com/campaigns/12137)
|
214
|
+
|
215
|
+
[![Flattr](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=benediktdeicke&url=https://github.com/benedikt/mongoid-tree&title=mongoid-tree&language=&tags=github&category=software)
|
216
|
+
|
217
|
+
|
218
|
+
## Copyright
|
219
|
+
|
220
|
+
Copyright (c) 2010-2013 Benedikt Deicke. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'yard'
|
3
|
+
|
4
|
+
spec = Gem::Specification.load("nobrainer-tree.gemspec")
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
YARD::Rake::YardocTask.new(:doc)
|
11
|
+
|
12
|
+
desc "Build the .gem file"
|
13
|
+
task :build do
|
14
|
+
system "gem build #{spec.name}.gemspec"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Push the .gem file to rubygems.org"
|
18
|
+
task :release => :build do
|
19
|
+
system "gem push #{spec.name}-#{spec.version}.gem"
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Open an irb session"
|
23
|
+
task :console do
|
24
|
+
sh "irb -rubygems -I lib -r ./spec/spec_helper.rb"
|
25
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
module NoBrainer
|
2
|
+
module Tree
|
3
|
+
##
|
4
|
+
# = NoBrainer::Tree::Ordering
|
5
|
+
#
|
6
|
+
# NoBrainer::Tree doesn't order the tree by default. To enable ordering of children
|
7
|
+
# include both NoBrainer::Tree and NoBrainer::Tree::Ordering into your document.
|
8
|
+
#
|
9
|
+
# == Utility methods
|
10
|
+
#
|
11
|
+
# This module adds methods to get related siblings depending on their position:
|
12
|
+
#
|
13
|
+
# node.lower_siblings
|
14
|
+
# node.higher_siblings
|
15
|
+
# node.first_sibling_in_list
|
16
|
+
# node.last_sibling_in_list
|
17
|
+
#
|
18
|
+
# There are several methods to move nodes around in the list:
|
19
|
+
#
|
20
|
+
# node.move_up
|
21
|
+
# node.move_down
|
22
|
+
# node.move_to_top
|
23
|
+
# node.move_to_bottom
|
24
|
+
# node.move_above(other)
|
25
|
+
# node.move_below(other)
|
26
|
+
#
|
27
|
+
# Additionally there are some methods to check aspects of the document
|
28
|
+
# in the list of children:
|
29
|
+
#
|
30
|
+
# node.at_top?
|
31
|
+
# node.at_bottom?
|
32
|
+
module Ordering
|
33
|
+
extend ActiveSupport::Concern
|
34
|
+
|
35
|
+
included do
|
36
|
+
field :position, :type => Integer
|
37
|
+
|
38
|
+
default_scope -> { order_by(:position => :asc) }
|
39
|
+
|
40
|
+
before_save :assign_default_position, :if => :assign_default_position?
|
41
|
+
before_save :reposition_former_siblings, :if => :sibling_reposition_required?
|
42
|
+
after_destroy :move_lower_siblings_up
|
43
|
+
end
|
44
|
+
|
45
|
+
def inc(increments)
|
46
|
+
update(increments.symbolize_keys.map{ |field, value| { field => (self[field] || 0) + value } }.first)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Returns a chainable criteria for this document's ancestors
|
52
|
+
#
|
53
|
+
# @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's ancestors
|
54
|
+
def ancestors
|
55
|
+
base_class.unscoped.without_index.where(:id.in => self.parent_ids).order_by(:depth => :asc)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Returns siblings below the current document.
|
60
|
+
# Siblings with a position greater than this document's position.
|
61
|
+
#
|
62
|
+
# @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's lower siblings
|
63
|
+
def lower_siblings
|
64
|
+
self.siblings.where(:position.gt => self.position)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Returns siblings above the current document.
|
69
|
+
# Siblings with a position lower than this document's position.
|
70
|
+
#
|
71
|
+
# @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's higher siblings
|
72
|
+
def higher_siblings
|
73
|
+
self.siblings.where(:position.lt => self.position)
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Returns siblings between the current document and the other document
|
78
|
+
# Siblings with a position between this document's position and the other document's position.
|
79
|
+
#
|
80
|
+
# @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the documents between this and the other document
|
81
|
+
def siblings_between(other)
|
82
|
+
range = [self.position, other.position].sort
|
83
|
+
self.siblings.where(:position.gt => range.first, :position.lt => range.last)
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Returns the lowest sibling (could be self)
|
88
|
+
#
|
89
|
+
# @return [NoBrainer::Document] The lowest sibling
|
90
|
+
def last_sibling_in_list
|
91
|
+
siblings_and_self.last
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Returns the highest sibling (could be self)
|
96
|
+
#
|
97
|
+
# @return [NoBrainer::Document] The highest sibling
|
98
|
+
def first_sibling_in_list
|
99
|
+
siblings_and_self.first
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Is this the highest sibling?
|
104
|
+
#
|
105
|
+
# @return [Boolean] Whether the document is the highest sibling
|
106
|
+
def at_top?
|
107
|
+
higher_siblings.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Is this the lowest sibling?
|
112
|
+
#
|
113
|
+
# @return [Boolean] Whether the document is the lowest sibling
|
114
|
+
def at_bottom?
|
115
|
+
lower_siblings.empty?
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Move this node above all its siblings
|
120
|
+
#
|
121
|
+
# @return [undefined]
|
122
|
+
def move_to_top
|
123
|
+
return true if at_top?
|
124
|
+
move_above(first_sibling_in_list)
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Move this node below all its siblings
|
129
|
+
#
|
130
|
+
# @return [undefined]
|
131
|
+
def move_to_bottom
|
132
|
+
return true if at_bottom?
|
133
|
+
move_below(last_sibling_in_list)
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Move this node one position up
|
138
|
+
#
|
139
|
+
# @return [undefined]
|
140
|
+
def move_up
|
141
|
+
switch_with_sibling_at_offset(-1) unless at_top?
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Move this node one position down
|
146
|
+
#
|
147
|
+
# @return [undefined]
|
148
|
+
def move_down
|
149
|
+
switch_with_sibling_at_offset(1) unless at_bottom?
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Move this node above the specified node
|
154
|
+
#
|
155
|
+
# This method changes the node's parent if nescessary.
|
156
|
+
#
|
157
|
+
# @param [NoBrainer::Tree] other document to move this document above
|
158
|
+
#
|
159
|
+
# @return [undefined]
|
160
|
+
def move_above(other)
|
161
|
+
ensure_to_be_sibling_of(other)
|
162
|
+
|
163
|
+
if position > other.position
|
164
|
+
new_position = other.position
|
165
|
+
self.siblings_between(other).each{ |object| object.inc(:position => 1) }
|
166
|
+
other.inc(:position => 1)
|
167
|
+
else
|
168
|
+
new_position = other.position - 1
|
169
|
+
self.siblings_between(other).each{ |object| object.inc(:position => -1) }
|
170
|
+
end
|
171
|
+
|
172
|
+
self.position = new_position
|
173
|
+
save
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Move this node below the specified node
|
178
|
+
#
|
179
|
+
# This method changes the node's parent if nescessary.
|
180
|
+
#
|
181
|
+
# @param [NoBrainer::Tree] other document to move this document below
|
182
|
+
#
|
183
|
+
# @return [undefined]
|
184
|
+
def move_below(other)
|
185
|
+
ensure_to_be_sibling_of(other)
|
186
|
+
|
187
|
+
if position > other.position
|
188
|
+
new_position = other.position + 1
|
189
|
+
self.siblings_between(other).each{ |object| object.inc(:position => 1) }
|
190
|
+
else
|
191
|
+
new_position = other.position
|
192
|
+
self.siblings_between(other).each{ |object| object.inc(:position => -1) }
|
193
|
+
other.inc(:position => -1)
|
194
|
+
end
|
195
|
+
|
196
|
+
self.position = new_position
|
197
|
+
save
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def switch_with_sibling_at_offset(offset)
|
203
|
+
siblings.where(:position => self.position + offset).first.inc(:position => -offset)
|
204
|
+
inc(:position => offset)
|
205
|
+
end
|
206
|
+
|
207
|
+
def ensure_to_be_sibling_of(other)
|
208
|
+
return if sibling_of?(other)
|
209
|
+
self.parent_id = other.parent_id
|
210
|
+
save
|
211
|
+
end
|
212
|
+
|
213
|
+
def move_lower_siblings_up
|
214
|
+
lower_siblings.each{ |object| object.inc(:position => -1) }
|
215
|
+
end
|
216
|
+
|
217
|
+
def reposition_former_siblings
|
218
|
+
former_siblings = base_class.where(:parent_id => self.parent_id_was).
|
219
|
+
where(:position.gt => (self.position_was || 0)).
|
220
|
+
where(:id.ne => self.id)
|
221
|
+
former_siblings.to_a.each{ |object| object.inc(:position => -1) }
|
222
|
+
end
|
223
|
+
|
224
|
+
def sibling_reposition_required?
|
225
|
+
parent_id_changed? && persisted?
|
226
|
+
end
|
227
|
+
|
228
|
+
def assign_default_position
|
229
|
+
self.position = self.last_sibling_in_list.position + 1 rescue 0
|
230
|
+
end
|
231
|
+
|
232
|
+
def assign_default_position?
|
233
|
+
self.position.nil? || self.parent_id_changed?
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module NoBrainer
|
2
|
+
module Tree
|
3
|
+
##
|
4
|
+
# = NoBrainer::Tree::Traversal
|
5
|
+
#
|
6
|
+
# NoBrainer::Tree::Traversal provides a #traverse method to walk through the tree.
|
7
|
+
# It supports these traversal methods:
|
8
|
+
#
|
9
|
+
# * depth_first
|
10
|
+
# * breadth_first
|
11
|
+
#
|
12
|
+
# == Depth First Traversal
|
13
|
+
#
|
14
|
+
# See http://en.wikipedia.org/wiki/Depth-first_search for a proper description.
|
15
|
+
#
|
16
|
+
# Given a tree like:
|
17
|
+
#
|
18
|
+
# node1:
|
19
|
+
# - node2:
|
20
|
+
# - node3
|
21
|
+
# - node4:
|
22
|
+
# - node5
|
23
|
+
# - node6
|
24
|
+
# - node7
|
25
|
+
#
|
26
|
+
# Traversing the tree using depth first traversal would visit each node in this order:
|
27
|
+
#
|
28
|
+
# node1, node2, node3, node4, node5, node6, node7
|
29
|
+
#
|
30
|
+
# == Breadth First Traversal
|
31
|
+
#
|
32
|
+
# See http://en.wikipedia.org/wiki/Breadth-first_search for a proper description.
|
33
|
+
#
|
34
|
+
# Given a tree like:
|
35
|
+
#
|
36
|
+
# node1:
|
37
|
+
# - node2:
|
38
|
+
# - node5
|
39
|
+
# - node3:
|
40
|
+
# - node6
|
41
|
+
# - node7
|
42
|
+
# - node4
|
43
|
+
#
|
44
|
+
# Traversing the tree using breadth first traversal would visit each node in this order:
|
45
|
+
#
|
46
|
+
# node1, node2, node3, node4, node5, node6, node7
|
47
|
+
#
|
48
|
+
module Traversal
|
49
|
+
extend ActiveSupport::Concern
|
50
|
+
|
51
|
+
|
52
|
+
##
|
53
|
+
# This module implements class methods that will be available
|
54
|
+
# on the document that includes NoBrainer::Tree::Traversal
|
55
|
+
module ClassMethods
|
56
|
+
##
|
57
|
+
# Traverses the entire tree, one root at a time, using the given traversal
|
58
|
+
# method (Default is :depth_first).
|
59
|
+
#
|
60
|
+
# See NoBrainer::Tree::Traversal for available traversal methods.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
#
|
64
|
+
# # Say we have the following tree, and want to print its hierarchy:
|
65
|
+
# # root_1
|
66
|
+
# # child_1_a
|
67
|
+
# # root_2
|
68
|
+
# # child_2_a
|
69
|
+
# # child_2_a_1
|
70
|
+
#
|
71
|
+
# Node.traverse(:depth_first) do |node|
|
72
|
+
# indentation = ' ' * node.depth
|
73
|
+
#
|
74
|
+
# puts "#{indentation}#{node.name}"
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
def traverse(type = :depth_first, &block)
|
78
|
+
roots.collect { |root| root.traverse(type, &block) }.flatten
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Traverses the tree using the given traversal method (Default is :depth_first)
|
84
|
+
# and passes each document node to the block.
|
85
|
+
#
|
86
|
+
# See NoBrainer::Tree::Traversal for available traversal methods.
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
#
|
90
|
+
# results = []
|
91
|
+
# root.traverse(:depth_first) do |node|
|
92
|
+
# results << node
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# root.traverse(:depth_first).map(&:name)
|
96
|
+
# root.traverse(:depth_first, &:name)
|
97
|
+
#
|
98
|
+
def traverse(type = :depth_first, &block)
|
99
|
+
block ||= lambda { |node| node }
|
100
|
+
send("#{type}_traversal", &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def depth_first_traversal(&block)
|
106
|
+
result = [block.call(self)] + self.children.collect { |c| c.send(:depth_first_traversal, &block) }
|
107
|
+
result.flatten
|
108
|
+
end
|
109
|
+
|
110
|
+
def breadth_first_traversal(&block)
|
111
|
+
result = []
|
112
|
+
queue = [self]
|
113
|
+
while queue.any? do
|
114
|
+
node = queue.shift
|
115
|
+
result << block.call(node)
|
116
|
+
queue += node.children
|
117
|
+
end
|
118
|
+
result
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|