Skip to content

Commit

Permalink
Merge pull request #66 from maze508/findcommand-update
Browse files Browse the repository at this point in the history
Add company search to find command and update UserGuide
  • Loading branch information
maze508 authored Apr 4, 2024
2 parents 068915d + ea8ff23 commit 39f0347
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 219 deletions.
30 changes: 17 additions & 13 deletions docs/UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,23 @@ Examples:

### Searching Contact : `find`

- Search feature supports searching by name and/or tags **ONLY**.
- Finds all contacts whose names or tags matches the substring keyword provided.
- Search feature supports substring search by name and/or tags and/or company **ONLY**.
- Finds all contacts whose names, tags or company matches the substring keyword provided.


General Format: `find FIELD/ KEYWORD FIELD/ KEYWORD ...`
- Where `FIELD` is either `n/` for name or `t/` for tag.
- `KEYWORD` is the keyword (**alphabets only**) to search for.
- Where `FIELD` is either `n/` for name or `t/` for tag or `c/` for company.
- `KEYWORD` is the keyword to search for, here are some guidelines:
- Name and Company should contain alphanumeric characters, spaces, hyphens and/or apostrophes only.
- Tags should contain alphanumeric characters only.
- The search is case-insensitive.
- Teh search will find contacts containing the provided keyword as a substring within the specified field(s)
- Multiple Search Fields are treated as a **Logical AND (&&)**. Therefore, a contact must match all specified keywords across any mentioned fields to appear in the search results.

#### Search Guidelines

* 'KEYWORD' can **ONLY** be alphabets and **CANNOT** contain spaces or be empty.
* e.g. `find n/John Doe` will **NOT** work. Try `find n/John n/Doe` instead to represent finding John and Doe
* 'KEYWORD' cannot be empty.
* e.g. `find n/` will **NOT** work as 'KEYWORD' cannot be empty.
* e.g. `find n/John123` will **NOT** work as 'KEYWORD' cannot contain non-alphabetic characters.


* 'KEYWORD' and next 'FIELD' should be separated by a space.
Expand All @@ -256,10 +260,11 @@ General Format: `find FIELD/ KEYWORD FIELD/ KEYWORD ...`
* and there should not be non-alphabetic characters in the 'KEYWORD' field.


* Multiple of the same 'FIELDs' will be treated as a **Logical AND (&&)**.
* Multiple Search 'FIELD's will be treated as a **Logical AND (&&)**.
* e.g. `find n/John n/Doe` will return all instances of John and Doe.
* e.g. `find n/Ale n/le` will still return the following example instances ["Alex Liew", "Alexis Lebrun", "Alec"]

* e.g. `find n/John t/friends c/ Meat` will return all instances of John that are tagged as friends and have Meat in their company name. This means if there exists a contact with the name John that is tagged as friends but has a company Mat, it will not be returned.
* e.g. `find n/Ale n/le` can return contacts such as ["Alex Lew", "Alexis Lebrun", "Alec"]


* 'KEYWORD' should **NOT** be empty and there should be at least one 'FIELD' and 'KEYWORD' pair.
* e.g. `find n/ t/` and `find ` will **NOT** work.
Expand All @@ -268,15 +273,14 @@ General Format: `find FIELD/ KEYWORD FIELD/ KEYWORD ...`
* There should not be prefixes before the first 'FIELD' and 'KEYWORD' pair.
* e.g. `find testing123 n/John` will **NOT** work.


* The search is case-insensitive.
* e.g. `find n/hans` will match `Hans Niemann` and `Hans Zimmer`

* The order of the keywords does not matter.
* e.g. Results of `find n/Hans n/Bo` will match the results of`find n/Bo n/Hans`

* You can have multiple of the same 'FIELD's.
* e.g. `find n/J n/Do` will match names with `J` AND `Do`, like `John Doe`
* e.g. `find n/J n/Do` will match names with `J` AND `Do`, like `John Doe` or `Dohnut Jibs`


Examples:
Expand All @@ -286,7 +290,7 @@ Examples:

* `find n/Alex t/friends` returns `Alex Yeoh` who is tagged as a `friend`

* `find n////` returns an error message as the 'KEYWORD' field must consist of alphabets only
* `find n////` returns an error message as the 'KEYWORD' field must consist of alphanumeric characters, spaces, hyphens and/or apostrophes only.

