Skip to content

Commit

Permalink
Merge branch 'master' into link-org-rec
Browse files Browse the repository at this point in the history
  • Loading branch information
wxwern committed Oct 26, 2023
2 parents d80c08f + 12de8f4 commit a49c560
Show file tree
Hide file tree
Showing 29 changed files with 1,997 additions and 24 deletions.
74 changes: 74 additions & 0 deletions src/main/java/seedu/address/commons/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,78 @@ public static String formatWithNullFallback(String format, Object... values) {

return String.format(format, values);
}

/**
* Returns true if the <code>inputString</code> is a fuzzy match of the <code>targetString</code>,
* false otherwise.
*
* <p>
* A fuzzy search is an approximate search algorithm. This implementation computes a fuzzy match by determining
* if there exists a <i>subsequence match</i> in linear time.
* </p>
*
* <p>
* As an example, <code>"abc"</code> is considered to be a fuzzy match of <code>"aa1b2ccc"</code>, since one may
* construct the subsequence <code>"abc"</code> by removing extra characters <code>"a1"</code>, <code>"2cc"</code>
* from <code>aa1b2ccc</code>.
* </p>
*
* @param inputString The partial fuzzy input string.
* @param targetString The target string to check if the input fuzzily matches with.
* @return true if the input fuzzy-matches (is fuzzily contained in) the target, false otherwise.
*/
public static boolean isFuzzyMatch(String inputString, String targetString) {
if (inputString == null || targetString == null) {
return inputString == null && targetString == null; // both null => true, otherwise false
}

int inputI = 0;
int targetI = 0;

while (inputI < inputString.length() && targetI < targetString.length()) {
char c = inputString.charAt(inputI);
char t = targetString.charAt(targetI);

if (c == t) {
inputI++;
}
targetI++;
}

return inputI >= inputString.length();
}

/**
* Returns a score representing how close it is to matching characters at the beginning of the target.
* The higher the value, the better it is. A failed match will have {@code -targetString.length()}, while
* a complete match will have {@code inputString.length()}.
* </p>
*/
public static int getFuzzyMatchScore(String inputString, String targetString) {
if (inputString == null || targetString == null) {
return 0;
}

int inputI = 0;
int targetI = 0;

int errorCount = 0;

while (inputI < inputString.length() && targetI < targetString.length()) {
char c = inputString.charAt(inputI);
char t = targetString.charAt(targetI);

if (c == t) {
errorCount += targetI - inputI - errorCount;
inputI++;
}
targetI++;
}

if (inputI < inputString.length()) {
return -targetString.length();
}

return inputI - errorCount;
}
}
8 changes: 8 additions & 0 deletions src/main/java/seedu/address/logic/Logic.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package seedu.address.logic;

import java.nio.file.Path;
import java.util.stream.Stream;

