mongoose 0.1.0
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/README +136 -0
- data/changes.txt +2 -0
- data/example/relation_examples.rb +117 -0
- data/example/simple_examples.rb +117 -0
- data/lib/mongoose.rb +45 -0
- data/lib/mongoose/column.rb +286 -0
- data/lib/mongoose/database.rb +113 -0
- data/lib/mongoose/linear_search.rb +114 -0
- data/lib/mongoose/skiplist.rb +412 -0
- data/lib/mongoose/table.rb +568 -0
- data/lib/mongoose/util.rb +43 -0
- data/test/tc_database.rb +27 -0
- data/test/tc_table.rb +167 -0
- data/test/ts_mongoose.rb +3 -0
- metadata +59 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
module Mongoose
|
2
|
+
|
3
|
+
#-------------------------------------------------------------------------------
|
4
|
+
# Database class
|
5
|
+
#-------------------------------------------------------------------------------
|
6
|
+
class Database
|
7
|
+
attr_reader :path, :tables
|
8
|
+
|
9
|
+
#-----------------------------------------------------------------------------
|
10
|
+
# initialize
|
11
|
+
#-----------------------------------------------------------------------------
|
12
|
+
def initialize(params={})
|
13
|
+
@path = params[:path] || './'
|
14
|
+
|
15
|
+
Table.db = self
|
16
|
+
|
17
|
+
@tables = {}
|
18
|
+
|
19
|
+
Dir.foreach(@path) do |filename|
|
20
|
+
next unless File.extname(filename) == TBL_HDR_EXT
|
21
|
+
|
22
|
+
table_name = File.basename(filename, ".*").to_sym
|
23
|
+
init_table(table_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#-----------------------------------------------------------------------------
|
28
|
+
# close
|
29
|
+
#-----------------------------------------------------------------------------
|
30
|
+
def close
|
31
|
+
@tables.each_key { |tbl_class| tbl_class.close }
|
32
|
+
end
|
33
|
+
|
34
|
+
#-----------------------------------------------------------------------------
|
35
|
+
# create_table
|
36
|
+
#-----------------------------------------------------------------------------
|
37
|
+
def create_table(table_name)
|
38
|
+
raise "Table already exists!" if table_exists?(table_name)
|
39
|
+
|
40
|
+
class_name = get_class_name_from_table_name(table_name)
|
41
|
+
|
42
|
+
tbl_header = {}
|
43
|
+
tbl_header[:table_name] = table_name
|
44
|
+
tbl_header[:class_name] = class_name
|
45
|
+
tbl_header[:last_id_used] = 0
|
46
|
+
tbl_header[:deleted_recs_counter] = 0
|
47
|
+
tbl_header[:columns] = []
|
48
|
+
tbl_header[:columns] << { :name => :id, :data_type => :integer,
|
49
|
+
:class => IDColumn.to_s }
|
50
|
+
|
51
|
+
File.open(File.join(@path, table_name.to_s + TBL_HDR_EXT), 'w') do |f|
|
52
|
+
YAML.dump(tbl_header, f)
|
53
|
+
end
|
54
|
+
|
55
|
+
fptr = File.open(File.join(@path, table_name.to_s + TBL_EXT), 'w')
|
56
|
+
fptr.close
|
57
|
+
|
58
|
+
init_table(table_name)
|
59
|
+
|
60
|
+
yield Object.const_get(class_name) if block_given?
|
61
|
+
end
|
62
|
+
|
63
|
+
#-----------------------------------------------------------------------------
|
64
|
+
# drop_table
|
65
|
+
#-----------------------------------------------------------------------------
|
66
|
+
def drop_table(table_name)
|
67
|
+
class_name = get_class_name_from_table_name(table_name)
|
68
|
+
|
69
|
+
@tables[Object.const_get(class_name)][:columns].each do |c|
|
70
|
+
if c.indexed?
|
71
|
+
File.delete(c.index_file_name) if File.exists?(c.index_file_name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
File.delete(File.join(@path, table_name.to_s + TBL_HDR_EXT))
|
76
|
+
File.delete(File.join(@path, table_name.to_s + TBL_EXT)) if \
|
77
|
+
File.exists?(File.join(@path, table_name.to_s + TBL_EXT))
|
78
|
+
|
79
|
+
@tables.delete(Object.const_get(class_name))
|
80
|
+
end
|
81
|
+
|
82
|
+
#-----------------------------------------------------------------------------
|
83
|
+
# table_exists?
|
84
|
+
#-----------------------------------------------------------------------------
|
85
|
+
def table_exists?(table_name)
|
86
|
+
return File.exists?(File.join(@path, table_name.to_s + TBL_HDR_EXT))
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
#-----------------------------------------------------------------------------
|
92
|
+
# init_table
|
93
|
+
#-----------------------------------------------------------------------------
|
94
|
+
def init_table(table_name)
|
95
|
+
class_name = get_class_name_from_table_name(table_name)
|
96
|
+
|
97
|
+
@tables[Object.full_const_get(class_name)] = { :class_name => class_name,
|
98
|
+
:table_name => table_name, :columns => [], :query => [],
|
99
|
+
:last_id_used => nil, :deleted_recs_counter => nil }
|
100
|
+
|
101
|
+
Object.full_const_get(class_name).init_table
|
102
|
+
end
|
103
|
+
|
104
|
+
#-----------------------------------------------------------------------------
|
105
|
+
# get_class_name_from_table_name
|
106
|
+
#-----------------------------------------------------------------------------
|
107
|
+
def get_class_name_from_table_name(table_name)
|
108
|
+
return table_name.to_s.split('_').inject('') { |full_name, word|
|
109
|
+
full_name + word.capitalize }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Mongoose
|
2
|
+
|
3
|
+
class LinearSearch
|
4
|
+
def initialize(col)
|
5
|
+
@col = col
|
6
|
+
end
|
7
|
+
|
8
|
+
def search_table(&search)
|
9
|
+
col_index = nil
|
10
|
+
@col.tbl_class.columns.each_with_index do |c,i|
|
11
|
+
if c.name == @col.name
|
12
|
+
col_index = i
|
13
|
+
break
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
result = []
|
18
|
+
|
19
|
+
@col.tbl_class.with_table do |fptr|
|
20
|
+
begin
|
21
|
+
while true
|
22
|
+
fpos = fptr.tell
|
23
|
+
|
24
|
+
rec_arr = Marshal.load(fptr)
|
25
|
+
|
26
|
+
next if rec_arr[0]
|
27
|
+
|
28
|
+
value = rec_arr[col_index+1]
|
29
|
+
|
30
|
+
if search.call(value)
|
31
|
+
result << rec_arr[1]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
rescue EOFError
|
35
|
+
end
|
36
|
+
return result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def >(other)
|
41
|
+
return search_table do |table_value|
|
42
|
+
if table_value.nil?
|
43
|
+
false
|
44
|
+
else
|
45
|
+
table_value > other
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def >=(other)
|
51
|
+
return search_table do |table_value|
|
52
|
+
if table_value.nil?
|
53
|
+
false
|
54
|
+
else
|
55
|
+
table_value >= other
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def <(other)
|
61
|
+
return search_table do |table_value|
|
62
|
+
if table_value.nil?
|
63
|
+
false
|
64
|
+
else
|
65
|
+
table_value < other
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def <=(other)
|
71
|
+
return search_table do |table_value|
|
72
|
+
if table_value.nil?
|
73
|
+
false
|
74
|
+
else
|
75
|
+
table_value <= other
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def ==(other)
|
81
|
+
return search_table do |table_value|
|
82
|
+
if table_value.nil?
|
83
|
+
false
|
84
|
+
else
|
85
|
+
table_value == other
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def one_of(*other)
|
91
|
+
return search_table do |table_value|
|
92
|
+
other.include?(table_value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def between(search_start, search_end, start_inclusive=false,
|
97
|
+
end_inclusive=false)
|
98
|
+
return search_table do |table_value|
|
99
|
+
if table_value < search_start
|
100
|
+
false
|
101
|
+
elsif table_value == search_start and not start_inclusive
|
102
|
+
false
|
103
|
+
elsif table_value == search_end and not end_inclusive
|
104
|
+
false
|
105
|
+
elsif table_value > search_end
|
106
|
+
false
|
107
|
+
else
|
108
|
+
true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,412 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
module Mongoose
|
4
|
+
|
5
|
+
#---------------------------------------------------------------------------
|
6
|
+
# SkipList Class
|
7
|
+
#---------------------------------------------------------------------------
|
8
|
+
class SkipList
|
9
|
+
VERSION = "0.1"
|
10
|
+
MAX_LEVEL = 31
|
11
|
+
OPTIMAL_PROBABILITY = 0.25
|
12
|
+
|
13
|
+
attr_reader :size
|
14
|
+
|
15
|
+
#-----------------------------------------------------------------------
|
16
|
+
# initialize
|
17
|
+
#-----------------------------------------------------------------------
|
18
|
+
#++
|
19
|
+
# Create a new skip list instance.
|
20
|
+
#
|
21
|
+
def initialize
|
22
|
+
@size = 0
|
23
|
+
|
24
|
+
# Create header and footer nodes.
|
25
|
+
@footer = FooterNode.new
|
26
|
+
@header = HeaderNode.new
|
27
|
+
|
28
|
+
# Point all header.forward references to footer node.
|
29
|
+
0.upto(MAX_LEVEL) { |i| @header.forward[i] = @footer }
|
30
|
+
|
31
|
+
# This attribute will hold the actual level of the skip list.
|
32
|
+
@level = 0
|
33
|
+
end
|
34
|
+
|
35
|
+
#-----------------------------------------------------------------------
|
36
|
+
# store
|
37
|
+
#-----------------------------------------------------------------------
|
38
|
+
#++
|
39
|
+
# If key not found, will insert new record, otherwise will update value
|
40
|
+
# of existing record.
|
41
|
+
#
|
42
|
+
def store(search_key, new_value)
|
43
|
+
# This array will be used to determine which records need their
|
44
|
+
# forward array re-adjusted after a new node is created.
|
45
|
+
update = []
|
46
|
+
|
47
|
+
# Start off at the header node.
|
48
|
+
x = @header
|
49
|
+
|
50
|
+
# Starting at the current highest level of the skip list, walk from
|
51
|
+
# left to right at that level, until you see that the next node's
|
52
|
+
# key is greater than the key you are searching for. When this
|
53
|
+
# happend, you want to add the current node you are on to the list
|
54
|
+
# of nodes that need to have their forward arrays updated. Next,
|
55
|
+
# you want to drop down a level on the current node and start
|
56
|
+
# walking forward again, until you again see that the next node's
|
57
|
+
# key is bigger than the search key. In this way, you are walking
|
58
|
+
# through the skip list, constantly moving to the right and moving
|
59
|
+
# down, until you reach level 0 and are on the node whose key is
|
60
|
+
# either equal to the search key (in which case an update will take
|
61
|
+
# place), or the highest key in the list that is still lower than
|
62
|
+
# the search key (in which case, you have found the place to do an
|
63
|
+
# insert).
|
64
|
+
@level.downto(0) do |i|
|
65
|
+
while x.forward[i].key < search_key
|
66
|
+
x = x.forward[i]
|
67
|
+
end
|
68
|
+
update[i] = x
|
69
|
+
end
|
70
|
+
|
71
|
+
x = x.forward[0]
|
72
|
+
|
73
|
+
# If the search key was found, simply update the value of the node.
|
74
|
+
if x.key == search_key
|
75
|
+
x.value << new_value unless x.value.include?(new_value)
|
76
|
+
# If this is an insert, determine the number of levels it will have
|
77
|
+
# using a random number. This is what keeps the skip list balanced.
|
78
|
+
else
|
79
|
+
lvl = random_level
|
80
|
+
|
81
|
+
# If the new level is higher than the actual current level, we
|
82
|
+
# need to make sure that the header node gets updated at these
|
83
|
+
# levels. Then, we set the actual current level equal to the
|
84
|
+
# new level.
|
85
|
+
if lvl > @level
|
86
|
+
(@level + 1).upto(lvl) { |i| update[i] = @header }
|
87
|
+
@level = lvl
|
88
|
+
end
|
89
|
+
|
90
|
+
# Create a new node.
|
91
|
+
x = Node.new(lvl, search_key, [new_value])
|
92
|
+
|
93
|
+
# Now, we need to update all of the nodes that will be affected
|
94
|
+
# by the insertion of the new node. These are nodes whose
|
95
|
+
# forward array either will point to the new node and the new
|
96
|
+
# node itself.
|
97
|
+
0.upto(lvl) do |i|
|
98
|
+
x.forward[i] = update[i].forward[i]
|
99
|
+
update[i].forward[i] = x
|
100
|
+
end
|
101
|
+
|
102
|
+
# Increment the size attribute by one.
|
103
|
+
@size += 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def remove(search_key, value)
|
108
|
+
update = []
|
109
|
+
|
110
|
+
x = @header
|
111
|
+
|
112
|
+
@level.downto(0) do |i|
|
113
|
+
while x.forward[i].key < search_key
|
114
|
+
x = x.forward[i]
|
115
|
+
end
|
116
|
+
update[i] = x
|
117
|
+
end
|
118
|
+
|
119
|
+
x = x.forward[0]
|
120
|
+
|
121
|
+
if x.key == search_key
|
122
|
+
x.value.delete(value)
|
123
|
+
|
124
|
+
if x.value.empty?
|
125
|
+
0.upto(@level) do |i|
|
126
|
+
break unless update[i].forward[i] == x
|
127
|
+
update[i].forward[i] = x.forward[i]
|
128
|
+
end
|
129
|
+
|
130
|
+
while @level > 0 and @header.forward[@level] == @footer
|
131
|
+
@level -= 1
|
132
|
+
end
|
133
|
+
|
134
|
+
@size -= 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def search(search_key)
|
140
|
+
result = []
|
141
|
+
x = @header
|
142
|
+
|
143
|
+
@level.downto(0) do |i|
|
144
|
+
while x.forward[i].key < search_key
|
145
|
+
x = x.forward[i]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
x = x.forward[0]
|
150
|
+
|
151
|
+
return x.value if x.key == search_key
|
152
|
+
end
|
153
|
+
|
154
|
+
def one_of(*other)
|
155
|
+
result = []
|
156
|
+
other.each do |o|
|
157
|
+
result.concat(search(o))
|
158
|
+
end
|
159
|
+
return result
|
160
|
+
end
|
161
|
+
|
162
|
+
def ==(other)
|
163
|
+
return search(other)
|
164
|
+
end
|
165
|
+
|
166
|
+
def >(search_key)
|
167
|
+
result = []
|
168
|
+
x = @header
|
169
|
+
|
170
|
+
@level.downto(0) do |i|
|
171
|
+
while x.forward[i].key < search_key
|
172
|
+
x = x.forward[i]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
x = x.forward[0]
|
177
|
+
|
178
|
+
x = x.forward[0] if x.key == search_key
|
179
|
+
|
180
|
+
while x != @footer
|
181
|
+
result.concat(x.value)
|
182
|
+
x = x.forward[0]
|
183
|
+
end
|
184
|
+
|
185
|
+
return result
|
186
|
+
end
|
187
|
+
|
188
|
+
def >=(search_key)
|
189
|
+
result = []
|
190
|
+
x = @header
|
191
|
+
|
192
|
+
@level.downto(0) do |i|
|
193
|
+
while x.forward[i].key < search_key
|
194
|
+
x = x.forward[i]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
x = x.forward[0]
|
199
|
+
|
200
|
+
while x != @footer
|
201
|
+
result.concat(x.value)
|
202
|
+
x = x.forward[0]
|
203
|
+
end
|
204
|
+
|
205
|
+
return result
|
206
|
+
end
|
207
|
+
|
208
|
+
def <(search_key)
|
209
|
+
result = []
|
210
|
+
x = @header
|
211
|
+
|
212
|
+
x = x.forward[0]
|
213
|
+
|
214
|
+
while x != @footer and x.key < search_key
|
215
|
+
result.concat(x.value)
|
216
|
+
x = x.forward[0]
|
217
|
+
end
|
218
|
+
|
219
|
+
return result
|
220
|
+
end
|
221
|
+
|
222
|
+
def <=(search_key)
|
223
|
+
result = []
|
224
|
+
x = @header
|
225
|
+
|
226
|
+
x = x.forward[0]
|
227
|
+
|
228
|
+
while x != @footer and x.key <= search_key
|
229
|
+
result.concat(x.value)
|
230
|
+
x = x.forward[0]
|
231
|
+
end
|
232
|
+
|
233
|
+
return result
|
234
|
+
end
|
235
|
+
|
236
|
+
def [](search_key)
|
237
|
+
return search(search_key)
|
238
|
+
end
|
239
|
+
|
240
|
+
def between(search_start, search_end, start_inclusive=false,
|
241
|
+
end_inclusive=false)
|
242
|
+
result = []
|
243
|
+
x = @header
|
244
|
+
|
245
|
+
@level.downto(0) do |i|
|
246
|
+
while x.forward[i].key < search_start
|
247
|
+
x = x.forward[i]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
x = x.forward[0]
|
252
|
+
|
253
|
+
x = x.forward[0] if x.key == search_start and not start_inclusive
|
254
|
+
|
255
|
+
while x != @footer and x.key < search_end
|
256
|
+
result.concat(x.value)
|
257
|
+
x = x.forward[0]
|
258
|
+
end
|
259
|
+
|
260
|
+
result = result.concat(x.value) if x.key == search_end and end_inclusive
|
261
|
+
return result
|
262
|
+
end
|
263
|
+
|
264
|
+
def each
|
265
|
+
x = @header.forward[0]
|
266
|
+
|
267
|
+
while x != @footer
|
268
|
+
yield x.key, x.value
|
269
|
+
x = x.forward[0]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def load(key, value)
|
274
|
+
# This array will be used to determine which records need their
|
275
|
+
# forward array re-adjusted after a new node is created.
|
276
|
+
update = []
|
277
|
+
|
278
|
+
# Start off at the header node.
|
279
|
+
x = @header
|
280
|
+
|
281
|
+
# Starting at the current highest level of the skip list, walk from
|
282
|
+
# left to right at that level, until you reach the footer node. When
|
283
|
+
# this happens, you want to add the current node you are on to the list
|
284
|
+
# of nodes that need to have their forward arrays updated. Next,
|
285
|
+
# you want to drop down a level on the current node and start
|
286
|
+
# walking forward again, until you again see that the next node is the
|
287
|
+
# footer node. In this way, you are walking through the skip list,
|
288
|
+
# constantly moving to the right and moving down, until you reach level
|
289
|
+
# 0 and are on the node whose key is the highest key in the list (in
|
290
|
+
# which case, you have found the place to do an insert).
|
291
|
+
@level.downto(0) do |i|
|
292
|
+
while x.forward[i] != @footer
|
293
|
+
x = x.forward[i]
|
294
|
+
end
|
295
|
+
update[i] = x
|
296
|
+
end
|
297
|
+
|
298
|
+
lvl = random_level
|
299
|
+
|
300
|
+
# If the new level is higher than the actual current level, we
|
301
|
+
# need to make sure that the header node gets updated at these
|
302
|
+
# levels. Then, we set the actual current level equal to the
|
303
|
+
# new level.
|
304
|
+
if lvl > @level
|
305
|
+
(@level + 1).upto(lvl) { |i| update[i] = @header }
|
306
|
+
@level = lvl
|
307
|
+
end
|
308
|
+
|
309
|
+
# Create a new node.
|
310
|
+
x = Node.new(lvl, key, value)
|
311
|
+
|
312
|
+
# Now, we need to update all of the nodes that will be affected
|
313
|
+
# by the insertion of the new node. These are nodes whose
|
314
|
+
# forward array either will point to the new node and the new
|
315
|
+
# node itself.
|
316
|
+
0.upto(lvl) do |i|
|
317
|
+
x.forward[i] = update[i].forward[i]
|
318
|
+
update[i].forward[i] = x
|
319
|
+
end
|
320
|
+
|
321
|
+
# Increment the size attribute by one.
|
322
|
+
@size += 1
|
323
|
+
end
|
324
|
+
|
325
|
+
def dump
|
326
|
+
x = @header
|
327
|
+
puts '---------------- Header -----------------------------'
|
328
|
+
puts x
|
329
|
+
x.forward.each_with_index do |f,i|
|
330
|
+
puts '**** Forward entry %d ****' % i
|
331
|
+
puts f.key
|
332
|
+
puts f.value unless f.is_a?(FooterNode)
|
333
|
+
end
|
334
|
+
puts '---------------- End Header -------------------------'
|
335
|
+
|
336
|
+
while not x.forward[0].is_a?(FooterNode)
|
337
|
+
x = x.forward[0]
|
338
|
+
puts '---------------- Node -------------------------'
|
339
|
+
puts x
|
340
|
+
puts x.key
|
341
|
+
puts x.value
|
342
|
+
puts x.lvl
|
343
|
+
x.forward.each_with_index do |f,i|
|
344
|
+
puts '**** Forward entry %d ****' % i
|
345
|
+
puts f.key
|
346
|
+
puts f.value unless f.is_a?(FooterNode)
|
347
|
+
end
|
348
|
+
puts '---------------- End Node ---------------------'
|
349
|
+
end
|
350
|
+
|
351
|
+
puts '--------------------- Footer ---------------------'
|
352
|
+
pp x.forward[0]
|
353
|
+
puts '--------------------- End Footer -----------------'
|
354
|
+
end
|
355
|
+
|
356
|
+
private
|
357
|
+
|
358
|
+
def random_level
|
359
|
+
lvl = 0
|
360
|
+
|
361
|
+
while rand < OPTIMAL_PROBABILITY and lvl < MAX_LEVEL
|
362
|
+
lvl += 1
|
363
|
+
end
|
364
|
+
|
365
|
+
return lvl
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
class HeaderNode
|
370
|
+
attr_accessor :forward
|
371
|
+
attr_reader :key
|
372
|
+
|
373
|
+
def initialize
|
374
|
+
@forward = []
|
375
|
+
@key = HeaderKey.new
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
class HeaderKey
|
380
|
+
def ==(other)
|
381
|
+
false
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
class FooterNode
|
386
|
+
attr_reader :key
|
387
|
+
|
388
|
+
def initialize
|
389
|
+
@key = FooterKey.new
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
class FooterKey
|
394
|
+
def <(other)
|
395
|
+
return false
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
|
400
|
+
class Node
|
401
|
+
attr_accessor :forward, :value
|
402
|
+
attr_reader :forward, :key, :lvl
|
403
|
+
|
404
|
+
def initialize(lvl, key, value)
|
405
|
+
@lvl = lvl
|
406
|
+
@forward = []
|
407
|
+
@key = key
|
408
|
+
@value = value
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
end
|