fuzzy-rater
Version:
Tooling for fuzzily rating text according to some query
222 lines • 18.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FuzzyMultiWordMatcher = void 0;
const createFuzzyNFATemplate_1 = require("./createFuzzyNFATemplate");
const NFADFA_1 = require("../../DFA/NFADFA/NFADFA");
/**
* A fuzzy word matcher that can be used to find a word in a number of items.
* Initial setup time is relatively long, but matching per string happens in linear time.
*/
class FuzzyMultiWordMatcher {
/**
* Constructs a new fuzzy word rater
* @param word The word to look for
* @param maxDistance The maximum error
*/
constructor(word, maxDistance) {
this.word = word;
this.maxDistance = maxDistance;
this.initialize(word, maxDistance);
}
/**
* Initializes the data structures used for rating
* @param word The word to look for
* @param maxDistance The maximal allowed distance
*/
initialize(word, maxDistance) {
const nfaTemplate = createFuzzyNFATemplate_1.createFuzzyNFATemplate(word, maxDistance, true);
this.NFA = new NFADFA_1.NFADFA(nfaTemplate, {
// Add the best combined fuzzy meta to ever node
nodeMeta: nodes => {
var _a;
return ((_a = this.getBestMatch(nodes, n => n.metadata)) === null || _a === void 0 ? void 0 : _a.metadata) || {
matched: false,
distance: 0,
};
},
// No need to augment the transitions
transitionMeta: transition => ({}),
});
}
/**
* Finds the best match in a set of NFA nodes
* @param matches The nodes to find the best match in
* @param getNode Retrieves the node data
* @param includeNonMatch Whether to fallback to the best non-match if no match was found
* @returns The best match
*/
getBestMatch(matches, getNode, includeNoneMatch = false) {
const best = matches.reduce((best, m) => {
const node = getNode(m);
return node.matched && (best == null || node.distance < best.distance)
? { item: m, distance: node.distance }
: best;
}, undefined);
if (best)
return best.item;
if (!includeNoneMatch)
return;
const bestNonMatch = matches.reduce((best, m) => {
const node = getNode(m);
return best == null || node.distance < best.distance
? { item: m, distance: node.distance }
: best;
}, undefined);
return bestNonMatch === null || bestNonMatch === void 0 ? void 0 : bestNonMatch.item;
}
/**
* Retrieves the best match in the given text
* @param text The text to find the query word in
* @returns Whether the text matched, and the distance from the query word
*/
getMatch(text) {
// Execute the DFA
const trace = this.NFA.executeDFATraced(text);
// Extract the matches from the data
const lastMatched = trace.final.matched;
const matchData = trace.path.reduceRight(({ matches, bestConsecutiveMatch }, { fromNode: { matched, distance } }, endIndex) => {
if (!matched) {
return {
matches: bestConsecutiveMatch
? [bestConsecutiveMatch, ...matches]
: matches,
bestConsecutiveMatch: null,
};
}
else {
// When a node matched, reduce it to the best option of a sequence before adding the match
return {
matches,
bestConsecutiveMatch: !bestConsecutiveMatch ||
bestConsecutiveMatch.distance > distance
? { endIndex, distance }
: bestConsecutiveMatch,
};
}
}, {
matches: [],
bestConsecutiveMatch: lastMatched
? { endIndex: text.length, distance: trace.final.distance }
: null,
});
// Return the matches
return matchData.bestConsecutiveMatch
? [matchData.bestConsecutiveMatch, ...matchData.matches]
: matchData.matches;
}
/**
* Retrieves the best NFA trace given a DFA match
* @param nfaDfaTrace The simplified NFA-DFA trace to obtain the best NFA trace in (text matches with lowest distance)
* @returns The NFA trace
*/
getBestTrace(nfaDfaTrace) {
const trace = nfaDfaTrace.getPath(dfaTrace => {
// Obtain the indices of transitions where to choose the best
const matchIndices = dfaTrace.path
.reduceRight(({ matches, bestConsecutive }, { fromNode }, index) => {
if (fromNode.matched) {
return {
matches,
bestConsecutive: !bestConsecutive ||
bestConsecutive.distance > fromNode.distance
? { index: index - 1, distance: fromNode.distance }
: bestConsecutive,
};
}
return {
matches: bestConsecutive
? [bestConsecutive, ...matches]
: matches,
bestConsecutive: null,
};
}, {
matches: [],
bestConsecutive: dfaTrace.final.matched
? {
index: dfaTrace.path.length - 1,
distance: dfaTrace.final.distance,
}
: null,
})
.matches.map(({ index }) => index);
// Return a function that chooses the transition from (/to since moving backwards) a node with the lowest distance, if we found that this leads to the best match
return (to, transitions, index, nodes) => {
const possibleTrans = transitions.filter(transition => transition.to == to.ID);
// Look for the best transition that came from a match, and choose that if it exists
const matching = matchIndices.includes(index) &&
possibleTrans.reduce((best, transition) => {
var _a;
const md = (_a = nodes[transition.from]) === null || _a === void 0 ? void 0 : _a.metadata;
if (md.matched && md.distance < best.distance)
return { transition, distance: md.distance };
return best;
}, { transition: null, distance: Infinity }).transition;
if (matching)
return matching;
return possibleTrans[0];
};
});
return trace;
}
/**
* Retrieves the best match in the given text, and data of how to obtain it
* @param text The text to find the query word in
* @returns The distances from the query word, for each match (no distances = no matches), and how the text differed
*/
getMatchData(text) {
const matches = this.NFA.executeTraced(text);
const best = this.getBestMatch(matches, ({ final }) => final, true);
const trace = best && this.getBestTrace(best);
// Convert the NFA trace to an alterations array
if (best && trace) {
const result = trace.reduce(({ alterations, index, distances, prevNode }, { transition, fromNode }) => {
var _a;
// Skip restart transitions, since they have no relevance in either the query or target
if (transition.type == "restart")
return {
alterations,
index,
distances: [...distances, fromNode.distance],
prevNode,
};
// Obtain the query and target data
const target = {
index,
character: transition.type == "skip" ? "" : text[index],
};
const query = {
index: transition.index,
character: (_a = transition.character) !== null && _a !== void 0 ? _a : "",
};
// Add any distances and alterations
const matched = fromNode.matched;
const newMatch = (prevNode === null || prevNode === void 0 ? void 0 : prevNode.matched) && !matched; // Make sure we don't include different distances of the same match
return {
alterations: [
...alterations,
{ target, query, type: transition.type },
],
distances: newMatch && prevNode
? [...distances, prevNode.distance]
: distances,
index: transition.type == "skip" ? index : index + 1,
prevNode: fromNode,
};
}, {
alterations: [],
distances: [],
index: 0,
prevNode: null,
});
return {
alterations: result.alterations,
distances: best.final.matched
? [...result.distances, best.final.distance]
: result.distances,
};
}
return { distances: [], alterations: [] };
}
}
exports.FuzzyMultiWordMatcher = FuzzyMultiWordMatcher;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRnV6enlNdWx0aVdvcmRNYXRjaGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2Z1enp5UmF0ZS93b3JkTWF0Y2hlci9GdXp6eU11bHRpV29yZE1hdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEscUVBQWdFO0FBSWhFLG9EQUErQztBQUcvQzs7O0dBR0c7QUFDSCxNQUFhLHFCQUFxQjtJQUs5Qjs7OztPQUlHO0lBQ0gsWUFBbUIsSUFBWSxFQUFFLFdBQW1CO1FBQ2hELElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1FBQ2pCLElBQUksQ0FBQyxXQUFXLEdBQUcsV0FBVyxDQUFDO1FBQy9CLElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxFQUFFLFdBQVcsQ0FBQyxDQUFDO0lBQ3ZDLENBQUM7SUFFRDs7OztPQUlHO0lBQ08sVUFBVSxDQUFDLElBQVksRUFBRSxXQUFtQjtRQUNsRCxNQUFNLFdBQVcsR0FBRywrQ0FBc0IsQ0FBQyxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQ3BFLElBQUksQ0FBQyxHQUFHLEdBQUcsSUFBSSxlQUFNLENBQUMsV0FBVyxFQUFFO1lBQy9CLGdEQUFnRDtZQUNoRCxRQUFRLEVBQUUsS0FBSyxDQUFDLEVBQUU7O2dCQUNkLE9BQUEsT0FBQSxJQUFJLENBQUMsWUFBWSxDQUFDLEtBQUssRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsMENBQUUsUUFBUSxLQUFJO29CQUNuRCxPQUFPLEVBQUUsS0FBSztvQkFDZCxRQUFRLEVBQUUsQ0FBQztpQkFDZCxDQUFBO2FBQUE7WUFDTCxxQ0FBcUM7WUFDckMsY0FBYyxFQUFFLFVBQVUsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUM7U0FDckMsQ0FBQyxDQUFDO0lBQ1AsQ0FBQztJQUVEOzs7Ozs7T0FNRztJQUNPLFlBQVksQ0FDbEIsT0FBWSxFQUNaLE9BQW9DLEVBQ3BDLG1CQUE0QixLQUFLO1FBRWpDLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxFQUFFLEVBQUU7WUFDcEMsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ3hCLE9BQU8sSUFBSSxDQUFDLE9BQU8sSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLElBQUksSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDO2dCQUNsRSxDQUFDLENBQUMsRUFBQyxJQUFJLEVBQUUsQ0FBQyxFQUFFLFFBQVEsRUFBRSxJQUFJLENBQUMsUUFBUSxFQUFDO2dCQUNwQyxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ2YsQ0FBQyxFQUFFLFNBQW9ELENBQUMsQ0FBQztRQUN6RCxJQUFJLElBQUk7WUFBRSxPQUFPLElBQUksQ0FBQyxJQUFJLENBQUM7UUFDM0IsSUFBSSxDQUFDLGdCQUFnQjtZQUFFLE9BQU87UUFFOUIsTUFBTSxZQUFZLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDLEVBQUUsRUFBRTtZQUM1QyxNQUFNLElBQUksR0FBRyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDeEIsT0FBTyxJQUFJLElBQUksSUFBSSxJQUFJLElBQUksQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDLFFBQVE7Z0JBQ2hELENBQUMsQ0FBQyxFQUFDLElBQUksRUFBRSxDQUFDLEVBQUUsUUFBUSxFQUFFLElBQUksQ0FBQyxRQUFRLEVBQUM7Z0JBQ3BDLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDZixDQUFDLEVBQUUsU0FBb0QsQ0FBQyxDQUFDO1FBQ3pELE9BQU8sWUFBWSxhQUFaLFlBQVksdUJBQVosWUFBWSxDQUFFLElBQUksQ0FBQztJQUM5QixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLFFBQVEsQ0FBQyxJQUFZO1FBQ3hCLGtCQUFrQjtRQUNsQixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLGdCQUFnQixDQUFDLElBQUksQ0FBQyxDQUFDO1FBRTlDLG9DQUFvQztRQUNwQyxNQUFNLFdBQVcsR0FBRyxLQUFLLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQztRQUN4QyxNQUFNLFNBQVMsR0FBRyxLQUFLLENBQUMsSUFBSSxDQUFDLFdBQVcsQ0FDcEMsQ0FDSSxFQUFDLE9BQU8sRUFBRSxvQkFBb0IsRUFBQyxFQUMvQixFQUFDLFFBQVEsRUFBRSxFQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUMsRUFBQyxFQUMvQixRQUFRLEVBQ1YsRUFBRTtZQUNBLElBQUksQ0FBQyxPQUFPLEVBQUU7Z0JBQ1YsT0FBTztvQkFDSCxPQUFPLEVBQUUsb0JBQW9CO3dCQUN6QixDQUFDLENBQUMsQ0FBQyxvQkFBb0IsRUFBRSxHQUFHLE9BQU8sQ0FBQzt3QkFDcEMsQ0FBQyxDQUFDLE9BQU87b0JBQ2Isb0JBQW9CLEVBQUUsSUFBSTtpQkFDN0IsQ0FBQzthQUNMO2lCQUFNO2dCQUNILDBGQUEwRjtnQkFDMUYsT0FBTztvQkFDSCxPQUFPO29CQUNQLG9CQUFvQixFQUNoQixDQUFDLG9CQUFvQjt3QkFDckIsb0JBQW9CLENBQUMsUUFBUSxHQUFHLFFBQVE7d0JBQ3BDLENBQUMsQ0FBQyxFQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUM7d0JBQ3RCLENBQUMsQ0FBQyxvQkFBb0I7aUJBQ2pDLENBQUM7YUFDTDtRQUNMLENBQUMsRUFDRDtZQUNJLE9BQU8sRUFBRSxFQUFFO1lBQ1gsb0JBQW9CLEVBQUUsV0FBVztnQkFDN0IsQ0FBQyxDQUFDLEVBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLEtBQUssQ0FBQyxLQUFLLENBQUMsUUFBUSxFQUFDO2dCQUN6RCxDQUFDLENBQUUsSUFBb0Q7U0FDOUQsQ0FDSixDQUFDO1FBRUYscUJBQXFCO1FBQ3JCLE9BQU8sU0FBUyxDQUFDLG9CQUFvQjtZQUNqQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsb0JBQW9CLEVBQUUsR0FBRyxTQUFTLENBQUMsT0FBTyxDQUFDO1lBQ3hELENBQUMsQ0FBQyxTQUFTLENBQUMsT0FBTyxDQUFDO0lBQzVCLENBQUM7SUFFRDs7OztPQUlHO0lBQ08sWUFBWSxDQUNsQixXQUErRTtRQU8vRSxNQUFNLEtBQUssR0FBRyxXQUFXLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFO1lBQ3pDLDZEQUE2RDtZQUM3RCxNQUFNLFlBQVksR0FBRyxRQUFRLENBQUMsSUFBSTtpQkFDN0IsV0FBVyxDQUNSLENBQUMsRUFBQyxPQUFPLEVBQUUsZUFBZSxFQUFDLEVBQUUsRUFBQyxRQUFRLEVBQUMsRUFBRSxLQUFLLEVBQUUsRUFBRTtnQkFDOUMsSUFBSSxRQUFRLENBQUMsT0FBTyxFQUFFO29CQUNsQixPQUFPO3dCQUNILE9BQU87d0JBQ1AsZUFBZSxFQUNYLENBQUMsZUFBZTs0QkFDaEIsZUFBZSxDQUFDLFFBQVEsR0FBRyxRQUFRLENBQUMsUUFBUTs0QkFDeEMsQ0FBQyxDQUFDLEVBQUMsS0FBSyxFQUFFLEtBQUssR0FBRyxDQUFDLEVBQUUsUUFBUSxFQUFFLFFBQVEsQ0FBQyxRQUFRLEVBQUM7NEJBQ2pELENBQUMsQ0FBQyxlQUFlO3FCQUM1QixDQUFDO2lCQUNMO2dCQUNELE9BQU87b0JBQ0gsT0FBTyxFQUFFLGVBQWU7d0JBQ3BCLENBQUMsQ0FBQyxDQUFDLGVBQWUsRUFBRSxHQUFHLE9BQU8sQ0FBQzt3QkFDL0IsQ0FBQyxDQUFDLE9BQU87b0JBQ2IsZUFBZSxFQUFFLElBQUk7aUJBQ3hCLENBQUM7WUFDTixDQUFDLEVBQ0Q7Z0JBQ0ksT0FBTyxFQUFFLEVBQXlDO2dCQUNsRCxlQUFlLEVBQUUsUUFBUSxDQUFDLEtBQUssQ0FBQyxPQUFPO29CQUNuQyxDQUFDLENBQUM7d0JBQ0ksS0FBSyxFQUFFLFFBQVEsQ0FBQyxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUM7d0JBQy9CLFFBQVEsRUFBRSxRQUFRLENBQUMsS0FBSyxDQUFDLFFBQVE7cUJBQ3BDO29CQUNILENBQUMsQ0FBRSxJQUFpRDthQUMzRCxDQUNKO2lCQUNBLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFDLEtBQUssRUFBQyxFQUFFLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQztZQUVyQyxpS0FBaUs7WUFDakssT0FBTyxDQUFDLEVBQUUsRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxFQUFFO2dCQUNyQyxNQUFNLGFBQWEsR0FBRyxXQUFXLENBQUMsTUFBTSxDQUNwQyxVQUFVLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxFQUFFLElBQUksRUFBRSxDQUFDLEVBQUUsQ0FDdkMsQ0FBQztnQkFFRixvRkFBb0Y7Z0JBQ3BGLE1BQU0sUUFBUSxHQUNWLFlBQVksQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDO29CQUM1QixhQUFhLENBQUMsTUFBTSxDQUNoQixDQUFDLElBQUksRUFBRSxVQUFVLEVBQUUsRUFBRTs7d0JBQ2pCLE1BQU0sRUFBRSxTQUFHLEtBQUssQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLDBDQUFFLFFBQVEsQ0FBQzt3QkFDNUMsSUFBSSxFQUFFLENBQUMsT0FBTyxJQUFJLEVBQUUsQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDLFFBQVE7NEJBQ3pDLE9BQU8sRUFBQyxVQUFVLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxRQUFRLEVBQUMsQ0FBQzt3QkFDL0MsT0FBTyxJQUFJLENBQUM7b0JBQ2hCLENBQUMsRUFDRCxFQUFDLFVBQVUsRUFBRSxJQUFJLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBQyxDQUN6QyxDQUFDLFVBQVUsQ0FBQztnQkFDakIsSUFBSSxRQUFRO29CQUFFLE9BQU8sUUFBUSxDQUFDO2dCQUU5QixPQUFPLGFBQWEsQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUM1QixDQUFDLENBQUM7UUFDTixDQUFDLENBQUMsQ0FBQztRQUVILE9BQU8sS0FBSyxDQUFDO0lBQ2pCLENBQUM7SUFFRDs7OztPQUlHO0lBQ0ksWUFBWSxDQUNmLElBQVk7UUFFWixNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM3QyxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLE9BQU8sRUFBRSxDQUFDLEVBQUMsS0FBSyxFQUFDLEVBQUUsRUFBRSxDQUFDLEtBQUssRUFBRSxJQUFJLENBQUMsQ0FBQztRQUNsRSxNQUFNLEtBQUssR0FBRyxJQUFJLElBQUksSUFBSSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUU5QyxnREFBZ0Q7UUFDaEQsSUFBSSxJQUFJLElBQUksS0FBSyxFQUFFO1lBQ2YsTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FDdkIsQ0FBQyxFQUFDLFdBQVcsRUFBRSxLQUFLLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBQyxFQUFFLEVBQUMsVUFBVSxFQUFFLFFBQVEsRUFBQyxFQUFFLEVBQUU7O2dCQUNsRSx1RkFBdUY7Z0JBQ3ZGLElBQUksVUFBVSxDQUFDLElBQUksSUFBSSxTQUFTO29CQUM1QixPQUFPO3dCQUNILFdBQVc7d0JBQ1gsS0FBSzt3QkFDTCxTQUFTLEVBQUUsQ0FBQyxHQUFHLFNBQVMsRUFBRSxRQUFRLENBQUMsUUFBUSxDQUFDO3dCQUM1QyxRQUFRO3FCQUNYLENBQUM7Z0JBRU4sbUNBQW1DO2dCQUNuQyxNQUFNLE1BQU0sR0FBRztvQkFDWCxLQUFLO29CQUNMLFNBQVMsRUFBRSxVQUFVLENBQUMsSUFBSSxJQUFJLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDO2lCQUMxRCxDQUFDO2dCQUNGLE1BQU0sS0FBSyxHQUFHO29CQUNWLEtBQUssRUFBRSxVQUFVLENBQUMsS0FBSztvQkFDdkIsU0FBUyxRQUFFLFVBQVUsQ0FBQyxTQUFTLG1DQUFJLEVBQUU7aUJBQ3hDLENBQUM7Z0JBRUYsb0NBQW9DO2dCQUNwQyxNQUFNLE9BQU8sR0FBRyxRQUFRLENBQUMsT0FBTyxDQUFDO2dCQUNqQyxNQUFNLFFBQVEsR0FBRyxDQUFBLFFBQVEsYUFBUixRQUFRLHVCQUFSLFFBQVEsQ0FBRSxPQUFPLEtBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxtRUFBbUU7Z0JBQ25ILE9BQU87b0JBQ0gsV0FBVyxFQUFFO3dCQUNULEdBQUcsV0FBVzt3QkFDZCxFQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLFVBQVUsQ0FBQyxJQUFJLEVBQUM7cUJBQ3pDO29CQUNELFNBQVMsRUFDTCxRQUFRLElBQUksUUFBUTt3QkFDaEIsQ0FBQyxDQUFDLENBQUMsR0FBRyxTQUFTLEVBQUUsUUFBUSxDQUFDLFFBQVEsQ0FBQzt3QkFDbkMsQ0FBQyxDQUFDLFNBQVM7b0JBQ25CLEtBQUssRUFBRSxVQUFVLENBQUMsSUFBSSxJQUFJLE1BQU0sQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLEdBQUcsQ0FBQztvQkFDcEQsUUFBUSxFQUFFLFFBQVE7aUJBQ3JCLENBQUM7WUFDTixDQUFDLEVBQ0Q7Z0JBQ0ksV0FBVyxFQUFFLEVBQXVCO2dCQUNwQyxTQUFTLEVBQUUsRUFBRTtnQkFDYixLQUFLLEVBQUUsQ0FBQztnQkFDUixRQUFRLEVBQUUsSUFBNkI7YUFDMUMsQ0FDSixDQUFDO1lBRUYsT0FBTztnQkFDSCxXQUFXLEVBQUUsTUFBTSxDQUFDLFdBQVc7Z0JBQy9CLFNBQVMsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU87b0JBQ3pCLENBQUMsQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDLFNBQVMsRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLFFBQVEsQ0FBQztvQkFDNUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTO2FBQ3pCLENBQUM7U0FDTDtRQUVELE9BQU8sRUFBQyxTQUFTLEVBQUUsRUFBRSxFQUFFLFdBQVcsRUFBRSxFQUFFLEVBQUMsQ0FBQztJQUM1QyxDQUFDO0NBQ0o7QUFsUUQsc0RBa1FDIn0=