radix_tree 1.0.0.dev → 1.0.0.dev.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -7,8 +7,8 @@ to avoid DoS via Algorithmic Complexity Attacks.
7
7
 
8
8
  == Performance
9
9
 
10
- * 25 times slower for 10 bytes key insertion
11
- * 20 times slower for 10 bytes key retrieval
10
+ * 25 times slower for 10 bytes key, 100000 elements insertion
11
+ * 10 times slower for 10 bytes key, 100000 elements retrieval
12
12
 
13
13
 
14
14
  == TODO
data/lib/radix_tree.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # Naive implementation of Radix Tree for avoiding DoS via Algorithmic
2
2
  # Complexity Attacks.
3
3
  #
4
- # 25 times slower for 10 bytes key insertion
5
- # 20 times slower for 10 bytes key retrieval
4
+ # 25 times slower for 10 bytes key, 100000 elements insertion
5
+ # 10 times slower for 10 bytes key, 100000 elements retrieval
6
6
  #
7
7
  # TODO: Implement following features for utilizing strength of Radix Tree.
8
8
  # * find predecessor
9
9
  # * find successor
10
10
  # * find_all by start string
11
11
  # * delete_all by start string
12
+ #
12
13
  class RadixTree
13
14
  include Enumerable
14
15
 
@@ -22,8 +23,12 @@ class RadixTree
22
23
  @key, @value, @children = key, value, children
23
24
  end
24
25
 
26
+ def undefined?
27
+ @value == UNDEFINED
28
+ end
29
+
25
30
  def empty?
26
- @children.nil? and @value == UNDEFINED
31
+ @children.nil? and undefined?
27
32
  end
28
33
 
29
34
  def size
@@ -60,47 +65,70 @@ class RadixTree
60
65
  @value = value
61
66
  else
62
67
  index = head_match_length(key)
63
- if index == @key.bytesize
68
+ if index == @key.size
64
69
  push(key[index..-1], value)
65
70
  else
66
71
  split(index)
67
- store(key, value) # search again
72
+ # search again after split the node
73
+ store(key, value)
68
74
  end
69
75
  end
70
76
  end
71
77
 
72
78
  def retrieve(key)
73
- return UNDEFINED unless @children
74
- key = child_key(key)
75
- if child = find_child(key)
76
- if child.key == key
77
- child.value
78
- else
79
+ if @key == key
80
+ @value
81
+ elsif !@children
82
+ UNDEFINED
83
+ else
84
+ key = child_key(key)
85
+ if child = find_child(key)
79
86
  child.retrieve(key)
87
+ else
88
+ UNDEFINED
80
89
  end
81
- else
82
- UNDEFINED
83
90
  end
84
91
  end
85
92
 
86
93
  def delete(key)
87
- return nil unless @children
88
- key = child_key(key)
89
- if child = find_child(key)
90
- if child.key == key
91
- value, child.value = child.value, UNDEFINED
92
- else
94
+ if @key == key
95
+ value, @value = @value, UNDEFINED
96
+ value
97
+ elsif !@children
98
+ nil
99
+ else
100
+ key = child_key(key)
101
+ if child = find_child(key)
93
102
  value = child.delete(key)
103
+ if value and child.undefined?
104
+ if child.children.nil?
105
+ delete_child(child)
106
+ elsif child.children.size == 1
107
+ # pull up the grand child as a child
108
+ delete_child(child)
109
+ grand = child.children.values.first
110
+ add_child(Node.new(child.key + grand.key, grand.value, grand.children))
111
+ end
112
+ end
113
+ value
94
114
  end
95
- delete_child(child) if value and child.children.nil?
96
- value
97
115
  end
98
116
  end
99
117
 
100
- protected
101
-
102
- def value=(value)
103
- @value = value
118
+ def dump_tree(io, prefix = '', indent = '')
119
+ prefix += @key
120
+ indent += ' '
121
+ if undefined?
122
+ io << sprintf("#<%s:0x%010x %s>", self.class.name, __id__, prefix.inspect)
123
+ else
124
+ io << sprintf("#<%s:0x%010x %s> => %s", self.class.name, __id__, prefix.inspect, @value.inspect)
125
+ end
126
+ if @children
127
+ @children.each do |k, v|
128
+ io << $/ + indent
129
+ v.dump_tree(io, prefix, indent)
130
+ end
131
+ end
104
132
  end
