UNPKG

fuzzy-rater

Version:

Tooling for fuzzily rating text according to some query

166 lines 14.3 kB
"use strict"; 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiV29yZE9yZGVyTWF0Y2hlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9mdXp6eVJhdGUvb3JkZXJNYXRjaGVyL1dvcmRPcmRlck1hdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBS0E7OztHQUdHO0FBQ0gsTUFBYSxnQkFBZ0I7SUFpQnpCLFlBQ0ksS0FBNEMsRUFDNUMsVUFBa0IsRUFBRTs7UUFFcEIsSUFBSSxDQUFDLEtBQUssR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQzFCLE9BQU8sSUFBSSxJQUFJLFFBQVEsQ0FBQyxDQUFDLENBQUMsRUFBQyxJQUFJLEVBQUUsT0FBTyxFQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FDbkQsQ0FBQztRQUNGLElBQUksQ0FBQyxLQUFLLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNsQixLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLElBQUksS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUNwQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHO2dCQUNmLFFBQVEsRUFDSixhQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQywwQ0FBRSxRQUFRLG1DQUFJLENBQUMsQ0FBQztvQkFDckMsYUFBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsMENBQUUsT0FBTyxtQ0FBSSxDQUFDLENBQUM7Z0JBQ3JDLFdBQVcsRUFBRSxDQUFDLEdBQUcsQ0FBQztnQkFDbEIsU0FBUyxFQUFFLENBQUM7YUFDZixDQUFDO1NBQ0w7SUFDTCxDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLFFBQVEsQ0FBQyxPQUErQjtRQUMzQyxPQUFPLElBQUksQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDLENBQUMs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