hyrarchy 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 The Indianapolis Star
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.rdoc ADDED
@@ -0,0 +1,69 @@
1
+ = Hyrarchy
2
+
3
+ Hyrarchy (Hybrid hieRarchy) is a gem and Rails plugin for working with hierarchic data in ActiveRecord. Your models gain methods for finding an instance's parent, children, ancestors, descendants, and depth, as well as a named scope for finding root nodes.
4
+
5
+ To use Hyrarchy in your Rails app, copy the plugin from the gem into your app's vendors/plugins directory. (The plugin is just a two-liner that loads and activates the gem.)
6
+
7
+ To use Hyrarchy in one of your models, add the following line to the class:
8
+
9
+ class Comment < ActiveRecord::Base
10
+ is_hierarchic
11
+ end
12
+
13
+ Then add the hierarchic columns to the model's database table:
14
+
15
+ class MakeCommentsHierarchic < ActiveRecord::Migration
16
+ def self.up
17
+ add_hierarchy :comments
18
+ end
19
+
20
+ def self.down
21
+ remove_hierarchy :comments
22
+ end
23
+ end
24
+
25
+ Or you can put it in the same migration as the table's creation:
26
+
27
+ class CreateCommentsTable < ActiveRecord::Migration
28
+ def self.up
29
+ create_table :comments do |t|
30
+ t.integer :author_id
31
+ t.text :body
32
+ end
33
+ add_hierarchy :comments
34
+ end
35
+
36
+ def self.down
37
+ drop_table :comments
38
+ end
39
+ end
40
+
41
+ == Performance
42
+
43
+ On MySQL, Hyrarchy scales to at least one million nodes with insertion and access times below 100ms. On SQLite, times are below 200ms.
44
+
45
+ == Database Compatibility
46
+
47
+ Hyrarchy has been tested on MySQL 5 and SQLite 3.
48
+
49
+ == Replacing awesome_nested_set
50
+
51
+ Hyrarchy is designed to be an almost-drop-in replacement for awesome_nested_set. All of awesome_nested_set's methods are implemented by Hyrarchy, but you'll need to replace calls to acts_as_nested_set with is_hierarchic. You'll also need to replace awesome_nested_set's database columns with Hyrarchy's, which you can do with an option to the add_hierarchy migration method:
52
+
53
+ add_hierarchy :comments, :convert => :awesome_nested_set
54
+
55
+ The convert option will modify the table structure but it won't rebuild the hierarchy information. You can rebuild it by calling rebuild! on your hierarchic model class:
56
+
57
+ Comment.rebuild!
58
+
59
+ The same option can be used with remove_hierarchy for the down half of a migration.
60
+
61
+ Hyrarchy doesn't yet support awesome_nested_set's scoping feature or its view helper.
62
+
63
+ == Implementation Details
64
+
65
+ Under the hood, Hyrarchy uses a combination of an adjacency list and a rational nested set. The nested set uses a technique developed by (I think) Vadim Tropashko, in which the left and right values are generated using Farey sequences. This makes it possible to insert new records without adjusting the left and right values of any other records. It also makes it possible to do many operations (like determining a record's depth in the tree) without accessing the database. For operations where rational nested sets perform poorly (such as finding a node's immediate descendants), the adjacency list is used.
66
+
67
+ == Credits and Copyright
68
+
69
+ Heavily based on works by Vadim Tropashko and Wim Lewis. Implemented by Dana Danger. Tolerated by VivaZoya. Copyright (c) 2008 The Indianapolis Star, released under the MIT license. See LICENSE for details.
data/lib/hyrarchy.rb ADDED
@@ -0,0 +1,321 @@
1
+ require 'hyrarchy/encoded_path'
2
+ require 'hyrarchy/collection_proxy'
3
+ require 'hyrarchy/awesome_nested_set_compatibility'
4
+
5
+ module Hyrarchy
6
+ # Fudge factor to account for imprecision with floating point approximations
7
+ # of a node's left and right fractions.
8
+ FLOAT_FUDGE_FACTOR = 0.00000000001 # :nodoc:
9
+
10
+ # Mixes Hyrarchy into ActiveRecord.
11
+ def self.activate!
12
+ ActiveRecord::Base.extend IsHierarchic
13
+ ActiveRecord::Migration.extend Migrations
14
+ end
15
+
16
+ # These methods are available in ActiveRecord migrations for adding and
17
+ # removing columns and indexes required by Hyrarchy.
18
+ module Migrations
19
+ def add_hierarchy(table, options = {})
20
+ convert = options.delete(:convert)
21
+ unless options.empty?
22
+ raise(ArgumentError, "unknown keys: #{options.keys.join(', ')}")
23
+ end
24
+
25
+ case convert
26
+ when :awesome_nested_set
27
+ remove_column table, :lft
28
+ remove_column table, :rgt
29
+ when '', nil
30
+ else
31
+ raise(ArgumentError, "don't know how to convert hierarchy from #{convert}")
32
+ end
33
+
34
+ add_column table, :lft, :float
35
+ add_column table, :rgt, :float
36
+ add_column table, :lft_numer, :integer
37
+ add_column table, :lft_denom, :integer
38
+ add_column table, :parent_id, :integer unless convert == :awesome_nested_set
39
+ add_index table, :lft
40
+ add_index table, [:lft_numer, :lft_denom], :unique => true
41
+ add_index table, :parent_id
42
+ end
43
+
44
+ def remove_hierarchy(table, options = {})
45
+ convert = options.delete(:convert)
46
+ unless options.empty?
47
+ raise(ArgumentError, "unknown keys: #{options.keys.join(', ')}")
48
+ end
49
+
50
+ remove_column table, :lft
51
+ remove_column table, :rgt
52
+ remove_column table, :lft_numer
53
+ remove_column table, :lft_denom
54
+ remove_column table, :parent_id, :integer unless convert == :awesome_nested_set
55
+
56
+ case convert
57
+ when :awesome_nested_set
58
+ add_column table, :lft, :integer
59
+ add_column table, :rgt, :integer
60
+ when '', nil
61
+ else
62
+ raise(ArgumentError, "don't know how to convert hierarchy to #{convert}")
63
+ end
64
+ end
65
+ end
66
+
67
+ module IsHierarchic
68
+ # Declares that a model represents hierarchic data. Adds a has_many
69
+ # association for instances' children, and a named scope for the model's
70
+ # root nodes (called +roots+).
71
+ def is_hierarchic
72
+ extend ClassMethods
73
+ include InstanceMethods
74
+
75
+ has_many :children,
76
+ :foreign_key => 'parent_id',
77
+ :order => 'rgt DESC, lft',
78
+ :class_name => self.to_s,
79
+ :dependent => :destroy
80
+
81
+ before_save :set_encoded_paths
82
+ before_save :set_parent_id
83
+ after_save :update_descendant_paths
84
+ after_save :reset_flags
85
+
86
+ named_scope :roots,
87
+ :conditions => { :parent_id => nil },
88
+ :order => 'rgt DESC, lft'
89
+ end
90
+ end
91
+
92
+ # These private methods are available to model classes that have been
93
+ # declared is_hierarchic. They're used internally and aren't intended to be
94
+ # used by application developers.
95
+ module ClassMethods # :nodoc:
96
+ include Hyrarchy::AwesomeNestedSetCompatibility::ClassMethods
97
+
98
+ private
99
+
100
+ # Finds the first unused child path beneath +parent_path+.
101
+ def next_child_encoded_path(parent_path)
102
+ if parent_path == Hyrarchy::EncodedPath::ROOT
103
+ if sibling = roots.last
104
+ child_path = sibling.send(:encoded_path).next_sibling
105
+ else
106
+ child_path = Hyrarchy::EncodedPath::ROOT.first_child
107
+ end
108
+ else
109
+ node = find_by_encoded_path(parent_path)
110
+ child_path = node ?
111
+ node.send(:next_child_encoded_path) : parent_path.first_child
112
+ end
113
+ while self.exists?(:lft_numer => child_path.numerator, :lft_denom => child_path.denominator)
114
+ child_path = child_path.next_sibling
115
+ end
116
+ child_path
117
+ end
118
+
119
+ # Returns the node with the specified encoded path.
120
+ def find_by_encoded_path(p)
121
+ find(:first, :conditions => {
122
+ :lft_numer => p.numerator,
123
+ :lft_denom => p.denominator
124
+ })
125
+ end
126
+ end
127
+
128
+ # These methods are available to instances of models that have been declared
129
+ # is_hierarchic.
130
+ module InstanceMethods
131
+ include Hyrarchy::AwesomeNestedSetCompatibility::InstanceMethods
132
+
133
+ # Returns this node's parent, or +nil+ if this is a root node.
134
+ def parent
135
+ return @new_parent if @new_parent
136
+ p = encoded_path.parent
137
+ return nil if p.nil?
138
+ self.class.send(:find_by_encoded_path, p)
139
+ end
140
+
141
+ # Sets this node's parent. To make this node a root node, set its parent to
142
+ # +nil+.
143
+ def parent=(other)
144
+ @make_root = false
145
+ if other.nil?
146
+ @new_parent = nil
147
+ @make_root = true
148
+ elsif encoded_path && other.encoded_path == (encoded_path.parent rescue nil)
149
+ @new_parent = nil
150
+ else
151
+ @new_parent = other
152
+ end
153
+ other
154
+ end
155
+
156
+ # Returns an array of this node's descendants: its children, grandchildren,
157
+ # and so on. The array returned by this method is a named scope.
158
+ def descendants
159
+ cached[:descendants] ||=
160
+ self_and_descendants.scoped :conditions => "id <> #{id}"
161
+ end
162
+
163
+ # Returns an array of this node's ancestors--its parent, grandparent, and
164
+ # so on--ordered from parent to root. The array returned by this method is
165
+ # a has_many association, so you can do things like this:
166
+ #
167
+ # node.ancestors.find(:all, :conditions => { ... })
168
+ #
169
+ def ancestors(with_self = false)
170
+ cache_key = with_self ? :self_and_ancestors : :ancestors
171
+ return cached[cache_key] if cached[cache_key]
172
+
173
+ paths = []
174
+ path = with_self ? encoded_path : encoded_path.parent
175
+ while path do
176
+ paths << path
177
+ path = path.parent
178
+ end
179
+
180
+ cached[cache_key] = CollectionProxy.new(
181
+ self,
182
+ cache_key,
183
+ :conditions => paths.empty? ? "id <> id" : [
184
+ paths.collect {|p| "(lft_numer = ? AND lft_denom = ?)"}.join(" OR "),
185
+ *(paths.collect {|p| [p.numerator, p.denominator]}.flatten)
186
+ ],
187
+ :order => 'rgt, lft DESC'
188
+ )
189
+ end
190
+
191
+ # Returns the root node related to this node, or nil if this node is a root
192
+ # node.
193
+ def root
194
+ return cached[:root] if cached[:root]
195
+
196
+ path = encoded_path.parent
197
+ while path do
198
+ parent = path.parent
199
+ break if parent.nil?
200
+ path = parent
201
+ end
202
+
203
+ if path
204
+ self.class.send :find_by_encoded_path, path
205
+ else
206
+ nil
207
+ end
208
+ end
209
+
210
+ # Returns the number of nodes between this one and the top of the tree.
211
+ def depth
212
+ encoded_path.depth - 1
213
+ end
214
+
215
+ # Overrides ActiveRecord's reload method to clear cached scopes and ad hoc
216
+ # associations.
217
+ def reload(options = nil) # :nodoc:
218
+ @cached = {}
219
+ reset_flags
220
+ super
221
+ end
222
+
223
+ protected
224
+
225
+ # Sets the node's encoded path, updating all relevant database columns to
226
+ # match.
227
+ def encoded_path=(r) # :nodoc:
228
+ @cached = {}
229
+ if r.nil?
230
+ self.lft_numer = nil
231
+ self.lft_denom = nil
232
+ self.lft = nil
233
+ self.rgt = nil
234
+ else
235
+ @path_has_changed = true
236
+ self.lft_numer = r.numerator
237
+ self.lft_denom = r.denominator
238
+ self.lft = r.to_f
239
+ self.rgt = encoded_path.next_farey_fraction.to_f
240
+ end
241
+ r
242
+ end
243
+
244
+ # Returns the node's encoded path (its rational left value).
245
+ def encoded_path # :nodoc:
246
+ return nil if lft_numer.nil? || lft_denom.nil?
247
+ Hyrarchy::EncodedPath(lft_numer, lft_denom)
248
+ end
249
+
250
+ # Returns a hash for caching scopes and ad hoc associations.
251
+ def cached # :nodoc:
252
+ @cached ||= {}
253
+ end
254
+
255
+ # Returns the first unused child path under this node.
256
+ def next_child_encoded_path
257
+ return nil unless encoded_path
258
+ if children.empty?
259
+ encoded_path.first_child
260
+ else
261
+ children.last.send(:encoded_path).next_sibling
262
+ end
263
+ end
264
+
265
+ private
266
+
267
+ # before_save callback to ensure that this node's encoded path is a child
268
+ # of its parent.
269
+ def set_encoded_paths # :nodoc:
270
+ @path_has_changed = false if @path_has_changed.nil?
271
+ p = nil
272
+ self.lft_numer = self.lft_denom = nil if @make_root
273
+
274
+ if @new_parent.nil?
275
+ if lft_numer.nil? || lft_denom.nil?
276
+ p = Hyrarchy::EncodedPath::ROOT
277
+ end
278
+ else
279
+ p = @new_parent.encoded_path
280
+ end
281
+
282
+ if p
283
+ new_path = self.class.send(:next_child_encoded_path, p)
284
+ if @path_has_changed = (encoded_path != new_path)
285
+ self.encoded_path = new_path
286
+ end
287
+ end
288
+
289
+ true
290
+ end
291
+
292
+ # before_save callback to ensure that this node's parent_id attribute
293
+ # agrees with its encoded path.
294
+ def set_parent_id # :nodoc:
295
+ parent = self.class.send(:find_by_encoded_path, encoded_path.parent(false))
296
+ self.parent_id = parent ? parent.id : nil
297
+ true
298
+ end
299
+
300
+ # after_save callback to ensure that this node's descendants are updated if
301
+ # this node has moved.
302
+ def update_descendant_paths # :nodoc:
303
+ return true unless @path_has_changed
304
+ children.reload if children.loaded? && children.empty?
305
+
306
+ child_path = encoded_path.first_child
307
+ children.each do |c|
308
+ c.encoded_path = child_path
309
+ c.save!
310
+ child_path = child_path.next_sibling
311
+ end
312
+
313
+ true
314
+ end
315
+
316
+ # Resets internal flags after saving.
317
+ def reset_flags # :nodoc:
318
+ @path_has_changed = @new_parent = @make_root = nil
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,375 @@
1
+ module Hyrarchy
2
+ module AwesomeNestedSetCompatibility
3
+ module ClassMethods
4
+ # Returns the first root node.
5
+ def root
6
+ roots.first
7
+ end
8
+
9
+ # Returns true if the model's left and right values are valid, and all
10
+ # root nodes have no ancestors.
11
+ def valid?
12
+ left_and_rights_valid? && all_roots_valid?
13
+ end
14
+
15
+ # Returns true if the model's left and right values match the parent_id
16
+ # attributes.
17
+ def left_and_rights_valid?
18
+ # Load all nodes and index them by ID so we can leave the database
19
+ # alone.
20
+ nodes = connection.select_all("SELECT id, lft_numer, lft_denom, parent_id FROM #{quoted_table_name}")
21
+ nodes_by_id = {}
22
+ nodes.each do |node|
23
+ node['id'] = node['id'].to_i
24
+ node['encoded_path'] = Hyrarchy::EncodedPath(node['lft_numer'].to_i, node['lft_denom'].to_i)
25
+ node['parent_id'] = node['parent_id'] ? node['parent_id'].to_i : nil
26
+ nodes_by_id[node['id']] = node
27
+ end
28
+ # Check to see if the structure defined by the nodes' encoded paths
29
+ # matches the structure defined by their parent_id attributes.
30
+ nodes.all? do |node|
31
+ if node['parent_id'].nil?
32
+ node['encoded_path'].parent == nil rescue false
33
+ else
34
+ parent = nodes_by_id[node['parent_id']]
35
+ parent && node['encoded_path'].parent == parent['encoded_path']
36
+ end
37
+ end
38
+ end
39
+
40
+ # Always returns true. This method exists solely for compatibility with
41
+ # awesome_nested_set; the test it performs doesn't apply to Hyrarchy.
42
+ def no_duplicates_for_columns?
43
+ true
44
+ end
45
+
46
+ # Returns true if all roots have no ancestors.
47
+ def all_roots_valid?
48
+ each_root_valid?(roots)
49
+ end
50
+
51
+ # Returns true if all of the nodes in +roots_to_validate+ have no
52
+ # ancestors.
53
+ def each_root_valid?(roots_to_validate)
54
+ roots_to_validate.all? {|r| r.root?}
55
+ end
56
+
57
+ # Rebuilds the model's hierarchy attributes based on the parent_id
58
+ # attributes.
59
+ def rebuild!
60
+ return true if (valid? rescue false)
61
+
62
+ update_all("lft = id, rgt = id, lft_numer = id, lft_denom = id")
63
+ paths_by_id = {}
64
+ order_by = columns_hash['created_at'] ? :created_at : :id
65
+
66
+ nodes = roots :order => order_by
67
+ until nodes.empty? do
68
+ nodes.each do |node|
69
+ parent_path = paths_by_id[node.parent_id] || Hyrarchy::EncodedPath::ROOT
70
+ node.send(:encoded_path=, next_child_encoded_path(parent_path))
71
+ node.send(:create_or_update_without_callbacks) || raise(RecordNotSaved)
72
+ paths_by_id[node.id] = node.send(:encoded_path)
73
+ end
74
+ node_ids = nodes.collect {|n| n.id}
75
+ nodes = find(:all, :conditions => { :parent_id => node_ids }, :order => order_by)
76
+ end
77
+ end
78
+ end
79
+
80
+ module InstanceMethods
81
+ # Returns this node's left value. Records that haven't yet been saved
82
+ # won't have left values.
83
+ def left
84
+ encoded_path
85
+ end
86
+
87
+ # Returns this node's left value. Records that haven't yet been saved
88
+ # won't have right values.
89
+ def right
90
+ encoded_path && encoded_path.next_farey_fraction
91
+ end
92
+
93
+ # Returns true if this is a root node.
94
+ def root?
95
+ (encoded_path.nil? || depth == 0 || @make_root) && !@new_parent
96
+ end
97
+
98
+ # Returns true if this node has no children.
99
+ def leaf?
100
+ children.empty?
101
+ end
102
+
103
+ # Returns true if this node is a child of another node.
104
+ def child?
105
+ !root?
106
+ end
107
+
108
+ # Compares two nodes by their left values.
109
+ def <=>(x)
110
+ x.left <=> left
111
+ end
112
+
113
+ # Returns an array containing this node and its ancestors, starting with
114
+ # this node and ending with its root. The array returned by this method
115
+ # is a has_many association, so you can do things like this:
116
+ #
117
+ # node.self_and_ancestors.find(:all, :conditions => { ... })
118
+ #
119
+ def self_and_ancestors
120
+ ancestors(true)
121
+ end
122
+
123
+ # Returns an array containing this node and its siblings. The array
124
+ # returned by this method is a has_many association, so you can do things
125
+ # like this:
126
+ #
127
+ # node.self_and_siblings.find(:all, :conditions => { ... })
128
+ #
129
+ def self_and_siblings
130
+ siblings(true)
131
+ end
132
+
133
+ # Returns an array containing this node's siblings. The array returned by
134
+ # this method is a has_many association, so you can do things like this:
135
+ #
136
+ # node.siblings.find(:all, :conditions => { ... })
137
+ #
138
+ def siblings(with_self = false)
139
+ cache_key = with_self ? :self_and_siblings : :siblings
140
+ return cached[cache_key] if cached[cache_key]
141
+
142
+ if with_self
143
+ conditions = { :parent_id => parent_id }
144
+ else
145
+ conditions = ["parent_id #{parent_id.nil? ? 'IS' : '='} ? AND id <> ?",
146
+ parent_id, id]
147
+ end
148
+
149
+ cached[cache_key] = self.class.scoped(
150
+ :conditions => conditions,
151
+ :order => 'rgt DESC, lft'
152
+ )
153
+ end
154
+
155
+ # Returns an array containing this node's childless descendants. The
156
+ # array returned by this method is a named scope.
157
+ def leaves
158
+ cached[:leaves] ||= descendants.scoped :conditions => "NOT EXISTS (
159
+ SELECT * FROM #{self.class.quoted_table_name} tt
160
+ WHERE tt.parent_id = #{self.class.quoted_table_name}.id
161
+ )"
162
+ end
163
+
164
+ # Alias for depth.
165
+ def level
166
+ depth
167
+ end
168
+
169
+ # Returns an array of this node and its descendants: its children,
170
+ # grandchildren, and so on. The array returned by this method is a
171
+ # has_many association, so you can do things like this:
172
+ #
173
+ # node.self_and_descendants.find(:all, :conditions => { ... })
174
+ #
175
+ def self_and_descendants
176
+ cached[:self_and_descendants] ||= CollectionProxy.new(
177
+ self,
178
+ :descendants,
179
+ :conditions => { :lft => (lft - FLOAT_FUDGE_FACTOR)..(rgt + FLOAT_FUDGE_FACTOR) },
180
+ :order => 'rgt DESC, lft',
181
+ # The query conditions intentionally load extra records that aren't
182
+ # descendants to account for floating point imprecision. This
183
+ # procedure removes the extra records.
184
+ :after => Proc.new do |records|
185
+ r = encoded_path.next_farey_fraction
186
+ records.delete_if do |n|
187
+ n.encoded_path < encoded_path || n.encoded_path >= r
188
+ end
189
+ end,
190
+ # The regular count method doesn't work because of the fudge factor
191
+ # in the conditions. This procedure uses the length of the records
192
+ # array if it's been loaded. Otherwise it does a raw SQL query (to
193
+ # avoid the expense of instantiating a bunch of ActiveRecord objects)
194
+ # and prunes the results in the same manner as the :after procedure.
195
+ :count => Proc.new do
196
+ if descendants.loaded?
197
+ descendants.length
198
+ else
199
+ rows = self.class.connection.select_all("
200
+ SELECT lft_numer, lft_denom
201
+ FROM #{self.class.quoted_table_name}
202
+ WHERE #{descendants.conditions}")
203
+ r = encoded_path.next_farey_fraction
204
+ rows.delete_if do |row|
205
+ p = Hyrarchy::EncodedPath(
206
+ row['lft_numer'].to_i,
207
+ row['lft_denom'].to_i)
208
+ p < encoded_path || p >= r
209
+ end
210
+ rows.length
211
+ end
212
+ end,
213
+ # Associations don't normally have an optimized index method, but
214
+ # this one does. :)
215
+ :index => Proc.new do |obj|
216
+ rows = self.class.connection.select_all("
217
+ SELECT id, lft_numer, lft_denom
218
+ FROM #{self.class.quoted_table_name}
219
+ WHERE #{descendants.conditions}
220
+ ORDER BY rgt DESC, lft")
221
+ r = encoded_path.next_farey_fraction
222
+ rows.delete_if do |row|
223
+ p = Hyrarchy::EncodedPath(
224
+ row['lft_numer'].to_i,
225
+ row['lft_denom'].to_i)
226
+ row.delete('lft_numer')
227
+ row.delete('lft_denom')
228
+ p < encoded_path || p >= r
229
+ end
230
+ rows.index({'id' => obj.id.to_s})
231
+ end
232
+ )
233
+ end
234
+
235
+ # Returns true if this node is a descendant of +other+.
236
+ def is_descendant_of?(other)
237
+ left > other.left && left <= other.right
238
+ end
239
+
240
+ # Returns true if this node is a descendant of +other+, or if this node
241
+ # is +other+.
242
+ def is_or_is_descendant_of?(other)
243
+ left >= other.left && left <= other.right
244
+ end
245
+
246
+ # Returns true if this node is an ancestor of +other+.
247
+ def is_ancestor_of?(other)
248
+ other.left > left && other.left <= right
249
+ end
250
+
251
+ # Returns true if this node is an ancestor of +other+, or if this node is
252
+ # +other+.
253
+ def is_or_is_ancestor_of?(other)
254
+ other.left >= left && other.left <= right
255
+ end
256
+
257
+ # Always returns true. This method exists solely for compatibility with
258
+ # awesome_nested_set; Hyrarchy doesn't support scoping (but maybe it will
259
+ # some day).
260
+ def same_scope?(other)
261
+ true
262
+ end
263
+
264
+ # Returns the sibling before this node. If this node is its parent's
265
+ # first child, returns nil.
266
+ def left_sibling # :nodoc:
267
+ path = send(:encoded_path)
268
+ return nil if path == path.parent.first_child
269
+ sibling_path = path.previous_sibling
270
+ until self.class.exists?(:lft_numer => sibling_path.numerator, :lft_denom => sibling_path.denominator)
271
+ sibling_path = sibling_path.previous_sibling
272
+ end
273
+ self.class.send(:find_by_encoded_path, sibling_path)
274
+ end
275
+
276
+ # Returns the sibling after this node. If this node is its parent's last
277
+ # child, returns nil.
278
+ def right_sibling
279
+ siblings = root? ? self.class.roots : other.parent.children
280
+ return nil if self == siblings.last
281
+ sibling_path = send(:encoded_path).next_sibling
282
+ until self.class.exists?(:lft_numer => sibling_path.numerator, :lft_denom => sibling_path.denominator)
283
+ sibling_path = sibling_path.next_sibling
284
+ end
285
+ self.class.send(:find_by_encoded_path, sibling_path)
286
+ end
287
+
288
+ def move_left # :nodoc:
289
+ raise NotImplementedError, "awesome_nested_set's move_left method isn't implemented in this version of Hyrarchy"
290
+ end
291
+
292
+ def move_right # :nodoc:
293
+ raise NotImplementedError, "awesome_nested_set's move_right method isn't implemented in this version of Hyrarchy"
294
+ end
295
+
296
+ # The semantics of left and right don't quite map exactly from
297
+ # awesome_nested_set to Hyrarchy. For the purpose of this method, "left"
298
+ # means "before."
299
+ #
300
+ # If this node isn't a sibling of +other+, its parent will be set to
301
+ # +other+'s parent.
302
+ def move_to_left_of(other)
303
+ # Don't attempt an impossible move.
304
+ if other.is_descendant_of?(self)
305
+ raise ArgumentError, "you can't move a node to the left of one of its descendants"
306
+ end
307
+ # Find the first unused path after +other+'s path.
308
+ open_path = other.send(:encoded_path).next_sibling
309
+ while self.class.exists?(:lft_numer => open_path.numerator, :lft_denom => open_path.denominator)
310
+ open_path = open_path.next_sibling
311
+ end
312
+ # Move +other+, and all nodes following it, down.
313
+ while open_path != other.send(:encoded_path)
314
+ p = open_path.previous_sibling
315
+ n = self.class.send(:find_by_encoded_path, p)
316
+ n.send(:encoded_path=, open_path)
317
+ n.save!
318
+ open_path = p
319
+ end
320
+ puts open_path
321
+ # Insert this node.
322
+ send(:encoded_path=, open_path)
323
+ save!
324
+ end
325
+
326
+ # The semantics of left and right don't quite map exactly from
327
+ # awesome_nested_set to Hyrarchy. For the purpose of this method, "right"
328
+ # means "after."
329
+ #
330
+ # If this node isn't a sibling of +other+, its parent will be set to
331
+ # +other+'s parent.
332
+ def move_to_right_of(other)
333
+ # Don't attempt an impossible move.
334
+ if other.is_descendant_of?(self)
335
+ raise ArgumentError, "you can't move a node to the right of one of its descendants"
336
+ end
337
+ # If +other+ is its parent's last child, we can simply append this node
338
+ # to the parent's children.
339
+ siblings = other.root? ? self.class.roots : other.parent.children
340
+ if other == siblings.last
341
+ send(:encoded_path=, other.send(:encoded_path).next_sibling)
342
+ save!
343
+ else
344
+ # Otherwise, this is equivalent to moving this node to the left of
345
+ # +other+'s right sibling.
346
+ move_to_left_of(other.right_sibling)
347
+ end
348
+ end
349
+
350
+ # Sets this node's parent to +node+ and calls save!.
351
+ def move_to_child_of(node)
352
+ node = self.class.find(node)
353
+ self.parent = node
354
+ save!
355
+ end
356
+
357
+ # Makes this node a root node and calls save!.
358
+ def move_to_root
359
+ self.parent = nil
360
+ save!
361
+ end
362
+
363
+ def move_possible?(target) # :nodoc:
364
+ raise NotImplementedError, "awesome_nested_set's move_possible? method isn't implemented in this version of Hyrarchy"
365
+ end
366
+
367
+ # Returns a textual representation of this node and its descendants.
368
+ def to_text
369
+ self_and_descendants.map do |node|
370
+ "#{'*'*(node.depth+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
371
+ end.join("\n")
372
+ end
373
+ end
374
+ end
375
+ end