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.
- checksums.yaml +7 -0
- data/lib/data/base_data_source.rb +44 -0
- data/lib/data/data_source_factory.rb +16 -0
- data/lib/data/file_data_source.rb +29 -0
- data/lib/data/line_state_machine.rb +86 -0
- data/lib/data/string_data_source.rb +31 -0
- data/lib/data/word_data_source.rb +229 -0
- data/lib/location.rb +165 -0
- data/lib/node.rb +63 -0
- data/lib/node_factory.rb +169 -0
- data/lib/persist/suffix_tree_db.rb +148 -0
- data/lib/search/searcher.rb +68 -0
- data/lib/suffix_linker.rb +16 -0
- data/lib/suffix_tree.rb +122 -0
- data/lib/visitor/base_visitor.rb +17 -0
- data/lib/visitor/bfs.rb +22 -0
- data/lib/visitor/data_source_visitor.rb +15 -0
- data/lib/visitor/dfs.rb +34 -0
- data/lib/visitor/k_common_visitor.rb +71 -0
- data/lib/visitor/leaf_count_visitor.rb +15 -0
- data/lib/visitor/node_count_visitor.rb +16 -0
- data/lib/visitor/numbering_visitor.rb +230 -0
- data/lib/visitor/suffix_offset_visitor.rb +23 -0
- data/lib/visitor/tree_print_visitor.rb +44 -0
- data/lib/visitor/value_depth_visitor.rb +34 -0
- data/spec/constant_lca_spec.rb +27 -0
- data/spec/data_source_spec.rb +51 -0
- data/spec/fixtures/arizona.txt +1 -0
- data/spec/fixtures/chapter1.txt +371 -0
- data/spec/fixtures/chapter1.txt.summary +3 -0
- data/spec/fixtures/chapter1.txt.values +0 -0
- data/spec/fixtures/chapter1.txt.words +1329 -0
- data/spec/fixtures/mississippi.txt +1 -0
- data/spec/fixtures/singlePara.txt +41 -0
- data/spec/fixtures/smallFile.txt +3 -0
- data/spec/fixtures/smallFile.txt.summary +2 -0
- data/spec/fixtures/smallFile.txt.values +0 -0
- data/spec/fixtures/smallFile.txt.words +14 -0
- data/spec/fixtures/testbook.txt +5414 -0
- data/spec/location_spec.rb +149 -0
- data/spec/node_factory_spec.rb +199 -0
- data/spec/search_spec.rb +182 -0
- data/spec/suffix_tree_spec.rb +270 -0
- data/spec/util_spec.rb +47 -0
- data/spec/visitor_spec.rb +310 -0
- 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
|