rambling-trie 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/lib/rambling/trie.rb +21 -9
  4. data/lib/rambling/trie/compressed_node.rb +112 -0
  5. data/lib/rambling/trie/compression.rb +13 -0
  6. data/lib/rambling/trie/compressor.rb +30 -31
  7. data/lib/rambling/trie/{root.rb → container.rb} +41 -38
  8. data/lib/rambling/trie/enumerable.rb +11 -7
  9. data/lib/rambling/trie/missing_node.rb +1 -1
  10. data/lib/rambling/trie/node.rb +25 -22
  11. data/lib/rambling/trie/plain_text_reader.rb +1 -1
  12. data/lib/rambling/trie/raw_node.rb +90 -0
  13. data/lib/rambling/trie/tasks/helpers/path.rb +13 -0
  14. data/lib/rambling/trie/tasks/helpers/time.rb +7 -0
  15. data/lib/rambling/trie/tasks/performance.rb +10 -91
  16. data/lib/rambling/trie/tasks/performance/all.rb +4 -0
  17. data/lib/rambling/trie/tasks/performance/benchmark.rb +172 -0
  18. data/lib/rambling/trie/tasks/performance/directory.rb +11 -0
  19. data/lib/rambling/trie/tasks/performance/profile/call_tree.rb +132 -0
  20. data/lib/rambling/trie/tasks/performance/profile/memory.rb +116 -0
  21. data/lib/rambling/trie/version.rb +1 -1
  22. data/rambling-trie.gemspec +6 -4
  23. data/spec/integration/rambling/trie_spec.rb +63 -9
  24. data/spec/lib/rambling/trie/compressed_node_spec.rb +35 -0
  25. data/spec/lib/rambling/trie/compressor_spec.rb +31 -0
  26. data/spec/lib/rambling/trie/container_spec.rb +470 -0
  27. data/spec/lib/rambling/trie/enumerable_spec.rb +2 -2
  28. data/spec/lib/rambling/trie/inspector_spec.rb +21 -14
  29. data/spec/lib/rambling/trie/node_spec.rb +72 -209
  30. data/spec/lib/rambling/trie/raw_node_spec.rb +377 -0
  31. data/spec/lib/rambling/trie_spec.rb +46 -25
  32. metadata +57 -16
  33. data/lib/rambling/trie/branches.rb +0 -149
  34. data/spec/lib/rambling/trie/branches_spec.rb +0 -52
  35. data/spec/lib/rambling/trie/root_spec.rb +0 -376
@@ -0,0 +1,116 @@
1
+ require_relative '../../helpers/path'
2
+ require_relative '../../helpers/time'
3
+
4
+ namespace :performance do
5
+ namespace :profile do
6
+ include Helpers::Path
7
+ include Helpers::Time
8
+
9
+ def with_gc_stats
10
+ puts "Live objects before - #{GC.stat[:heap_live_slots]}"
11
+ yield
12
+ puts "Live objects after - #{GC.stat[:heap_live_slots]}"
13
+ end
14
+
15
+ def memory_profile name
16
+ puts
17
+ puts name
18
+
19
+ result = MemoryProfiler.report allow_files: 'lib/rambling/trie', ignore_files: 'tasks/performance' do
20
+ yield
21
+ end
22
+
23
+ dir = path 'reports', Rambling::Trie::VERSION, 'memory', time
24
+ FileUtils.mkdir_p dir
25
+ result.pretty_print to_file: File.join(dir, name)
26
+ end
27
+
28
+ def dictionary
29
+ dictionary = path 'assets', 'dictionaries', 'words_with_friends.txt'
30
+ end
31
+
32
+ namespace :memory do
33
+ task creation: ['performance:directory'] do
34
+ puts 'Generating memory profiling reports for creation...'
35
+
36
+ trie = nil
37
+
38
+ memory_profile "memory-profile-new-trie" do
39
+ with_gc_stats { trie = Rambling::Trie.create dictionary }
40
+ end
41
+ end
42
+
43
+ task compression: ['performance:directory'] do
44
+ trie = Rambling::Trie.create dictionary
45
+
46
+ memory_profile "memory-profile-trie-and-compress" do
47
+ with_gc_stats { trie.compress! }
48
+ end
49
+
50
+ with_gc_stats { GC.start }
51
+ end
52
+
53
+ task lookups: ['performance:directory'] do
54
+ trie = Rambling::Trie.create dictionary
55
+ words = %w(hi help beautiful impressionism anthropological)
56
+
57
+ tries = [ trie, trie.clone.compress! ]
58
+
59
+ tries.each do |trie|
60
+ times = 10
61
+
62
+ name = "memory-profile-searching-#{trie.compressed? ? 'compressed' : 'uncompressed'}-trie-word"
63
+ memory_profile name do
64
+ with_gc_stats do
65
+ words.each do |word|
66
+ times.times do
67
+ trie.word? word
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ name = "memory-profile-searching-#{trie.compressed? ? 'compressed' : 'uncompressed'}-trie-partial-word"
74
+ memory_profile name do
75
+ with_gc_stats do
76
+ words.each do |word|
77
+ times.times do
78
+ trie.partial_word? word
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ task scans: ['performance:directory'] do
87
+ words = {
88
+ hi: 1,
89
+ help: 100,
90
+ beautiful: 100,
91
+ impressionism: 200,
92
+ anthropological: 200,
93
+ }
94
+
95
+ trie = Rambling::Trie.create dictionary
96
+ [ trie, trie.clone.compress! ].each do |trie|
97
+ name = "memory-profile-#{trie.compressed? ? 'compressed' : 'uncompressed'}-trie-scan"
98
+ memory_profile name do
99
+ words.each do |word, times|
100
+ times.times do
101
+ trie.scan(word.to_s).size
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ task all: [
109
+ 'performance:profile:memory:creation',
110
+ 'performance:profile:memory:compression',
111
+ 'performance:profile:memory:lookups',
112
+ 'performance:profile:memory:scans'
113
+ ]
114
+ end
115
+ end
116
+ end
@@ -1,6 +1,6 @@
1
1
  module Rambling
2
2
  module Trie
3
3
  # Current version of the rambling-trie.
4
- VERSION = '0.8.1'
4
+ VERSION = '0.9.0'
5
5
  end
6
6
  end
@@ -20,9 +20,11 @@ Gem::Specification.new do |gem|
20
20
  gem.version = Rambling::Trie::VERSION
21
21
  gem.platform = Gem::Platform::RUBY
22
22
 
23
- gem.add_development_dependency 'rspec', '~> 3.4'
24
- gem.add_development_dependency 'rake', '~> 10.5'
25
- gem.add_development_dependency 'ruby-prof', '~> 0.15.2'
26
- gem.add_development_dependency 'yard', '~> 0.8.7'
23
+ gem.add_development_dependency 'rspec', '~> 3.5'
24
+ gem.add_development_dependency 'rake', '~> 12.0'
25
+ gem.add_development_dependency 'ruby-prof', '~> 0.16.2'
26
+ gem.add_development_dependency 'memory_profiler', '~> 0.9.7'
27
+ gem.add_development_dependency 'benchmark-ips', '~> 2.7.2'
28
+ gem.add_development_dependency 'yard', '~> 0.9.5'
27
29
  gem.add_development_dependency 'redcarpet', '~> 3.3.4'
28
30
  end
@@ -1,20 +1,74 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Rambling::Trie do
4
- describe 'when a filepath is provided' do
5
- let(:filepath) { File.join ::SPEC_ROOT, 'assets', 'test_words.txt' }
6
- let(:words) { File.readlines(filepath).map &:chomp }
7
- subject { Rambling::Trie.create filepath }
4
+ shared_examples_for 'a compressable trie' do
5
+ context 'and the trie is not compressed' do
6
+ it_behaves_like 'a trie data structure'
8
7
 
8
+ it 'does not alter the input' do
9
+ word = 'string'
10
+ trie.add word
11
+
12
+ expect(word).to eq 'string'
13
+ end
14
+
15
+ it 'is marked as not compressed' do
16
+ expect(trie).not_to be_compressed
17
+ end
18
+ end
19
+
20
+ context 'and the trie is compressed' do
21
+ before { trie.compress! }
22
+
23
+ it_behaves_like 'a trie data structure'
24
+
25
+ it 'is marked as compressed' do
26
+ expect(trie).to be_compressed
27
+ end
28
+ end
29
+ end
30
+
31
+ shared_examples_for 'a trie data structure' do
9
32
  it 'contains all the words from the file' do
10
- words.each { |word| expect(subject).to include word }
33
+ words.each do |word|
34
+ expect(trie).to include word
35
+ expect(trie.word? word).to be true
36
+ end
11
37
  end
12
38
 
13
- describe 'and the trie is compressed' do
14
- it 'still contains all the words from the file' do
15
- subject.compress!
16
- words.each { |word| expect(subject).to include word }
39
+ it 'matches the start of all the words from the file' do
40
+ words.each do |word|
41
+ expect(trie.match? word).to be true
42
+ expect(trie.match? word[0..-2]).to be true
43
+ expect(trie.partial_word? word).to be true
44
+ expect(trie.partial_word? word[0..-2]).to be true
17
45
  end
18
46
  end
47
+
48
+ it 'allows iterating over all the words' do
49
+ expect(trie.to_a.sort).to eq words.sort
50
+ end
51
+ end
52
+
53
+ describe 'with words provided directly' do
54
+ it_behaves_like 'a compressable trie' do
55
+ let(:words) { %w[a couple of words for our full trie integration test] }
56
+ let(:trie) { Rambling::Trie.create }
57
+
58
+ before do
59
+ words.each do |word|
60
+ trie << word
61
+ trie.add word
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'with words from a file' do
68
+ it_behaves_like 'a compressable trie' do
69
+ let(:filepath) { File.join ::SPEC_ROOT, 'assets', 'test_words.txt' }
70
+ let(:words) { File.readlines(filepath).map &:chomp }
71
+ let(:trie) { Rambling::Trie.create filepath }
72
+ end
19
73
  end
