From 0d981b7e555c44c5fc3208bfe6765c9e09b773f1 Mon Sep 17 00:00:00 2001 From: iynixil Date: Wed, 13 Mar 2024 19:36:38 +0800 Subject: [PATCH] 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()); } }