You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
100 lines
2.7 KiB
100 lines
2.7 KiB
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;
|
|
|