Skip to content

Commit

Permalink
Merge pull request nus-cs2103-AY2223S2#70 from hongshenggg/master
Browse files Browse the repository at this point in the history
Implement find command to find by prefix and keyword pairs
  • Loading branch information
SHni99 authored Mar 25, 2023
2 parents 8b6bd78 + 0b16f85 commit bd75a10
Show file tree
Hide file tree
Showing 23 changed files with 322 additions and 163 deletions.
34 changes: 24 additions & 10 deletions docs/UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Adding a person: `add`
* Deleting a person: `delete`
* Listing all contacts: `list`
* Locating persons by name/tags: `find`
* Locating persons by keywords: `find`
* Add an image for contacts: `add-image`
* Delete an image for contacts: `delete-image`
* Quick import admin contacts: `import`
Expand Down Expand Up @@ -76,18 +76,32 @@ List all contacts in the address book.

Format: `list`

### Locating persons by name/tags: `find`
### Locating persons by keywords: `find`

Finds persons whose names contain any of the given keywords.
Finds persons whose contact details contain any of the given keywords based on the
prefix specified.

Format: `find KEYWORD [MORE_KEYWORDS]`
Format: `find [PREFIX]/KEYWORD [MORE [PREFIX]/KEYWORD]...`

* The search is case-insensitive e.g. `hans` will match `Hans`
* The order of the keywords does not matter e.g. `Hans Bo` will match `Bo Hans`
* Only the name is searched
* Only full words will be matched e.g. `Han` will not match `Hans`
* Persons matching at least one keyword will be returned (i.e. `OR` search) e.g. `Hans Bo` will return `Hans Gruber`
, `Bo Yang`
* The search will filter by the `PREFIX` provided, e.g. `n/` searches through the
names of the contacts, `p/` searches through the phone number of the contacts, `t/`
searches through the tags of the contact, etc...
* Each prefix must be followed by one and only one keyword. See below for example usage.
* The search is done via the logical ***AND*** operator, i.e. `find n/john t/cs` will return
the list of contacts where his name is `john` and has a tag that contains `cs`.
* The following shows a list of allowed prefixes:
1. `n/` which represents the name
2. `s/` which represents the status
3. `p/` which represents the phone number
4. `e/` which represents the email
5. `a/` which represents the address
6. `t/` which represents the tags

Example:

`find n/amy t/cs2103 e/gmail` will return the list of contacts whose names are `amy`,
has a tag labeled `cs2103`, and whose emails contain `gmail`.

### Add an image for contacts

Expand All @@ -108,7 +122,7 @@ Examples:

* `list` followed by `add-image 2 C:/Users/user/Downloads/weekiat.png` adds the image `weekiat.png` to the 2nd person in the address book

## Delete an Image for contacts
### Delete an Image for contacts

Delete the image of a contact.