105
133
 
106
134
  private
@@ -129,19 +157,23 @@ class RadixTree
129
157
  end
130
158
 
131
159
  def child_key(key)
132
- index = head_match_length(key)
133
- key[index..-1]
160
+ key[head_match_length(key)..-1]
134
161
  end
135
162
 
163
+ # assert: check != @key
136
164
  def head_match_length(check)
137
- 0.upto([check.bytesize, @key.bytesize].min) do |index|
138
- return index if check[index] != @key[index]
165
+ 0.upto(check.size) do |index|
166
+ if check[index] != @key[index]
167
+ return index
168
+ end
139
169
  end
140
- raise 'assert check != @key'
170
+ raise 'assert: check != @key'
141
171
  end
142
172
 
143
173
  def find_child(key)
144
- @children[key[0]] if @children
174
+ if @children
175
+ @children[key[0]]
176
+ end
145
177
  end
146
178
 
147
179
  def add_child(child)
@@ -151,14 +183,18 @@ class RadixTree
151
183
 
152
184
  def delete_child(child)
153
185
  @children.delete(child.key[0])
154
- @children = nil if @children.empty?
186
+ if @children.empty?
187
+ @children = nil
188
+ end
155
189
  end
156
190
  end
157
191
 
158
192
  DEFAULT = Object.new
159
193
 
160
194
  def initialize(default = DEFAULT, &block)
161
- raise ArgumentError, 'wrong number of arguments' if block && default != DEFAULT
195
+ if block && default != DEFAULT
196
+ raise ArgumentError, 'wrong number of arguments'
197
+ end
162
198
  @root = Node.new('')
163
199
  @default = default
164
200
  @default_proc = block
@@ -185,16 +221,16 @@ class RadixTree
185
221
  end
186
222
 
187
223
  def []=(key, value)
188
- @root.store(key, value)
224
+ @root.store(key.to_s, value)
189
225
  end
190
226
 
191
227
  def key?(key)
192
- @root.retrieve(key) != Node::UNDEFINED
228
+ @root.retrieve(key.to_s) != Node::UNDEFINED
193
229
  end
194
230
  alias has_key? key?
195
231
 
196
232
  def [](key)
197
- value = @root.retrieve(key)
233
+ value = @root.retrieve(key.to_s)
198
234
  if value == Node::UNDEFINED
199
235
  if @default != DEFAULT
200
236
  @default
@@ -209,6 +245,11 @@ class RadixTree
209
245
  end
210
246
 
211
247
  def delete(key)
212
- @root.delete(key)
248
+ @root.delete(key.to_s)
249
+ end
250
+
251
+ def dump_tree(io = '')
252
+ @root.dump_tree(io)
253
+ io
213
254
  end
214
255
  end
data/test/helper.rb CHANGED
@@ -1,2 +1,4 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
1
3
  require "test/unit"
2
4
  require "radix_tree"
@@ -1,3 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
1
2
  require File.expand_path('./helper', File.dirname(__FILE__))
2
3
 
3
4
  class TestRadixTree < Test::Unit::TestCase
@@ -7,8 +8,13 @@ class TestRadixTree < Test::Unit::TestCase
7
8
  assert_equal nil, h['def']
8
9
  end
9
10
 
10
- def test_aref_empty
11
+ def test_empty
11
12
  h = RadixTree.new
13
+ h['abc'] = 0
14
+ assert_equal nil, h['']
15
+ h[''] = 1
16
+ assert_equal 1, h['']
17
+ h.delete('')
12
18
  assert_equal nil, h['']
13
19
  end
14
20
 
@@ -138,4 +144,115 @@ class TestRadixTree < Test::Unit::TestCase
138
144
  assert_equal 0, h.size
139
145
  assert h.empty?
140
146
  end
147
+
148
+ def test_delete_compaction_middle
149
+ h = RadixTree.new
150
+ h['a'] = 1
151
+ h['abc'] = 2
152
+ h['bb'] = 3
153
+ h['abcdefghi'] = 4
154
+ h['abcdefghijzz'] = 5
155
+ h['abcdefghikzz'] = 6
156
+ assert_equal 7, h.dump_tree.split($/).size
157
+ h.delete('a')
158
+ assert_equal 6, h.dump_tree.split($/).size
159
+ h['a'] = 1
160
+ assert_equal 7, h.dump_tree.split($/).size
161
+ end
162
+
163
+ def test_delete_compaction_leaf
164
+ h = RadixTree.new
165
+ h['a'] = 1
166
+ h['abc'] = 2
167
+ h['bb'] = 3
168
+ h['abcdefghijzz'] = 4
169
+ assert_equal 5, h.dump_tree.split($/).size
170
+ h['abcdefghikzz'] = 5
171
+ assert_equal 7, h.dump_tree.split($/).size
172
+ h.delete('abcdefghijzz')
173
+ assert_equal 5, h.dump_tree.split($/).size
174
+ h['abcdefghijzz'] = 4
175
+ assert_equal 7, h.dump_tree.split($/).size
176
+ end
177
+
178
+ def test_each
179
+ h = RadixTree.new
180
+ s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
181
+ s.each do |k, v|
182
+ h[k] = v
183
+ end
184
+ values = []
185
+ h.each do |k, v|
186
+ values << [k, v]
187
+ end
188
+ assert_equal s.to_a.sort_by { |k, v| k }, values.sort_by { |k, v| k }
189
+ end
190
+
191
+ def test_keys
192
+ h = RadixTree.new
193
+ s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
194
+ s.each do |k, v|
195
+ h[k] = v
196
+ end
197
+ assert_equal s.keys.sort, h.keys.sort
198
+ end
199
+
200
+ def test_values
201
+ h = RadixTree.new
202
+ s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
203
+ s.each do |k, v|
204
+ h[k] = v
205
+ end
206
+ assert_equal s.values.sort, h.values.sort
207
+ end
208
+
209
+ def test_to_s
210
+ h = RadixTree.new
211
+ h[:abc] = 1
212
+ assert_equal 1, h["abc"]
213
+ assert_equal 1, h[:abc]
214
+ end
215
+
216
+ def test_key?
217
+ h = RadixTree.new
218
+ assert !h.key?('a')
219
+ s = { 'aa' => 1, 'ab' => 2, 'bb' => 3, 'bc' => 4, 'a' => 5, 'abc' => 6 }
220
+ s.each do |k, v|
221
+ h[k] = v
222
+ end
223
+ assert h.key?('a')
224
+ end
225
+
226
+ def test_default
227
+ assert_raise(ArgumentError) do
228
+ RadixTree.new('both') { :not_allowed }
229
+ end
230
+
231
+ h = RadixTree.new('abc')
232
+ assert_equal 'abc', h['foo']
233
+ assert_equal 'abc', h['bar']
234
+ assert h['baz'].object_id == h['qux'].object_id
235
+
236
+ h = RadixTree.new { [1, 2] }
237
+ assert_equal [1, 2], h['foo']
238
+ assert_equal [1, 2], h['bar']
239
+ assert h['baz'].object_id != h['qux'].object_id
240
+ end
241
+
242
+ if RUBY_VERSION >= '1.9.0'
243
+ def test_encoding
244
+ h = RadixTree.new
245
+ s = { 'ああ' => 1, 'あい' => 2, 'いい' => 3, 'いう' => 4, 'あ' => 5, 'あいう' => 6 }
246
+ s.each do |k, v|
247
+ h[k] = v
248
+ end
249
+ assert_equal 6, h.size
250
+ s.each do |k, v|
251
+ assert_equal v, h[k]
252
+ end
253
+ str = 'ああ'
254
+ str.force_encoding('US-ASCII')
255
+ assert_equal nil, h[str]
256
+ end
257
+ end
141
258
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: radix_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.dev
4
+ version: 1.0.0.dev.2
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors: