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