Expand Down
5 changes: 2 additions & 3 deletions src/main/java/seedu/address/commons/util/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,15 @@ public class StringUtil {
public static boolean containsWordIgnoreCase(String sentence, String word) {
requireNonNull(sentence);
requireNonNull(word);

String preppedWord = word.trim();
String preppedWord = word.trim().toLowerCase();
checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty");
checkArgument(preppedWord.split("\\s+").length == 1, "Word parameter should be a single word");

String preppedSentence = sentence;
String[] wordsInPreppedSentence = preppedSentence.split("\\s+");

return Arrays.stream(wordsInPreppedSentence)
.anyMatch(preppedWord::equalsIgnoreCase);
.anyMatch(wordInPreppedSentence -> wordInPreppedSentence.toLowerCase().contains(preppedWord));
}

/**
Expand Down
37 changes: 17 additions & 20 deletions src/main/java/seedu/address/logic/commands/FindCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,35 @@

import static java.util.Objects.requireNonNull;

import java.util.function.Predicate;

import seedu.address.commons.core.Messages;
import seedu.address.model.Model;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.PhoneContainsKeywordsPredicate;
import seedu.address.model.person.Person;

/**
* Finds and lists all persons in address book whose name contains any of the argument keywords.
* Finds and lists all persons in address book whose details contains any of the
* argument keywords based on the prefixes in the user input.
* Keyword matching is case insensitive.
*/
public class FindCommand extends Command {

public static final String COMMAND_WORD = "find";

public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of "
+ "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n"
+ "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ "Example: " + COMMAND_WORD + " alice bob charlie";

public static final String MESSAGE_USAGE_2 = COMMAND_WORD + ": Finds all persons whose phone numbers contain any "
+ "of the specified keywords / phone number substring and displays them as a list with index numbers.\n"
+ "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ "Example: " + COMMAND_WORD + " 99999999";

private NameContainsKeywordsPredicate predicate;
public static final String MESSAGE_USAGE = COMMAND_WORD
+ ": Finds all persons whose details contain any of "
+ "the specified keywords (case-insensitive) based on "
+ "the prefix provided and displays them as a list with index numbers.\n"
+ "Each prefix must be followed by one and only one keyword.\n"
+ "Please use the \"help\" command for more information on "
+ "the usage of this command.\n"
+ "Parameters: [PREFIX]/KEYWORD [MORE [PREFIX]/KEYWORD]...\n"
+ "Example: " + COMMAND_WORD + " n/alice s/y4 p/91234567"
+ " e/[email protected] a/blk 123 t/cs2103";

private PhoneContainsKeywordsPredicate phonePredicate;

public FindCommand(PhoneContainsKeywordsPredicate phonePredicate) {
this.phonePredicate = phonePredicate;
}
private Predicate<Person> predicate;

public FindCommand(NameContainsKeywordsPredicate predicate) {
public FindCommand(Predicate<Person> predicate) {
this.predicate = predicate;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ public List<String> getAllValues(Prefix prefix) {
if (!argMultimap.containsKey(prefix)) {
return new ArrayList<>();
}
System.out.println(argMultimap.get(prefix));
return new ArrayList<>(argMultimap.get(prefix));
}

Expand Down
94 changes: 69 additions & 25 deletions src/main/java/seedu/address/logic/parser/FindCommandParser.java
Original file line number Diff line number Diff line change
@@ -1,54 +1,98 @@
package seedu.address.logic.parser;

import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS;
import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL;
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE;
import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.PhoneContainsKeywordsPredicate;
import seedu.address.model.person.Person;
import seedu.address.model.person.predicates.AddressContainsKeywordPredicate;
import seedu.address.model.person.predicates.EmailContainsKeywordPredicate;
import seedu.address.model.person.predicates.NameContainsKeywordPredicate;
import seedu.address.model.person.predicates.PhoneContainsKeywordsPredicate;
import seedu.address.model.person.predicates.StatusContainsKeywordsPredicate;
import seedu.address.model.person.predicates.TagContainsKeywordsPredicate;

/**
* Parses input arguments and creates a new FindCommand object
*/
public class FindCommandParser implements Parser<FindCommand> {

private Prefix[] possiblePrefixes = {
PREFIX_NAME,
PREFIX_STATUS,
PREFIX_PHONE,
PREFIX_EMAIL,
PREFIX_ADDRESS,
PREFIX_TAG
};

/**
* Parses the given {@code String} of arguments in the context of the FindCommand
* and returns a FindCommand object for execution.
*
* @throws ParseException if the user input does not conform the expected format
*/
public FindCommand parse(String args) throws ParseException {
String trimmedArgs = args.trim();
Boolean isNumber = onlyDigits(trimmedArgs);
if (trimmedArgs.isEmpty()) {
if (isNumber) {
throw new ParseException(
String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
} else {
throw new ParseException(
String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}
}

if (isNumber) {
return new FindCommand(new PhoneContainsKeywordsPredicate(trimmedArgs));
} else {
String[] nameKeywords = trimmedArgs.split("\\s+");
if (args.isEmpty()) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}
ArgumentMultimap argMultimap =
ArgumentTokenizer.tokenize(args, possiblePrefixes);

return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords)));
Predicate<Person> predicate = null;
for (Prefix p : possiblePrefixes) {
List<String> prefixArguments = argMultimap.getAllValues(p);
if (prefixArguments.isEmpty()) {
continue;
}
for (String arg : prefixArguments) {
if (!isValidArgument(arg)) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}
Predicate<Person> currentPredicate = null;
if (p == PREFIX_NAME) {
currentPredicate = new NameContainsKeywordPredicate(arg);
} else if (p == PREFIX_STATUS) {
currentPredicate = new StatusContainsKeywordsPredicate(arg);
} else if (p == PREFIX_PHONE) {
currentPredicate = new PhoneContainsKeywordsPredicate(arg);
} else if (p == PREFIX_EMAIL) {
currentPredicate = new EmailContainsKeywordPredicate(arg);
} else if (p == PREFIX_ADDRESS) {
currentPredicate = new AddressContainsKeywordPredicate(arg);
} else if (p == PREFIX_TAG) {
currentPredicate = new TagContainsKeywordsPredicate(arg);
}
assert currentPredicate != null;
if (predicate == null) {
predicate = currentPredicate;
} else {
predicate = predicate.and(currentPredicate);
}
}
}
if (predicate == null) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}

return new FindCommand(predicate);
}

private boolean onlyDigits(String str) {
for (int i = 0; i < str.length(); i++) {
if (!Character.isDigit(str.charAt(i))) {
return false;
}
private static boolean isValidArgument(String argument) {
String preppedWord = argument.trim().toLowerCase();
if (preppedWord.isEmpty() || preppedWord.split("\\s+").length != 1) {
return false;
}
return true;
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package seedu.address.model.person.predicates;

import java.util.function.Predicate;

import seedu.address.commons.util.StringUtil;
import seedu.address.model.person.Person;

/**
* Tests that a {@code Person}'s {@code Address} matches any of the keywords given.
*/
public class AddressContainsKeywordPredicate implements Predicate<Person> {
private final String keyword;

public AddressContainsKeywordPredicate(String keyword) {
this.keyword = keyword;
}

@Override
public boolean test(Person person) {
return StringUtil.containsWordIgnoreCase(person.getAddress().toString(), keyword);
}

@Override
public boolean equals(Object other) {
return other == this // short circuit if same object
|| (other instanceof AddressContainsKeywordPredicate // instanceof handles nulls
&& keyword.equals(((AddressContainsKeywordPredicate) other).keyword)); // state check
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package seedu.address.model.person.predicates;

import java.util.function.Predicate;

import seedu.address.commons.util.StringUtil;
import seedu.address.model.person.Person;

/**
* Tests that a {@code Person}'s {@code Email} matches any of the keywords given.
*/
public class EmailContainsKeywordPredicate implements Predicate<Person> {
private final String keyword;

public EmailContainsKeywordPredicate(String keyword) {
this.keyword = keyword;
}

@Override
public boolean test(Person person) {
return StringUtil.containsWordIgnoreCase(person.getEmail().toString(), keyword);
}

@Override
public boolean equals(Object other) {
return other == this // short circuit if same object
|| (other instanceof EmailContainsKeywordPredicate // instanceof handles nulls
&& keyword.equals(((EmailContainsKeywordPredicate) other).keyword)); // state check
}

}
Loading

0 comments on commit bd75a10

Please sign in to comment.