Skip to content

Commit 9dcf018

Browse files
author
Thidas Senavirathna
committed
Add Did You Mean Suggestion for Mistyped Commands #40
1 parent 2d8d902 commit 9dcf018

File tree

2 files changed

+92
-43
lines changed

2 files changed

+92
-43
lines changed

src/main/java/com/mycmd/App.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.mycmd;
22

33
import com.mycmd.commands.*;
4+
import com.mycmd.utils.StringUtils;
5+
46
import java.util.*;
7+
import java.util.Scanner;
58

69
public class App {
710
public static void main(String[] args) {
@@ -35,12 +38,18 @@ public static void main(String[] args) {
3538
System.out.println("Error: " + e.getMessage());
3639
}
3740
} else {
38-
List<String> validCommands = new ArrayList<>(commands.keySet());
39-
String suggestion = StringUtils.findClosest(cmd, validCommands);
40-
if (suggestion != null && !suggestion.equals(cmd)) {
41-
System.out.println("'" + cmd + "' is not recognized as an internal or external command. Did you mean '" + suggestion + "'?");
42-
} else {
43-
System.out.println("'" + cmd + "' is not recognized as an internal or external command.");
41+
// Single, clear not-recognized message + optional suggestion
42+
System.out.println("'" + cmd + "' is not recognized as an internal or external command.");
43+
44+
// compute suggestion safely
45+
try {
46+
List<String> validCommands = new ArrayList<>(commands.keySet());
47+
String suggestion = StringUtils.findClosest(cmd, validCommands);
48+
if (suggestion != null && !suggestion.equalsIgnoreCase(cmd)) {
49+
System.out.println("Did you mean '" + suggestion + "'?");
50+
}
51+
} catch (Exception ex) {
52+
// don't let suggestion errors break the shell
4453
}
4554
}
4655
}
Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,88 @@
1-
package com.mycmd;
1+
package com.mycmd.utils;
22

3-
import java.util.List;
3+
import java.util.Collection;
4+
import java.util.Objects;
45

5-
public class StringUtils {
6-
// Compute Levenshtein distance between two strings
7-
public static int levenshtein(String a, String b) {
8-
int[] costs = new int[b.length() + 1];
9-
for (int j = 0; j < costs.length; j++) {
10-
costs[j] = j;
6+
/**
7+
* Small utility for string-based helper methods.
8+
* Provides a findClosest(...) implementation using Levenshtein distance.
9+
*/
10+
public final class StringUtils {
11+
12+
private StringUtils() {}
13+
14+
/**
15+
* Find the closest string in candidates to the input.
16+
* Returns null when no candidate is close enough.
17+
*
18+
* @param input the input string
19+
* @param candidates candidate strings
20+
* @return the closest candidate or null
21+
*/
22+
public static String findClosest(String input, Collection<String> candidates) {
23+
if (input == null || input.isEmpty() || candidates == null || candidates.isEmpty()) {
24+
return null;
1125
}
12-
for (int i = 1; i <= a.length(); i++) {
13-
costs[0] = i;
14-
int nw = i - 1;
15-
for (int j = 1; j <= b.length(); j++) {
16-
int cj = Math.min(1 + Math.min(costs[j], costs[j - 1]), a.charAt(i - 1) == b.charAt(j - 1) ? nw : nw + 1);
17-
nw = costs[j];
18-
costs[j] = cj;
26+
27+
String best = null;
28+
int bestDistance = Integer.MAX_VALUE;
29+
30+
for (String cand : candidates) {
31+
if (cand == null || cand.isEmpty()) continue;
32+
if (cand.equalsIgnoreCase(input)) {
33+
// exact match ignoring case - return immediately
34+
return cand;
35+
}
36+
int dist = levenshteinDistance(input.toLowerCase(), cand.toLowerCase());
37+
if (dist < bestDistance) {
38+
bestDistance = dist;
39+
best = cand;
1940
}
2041
}
21-
return costs[b.length()];
42+
43+
// Choose a threshold: allow suggestion only when distance is reasonably small.
44+
// Here we allow suggestions when distance <= max(1, input.length()/3)
45+
int threshold = Math.max(1, input.length() / 3);
46+
if (bestDistance <= threshold) {
47+
return best;
48+
}
49+
return null;
2250
}
2351

24-
// Find the closest string from a list
25-
public static String findClosest(String input, List<String> candidates) {
26-
String closest = null;
27-
int minDist = Integer.MAX_VALUE;
28-
for (String candidate : candidates) {
29-
int dist = levenshtein(input, candidate);
30-
if (dist < minDist) {
31-
minDist = dist;
32-
closest = candidate;
33-
}
52+
// Classic iterative Levenshtein algorithm (memory O(min(m,n)))
53+
private static int levenshteinDistance(String a, String b) {
54+
if (Objects.equals(a, b)) return 0;
55+
if (a.length() == 0) return b.length();
56+
if (b.length() == 0) return a.length();
57+
58+
// ensure a is the shorter
59+
if (a.length() > b.length()) {
60+
String tmp = a;
61+
a = b;
62+
b = tmp;
3463
}
35-
// Only return a suggestion when the distance is small enough to be useful.
36-
// A threshold of 2 works well for short typos (e.g. "di" -> "dir"),
37-
// and we also allow a slightly larger threshold for longer candidates.
38-
if (closest == null) return null;
39-
int threshold = 2;
40-
int len = Math.max(input.length(), closest.length());
41-
// allow up to ~25% of the length as distance for long words (min 2)
42-
int adaptive = Math.max(threshold, len / 4);
43-
if (minDist <= adaptive) {
44-
return closest;
64+
65+
int[] prev = new int[a.length() + 1];
66+
int[] curr = new int[a.length() + 1];
67+
68+
for (int i = 0; i <= a.length(); i++) prev[i] = i;
69+
70+
for (int j = 1; j <= b.length(); j++) {
71+
curr[0] = j;
72+
char bj = b.charAt(j - 1);
73+
for (int i = 1; i <= a.length(); i++) {
74+
int cost = (a.charAt(i - 1) == bj) ? 0 : 1;
75+
curr[i] = min3(curr[i - 1] + 1, prev[i] + 1, prev[i - 1] + cost);
76+
}
77+
// swap prev and curr
78+
int[] tmp = prev;
79+
prev = curr;
80+
curr = tmp;
4581
}
46-
return null;
82+
return prev[a.length()];
83+
}
84+
85+
private static int min3(int x, int y, int z) {
86+
return Math.min(x, Math.min(y, z));
4787
}
4888
}

0 commit comments

Comments
 (0)