123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- const maxDistance = 3;
- function editDistance(a, b) {
- // https://en.wikipedia.org/wiki/Damerau–Levenshtein_distance
- // Calculating optimal string alignment distance, no substring is edited more than once.
- // (Simple implementation.)
- // Quick early exit, return worst case.
- if (Math.abs(a.length - b.length) > maxDistance) return Math.max(a.length, b.length);
- // distance between prefix substrings of a and b
- const d = [];
- // pure deletions turn a into empty string
- for (let i = 0; i <= a.length; i++) {
- d[i] = [i];
- }
- // pure insertions turn empty string into b
- for (let j = 0; j <= b.length; j++) {
- d[0][j] = j;
- }
- // fill matrix
- for (let j = 1; j <= b.length; j++) {
- for (let i = 1; i <= a.length; i++) {
- let cost = 1;
- if (a[i - 1] === b[j - 1]) {
- cost = 0;
- } else {
- cost = 1;
- }
- d[i][j] = Math.min(
- d[i - 1][j] + 1, // deletion
- d[i][j - 1] + 1, // insertion
- d[i - 1][j - 1] + cost // substitution
- );
- // transposition
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
- d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
- }
- }
- }
- return d[a.length][b.length];
- }
- /**
- * Find close matches, restricted to same number of edits.
- *
- * @param {string} word
- * @param {string[]} candidates
- * @returns {string}
- */
- function suggestSimilar(word, candidates) {
- if (!candidates || candidates.length === 0) return '';
- // remove possible duplicates
- candidates = Array.from(new Set(candidates));
- const searchingOptions = word.startsWith('--');
- if (searchingOptions) {
- word = word.slice(2);
- candidates = candidates.map(candidate => candidate.slice(2));
- }
- let similar = [];
- let bestDistance = maxDistance;
- const minSimilarity = 0.4;
- candidates.forEach((candidate) => {
- if (candidate.length <= 1) return; // no one character guesses
- const distance = editDistance(word, candidate);
- const length = Math.max(word.length, candidate.length);
- const similarity = (length - distance) / length;
- if (similarity > minSimilarity) {
- if (distance < bestDistance) {
- // better edit distance, throw away previous worse matches
- bestDistance = distance;
- similar = [candidate];
- } else if (distance === bestDistance) {
- similar.push(candidate);
- }
- }
- });
- similar.sort((a, b) => a.localeCompare(b));
- if (searchingOptions) {
- similar = similar.map(candidate => `--${candidate}`);
- }
- if (similar.length > 1) {
- return `\n(Did you mean one of ${similar.join(', ')}?)`;
- }
- if (similar.length === 1) {
- return `\n(Did you mean ${similar[0]}?)`;
- }
- return '';
- }
- exports.suggestSimilar = suggestSimilar;
|