JohnSmall-acts-as-hausdorff-space 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +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
|