* `find n/` or `find t/` or `find n/ t/` returns an error message as the 'KEYWORD' field cannot be empty

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/seedu/address/logic/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public class Messages {
public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!";
public static final String MESSAGE_DUPLICATE_FIELDS =
"Multiple values specified for the following single-valued field(s): ";
public static final String MESSAGE_ALPHABET_ONLY = "Value for %1$s must consist of alphabets only.";
public static final String MESSAGE_ALPHANUMERIC_ONLY = "Value for %1$s must consist of alphabets or numbers only.";
public static final String MESSAGE_NAME_COMPANY_CONSTRAINTS = "Value for %1$s must consist of "
+ "alphanumeric characters, spaces, hyphens and/or apostrophes only";

public static final String MESSAGE_CANNOT_BE_EMPTY = "Keyword Value of FIELD %1$s cannot be empty.";

Expand Down
18 changes: 9 additions & 9 deletions src/main/java/seedu/address/logic/commands/FindCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
import seedu.address.commons.util.ToStringBuilder;
import seedu.address.logic.Messages;
import seedu.address.model.Model;
import seedu.address.model.person.NameAndTagContainsKeywordsPredicate;
import seedu.address.model.person.SearchPredicate;

/**
* 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 name, tags or company name contains any of the argument keywords.
* 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 "
public static final String MESSAGE_USAGE = COMMAND_WORD
+ ": Finds all persons whose names, tags or company names contain all of "
+ "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n"
+ "Parameters: TYPE/KEYWORD TYPE/KEYWORD ... \n"
+ "Where TYPE can be n/ or t/ to specify name or tag respectively.\n"
+ "Example: " + COMMAND_WORD + " n/alice n/bob t/friends t/owesMoney \n"
+ "Please Refer to User Guide for more details.";
+ "Parameters: n/KEYWORD [MORE_KEYWORDS]... t/KEYWORD [MORE_KEYWORDS]... c/KEYWORD [MORE_KEYWORDS]...\n"
+ "Example: " + COMMAND_WORD + " n/alice t/friends c/Meat \n"
+ "Note: Multiple keywords (name, tag or company) are treated with a logical AND.";

private final NameAndTagContainsKeywordsPredicate predicate;
private final SearchPredicate predicate;

public FindCommand(NameAndTagContainsKeywordsPredicate predicate) {
public FindCommand(SearchPredicate predicate) {
this.predicate = predicate;
}

Expand Down
32 changes: 24 additions & 8 deletions src/main/java/seedu/address/logic/parser/ArgumentMultimap.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,38 @@ public void verifyNonEmptyKeywordValues(Prefix... prefixes) throws ParseExceptio

/**
* Throws a {@code ParseException} if any of the keywords in the prefixes given in {@code prefixes}
* are not alphabets.
* are not alphanumeric.
*/
public void verifyAllValuesAlpha(Prefix... prefixes) throws ParseException {
public void verifyAllValuesAlphanumeric(Prefix... prefixes) throws ParseException {
for (Prefix prefix : prefixes) {
List<String> values = getAllValues(prefix);
checkValuesAlpha(values, prefix);
checkValuesAlphanumeric(values, prefix);
}
}

/**
* Throws a {@code ParseException} if any of the characters in the prefixes given in {@code prefixes}
* are not alphabets.
* Throws a {@code ParseException} if any of the characters in the given values
* are not alphanumeric.
*/
private void checkValuesAlpha(List<String> values, Prefix prefix) throws ParseException {
if (values.stream().anyMatch(value -> !value.matches("^[a-zA-Z]+$"))) {
throw new ParseException(String.format(Messages.MESSAGE_ALPHABET_ONLY, prefix.getPrefix()));
private void checkValuesAlphanumeric(List<String> values, Prefix prefix) throws ParseException {
if (values.stream().anyMatch(value -> !value.matches("^[a-zA-Z0-9]+$"))) {
throw new ParseException(String.format(Messages.MESSAGE_ALPHANUMERIC_ONLY, prefix.getPrefix()));
}
}

/**
* Throws a {@code ParseException} if any of the keywords in the prefixes given in {@code prefixes}
* do not adhere to the validation regex for names and company names.
*/
public void verifyValuesNameCompany(Prefix... prefixes) throws ParseException {
String validationRegex = "[\\p{Alnum}'\\-][\\p{Alnum}'\\- ]*";
for (Prefix prefix : prefixes) {
if (argMultimap.containsKey(prefix)) {
List<String> values = getAllValues(prefix);
if (values.stream().anyMatch(value -> !value.matches(validationRegex))) {
throw new ParseException(String.format(Messages.MESSAGE_NAME_COMPANY_CONSTRAINTS, prefix));
}
}
}
}

Expand Down
17 changes: 10 additions & 7 deletions src/main/java/seedu/address/logic/parser/FindCommandParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@


import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
import static seedu.address.logic.parser.CliSyntax.PREFIX_COMPANY;
import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;

import java.util.List;

import seedu.address.logic.commands.FindCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.NameAndTagContainsKeywordsPredicate;
import seedu.address.model.person.SearchPredicate;

/**
* Parses input arguments and creates a new FindCommand object
Expand All @@ -24,28 +25,30 @@ public class FindCommandParser implements Parser<FindCommand> {
@Override
public FindCommand parse(String args) throws ParseException {
ArgumentMultimap argMultimap =
ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_TAG);
ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_TAG, PREFIX_COMPANY);

// Ensure no preamble exists.
if (!argMultimap.getPreamble().isEmpty()) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}

// Check for non-empty Keywords after all Prefixes
argMultimap.verifyNonEmptyKeywordValues(PREFIX_NAME, PREFIX_TAG);
argMultimap.verifyNonEmptyKeywordValues(PREFIX_NAME, PREFIX_TAG, PREFIX_COMPANY);

// Ensure all keywords are alphabets
argMultimap.verifyAllValuesAlpha(PREFIX_NAME, PREFIX_TAG);
// Ensure all keywords are alphanumeric ONLY for tag and alphanumeric + hyphen + apostrophe for name and company
argMultimap.verifyAllValuesAlphanumeric(PREFIX_TAG);
argMultimap.verifyValuesNameCompany(PREFIX_NAME, PREFIX_COMPANY);

List<String> nameKeywords = argMultimap.getAllValues(PREFIX_NAME);
List<String> companyKeywords = argMultimap.getAllValues(PREFIX_COMPANY);
List<String> tagKeywords = argMultimap.getAllValues(PREFIX_TAG);

// Ensure that at least one name or tag keyword is provided
if (nameKeywords.isEmpty() && tagKeywords.isEmpty()) {
if (nameKeywords.isEmpty() && tagKeywords.isEmpty() && companyKeywords.isEmpty()) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE));
}

return new FindCommand(new NameAndTagContainsKeywordsPredicate(nameKeywords, tagKeywords));
return new FindCommand(new SearchPredicate(nameKeywords, tagKeywords, companyKeywords));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@
/**
* Tests that a {@code Person}'s {@code Name} and {@code Tag} matches any of the keywords given.
*/
public class NameAndTagContainsKeywordsPredicate implements Predicate<Person> {
public class SearchPredicate implements Predicate<Person> {
private final List<String> nameKeywords;
private final List<String> tagKeywords;
private final List<String> companyKeywords;

/**
* Constructor for NameAndTagContainsKeywordsPredicate.
* Constructor for SearchPredicate.
* @param nameKeywords List of name keywords to search for.
* @param tagKeywords List of tag keywords to search for.
* @param companyKeywords List of company keywords to search for.
*/
public NameAndTagContainsKeywordsPredicate(List<String> nameKeywords, List<String> tagKeywords) {
public SearchPredicate(List<String> nameKeywords, List<String> tagKeywords,
List<String> companyKeywords) {
this.nameKeywords = nameKeywords;
this.tagKeywords = tagKeywords;
this.companyKeywords = companyKeywords;

}

@Override
Expand All @@ -31,7 +36,9 @@ public boolean test(Person person) {
boolean matchesTags = tagKeywords.stream()
.allMatch(keyword -> person.getTags().stream()
.anyMatch(tag -> StringUtil.containsSubstringIgnoreCase(tag.tagName, keyword)));
return matchesName && matchesTags;
boolean matchesCompany = companyKeywords.stream()
.allMatch(keyword -> StringUtil.containsSubstringIgnoreCase(person.getCompany().companyName, keyword));
return matchesName && matchesTags && matchesCompany;
}

@Override
Expand All @@ -41,20 +48,22 @@ public boolean equals(Object other) {
}

// instanceof handles nulls
if (!(other instanceof NameAndTagContainsKeywordsPredicate)) {
if (!(other instanceof SearchPredicate)) {
return false;
}

NameAndTagContainsKeywordsPredicate that = (NameAndTagContainsKeywordsPredicate) other;
SearchPredicate that = (SearchPredicate) other;
return Objects.equals(nameKeywords, that.nameKeywords)
&& Objects.equals(tagKeywords, that.tagKeywords);
&& Objects.equals(tagKeywords, that.tagKeywords)
&& Objects.equals(companyKeywords, that.companyKeywords);
}

@Override
public String toString() {
return "NameAndTagContainsKeywordsPredicate{"
return "SearchPredicate{"
+ "nameKeywords=" + nameKeywords
+ ", tagKeywords=" + tagKeywords
+ ", companyKeywords=" + companyKeywords
+ '}';
}
}
Loading

0 comments on commit 39f0347

Please sign in to comment.