rambling-trie 0.8.1 → 0.9.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.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/lib/rambling/trie.rb +21 -9
- data/lib/rambling/trie/compressed_node.rb +112 -0
- data/lib/rambling/trie/compression.rb +13 -0
- data/lib/rambling/trie/compressor.rb +30 -31
- data/lib/rambling/trie/{root.rb → container.rb} +41 -38
- data/lib/rambling/trie/enumerable.rb +11 -7
- data/lib/rambling/trie/missing_node.rb +1 -1
- data/lib/rambling/trie/node.rb +25 -22
- data/lib/rambling/trie/plain_text_reader.rb +1 -1
- data/lib/rambling/trie/raw_node.rb +90 -0
- data/lib/rambling/trie/tasks/helpers/path.rb +13 -0
- data/lib/rambling/trie/tasks/helpers/time.rb +7 -0
- data/lib/rambling/trie/tasks/performance.rb +10 -91
- data/lib/rambling/trie/tasks/performance/all.rb +4 -0
- data/lib/rambling/trie/tasks/performance/benchmark.rb +172 -0
- data/lib/rambling/trie/tasks/performance/directory.rb +11 -0
- data/lib/rambling/trie/tasks/performance/profile/call_tree.rb +132 -0
- data/lib/rambling/trie/tasks/performance/profile/memory.rb +116 -0
- data/lib/rambling/trie/version.rb +1 -1
- data/rambling-trie.gemspec +6 -4
- data/spec/integration/rambling/trie_spec.rb +63 -9
- data/spec/lib/rambling/trie/compressed_node_spec.rb +35 -0
- data/spec/lib/rambling/trie/compressor_spec.rb +31 -0
- data/spec/lib/rambling/trie/container_spec.rb +470 -0
- data/spec/lib/rambling/trie/enumerable_spec.rb +2 -2
- data/spec/lib/rambling/trie/inspector_spec.rb +21 -14
- data/spec/lib/rambling/trie/node_spec.rb +72 -209
- data/spec/lib/rambling/trie/raw_node_spec.rb +377 -0
- data/spec/lib/rambling/trie_spec.rb +46 -25
- metadata +57 -16
- data/lib/rambling/trie/branches.rb +0 -149
- data/spec/lib/rambling/trie/branches_spec.rb +0 -52
- 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
|
data/rambling-trie.gemspec
CHANGED
@@ -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.
|
24
|
-
gem.add_development_dependency 'rake', '~>
|
25
|
-
gem.add_development_dependency 'ruby-prof', '~> 0.
|
26
|
-
gem.add_development_dependency '
|
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
|
-
|
5
|
-
|
6
|
-
|
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
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|