JohnSmall-acts-as-hausdorff-space 0.1.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) 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