suffix_tree 0.0.1

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/lib/data/base_data_source.rb +44 -0
  3. data/lib/data/data_source_factory.rb +16 -0
  4. data/lib/data/file_data_source.rb +29 -0
  5. data/lib/data/line_state_machine.rb +86 -0
  6. data/lib/data/string_data_source.rb +31 -0
  7. data/lib/data/word_data_source.rb +229 -0
  8. data/lib/location.rb +165 -0
  9. data/lib/node.rb +63 -0
  10. data/lib/node_factory.rb +169 -0
  11. data/lib/persist/suffix_tree_db.rb +148 -0
  12. data/lib/search/searcher.rb +68 -0
  13. data/lib/suffix_linker.rb +16 -0
  14. data/lib/suffix_tree.rb +122 -0
  15. data/lib/visitor/base_visitor.rb +17 -0
  16. data/lib/visitor/bfs.rb +22 -0
  17. data/lib/visitor/data_source_visitor.rb +15 -0
  18. data/lib/visitor/dfs.rb +34 -0
  19. data/lib/visitor/k_common_visitor.rb +71 -0
  20. data/lib/visitor/leaf_count_visitor.rb +15 -0
  21. data/lib/visitor/node_count_visitor.rb +16 -0
  22. data/lib/visitor/numbering_visitor.rb +230 -0
  23. data/lib/visitor/suffix_offset_visitor.rb +23 -0
  24. data/lib/visitor/tree_print_visitor.rb +44 -0
  25. data/lib/visitor/value_depth_visitor.rb +34 -0
  26. data/spec/constant_lca_spec.rb +27 -0
  27. data/spec/data_source_spec.rb +51 -0
  28. data/spec/fixtures/arizona.txt +1 -0
  29. data/spec/fixtures/chapter1.txt +371 -0
  30. data/spec/fixtures/chapter1.txt.summary +3 -0
  31. data/spec/fixtures/chapter1.txt.values +0 -0
  32. data/spec/fixtures/chapter1.txt.words +1329 -0
  33. data/spec/fixtures/mississippi.txt +1 -0
  34. data/spec/fixtures/singlePara.txt +41 -0
  35. data/spec/fixtures/smallFile.txt +3 -0
  36. data/spec/fixtures/smallFile.txt.summary +2 -0
  37. data/spec/fixtures/smallFile.txt.values +0 -0
  38. data/spec/fixtures/smallFile.txt.words +14 -0
  39. data/spec/fixtures/testbook.txt +5414 -0
  40. data/spec/location_spec.rb +149 -0
  41. data/spec/node_factory_spec.rb +199 -0
  42. data/spec/search_spec.rb +182 -0
  43. data/spec/suffix_tree_spec.rb +270 -0
  44. data/spec/util_spec.rb +47 -0
  45. data/spec/visitor_spec.rb +310 -0
  46. metadata +87 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 188a9ad0d6fdd0a21bd58f8715abf6b0fb8643e5
4
+ data.tar.gz: 64f9ef00d6bba6b8491c73e64e314b7b9e9b6003
5
+ SHA512:
6
+ metadata.gz: 3101607dbc019a2a6e8ccc0ca271b909d5e2d2ce14ee23ed59e2725a0ee174d513ad2e5c427b7f6fcd7abfb5e617d719b38c7a95379aab65a5f0f2ae13ad5f95
7
+ data.tar.gz: c960dd1df4823fdfbe089642846db70016a30a1adda1f9ddd39858937937980686f135a0004348f54c0a22eea72881f29e619a834db45a081e3d41b37e0af0b6
@@ -0,0 +1,44 @@
1
+ class BaseDataSource
2
+ attr_accessor :startOffset
3
+
4
+ def initialize(startOffset = 0)
5
+ @nextDataSource = nil
6
+ @startOffset = startOffset
7
+ end
8
+
9
+ def each_with_index(offset = 0)
10
+ while ((value = self.valueAt(offset)) != nil) do
11
+ yield value, offset
12
+ offset += 1
13
+ end
14
+ end
15
+
16
+ def extendWith(dataSource, startOffset)
17
+ if (@nextDataSource == nil) then
18
+ @nextDataSource = dataSource
19
+ dataSource.startOffset = startOffset
20
+ else
21
+ @nextDataSource.extendWith(dataSource, startOffset)
22
+ end
23
+ end
24
+
25
+ def has_terminator?
26
+ false
27
+ end
28
+
29
+ def nextDataSourceValueAt(offset)
30
+ if (@nextDataSource != nil) then
31
+ return @nextDataSource.valueAt(offset)
32
+ else
33
+ return nil
34
+ end
35
+ end
36
+
37
+ def valueSequence(startOffset, endOffset)
38
+ result = ""
39
+ (startOffset..endOffset).each do |offset|
40
+ result += self.valueAt(offset)
41
+ end
42
+ result
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'string_data_source'
2
+ require_relative 'file_data_source'
3
+
4
+ class DataSourceFactory
5
+
6
+ STRING_DATA_SOURCE = 'string'
7
+ FILE_DATA_SOURCE = 'file'
8
+
9
+ def newDataSource(dataSourceType, dataSourceValue)
10
+ if (dataSourceType == STRING_DATA_SOURCE) then
11
+ return StringDataSource.new(dataSourceValue)
12
+ elsif (dataSourceType == FILE_DATA_SOURCE) then
13
+ return FileDataSource.new(dataSourceValue)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'base_data_source'
2
+
3
+ class FileDataSource < BaseDataSource
4
+ def initialize(path)
5
+ @inFile = File.open(path, "rb")
6
+ @checkFile = File.open(path, "rb")
7
+ super(0)
8
+ end
9
+
10
+ def valueAt(offset)
11
+ @checkFile.seek(offset - @startOffset, IO::SEEK_SET)
12
+ result = @checkFile.getc
13
+ if (result == nil) then
14
+ return self.nextDataSourceValueAt(offset)
15
+ end
16
+ result
17
+ end
18
+
19
+ # substring
20
+ def toString(startOffset, endOffset)
21
+ @checkFile.seek(startOffset - @startOffset, IO::SEEK_SET)
22
+ if (endOffset >= startOffset) then
23
+ return @checkFile.read(endOffset - startOffset + 1)
24
+ else
25
+ return @checkFile.read()
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,86 @@
1
+ require 'state_machine'
2
+
3
+ #
4
+ # First pass at state machine for converting sequence of formatted lines into a different
5
+ # set of word values, in this case "<N>, blank, |, blank, <footer title>" get converted
6
+ # into [ "END_OF_PAGE", "<page number>", "<title as a single word>"]
7
+ #
8
+ class LineStateMachine
9
+ attr_accessor :bucket, :pages
10
+
11
+ def initialize
12
+ @bucket = "Page 0"
13
+ @pages = {}
14
+ @dataQueue = []
15
+ super
16
+ end
17
+
18
+ def resetState(data)
19
+ self.reset
20
+ result = []
21
+ result << @dataQueue
22
+ result << data
23
+ @dataQueue = []
24
+ return result.flatten
25
+ end
26
+
27
+ def process(line, wordIndex)
28
+ data = line.split
29
+
30
+ # we are looking for a blank, a pipe, or a page number
31
+ if (data.length == 0) then
32
+ if (self.foundBlank) then
33
+ return []
34
+ end
35
+ end
36
+ if (data.length == 1) then
37
+ if (data[0] == "|") then
38
+ if (self.foundPipe) then
39
+ return []
40
+ end
41
+ end
42
+ ival = data[0].to_i
43
+ if (ival > 0) then
44
+ if (self.foundN) then
45
+ @potentialPageNumber = ival
46
+ @dataQueue << data # in case this really isn't it
47
+ return []
48
+ end
49
+ end
50
+ end
51
+
52
+ # if we are looking for the title, the entire line is the title
53
+ if (data.length > 0) then
54
+ if (self.foundTitle) then
55
+ @dataQueue = []
56
+ @bucket = "Page #{@potentialPageNumber}"
57
+ @pages[@bucket] = wordIndex
58
+ return []
59
+ end
60
+ end
61
+
62
+ resetState(data)
63
+ end
64
+
65
+ state_machine :state, :initial => :lookingForN do
66
+ event :foundN do
67
+ transition :lookingForN => :lookingForFirstBlank
68
+ end
69
+
70
+ event :foundBlank do
71
+ transition :lookingForFirstBlank => :lookingForPipe, :lookingForSecondBlank => :lookingForTitle
72
+ end
73
+
74
+ event :foundPipe do
75
+ transition :lookingForPipe => :lookingForSecondBlank
76
+ end
77
+
78
+ event :foundTitle do
79
+ transition :lookingForTitle => :lookingForN
80
+ end
81
+
82
+ event :reset do
83
+ transition all => :lookingForN
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'base_data_source'
2
+
3
+ class StringDataSource < BaseDataSource
4
+
5
+ def initialize(s)
6
+ @s = s
7
+ super()
8
+ end
9
+
10
+ def numberValues
11
+ return @s.length
12
+ end
13
+
14
+ def valueAt(offset)
15
+ value = @s[ offset - @startOffset ]
16
+ if (value == nil) then
17
+ return self.nextDataSourceValueAt(offset)
18
+ else
19
+ return value
20
+ end
21
+ end
22
+
23
+ # substring
24
+ def toString(startOffset, endOffset)
25
+ if (endOffset >= startOffset) then
26
+ return @s[startOffset..endOffset]
27
+ else
28
+ return @s[startOffset..(@s.length - 1)]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,229 @@
1
+ require_relative 'base_data_source'
2
+
3
+ class WordDataSource < BaseDataSource
4
+ attr_reader :words, :numberWordsInFile
5
+
6
+ def initialize(filePath, regex = "/[^a-z0-9\-\s]/i")
7
+ @filePath = filePath
8
+ @words = []
9
+ @regex = regex
10
+ File.open(filePath, "r") do |file|
11
+ file.each_line do |line|
12
+ line.chomp!
13
+ if (self.process(line)) then
14
+ break
15
+ end
16
+ end
17
+ end
18
+ @numberWordsInFile = @words.length
19
+ end
20
+
21
+ def numberValues
22
+ return @words.length
23
+ end
24
+
25
+ def process(line)
26
+ line = self.preprocessLine(line)
27
+ return self.processData(line.split)
28
+ end
29
+
30
+ def processData(data)
31
+ data.each do |word|
32
+ word = word.chomp(",")
33
+ @words << word
34
+ end
35
+ return false
36
+ end
37
+
38
+ def preprocessLine(line)
39
+ line.downcase.gsub(@regex, ' ')
40
+ end
41
+
42
+ def valueAt(offset)
43
+ return @words[offset] if (offset < @numberWordsInFile)
44
+ return nil
45
+ end
46
+
47
+ def toString(startOffset, endOffset)
48
+ if (endOffset == -1) then
49
+ result = "#{@words[startOffset]} ..*"
50
+ else
51
+ result = ""
52
+ (startOffset..endOffset).each do |offset|
53
+ result += "#{@words[offset]} "
54
+ end
55
+ end
56
+ result
57
+ end
58
+ end
59
+
60
+ class SingleWordDataSource < BaseDataSource
61
+ def initialize(word)
62
+ @word = word
63
+ end
64
+
65
+ def numberValues
66
+ return 1
67
+ end
68
+
69
+ def valueAt(offset)
70
+ return nil if (offset > 0)
71
+ return @word
72
+ end
73
+ end
74
+
75
+ class ArrayWordDataSource
76
+ attr_reader :wordCounts
77
+
78
+ def initialize(wordList, offsetList, size)
79
+ @wordList = wordList
80
+ @offsetList = offsetList
81
+ @size = size
82
+ @wordCounts = createWordCounts
83
+ end
84
+
85
+ def valueAt(offset)
86
+ if (offset < @size) then
87
+ return @wordList[@offsetList[offset]]
88
+ else
89
+ return nil
90
+ end
91
+ end
92
+
93
+ def verify(word, count)
94
+ if (@wordCounts == nil) then
95
+ createWordCounts
96
+ end
97
+ @wordCounts[word] == count
98
+ end
99
+
100
+ def each_word(offset = 0)
101
+ while ((value = self.valueAt(offset)) != nil) do
102
+ yield value
103
+ offset += 1
104
+ end
105
+ end
106
+
107
+ private
108
+ def createWordCounts()
109
+ wordCounts = {}
110
+ @wordList.each do |word|
111
+ if (!wordCounts.has_key?(word)) then
112
+ wordCounts[word] = 0
113
+ end
114
+ wordCounts[word] += 1
115
+ end
116
+ wordCounts
117
+ end
118
+ end
119
+
120
+ class DelimitedWordDataSource < WordDataSource
121
+ attr_reader :buckets, :wordCounts, :wordAsEncountered, :wordValueSequence
122
+
123
+ def initialize(filePath, lineStateMachine, limit)
124
+ @lineStateMachine = lineStateMachine
125
+ @limit = limit
126
+ @count = 0
127
+ @buckets = {}
128
+ @wordCounts = {}
129
+ @wordValueSequence = [] # list of words in file in terms of index into @wordAsEncountered
130
+ @wordAsEncounteredIndex = {} # key is word, value is number as encountered
131
+ @wordAsEncountered = [] # array entry added only when a new word is encountered
132
+ @nextWordEncounteredIndex = 0
133
+ super(filePath,"/[^[:print:]]/")
134
+ end
135
+
136
+ def bucket
137
+ @lineStateMachine.bucket
138
+ end
139
+
140
+ def save
141
+ File.open("#{@filePath}.words", 'w') do |file|
142
+ @wordAsEncountered.each do |word|
143
+ file.write("#{word}\n")
144
+ end
145
+ end
146
+ File.open("#{@filePath}.values", 'wb') do |file|
147
+ file << @wordValueSequence.pack("N*")
148
+ end
149
+ File.open("#{@filePath}.summary", "w") do |file|
150
+ file << "#{@numberWordsInFile} words in file\n"
151
+ file << "#{@nextWordEncounteredIndex} distinct words\n"
152
+ file << "Metadata\n"
153
+
154
+ # uh-oh, this seems to reverse the hash in place!
155
+ @lineStateMachine.pages.sort_by(&:reverse).each do |page, wordOffset|
156
+ file << "#{wordOffset} #{page}\n"
157
+ end
158
+ end
159
+ end
160
+
161
+ # TODO: fix this, linear metadata search, O(N) should be O(lg N)
162
+ def metaDataFor(offset)
163
+ previousMetadata = "unknown"
164
+ @lineStateMachine.pages.sort_by(&:reverse).each do |metadata, wordOffset|
165
+ if (wordOffset < offset) then
166
+ previousMetadata = metadata
167
+ else
168
+ return previousMetadata
169
+ end
170
+ end
171
+ return previousMetadata
172
+ end
173
+
174
+ def wordCount(word)
175
+ return @wordCounts[word] if @wordCounts.has_key?(word)
176
+ return 0
177
+ end
178
+
179
+ def processData(data,bucket)
180
+ data.each do |word|
181
+ word = word.chomp(",")
182
+ word = word.chomp(".")
183
+ if (word.length > 0) then
184
+ @words << word
185
+ if (!@wordCounts.has_key?(word)) then
186
+ # we have a new word
187
+ @wordAsEncounteredIndex[word] = @nextWordEncounteredIndex
188
+ @wordAsEncountered << word
189
+ @nextWordEncounteredIndex += 1
190
+ @wordCounts[word] = 0
191
+ end
192
+ @wordCounts[word] += 1
193
+ if (!@buckets[bucket].has_key?(word)) then
194
+ @buckets[bucket][word] = 0
195
+ end
196
+ @buckets[bucket][word] += 1
197
+ @wordValueSequence << @wordAsEncounteredIndex[word]
198
+ @count += 1
199
+ if ((@limit > 0) && (@count >= @limit)) then
200
+ return true
201
+ end
202
+ end
203
+ end
204
+ return false
205
+ end
206
+
207
+ def process(line)
208
+ line = self.preprocessLine(line)
209
+ data = @lineStateMachine.process(line, @wordValueSequence.length)
210
+ if (data.length > 0) then
211
+ bucket = @lineStateMachine.bucket
212
+ @buckets[bucket] = {} if (!@buckets.has_key?(bucket))
213
+ return self.processData(data,bucket)
214
+ end
215
+ return false
216
+ end
217
+
218
+ def verify(word, count)
219
+ @wordCounts[word] == count
220
+ end
221
+
222
+ def has_terminator?
223
+ true
224
+ end
225
+
226
+ def terminator
227
+ "END_OF_DOCUMENT"
228
+ end
229
+ end
data/lib/location.rb ADDED
@@ -0,0 +1,165 @@
1
+ require_relative 'node'
2
+
3
+ #
4
+ # This class keeps track of the next value to check in a suffix tree
5
+ #
6
+ # If we are located at a node, there are several options for the next value
7
+ # which are in the map of value-to-node.
8
+ #
9
+ # If we are not on a node, there is an incoming edge with at least one value
10
+ # so we store the offset of that value in the data source
11
+ #
12
+ # The location can never be onNode at a leaf, but can be at a leaf with
13
+ # an incomingEdgeOffset at or past the leaf's incomingEdgeStartOffset
14
+ #
15
+ class Location
16
+ attr_reader :node, :onNode, :incomingEdgeOffset
17
+
18
+ #
19
+ # optional parameters needed for testing
20
+ #
21
+ def initialize(node, onNode = true, incomingEdgeOffset = Node::UNSPECIFIED_OFFSET)
22
+ @node = node
23
+ @onNode = onNode
24
+ @incomingEdgeOffset = incomingEdgeOffset
25
+ end
26
+
27
+ #
28
+ # traverse to parent, return the range of characters covered
29
+ #
30
+ def traverseUp
31
+ incomingEdgeStart = @node.incomingEdgeStartOffset
32
+ if (@onNode) then
33
+ incomingEdgeEnd = @node.incomingEdgeEndOffset
34
+ else
35
+ incomingEdgeEnd = @incomingEdgeOffset - 1
36
+ end
37
+ @node = @node.parent
38
+ @incomingEdgeOffset = Node::UNSPECIFIED_OFFSET
39
+ @onNode = true
40
+ return incomingEdgeStart, incomingEdgeEnd
41
+ end
42
+
43
+ def traverseSuffixLink
44
+ self.jumpToNode(@node.suffixLink)
45
+ end
46
+
47
+ #
48
+ # From the current Node with a given child value, traverse past that value
49
+ #
50
+ def traverseDownChildValue(value)
51
+ @node = @node.children[value]
52
+ if (@node.incomingEdgeLength == 1) then
53
+ @onNode = true
54
+ @incomingEdgeOffset = Node::UNSPECIFIED_OFFSET
55
+ else
56
+ @onNode = false
57
+ @incomingEdgeOffset = @node.incomingEdgeStartOffset + 1
58
+ end
59
+ end
60
+
61
+ #
62
+ # From the current location that does NOT have a suffix link, either because it
63
+ # is on an edge or because it is on a newly created internal node, traverse
64
+ # to the next suffix
65
+ #
66
+ # Returns true if it actually traversed, otherwise false
67
+ #
68
+ def traverseToNextSuffix(dataSource)
69
+ if (@node.isRoot) then
70
+ return false
71
+ end
72
+ upStart, upEnd = self.traverseUp
73
+ if (@node.isRoot) then
74
+ if (upStart < upEnd) then
75
+ self.traverseSkipCountDown(dataSource, upStart + 1, upEnd)
76
+ else
77
+ @onNode = true
78
+ end
79
+ else
80
+ @node = @node.suffixLink
81
+ self.traverseSkipCountDown(dataSource, upStart, upEnd)
82
+ end
83
+ return true
84
+ end
85
+
86
+ #
87
+ # From the current location on a Node, traverse down assuming the characters
88
+ # on the path exist, which allows skip/count method to be used to move down.
89
+ #
90
+ def traverseSkipCountDown(dataSource, startOffset, endOffset)
91
+ done = false
92
+ while (!done) do
93
+ @node = @node.children[dataSource.valueAt(startOffset)]
94
+ if (@node.isLeaf) then
95
+ @onNode = false
96
+ @incomingEdgeOffset = @node.incomingEdgeStartOffset + (endOffset - startOffset + 1)
97
+ else
98
+ incomingEdgeLength = @node.incomingEdgeLength
99
+ startOffset += incomingEdgeLength
100
+ remainingLength = endOffset - startOffset + 1
101
+ @onNode = (remainingLength == 0)
102
+ # if remaining length is negative, it means we have past where we need to be
103
+ # by that amount, incoming edge offset is set to end reduced by that amount
104
+ if (remainingLength < 0) then
105
+ @incomingEdgeOffset = @node.incomingEdgeEndOffset + remainingLength + 1
106
+ else
107
+ @incomingEdgeOffset = @node.incomingEdgeStartOffset
108
+ end
109
+ end
110
+
111
+ done = (@node.isLeaf || (remainingLength <= 0))
112
+ end
113
+ end
114
+
115
+ def traverseDownEdgeValue()
116
+ @incomingEdgeOffset += 1
117
+ if (!@node.isLeaf && (@incomingEdgeOffset > @node.incomingEdgeEndOffset)) then
118
+ @onNode = true
119
+ end
120
+ end
121
+
122
+ def matchDataSource(dataSource, matchThis)
123
+ matchThis.each_with_index do |value, index|
124
+ if (!self.matchValue(dataSource, value)) then
125
+ break
126
+ end
127
+ end
128
+ self
129
+ end
130
+
131
+ def matchValue(dataSource, value)
132
+ if (@onNode) then
133
+ if (@node.children.has_key?(value)) then
134
+ self.traverseDownChildValue(value)
135
+ return true
136
+ end
137
+ else
138
+ if (dataSource.valueAt(@incomingEdgeOffset) == value) then
139
+ self.traverseDownEdgeValue()
140
+ return true
141
+ end
142
+ end
143
+ return false
144
+ end
145
+
146
+ #
147
+ # get the depth of the location
148
+ #
149
+ # Requires nodes with "valueDepth" property (nodeFactory with :valueDepth=>true, followed by traversal with ValueDepthVisitor)
150
+ #
151
+ def depth
152
+ if (@onNode) then
153
+ return @node.valueDepth
154
+ else
155
+ return @node.parent.valueDepth + @incomingEdgeOffset - @node.incomingEdgeStartOffset
156
+ end
157
+ end
158
+
159
+ def jumpToNode(node)
160
+ @node = node
161
+ @onNode = true
162
+ @incomingEdgeOffset = Node::UNSPECIFIED_OFFSET
163
+ end
164
+
165
+ end
data/lib/node.rb ADDED
@@ -0,0 +1,63 @@
1
+ class Node
2
+ # Leaf nodes use this due to Rule 1: once a leaf, always a leaf
3
+ CURRENT_ENDING_OFFSET = -1
4
+
5
+ # Root uses this, it has no incoming edge, yet as a Node has incoming edge offset properties
6
+ UNSPECIFIED_OFFSET = -2
7
+
8
+ # Leaf nodes get special depth, since they vary as characters get added
9
+ LEAF_DEPTH = -3
10
+
11
+ attr_accessor :incomingEdgeStartOffset, :incomingEdgeEndOffset, :suffixOffset
12
+ attr_accessor :parent, :suffixLink, :children
13
+ attr_reader :nodeId
14
+
15
+ def initialize(nodeId, suffixOffset = UNSPECIFIED_OFFSET)
16
+ @nodeId = nodeId
17
+ @incomingEdgeStartOffset = UNSPECIFIED_OFFSET
18
+ @incomingEdgeEndOffset = UNSPECIFIED_OFFSET
19
+ @suffixOffset = suffixOffset
20
+
21
+ @parent = nil
22
+ @suffixLink = nil
23
+ @children = nil
24
+ end
25
+
26
+ def isRoot
27
+ return @parent == nil
28
+ end
29
+
30
+ def isLeaf
31
+ return @incomingEdgeEndOffset == CURRENT_ENDING_OFFSET
32
+ end
33
+
34
+ def isInternal
35
+ return !isLeaf && !isRoot
36
+ end
37
+
38
+ def incomingEdgeLength
39
+ return @incomingEdgeEndOffset - @incomingEdgeStartOffset + 1
40
+ end
41
+
42
+ #
43
+ # some algorithms require additional accessors, allow these to be created dynamically
44
+ #
45
+ def createAccessor(name)
46
+ self.class.send(:attr_accessor, name)
47
+ end
48
+
49
+ #
50
+ # suffix offset enumerator (not sure this belongs here)
51
+ #
52
+ def each_suffix
53
+ if (self.isLeaf) then
54
+ yield suffixOffset
55
+ else
56
+ children.keys.sort.each do |key|
57
+ children[key].each_suffix do |suffixOffset|
58
+ yield suffixOffset
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end