JohnSmall-acts-as-hausdorff-space 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 John Small
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,184 @@
1
+ == acts_as_hausdorff_space
2
+
3
+ acts_as_hausdorff_space is a gemified mixin for Rails ActiveRecord. It implements the nested set model for maintaining recursive
4
+ trees in a database, but using real numbers rather than integers because real numbers are a {Hausdorff Space}[http://en.wikipedia.org/wiki/Hausdorff_space]
5
+ and the integers aren't
6
+
7
+ == Caveats
8
+ This version 0.1.0 is a proof of concept and a run through of all the github/lighthouse/google-group ecology of a gem. It works and
9
+ you're welcome to play with it and make comments, but don't trust it in production code just yet.
10
+
11
+ == Installation
12
+ 1. It's not a drop in replacement for acts_as_nested_set so watch out.
13
+ 2. Make sure you have http://gems.github.com set up in your gem sources
14
+ 3. sudo gem install acts-as-hausdorff-space
15
+ 4. In environment.rb add
16
+ <tt> config.gem "acts-as-hausdorff-space"</tt>
17
+ 5. Create a migration with left and right columns using the biggest decimal representations available on your database.
18
+ 6. Add
19
+ <tt> acts_as_hausdorff_space </tt>
20
+ to your model.
21
+
22
+
23
+ == Other references
24
+
25
+ 1. Source is at git://github.com/JohnSmall/acts-as-hausdorff-space.git
26
+ 2. Wiki is at http://wiki.github.com/JohnSmall/acts-as-hausdorff-space
27
+ 3. Lighthouse bugtracking is at http://mindoro.lighthouseapp.com/projects/29279-acts_as_hausdorff_space/overview
28
+ 4. Google group for discussion is at http://groups.google.com/group/acts_as_hausdorff_space
29
+ 5. I'm at {John Small}[http://www.mindoro-marine.co.uk]
30
+
31
+
32
+ == A Problem with Nested Sets - Mega Peformance Issues
33
+
34
+ I was using the better_nested_set plugin in a territories model and trying to load all the countries in the world and all the regions and sub-regions
35
+ and it was taking utterly ages. So I set about thinking what could be done to improve performance. I'd written the
36
+ code for nested sets before a long time ago using Delphi & Interbase, so I knew the problems. But in the meantime I'd
37
+ learned some topology theory so I had a name and a concept to put to what must have been starting everyone in the face
38
+ . Hausdorff Spaces! Integers aren't a Hausdorff space but we're trying to implement a concept, namely nested sets,
39
+ which are the defining characteristic of a Hausdorff space, so we have to write extra code to fudge integers to behave
40
+ like they are a Hausdorff space and that is the source of the performance hit. So the obvious thing to do is to use
41
+ real numbers which are a Hausdorff space and then the extra code and associated performance hit melts away. After some
42
+ fussing about version 0.1.0 is now released. For small trees the overhead of using BigDecimal rather than integers makes this
43
+ method slower, but as the trees get bigger the relative performance soon switches in favour of using BigDecimals.
44
+ check the table at the bottom to see how big the performance improvements can be.
45
+
46
+ == A bit of history
47
+
48
+ Joe Celko, an SQL guru, popularized the idea of using nested sets for databases back in his 1996 article reproduced
49
+ {here}[http://www.dbmsmag.com/9603d06.html].
50
+ Though the earliest description is in {Kamfonas}[http://www.kamfonas.com/id3.html].
51
+ Since it's usually a bad idea to go against the advice of an SQL guru every implementation I've seen follows his example in using integers to
52
+ set the nesting boundaries. So every implementation has had to deal with the awkwardness that comes with using integers
53
+ in a way they can't inherently be used. The description in {Kamfonas}[http://www.kamfonas.com/id3.html] recomends using a SKIP-VALUE
54
+ to make sure you've got some space to put new entries in. Though the code for that is obviously going to be complicated because
55
+ you'd need to work out a skip value for each node if you're adding a collection of nodes inside an already existing node. This is the
56
+ kind of coding fudge people have to do to make nested sets maintainable.
57
+
58
+ == The Basic Idea
59
+ This is copied from {ThreeBit}[http://threebit.net/tutorials/nestedset/tutorial1.html] and is the example used in
60
+ better_nested_sets. The example uses integers.
61
+
62
+ An easy way to visualize how a nested set works is to think of a parent entity surrounding all
63
+ of its children, and its parent surrounding it, etc. So this tree:
64
+ root
65
+ |_ Child 1
66
+ |_ Child 1.1
67
+ |_ Child 1.2
68
+ |_ Child 2
69
+ |_ Child 2.1
70
+ |_ Child 2.2
71
+
72
+ Could be visualized like this:
73
+ ___________________________________________________________________
74
+ | Root |
75
+ | ____________________________ ____________________________ |
76
+ | | Child 1 | | Child 2 | |
77
+ | | __________ _________ | | __________ _________ | |
78
+ | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
79
+ 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
80
+ | |___________________________| |___________________________| |
81
+ |___________________________________________________________________|
82
+
83
+ The numbers represent the left and right boundaries. The table then might
84
+ look like this:
85
+ id | parent_id | lft | rgt | data
86
+ 1 | | 1 | 14 | root
87
+ 2 | 1 | 2 | 7 | Child 1
88
+ 3 | 2 | 3 | 4 | Child 1.1
89
+ 4 | 2 | 5 | 6 | Child 1.2
90
+ 5 | 1 | 8 | 13 | Child 2
91
+ 6 | 5 | 9 | 10 | Child 2.1
92
+ 7 | 5 | 11 | 12 | Child 2.2
93
+
94
+ To get all children of an entry +parent+, you
95
+ SELECT * WHERE lft IS BETWEEN parent.lft AND parent.rgt
96
+
97
+ To get the number of children, it's
98
+ (right - left - 1)/2
99
+
100
+ To get a node and all its ancestors going back to the root, you
101
+ SELECT * WHERE node.lft IS BETWEEN lft AND rgt
102
+
103
+ == Notes.
104
+
105
+ Pretty obviously if you wanted to add a new child with C.1.1 as parent you'll have to update the entire tree from 4 onwards
106
+ because there is no integer between 3 and 4.
107
+
108
+
109
+ == A bit of topological theory
110
+
111
+ There is no integer between 3 and 4, but there are an uncountable infinity of real numbers between 3 and 4. That is the crux of the problem.
112
+ Between any two real numbers there is always another real number, no matter how close they are. This property was first described
113
+ by Archimedes and it's called the {Archimedean property of real numbers}[http://en.wikipedia.org/wiki/Archimedean_property#Archimedean_property_of_the_real_numbers]
114
+
115
+ What that means is that for real numbers we can surround any two of them with disjoint open neighbourhoods and that is the defining
116
+ property of a {Hausdorff Space}[http://en.wikipedia.org/wiki/Hausdorff_space]. That property of being able to put any two points
117
+ inside disjoint sets of nearby points means you can have infinite levels of nesting. The notion of nested sets is inherently part
118
+ of a Hausdorff space, but not of the integers, which is why you have to write lots of awkward slow code to maintain nested sets implemented
119
+ with integers
120
+
121
+ == The Implementation in Abstract
122
+
123
+ The implementation is the same as above, with two differences. I'm not using parent_id because that defeats the main advantage of the nested set model over the adjaceny list model
124
+ and I'm using real numbers. That means if we want to add a record between 3 and 4, we can make lft = 3.25 and rgt = 3.5, and we
125
+ can keep on adding records to any depth or width of tree we like, without having to update the rest of the tree.
126
+
127
+ == The Real Life Implementation Constraints
128
+
129
+ Computers don't use real numbers, they use binary arithmetic to emulate real numbers. That means the lovely idea has
130
+ to get ugly when it meets the real world. There are maximum sizes for the real numbers used and also minimum sizes depending
131
+ on the real number precision limits. But within those constraints we can implement the concept of nested sets using real numbers
132
+ fairly easily. We just have to keep our calculations away from bumping up against the lower and upper limits. It means that
133
+ there's going to be a limit to the number of children a parent node can own, and also a limit to the depth of the tree. Most trees
134
+ used in nested sets in relational databases aren't very deep, but they can be very wide. When we add a new node into a gap in
135
+ the tree we have to make sure it leaves a gap between itself and its siblings or the parent lefts and rights. That way we will
136
+ always have some space to put a new record in the gap, as long as the gap isn't so small that we're bumping up against the limits
137
+ of decimal precision. So when you set up your tables always choose the maximum precision for that database.
138
+
139
+ The other issue when we add a new record, we want to leave space for more records but we don't know in advance how many
140
+ records to leave space for. We could do what's done in the integer implementations of nested sets, move every lft and rgt along
141
+ a bit, but that is where the performance hit comes from. So we have to leave gaps and fill them as best we can. If each new node
142
+ were to take up half of the remaining space between its siblings and the boundaries
143
+ of its parent, then we'd pretty soon bump up against the decimal precision limit. To avoid that I've set things up so that there's
144
+ a number called the #bias, which is a division factor. It works like this, I add a new child to a parent, I use the bias
145
+ to work out where to place the child inside what ever gap remains, roughly (remaining gap)/bias. So the bigger the bias the less
146
+ of the remaining gap is taken up. The default bias is one million, to allow for wide and shallow trees.
147
+
148
+ == Implementation Details
149
+ see
150
+
151
+ == Relative Performance
152
+
153
+ For small trees integers are faster, for large trees acts_as_hausdorff_space using BigDecimals are faster by a large margin. To get a rough estimate of
154
+ performance I wrote a test which adds 1000 children to a root node, then goes through each child node adding children. This really
155
+ hammers the integer implementation of nested sets since every new insertion requires updating every node to the right of the new
156
+ insertion. The full test description and updated results are {here}[http://mindoro-marine.co.uk/nested_sets_tests].
157
+
158
+ No. of children added is the number of child nodes added to each of the 1000 children of the root. aahs = acts_as_hausdorff_space,
159
+ bns = using the better_nested_set mixin.
160
+
161
+ | SQLite3 | MySQL |
162
+ No of | aahs | bns | aahs | bns |
163
+ children
164
+ added
165
+ 0 | 28 secs | 15 secs | 28 secs | 9 secs |
166
+ 1 | 39 | 55 | 40 | 150 |
167
+ 2 | 51 | 116 | 51 | 7,692 |
168
+ 3 | 62 | 203 | 63 | 24,555 |
169
+ 4 | 73 | 312 | 75 | I gave up |
170
+ 5 | 84 | 444 | 87 | |
171
+ 6 | 96 | 596 | 99 | |
172
+ 7 | 109 | 765 | 113 | |
173
+ 8 | 121 | 952 | 129 | |
174
+ 9 | 131 | 1156 | 144 | |
175
+
176
+ As you can see adding children into large trees using acts_as_hausdorff_space (aahs) is nice and linear in the number
177
+ of children added. Using the integer method implemented by better_nested_set things are much slower
178
+ and on MySQL the time taken blows up quite quickly.
179
+
180
+ I'll be testing Postgres and trying to work out why MySQL blows up so badly.
181
+
182
+ == Copyright
183
+
184
+ Copyright (c) 2009 John Small. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,66 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "acts-as-hausdorff-space"
8
+ gem.summary = %Q{Use real numbers instead of integers for nested sets because real numbers are a Hausdorff space}
9
+ gem.email = "jds340@gmail.com"
10
+ gem.homepage = "http://github.com/JohnSmall/acts-as-hausdorff-space"
11
+ gem.authors = ["John Small"]
12
+
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ #Rake::TestTask.new(:test) do |test|
21
+ # test.libs << 'lib' << 'test'
22
+ # test.pattern = 'test/**/*_test.rb'
23
+ # test.verbose = true
24
+ #end
25
+
26
+ desc 'Run tests on all database adapters.'
27
+ task :default => [:test_mysql, :test_sqlite3, :test_postgresql]
28
+ for adapter in %w(mysql postgresql sqlite3)
29
+ Rake::TestTask.new("test_#{adapter}") do |t|
30
+ t.libs << 'lib'
31
+ t.pattern = "test/#{adapter}.rb"
32
+ t.verbose = true
33
+ end
34
+ end
35
+
36
+ begin
37
+ require 'rcov/rcovtask'
38
+ Rcov::RcovTask.new do |test|
39
+ test.libs << 'test'
40
+ test.pattern = 'test/**/*_test.rb'
41
+ test.verbose = true
42
+ end
43
+ rescue LoadError
44
+ task :rcov do
45
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
46
+ end
47
+ end
48
+
49
+ task :default => :test
50
+
51
+ require 'rake/rdoctask'
52
+ Rake::RDocTask.new do |rdoc|
53
+ if File.exist?('VERSION.yml')
54
+ config = YAML.load(File.read('VERSION.yml'))
55
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
56
+ else
57
+ version = ""
58
+ end
59
+ rdoc.rdoc_dir = 'rdoc'
60
+ rdoc.title = "acts-as-hausdorff-space #{version}"
61
+ rdoc.rdoc_files.include('*.rdoc')
62
+ rdoc.rdoc_files.include('lib/**/*.rb')
63
+ end
64
+
65
+
66
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 3
@@ -0,0 +1,7 @@
1
+ # acts_as_hausdorff_space
2
+ # (c) John Small 2009
3
+ require "acts_as_hausdorff_space"
4
+
5
+ ActiveRecord::Base.class_eval do
6
+ include MindoroMarine::Acts::HausdorffSpace
7
+ end
@@ -0,0 +1,425 @@
1
+ # ActsAsHausdorffSpace
2
+ module MindoroMarine
3
+ module Acts #:nodoc:
4
+ module HausdorffSpace #:nodoc:
5
+
6
+ def self.included(base)
7
+ base.extend(ActMethods)
8
+ end
9
+ module ActMethods
10
+ # Configuration options are:
11
+ # * +left_column+ - Column name for the left index (default: +lft+).
12
+ # * +right_column+ - Column name for the right index (default: +rgt+). NOTE:
13
+ # Don't use +left+ and +right+, since these are reserved database words.
14
+ # * +bias+ - A divisor used to calculate where in a gap a new record should go. Bigger numbers are preferred for wide shallow trees
15
+ # Default is 1E6
16
+ # * +max_double+ - This is the outer left and right boundaries of all the trees. Set it according to the precision and scale you're
17
+ # using in your database columns. Default is 1E20
18
+ def acts_as_hausdorff_space(options = {})
19
+ #[TODO] use reverse merge here
20
+ write_inheritable_attribute(:acts_as_hausdorff_space_options,
21
+ { :left_column => (options[:left_column] || 'lft'),
22
+ :right_column => (options[:right_column] || 'rgt'),
23
+ :bias => (options[:bias] || 1E6),
24
+ :max_double => (options[:max_double] || 1E20 ),
25
+ :class => self , # for single-table inheritance
26
+ :virtual_root => VirtualRoot.new(-(options[:max_double] || 1E20 ),
27
+ (options[:max_double] || 1E20 ))
28
+ } )
29
+
30
+ class_inheritable_reader :acts_as_hausdorff_space_options
31
+ unless included_modules.include? MindoroMarine::Acts::HausdorffSpace::InstanceMethods
32
+ include MindoroMarine::Acts::HausdorffSpace::InstanceMethods
33
+ extend MindoroMarine::Acts::HausdorffSpace::ClassMethods
34
+ end
35
+ end
36
+ end
37
+
38
+ # Each branch has branches and/or leaves and gaps to put new things in
39
+ # A gap has attributes left_col_val and right_col_val
40
+ class Gap
41
+ attr_accessor :left_col_val,:right_col_val
42
+ end
43
+
44
+ # An instance of VirtualRoot is a hidden root that "owns" the actual roots. It's only here so we can use the same code for
45
+ # real roots and children. Otherwise we have to write special code for the top level parents.
46
+ # Note: This is very different from the concept of virtual roots in better_nested_set which is the equivalent of <Klass>.root.children
47
+ # to get the immediate children of a top level root
48
+ class VirtualRoot
49
+ attr_accessor :left_col_val,:right_col_val
50
+ attr_reader :children
51
+
52
+ # initialize with a left and right value that will be the bounds of the entire tree of all the roots
53
+ def initialize( left_val,right_val)
54
+ self.left_col_val = left_val
55
+ self.right_col_val = right_val
56
+ @children = HSArray.new
57
+ @children.parent = self
58
+ end
59
+
60
+ end #VirtualRoot
61
+
62
+ # We need an array to hold children, but because we don't use parent_id we can't return the children as an AR has_many association
63
+ # Therefore we use an array. Because we want to be able to write
64
+ # <tt>parent.children << SomeModel.new</tt>
65
+ # then we need to do special stuff in "<<". But we don't want to mixin into Array because that might (almost certainly would)
66
+ # mess up other code, so we'll get very Java-like and subclass Array
67
+ class HSArray < Array
68
+ attr_accessor :parent
69
+
70
+ # subclass << to set the parent on the new items being added
71
+ def <<(elem)
72
+ if elem.is_a?(Array) # if it is an array then load each item into the HSArray in turn
73
+ elem.each do |item|
74
+ self << item
75
+ end
76
+ else
77
+ super
78
+ elem.parent = self.parent if self.parent && elem.respond_to?('parent')
79
+ end
80
+ end #<<(elem)
81
+ end #HSArray
82
+
83
+
84
+ module ClassMethods
85
+
86
+ # Get all the top level roots. This does a find of all records with no immediate parent in the database.
87
+ # When they're loaded they get a virtual root attached as their parent. This is to make sure that every node has a parent which helps to
88
+ # generalise and simplify the code for working out left and right boundaries.
89
+ def roots
90
+ top_levels = self.find( :all, :conditions => "not exists (select t1.id from #{self.table_name} t1
91
+ where t1.#{left_col_name} <#{self.table_name}.#{left_col_name}
92
+ and t1.#{right_col_name} > #{self.table_name}.#{right_col_name})",:order=>"#{left_col_name}")
93
+ self.virtual_root.children.clear
94
+ top_levels.each do |tl|
95
+ begin
96
+ tl.in_tree = true
97
+ self.virtual_root.children << tl
98
+ tl.in_tree = false
99
+ rescue
100
+ tl.in_tree = false
101
+ raise
102
+ end
103
+ end
104
+ end
105
+ # get the first root from roots
106
+ def root
107
+ roots.first
108
+ end
109
+ # use the instance method <tt>build_full_tree</tt> on the root to get all the children set up in a tree
110
+ def full_tree
111
+ root.build_full_tree
112
+ end
113
+
114
+ # Return the left column name. Either the one supplied in the options to acts_as_hausdorff_space or the default "lft"
115
+ def left_col_name
116
+ acts_as_hausdorff_space_options[:left_column]
117
+ end
118
+
119
+ # Return the right column name. Either the one supplied in the options to acts_as_hausdorff_space or the default "rgt"
120
+ def right_col_name
121
+ acts_as_hausdorff_space_options[:right_column]
122
+ end
123
+
124
+ # Return the max_double supplied in the options to to acts_as_hausdorff_space or the default 1E20
125
+ def max_double
126
+ acts_as_hausdorff_space_options[:max_double]
127
+ end
128
+
129
+ # Return the bias supplied in the options to to acts_as_hausdorff_space or the default 1E6
130
+ def bias
131
+ acts_as_hausdorff_space_options[:bias]
132
+ end
133
+ # Return the virtual root. This is an in memory only parent to all the roots, the left is set to -max_double and the right to max_double
134
+ def virtual_root
135
+ acts_as_hausdorff_space_options[:virtual_root]
136
+ end
137
+
138
+ end #module ClassMethods
139
+
140
+ ############################ INSTANCE METHODS ######################################################
141
+ module InstanceMethods
142
+ attr_accessor :parent,:in_tree
143
+
144
+ # initialize isn't always called so we can't do this in initialize. This sets up things we need to hold the tree together
145
+ def after_initialize
146
+ @parent = nil
147
+ @children = HSArray.new
148
+ @children.parent = self
149
+ @root = nil
150
+ @is_root=false
151
+ @in_tree = false
152
+ @prev_left = nil
153
+ @prev_right = nil
154
+ end
155
+
156
+ # Short hand method to save typing self.class.left_col_name
157
+ def left_col_name
158
+ self.class.left_col_name
159
+ end
160
+
161
+ # Short hand method to save typing self.class.right_col_name
162
+ def right_col_name
163
+ self.class.right_col_name
164
+ end
165
+
166
+ # get the left col value
167
+ def left_col_val
168
+ read_attribute(left_col_name)
169
+ end
170
+
171
+ # set the left col value
172
+ def left_col_val=(value)
173
+ write_attribute(left_col_name,value)
174
+ end
175
+
176
+ # get the right col value
177
+ def right_col_val
178
+ read_attribute(right_col_name)
179
+ end
180
+
181
+ # set the right col value
182
+ def right_col_val=(value)
183
+ write_attribute(right_col_name,value)
184
+ end
185
+
186
+ # Get the parent.
187
+ # If the parent hasn't already been set, and if we're not building the full tree (which sets parents) and if we've
188
+ # got a valid lft and right then construct the SQL to get the parent from the DB. If none is found then it's a root so
189
+ # set the parent to be the class.virtual_root
190
+ # If the parent is already set then don't go to the database to look for it, just return it
191
+ def parent
192
+ if !in_tree && left_col_val && right_col_val && !@parent # only get the parent by SQL if we don't know what it is
193
+ @parent = self.class.find :first, :conditions => " #{left_col_val} > #{self.class.left_col_name} and #{left_col_val} < #{self.class.right_col_name} ",:order =>'#{self.class.left_col_name} DESC'
194
+ if !@parent
195
+ # @is_root = (lft == rgt) || (id == root.id)
196
+ return self.class.virtual_root
197
+ end
198
+ else
199
+ @parent
200
+ end
201
+ end
202
+
203
+ # set a new parent. This will save the parent, which will force a save of this child after the parent has had its lft and rgt set
204
+ # If we're loading a full tree, we don't save the parent and child as that would be daft
205
+ def parent=(new_parent)
206
+ if @parent != new_parent
207
+ @parent = new_parent
208
+ needs_moving!
209
+ @is_root = @parent.is_a?(VirtualRoot)
210
+ end
211
+ # [TODO] clean this up with parent.save_children
212
+ parent.save unless in_tree || @parent.is_a?(VirtualRoot)
213
+ return @parent
214
+ end
215
+
216
+ # sets the in_tree flag, pulls in all the children of this record with the level set correctly and ordered by lft.
217
+ # Then it executes a private method which does a depth first recursion of the returned dataset to build the tree.
218
+ # Returns this record as the root of the tree
219
+ def build_full_tree
220
+ self.in_tree = true
221
+ begin
222
+ # get all the children with levels counted and ordered in one SQL statement - this is what nested sets are all about
223
+ # then do a depth first traversal of all the entire list to build the tree in memory
224
+ child_list = self.class.find_by_sql("SELECT t1.*,(select count(*) from #{self.class.table_name} t2 where t2.#{left_col_name}<t1.#{left_col_name} and t2.#{right_col_name} > t1.#{right_col_name}) as depth from #{self.class.table_name} t1 where t1.#{left_col_name} >#{left_col_val} and t1.#{right_col_name} <#{right_col_val} order by #{left_col_name} desc")
225
+ child_list.each{|r| r.in_tree = true}
226
+ #for some strange reason depth comes back as a string in Postgresql hence the .to_i
227
+ build_tree_by_recursion(self,child_list.last.depth.to_i,child_list)
228
+ rescue
229
+ self.in_tree = false
230
+ raise
231
+ end
232
+ return self
233
+ end
234
+
235
+ # get the gap on the left of this record
236
+ def lft_gap
237
+ # puts " in lft gap #{parent.class.name}"
238
+ gap = Gap.new
239
+ gap.right_col_val = self.left_col_val
240
+ if parent.children.first == self
241
+ gap.left_col_val = parent.left_col_val
242
+ else
243
+ # puts "get prev index"
244
+ prev_index = parent.children.index(self)-1
245
+ # puts "prev index = #{prev_index} count = #{parent.children.size}"
246
+ gap.left_col_val = parent.children[prev_index].right_col_val
247
+ end
248
+ return gap
249
+ end
250
+
251
+ # get the gap on the right of this record
252
+ def rgt_gap
253
+ # puts " in rgt gap #{parent.class.name}"
254
+ gap = Gap.new
255
+ gap.left_col_val = self.right_col_val
256
+ if parent.children.last == self
257
+ gap.right_col_val = parent.right_col_val
258
+ else
259
+ # puts "get next index"
260
+ next_index = parent.children.index(self)+1
261
+ # puts "next index = #{next_index} count = #{parent.children.size}"
262
+ gap.right_col_val = parent.children[next_index].left_col_val
263
+ end
264
+ return gap
265
+ end # rgt_gap
266
+
267
+ # is this record a root
268
+ def is_root?
269
+ @is_root
270
+ end
271
+
272
+ # find the root that owns this record. This is called "my_root" rather than "root" so you don't mix it up with <class>.root
273
+ # This is different to how it's done in acts_as_nested_set
274
+ def my_root
275
+ unless @root
276
+ @root = (self.class.find :first, :conditions => " #{left_col_val} >= #{left_col_name} and #{right_col_val}<= #{right_col_name} ",:order =>"#{left_col_name}") #between includes the end points
277
+ else
278
+ @root
279
+ end
280
+ end
281
+
282
+ # Find the immediate children of this record. If the children have already been loaded before either via build_full_tree or
283
+ # a previous call to children then this just returns what was previously loaded. Otherwise it goes to the database.
284
+ # The sql is a bit more involved than used in acts_as_nested_set because this is a pure nested set model and there's no parent_id
285
+ # Because we're not using parent_id the returned array is not an AR associattion, unlike acts_as_nested_set. You have been warned.
286
+ def children
287
+ unless ( @children.size>0 || ( left_col_val == right_col_val ) || in_tree) # don't get children unless lft is not equal to rgt
288
+ # puts "load_tree = #{in_tree}"
289
+ @children << self.class.find( :all,:conditions => " #{self.class.table_name}.#{left_col_name} >#{left_col_val} and #{self.class.table_name}.#{right_col_name} < #{right_col_val} and #{left_col_val} = (select max(t1.#{left_col_name}) from #{self.class.table_name} t1 where #{self.class.table_name}.#{left_col_name} between t1.#{left_col_name} and t1.#{right_col_name})" )
290
+ else
291
+ @children
292
+ end
293
+ end
294
+
295
+ # Where most of the clever action happens.
296
+ # 1. If there's no parent then set the <class>.virtual_root to be the parent before continuing
297
+ # 2. Find the left boundary and the right boundary from the parent or the siblings or both
298
+ # 3. If this record has no children then set lft=rgt and put it well on the left of the gap
299
+ # 4. If there are children then set the lft and rgt to a comfortable size to hold the children
300
+ def before_save
301
+ if !@parent
302
+ self.class.roots # this loads virtual_root with roots
303
+ self.class.virtual_root.children << self # if we haven't got a parent then make the virtual root the parent
304
+ end
305
+ far_left = furthrest_left
306
+ far_right = furthrest_right
307
+ #puts "furthrest_right = #{far_right} furthrest_left = #{far_left}"
308
+ gap = far_right - far_left
309
+ if @children.size>0
310
+ self.left_col_val = far_left + gap/4
311
+ self.right_col_val = far_right - gap/4
312
+ else
313
+ self.left_col_val = self.right_col_val = far_left+(gap/self.class.bias)
314
+ end
315
+ end
316
+
317
+ # Where most of the remaining action happens
318
+ # If we've moved the record into a new parent from somewhere else then we need to update the old child records
319
+ # to bring those into place. This requires a scaling, to fit into the new gap, and a translation to move into the corrent place
320
+ # The code works out the values required for a single SQL statement that pulls the entire branch across
321
+ #
322
+ # If we haven't moved from elsewhere then save each child that needs saving
323
+ def after_save
324
+ if @prev_left && @prev_right && @children.size > 0 # if we're moving from somewhere else
325
+ @children.clear
326
+ scale = ((right_col_val-left_col_val)/(@prev_right - @prev_left)).abs
327
+ old_mid_point = (@prev_right + @prev_left)/2
328
+ new_mid_point = (right_col_val+left_col_val)/2
329
+ sql = "update #{self.class.table_name} t1 set t1.#{left_col_name} = ((t1.#{left_col_name} - #{old_mid_point})*#{scale})+#{new_mid_point}, t1.#{right_col_name} = ((t1.#{right_col_name} - #{old_mid_point})*#{scale})+#{new_mid_point} where t1.#{left_col_name} >#{@prev_left} and t1.#{right_col_name} < #{@prev_right} "
330
+ connection.update_sql(sql)
331
+ build_full_tree # get the children back
332
+ else
333
+ @children.each{|child| child.save if !(child.left_col_val && child.right_col_val) } # save only the ones that need lft and rgt
334
+ end
335
+ @prev_left = @prev_right = nil
336
+ end
337
+
338
+ # get the siblings. All immediate children of the parent minus self
339
+ def siblings
340
+ if parent
341
+ self_and_siblings.reject{|r| r == self}
342
+ end
343
+ end
344
+
345
+ # get all children of the parent of this record
346
+ def self_and_siblings
347
+ if parent
348
+ parent.children
349
+ end
350
+ end
351
+ # get the line of owners with the root first
352
+ def self_and_ancestors
353
+ self.class.find :all, :conditions =>"#{left_col_name} <= #{left_col_val} and #{right_col_name} >= #{right_col_val} ",:order=>"#{left_col_name}"
354
+ end
355
+
356
+ # self_and_ancestors with self removed
357
+ def ancestors
358
+ self_and_ancestors.reject{|r| r == self}
359
+ end
360
+
361
+ # find all children at any depth with lft=rgt
362
+ def leaf_nodes
363
+ self.class.find :all,:conditions => " #{left_col_name} >#{left_col_val} and #{right_col_name} < #{right_col_val} and #{left_col_name} = #{right_col_name}"
364
+ end
365
+
366
+ # find the mid point of the gap this record fits in
367
+ def mid_point
368
+ (furthrest_left+furthrest_right)/2
369
+ end
370
+
371
+ # if the record needs moving then save the old lft and rgt and set the current lft and rgt to nil
372
+ def needs_moving!
373
+ if left_col_val && right_col_val && !in_tree
374
+ @prev_left = left_col_val
375
+ self.left_col_val = nil
376
+ @prev_right = right_col_val
377
+ self.right_col_val = nil
378
+ end
379
+ end
380
+
381
+ =begin
382
+ def scale_and_translate(move_to_origin,scale_by,translate_by)
383
+ self.lft = ((self.lft - move_to_origin)*scale_by) + translate_by
384
+ self.lft = ((self.lft - move_to_origin)*scale_by) + translate_by
385
+ children.each{|child| child.scale_and_translate(move_to_origin,scale_by,translate_by)}
386
+ end
387
+ =end
388
+
389
+ private
390
+
391
+
392
+ def furthrest_left
393
+ lft_gap.left_col_val
394
+ end
395
+
396
+ def furthrest_right
397
+ rgt_gap.right_col_val
398
+ end
399
+
400
+ # this assumes a nicely ordered tree such that the next depth is always one more tha the existing depth
401
+ # otherwise throw a wobbly
402
+ # the strange .to_i has to be there because depth can come back from the db as a string
403
+ def build_tree_by_recursion(parent,depth,ar_results)
404
+ while ar_results.size > 0 && ar_results.last.depth.to_i >= depth
405
+ if ar_results.last.depth.to_i == depth
406
+ current_record = ar_results.pop
407
+ # puts "curr record in tree =#{current_record.in_tree} parent in tree = #{parent.in_tree}"
408
+ parent.children << current_record
409
+ current_record.in_tree = false
410
+ # puts "+++"
411
+ elsif ar_results.last.depth.to_i == (depth + 1)
412
+ current_record.in_tree = true
413
+ build_tree_by_recursion(current_record,ar_results.last.depth.to_i,ar_results)
414
+ current_record.in_tree = false
415
+ elsif ar_results.last.depth.to_i > (depth + 1)
416
+ raise "badly formed nested set result set"
417
+ end
418
+ end # while
419
+ end # def build_tree_by_recursion(parent,depth,ar_results)
420
+
421
+
422
+ end #module InstanceMethods
423
+ end # module HausdorffSpace
424
+ end #module Acts
425
+ end#module MindoroMarine
@@ -0,0 +1,183 @@
1
+ require File.dirname(__FILE__) +'/test_helper'
2
+
3
+ MindoroMarine::Acts::Tests.open_db(ENV['DB'])
4
+ class ActsAsHausdorffSpaceTest < Test::Unit::TestCase
5
+
6
+ def teardown
7
+ HausdorffSpaceTest.delete_all # irritating. I can't get user_transactional_fixtures = true to work outside the rails testing framework
8
+ end
9
+ # check all the methods are mixed in
10
+ should 'mixin methods' do
11
+ rns = HausdorffSpaceTest.new
12
+ check_method_mixins(rns)
13
+ check_class_method_mixins(HausdorffSpaceTest)
14
+ end
15
+ # add one and check that tear down removes it - hence the aa to keep them at the very front of all other tests
16
+ should "aa add one " do
17
+ r = HausdorffSpaceTest.new(:name=>'top')
18
+ assert r.save
19
+ assert_equal r.left_col_val,r.right_col_val,'for a single record left should equal right'
20
+ end
21
+
22
+ should 'aa check one' do
23
+ assert_equal 0, HausdorffSpaceTest.count, "the teardown didn't clear out the test table"
24
+ end
25
+
26
+ should 'set left and right cols' do
27
+ assert_equal('left_col',HausdorffSpaceTest.left_col_name,'class attribute left col not working')
28
+ assert_equal('right_col',HausdorffSpaceTest.right_col_name,'class attribute right col not working')
29
+ end
30
+
31
+ ####### Test Class Methods #######
32
+ context 'Class Methods - Add a Root' do
33
+ setup do
34
+ @hst = HausdorffSpaceTest.create(:name=>'top 1')
35
+ end
36
+
37
+ should 'read root and virtual root' do
38
+ read_root = HausdorffSpaceTest.root
39
+ assert_equal @hst,HausdorffSpaceTest.root,'The first root should be the saved record'
40
+ assert_equal(HausdorffSpaceTest.virtual_root, HausdorffSpaceTest.root.parent,'The virtual root should be the parent of a root node')
41
+ assert_not_nil read_root.right_col_val, 'read_root.rgt is nil'
42
+ assert_equal read_root.right_col_val, read_root.left_col_val, 'left != rgt'
43
+ assert_equal [],read_root.children, 'hs_children is not empty'
44
+ end
45
+
46
+ should 'allow other roots to be added' do
47
+ HausdorffSpaceTest.create(:name=>'top 2')
48
+ read_roots = HausdorffSpaceTest.roots
49
+ assert_equal(2,read_roots.size,'failed to get multiple roots')
50
+ assert_not_equal(read_roots[0].left_col_val,read_roots[1].left_col_val,'the roots have the same left col values')
51
+ end
52
+ end # context
53
+ # instance methods
54
+ context 'Instance Methods - Add a Root' do
55
+ setup do
56
+ @hst = HausdorffSpaceTest.create(:name=>'top 1')
57
+ end
58
+
59
+ context 'add one child to root' do
60
+ setup do
61
+ @sub_level = HausdorffSpaceTest.new(:name=>'child 1')
62
+ @hst.children << @sub_level
63
+ end
64
+
65
+ should 'link parent and child' do
66
+ validate_parent_one_child_settings(@hst,@sub_level)
67
+ assert_equal @hst,@sub_level.my_root
68
+ end
69
+
70
+ should 'read parent and child back' do
71
+ full_tree = HausdorffSpaceTest.full_tree
72
+ validate_parent_one_child_settings(full_tree,full_tree.children[0])
73
+ assert_equal full_tree,full_tree.children[0].my_root
74
+ end
75
+ end # context "add one child to root"
76
+
77
+ context 'add many children' do
78
+ setup do
79
+ (0..4).each{|n| @hst.children << HausdorffSpaceTest.new(:name=>"child #{n}") }
80
+ end
81
+
82
+ should 'have gaps between children' do
83
+ read_root = HausdorffSpaceTest.full_tree
84
+ (1..4).each do |n|
85
+ assert read_root.children[n-1].right_col_val < read_root.children[n].left_col_val,' there should be a gap between each child'
86
+ end
87
+ end
88
+
89
+ should 'move a child to an child of another' do
90
+ @hst.children.first.children << @hst.children.last
91
+ read_root = HausdorffSpaceTest.full_tree
92
+ assert_equal 4,read_root.children.size,"the child didn't get moved"
93
+ assert_equal 1,read_root.children.first.children.size,"the child didn't appear in the right place"
94
+ end
95
+ end # context 'add many children'
96
+
97
+ context 'add many grandchildren' do
98
+ setup do
99
+ (0..4).each do |n1|
100
+ new_child = HausdorffSpaceTest.new(:name=>"child #{n1}")
101
+ @hst.children << new_child
102
+ (0..4).each do |n2|
103
+ new_child.children << HausdorffSpaceTest.new(:name=>"grandchild #{n2} of child #{n1} ")
104
+ end
105
+ end
106
+ end # setup
107
+
108
+ should 'count all leaf nodes ' do
109
+ assert_equal 25,@hst.leaf_nodes.size,'error counting leaf nodes'
110
+ end
111
+
112
+ should 'move whole branches' do
113
+ @hst.children.first.children << @hst.children.last
114
+ read_root = HausdorffSpaceTest.full_tree
115
+ assert_equal 4,read_root.children.size
116
+ assert_equal 6,read_root.children.first.children.size
117
+ assert_equal 10,read_root.children.first.leaf_nodes.size
118
+ end
119
+
120
+ should 'get self and ancestors' do
121
+ saa = @hst.children.first.children.first.self_and_ancestors
122
+ assert_equal 3, saa.size, 'There should be three records'
123
+ assert_equal @hst,saa[0], 'The first ancestor should be the top level owner'
124
+ assert_equal @hst.children.first, saa[1],'The second ancestor should be child of the top level '
125
+ assert_equal @hst.children.first.children.first, saa[2],'The third ancestor should be this record'
126
+ end
127
+
128
+ should 'get only ancestors' do
129
+ a = @hst.children.first.children.first.ancestors
130
+ assert_equal 2, a.size, 'There should be two records'
131
+ assert !a.include?(@hst.children.first.children.first),' this record should not be in the array'
132
+ end
133
+
134
+ should 'get self and siblings' do
135
+ sas = @hst.children.first.children.first.self_and_siblings
136
+ assert_equal 5,sas.size, 'There should be five records in self and siblings'
137
+ sas.each do |rec|
138
+ assert_equal @hst.children.first,rec.parent,'each sibling must have the same parent'
139
+ end
140
+ end
141
+
142
+ should 'get only siblings' do
143
+ s = @hst.children.first.children.first.siblings
144
+ assert_equal 4,s.size, 'There should be four records in siblings'
145
+ assert !s.include?(@hst.children.first.children.first),'This record should not be in the array of siblings'
146
+ end
147
+
148
+ end # context 'add many grandchildren' do
149
+
150
+ end # context 'Instance Methods - Add a Root'
151
+ private ############################ PRIVATE ##########################################
152
+ def check_method_mixins(obj)
153
+ [:build_full_tree,
154
+ :before_save,
155
+ :siblings,
156
+ :left_col_val,
157
+ :right_col_val].each{|symbol| assert(obj.respond_to?(symbol),"instance method #{symbol} is missing")}
158
+ end
159
+
160
+ def check_class_method_mixins(klass)
161
+ [:root,
162
+ :roots,
163
+ :left_col_name,
164
+ :right_col_name,
165
+ :full_tree,
166
+ :virtual_root].each do |symbol|
167
+ assert(klass.respond_to?(symbol),"class method #{symbol} is missing")
168
+ end
169
+ end
170
+
171
+ def validate_parent_one_child_settings(parent,child)
172
+ assert_equal parent,child.parent,'the sublevel should have a parent'
173
+ assert_equal child,parent.children[0], 'the parent should own the child'
174
+ assert_not_equal parent.left_col_val,child.left_col_val, 'there should be a gap between the parent left and the child left'
175
+ assert_not_equal parent.right_col_val,child.right_col_val, 'there should be a gap between the parent right and the child right '
176
+ assert parent.left_col_val < child.left_col_val,' the parent left should be less that the child left'
177
+ assert parent.right_col_val > child.right_col_val, ' the parent right should be more that the child right'
178
+ assert_equal parent.left_col_val,child.lft_gap.left_col_val,'the parent left should be the left of the childs left gap'
179
+ assert_equal child.left_col_val,child.lft_gap.right_col_val,'the child left should be the right of the child left gap'
180
+ assert_equal parent.right_col_val,child.rgt_gap.right_col_val,'the parent right should be the right of the child right gap'
181
+ assert_equal child.right_col_val,child.rgt_gap.left_col_val,' the child right should the the left of the child right gap'
182
+ end
183
+ end
data/test/database.yml ADDED
@@ -0,0 +1,15 @@
1
+ mysql:
2
+ :adapter: mysql
3
+ :host: localhost
4
+ :username: gem_test
5
+ :password: gem_test_password
6
+ :database: gem_test
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :host: localhost
10
+ :username: gem_test
11
+ :password: gem_test_password
12
+ :database: gem_test
13
+ sqlite3:
14
+ :adapter: sqlite3
15
+ :dbfile: test/acts_as_hausdorff_space_gem_test.db
data/test/mysql.rb ADDED
@@ -0,0 +1,3 @@
1
+ ENV['DB'] = 'mysql'
2
+ require File.dirname(__FILE__) + '/acts_as_hausdorff_space_test'
3
+
@@ -0,0 +1,2 @@
1
+ ENV['DB'] = 'postgresql'
2
+ require File.dirname(__FILE__) + '//acts_as_hausdorff_space_test'
data/test/schema.rb ADDED
@@ -0,0 +1,12 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+
3
+ create_table "hausdorff_space_tests", :force => true do |t|
4
+ t.string "name"
5
+ t.decimal "left_col", :precision => 63, :scale => 30
6
+ t.decimal "right_col", :precision => 63, :scale => 30
7
+ end
8
+
9
+ add_index "hausdorff_space_tests", ["left_col"], :name => "lft_idx"
10
+ add_index "hausdorff_space_tests", ["right_col"], :name => "rgt_idx"
11
+
12
+ end
data/test/sqlite3.rb ADDED
@@ -0,0 +1,2 @@
1
+ ENV['DB'] = 'sqlite3'
2
+ require File.dirname(__FILE__) + '/acts_as_hausdorff_space_test'
@@ -0,0 +1,29 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require 'rubygems'
3
+ require 'activerecord'
4
+ require 'acts-as-hausdorff-space'
5
+ require 'test/unit'
6
+ require 'shoulda'
7
+
8
+ class HausdorffSpaceTest < ActiveRecord::Base
9
+ acts_as_hausdorff_space :left_column=>'left_col',:right_column=>'right_col'
10
+ end
11
+
12
+
13
+ #class Test::Unit::TestCase
14
+ # use_transactional_fixtures = true
15
+ #end
16
+
17
+ module MindoroMarine
18
+ module Acts
19
+ module Tests
20
+ def self.open_db(db_name='sqlite3')
21
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
22
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
23
+ cs = ActiveRecord::Base.establish_connection(config[db_name])
24
+ puts "Using #{cs.connection.class.to_s} adapter"
25
+ load(File.dirname(__FILE__) + '/schema.rb')
26
+ end
27
+ end
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: JohnSmall-acts-as-hausdorff-space
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - John Small
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-25 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: jds340@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - lib/acts-as-hausdorff-space.rb
31
+ - lib/acts_as_hausdorff_space.rb
32
+ - test/acts_as_hausdorff_space_test.rb
33
+ - test/database.yml
34
+ - test/mysql.rb
35
+ - test/postgresql.rb
36
+ - test/schema.rb
37
+ - test/sqlite3.rb
38
+ - test/test_helper.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/JohnSmall/acts-as-hausdorff-space
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --charset=UTF-8
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Use real numbers instead of integers for nested sets because real numbers are a Hausdorff space
65
+ test_files:
66
+ - test/schema.rb
67
+ - test/sqlite3.rb
68
+ - test/mysql.rb
69
+ - test/test_helper.rb
70
+ - test/acts_as_hausdorff_space_test.rb
71
+ - test/postgresql.rb