import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
Expand All @@ -23,6 +24,13 @@ public interface Logic {
*/
CommandResult execute(String commandText) throws CommandException, ParseException;

/**
* Parses the command and returns any autocompletion results.
* @param commandText The command as entered by the user.
* @return the result of the command execution.
*/
Stream<String> generateCompletions(String commandText);

/**
* Returns the AddressBook.
*
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/seedu/address/logic/LogicManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
import java.nio.file.AccessDeniedException;
import java.nio.file.Path;
import java.util.logging.Logger;
import java.util.stream.Stream;

import javafx.collections.ObservableList;
import seedu.address.commons.core.GuiSettings;
import seedu.address.commons.core.LogsCenter;
import seedu.address.logic.commands.Command;
import seedu.address.logic.commands.CommandResult;
import seedu.address.logic.commands.exceptions.CommandException;
import seedu.address.logic.parser.AddressBookParser;
import seedu.address.logic.parser.AppParser;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.Model;
import seedu.address.model.ReadOnlyAddressBook;
Expand All @@ -31,23 +32,23 @@ public class LogicManager implements Logic {

private final Model model;
private final Storage storage;
private final AddressBookParser addressBookParser;
private final AppParser appParser;

/**
* Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}.
*/
public LogicManager(Model model, Storage storage) {
this.model = model;
this.storage = storage;
addressBookParser = new AddressBookParser();
appParser = new AppParser();
}

@Override
public CommandResult execute(String commandText) throws CommandException, ParseException {
logger.info("----------------[USER COMMAND][" + commandText + "]");

CommandResult commandResult;
Command command = addressBookParser.parseCommand(commandText);
Command command = appParser.parseCommand(commandText);
commandResult = command.execute(model);

try {
Expand All @@ -61,6 +62,13 @@ public CommandResult execute(String commandText) throws CommandException, ParseE
return commandResult;
}

@Override
public Stream<String> generateCompletions(String commandText) {
return appParser
.parseCompletionGenerator(commandText)
.generateCompletions(commandText, model);
}

@Override
public ReadOnlyAddressBook getAddressBook() {
return model.getAddressBook();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package seedu.address.logic.autocomplete;

import java.util.Comparator;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import seedu.address.commons.util.StringUtil;
import seedu.address.logic.parser.Flag;
import seedu.address.model.Model;

/**
* Creates a generator based on the given supplier or reference commands, so as it can generate
* auto-completions when requested.
*/
public class AutocompleteGenerator {

/** An autocompletion generator that generates no results. */
public static final AutocompleteGenerator NO_RESULTS = new AutocompleteGenerator(Stream::empty);


/** A comparator used to order fuzzily matched strings where better matches against the input go first. */
private static final Function<String, Comparator<String>> TEXT_FUZZY_MATCH_COMPARATOR = (input) -> (s1, s2) -> {
// Get how well s1 is ahead of s2 (note: higher is better).
int score = StringUtil.getFuzzyMatchScore(input, s1) - StringUtil.getFuzzyMatchScore(input, s2);

return -score; // -ve implies s1 < s2
};

/** A comparator used to order fuzzily matched flags where better matches against the input go first. */
private static final Function<String, Comparator<Flag>> FLAG_FUZZY_MATCH_COMPARATOR = (input) -> (f1, f2) -> {
// Get how well f1 is ahead of f2 in both metrics (note: higher is better).
int score = Math.max(
StringUtil.getFuzzyMatchScore(input, f1.getFlagString()),
StringUtil.getFuzzyMatchScore(input, f1.getFlagAliasString())
) - Math.max(
StringUtil.getFuzzyMatchScore(input, f2.getFlagString()),
StringUtil.getFuzzyMatchScore(input, f2.getFlagAliasString())
);

return -score; // -ve implies f1 < f2
};



/** The cached instance of the result evaluation function. */
private final BiFunction<String, Model, Stream<String>> resultEvaluationFunction;

/**
* Constructs an autocomplete generator based on the given set of reference full command strings.
*/
public AutocompleteGenerator(Stream<String> referenceCommands) {
this(() -> referenceCommands);
}

/**
* Constructs an autocomplete generator based on the given supplier of full command strings.
*/
public AutocompleteGenerator(Supplier<Stream<String>> referenceCommandsSupplier) {
resultEvaluationFunction = (partialCommand, model) -> {
if (partialCommand == null) {
return Stream.empty();
}

return referenceCommandsSupplier.get()
.filter(term -> StringUtil.isFuzzyMatch(partialCommand, term))
.sorted(TEXT_FUZZY_MATCH_COMPARATOR.apply(partialCommand))
.distinct();
};
}

/**
* Constructs an autocomplete generator based on the given {@link AutocompleteSupplier}.
*/
public AutocompleteGenerator(AutocompleteSupplier supplier) {
resultEvaluationFunction = (partialCommand, model) -> {

PartitionedCommand command = new PartitionedCommand(partialCommand == null ? "" : partialCommand);
String trailingText = command.getTrailingText();

// Compute the possible flags and flag-values.
Stream<String> possibleFlags = getPossibleFlags(command, supplier)
.filter(flag -> StringUtil.isFuzzyMatch(trailingText, flag.getFlagString())
|| StringUtil.isFuzzyMatch(trailingText, flag.getFlagAliasString()))
.sorted(FLAG_FUZZY_MATCH_COMPARATOR.apply(trailingText))
.map(Flag::getFlagString);

Stream<String> possibleValues = getPossibleValues(command, supplier, model)
.map(s -> s.filter(term -> StringUtil.isFuzzyMatch(trailingText, term))
.sorted(TEXT_FUZZY_MATCH_COMPARATOR.apply(trailingText)))
.orElse(possibleFlags);

// Decide which stream to use based on whether it's of a flag syntax or not.
Stream<String> possibleTerminalValues;
if (command.hasFlagSyntaxPrefixInTrailingText()) {
possibleTerminalValues = possibleFlags;
} else {
possibleTerminalValues = possibleValues;
}

// Return the results as a full completion string, distinct.
return possibleTerminalValues
.map(command::toStringWithNewTrailingTerm)
.distinct();
};
}

/**
* Generates a stream of completions based on the partial command given and no model.
* Note that omitting the model may limit the ability to return useful results.
*/
public Stream<String> generateCompletions(String command) {
return resultEvaluationFunction.apply(command, null);
}

/**
* Generates a stream of completions based on the partial command given and the model.
*/
public Stream<String> generateCompletions(String command, Model model) {
return resultEvaluationFunction.apply(command, model);
}




/**
* Obtains the set of possible flags based on the partitioned command and supplier.
*/
private static Stream<Flag> getPossibleFlags(
PartitionedCommand command,
AutocompleteSupplier supplier
) {
Flag[] allPossibleFlags = supplier.getAllPossibleFlags().toArray(Flag[]::new);

Set<Flag> existingCommandFlags = command.getConfirmedFlagStrings()
.stream()
.map(flagStr -> Flag.findMatch(flagStr, allPossibleFlags))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());


return supplier.getOtherPossibleFlagsAsideFromFlagsPresent(existingCommandFlags).stream();
}

/**
* Obtains the optional set of possible values based on the partitioned command, supplier, and model.
* If this optional is empty, that means it is explicitly specified that the flag cannot accept values.
*/
private static Optional<Stream<String>> getPossibleValues(
PartitionedCommand command,
AutocompleteSupplier supplier,
Model model
) {
return supplier.getValidValues(
Flag.parseOptional(
command.getLastConfirmedFlagString().orElse(null)
).orElse(null),
model
);
}

}
Loading

0 comments on commit a49c560

Please sign in to comment.