From 5736017ae99d75bcbaf39c231a89487a996319b2 Mon Sep 17 00:00:00 2001 From: iynixil Date: Wed, 13 Mar 2024 13:19:39 +0800 Subject: [PATCH 1/4] Add filter functionality for single tag --- .../logic/commands/FilterCommand.java | 47 +++++++++++++++++++ .../logic/parser/FilterCommandParser.java | 42 +++++++++++++++++ .../logic/parser/StaffConnectParser.java | 4 ++ .../model/person/PersonHasTagPredicate.java | 43 +++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 src/main/java/staffconnect/logic/commands/FilterCommand.java create mode 100644 src/main/java/staffconnect/logic/parser/FilterCommandParser.java create mode 100644 src/main/java/staffconnect/model/person/PersonHasTagPredicate.java diff --git a/src/main/java/staffconnect/logic/commands/FilterCommand.java b/src/main/java/staffconnect/logic/commands/FilterCommand.java new file mode 100644 index 00000000000..5f47d1170d3 --- /dev/null +++ b/src/main/java/staffconnect/logic/commands/FilterCommand.java @@ -0,0 +1,47 @@ +package staffconnect.logic.commands; + +import static java.util.Objects.requireNonNull; + +import staffconnect.logic.Messages; +// import static staffconnect.logic.parser.CliSyntax.PREFIX_FACULTY; // TODO: add filtering for faculty and module +// import static staffconnect.logic.parser.CliSyntax.PREFIX_MODULE; +import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; +import staffconnect.model.Model; +import staffconnect.model.person.PersonHasTagPredicate; // TagContainsKeywordsPredicate + +/** + * Filters all persons in staff book whose module code or faculty shorthand or + * tags include the given filtering criteria. + * Criteria 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 in staff book whose tags" + + " include the given tag name (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: " + // + "[" + PREFIX_FACULTY + "FACULTY]\n" + // + "[" + PREFIX_MODULE + "MOODULE]\n" + + "[" + PREFIX_TAG + "TAG]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_TAG + "BestProf"; + + private final PersonHasTagPredicate tagPredicate; + + /** + * Creates a FilterTagCommand to filter for the specified {@code Tag} + */ + public FilterCommand(PersonHasTagPredicate tagPredicate) { + this.tagPredicate = tagPredicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(tagPredicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + +} diff --git a/src/main/java/staffconnect/logic/parser/FilterCommandParser.java b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java new file mode 100644 index 00000000000..f4b96db974b --- /dev/null +++ b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java @@ -0,0 +1,42 @@ +package staffconnect.logic.parser; + +import static java.util.Objects.requireNonNull; +import static staffconnect.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +// import static staffconnect.logic.parser.CliSyntax.PREFIX_FACULTY; // TODO: add parsing for faculty and module +// import static staffconnect.logic.parser.CliSyntax.PREFIX_MODULE; +import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; + + +import staffconnect.commons.exceptions.IllegalValueException; +import staffconnect.logic.commands.FilterCommand; +import staffconnect.logic.parser.exceptions.ParseException; +import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.tag.Tag; + +/** + * 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 { + requireNonNull(args); + ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_TAG); + + Tag tagName; + try { + tagName = ParserUtil.parseTag(argumentMultimap.getValue(PREFIX_TAG).orElse("")); + } catch (IllegalValueException ive) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + FilterCommand.MESSAGE_USAGE), ive); + } + + return new FilterCommand(new PersonHasTagPredicate(tagName)); + } + +} diff --git a/src/main/java/staffconnect/logic/parser/StaffConnectParser.java b/src/main/java/staffconnect/logic/parser/StaffConnectParser.java index 60281ff34a8..d8b9b53f96b 100644 --- a/src/main/java/staffconnect/logic/parser/StaffConnectParser.java +++ b/src/main/java/staffconnect/logic/parser/StaffConnectParser.java @@ -14,6 +14,7 @@ import staffconnect.logic.commands.DeleteCommand; import staffconnect.logic.commands.EditCommand; import staffconnect.logic.commands.ExitCommand; +import staffconnect.logic.commands.FilterCommand; import staffconnect.logic.commands.FindCommand; import staffconnect.logic.commands.HelpCommand; import staffconnect.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/staffconnect/model/person/PersonHasTagPredicate.java b/src/main/java/staffconnect/model/person/PersonHasTagPredicate.java new file mode 100644 index 00000000000..c75b8fbcf90 --- /dev/null +++ b/src/main/java/staffconnect/model/person/PersonHasTagPredicate.java @@ -0,0 +1,43 @@ +package staffconnect.model.person; + +import java.util.function.Predicate; + +import staffconnect.commons.util.ToStringBuilder; +import staffconnect.model.tag.Tag; + +/** + * Tests that a {@code Person}'s {@code Tag} matches any of the tag names given. + */ +public class PersonHasTagPredicate implements Predicate { + private final Tag tag; // TODO: change to multiple tags in later iterations + + public PersonHasTagPredicate(Tag tag) { + this.tag = tag; + } + + @Override + public boolean test(Person person) { + return person.getTags().stream() + .anyMatch(t -> t.tagName.equalsIgnoreCase(tag.tagName)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonHasTagPredicate)) { + return false; + } + + PersonHasTagPredicate otherPersonHasTagPredicate = (PersonHasTagPredicate) other; + return tag.equals(otherPersonHasTagPredicate.tag); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("tag name", tag).toString(); + } +} From 435dcbdf69793d0e37130979347d10f39db5efcc Mon Sep 17 00:00:00 2001 From: iynixil Date: Wed, 13 Mar 2024 13:41:01 +0800 Subject: [PATCH 2/4] Add equals() to FilterCommand --- .../logic/commands/FilterCommand.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/staffconnect/logic/commands/FilterCommand.java b/src/main/java/staffconnect/logic/commands/FilterCommand.java index 5f47d1170d3..9faac02fe10 100644 --- a/src/main/java/staffconnect/logic/commands/FilterCommand.java +++ b/src/main/java/staffconnect/logic/commands/FilterCommand.java @@ -6,6 +6,8 @@ // import static staffconnect.logic.parser.CliSyntax.PREFIX_FACULTY; // TODO: add filtering for faculty and module // import static staffconnect.logic.parser.CliSyntax.PREFIX_MODULE; import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; + +import staffconnect.commons.util.ToStringBuilder; import staffconnect.model.Model; import staffconnect.model.person.PersonHasTagPredicate; // TagContainsKeywordsPredicate @@ -44,4 +46,26 @@ public CommandResult execute(Model model) { 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 FilterCommand)) { + return false; + } + + FilterCommand otherFilterCommand = (FilterCommand) other; + return tagPredicate.equals(otherFilterCommand.tagPredicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("tagPredicate", tagPredicate) + .toString(); + } + } From 68f35d1cbf3fbccddc33d18e4455bf1c44db16c1 Mon Sep 17 00:00:00 2001 From: iynixil Date: Wed, 13 Mar 2024 16:11:29 +0800 Subject: [PATCH 3/4] Add tests for single tag filter and fix style --- .../logic/commands/FilterCommand.java | 3 +- .../logic/parser/FilterCommandParser.java | 1 - .../logic/commands/FilterCommandTest.java | 89 +++++++++++++++++++ .../logic/parser/FilterCommandParserTest.java | 53 +++++++++++ .../logic/parser/StaffConnectParserTest.java | 11 +++ .../person/PersonHasTagPredicateTest.java | 73 +++++++++++++++ 6 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 src/test/java/staffconnect/logic/commands/FilterCommandTest.java create mode 100644 src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java create mode 100644 src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java diff --git a/src/main/java/staffconnect/logic/commands/FilterCommand.java b/src/main/java/staffconnect/logic/commands/FilterCommand.java index 9faac02fe10..5a8b51eda3a 100644 --- a/src/main/java/staffconnect/logic/commands/FilterCommand.java +++ b/src/main/java/staffconnect/logic/commands/FilterCommand.java @@ -1,13 +1,12 @@ package staffconnect.logic.commands; import static java.util.Objects.requireNonNull; - -import staffconnect.logic.Messages; // import static staffconnect.logic.parser.CliSyntax.PREFIX_FACULTY; // TODO: add filtering for faculty and module // import static staffconnect.logic.parser.CliSyntax.PREFIX_MODULE; import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; import staffconnect.commons.util.ToStringBuilder; +import staffconnect.logic.Messages; import staffconnect.model.Model; import staffconnect.model.person.PersonHasTagPredicate; // TagContainsKeywordsPredicate diff --git a/src/main/java/staffconnect/logic/parser/FilterCommandParser.java b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java index f4b96db974b..cdaf2d5954a 100644 --- a/src/main/java/staffconnect/logic/parser/FilterCommandParser.java +++ b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java @@ -6,7 +6,6 @@ // import static staffconnect.logic.parser.CliSyntax.PREFIX_MODULE; import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; - import staffconnect.commons.exceptions.IllegalValueException; import staffconnect.logic.commands.FilterCommand; import staffconnect.logic.parser.exceptions.ParseException; diff --git a/src/test/java/staffconnect/logic/commands/FilterCommandTest.java b/src/test/java/staffconnect/logic/commands/FilterCommandTest.java new file mode 100644 index 00000000000..a3d04c1de0f --- /dev/null +++ b/src/test/java/staffconnect/logic/commands/FilterCommandTest.java @@ -0,0 +1,89 @@ +package staffconnect.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 staffconnect.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static staffconnect.logic.commands.CommandTestUtil.assertCommandSuccess; +import static staffconnect.testutil.TypicalPersons.ALICE; +import static staffconnect.testutil.TypicalPersons.BENSON; +import static staffconnect.testutil.TypicalPersons.DANIEL; +import static staffconnect.testutil.TypicalPersons.getTypicalStaffBook; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import staffconnect.model.Model; +import staffconnect.model.ModelManager; +import staffconnect.model.UserPrefs; +import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.tag.Tag; + +public class FilterCommandTest { + + private Model model = new ModelManager(getTypicalStaffBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalStaffBook(), new UserPrefs()); + + @Test + public void execute_personHasTag_noPersonFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); + PersonHasTagPredicate tagPredicate = prepareTagPredicate("hello"); + FilterCommand command = new FilterCommand(tagPredicate); + expectedModel.updateFilteredPersonList(tagPredicate); + assertCommandSuccess(command, model, expectedMessage, model); + assertEquals(Collections.emptyList(), model.getFilteredPersonList()); + } + + @Test + public void execute_personHasTag_multiplePersonsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); + PersonHasTagPredicate tagPredicate = prepareTagPredicate("friends"); + FilterCommand command = new FilterCommand(tagPredicate); + expectedModel.updateFilteredPersonList(tagPredicate); + assertCommandSuccess(command, model, expectedMessage, model); + assertEquals(Arrays.asList(ALICE, BENSON, DANIEL), model.getFilteredPersonList()); + } + + @Test + public void equals() { + PersonHasTagPredicate firstTagPredicate = new PersonHasTagPredicate(new Tag("friend")); + PersonHasTagPredicate secondTagPredicate = new PersonHasTagPredicate(new Tag("colleagues")); + + FilterCommand filterTagFirstCommand = new FilterCommand(firstTagPredicate); + FilterCommand filterTagSecondCommand = new FilterCommand(secondTagPredicate); + + // same object -> returns true + assertTrue(filterTagFirstCommand.equals(filterTagFirstCommand)); + + // same values -> returns true + FilterCommand filterTagFirstCommandCopy = new FilterCommand(firstTagPredicate); + assertTrue(filterTagFirstCommand.equals(filterTagFirstCommandCopy)); + + // different types -> returns false + assertFalse(filterTagFirstCommand.equals(1)); + + // null -> returns false + assertFalse(filterTagFirstCommand.equals(null)); + + // different tag -> returns false + assertFalse(filterTagFirstCommand.equals(filterTagSecondCommand)); + } + + @Test + public void toStringMethod() { + PersonHasTagPredicate tagPredicate = new PersonHasTagPredicate(new Tag("hello")); + FilterCommand filterCommand = new FilterCommand(tagPredicate); + String expected = FilterCommand.class.getCanonicalName() + "{tagPredicate=" + tagPredicate + "}"; + assertEquals(expected, filterCommand.toString()); + } + + /** + * Parses {@code userInput} into a {@code PersonHasTagPredicate}. + */ + private PersonHasTagPredicate prepareTagPredicate(String userInput) { + return new PersonHasTagPredicate(new Tag(userInput)); + } + +} diff --git a/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java b/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java new file mode 100644 index 00000000000..f62a0565ff0 --- /dev/null +++ b/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java @@ -0,0 +1,53 @@ +package staffconnect.logic.parser; + +import static staffconnect.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static staffconnect.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +import static staffconnect.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static staffconnect.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; +import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; +import static staffconnect.logic.parser.CommandParserTestUtil.assertParseFailure; +import static staffconnect.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import staffconnect.logic.commands.FilterCommand; +import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.tag.Tag; + +public class FilterCommandParserTest { + + private static final String TAG_EMPTY = " " + PREFIX_TAG; // should have a space before tag prefix + + private FilterCommandParser parser = new FilterCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidTagName_throwsParseException() { + // no whitespace before PREFIX_TAG + assertParseFailure(parser, PREFIX_TAG + VALID_TAG_FRIEND, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + // tagname is non-alphanumeric (contains '*') + assertParseFailure(parser, INVALID_TAG_DESC, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + + } + + @Test + public void parse_validTagName_success() { + // 1 leading and no trailing whitespaces + FilterCommand expectedFilterCommand = new FilterCommand(new PersonHasTagPredicate(new Tag(VALID_TAG_FRIEND))); + assertParseSuccess(parser, TAG_DESC_FRIEND, expectedFilterCommand); + + // 1 leading and multiple trailing whitespaces + assertParseSuccess(parser, TAG_DESC_FRIEND + " ", expectedFilterCommand); // 1 leading, 3 trailing + + // multiple leading and trailing whitespaces + assertParseSuccess(parser, " " + TAG_DESC_FRIEND + " ", expectedFilterCommand); // 2 leading, 1 trailing + assertParseSuccess(parser, " " + TAG_DESC_FRIEND + " ", expectedFilterCommand); // 5 leading, 3 trailing + } + +} diff --git a/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java b/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java index 7662e5b73ff..657444f98b5 100644 --- a/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java +++ b/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java @@ -19,12 +19,15 @@ import staffconnect.logic.commands.EditCommand; import staffconnect.logic.commands.EditCommand.EditPersonDescriptor; import staffconnect.logic.commands.ExitCommand; +import staffconnect.logic.commands.FilterCommand; import staffconnect.logic.commands.FindCommand; import staffconnect.logic.commands.HelpCommand; import staffconnect.logic.commands.ListCommand; import staffconnect.logic.parser.exceptions.ParseException; import staffconnect.model.person.NameContainsKeywordsPredicate; import staffconnect.model.person.Person; +import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.tag.Tag; import staffconnect.testutil.EditPersonDescriptorBuilder; import staffconnect.testutil.PersonBuilder; import staffconnect.testutil.PersonUtil; @@ -68,6 +71,14 @@ public void parseCommand_exit() throws Exception { assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); } + @Test + public void parseCommand_filter() throws Exception { + String tag = "hello"; + FilterCommand command = (FilterCommand) parser.parseCommand(FilterCommand.COMMAND_WORD + + " t/" + tag); + assertEquals(new FilterCommand(new PersonHasTagPredicate(new Tag(tag))), command); + } + @Test public void parseCommand_find() throws Exception { List keywords = Arrays.asList("foo", "bar", "baz"); diff --git a/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java b/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java new file mode 100644 index 00000000000..a99e7dcf0ca --- /dev/null +++ b/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java @@ -0,0 +1,73 @@ +package staffconnect.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 org.junit.jupiter.api.Test; + +import staffconnect.model.tag.Tag; +import staffconnect.testutil.PersonBuilder; + +public class PersonHasTagPredicateTest { + + @Test + public void equals() { + Tag firstPredicateTag = new Tag("first"); + Tag secondPredicateTag = new Tag("second"); + + PersonHasTagPredicate firstPredicate = new PersonHasTagPredicate(firstPredicateTag); + PersonHasTagPredicate secondPredicate = new PersonHasTagPredicate(secondPredicateTag); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + PersonHasTagPredicate firstPredicateCopy = new PersonHasTagPredicate(firstPredicateTag); + 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_personHasTag_returnsTrue() { + // predicate set to track "tester" tag + PersonHasTagPredicate predicate = new PersonHasTagPredicate(new Tag("tester")); + + // person only has tag "tester" + assertTrue(predicate.test(new PersonBuilder().withTags("tester").build())); + + // person has multiple tags and has "tester" + assertTrue(predicate.test(new PersonBuilder().withTags("tester", "tester2").build())); + + // case-insensitivity checks + assertTrue(predicate.test(new PersonBuilder().withTags("tesTER").build())); + assertTrue(predicate.test(new PersonBuilder().withTags("TESTER").build())); + assertTrue(predicate.test(new PersonBuilder().withTags("tEsTeR").build())); + } + + @Test + public void test_personDoesNotHaveTag_returnsFalse() { + // predicate set to track "tester" tag + PersonHasTagPredicate predicate = new PersonHasTagPredicate(new Tag("tester")); + + // person does not have tag "tester" + assertFalse(predicate.test(new PersonBuilder().withTags("tester2").build())); + } + + @Test + public void toStringMethod() { + Tag tag = new Tag("hello"); + PersonHasTagPredicate predicate = new PersonHasTagPredicate(tag); + + String expected = PersonHasTagPredicate.class.getCanonicalName() + "{tag name=" + tag + "}"; + assertEquals(expected, predicate.toString()); + } +} From 0d981b7e555c44c5fc3208bfe6765c9e09b773f1 Mon Sep 17 00:00:00 2001 From: iynixil Date: Wed, 13 Mar 2024 19:36:38 +0800 Subject: [PATCH 4/4] Update filter by tag to handle multiple tags --- .../logic/commands/FilterCommand.java | 6 +- .../logic/parser/FilterCommandParser.java | 19 +++++-- .../model/person/PersonHasTagPredicate.java | 43 -------------- .../model/person/PersonHasTagsPredicate.java | 54 ++++++++++++++++++ .../logic/commands/FilterCommandTest.java | 24 +++++--- .../logic/parser/FilterCommandParserTest.java | 50 +++++++++++++--- .../logic/parser/StaffConnectParserTest.java | 17 +++++- .../person/PersonHasTagPredicateTest.java | 57 +++++++++++++++---- 8 files changed, 188 insertions(+), 82 deletions(-) delete mode 100644 src/main/java/staffconnect/model/person/PersonHasTagPredicate.java create mode 100644 src/main/java/staffconnect/model/person/PersonHasTagsPredicate.java diff --git a/src/main/java/staffconnect/logic/commands/FilterCommand.java b/src/main/java/staffconnect/logic/commands/FilterCommand.java index 5a8b51eda3a..890b9356b4e 100644 --- a/src/main/java/staffconnect/logic/commands/FilterCommand.java +++ b/src/main/java/staffconnect/logic/commands/FilterCommand.java @@ -8,7 +8,7 @@ import staffconnect.commons.util.ToStringBuilder; import staffconnect.logic.Messages; import staffconnect.model.Model; -import staffconnect.model.person.PersonHasTagPredicate; // TagContainsKeywordsPredicate +import staffconnect.model.person.PersonHasTagsPredicate; // TagContainsKeywordsPredicate /** * Filters all persons in staff book whose module code or faculty shorthand or @@ -28,12 +28,12 @@ public class FilterCommand extends Command { + "Example: " + COMMAND_WORD + " " + PREFIX_TAG + "BestProf"; - private final PersonHasTagPredicate tagPredicate; + private final PersonHasTagsPredicate tagPredicate; /** * Creates a FilterTagCommand to filter for the specified {@code Tag} */ - public FilterCommand(PersonHasTagPredicate tagPredicate) { + public FilterCommand(PersonHasTagsPredicate tagPredicate) { this.tagPredicate = tagPredicate; } diff --git a/src/main/java/staffconnect/logic/parser/FilterCommandParser.java b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java index cdaf2d5954a..90f044324e9 100644 --- a/src/main/java/staffconnect/logic/parser/FilterCommandParser.java +++ b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java @@ -6,10 +6,12 @@ // import static staffconnect.logic.parser.CliSyntax.PREFIX_MODULE; import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Set; + import staffconnect.commons.exceptions.IllegalValueException; import staffconnect.logic.commands.FilterCommand; import staffconnect.logic.parser.exceptions.ParseException; -import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.person.PersonHasTagsPredicate; import staffconnect.model.tag.Tag; /** @@ -18,24 +20,29 @@ public class FilterCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the FilterCommand + * 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 { requireNonNull(args); - ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); - Tag tagName; + if (argMultimap.getAllValues(PREFIX_TAG).size() == 0) { // TODO: update for filter faculty/module + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + Set tags; try { - tagName = ParserUtil.parseTag(argumentMultimap.getValue(PREFIX_TAG).orElse("")); + tags = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); } catch (IllegalValueException ive) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE), ive); } - return new FilterCommand(new PersonHasTagPredicate(tagName)); + return new FilterCommand(new PersonHasTagsPredicate(tags)); } } diff --git a/src/main/java/staffconnect/model/person/PersonHasTagPredicate.java b/src/main/java/staffconnect/model/person/PersonHasTagPredicate.java deleted file mode 100644 index c75b8fbcf90..00000000000 --- a/src/main/java/staffconnect/model/person/PersonHasTagPredicate.java +++ /dev/null @@ -1,43 +0,0 @@ -package staffconnect.model.person; - -import java.util.function.Predicate; - -import staffconnect.commons.util.ToStringBuilder; -import staffconnect.model.tag.Tag; - -/** - * Tests that a {@code Person}'s {@code Tag} matches any of the tag names given. - */ -public class PersonHasTagPredicate implements Predicate { - private final Tag tag; // TODO: change to multiple tags in later iterations - - public PersonHasTagPredicate(Tag tag) { - this.tag = tag; - } - - @Override - public boolean test(Person person) { - return person.getTags().stream() - .anyMatch(t -> t.tagName.equalsIgnoreCase(tag.tagName)); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonHasTagPredicate)) { - return false; - } - - PersonHasTagPredicate otherPersonHasTagPredicate = (PersonHasTagPredicate) other; - return tag.equals(otherPersonHasTagPredicate.tag); - } - - @Override - public String toString() { - return new ToStringBuilder(this).add("tag name", tag).toString(); - } -} diff --git a/src/main/java/staffconnect/model/person/PersonHasTagsPredicate.java b/src/main/java/staffconnect/model/person/PersonHasTagsPredicate.java new file mode 100644 index 00000000000..464eff4057a --- /dev/null +++ b/src/main/java/staffconnect/model/person/PersonHasTagsPredicate.java @@ -0,0 +1,54 @@ +package staffconnect.model.person; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import staffconnect.commons.util.ToStringBuilder; +import staffconnect.model.tag.Tag; + +/** + * Tests that a {@code Person}'s {@code Tag} matches any of the tag names given. + */ +public class PersonHasTagsPredicate implements Predicate { + private final Set tags; // TODO: change to multiple ss in later iterations + + public PersonHasTagsPredicate(Set tags) { + this.tags = tags; + } + + @Override + public boolean test(Person person) { + // get list of person tags + List personTags = person.getTags().stream().map(t -> t.tagName.toLowerCase()) + .collect(Collectors.toList()); + // get stream of tags to filter from + Stream tagsToFilter = tags.stream().map(t -> t.tagName.toLowerCase()) + .collect(Collectors.toList()).stream(); + // check if the person DOES NOT contain any of the tags to filter from, if true + // then predicate is not satisfied + return !tagsToFilter.anyMatch(tag -> !personTags.contains(tag)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonHasTagsPredicate)) { + return false; + } + + PersonHasTagsPredicate otherPersonHasTagPredicate = (PersonHasTagsPredicate) other; + return tags.equals(otherPersonHasTagPredicate.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("tag name", tags).toString(); + } +} diff --git a/src/test/java/staffconnect/logic/commands/FilterCommandTest.java b/src/test/java/staffconnect/logic/commands/FilterCommandTest.java index a3d04c1de0f..cb6702eb4b9 100644 --- a/src/test/java/staffconnect/logic/commands/FilterCommandTest.java +++ b/src/test/java/staffconnect/logic/commands/FilterCommandTest.java @@ -12,13 +12,17 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import staffconnect.model.Model; import staffconnect.model.ModelManager; import staffconnect.model.UserPrefs; -import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.person.PersonHasTagsPredicate; import staffconnect.model.tag.Tag; public class FilterCommandTest { @@ -29,7 +33,7 @@ public class FilterCommandTest { @Test public void execute_personHasTag_noPersonFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); - PersonHasTagPredicate tagPredicate = prepareTagPredicate("hello"); + PersonHasTagsPredicate tagPredicate = prepareTagPredicate("hello"); FilterCommand command = new FilterCommand(tagPredicate); expectedModel.updateFilteredPersonList(tagPredicate); assertCommandSuccess(command, model, expectedMessage, model); @@ -39,7 +43,7 @@ public void execute_personHasTag_noPersonFound() { @Test public void execute_personHasTag_multiplePersonsFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); - PersonHasTagPredicate tagPredicate = prepareTagPredicate("friends"); + PersonHasTagsPredicate tagPredicate = prepareTagPredicate("friends"); FilterCommand command = new FilterCommand(tagPredicate); expectedModel.updateFilteredPersonList(tagPredicate); assertCommandSuccess(command, model, expectedMessage, model); @@ -48,8 +52,8 @@ public void execute_personHasTag_multiplePersonsFound() { @Test public void equals() { - PersonHasTagPredicate firstTagPredicate = new PersonHasTagPredicate(new Tag("friend")); - PersonHasTagPredicate secondTagPredicate = new PersonHasTagPredicate(new Tag("colleagues")); + PersonHasTagsPredicate firstTagPredicate = prepareTagPredicate("friend"); + PersonHasTagsPredicate secondTagPredicate = prepareTagPredicate("colleagues"); FilterCommand filterTagFirstCommand = new FilterCommand(firstTagPredicate); FilterCommand filterTagSecondCommand = new FilterCommand(secondTagPredicate); @@ -73,7 +77,7 @@ public void equals() { @Test public void toStringMethod() { - PersonHasTagPredicate tagPredicate = new PersonHasTagPredicate(new Tag("hello")); + PersonHasTagsPredicate tagPredicate = prepareTagPredicate("hello"); FilterCommand filterCommand = new FilterCommand(tagPredicate); String expected = FilterCommand.class.getCanonicalName() + "{tagPredicate=" + tagPredicate + "}"; assertEquals(expected, filterCommand.toString()); @@ -82,8 +86,12 @@ public void toStringMethod() { /** * Parses {@code userInput} into a {@code PersonHasTagPredicate}. */ - private PersonHasTagPredicate prepareTagPredicate(String userInput) { - return new PersonHasTagPredicate(new Tag(userInput)); + private PersonHasTagsPredicate prepareTagPredicate(String userInput) { + List tagList = Stream.of(userInput.split(" ")).map(str -> new Tag(str)).collect(Collectors.toList()); + for (String separatedTag : userInput.split(" ")) { + tagList.add(new Tag(separatedTag)); + } + return new PersonHasTagsPredicate(new HashSet(tagList)); } } diff --git a/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java b/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java index f62a0565ff0..79b03465a1d 100644 --- a/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java +++ b/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java @@ -3,15 +3,21 @@ import static staffconnect.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static staffconnect.logic.commands.CommandTestUtil.INVALID_TAG_DESC; import static staffconnect.logic.commands.CommandTestUtil.TAG_DESC_FRIEND; +import static staffconnect.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; import static staffconnect.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; +import static staffconnect.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static staffconnect.logic.parser.CliSyntax.PREFIX_TAG; import static staffconnect.logic.parser.CommandParserTestUtil.assertParseFailure; import static staffconnect.logic.parser.CommandParserTestUtil.assertParseSuccess; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.junit.jupiter.api.Test; import staffconnect.logic.commands.FilterCommand; -import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.person.PersonHasTagsPredicate; import staffconnect.model.tag.Tag; public class FilterCommandParserTest { @@ -27,9 +33,6 @@ public void parse_emptyArg_throwsParseException() { @Test public void parse_invalidTagName_throwsParseException() { - // no whitespace before PREFIX_TAG - assertParseFailure(parser, PREFIX_TAG + VALID_TAG_FRIEND, - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); // tagname is non-alphanumeric (contains '*') assertParseFailure(parser, INVALID_TAG_DESC, String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); @@ -38,16 +41,45 @@ public void parse_invalidTagName_throwsParseException() { @Test public void parse_validTagName_success() { - // 1 leading and no trailing whitespaces - FilterCommand expectedFilterCommand = new FilterCommand(new PersonHasTagPredicate(new Tag(VALID_TAG_FRIEND))); + Set singleTag = new HashSet(Arrays.asList(new Tag(VALID_TAG_FRIEND))); + + // single tag + // 1 leading and no trailing whitespaces (TAG_DESC_xxx always has 1 leading) + FilterCommand expectedFilterCommand = new FilterCommand(new PersonHasTagsPredicate(singleTag)); assertParseSuccess(parser, TAG_DESC_FRIEND, expectedFilterCommand); // 1 leading and multiple trailing whitespaces - assertParseSuccess(parser, TAG_DESC_FRIEND + " ", expectedFilterCommand); // 1 leading, 3 trailing + // 1 leading, 3 trailing + assertParseSuccess(parser, TAG_DESC_FRIEND + " ", expectedFilterCommand); + + // multiple leading and trailing whitespaces + // 2 leading, 1 trailing + assertParseSuccess(parser, " " + TAG_DESC_FRIEND + " ", expectedFilterCommand); + // 5 leading, 3 trailing + assertParseSuccess(parser, " " + TAG_DESC_FRIEND + " ", expectedFilterCommand); + + // multiple tags + Set multipleTags = new HashSet(Arrays.asList(new Tag(VALID_TAG_FRIEND), new Tag(VALID_TAG_HUSBAND))); + expectedFilterCommand = new FilterCommand(new PersonHasTagsPredicate(multipleTags)); + + // 1 leading and no trailing whitespaces + assertParseSuccess(parser, TAG_DESC_FRIEND + TAG_DESC_HUSBAND, expectedFilterCommand); + + // 1 leading and multiple trailing whitespaces + // 1 leading, 3 trailing + assertParseSuccess(parser, TAG_DESC_FRIEND + TAG_DESC_HUSBAND + " ", expectedFilterCommand); // multiple leading and trailing whitespaces - assertParseSuccess(parser, " " + TAG_DESC_FRIEND + " ", expectedFilterCommand); // 2 leading, 1 trailing - assertParseSuccess(parser, " " + TAG_DESC_FRIEND + " ", expectedFilterCommand); // 5 leading, 3 trailing + // 2 leading, 1 trailing + assertParseSuccess(parser, " " + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + " ", expectedFilterCommand); + // 5 leading, 3 trailing + assertParseSuccess(parser, " " + TAG_DESC_FRIEND + TAG_DESC_HUSBAND + " ", expectedFilterCommand); + + // whitespaces in middle + // 1 leading, 1 middle, 3 trailing + assertParseSuccess(parser, TAG_DESC_FRIEND + " " + TAG_DESC_HUSBAND + " ", expectedFilterCommand); + // 1 leading, 3 middle, 1 trailing + assertParseSuccess(parser, TAG_DESC_FRIEND + " " + TAG_DESC_HUSBAND + " ", expectedFilterCommand); } } diff --git a/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java b/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java index 657444f98b5..aa7eded4102 100644 --- a/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java +++ b/src/test/java/staffconnect/logic/parser/StaffConnectParserTest.java @@ -8,7 +8,9 @@ import static staffconnect.testutil.TypicalIndexes.INDEX_FIRST_PERSON; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -26,7 +28,7 @@ import staffconnect.logic.parser.exceptions.ParseException; import staffconnect.model.person.NameContainsKeywordsPredicate; import staffconnect.model.person.Person; -import staffconnect.model.person.PersonHasTagPredicate; +import staffconnect.model.person.PersonHasTagsPredicate; import staffconnect.model.tag.Tag; import staffconnect.testutil.EditPersonDescriptorBuilder; import staffconnect.testutil.PersonBuilder; @@ -73,10 +75,19 @@ public void parseCommand_exit() throws Exception { @Test public void parseCommand_filter() throws Exception { + // single tag String tag = "hello"; - FilterCommand command = (FilterCommand) parser.parseCommand(FilterCommand.COMMAND_WORD + Set singleTag = new HashSet(Arrays.asList(new Tag(tag))); + FilterCommand singleTagCommand = (FilterCommand) parser.parseCommand(FilterCommand.COMMAND_WORD + " t/" + tag); - assertEquals(new FilterCommand(new PersonHasTagPredicate(new Tag(tag))), command); + assertEquals(new FilterCommand(new PersonHasTagsPredicate(singleTag)), singleTagCommand); + + // multiple tags + String tag2 = "hello2"; + Set multipleTags = new HashSet(Arrays.asList(new Tag(tag), new Tag(tag2))); + FilterCommand multipleTagsCommand = (FilterCommand) parser.parseCommand(FilterCommand.COMMAND_WORD + + " t/" + tag + " t/" + tag2); + assertEquals(new FilterCommand(new PersonHasTagsPredicate(multipleTags)), multipleTagsCommand); } @Test diff --git a/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java b/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java index a99e7dcf0ca..d565a877565 100644 --- a/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java +++ b/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java @@ -4,6 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.junit.jupiter.api.Test; import staffconnect.model.tag.Tag; @@ -13,17 +17,17 @@ public class PersonHasTagPredicateTest { @Test public void equals() { - Tag firstPredicateTag = new Tag("first"); - Tag secondPredicateTag = new Tag("second"); + Set firstPredicateTag = new HashSet(Arrays.asList(new Tag("first"))); + Set secondPredicateTag = new HashSet(Arrays.asList(new Tag("second"))); - PersonHasTagPredicate firstPredicate = new PersonHasTagPredicate(firstPredicateTag); - PersonHasTagPredicate secondPredicate = new PersonHasTagPredicate(secondPredicateTag); + PersonHasTagsPredicate firstPredicate = new PersonHasTagsPredicate(firstPredicateTag); + PersonHasTagsPredicate secondPredicate = new PersonHasTagsPredicate(secondPredicateTag); // same object -> returns true assertTrue(firstPredicate.equals(firstPredicate)); // same values -> returns true - PersonHasTagPredicate firstPredicateCopy = new PersonHasTagPredicate(firstPredicateTag); + PersonHasTagsPredicate firstPredicateCopy = new PersonHasTagsPredicate(firstPredicateTag); assertTrue(firstPredicate.equals(firstPredicateCopy)); // different types -> returns false @@ -39,7 +43,8 @@ public void equals() { @Test public void test_personHasTag_returnsTrue() { // predicate set to track "tester" tag - PersonHasTagPredicate predicate = new PersonHasTagPredicate(new Tag("tester")); + Set tag = new HashSet(Arrays.asList(new Tag("tester"))); + PersonHasTagsPredicate predicate = new PersonHasTagsPredicate(tag); // person only has tag "tester" assertTrue(predicate.test(new PersonBuilder().withTags("tester").build())); @@ -53,21 +58,53 @@ public void test_personHasTag_returnsTrue() { assertTrue(predicate.test(new PersonBuilder().withTags("tEsTeR").build())); } + @Test + public void test_personHasMultipleTags_returnsTrue() { + // predicate set to track "tester", "tester2" tags + Set multipleTags = new HashSet(Arrays.asList(new Tag("tester"), new Tag("tester2"))); + PersonHasTagsPredicate predicate = new PersonHasTagsPredicate(multipleTags); + + // person has multiple tags and has "tester", "tester2" + assertTrue(predicate.test(new PersonBuilder().withTags("tester", "tester2").build())); + + // case-insensitivity checks + assertTrue(predicate.test(new PersonBuilder().withTags("tesTER", "tesTER2").build())); + assertTrue(predicate.test(new PersonBuilder().withTags("TESTER", "tester2").build())); + assertTrue(predicate.test(new PersonBuilder().withTags("tEsTeR", "TeStEr2").build())); + } + @Test public void test_personDoesNotHaveTag_returnsFalse() { // predicate set to track "tester" tag - PersonHasTagPredicate predicate = new PersonHasTagPredicate(new Tag("tester")); + Set tag = new HashSet(Arrays.asList(new Tag("tester"))); + PersonHasTagsPredicate predicate = new PersonHasTagsPredicate(tag); // person does not have tag "tester" assertFalse(predicate.test(new PersonBuilder().withTags("tester2").build())); } + @Test + public void test_personDoesNotHaveMultipleTags_returnsTrue() { + // predicate set to track "tester", "tester2", "tester3" tags + Set multipleTags = new HashSet( + Arrays.asList(new Tag("tester"), new Tag("tester2"), new Tag("tester3"))); + PersonHasTagsPredicate predicate = new PersonHasTagsPredicate(multipleTags); + + // person only has 1 tag + assertFalse(predicate.test(new PersonBuilder().withTags("tester").build())); + + // case-insensitivity checks + assertFalse(predicate.test(new PersonBuilder().withTags("tesTER", "tesTER2").build())); + assertFalse(predicate.test(new PersonBuilder().withTags("TESTER", "tester2").build())); + assertFalse(predicate.test(new PersonBuilder().withTags("tEsTeR", "TeStEr2").build())); + } + @Test public void toStringMethod() { - Tag tag = new Tag("hello"); - PersonHasTagPredicate predicate = new PersonHasTagPredicate(tag); + Set tag = new HashSet(Arrays.asList(new Tag("hello"))); + PersonHasTagsPredicate predicate = new PersonHasTagsPredicate(tag); - String expected = PersonHasTagPredicate.class.getCanonicalName() + "{tag name=" + tag + "}"; + String expected = PersonHasTagsPredicate.class.getCanonicalName() + "{tag name=" + tag + "}"; assertEquals(expected, predicate.toString()); } }