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..890b9356b4e --- /dev/null +++ b/src/main/java/staffconnect/logic/commands/FilterCommand.java @@ -0,0 +1,70 @@ +package staffconnect.logic.commands; + +import static java.util.Objects.requireNonNull; +// 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.PersonHasTagsPredicate; // 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 PersonHasTagsPredicate tagPredicate; + + /** + * Creates a FilterTagCommand to filter for the specified {@code Tag} + */ + public FilterCommand(PersonHasTagsPredicate 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())); + } + + @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(); + } + +} 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..90f044324e9 --- /dev/null +++ b/src/main/java/staffconnect/logic/parser/FilterCommandParser.java @@ -0,0 +1,48 @@ +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 java.util.Set; + +import staffconnect.commons.exceptions.IllegalValueException; +import staffconnect.logic.commands.FilterCommand; +import staffconnect.logic.parser.exceptions.ParseException; +import staffconnect.model.person.PersonHasTagsPredicate; +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 argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_TAG); + + 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 { + 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 PersonHasTagsPredicate(tags)); + } + +} 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/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 new file mode 100644 index 00000000000..cb6702eb4b9 --- /dev/null +++ b/src/test/java/staffconnect/logic/commands/FilterCommandTest.java @@ -0,0 +1,97 @@ +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 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.PersonHasTagsPredicate; +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); + PersonHasTagsPredicate 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); + PersonHasTagsPredicate 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() { + PersonHasTagsPredicate firstTagPredicate = prepareTagPredicate("friend"); + PersonHasTagsPredicate secondTagPredicate = prepareTagPredicate("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() { + PersonHasTagsPredicate tagPredicate = prepareTagPredicate("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 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 new file mode 100644 index 00000000000..79b03465a1d --- /dev/null +++ b/src/test/java/staffconnect/logic/parser/FilterCommandParserTest.java @@ -0,0 +1,85 @@ +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.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.PersonHasTagsPredicate; +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() { + // 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() { + 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 + // 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 + // 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 7662e5b73ff..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; @@ -19,12 +21,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.PersonHasTagsPredicate; +import staffconnect.model.tag.Tag; import staffconnect.testutil.EditPersonDescriptorBuilder; import staffconnect.testutil.PersonBuilder; import staffconnect.testutil.PersonUtil; @@ -68,6 +73,23 @@ public void parseCommand_exit() throws Exception { assertTrue(parser.parseCommand(ExitCommand.COMMAND_WORD + " 3") instanceof ExitCommand); } + @Test + public void parseCommand_filter() throws Exception { + // single tag + String tag = "hello"; + Set singleTag = new HashSet(Arrays.asList(new Tag(tag))); + FilterCommand singleTagCommand = (FilterCommand) parser.parseCommand(FilterCommand.COMMAND_WORD + + " t/" + tag); + 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 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..d565a877565 --- /dev/null +++ b/src/test/java/staffconnect/model/person/PersonHasTagPredicateTest.java @@ -0,0 +1,110 @@ +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 java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import staffconnect.model.tag.Tag; +import staffconnect.testutil.PersonBuilder; + +public class PersonHasTagPredicateTest { + + @Test + public void equals() { + Set firstPredicateTag = new HashSet(Arrays.asList(new Tag("first"))); + Set secondPredicateTag = new HashSet(Arrays.asList(new Tag("second"))); + + PersonHasTagsPredicate firstPredicate = new PersonHasTagsPredicate(firstPredicateTag); + PersonHasTagsPredicate secondPredicate = new PersonHasTagsPredicate(secondPredicateTag); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + PersonHasTagsPredicate firstPredicateCopy = new PersonHasTagsPredicate(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 + 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())); + + // 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_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 + 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() { + Set tag = new HashSet(Arrays.asList(new Tag("hello"))); + PersonHasTagsPredicate predicate = new PersonHasTagsPredicate(tag); + + String expected = PersonHasTagsPredicate.class.getCanonicalName() + "{tag name=" + tag + "}"; + assertEquals(expected, predicate.toString()); + } +}