nobrainer-tree 0.0.1
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.
- 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  [](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
|
+
[](http://www.pledgie.com/campaigns/12137)
|
214
|
+
|
215
|
+
[](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
|