20
74
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rambling::Trie::CompressedNode do
4
+ let(:node) { Rambling::Trie::CompressedNode.new }
5
+
6
+ describe '#compressed?' do
7
+ it 'returns true' do
8
+ expect(node).to be_compressed
9
+ end
10
+ end
11
+
12
+ describe '.new' do
13
+ context 'with no parent' do
14
+ let(:node) { Rambling::Trie::CompressedNode.new }
15
+
16
+ it 'is marked as root' do
17
+ expect(node).to be_root
18
+ end
19
+ end
20
+
21
+ context 'with a specified' do
22
+ let(:node) { Rambling::Trie::CompressedNode.new double(:root) }
23
+
24
+ it 'is not marked as root' do
25
+ expect(node).not_to be_root
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#add' do
31
+ it 'raises an error' do
32
+ expect { node.add 'restaurant' }.to raise_error Rambling::Trie::InvalidOperation
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rambling::Trie::Compressor do
4
+ let(:compressor) { Rambling::Trie::Compressor.new }
5
+
6
+ describe '#compress' do
7
+ let(:words) { %w(a few words hello hell) }
8
+ let(:root) do
9
+ Rambling::Trie::RawNode.new
10
+ end
11
+
12
+ before do
13
+ words.each { |w| root.add w.clone }
14
+ end
15
+
16
+ it 'generates a new root with the words from the passed root' do
17
+ new_root = compressor.compress root
18
+
19
+ expect(words).not_to be_empty
20
+ words.each do |word|
21
+ expect(new_root).to include word
22
+ end
23
+ end
24
+
25
+ it 'compresses the new root' do
26
+ new_root = compressor.compress root
27
+
28
+ expect(new_root.children_tree.keys).to eq %i(a few words hell)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,470 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rambling::Trie::Container do
4
+ let(:container) { Rambling::Trie::Container.new root, compressor }
5
+ let(:compressor) { Rambling::Trie::Compressor.new }
6
+ let(:root) { Rambling::Trie::RawNode.new }
7
+
8
+ describe '.new' do
9
+ context 'without a specified root' do
10
+ before do
11
+ allow(Rambling::Trie::RawNode).to receive(:new)
12
+ .and_return root
13
+ end
14
+
15
+ it 'initializes an empty trie root node' do
16
+ Rambling::Trie::Container.new
17
+ expect(Rambling::Trie::RawNode).to have_received :new
18
+ end
19
+ end
20
+
21
+ context 'without a specified compressor' do
22
+ before do
23
+ allow(Rambling::Trie::Compressor).to receive(:new)
24
+ .and_return compressor
25
+ end
26
+
27
+ it 'initializes a compressor' do
28
+ Rambling::Trie::Container.new
29
+ expect(Rambling::Trie::Compressor).to have_received :new
30
+ end
31
+ end
32
+
33
+ context 'with a block' do
34
+ it 'yields the container' do
35
+ yielded_container = nil
36
+
37
+ container = Rambling::Trie::Container.new root do |container|
38
+ yielded_container = container
39
+ end
40
+
41
+ expect(yielded_container).to eq container
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '#add' do
47
+ let(:clone) { double :clone }
48
+ let(:word) { double :word, clone: clone }
49
+
50
+ before do
51
+ allow(root).to receive(:add)
52
+ end
53
+
54
+ it 'clones the original word' do
55
+ container.add word
56
+ expect(root).to have_received(:add).with clone
57
+ end
58
+ end
59
+
60
+ describe '#compress!' do
61
+ let(:node) { double :node, add: nil, compressed?: false }
62
+
63
+ before do
64
+ allow(compressor).to receive(:compress).and_return node
65
+ allow(root).to receive(:add)
66
+ end
67
+
68
+ it 'compresses the trie using the compressor' do
69
+ container.compress!
70
+
71
+ expect(compressor).to have_received(:compress)
72
+ .with root
73
+ end
74
+
75
+ it 'changes to the root returned by the compressor' do
76
+ container.compress!
77
+ container.add 'word'
78
+
79
+ expect(root).not_to have_received :add
80
+ expect(node).to have_received :add
81
+ end
82
+
83
+ it 'returns itself' do
84
+ expect(container.compress!).to eq container
85
+ end
86
+
87
+ it 'does not compress multiple times' do
88
+ container.compress!
89
+ allow(node).to receive(:compressed?).and_return(true)
90
+
91
+ container.compress!
92
+ expect(compressor).to have_received(:compress).once
93
+ end
94
+ end
95
+
96
+ describe '#word?' do
97
+ context 'for an uncompressed root' do
98
+ let(:root) do
99
+ double :root,
100
+ compressed?: false,
101
+ word?: nil
102
+ end
103
+
104
+ it 'calls the root with the word characters' do
105
+ container.word? 'words'
106
+ expect(root).to have_received(:word?).with %w(w o r d s)
107
+ end
108
+ end
109
+
110
+ context 'for a compressed root' do
111
+ let(:root) do
112
+ double :root,
113
+ compressed?: true,
114
+ word?: nil
115
+ end
116
+
117
+ it 'calls the root with the full word' do
118
+ container.word? 'words'
119
+ expect(root).to have_received(:word?).with %w(w o r d s)
120
+ end
121
+ end
122
+ end
123
+
124
+ describe '#partial_word?' do
125
+ context 'for an uncompressed root' do
126
+ let(:root) do
127
+ double :root,
128
+ compressed?: false,
129
+ partial_word?: nil
130
+ end
131
+
132
+ it 'calls the root with the word characters' do
133
+ container.partial_word? 'words'
134
+ expect(root).to have_received(:partial_word?).with %w(w o r d s)
135
+ end
136
+ end
137
+
138
+ context 'for a compressed root' do
139
+ let(:root) do
140
+ double :root,
141
+ compressed?: true,
142
+ partial_word?: nil
143
+ end
144
+
145
+ it 'calls the root with the word characters' do
146
+ container.partial_word? 'words'
147
+ expect(root).to have_received(:partial_word?).with %w(w o r d s)
148
+ end
149
+ end
150
+ end
151
+
152
+ describe 'delegates and aliases' do
153
+ before do
154
+ allow(root).to receive_messages(
155
+ word?: nil,
156
+ partial_word?: nil,
157
+ scan: nil,
158
+ add: nil,
159
+ each: nil,
160
+ compressed?: nil
161
+ )
162
+ end
163
+
164
+ it 'aliases `#include?` to `#word?`' do
165
+ container.include? 'words'
166
+ expect(root).to have_received(:word?).with %w(w o r d s)
167
+ end
168
+
169
+ it 'aliases `#match?` to `#partial_word?`' do
170
+ container.match? 'words'
171
+ expect(root).to have_received(:partial_word?).with %w(w o r d s)
172
+ end
173
+
174
+ it 'aliases `#words` to `#scan`' do
175
+ container.words 'hig'
176
+ expect(root).to have_received(:scan).with %w(h i g)
177
+ end
178
+
179
+ it 'aliases `#<<` to `#add`' do
180
+ container << 'words'
181
+ expect(root).to have_received(:add).with 'words'
182
+ end
183
+
184
+ it 'delegates `#each` to the root node' do
185
+ container.each
186
+ expect(root).to have_received :each
187
+ end
188
+
189
+ it 'delegates `#compressed?` to the root node' do
190
+ container.compressed?
191
+ expect(root).to have_received :compressed?
192
+ end
193
+ end
194
+
195
+ describe '#compress!' do
196
+ let(:compressor) { Rambling::Trie::Compressor.new }
197
+ let(:root) { Rambling::Trie::RawNode.new }
198
+
199
+ context 'with at least one word' do
200
+ it 'keeps the root letter nil' do
201
+ container.add 'all'
202
+ container.compress!
203
+
204
+ expect(container.letter).to be_nil
205
+ end
206
+ end
207
+
208
+ context 'with a single word' do
209
+ before do
210
+ container.add 'all'
211
+ container.compress!
212
+ end
213
+
214
+ it 'compresses into a single node without children' do
215
+ expect(container[:all].letter).to eq :all
216
+ expect(container[:all].children.size).to eq 0
217
+ expect(container[:all]).to be_terminal
218
+ expect(container[:all]).to be_compressed
219
+ end
220
+ end
221
+
222
+ context 'with two words' do
223
+ before do
224
+ container.add 'all'
225
+ container.add 'ask'
226
+ container.compress!
227
+ end
228
+
229
+ it 'compresses into corresponding three nodes' do
230
+ expect(container[:a].letter).to eq :a
231
+ expect(container[:a].children.size).to eq 2
232
+
233
+ expect(container[:a][:ll].letter).to eq :ll
234
+ expect(container[:a][:sk].letter).to eq :sk
235
+
236
+ expect(container[:a][:ll].children.size).to eq 0
237
+ expect(container[:a][:sk].children.size).to eq 0
238
+
239
+ expect(container[:a][:ll]).to be_terminal
240
+ expect(container[:a][:sk]).to be_terminal
241
+
242
+ expect(container[:a][:ll]).to be_compressed
243
+ expect(container[:a][:sk]).to be_compressed
244
+ end
245
+ end
246
+
247
+ it 'reassigns the parent nodes correctly' do
248
+ container.add 'repay'
249
+ container.add 'rest'
250
+ container.add 'repaint'
251
+ container.compress!
252
+
253
+ expect(container[:re].letter).to eq :re
254
+ expect(container[:re].children.size).to eq 2
255
+
256
+ expect(container[:re][:pa].letter).to eq :pa
257
+ expect(container[:re][:st].letter).to eq :st
258
+
259
+ expect(container[:re][:pa].children.size).to eq 2
260
+ expect(container[:re][:st].children.size).to eq 0
261
+
262
+ expect(container[:re][:pa][:y].letter).to eq :y
263
+ expect(container[:re][:pa][:int].letter).to eq :int
264
+
265
+ expect(container[:re][:pa][:y].children.size).to eq 0
266
+ expect(container[:re][:pa][:int].children.size).to eq 0
267
+
268
+ expect(container[:re][:pa][:y].parent).to eq container[:re][:pa]
269
+ expect(container[:re][:pa][:int].parent).to eq container[:re][:pa]
270
+ end
271
+
272
+ it 'does not compress terminal nodes' do
273
+ container.add 'you'
274
+ container.add 'your'
275
+ container.add 'yours'
276
+
277
+ container.compress!
278
+
279
+ expect(container[:you].letter).to eq :you
280
+
281
+ expect(container[:you][:r].letter).to eq :r
282
+ expect(container[:you][:r]).to be_compressed
283
+
284
+ expect(container[:you][:r][:s].letter).to eq :s
285
+ expect(container[:you][:r][:s]).to be_compressed
286
+ end
287
+
288
+ describe 'and trying to add a word' do
289
+ it 'raises an error' do
290
+ container.add 'repay'
291
+ container.add 'rest'
292
+ container.add 'repaint'
293
+ container.compress!
294
+
295
+ expect { container.add 'restaurant' }.to raise_error Rambling::Trie::InvalidOperation
296
+ end
297
+ end
298
+ end
299
+
300
+ describe '#word?' do
301
+ let(:compressor) { Rambling::Trie::Compressor.new }
302
+ let(:root) { Rambling::Trie::RawNode.new }
303
+
304
+ context 'word is contained' do
305
+ before do
306
+ container.add 'hello'
307
+ container.add 'high'
308
+ end
309
+
310
+ it 'matches the whole word' do
311
+ expect(container.word? 'hello').to be true
312
+ expect(container.word? 'high').to be true
313
+ end
314
+
315
+ context 'and the root has been compressed' do
316
+ before do
317
+ container.compress!
318
+ end
319
+
320
+ it 'matches the whole word' do
321
+ expect(container.word? 'hello').to be true
322
+ expect(container.word? 'high').to be true
323
+ end
324
+ end
325
+ end
326
+
327
+ context 'word is not contained' do
328
+ before do
329
+ container.add 'hello'
330
+ end
331
+
332
+ it 'does not match the whole word' do
333
+ expect(container.word? 'halt').to be false
334
+ expect(container.word? 'al').to be false
335
+ end
336
+
337
+ context 'and the root has been compressed' do
338
+ before do
339
+ container.compress!
340
+ end
341
+
342
+ it 'does not match the whole word' do
343
+ expect(container.word? 'halt').to be false
344
+ expect(container.word? 'al').to be false
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ describe '#partial_word?' do
351
+ context 'word is contained' do
352
+ before do
353
+ container.add 'hello'
354
+ container.add 'high'
355
+ end
356
+
357
+ it 'matches part of the word' do
358
+ expect(container.partial_word? 'hell').to be true
359
+ expect(container.partial_word? 'hig').to be true
360
+ end
361
+
362
+ context 'and the root has been compressed' do
363
+ before do
364
+ container.compress!
365
+ end
366
+
367
+ it 'matches part of the word' do
368
+ expect(container.partial_word? 'h').to be true
369
+ expect(container.partial_word? 'he').to be true
370
+ expect(container.partial_word? 'hell').to be true
371
+ expect(container.partial_word? 'hello').to be true
372
+ expect(container.partial_word? 'hi').to be true
373
+ expect(container.partial_word? 'hig').to be true
374
+ expect(container.partial_word? 'high').to be true
375
+ end
376
+ end
377
+ end
378
+
379
+ context 'word is not contained' do
380
+ before do
381
+ container.add 'hello'
382
+ end
383
+
384
+ it 'does not match any part of the word' do
385
+ expect(container.partial_word? 'ha').to be false
386
+ expect(container.partial_word? 'hal').to be false
387
+ expect(container.partial_word? 'al').to be false
388
+ end
389
+
390
+ context 'and the root has been compressed' do
391
+ before do
392
+ container.compress!
393
+ end
394
+
395
+ it 'does not match any part of the word' do
396
+ expect(container.partial_word? 'ha').to be false
397
+ expect(container.partial_word? 'hal').to be false
398
+ expect(container.partial_word? 'al').to be false
399
+ end
400
+ end
401
+ end
402
+ end
403
+
404
+ describe '#scan' do
405
+ context 'words that match are not contained' do
406
+ before do
407
+ container.add 'hi'
408
+ container.add 'hello'
409
+ container.add 'high'
410
+ container.add 'hell'
411
+ container.add 'highlight'
412
+ container.add 'histerical'
413
+ end
414
+
415
+ it 'returns an array with the words that match' do
416
+ expect(container.scan 'hi').to eq [
417
+ 'hi',
418
+ 'high',
419
+ 'highlight',
420
+ 'histerical'
421
+ ]
422
+
423
+ expect(container.scan 'hig').to eq [
424
+ 'high',
425
+ 'highlight'
426
+ ]
427
+ end
428
+
429
+ context 'and the root has been compressed' do
430
+ before do
431
+ container.compress!
432
+ end
433
+
434
+ it 'returns an array with the words that match' do
435
+ expect(container.scan 'hi').to eq [
436
+ 'hi',
437
+ 'high',
438
+ 'highlight',
439
+ 'histerical'
440
+ ]
441
+
442
+ expect(container.scan 'hig').to eq [
443
+ 'high',
444
+ 'highlight'
445
+ ]
446
+ end
447
+ end
448
+ end
449
+
450
+ context 'words that match are not contained' do
451
+ before do
452
+ container.add 'hello'
453
+ end
454
+
455
+ it 'returns an empty array' do
456
+ expect(container.scan 'hi').to eq []
457
+ end
458
+
459
+ context 'and the root has been compressed' do
460
+ before do
461
+ container.compress!
462
+ end
463
+
464
+ it 'returns an empty array' do
465
+ expect(container.scan 'hi').to eq []
466
+ end
467
+ end
468
+ end
469
+ end
470
+ end