Skip to content

Commit

Permalink
Merge pull request #67 from iynixil/49-filter-contacts
Browse files Browse the repository at this point in the history
Add filter functionality for tags
  • Loading branch information
whitesnowx authored Mar 14, 2024
2 parents 618b8b8 + 0d981b7 commit 6874f37
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 0 deletions.
70 changes: 70 additions & 0 deletions src/main/java/staffconnect/logic/commands/FilterCommand.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
48 changes: 48 additions & 0 deletions src/main/java/staffconnect/logic/parser/FilterCommandParser.java
Original file line number Diff line number Diff line change
@@ -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<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 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<Tag> 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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Person> {
private final Set<Tag> tags; // TODO: change to multiple ss in later iterations

public PersonHasTagsPredicate(Set<Tag> tags) {
this.tags = tags;
}

@Override
public boolean test(Person person) {
// get list of person tags
List<String> personTags = person.getTags().stream().map(t -> t.tagName.toLowerCase())
.collect(Collectors.toList());
// get stream of tags to filter from
Stream<String> 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();
}
}
97 changes: 97 additions & 0 deletions src/test/java/staffconnect/logic/commands/FilterCommandTest.java
Original file line number Diff line number Diff line change
@@ -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<Tag> 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<Tag>(tagList));
}

}
Original file line number Diff line number Diff line change
@@ -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<Tag> singleTag = new HashSet<Tag>(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<Tag> multipleTags = new HashSet<Tag>(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);
}

}
Loading

0 comments on commit 6874f37

Please sign in to comment.