fuzzy-rater
Version:
Tooling for fuzzily rating text according to some query
166 lines • 14.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WordOrderMatcher = void 0;
/**
* A class that can judge how well the matches match the order and spacing of the input query.
* Uses an approach similar to the dynamic programming approach for computing levenshtein distance.
*/
class WordOrderMatcher {
constructor(words, penalty = 10) {
var _a, _b, _c, _d;
this.words = words.map(word => typeof word == "string" ? { word, penalty } : word);
this.table = [[]];
for (let j = 0; j <= words.length; j++) {
this.table[0][j] = {
distance: ((_b = (_a = this.table[0][j - 1]) === null || _a === void 0 ? void 0 : _a.distance) !== null && _b !== void 0 ? _b : 0) +
((_d = (_c = this.words[j - 1]) === null || _c === void 0 ? void 0 : _c.penalty) !== null && _d !== void 0 ? _d : 0),
skippedWord: j > 0,
lastIndex: 0,
};
}
}
/**
* Rates how well the given sequence of words matches the input sequence
* @param matches The sequence of word matches
* @returns How well the given input order matches
*/
getMatch(matches) {
return this.findMatch(matches).distance;
}
/**
* Rates how well the given sequence of words matches the input sequence
* @param matches The sequence of word matches
* @returns How well the given input order matches, and what the last considered word was
*/
findMatch(matches) {
const words = this.words;
const table = this.table;
// Initialize the first column of values
for (let i = 1; i <= matches.length; i++)
table[i] = [{ distance: 0, skippedWord: false, lastIndex: 0 }];
// Compute the distances
for (let i = 1; i <= matches.length; i++) {
const match = matches[i - 1];
const { word: matchWord } = match;
for (let j = 1; j <= words.length; j++) {
const { word, penalty: missingWordRatio } = words[j - 1];
if (word == matchWord) {
// Consider either the best match for all words so far, or skipping all previous words (similar to "Largest Sum Contiguous Subarray" problem)
// All words so far
// Choose whether this match, or a previous match of the same word (with greater distance) is preferable
const prevWord = table[i - 1][j - 1];
const prevWordAddedDistance = Math.abs(match.index -
prevWord.lastIndex -
// Subtract 1, since we expect spaces to be present (this slightly penalizes no spaces being present)
1);
let extendedMatch = {
distance: prevWord.distance + prevWordAddedDistance + match.cost,
skippedWord: false,
lastIndex: match.endIndex,
};
const prevMatch = table[i - 1][j];
if (
// If the extra distance of the new match is greater than the extra lastIndex distance of the previous match, use the previous match
extendedMatch.distance - prevMatch.distance >
extendedMatch.lastIndex - prevMatch.lastIndex)
extendedMatch = prevMatch;
// Skipping previous words, since the penalties may be better than the distance from previous words
const newMatch = {
distance: table[0][j - 1].distance + match.cost,
skippedWord: true,
lastIndex: match.endIndex,
};
// Choose the best option
table[i][j] =
newMatch.distance <= extendedMatch.distance
? newMatch
: extendedMatch;
}
else {
// Compute option of either skipping the query word, or the input match
const skipWord = table[i][j - 1];
const skipWordNew = {
distance: missingWordRatio + skipWord.distance,
skippedWord: true,
lastIndex: skipWord.lastIndex,
};
const skipMatch = table[i - 1][j];
const skipMatchNew = {
distance: skipMatch.distance,
skippedWord: false,
lastIndex: skipMatch.lastIndex,
};
// IF the distance is equally expensive, choose for the furthest match, since it will make for cheaper extended matches
if (skipMatchNew.distance == skipWordNew.distance)
table[i][j] =
skipMatchNew.lastIndex >= skipWordNew.lastIndex
? skipMatchNew
: skipWordNew;
// If one is cheaper than the other, choose the cheapest
else
table[i][j] =
skipMatchNew.distance <= skipWordNew.distance
? skipMatchNew
: skipWordNew;
}
}
}
// Find the best result
let bestMatch = table[0][words.length];
let bestIndex = 0;
for (let i = 1; i <= matches.length; i++) {
if (table[i][words.length].distance < bestMatch.distance) {
bestMatch = table[i][words.length];
bestIndex = i;
}
}
return { lastMatchIndex: bestIndex, distance: bestMatch.distance };
}
/**
* Rates how well the given sequence of words matches the input sequence, and returns which were matched
* @param matches The sequence of word matches
* @returns How well the given input order matches, and the used matches
*/
getMatchData(matches) {
const words = this.words;
const table = this.table;
const match = this.findMatch(matches);
const res = [];
// Follow the best trace backwards and add all matches to the result
let i = match.lastMatchIndex, j = words.length;
while (i > 0 && j > 0) {
let m = table[i][j];
if (words[j - 1].word == matches[i - 1].word) {
if (m.lastIndex != matches[i - 1].endIndex) {
i -= 1;
}
else {
res.unshift({
match: matches[i - 1],
matchIndex: i - 1,
wordIndex: j - 1,
});
if (m.skippedWord) {
break;
}
else {
i -= 1;
j -= 1;
}
}
}
else {
if (m.skippedWord) {
j -= 1;
}
else {
i -= 1;
}
}
}
// return the result
return { distance: match.distance, matches: res };
}
}
exports.WordOrderMatcher = WordOrderMatcher;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiV29yZE9yZGVyTWF0Y2hlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9mdXp6eVJhdGUvb3JkZXJNYXRjaGVyL1dvcmRPcmRlck1hdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBS0E7OztHQUdHO0FBQ0gsTUFBYSxnQkFBZ0I7SUFpQnpCLFlBQ0ksS0FBNEMsRUFDNUMsVUFBa0IsRUFBRTs7UUFFcEIsSUFBSSxDQUFDLEtBQUssR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQzFCLE9BQU8sSUFBSSxJQUFJLFFBQVEsQ0FBQyxDQUFDLENBQUMsRUFBQyxJQUFJLEVBQUUsT0FBTyxFQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FDbkQsQ0FBQztRQUNGLElBQUksQ0FBQyxLQUFLLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNsQixLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLElBQUksS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUNwQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHO2dCQUNmLFFBQVEsRUFDSixhQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQywwQ0FBRSxRQUFRLG1DQUFJLENBQUMsQ0FBQztvQkFDckMsYUFBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsMENBQUUsT0FBTyxtQ0FBSSxDQUFDLENBQUM7Z0JBQ3JDLFdBQVcsRUFBRSxDQUFDLEdBQUcsQ0FBQztnQkFDbEIsU0FBUyxFQUFFLENBQUM7YUFDZixDQUFDO1NBQ0w7SUFDTCxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLFFBQVEsQ0FBQyxPQUErQjtRQUMzQyxPQUFPLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUMsUUFBUSxDQUFDO0lBQzVDLENBQUM7SUFFRDs7OztPQUlHO0lBQ08sU0FBUyxDQUNmLE9BQStCO1FBRS9CLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUM7UUFDekIsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUV6Qix3Q0FBd0M7UUFDeEMsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxFQUFFO1lBQ3BDLEtBQUssQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEVBQUMsUUFBUSxFQUFFLENBQUMsRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLFNBQVMsRUFBRSxDQUFDLEVBQUMsQ0FBQyxDQUFDO1FBRWpFLHdCQUF3QjtRQUN4QixLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLElBQUksT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUN0QyxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO1lBQzdCLE1BQU0sRUFBQyxJQUFJLEVBQUUsU0FBUyxFQUFDLEdBQUcsS0FBSyxDQUFDO1lBQ2hDLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsSUFBSSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFO2dCQUNwQyxNQUFNLEVBQUMsSUFBSSxFQUFFLE9BQU8sRUFBRSxnQkFBZ0IsRUFBQyxHQUFHLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7Z0JBQ3ZELElBQUksSUFBSSxJQUFJLFNBQVMsRUFBRTtvQkFDbkIsNklBQTZJO29CQUU3SSxtQkFBbUI7b0JBQ25CLHdHQUF3RztvQkFDeEcsTUFBTSxRQUFRLEdBQUcsS0FBSyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7b0JBQ3JDLE1BQU0scUJBQXFCLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FDbEMsS0FBSyxDQUFDLEtBQUs7d0JBQ1AsUUFBUSxDQUFDLFNBQVM7d0JBQ2xCLHFHQUFxRzt3QkFDckcsQ0FBQyxDQUNSLENBQUM7b0JBQ0YsSUFBSSxhQUFhLEdBQUc7d0JBQ2hCLFFBQVEsRUFBRSxRQUFRLENBQUMsUUFBUSxHQUFHLHFCQUFxQixHQUFHLEtBQUssQ0FBQyxJQUFJO3dCQUNoRSxXQUFXLEVBQUUsS0FBSzt3QkFDbEIsU0FBUyxFQUFFLEtBQUssQ0FBQyxRQUFRO3FCQUM1QixDQUFDO29CQUVGLE1BQU0sU0FBUyxHQUFHLEtBQUssQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7b0JBQ2xDO29CQUNJLG9JQUFvSTtvQkFDcEksYUFBYSxDQUFDLFFBQVEsR0FBRyxTQUFTLENBQUMsUUFBUTt3QkFDM0MsYUFBYSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUMsU0FBUzt3QkFFN0MsYUFBYSxHQUFHLFNBQVMsQ0FBQztvQkFFOUIsbUdBQW1HO29CQUNuRyxNQUFNLFFBQVEsR0FBRzt3QkFDYixRQUFRLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLEdBQUcsS0FBSyxDQUFDLElBQUk7d0JBQy9DLFdBQVcsRUFBRSxJQUFJO3dCQUNqQixTQUFTLEVBQUUsS0FBSyxDQUFDLFFBQVE7cUJBQzVCLENBQUM7b0JBRUYseUJBQXlCO29CQUN6QixLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO3dCQUNQLFFBQVEsQ0FBQyxRQUFRLElBQUksYUFBYSxDQUFDLFFBQVE7NEJBQ3ZDLENBQUMsQ0FBQyxRQUFROzRCQUNWLENBQUMsQ0FBQyxhQUFhLENBQUM7aUJBQzNCO3FCQUFNO29CQUNILHVFQUF1RTtvQkFDdkUsTUFBTSxRQUFRLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztvQkFDakMsTUFBTSxXQUFXLEdBQUc7d0JBQ2hCLFFBQVEsRUFBRSxnQkFBZ0IsR0FBRyxRQUFRLENBQUMsUUFBUTt3QkFDOUMsV0FBVyxFQUFFLElBQUk7d0JBQ2pCLFNBQVMsRUFBRSxRQUFRLENBQUMsU0FBUztxQkFDaEMsQ0FBQztvQkFDRixNQUFNLFNBQVMsR0FBRyxLQUFLLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO29CQUNsQyxNQUFNLFlBQVksR0FBRzt3QkFDakIsUUFBUSxFQUFFLFNBQVMsQ0FBQyxRQUFRO3dCQUM1QixXQUFXLEVBQUUsS0FBSzt3QkFDbEIsU0FBUyxFQUFFLFNBQVMsQ0FBQyxTQUFTO3FCQUNqQyxDQUFDO29CQUVGLHVIQUF1SDtvQkFDdkgsSUFBSSxZQUFZLENBQUMsUUFBUSxJQUFJLFdBQVcsQ0FBQyxRQUFRO3dCQUM3QyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDOzRCQUNQLFlBQVksQ0FBQyxTQUFTLElBQUksV0FBVyxDQUFDLFNBQVM7Z0NBQzNDLENBQUMsQ0FBQyxZQUFZO2dDQUNkLENBQUMsQ0FBQyxXQUFXLENBQUM7b0JBQzFCLHdEQUF3RDs7d0JBRXBELEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7NEJBQ1AsWUFBWSxDQUFDLFFBQVEsSUFBSSxXQUFXLENBQUMsUUFBUTtnQ0FDekMsQ0FBQyxDQUFDLFlBQVk7Z0NBQ2QsQ0FBQyxDQUFDLFdBQVcsQ0FBQztpQkFDN0I7YUFDSjtTQUNKO1FBRUQsdUJBQXVCO1FBQ3ZCLElBQUksU0FBUyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDdkMsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFDO1FBQ2xCLEtBQUssSUFBSSxDQUFDLEdBQUcsQ0FBQyxFQUFFLENBQUMsSUFBSSxPQUFPLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFO1lBQ3RDLElBQUksS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxRQUFRLEdBQUcsU0FBUyxDQUFDLFFBQVEsRUFBRTtnQkFDdEQsU0FBUyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLENBQUM7Z0JBQ25DLFNBQVMsR0FBRyxDQUFDLENBQUM7YUFDakI7U0FDSjtRQUVELE9BQU8sRUFBQyxjQUFjLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBRSxTQUFTLENBQUMsUUFBUSxFQUFDLENBQUM7SUFDckUsQ0FBQztJQUVEOzs7O09BSUc7SUFDSSxZQUFZLENBQ2YsT0FBK0I7UUFFL0IsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDO1FBRXpCLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUM7UUFFdEMsTUFBTSxHQUFHLEdBQXNCLEVBQUUsQ0FBQztRQUVsQyxvRUFBb0U7UUFDcEUsSUFBSSxDQUFDLEdBQUcsS0FBSyxDQUFDLGNBQWMsRUFDeEIsQ0FBQyxHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUM7UUFDckIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUU7WUFDbkIsSUFBSSxDQUFDLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ3BCLElBQUksS0FBSyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLElBQUksT0FBTyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUU7Z0JBQzFDLElBQUksQ0FBQyxDQUFDLFNBQVMsSUFBSSxPQUFPLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsRUFBRTtvQkFDeEMsQ0FBQyxJQUFJLENBQUMsQ0FBQztpQkFDVjtxQkFBTTtvQkFDSCxHQUFHLENBQUMsT0FBTyxDQUFDO3dCQUNSLEtBQUssRUFBRSxPQUFPLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQzt3QkFDckIsVUFBVSxFQUFFLENBQUMsR0FBRyxDQUFDO3dCQUNqQixTQUFTLEVBQUUsQ0FBQyxHQUFHLENBQUM7cUJBQ25CLENBQUMsQ0FBQztvQkFDSCxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUU7d0JBQ2YsTUFBTTtxQkFDVDt5QkFBTTt3QkFDSCxDQUFDLElBQUksQ0FBQyxDQUFDO3dCQUNQLENBQUMsSUFBSSxDQUFDLENBQUM7cUJBQ1Y7aUJBQ0o7YUFDSjtpQkFBTTtnQkFDSCxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUU7b0JBQ2YsQ0FBQyxJQUFJLENBQUMsQ0FBQztpQkFDVjtxQkFBTTtvQkFDSCxDQUFDLElBQUksQ0FBQyxDQUFDO2lCQUNWO2FBQ0o7U0FDSjtRQUVELG9CQUFvQjtRQUNwQixPQUFPLEVBQUMsUUFBUSxFQUFFLEtBQUssQ0FBQyxRQUFRLEVBQUUsT0FBTyxFQUFFLEdBQUcsRUFBQyxDQUFDO0lBQ3BELENBQUM7Q0FDSjtBQXBNRCw0Q0FvTUMifQ==