radix_tree 1.0.0.dev → 1.0.0.dev.2

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 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: