From d8facd44397ae0cd282fa00a2dffbad9bcbd35c4 Mon Sep 17 00:00:00 2001 From: bwangpj <70694994+bwangpj@users.noreply.github.com> Date: Sun, 29 Oct 2023 16:29:16 +0800 Subject: [PATCH 1/2] Add filter by tag command Add filter command to allow users to list all contacts matching a certain Tag (case insensitive). Tests and documentation update to follow. --- .../context/logic/commands/FilterCommand.java | 58 +++++++++++++++++++ .../logic/parser/FilterCommandParser.java | 28 +++++++++ .../swe/context/logic/parser/InputParser.java | 3 + .../model/contact/ContainsTagPredicate.java | 50 ++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 src/main/java/swe/context/logic/commands/FilterCommand.java create mode 100644 src/main/java/swe/context/logic/parser/FilterCommandParser.java create mode 100644 src/main/java/swe/context/model/contact/ContainsTagPredicate.java diff --git a/src/main/java/swe/context/logic/commands/FilterCommand.java b/src/main/java/swe/context/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..b46ea1b5d57 --- /dev/null +++ b/src/main/java/swe/context/logic/commands/FilterCommand.java @@ -0,0 +1,58 @@ +package swe.context.logic.commands; + +import static java.util.Objects.requireNonNull; + +import swe.context.commons.util.ToStringBuilder; +import swe.context.logic.Messages; +import swe.context.model.Model; +import swe.context.model.contact.ContainsTagPredicate; + +/** + * Filters and lists {@link Contact}s whose tags match the specified + * tag in full. Case insensitive. + */ +public class FilterCommand extends Command { + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Filters and lists all contacts whose tags match the" + + " specified tag in full. Case insensitive." + + "\nParameters: TAG" + + "\nExample: " + COMMAND_WORD + " Friend"; + + private final ContainsTagPredicate predicate; + + public FilterCommand(ContainsTagPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setContactsFilter(predicate); + return new CommandResult( + String.format(Messages.CONTACTS_LISTED_OVERVIEW, model.getFilteredContactList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilterCommand)) { + 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/swe/context/logic/parser/FilterCommandParser.java b/src/main/java/swe/context/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..bb41f69180a --- /dev/null +++ b/src/main/java/swe/context/logic/parser/FilterCommandParser.java @@ -0,0 +1,28 @@ +package swe.context.logic.parser; + +import swe.context.logic.Messages; +import swe.context.logic.commands.FilterCommand; +import swe.context.logic.parser.exceptions.ParseException; +import swe.context.model.contact.ContainsTagPredicate; + + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + /** + * Returns a {@link FilterCommand} from parsing the specified arguments. + * + * @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( + Messages.commandInvalidFormat(FilterCommand.MESSAGE_USAGE) + ); + } + + return new FilterCommand(new ContainsTagPredicate(trimmedArgs)); + } +} diff --git a/src/main/java/swe/context/logic/parser/InputParser.java b/src/main/java/swe/context/logic/parser/InputParser.java index 87ec5583dba..f86263e1b14 100644 --- a/src/main/java/swe/context/logic/parser/InputParser.java +++ b/src/main/java/swe/context/logic/parser/InputParser.java @@ -11,6 +11,7 @@ import swe.context.logic.commands.DeleteCommand; import swe.context.logic.commands.EditCommand; import swe.context.logic.commands.ExitCommand; +import swe.context.logic.commands.FilterCommand; import swe.context.logic.commands.FindCommand; import swe.context.logic.commands.HelpCommand; import swe.context.logic.commands.ListCommand; @@ -52,6 +53,8 @@ public static Command parseCommand(String userInput) throws ParseException { return new ListCommand(); case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); case DeleteCommand.COMMAND_WORD: diff --git a/src/main/java/swe/context/model/contact/ContainsTagPredicate.java b/src/main/java/swe/context/model/contact/ContainsTagPredicate.java new file mode 100644 index 00000000000..4c90e17883c --- /dev/null +++ b/src/main/java/swe/context/model/contact/ContainsTagPredicate.java @@ -0,0 +1,50 @@ +package swe.context.model.contact; + +import java.util.function.Predicate; +import java.util.Set; + +import swe.context.commons.util.ToStringBuilder; +import swe.context.model.tag.Tag; + +/** + * Tests that a {@code Contact}'s Tags matches the tag given in full, case insensitive. + */ +public class ContainsTagPredicate implements Predicate { + private final String keyword; + + public ContainsTagPredicate(String keyword) { + this.keyword = keyword; + } + + @Override + public boolean test(Contact contact) { + Set tagSet = contact.getTags(); + + for (Tag tag : tagSet) { + if (tag.toString().equalsIgnoreCase(this.keyword)) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ContainsTagPredicate)) { + return false; + } + + ContainsTagPredicate otherContainsTagPredicate = (ContainsTagPredicate) other; + return keyword.equals(otherContainsTagPredicate.keyword); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keyword", keyword).toString(); + } +} From 78d602a83dc7577a4608f6a90b0f1f8e1fa6a5dc Mon Sep 17 00:00:00 2001 From: bwangpj <70694994+bwangpj@users.noreply.github.com> Date: Mon, 30 Oct 2023 08:43:45 +0800 Subject: [PATCH 2/2] Add tests for filter command Add test coverage for filter command. --- .../logic/commands/FilterCommandTest.java | 92 +++++++++++++++++++ .../logic/parser/FilterCommandParserTest.java | 27 ++++++ .../context/logic/parser/InputParserTest.java | 10 ++ .../contact/ContainsTagPredicateTest.java | 73 +++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 src/test/java/swe/context/logic/commands/FilterCommandTest.java create mode 100644 src/test/java/swe/context/logic/parser/FilterCommandParserTest.java create mode 100644 src/test/java/swe/context/model/contact/ContainsTagPredicateTest.java diff --git a/src/test/java/swe/context/logic/commands/FilterCommandTest.java b/src/test/java/swe/context/logic/commands/FilterCommandTest.java new file mode 100644 index 00000000000..9e5cd017b7a --- /dev/null +++ b/src/test/java/swe/context/logic/commands/FilterCommandTest.java @@ -0,0 +1,92 @@ +package swe.context.logic.commands; + +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 static swe.context.logic.commands.CommandTestUtil.assertCommandSuccess; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import swe.context.logic.Messages; +import swe.context.model.Model; +import swe.context.model.ModelManager; +import swe.context.model.Settings; +import swe.context.model.contact.ContainsTagPredicate; +import swe.context.testutil.TestData; + +/** + * Contains integration tests (interaction with the Model) for {@code FilterCommand}. + */ +public class FilterCommandTest { + private Model model = new ModelManager(TestData.Valid.Contact.getTypicalContacts(), new Settings()); + private Model expectedModel = new ModelManager(TestData.Valid.Contact.getTypicalContacts(), new Settings()); + + @Test + public void equals() { + ContainsTagPredicate firstPredicate = + new ContainsTagPredicate("first"); + ContainsTagPredicate secondPredicate = + new ContainsTagPredicate("second"); + + FilterCommand findFirstCommand = new FilterCommand(firstPredicate); + FilterCommand findSecondCommand = new FilterCommand(secondPredicate); + + // same object -> returns true + assertTrue(findFirstCommand.equals(findFirstCommand)); + + // same values -> returns true + FilterCommand findFirstCommandCopy = new FilterCommand(firstPredicate); + assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + + // different types -> returns false + assertFalse(findFirstCommand.equals(1)); + + // null -> returns false + assertFalse(findFirstCommand.equals(null)); + + // different contact -> returns false + assertFalse(findFirstCommand.equals(findSecondCommand)); + } + + @Test + public void execute_zeroKeywords_noContactFound() { + String expectedMessage = String.format(Messages.CONTACTS_LISTED_OVERVIEW, 0); + ContainsTagPredicate predicate = preparePredicate(" "); + FilterCommand command = new FilterCommand(predicate); + expectedModel.setContactsFilter(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredContactList()); + } + + @Test + public void execute_multipleKeywords_multipleContactsFound() { + String expectedMessage = String.format(Messages.CONTACTS_LISTED_OVERVIEW, 3); + ContainsTagPredicate predicate = preparePredicate("Friends"); + FilterCommand command = new FilterCommand(predicate); + expectedModel.setContactsFilter(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals( + Arrays.asList(TestData.Valid.Contact.ALICE, TestData.Valid.Contact.BENSON, + TestData.Valid.Contact.DANIEL), + model.getFilteredContactList() + ); + } + + @Test + public void toStringMethod() { + ContainsTagPredicate predicate = new ContainsTagPredicate("keyword"); + FilterCommand findCommand = new FilterCommand(predicate); + String expected = FilterCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + assertEquals(expected, findCommand.toString()); + } + + /** + * Parses {@code userInput} into a {@code ContainsTagPredicate}. + */ + private ContainsTagPredicate preparePredicate(String userInput) { + return new ContainsTagPredicate(userInput); + } +} diff --git a/src/test/java/swe/context/logic/parser/FilterCommandParserTest.java b/src/test/java/swe/context/logic/parser/FilterCommandParserTest.java new file mode 100644 index 00000000000..1aa3f016b45 --- /dev/null +++ b/src/test/java/swe/context/logic/parser/FilterCommandParserTest.java @@ -0,0 +1,27 @@ +package swe.context.logic.parser; + +import static swe.context.logic.parser.CommandParserTestUtil.assertParseFailure; +import static swe.context.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import swe.context.logic.Messages; +import swe.context.logic.commands.FilterCommand; +import swe.context.model.contact.ContainsTagPredicate; + + +public class FilterCommandParserTest { + private FilterCommandParser parser = new FilterCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", Messages.commandInvalidFormat(FilterCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsFilterCommand() { + FilterCommand expectedFindCommand = + new FilterCommand(new ContainsTagPredicate("Friends")); + assertParseSuccess(parser, "Friends", expectedFindCommand); + } +} diff --git a/src/test/java/swe/context/logic/parser/InputParserTest.java b/src/test/java/swe/context/logic/parser/InputParserTest.java index f4f41934f5a..42a4c9aea47 100644 --- a/src/test/java/swe/context/logic/parser/InputParserTest.java +++ b/src/test/java/swe/context/logic/parser/InputParserTest.java @@ -19,11 +19,13 @@ import swe.context.logic.commands.EditCommand; import swe.context.logic.commands.EditCommand.EditContactDescriptor; import swe.context.logic.commands.ExitCommand; +import swe.context.logic.commands.FilterCommand; import swe.context.logic.commands.FindCommand; import swe.context.logic.commands.HelpCommand; import swe.context.logic.commands.ListCommand; import swe.context.logic.parser.exceptions.ParseException; import swe.context.model.contact.Contact; +import swe.context.model.contact.ContainsTagPredicate; import swe.context.model.contact.NameContainsKeywordsPredicate; import swe.context.testutil.CommandUtil; import swe.context.testutil.ContactBuilder; @@ -84,6 +86,14 @@ public void parseCommand_exit() throws Exception { assertTrue(InputParser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); } + @Test + public void parseCommand_filter() throws Exception { + String keyword = "foobar"; + FilterCommand command = (FilterCommand) InputParser.parseCommand( + FilterCommand.COMMAND_WORD + " " + keyword); + assertEquals(new FilterCommand(new ContainsTagPredicate(keyword)), command); + } + @Test public void parseCommand_find() throws Exception { List keywords = Arrays.asList("foo", "bar", "baz"); diff --git a/src/test/java/swe/context/model/contact/ContainsTagPredicateTest.java b/src/test/java/swe/context/model/contact/ContainsTagPredicateTest.java new file mode 100644 index 00000000000..7411165c888 --- /dev/null +++ b/src/test/java/swe/context/model/contact/ContainsTagPredicateTest.java @@ -0,0 +1,73 @@ +package swe.context.model.contact; + +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 org.junit.jupiter.api.Test; + +import swe.context.testutil.ContactBuilder; + + +public class ContainsTagPredicateTest { + + @Test + public void equals() { + String firstPredicateKeyword = "friend"; + String secondPredicateKeyword = "colleague"; + + ContainsTagPredicate firstPredicate = new ContainsTagPredicate(firstPredicateKeyword); + ContainsTagPredicate secondPredicate = new ContainsTagPredicate(secondPredicateKeyword); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + ContainsTagPredicate firstPredicateCopy = new ContainsTagPredicate(firstPredicateKeyword); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different contact -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_nameContainsKeywords_returnsTrue() { + // One tag + ContainsTagPredicate predicate = new ContainsTagPredicate("friend"); + assertTrue(predicate.test(new ContactBuilder().withTags("friend").build())); + + // Mixed-case tag + predicate = new ContainsTagPredicate("Friend"); + assertTrue(predicate.test(new ContactBuilder().withTags("friend").build())); + + // Multiple tags + predicate = new ContainsTagPredicate("friend"); + assertTrue(predicate.test(new ContactBuilder().withTags("friend", "colleague").build())); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + // Non-matching tag + ContainsTagPredicate predicate = new ContainsTagPredicate("colleague"); + assertFalse(predicate.test(new ContactBuilder().withTags("friend").build())); + + // Multiple non-matching tags + predicate = new ContainsTagPredicate("colleague"); + assertFalse(predicate.test(new ContactBuilder().withTags("friend", "student").build())); + } + + @Test + public void toStringMethod() { + String keyword = "keyword1"; + ContainsTagPredicate predicate = new ContainsTagPredicate(keyword); + + String expected = ContainsTagPredicate.class.getCanonicalName() + "{keyword=" + keyword + "}"; + assertEquals(expected, predicate.toString()); + } +}