mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-20 23:37:33 +09:00
340 lines
11 KiB
JavaScript
340 lines
11 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
this.EXPORTED_SYMBOLS = ["SuffixTree", "MultiSuffixTree"];
|
|
|
|
/**
|
|
* Given a list of strings and a corresponding map of items that those strings
|
|
* correspond to, build a suffix tree.
|
|
*/
|
|
function MultiSuffixTree(aStrings, aItems) {
|
|
if (aStrings.length != aItems.length)
|
|
throw new Error("Array lengths need to be the same.");
|
|
|
|
let s = '';
|
|
let offsetsToItems = [];
|
|
let lastLength = 0;
|
|
for (let i = 0; i < aStrings.length; i++) {
|
|
s += aStrings[i];
|
|
offsetsToItems.push(lastLength, s.length, aItems[i]);
|
|
lastLength = s.length;
|
|
}
|
|
|
|
this._construct(s);
|
|
this._offsetsToItems = offsetsToItems;
|
|
this._numItems = aItems.length;
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function State(aStartIndex, aEndIndex, aSuffix) {
|
|
this.start = aStartIndex;
|
|
this.end = aEndIndex;
|
|
this.suffix = aSuffix;
|
|
}
|
|
|
|
var dump;
|
|
if (dump === undefined) {
|
|
dump = function(a) {
|
|
print(a.slice(0, -1));
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Since objects are basically hash-tables anyways, we simply create an
|
|
* attribute whose name is the first letter of the edge string. (So, the
|
|
* edge string can conceptually be a multi-letter string, but since we would
|
|
* split it were there any ambiguity, it's okay to just use the single letter.)
|
|
* This avoids having to update the attribute name or worry about tripping our
|
|
* implementation up.
|
|
*/
|
|
State.prototype = {
|
|
get isExplicit() {
|
|
// our end is not inclusive...
|
|
return (this.end <= this.start);
|
|
},
|
|
get isImplicit() {
|
|
// our end is not inclusive...
|
|
return (this.end > this.start);
|
|
},
|
|
|
|
get length() {
|
|
return this.end - this.start;
|
|
},
|
|
|
|
toString: function State_toString() {
|
|
return "[Start: " + this.start + " End: " + this.end +
|
|
(this.suffix ? " non-null suffix]" : " null suffix]");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Suffix tree implemented using Ukkonen's algorithm.
|
|
* @constructor
|
|
*/
|
|
function SuffixTree(aStr) {
|
|
this._construct(aStr);
|
|
}
|
|
|
|
/**
|
|
* States are
|
|
*/
|
|
SuffixTree.prototype = {
|
|
/**
|
|
* Find all items matching the provided substring.
|
|
*/
|
|
findMatches: function findMatches(aSubstring) {
|
|
let results = [];
|
|
let state = this._root;
|
|
let index=0;
|
|
let end = aSubstring.length;
|
|
while(index < end) {
|
|
state = state[aSubstring[index]];
|
|
// bail if there was no edge
|
|
if (state === undefined)
|
|
return results;
|
|
// bail if the portion of the edge we traversed is not equal to that
|
|
// portion of our pattern
|
|
let actualTraverseLength = Math.min(state.length,
|
|
end - index);
|
|
if (this._str.substring(state.start,
|
|
state.start + actualTraverseLength) !=
|
|
aSubstring.substring(index, index + actualTraverseLength))
|
|
return results;
|
|
index += state.length;
|
|
}
|
|
|
|
// state should now be the node which itself and all its children match...
|
|
// The delta is to adjust us to the offset of the last letter of our match;
|
|
// the edge we traversed to get here may have found us traversing more
|
|
// than we wanted.
|
|
// index - end captures the over-shoot of the edge traversal,
|
|
// index - end + 1 captures the fact that we want to find the last letter
|
|
// that matched, not just the first letter beyond it
|
|
// However, if this state is a leaf node (end == 'infinity'), then 'end'
|
|
// isn't describing an edge at all and we want to avoid accounting for it.
|
|
let delta;
|
|
/*
|
|
if (state.end != this._infinity)
|
|
//delta = index - end + 1;
|
|
delta = end - (index - state.length);
|
|
else */
|
|
delta = index - state.length - end + 1;
|
|
|
|
this._resultGather(state, results, {}, end, delta, true);
|
|
return results;
|
|
},
|
|
|
|
_resultGather: function resultGather(aState, aResults, aPresence,
|
|
aPatLength, aDelta, alreadyAdjusted) {
|
|
// find the item that this state originated from based on the state's
|
|
// start character. offsetToItem holds [string start index, string end
|
|
// index (exclusive), item reference]. So we want to binary search to
|
|
// find the string whose start/end index contains the state's start index.
|
|
let low = 0;
|
|
let high = this._numItems-1;
|
|
let mid, stringStart, stringEnd;
|
|
|
|
let patternLast = aState.start - aDelta;
|
|
while (low <= high) {
|
|
mid = low + Math.floor((high - low) / 2); // excessive, especially with js nums
|
|
stringStart = this._offsetsToItems[mid*3];
|
|
let startDelta = stringStart - patternLast;
|
|
stringEnd = this._offsetsToItems[mid*3+1];
|
|
let endDelta = stringEnd - patternLast;
|
|
if (startDelta > 0)
|
|
high = mid - 1;
|
|
else if (endDelta <= 0)
|
|
low = mid + 1;
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// - The match occurred completely inside a source string. Success.
|
|
// - The match spans more than one source strings, and is therefore not
|
|
// a match.
|
|
|
|
// at this point, we have located the origin string that corresponds to the
|
|
// start index of this state.
|
|
// - The match terminated with the end of the preceding string, and does
|
|
// not match us at all. We, and potentially our children, are merely
|
|
// serving as a unique terminal.
|
|
// - The
|
|
|
|
let patternFirst = patternLast - (aPatLength - 1);
|
|
|
|
if (patternFirst >= stringStart) {
|
|
if (!(stringStart in aPresence)) {
|
|
aPresence[stringStart] = true;
|
|
aResults.push(this._offsetsToItems[mid*3+2]);
|
|
}
|
|
}
|
|
|
|
// bail if we had it coming OR
|
|
// if the result terminates at/part-way through this state, meaning any
|
|
// of its children are not going to be actual results, just hangers
|
|
// on.
|
|
/*
|
|
if (bail || (end <= aState.end)) {
|
|
dump(" bailing! (bail was: " + bail + ")\n");
|
|
return;
|
|
}
|
|
*/
|
|
// process our children...
|
|
for (let key in aState) {
|
|
// edges have attributes of length 1...
|
|
if (key.length == 1) {
|
|
let statePrime = aState[key];
|
|
this._resultGather(statePrime, aResults, aPresence, aPatLength,
|
|
aDelta + aState.length, //(alreadyAdjusted ? 0 : aState.length),
|
|
false);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Given a reference 'pair' of a state and a string (may be 'empty'=explicit,
|
|
* which means no work to do and we return immediately) follow that state
|
|
* (and then the successive states)'s transitions until we run out of
|
|
* transitions. This happens either when we find an explicit state, or
|
|
* find ourselves partially along an edge (conceptually speaking). In
|
|
* the partial case, we return the state prior to the edge traversal.
|
|
* (The information about the 'edge' is contained on its target State;
|
|
* we can do this because a state is only referenced by one other state.)
|
|
*/
|
|
_canonize: function canonize(aState, aStart, aEnd) {
|
|
if (aEnd <= aStart) {
|
|
return [aState, aStart];
|
|
}
|
|
|
|
let statePrime;
|
|
// we treat an aState of null as 'bottom', which has transitions for every
|
|
// letter in the alphabet to 'root'. rather than create all those
|
|
// transitions, we special-case here.
|
|
if (aState === null)
|
|
statePrime = this._root;
|
|
else
|
|
statePrime = aState[this._str[aStart]];
|
|
while (statePrime.length <= aEnd - aStart) { // (no 1 adjustment required)
|
|
aStart += statePrime.length;
|
|
aState = statePrime;
|
|
if (aStart < aEnd) {
|
|
statePrime = aState[this._str[aStart]];
|
|
}
|
|
}
|
|
return [aState, aStart];
|
|
},
|
|
|
|
/**
|
|
* Given a reference 'pair' whose state may or may not be explicit (and for
|
|
* which we will perform the required splitting to make it explicit), test
|
|
* whether it already possesses a transition corresponding to the provided
|
|
* character.
|
|
* @return A list of: whether we had to make it explicit, the (potentially)
|
|
* new explicit state.
|
|
*/
|
|
_testAndSplit: function testAndSplit(aState, aStart, aEnd, aChar) {
|
|
if (aStart < aEnd) { // it's not explicit
|
|
let statePrime = aState[this._str[aStart]];
|
|
let length = aEnd - aStart;
|
|
if (aChar == this._str[statePrime.start + length]) {
|
|
return [true, aState];
|
|
}
|
|
else {
|
|
// do splitting... aState -> rState -> statePrime
|
|
let rState = new State(statePrime.start, statePrime.start + length);
|
|
aState[this._str[statePrime.start]] = rState;
|
|
statePrime.start += length;
|
|
rState[this._str[statePrime.start]] = statePrime;
|
|
return [false, rState];
|
|
}
|
|
}
|
|
else { // it's already explicit
|
|
if (aState === null) { // bottom case... shouldn't happen, but hey.
|
|
return [true, aState];
|
|
}
|
|
return [(aChar in aState), aState];
|
|
}
|
|
|
|
},
|
|
|
|
_update: function update(aState, aStart, aIndex) {
|
|
let oldR = this._root;
|
|
let textAtIndex = this._str[aIndex]; // T sub i (0-based corrected...)
|
|
// because of the way we store the 'end' value as a one-past form, we do
|
|
// not need to subtract 1 off of aIndex.
|
|
let [endPoint, rState] = this._testAndSplit(aState, aStart, aIndex, //no -1
|
|
textAtIndex);
|
|
while (!endPoint) {
|
|
let rPrime = new State(aIndex, this._infinity);
|
|
rState[textAtIndex] = rPrime;
|
|
if (oldR !== this._root)
|
|
oldR.suffix = rState;
|
|
oldR = rState;
|
|
[aState, aStart] = this._canonize(aState.suffix, aStart, aIndex); // no -1
|
|
[endPoint, rState] = this._testAndSplit(aState, aStart, aIndex, // no -1
|
|
textAtIndex);
|
|
}
|
|
if (oldR !== this._root)
|
|
oldR.suffix = aState;
|
|
|
|
return [aState, aStart];
|
|
},
|
|
|
|
_construct: function construct(aStr) {
|
|
this._str = aStr;
|
|
// just needs to be longer than the string.
|
|
this._infinity = aStr.length + 1;
|
|
|
|
//this._bottom = new State(0, -1, null);
|
|
this._root = new State(-1, 0, null); // null === bottom
|
|
let state = this._root;
|
|
let start = 0;
|
|
|
|
for (let i = 0; i < aStr.length; i++) {
|
|
[state, start] = this._update(state, start, i); // treat as flowing -1...
|
|
[state, start] = this._canonize(state, start, i+1); // 1-length string
|
|
}
|
|
},
|
|
|
|
dump: function SuffixTree_show(aState, aIndent, aKey) {
|
|
if (aState === undefined)
|
|
aState = this._root;
|
|
if (aIndent === undefined) {
|
|
aIndent = "";
|
|
aKey = ".";
|
|
}
|
|
|
|
if (aState.isImplicit) {
|
|
let snip;
|
|
if (aState.length > 10)
|
|
snip = this._str.slice(aState.start,
|
|
Math.min(aState.start+10, this._str.length)) + "...";
|
|
else
|
|
snip = this._str.slice(aState.start,
|
|
Math.min(aState.end, this._str.length));
|
|
dump(aIndent + aKey + ":" + snip + "(" +
|
|
aState.start + ":" + aState.end + ")\n");
|
|
}
|
|
else
|
|
dump(aIndent + aKey + ": (explicit:" + aState.start + ":" + aState.end +")\n");
|
|
let nextIndent = aIndent + " ";
|
|
let keys = Object.keys(aState).filter(c => c.length == 1);
|
|
for (let key of keys) {
|
|
this.dump(aState[key], nextIndent, key);
|
|
}
|
|
}
|
|
};
|
|
MultiSuffixTree.prototype = SuffixTree.prototype;
|
|
|
|
function examplar() {
|
|
let names = ["AndrewSmith", "AndrewJones", "MarkSmith", "BryanClark",
|
|
"MarthaJones", "DavidAscher", "DanMosedale", "DavidBienvenu",
|
|
"JanetDavis", "JosephBryant"];
|
|
let b = new MultiSuffixTree(names, names);
|
|
b.dump();
|
|
dump(b.findMatches("rya") + "\n");
|
|
}
|