101 lines
		
	
	
		
			2.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			101 lines
		
	
	
		
			2.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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;
 |