Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find contacts by tag #71

Merged
merged 2 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ public abstract class AbstractFindCommand extends Command {
public static final String NAME_COMMAND_WORD = " n/";
public static final String EMAIL_COMMAND_WORD = " e/";
public static final String CONTACT_COMMAND_WORD = " c/";
public static final String TAG_COMMAND_WORD = " t/";

public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names, contacts or emails "
+ "contain any of the specified keywords (case-insensitive) and displays them as a list with indices.\n"
public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names, contacts, emails "
+ "or tagscontain any of the specified keywords (case-insensitive) and displays"
+ "them as a list with indices.\n"
+ "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ "Example:\n"
+ COMMAND_WORD + NAME_COMMAND_WORD + "alice bob charlie\n"
+ COMMAND_WORD + EMAIL_COMMAND_WORD + "[email protected]\n"
+ COMMAND_WORD + CONTACT_COMMAND_WORD + "12345678\n";
+ COMMAND_WORD + CONTACT_COMMAND_WORD + "12345678\n"
+ COMMAND_WORD + TAG_COMMAND_WORD + "CS2100_classmate\n";

private final ContainsKeywordsPredicate predicate;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class FindByNameCommand extends AbstractFindCommand {
+ "contain any of the specified keywords (case-insensitive) and displays them as a list with indices.\n"
+ "Parameters: KEYWORD [MORE_KEYWORDS]...\n"
+ "Example: " + COMMAND_WORD + " alice bob charlie";

public FindByNameCommand(NameContainsKeywordsPredicate predicate) {
super(predicate);
}
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/seedu/address/logic/commands/FindByTagCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package seedu.address.logic.commands;

import seedu.address.model.person.TagContainsKeywordsPredicate;

/**
* Finds and lists all persons in address book whose name contains any of the argument keywords.
* Keyword matching is case-insensitive.
*/
public class FindByTagCommand extends AbstractFindCommand {
public static final String COMMAND_WORD = "find t/";

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

public FindByTagCommand(TagContainsKeywordsPredicate predicate) {
super(predicate);
}

@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}

// instanceof handles nulls
if (!(other instanceof FindByTagCommand)) {
return false;

Check warning on line 29 in src/main/java/seedu/address/logic/commands/FindByTagCommand.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/seedu/address/logic/commands/FindByTagCommand.java#L29

Added line #L29 was not covered by tests
}

FindByTagCommand otherFindCommand = (FindByTagCommand) other;
return this.getPredicate().equals(otherFindCommand.getPredicate());
}
}
20 changes: 13 additions & 7 deletions src/main/java/seedu/address/logic/parser/FindCommandParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,28 @@
import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import seedu.address.logic.commands.AbstractFindCommand;
import seedu.address.logic.commands.FindByContactCommand;
import seedu.address.logic.commands.FindByEmailCommand;
import seedu.address.logic.commands.FindByNameCommand;
import seedu.address.logic.commands.FindByTagCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.ContactContainsKeywordsPredicate;
import seedu.address.model.person.EmailContainsKeywordsPredicate;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.TagContainsKeywordsPredicate;

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

public static final Pattern KEYWORD_EXTRACTOR =
Pattern.compile("^(?<tag>[cen]/)\\s*(?<arguments>[\\S\\s]+)$");
Pattern.compile("^(?<type>[cent]/)\\s*(?<arguments>[\\S\\s]+)$");

/**
* Parses the given {@code String} of arguments in the context of the FindCommand
Expand All @@ -39,21 +42,24 @@ public AbstractFindCommand parse(String args) throws ParseException {
}

// extract tag and search argument
String tag = m.group("tag");
String searchTerms = m.group("arguments");
String[] searchTermArray = searchTerms.split("\\s+");
String tag = m.group("type");
String[] searchTerms = m.group("arguments").split("\\s+");
List<String> searchTermArray = Arrays.asList(searchTerms);

// return appropriate FindCommand class depending on tag
switch (tag) {
case "n/":
return new FindByNameCommand(
new NameContainsKeywordsPredicate(Arrays.asList(searchTermArray)));
new NameContainsKeywordsPredicate(searchTermArray));
case "c/":
return new FindByContactCommand(
new ContactContainsKeywordsPredicate(Arrays.asList(searchTermArray)));
new ContactContainsKeywordsPredicate(searchTermArray));
case "e/":
return new FindByEmailCommand(
new EmailContainsKeywordsPredicate(Arrays.asList(searchTermArray)));
new EmailContainsKeywordsPredicate(searchTermArray));
case "t/":
return new FindByTagCommand(
new TagContainsKeywordsPredicate(searchTermArray));
default:
return null; // temporary value, this should not occur due to regex
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package seedu.address.model.person;

import java.util.List;

import seedu.address.commons.util.ToStringBuilder;

/**
* Tests that a {@code Person}'s {@code Tag} matches any of the keywords given.
*/
public class TagContainsKeywordsPredicate extends ContainsKeywordsPredicate {

public TagContainsKeywordsPredicate(List<String> keywords) {
super(keywords);
}

@Override
public boolean test(Person person) {
return this.getKeywords().stream()
.anyMatch(keyword -> person.getTags().toString().toLowerCase().contains(keyword.toLowerCase()));
}

@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}

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

