Spaces:
Sleeping
Sleeping
| 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; | |