rambling-trie 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/README.markdown +14 -8
- data/lib/rambling-trie.rb +16 -11
- data/lib/rambling-trie/branches.rb +38 -29
- data/lib/rambling-trie/children_hash_deferer.rb +28 -27
- data/lib/rambling-trie/compressor.rb +8 -10
- data/lib/rambling-trie/node.rb +12 -14
- data/lib/rambling-trie/root.rb +15 -18
- data/lib/rambling-trie/tasks/performance.rb +22 -14
- data/lib/rambling-trie/version.rb +1 -1
- data/rambling-trie.gemspec +1 -1
- data/spec/lib/rambling-trie/branches_spec.rb +52 -0
- data/spec/lib/rambling-trie/children_hash_deferer_spec.rb +59 -0
- data/spec/lib/rambling-trie/node_spec.rb +1 -34
- data/spec/lib/rambling-trie/root_spec.rb +30 -18
- data/spec/lib/rambling-trie_spec.rb +18 -1
- metadata +6 -5
- data/lib/rambling.rb +0 -3
data/.gitignore
CHANGED
data/README.markdown
CHANGED
@@ -28,31 +28,37 @@ gem 'rambling-trie'
|
|
28
28
|
|
29
29
|
## How to use the Rambling Trie
|
30
30
|
|
31
|
+
### Deprecation warning
|
32
|
+
|
33
|
+
Starting from version 0.4.0, `Rambling::Trie.new` is deprecated. Please use `Rambling::Trie.create` instead.
|
34
|
+
|
31
35
|
To create the trie, initialize it like this:
|
32
36
|
|
33
37
|
``` ruby
|
34
|
-
trie = Rambling::Trie.
|
38
|
+
trie = Rambling::Trie.create
|
35
39
|
```
|
36
40
|
|
37
41
|
You can also provide a file which contains all the words to be added to the trie, and it will read the file and create the structure for you, like this:
|
38
42
|
|
39
43
|
``` ruby
|
40
|
-
trie = Rambling::Trie.
|
44
|
+
trie = Rambling::Trie.create '/path/to/file'
|
41
45
|
```
|
42
46
|
|
43
|
-
To add new words to the trie, use `add_branch_from
|
47
|
+
To add new words to the trie, use `add_branch_from` or `<<`:
|
44
48
|
|
45
49
|
``` ruby
|
46
50
|
trie.add_branch_from 'word'
|
51
|
+
trie << 'word'
|
47
52
|
```
|
48
53
|
|
49
|
-
And to find out if a word already exists in the trie, use `is_word?`:
|
54
|
+
And to find out if a word already exists in the trie, use `is_word?` or `include?`:
|
50
55
|
|
51
56
|
``` ruby
|
52
57
|
trie.is_word? 'word'
|
58
|
+
trie.include? 'word'
|
53
59
|
```
|
54
60
|
|
55
|
-
If you wish to find if part of a word exists in the
|
61
|
+
If you wish to find if part of a word exists in the trie instance, you should call `has_branch_for?`:
|
56
62
|
|
57
63
|
``` ruby
|
58
64
|
trie.has_branch_for? 'partial_word'
|
@@ -62,7 +68,7 @@ trie.has_branch_for? 'partial_word'
|
|
62
68
|
|
63
69
|
By default, the Rambling Trie works as a Standard Trie.
|
64
70
|
Starting from version 0.1.0, you can obtain a Compressed Trie from the Standard one, by using the compression feature.
|
65
|
-
Just call the `compress!` method on the
|
71
|
+
Just call the `compress!` method on the trie instance:
|
66
72
|
|
67
73
|
``` ruby
|
68
74
|
trie.compress!
|
@@ -72,10 +78,10 @@ This will reduce the amount of Trie nodes by eliminating the redundant ones, whi
|
|
72
78
|
|
73
79
|
Starting from version 0.3.2, the `has_branch_for?` and `is_word?` methods work as expected on a compressed trie.
|
74
80
|
|
75
|
-
__Note that the `compress!` method acts over the
|
81
|
+
__Note that the `compress!` method acts over the trie instance it belongs to.__
|
76
82
|
__Also, adding words after compression is not supported.__
|
77
83
|
|
78
|
-
You can find out if a
|
84
|
+
You can find out if a trie instance is compressed by calling the `compressed?` method:
|
79
85
|
|
80
86
|
``` ruby
|
81
87
|
trie.compressed?
|
data/lib/rambling-trie.rb
CHANGED
@@ -1,22 +1,27 @@
|
|
1
1
|
[
|
2
|
-
'
|
3
|
-
'
|
4
|
-
'
|
5
|
-
'
|
6
|
-
'
|
7
|
-
'
|
8
|
-
'
|
9
|
-
|
10
|
-
].each do |file|
|
11
|
-
require File.join File.dirname(__FILE__), file
|
12
|
-
end
|
2
|
+
'invalid_operation',
|
3
|
+
'children_hash_deferer',
|
4
|
+
'compressor',
|
5
|
+
'branches',
|
6
|
+
'node',
|
7
|
+
'root',
|
8
|
+
'version'
|
9
|
+
].map { |file| File.join('rambling-trie', file) }.each &method(:require)
|
13
10
|
|
14
11
|
module Rambling
|
15
12
|
module Trie
|
16
13
|
class << self
|
14
|
+
# Creates a new Trie. Entry point for the Rambling::Trie API.
|
15
|
+
# @param [String, nil] filename the file to load the words from (defaults to nil).
|
17
16
|
def create(*params)
|
18
17
|
Root.new *params
|
19
18
|
end
|
19
|
+
|
20
|
+
# @deprecated Please use {#create} instead
|
21
|
+
def new(*params)
|
22
|
+
warn '[DEPRECATION] `new` is deprecated. Please use `create` instead.'
|
23
|
+
create *params
|
24
|
+
end
|
20
25
|
end
|
21
26
|
end
|
22
27
|
end
|
@@ -9,71 +9,65 @@ module Rambling
|
|
9
9
|
def add_branch_from(word)
|
10
10
|
raise InvalidOperation.new('Cannot add branch to compressed trie') if compressed?
|
11
11
|
if word.empty?
|
12
|
-
@
|
12
|
+
@terminal = true
|
13
13
|
return
|
14
14
|
end
|
15
15
|
|
16
16
|
first_letter = word.slice(0).to_sym
|
17
17
|
|
18
|
-
if @children.has_key?
|
19
|
-
word.slice!
|
18
|
+
if @children.has_key? first_letter
|
19
|
+
word.slice! 0
|
20
20
|
child = @children[first_letter]
|
21
|
-
child
|
21
|
+
child << word
|
22
22
|
child
|
23
23
|
else
|
24
24
|
@children[first_letter] = Node.new word, self
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
alias_method :<<, :add_branch_from
|
29
|
+
|
28
30
|
protected
|
29
31
|
|
30
|
-
def
|
31
|
-
chars.empty? or fulfills_uncompressed_condition?(:
|
32
|
+
def uncompressed_has_branch_for?(chars)
|
33
|
+
chars.empty? or fulfills_uncompressed_condition?(:uncompressed_has_branch_for?, chars)
|
32
34
|
end
|
33
35
|
|
34
|
-
def
|
36
|
+
def compressed_has_branch_for?(chars)
|
35
37
|
return true if chars.empty?
|
36
38
|
|
37
|
-
first_letter = chars.slice!
|
38
|
-
|
39
|
-
@children.keys.each do |x|
|
40
|
-
x = x.to_s
|
41
|
-
if x.start_with?(first_letter)
|
42
|
-
key = x
|
43
|
-
break
|
44
|
-
end
|
45
|
-
end
|
39
|
+
first_letter = chars.slice! 0
|
40
|
+
current_key, current_key_string = current_key first_letter
|
46
41
|
|
47
|
-
unless
|
48
|
-
|
49
|
-
return @children[sym_key].has_compressed_branch_for?(chars) if key.length == first_letter.length
|
42
|
+
unless current_key.nil?
|
43
|
+
return @children[current_key].compressed_has_branch_for?(chars) if current_key_string.length == first_letter.length
|
50
44
|
|
51
45
|
while not chars.empty?
|
52
|
-
char = chars.slice!
|
46
|
+
char = chars.slice! 0
|
53
47
|
|
54
|
-
break unless
|
48
|
+
break unless current_key_string[first_letter.length] == char
|
55
49
|
|
56
50
|
first_letter += char
|
57
51
|
return true if chars.empty?
|
58
|
-
return @children[
|
52
|
+
return @children[current_key].compressed_has_branch_for?(chars) if current_key_string.length == first_letter.length
|
59
53
|
end
|
60
54
|
end
|
61
55
|
|
62
56
|
false
|
63
57
|
end
|
64
58
|
|
65
|
-
def
|
66
|
-
(chars.empty? and terminal?) or fulfills_uncompressed_condition?(:
|
59
|
+
def uncompressed_is_word?(chars)
|
60
|
+
(chars.empty? and terminal?) or fulfills_uncompressed_condition?(:uncompressed_is_word?, chars)
|
67
61
|
end
|
68
62
|
|
69
|
-
def
|
63
|
+
def compressed_is_word?(chars)
|
70
64
|
return true if chars.empty? and terminal?
|
71
65
|
|
72
66
|
first_letter = ''
|
73
67
|
while not chars.empty?
|
74
|
-
first_letter += chars.slice!
|
68
|
+
first_letter += chars.slice! 0
|
75
69
|
key = first_letter.to_sym
|
76
|
-
return @children[key].
|
70
|
+
return @children[key].compressed_is_word?(chars) if @children.has_key? key
|
77
71
|
end
|
78
72
|
|
79
73
|
false
|
@@ -81,11 +75,26 @@ module Rambling
|
|
81
75
|
|
82
76
|
private
|
83
77
|
|
78
|
+
def current_key(letter)
|
79
|
+
current_key_string = current_key = nil
|
80
|
+
|
81
|
+
@children.keys.each do |key|
|
82
|
+
key_string = key.to_s
|
83
|
+
if key_string.start_with? letter
|
84
|
+
current_key = key
|
85
|
+
current_key_string = key_string
|
86
|
+
break
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
[current_key, current_key_string]
|
91
|
+
end
|
92
|
+
|
84
93
|
def fulfills_uncompressed_condition?(method, chars)
|
85
|
-
first_letter = chars.slice!
|
94
|
+
first_letter = chars.slice! 0
|
86
95
|
unless first_letter.nil?
|
87
96
|
first_letter_sym = first_letter.to_sym
|
88
|
-
return @children[first_letter_sym].send(method, chars) if @children.has_key?
|
97
|
+
return @children[first_letter_sym].send(method, chars) if @children.has_key? first_letter_sym
|
89
98
|
end
|
90
99
|
|
91
100
|
false
|
@@ -1,34 +1,35 @@
|
|
1
1
|
module Rambling
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
module Trie
|
3
|
+
# Provides some proxy methods to the children's hash for readability.
|
4
|
+
module ChildrenHashDeferer
|
5
|
+
# Proxies to @children[key]
|
6
|
+
# @param [Symbol] key the key to look for in the children's hash.
|
7
|
+
# @return [Node, nil] the child node with that key or nil.
|
8
|
+
def [](key)
|
9
|
+
@children[key]
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
# Proxies to @children[key] = value.
|
13
|
+
# @param [Symbol] key the to add or change the value for.
|
14
|
+
# @param [Node] value the node to add to the children's hash.
|
15
|
+
# @return [Node, nil] the child node with that key or nil.
|
16
|
+
def []=(key, value)
|
17
|
+
@children[key] = value
|
18
|
+
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
# Proxies to @children.delete(key)
|
21
|
+
# @param [Symbol] key the key to delete in the children's hash.
|
22
|
+
# @return [Node, nil] the child node corresponding to the key just deleted or nil.
|
23
|
+
def delete(key)
|
24
|
+
@children.delete(key)
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
# Proxies to @children.has_key?(key)
|
28
|
+
# @param [Symbol] key the key to look for in the children's hash.
|
29
|
+
# @return [Boolean] `true` for the keys that exist in the children's hash, false otherwise.
|
30
|
+
def has_key?(key)
|
31
|
+
@children.has_key?(key)
|
32
|
+
end
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
34
|
-
|
@@ -8,15 +8,13 @@ module Rambling
|
|
8
8
|
@parent.nil? ? false : @parent.compressed?
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
def compress_own_tree!
|
11
|
+
def compress_tree!
|
14
12
|
if @children.size == 1 and not terminal? and not @letter.nil?
|
15
|
-
merge_with!
|
16
|
-
|
13
|
+
merge_with! @children.values.first
|
14
|
+
compress_tree!
|
17
15
|
end
|
18
16
|
|
19
|
-
@children.values.each
|
17
|
+
@children.values.each &:compress_tree!
|
20
18
|
|
21
19
|
self
|
22
20
|
end
|
@@ -26,8 +24,8 @@ module Rambling
|
|
26
24
|
def merge_with!(child)
|
27
25
|
new_letter = (@letter.to_s + child.letter.to_s).to_sym
|
28
26
|
|
29
|
-
rehash_on_parent!
|
30
|
-
redefine_self!
|
27
|
+
rehash_on_parent! @letter, new_letter
|
28
|
+
redefine_self! new_letter, child
|
31
29
|
|
32
30
|
@children.values.each { |node| node.parent = self }
|
33
31
|
end
|
@@ -35,14 +33,14 @@ module Rambling
|
|
35
33
|
def rehash_on_parent!(old_letter, new_letter)
|
36
34
|
return if @parent.nil?
|
37
35
|
|
38
|
-
@parent.delete
|
36
|
+
@parent.delete old_letter
|
39
37
|
@parent[new_letter] = self
|
40
38
|
end
|
41
39
|
|
42
40
|
def redefine_self!(new_letter, merged_node)
|
43
41
|
@letter = new_letter
|
44
42
|
@children = merged_node.children
|
45
|
-
@
|
43
|
+
@terminal = merged_node.terminal?
|
46
44
|
end
|
47
45
|
end
|
48
46
|
end
|
data/lib/rambling-trie/node.rb
CHANGED
@@ -5,6 +5,7 @@ module Rambling
|
|
5
5
|
include ChildrenHashDeferer
|
6
6
|
include Compressor
|
7
7
|
include Branches
|
8
|
+
include Enumerable
|
8
9
|
|
9
10
|
# Letter or letters corresponding to this node.
|
10
11
|
# @return [Symbol, nil] the corresponding letter(s) or nil.
|
@@ -15,37 +16,34 @@ module Rambling
|
|
15
16
|
attr_reader :children
|
16
17
|
|
17
18
|
# Parent node.
|
18
|
-
# @return [
|
19
|
+
# @return [Node, nil] the parent node or nil for the root element.
|
19
20
|
attr_accessor :parent
|
20
21
|
|
21
|
-
# Creates a new
|
22
|
-
# @param [String] word the word from which to create this
|
23
|
-
# @param [
|
22
|
+
# Creates a new Node.
|
23
|
+
# @param [String] word the word from which to create this Node and his branch.
|
24
|
+
# @param [Node] parent the parent of this node.
|
24
25
|
def initialize(word, parent = nil)
|
25
|
-
@letter = nil
|
26
|
-
@parent = parent
|
27
|
-
@is_terminal = false
|
28
|
-
@children = {}
|
26
|
+
@letter, @parent, @terminal, @children = [nil, parent, false, {}]
|
29
27
|
|
30
28
|
unless word.nil? or word.empty?
|
31
|
-
letter = word.slice!
|
29
|
+
letter = word.slice! 0
|
32
30
|
@letter = letter.to_sym unless letter.nil?
|
33
|
-
@
|
34
|
-
|
31
|
+
@terminal = word.empty?
|
32
|
+
self << word
|
35
33
|
end
|
36
34
|
end
|
37
35
|
|
38
36
|
# Flag for terminal nodes.
|
39
37
|
# @return [Boolean] `true` for terminal nodes, `false` otherwise.
|
40
38
|
def terminal?
|
41
|
-
@
|
39
|
+
@terminal
|
42
40
|
end
|
43
41
|
|
44
42
|
# String representation of the current node, if it is a terminal node.
|
45
43
|
# @return [String] the string representation of the current node.
|
46
|
-
# @raise [
|
44
|
+
# @raise [InvalidOperation] if node is not terminal or is root.
|
47
45
|
def as_word
|
48
|
-
raise InvalidOperation.new
|
46
|
+
raise InvalidOperation.new unless @letter.nil? or terminal?
|
49
47
|
get_letter_string
|
50
48
|
end
|
51
49
|
|
data/lib/rambling-trie/root.rb
CHANGED
@@ -8,50 +8,47 @@ module Rambling
|
|
8
8
|
super(nil)
|
9
9
|
|
10
10
|
@filename = filename
|
11
|
-
@
|
11
|
+
@compressed = false
|
12
12
|
add_all_nodes if filename
|
13
13
|
end
|
14
14
|
|
15
15
|
# Compresses the existing tree using redundant node elimination. Flags the trie as compressed.
|
16
|
-
# @return [
|
16
|
+
# @return [Root] same object
|
17
17
|
def compress!
|
18
|
-
unless compressed?
|
19
|
-
compress_own_tree!
|
20
|
-
@is_compressed = true
|
21
|
-
end
|
22
|
-
|
18
|
+
@compressed = (not compress_tree!.nil?) unless compressed?
|
23
19
|
self
|
24
20
|
end
|
25
21
|
|
26
|
-
# Flag for compressed tries. Overrides {
|
22
|
+
# Flag for compressed tries. Overrides {Compressor#compressed?}.
|
27
23
|
# @return [Boolean] `true` for compressed tries, `false` otherwise.
|
28
24
|
def compressed?
|
29
|
-
@
|
25
|
+
@compressed
|
30
26
|
end
|
31
27
|
|
32
28
|
# Checks if a path for a word or partial word exists in the trie.
|
33
29
|
# @param [String] word the word or partial word to look for in the trie.
|
34
30
|
# @return [Boolean] `true` if the word or partial word is found, `false` otherwise.
|
35
31
|
def has_branch_for?(word = '')
|
36
|
-
|
37
|
-
compressed? ? has_compressed_branch_for?(chars) : has_uncompressed_branch_for?(chars)
|
32
|
+
fulfills_condition word, :has_branch_for?
|
38
33
|
end
|
39
34
|
|
40
35
|
# Checks if a whole word exists in the trie.
|
41
36
|
# @param [String] word the word to look for in the trie.
|
42
37
|
# @return [Boolean] `true` only if the word is found and the last character corresponds to a terminal node.
|
43
38
|
def is_word?(word = '')
|
44
|
-
|
45
|
-
compressed? ? is_compressed_word?(chars) : is_uncompressed_word?(chars)
|
39
|
+
fulfills_condition word, :is_word?
|
46
40
|
end
|
47
41
|
|
42
|
+
alias_method :include?, :is_word?
|
43
|
+
|
48
44
|
private
|
45
|
+
def fulfills_condition(word, method)
|
46
|
+
method = compressed? ? "compressed_#{method}" : "uncompressed_#{method}"
|
47
|
+
send(method, word.chars.to_a)
|
48
|
+
end
|
49
|
+
|
49
50
|
def add_all_nodes
|
50
|
-
File.open(@filename)
|
51
|
-
while word = file.gets
|
52
|
-
add_branch_from(word.chomp)
|
53
|
-
end
|
54
|
-
end
|
51
|
+
File.open(@filename).each_line { |line| self << line.chomp }
|
55
52
|
end
|
56
53
|
end
|
57
54
|
end
|
@@ -9,8 +9,8 @@ namespace :performance do
|
|
9
9
|
methods.each do |method|
|
10
10
|
output.puts "`#{method}`:"
|
11
11
|
words.each do |word|
|
12
|
-
output.print "#{word} - #{trie.send(method, word)}".ljust
|
13
|
-
output.puts Benchmark.measure { 200_000.times {trie.send
|
12
|
+
output.print "#{word} - #{trie.send(method, word)}".ljust 30
|
13
|
+
output.puts Benchmark.measure { 200_000.times { trie.send method, word }}
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -18,21 +18,29 @@ namespace :performance do
|
|
18
18
|
def generate_report(filename = nil)
|
19
19
|
output = filename.nil? ? $stdout : File.open(filename, 'a+')
|
20
20
|
|
21
|
-
trie = Rambling::Trie.new(get_path('assets', 'dictionaries', 'words_with_friends.txt'))
|
22
|
-
|
23
21
|
output.puts "\nReport for rambling-trie version #{Rambling::Trie::VERSION}"
|
24
|
-
report('Uncompressed', trie, output)
|
25
22
|
|
26
|
-
|
23
|
+
trie = nil
|
24
|
+
measure = Benchmark.measure { trie = Rambling::Trie.create get_path('assets', 'dictionaries', 'words_with_friends.txt') }
|
25
|
+
|
26
|
+
if ENV['profile_creation']
|
27
|
+
output.puts '==> Creation'
|
28
|
+
output.print 'Rambling::Trie.create'.ljust 30
|
29
|
+
output.puts measure
|
30
|
+
end
|
31
|
+
|
32
|
+
report 'Uncompressed', trie, output
|
33
|
+
|
34
|
+
return unless trie.respond_to? :compress!
|
27
35
|
|
28
36
|
trie.compress!
|
29
|
-
report
|
37
|
+
report 'Compressed', trie, output
|
30
38
|
|
31
39
|
output.close
|
32
40
|
end
|
33
41
|
|
34
42
|
def get_path(*filename)
|
35
|
-
File.join
|
43
|
+
File.join File.dirname(__FILE__), '..', '..', '..', *filename
|
36
44
|
end
|
37
45
|
|
38
46
|
desc 'Generate performance report'
|
@@ -45,7 +53,7 @@ namespace :performance do
|
|
45
53
|
desc 'Generate performance report and append result to reports/performance'
|
46
54
|
task :save do
|
47
55
|
puts 'Generating performance report...'
|
48
|
-
generate_report
|
56
|
+
generate_report get_path('reports', 'performance')
|
49
57
|
puts 'Report has been saved to reports/performance'
|
50
58
|
end
|
51
59
|
end
|
@@ -56,7 +64,7 @@ namespace :performance do
|
|
56
64
|
|
57
65
|
puts 'Generating profiling reports...'
|
58
66
|
|
59
|
-
rambling_trie = Rambling::Trie.
|
67
|
+
rambling_trie = Rambling::Trie.create get_path('assets', 'dictionaries', 'words_with_friends.txt')
|
60
68
|
words = ['hi', 'help', 'beautiful', 'impressionism', 'anthropological']
|
61
69
|
methods = [:has_branch_for?, :is_word?]
|
62
70
|
tries = [lambda {rambling_trie.clone}, lambda {rambling_trie.clone.compress!}]
|
@@ -66,12 +74,12 @@ namespace :performance do
|
|
66
74
|
trie = trie_generator.call
|
67
75
|
result = RubyProf.profile do
|
68
76
|
words.each do |word|
|
69
|
-
200_000.times { trie.send
|
77
|
+
200_000.times { trie.send method, word }
|
70
78
|
end
|
71
79
|
end
|
72
80
|
|
73
81
|
File.open get_path('reports', "profile-#{trie.compressed? ? 'compressed' : 'uncompressed'}-#{method.to_s.sub(/\?/, '')}-#{Time.now.to_i}"), 'w' do |file|
|
74
|
-
RubyProf::CallTreePrinter.new(result).print
|
82
|
+
RubyProf::CallTreePrinter.new(result).print file
|
75
83
|
end
|
76
84
|
end
|
77
85
|
end
|
@@ -85,7 +93,7 @@ namespace :performance do
|
|
85
93
|
|
86
94
|
puts 'Generating cpu profiling reports...'
|
87
95
|
|
88
|
-
rambling_trie = Rambling::Trie.
|
96
|
+
rambling_trie = Rambling::Trie.create get_path('assets', 'dictionaries', 'words_with_friends.txt')
|
89
97
|
words = ['hi', 'help', 'beautiful', 'impressionism', 'anthropological']
|
90
98
|
methods = [:has_branch_for?, :is_word?]
|
91
99
|
tries = [lambda {rambling_trie.clone}, lambda {rambling_trie.clone.compress!}]
|
@@ -95,7 +103,7 @@ namespace :performance do
|
|
95
103
|
trie = trie_generator.call
|
96
104
|
result = PerfTools::CpuProfiler.start get_path('reports', "cpu_profile-#{trie.compressed? ? 'compressed' : 'uncompressed'}-#{method.to_s.sub(/\?/, '')}-#{Time.now.to_i}") do
|
97
105
|
words.each do |word|
|
98
|
-
200_000.times { trie.send
|
106
|
+
200_000.times { trie.send method, word }
|
99
107
|
end
|
100
108
|
end
|
101
109
|
end
|
data/rambling-trie.gemspec
CHANGED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Rambling
|
4
|
+
module Trie
|
5
|
+
describe Branches do
|
6
|
+
describe '#add_branch_from' do
|
7
|
+
context 'new word for existing branch' do
|
8
|
+
let(:node) { Node.new 'back' }
|
9
|
+
|
10
|
+
before :each do
|
11
|
+
node.add_branch_from 'a'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'does not increment the child count' do
|
15
|
+
node.should have(1).children
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'marks it as terminal' do
|
19
|
+
node[:a].should be_terminal
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'old word for existing branch' do
|
24
|
+
let(:node) { Node.new 'back' }
|
25
|
+
|
26
|
+
before :each do
|
27
|
+
node.add_branch_from 'ack'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'does not increment any child count' do
|
31
|
+
node.should have(1).children
|
32
|
+
node[:a].should have(1).children
|
33
|
+
node[:a][:c].should have(1).children
|
34
|
+
node[:a][:c][:k].should have(0).children
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#<<' do
|
40
|
+
let(:root) { Root.new }
|
41
|
+
let(:word) { 'word' }
|
42
|
+
|
43
|
+
it 'delegates to #add_branch_from' do
|
44
|
+
[true, false].each do |value|
|
45
|
+
root.stub(:add_branch_from).with(word).and_return value
|
46
|
+
root << word
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Rambling
|
4
|
+
module Trie
|
5
|
+
describe ChildrenHashDeferer do
|
6
|
+
class ClassWithChildren
|
7
|
+
include ChildrenHashDeferer
|
8
|
+
attr_accessor :children
|
9
|
+
|
10
|
+
def initialize()
|
11
|
+
@children = {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:deferer) { ClassWithChildren.new }
|
16
|
+
|
17
|
+
describe '#[]' do
|
18
|
+
let(:key) { :key }
|
19
|
+
let(:value) { 'value' }
|
20
|
+
|
21
|
+
it 'defers to the children hash' do
|
22
|
+
deferer.children.should_receive(:[]).with(key).and_return value
|
23
|
+
deferer[key].should == value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#[]=' do
|
28
|
+
let(:key) { :key }
|
29
|
+
let(:value) { 'value' }
|
30
|
+
|
31
|
+
it 'defers to the children hash' do
|
32
|
+
deferer.children.should_receive(:[]=).with(key, value)
|
33
|
+
deferer[key] = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#delete' do
|
38
|
+
let(:key) { :key }
|
39
|
+
let(:value) { 'value' }
|
40
|
+
|
41
|
+
it 'defers to the children hash' do
|
42
|
+
deferer.children.should_receive(:delete).with(key).and_return value
|
43
|
+
deferer.delete(key).should == value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#has_key' do
|
48
|
+
let(:key) { :key }
|
49
|
+
|
50
|
+
it 'defers to the children hash' do
|
51
|
+
[true, false].each do |value|
|
52
|
+
deferer.children.should_receive(:has_key?).with(key).and_return value
|
53
|
+
deferer.has_key?(key).should send("be_#{value}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -95,39 +95,6 @@ module Rambling
|
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
|
-
describe '#add_branch_from' do
|
99
|
-
context 'new word for existing branch' do
|
100
|
-
let(:node) { Node.new 'back' }
|
101
|
-
|
102
|
-
before :each do
|
103
|
-
node.add_branch_from 'a'
|
104
|
-
end
|
105
|
-
|
106
|
-
it 'does not increment the child count' do
|
107
|
-
node.should have(1).children
|
108
|
-
end
|
109
|
-
|
110
|
-
it 'marks it as terminal' do
|
111
|
-
node[:a].should be_terminal
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
context 'old word for existing branch' do
|
116
|
-
let(:node) { Node.new 'back' }
|
117
|
-
|
118
|
-
before :each do
|
119
|
-
node.add_branch_from 'ack'
|
120
|
-
end
|
121
|
-
|
122
|
-
it 'does not increment any child count' do
|
123
|
-
node.should have(1).children
|
124
|
-
node[:a].should have(1).children
|
125
|
-
node[:a][:c].should have(1).children
|
126
|
-
node[:a][:c][:k].should have(0).children
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
98
|
describe '#as_word' do
|
132
99
|
context 'for an empty node' do
|
133
100
|
let(:node) { Node.new '' }
|
@@ -174,7 +141,7 @@ module Rambling
|
|
174
141
|
end
|
175
142
|
|
176
143
|
describe '#compressed?' do
|
177
|
-
let(:root) { double
|
144
|
+
let(:root) { double 'Root' }
|
178
145
|
let(:node) { Node.new '', root }
|
179
146
|
|
180
147
|
context 'parent is compressed' do
|
@@ -37,7 +37,7 @@ module Rambling
|
|
37
37
|
end
|
38
38
|
|
39
39
|
it 'loads every word' do
|
40
|
-
File.open
|
40
|
+
File.open filename do |file|
|
41
41
|
file.readlines.each { |word| root.is_word?(word.chomp).should be_true }
|
42
42
|
end
|
43
43
|
end
|
@@ -86,7 +86,7 @@ module Rambling
|
|
86
86
|
|
87
87
|
context 'with at least one word' do
|
88
88
|
it 'keeps the root letter nil' do
|
89
|
-
root.add_branch_from
|
89
|
+
root.add_branch_from 'all'
|
90
90
|
root.compress!
|
91
91
|
|
92
92
|
root.letter.should be_nil
|
@@ -95,7 +95,7 @@ module Rambling
|
|
95
95
|
|
96
96
|
context 'with a single word' do
|
97
97
|
before :each do
|
98
|
-
root.add_branch_from
|
98
|
+
root.add_branch_from 'all'
|
99
99
|
root.compress!
|
100
100
|
end
|
101
101
|
|
@@ -109,8 +109,8 @@ module Rambling
|
|
109
109
|
|
110
110
|
context 'with two words' do
|
111
111
|
before :each do
|
112
|
-
root.add_branch_from
|
113
|
-
root.add_branch_from
|
112
|
+
root.add_branch_from 'all'
|
113
|
+
root.add_branch_from 'ask'
|
114
114
|
root.compress!
|
115
115
|
end
|
116
116
|
|
@@ -133,9 +133,9 @@ module Rambling
|
|
133
133
|
end
|
134
134
|
|
135
135
|
it 'reassigns the parent nodes correctly' do
|
136
|
-
root.add_branch_from
|
137
|
-
root.add_branch_from
|
138
|
-
root.add_branch_from
|
136
|
+
root.add_branch_from 'repay'
|
137
|
+
root.add_branch_from 'rest'
|
138
|
+
root.add_branch_from 'repaint'
|
139
139
|
root.compress!
|
140
140
|
|
141
141
|
root[:re].letter.should == :re
|
@@ -158,9 +158,9 @@ module Rambling
|
|
158
158
|
end
|
159
159
|
|
160
160
|
it 'does not compress terminal nodes' do
|
161
|
-
root.add_branch_from
|
162
|
-
root.add_branch_from
|
163
|
-
root.add_branch_from
|
161
|
+
root.add_branch_from 'you'
|
162
|
+
root.add_branch_from 'your'
|
163
|
+
root.add_branch_from 'yours'
|
164
164
|
|
165
165
|
root.compress!
|
166
166
|
|
@@ -175,9 +175,9 @@ module Rambling
|
|
175
175
|
|
176
176
|
describe 'and trying to add a branch' do
|
177
177
|
it 'raises an error' do
|
178
|
-
root.add_branch_from
|
179
|
-
root.add_branch_from
|
180
|
-
root.add_branch_from
|
178
|
+
root.add_branch_from 'repay'
|
179
|
+
root.add_branch_from 'rest'
|
180
|
+
root.add_branch_from 'repaint'
|
181
181
|
root.compress!
|
182
182
|
|
183
183
|
lambda { root.add_branch_from('restaurant') }.should raise_error(InvalidOperation)
|
@@ -191,8 +191,8 @@ module Rambling
|
|
191
191
|
context 'word is contained' do
|
192
192
|
shared_examples_for 'word is found' do
|
193
193
|
it 'matches part of the word' do
|
194
|
-
root.should have_branch_for
|
195
|
-
root.should have_branch_for
|
194
|
+
root.should have_branch_for 'hell'
|
195
|
+
root.should have_branch_for 'hig'
|
196
196
|
end
|
197
197
|
|
198
198
|
it 'matches the whole word' do
|
@@ -220,8 +220,8 @@ module Rambling
|
|
220
220
|
context 'word is not contained' do
|
221
221
|
shared_examples_for 'word not found' do
|
222
222
|
it 'does not match any part of the word' do
|
223
|
-
root.should_not have_branch_for
|
224
|
-
root.should_not have_branch_for
|
223
|
+
root.should_not have_branch_for 'ha'
|
224
|
+
root.should_not have_branch_for 'hal'
|
225
225
|
end
|
226
226
|
|
227
227
|
it 'does not match the whole word' do
|
@@ -244,6 +244,18 @@ module Rambling
|
|
244
244
|
end
|
245
245
|
end
|
246
246
|
end
|
247
|
+
|
248
|
+
describe '#include?' do
|
249
|
+
let(:root) { Root.new }
|
250
|
+
let(:word) { 'word' }
|
251
|
+
|
252
|
+
it 'delegates to #is_word?' do
|
253
|
+
[true, false].each do |value|
|
254
|
+
root.stub(:is_word?).with(word).and_return value
|
255
|
+
root.include?(word).should &method("be_#{value}".to_sym)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
247
259
|
end
|
248
260
|
end
|
249
261
|
end
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
module Rambling
|
4
4
|
describe Trie do
|
5
5
|
describe '.create' do
|
6
|
-
let(:root) { double
|
6
|
+
let(:root) { double 'Trie::Root' }
|
7
7
|
|
8
8
|
before :each do
|
9
9
|
Trie::Root.stub(:new).and_return root
|
@@ -13,5 +13,22 @@ module Rambling
|
|
13
13
|
Trie.create.should == root
|
14
14
|
end
|
15
15
|
end
|
16
|
+
|
17
|
+
describe '.new' do
|
18
|
+
let(:root) { double 'Trie::Root' }
|
19
|
+
|
20
|
+
before :each do
|
21
|
+
Trie.should_receive(:create).and_return root
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns the new trie root node instance' do
|
25
|
+
Trie.new.should == root
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'warns about deprecation' do
|
29
|
+
Trie.should_receive(:warn).with '[DEPRECATION] `new` is deprecated. Please use `create` instead.'
|
30
|
+
Trie.new
|
31
|
+
end
|
32
|
+
end
|
16
33
|
end
|
17
34
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rambling-trie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-07-
|
12
|
+
date: 2012-07-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -118,10 +118,11 @@ files:
|
|
118
118
|
- lib/rambling-trie/tasks/gem.rb
|
119
119
|
- lib/rambling-trie/tasks/performance.rb
|
120
120
|
- lib/rambling-trie/version.rb
|
121
|
-
- lib/rambling.rb
|
122
121
|
- rambling-trie.gemspec
|
123
122
|
- reports/performance
|
124
123
|
- spec/assets/test_words.txt
|
124
|
+
- spec/lib/rambling-trie/branches_spec.rb
|
125
|
+
- spec/lib/rambling-trie/children_hash_deferer_spec.rb
|
125
126
|
- spec/lib/rambling-trie/node_spec.rb
|
126
127
|
- spec/lib/rambling-trie/root_spec.rb
|
127
128
|
- spec/lib/rambling-trie_spec.rb
|
@@ -140,7 +141,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
141
|
version: '0'
|
141
142
|
segments:
|
142
143
|
- 0
|
143
|
-
hash:
|
144
|
+
hash: 1932693806363009255
|
144
145
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
146
|
none: false
|
146
147
|
requirements:
|
@@ -149,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
150
|
version: '0'
|
150
151
|
segments:
|
151
152
|
- 0
|
152
|
-
hash:
|
153
|
+
hash: 1932693806363009255
|
153
154
|
requirements: []
|
154
155
|
rubyforge_project:
|
155
156
|
rubygems_version: 1.8.24
|
data/lib/rambling.rb
DELETED