From 0945fb8d0a6b38f595d494767d03d577b49e38f6 Mon Sep 17 00:00:00 2001 From: wolffe88 Date: Mon, 18 Mar 2024 23:33:39 +0800 Subject: [PATCH 1/2] add filter command --- .../address/logic/commands/FilterCommand.java | 58 +++++++++++ .../logic/parser/AddressBookParser.java | 4 + .../logic/parser/FilterCommandParser.java | 33 +++++++ .../address/model/person/Department.java | 4 +- .../person/NameContainsKeywordsPredicate.java | 3 +- .../PersonContainsKeywordsPredicate.java | 45 +++++++++ .../logic/commands/FilterCommandTest.java | 51 ++++++++++ .../logic/parser/FilterCommandParserTest.java | 20 ++++ .../NameContainsKeywordsPredicateTest.java | 5 - .../PersonContainsKeywordsPredicateTest.java | 96 +++++++++++++++++++ 10 files changed, 310 insertions(+), 9 deletions(-) create mode 100644 src/main/java/seedu/address/logic/commands/FilterCommand.java create mode 100644 src/main/java/seedu/address/logic/parser/FilterCommandParser.java create mode 100644 src/main/java/seedu/address/model/person/PersonContainsKeywordsPredicate.java create mode 100644 src/test/java/seedu/address/logic/commands/FilterCommandTest.java create mode 100644 src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java create mode 100644 src/test/java/seedu/address/model/person/PersonContainsKeywordsPredicateTest.java diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..a4bc09a1846 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.PersonContainsKeywordsPredicate; + +/** + * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FilterCommand extends Command { + + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters all persons by the given departments or tags" + + "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"; + + private final PersonContainsKeywordsPredicate predicate; + + public FilterCommand(PersonContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindCommand)) { + return false; + } + + FilterCommand otherFilterCommand = (FilterCommand) other; + return predicate.equals(otherFilterCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..c76ab2b8429 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -14,6 +14,7 @@ import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; @@ -77,6 +78,9 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..b40b958e545 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.PersonContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FilterCommand + * and returns a FilterCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FilterCommand(new PersonContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/model/person/Department.java b/src/main/java/seedu/address/model/person/Department.java index 3280bf44107..467d555bac2 100644 --- a/src/main/java/seedu/address/model/person/Department.java +++ b/src/main/java/seedu/address/model/person/Department.java @@ -23,7 +23,7 @@ public class Department { public Department(String department) { requireNonNull(department); checkArgument(isValidDepartment(department), MESSAGE_CONSTRAINTS); - this.department = department; + this.department = department.toUpperCase(); } /** @@ -57,7 +57,7 @@ public int hashCode() { * Format state as text for viewing. */ public String toString() { - return '{' + department + '}'; + return '{' + department.toUpperCase() + '}'; } } diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index b7ccc452b11..f39ed0763fc 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -20,8 +20,7 @@ public NameContainsKeywordsPredicate(List keywords) { public boolean test(Person person) { return keywords.stream() .anyMatch(keyword -> person.getName().fullName.toLowerCase().replaceAll("\\s", "") - .contains(keyword.toLowerCase()) - || person.getTags().toString().toLowerCase().contains(keyword.toLowerCase())); + .contains(keyword.toLowerCase())); } @Override diff --git a/src/main/java/seedu/address/model/person/PersonContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PersonContainsKeywordsPredicate.java new file mode 100644 index 00000000000..0fe95296e60 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonContainsKeywordsPredicate.java @@ -0,0 +1,45 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class PersonContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public PersonContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> person.getDepartment().toString().toLowerCase().replaceAll("\\s", "") + .contains(keyword.toLowerCase()) + || person.getTags().toString().toLowerCase().contains(keyword.toLowerCase())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonContainsKeywordsPredicate)) { + return false; + } + + PersonContainsKeywordsPredicate otherPersonContainsKeywordsPredicate = (PersonContainsKeywordsPredicate) other; + return keywords.equals(otherPersonContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/test/java/seedu/address/logic/commands/FilterCommandTest.java b/src/test/java/seedu/address/logic/commands/FilterCommandTest.java new file mode 100644 index 00000000000..bbfc02458ee --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/FilterCommandTest.java @@ -0,0 +1,51 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; +import java.util.Collections; + +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.PersonContainsKeywordsPredicate; + +class FilterCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_zeroKeywords_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + PersonContainsKeywordsPredicate predicate = preparePredicate(" "); + FilterCommand command = new FilterCommand(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } + + @Test + public void execute_multipleKeywords_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 2); + PersonContainsKeywordsPredicate predicate = preparePredicate("Marketing Production"); + FilterCommand command = new FilterCommand(predicate); + expectedModel.updateFilteredPersonList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(CARL, DANIEL), model.getFilteredPersonList()); + } + + /** + * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. + */ + private PersonContainsKeywordsPredicate preparePredicate(String userInput) { + return new PersonContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java new file mode 100644 index 00000000000..49e5004763d --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java @@ -0,0 +1,20 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.FilterCommand; + +public class FilterCommandParserTest { + + private FilterCommandParser parser = new FilterCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + +} diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java index 28a3bf1e362..68e86917799 100644 --- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java +++ b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java @@ -64,11 +64,6 @@ public void test_nameContainsKeywords_returnsTrue() { // Keywords with substrings predicate = new NameContainsKeywordsPredicate(Collections.singletonList("ice")); assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - - // Keywords match tags - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "friend", "Street")); - assertTrue(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") - .withEmail("alice@email.com").withAddress("Main Street").withTags("friend").build())); } @Test diff --git a/src/test/java/seedu/address/model/person/PersonContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/PersonContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..5a9b8e5e63c --- /dev/null +++ b/src/test/java/seedu/address/model/person/PersonContainsKeywordsPredicateTest.java @@ -0,0 +1,96 @@ +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; + +class PersonContainsKeywordsPredicateTest { + + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + PersonContainsKeywordsPredicate firstPredicate = new PersonContainsKeywordsPredicate(firstPredicateKeywordList); + PersonContainsKeywordsPredicate secondPredicate = + new PersonContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + PersonContainsKeywordsPredicate firstPredicateCopy = + new PersonContainsKeywordsPredicate(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_nameContainsKeywords_returnsTrue() { + // One keyword + PersonContainsKeywordsPredicate predicate = + new PersonContainsKeywordsPredicate(Collections.singletonList("Finance")); + assertTrue(predicate.test(new PersonBuilder().withDepartment("Finance").build())); + + // Multiple keywords + predicate = new PersonContainsKeywordsPredicate(Arrays.asList("Quantitative", "Finance")); + assertTrue(predicate.test(new PersonBuilder().withDepartment("Quantitative Finance").build())); + + // Only one matching keyword + predicate = new PersonContainsKeywordsPredicate(Arrays.asList("Finance", "Marketing")); + assertTrue(predicate.test(new PersonBuilder().withDepartment("Quantitative Finance").build())); + + // Mixed-case keywords + predicate = new PersonContainsKeywordsPredicate(Arrays.asList("finAnce", "quaNtitative")); + assertTrue(predicate.test(new PersonBuilder().withDepartment("Quantitative Finance").build())); + + // Keywords without spacing + predicate = new PersonContainsKeywordsPredicate(Collections.singletonList("quantitativefinance")); + assertTrue(predicate.test(new PersonBuilder().withDepartment("Quantitative Finance").build())); + + // Keywords with substrings + predicate = new PersonContainsKeywordsPredicate(Collections.singletonList("nan")); + assertTrue(predicate.test(new PersonBuilder().withDepartment("Quantitative Finance").build())); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + // Zero keywords + NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PersonBuilder().withDepartment("Finance").build())); + + // Non-matching keyword + predicate = new NameContainsKeywordsPredicate(Arrays.asList("Marketing")); + assertFalse(predicate.test(new PersonBuilder().withDepartment("Finance").build())); + + // Keywords match phone, email and address, but does not match department + predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") + .withEmail("alice@email.com").withAddress("Main Street").withDepartment("Finance").build())); + } + + @Test + public void toStringMethod() { + List keywords = List.of("keyword1", "keyword2"); + PersonContainsKeywordsPredicate predicate = new PersonContainsKeywordsPredicate(keywords); + + String expected = PersonContainsKeywordsPredicate.class.getCanonicalName() + "{keywords=" + keywords + "}"; + assertEquals(expected, predicate.toString()); + } +} From 84eaf5f980de67ed6b0e6f10a3c929817ea1968f Mon Sep 17 00:00:00 2001 From: wolffe88 Date: Mon, 18 Mar 2024 23:47:45 +0800 Subject: [PATCH 2/2] add filter command --- .../seedu/address/logic/commands/FilterCommand.java | 2 +- .../address/logic/parser/AddressBookParserTest.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java index a4bc09a1846..d07712b13cd 100644 --- a/src/main/java/seedu/address/logic/commands/FilterCommand.java +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -41,7 +41,7 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof FindCommand)) { + if (!(other instanceof FilterCommand)) { return false; } diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 5a1ab3dbc0c..afdb5fc146f 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -19,12 +19,14 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonContainsKeywordsPredicate; import seedu.address.testutil.EditPersonDescriptorBuilder; import seedu.address.testutil.PersonBuilder; import seedu.address.testutil.PersonUtil; @@ -76,6 +78,14 @@ public void parseCommand_find() throws Exception { assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); } + @Test + public void parseCommand_filter() throws Exception { + List keywords = Arrays.asList("foo", "bar", "baz"); + FilterCommand command = (FilterCommand) parser.parseCommand( + FilterCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); + assertEquals(new FilterCommand(new PersonContainsKeywordsPredicate(keywords)), command); + } + @Test public void parseCommand_help() throws Exception { assertTrue(parser.parseCommand(HelpCommand.COMMAND_WORD) instanceof HelpCommand);