TagContainsKeywordsPredicate otherTagContainsKeywordsPredicate = (TagContainsKeywordsPredicate) other;
return this.getKeywords().equals(otherTagContainsKeywordsPredicate.getKeywords());
}

@Override
public String toString() {
return new ToStringBuilder(this).add("keywords", this.getKeywords()).toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package seedu.address.logic.commands;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW;
import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess;
import static seedu.address.testutil.TypicalPersons.ALICE;
import static seedu.address.testutil.TypicalPersons.BENSON;
import static seedu.address.testutil.TypicalPersons.DANIEL;
import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.Test;

import seedu.address.model.Model;
import seedu.address.model.ModelManager;
import seedu.address.model.UserPrefs;
import seedu.address.model.person.TagContainsKeywordsPredicate;

public class FindByTagCommandTest {

private final Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs());
private final Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs());

@Test
public void equals() {
TagContainsKeywordsPredicate firstPredicate =
new TagContainsKeywordsPredicate(Collections.singletonList("CS2100classmate"));
TagContainsKeywordsPredicate secondPredicate =
new TagContainsKeywordsPredicate(Collections.singletonList("CS1101STA"));

FindByTagCommand findFirstCommand = new FindByTagCommand(firstPredicate);
FindByTagCommand findSecondCommand = new FindByTagCommand(secondPredicate);

// same object -> returns true
assertEquals(findFirstCommand, findFirstCommand);

// same values -> returns true
FindByTagCommand findFirstCommandCopy = new FindByTagCommand(firstPredicate);
assertEquals(findFirstCommand, findFirstCommandCopy);

// different types -> returns false
assertNotEquals(1, findFirstCommand);

// null -> returns false
assertNotEquals(null, findFirstCommand);

// different person -> returns false
assertNotEquals(findFirstCommand, findSecondCommand);
}

@Test
public void execute_zeroKeywords_noPersonFound() {
String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0);
TagContainsKeywordsPredicate predicate = preparePredicate(" ");
FindByTagCommand command = new FindByTagCommand(predicate);
expectedModel.updateFilteredPersonList(predicate);
assertCommandSuccess(command, model, expectedMessage, expectedModel);
assertEquals(Collections.emptyList(), model.getFilteredPersonList());
}

@Test
public void execute_resultsWithPartialMatch_multiplePersonsFound() {
System.out.println(expectedModel.getAddressBook().toString());
String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3);
TagContainsKeywordsPredicate predicate = preparePredicate("en");
FindByTagCommand command = new FindByTagCommand(predicate);
expectedModel.updateFilteredPersonList(predicate);
assertCommandSuccess(command, model, expectedMessage, expectedModel);
assertEquals(Arrays.asList(ALICE, BENSON, DANIEL), model.getFilteredPersonList());
}

@Test
public void toStringMethod() {
TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(List.of("keyword"));
FindByTagCommand findCommand = new FindByTagCommand(predicate);
String expected = FindByTagCommand.class.getCanonicalName() + "{predicate=" + predicate + "}";
assertEquals(expected, findCommand.toString());
}

