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 +20 -0
- data/README.rdoc +184 -0
- data/Rakefile +66 -0
- data/VERSION.yml +4 -0
- data/lib/acts-as-hausdorff-space.rb +7 -0
- data/lib/acts_as_hausdorff_space.rb +425 -0
- data/test/acts_as_hausdorff_space_test.rb +183 -0
- data/test/database.yml +15 -0
- data/test/mysql.rb +3 -0
- data/test/postgresql.rb +2 -0
- data/test/schema.rb +12 -0
- data/test/sqlite3.rb +2 -0
- data/test/test_helper.rb +29 -0
- metadata +71 -0
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,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
data/test/postgresql.rb
ADDED
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
data/test/test_helper.rb
ADDED
@@ -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
|