/**
* Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}.
*/
private TagContainsKeywordsPredicate preparePredicate(String userInput) {
return new TagContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+")));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import seedu.address.logic.commands.FindByContactCommand;
import seedu.address.logic.commands.FindByEmailCommand;
import seedu.address.logic.commands.FindByNameCommand;
import seedu.address.logic.commands.FindByTagCommand;
import seedu.address.model.person.ContactContainsKeywordsPredicate;
import seedu.address.model.person.EmailContainsKeywordsPredicate;
import seedu.address.model.person.NameContainsKeywordsPredicate;
import seedu.address.model.person.TagContainsKeywordsPredicate;

public class FindCommandParserTest {

Expand Down Expand Up @@ -64,4 +66,17 @@ public void parse_validArgs_returnsFindByEmailCommand() {
assertParseSuccess(parser, "e/ \n [email protected] \n \t [email protected] \t", expectedFindCommand);
}

@Test
public void parse_validArgs_returnsFindByTagCommand() {
FindByTagCommand expectedFindCommand =
new FindByTagCommand(new TagContainsKeywordsPredicate(
Arrays.asList("PC2174ALecturer", "PC2032classmate")));

// no leading and trailing whitespaces
assertParseSuccess(parser, "t/PC2174ALecturer PC2032classmate", expectedFindCommand);

// multiple whitespaces between keywords
assertParseSuccess(parser, "t/ \n PC2174ALecturer \n \t PC2032classmate \t", expectedFindCommand);
}

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.Test;

import seedu.address.testutil.PersonBuilder;

public class TagContainsKeywordsPredicateTest {

@Test
public void equals() {
List<String> firstPredicateKeywordList = Collections.singletonList("first");
List<String> secondPredicateKeywordList = Arrays.asList("first", "second");

TagContainsKeywordsPredicate firstPredicate = new TagContainsKeywordsPredicate(firstPredicateKeywordList);
TagContainsKeywordsPredicate secondPredicate = new TagContainsKeywordsPredicate(secondPredicateKeywordList);

// same object -> returns true
assertTrue(firstPredicate.equals(firstPredicate));

// same values -> returns true
TagContainsKeywordsPredicate firstPredicateCopy = new TagContainsKeywordsPredicate(firstPredicateKeywordList);
assertTrue(firstPredicate.equals(firstPredicateCopy));

// different types -> returns false
assertFalse(firstPredicate.equals(1));

// null -> returns false
assertFalse(firstPredicate.equals(null));

// different person -> returns false
assertFalse(firstPredicate.equals(secondPredicate));
}

@Test
public void test_tagContainsKeywords_returnsTrue() {
// One keyword
TagContainsKeywordsPredicate predicate =
new TagContainsKeywordsPredicate(Collections.singletonList("CS"));
assertTrue(predicate.test(new PersonBuilder().withTags("CS2100", "PC2174A").build()));

// Multiple keywords
predicate = new TagContainsKeywordsPredicate(Arrays.asList("CS", "PC"));
assertTrue(predicate.test(new PersonBuilder().withTags("CS2100", "PC2174A").build()));

// Only one matching keyword
predicate = new TagContainsKeywordsPredicate(Arrays.asList("IS1108", "NUSFloorball"));
assertTrue(predicate.test(new PersonBuilder().withTags("NUSFloorball", "PC2174A").build()));

// Mixed-case keywords
predicate = new TagContainsKeywordsPredicate(Arrays.asList("nUsfLoOrBaLL", "NUscHoIr"));
assertTrue(predicate.test(new PersonBuilder().withTags("NUSFloorball", "NUSChoir").build()));
}

@Test
public void test_tagDoesNotContainKeywords_returnsFalse() {
// Zero keywords
TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.emptyList());
assertFalse(predicate.test(new PersonBuilder().withTags("CS2100", "PC2174A").build()));

// Non-matching keyword
predicate = new TagContainsKeywordsPredicate(Arrays.asList("IS", "8"));
assertFalse(predicate.test(new PersonBuilder().withTags("CS2100", "PC2174A").build()));

// Keywords match phone, email address and name, but does not match tag
predicate = new TagContainsKeywordsPredicate(Arrays.asList("12345", "[email protected]", "Main", "Street"));
assertFalse(predicate.test(new PersonBuilder().withTags("Alice").withPhone("12345")
.withEmail("[email protected]").withAddress("Main Street").withTags("hehe").build()));
}

@Test
public void toStringMethod() {
List<String> keywords = List.of("keyword1", "keyword2");
TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(keywords);

String expected = TagContainsKeywordsPredicate.class.getCanonicalName() + "{keywords=" + keywords + "}";
assertEquals(expected, predicate.toString());
}
}