Skip to content

Commit

Permalink
Merge pull request #72 from itsme-zeix/add-multiple-filtering
Browse files Browse the repository at this point in the history
Implement Multi-Flag Filtering Feature
  • Loading branch information
itsme-zeix authored Oct 16, 2024
2 parents 124717d + d7208bc commit ed277a1
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 67 deletions.
40 changes: 21 additions & 19 deletions docs/UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,21 +203,26 @@ edit 69 n/ TAN LESHEW p/ 77337733 e/ [email protected] a/ COM3 j/ doctor i/ 100000000
This feature allows users to search for customers by specific details such as name, address, email, phone number, job title, or remarks.

**How to Use It:**
To perform a search, use the `filter` command followed by a flag (indicating the field you want to search) and the corresponding search term. Searches are **case-insensitive** and use **substring-matching**. See [substring-matching](#substring-matching) to learn more.
To perform a search, use the `filter` command followed by one or more flags (indicating the fields to search) and the corresponding search terms.
Searches are **case-insensitive** and use [**substring-matching**](#substring-matching).

- **Command Format:**
```
filter <FLAG>/ <SEARCH TERM>
```
- **Examples:**
- Filter customers by name:
```
filter n/ TAN LESHEW
filter <FLAG>/ <SEARCH TERM> [<ADDITIONAL_FLAGS>/ <SEARCH TERM>]
```
- **Examples:**
- Filter customers by name:
```
filter n/ TAN LESHEW
```
- Filter customers by job:
```
e.g. filter j/ doctor
```
```
filter j/ doctor
```
- Filter customers by name, job and remark:
```
filter n/ Gordon Moore j/ doctor r/ award winner
```

#### Parameters
| Parameter | Expected Format | Explanation |
Expand All @@ -235,30 +240,27 @@ filter <FLAG>/ <SEARCH TERM>

#### Substring Matching:
- Substring matching is used for searches, meaning that the search term must match a part of the field in the same order as it appears in the customer record.
- For instance, if a customer’s name is "John Lee", the search term "John", "Lee", or "John Lee" will match, but "Lee John" will not.

- For instance, if a customer’s name is `Gordon Moore`, the search term `Gordon`, `Moore`, or `Gordon Moore` will match, but `Moore Gordon` will not.

#### What to Expect
- **If Successful:**
- Message: "Here are all the customers that match your search: (List of customers)."
- **If Unsuccessful (No Matches Found):**
- Message: "No customers match your search criteria."
- **If Multiple Filters Are Used:**
- Message: "Using multiple filters is not supported yet. Please use only one filter."
- **If There is an Error:**
- No Valid Flags Used:
- Message:

"filter: Searches for all customers whose specified field contains the given
substring (case-insensitive) and displays the results in a numbered list.
"filter: Searches for all customers whose specified field contains the given substring (case-insensitive) and displays the results in a numbered list.

Parameters: `<FLAG>/ <SEARCH TERM>`

Flags: `n/` (name), `p/` (phone), `e/` (email), `a/` (address), `j/` (job), `r/` (remarks)

Example: `filter n/ Alice`

This will find all customers whose names contain 'Alice'."
Example: `filter n/ Alice p/ 91112222`

This will find all customers whose names contain 'Alice' and has phone number '91112222'."

- If Search Term Fails to Meet Requirement (i.e. Phone Number longer than 8 digits):
- The system will display usage hints specific to the first invalid search term.

Expand Down
7 changes: 2 additions & 5 deletions src/main/java/seedu/address/logic/commands/FilterCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@ public class FilterCommand extends Command {
+ "contains the given substring (case-insensitive) and displays the results in a numbered list.\n"
+ "Parameters: <FLAG>/ <SEARCH TERM>\n"
+ "Flags: n/ (name), p/ (phone), e/ (email), a/ (address), j/ (job), r/ (remarks)\n"
+ "Example: " + COMMAND_WORD + " n/ Alice\n"
+ "This will find all customers whose names contain 'Alice'.";

public static final String MULTIPLE_FILTERS_NOT_IMPLEMENTED = "Using multiple filters is not supported yet. "
+ "Please use only one filter.";
+ "Example: " + COMMAND_WORD + " n/ Alice" + " p/ 91112222\n"
+ "This will find all customers whose names contain 'Alice' and whose phone number is '91112222'.";

private final Predicate<Person> predicate;

Expand Down
82 changes: 64 additions & 18 deletions src/main/java/seedu/address/logic/parser/FilterCommandParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@
import static seedu.address.logic.parser.CliSyntax.PREFIX_REMARK;
import static seedu.address.logic.parser.CliSyntax.PREFIX_TIER;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

import seedu.address.logic.commands.FilterCommand;
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.Person;
import seedu.address.model.person.predicates.AddressContainsSubstringPredicate;
import seedu.address.model.person.predicates.CombinedPredicate;
import seedu.address.model.person.predicates.EmailContainsSubstringPredicate;
import seedu.address.model.person.predicates.JobContainsSubstringPredicate;
import seedu.address.model.person.predicates.NameContainsSubstringPredicate;
Expand All @@ -41,50 +47,90 @@ public FilterCommand parse(String args) throws ParseException {
argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS,
PREFIX_INCOME, PREFIX_JOB, PREFIX_REMARK, PREFIX_TIER);

// Filtering by multiple fields/flags has not been implemented yet
// Throw an error if no filters are used
long numberOfFiltersUsed = countPrefixesUsed(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL,
PREFIX_ADDRESS, PREFIX_JOB, PREFIX_INCOME, PREFIX_REMARK, PREFIX_TIER);
if (numberOfFiltersUsed > 1) {
throw new ParseException(FilterCommand.MULTIPLE_FILTERS_NOT_IMPLEMENTED);

if (numberOfFiltersUsed == 0) {
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
}

// Handle flags and search terms
List<Predicate<Person>> predicates = collectPredicates(argMultimap);
Predicate<Person> combinedPredicate = combinePredicates(predicates);

return new FilterCommand(combinedPredicate);
}

/**
* Counts the number of prefixes used in the given {@code ArgumentMultimap}.
*
* @param argMultimap The ArgumentMultimap containing the parsed arguments.
* @param prefixes The prefixes to check in the argument map.
* @return The number of prefixes that are present in the argument map.
*/
private long countPrefixesUsed(ArgumentMultimap argMultimap, Prefix... prefixes) {
return Arrays.stream(prefixes)
.filter(prefix -> argMultimap.getValue(prefix).isPresent())
.count();
}

/**
* Collects the list of predicates based on the provided argument map.
*
* @param argMultimap The argument multimap containing the parsed arguments.
* @return A list of predicates corresponding to the filters provided.
* @throws ParseException if there are any parsing issues.
*/
private List<Predicate<Person>> collectPredicates(ArgumentMultimap argMultimap) throws ParseException {
// Collect individual predicates to be combined with AND operator later.
// This "collection then combine" approach enhances testability by avoiding the use of anonymous
// lambda predicates which ensures that the combined predicate can be easily tested and debugged.

List<Predicate<Person>> predicates = new ArrayList<>();

if (argMultimap.getValue(PREFIX_NAME).isPresent()) {
String substring = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()).fullName;
return new FilterCommand(new NameContainsSubstringPredicate(substring));
predicates.add(new NameContainsSubstringPredicate(substring));
}
if (argMultimap.getValue(PREFIX_PHONE).isPresent()) {
String substring = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()).value;
return new FilterCommand(new PhoneContainsSubstringPredicate(substring));
predicates.add(new PhoneContainsSubstringPredicate(substring));
}
if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) {
String substring = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()).value;
return new FilterCommand(new EmailContainsSubstringPredicate(substring));
predicates.add(new EmailContainsSubstringPredicate(substring));
}
if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) {
String substring = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()).value;
return new FilterCommand(new AddressContainsSubstringPredicate(substring));
predicates.add(new AddressContainsSubstringPredicate(substring));
}
if (argMultimap.getValue(PREFIX_JOB).isPresent()) {
String substring = ParserUtil.parseJob(argMultimap.getValue(PREFIX_JOB).get()).value;
return new FilterCommand(new JobContainsSubstringPredicate(substring));
predicates.add(new JobContainsSubstringPredicate(substring));
}
if (argMultimap.getValue(PREFIX_REMARK).isPresent()) {
String substring = ParserUtil.parseRemark(argMultimap.getValue(PREFIX_REMARK).get()).value;
return new FilterCommand(new RemarkContainsSubstringPredicate(substring));
predicates.add(new RemarkContainsSubstringPredicate(substring));
}
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));

return predicates;
}

/**
* Counts the number of prefixes used in the given {@code ArgumentMultimap}.
* Combines the given list of predicates into a single predicate using the AND operator.
* The combined predicate returns {@code true} only if all predicates return {@code true}.
*
* @param argMultimap The ArgumentMultimap containing the parsed arguments.
* @param prefixes The prefixes to check in the argument map.
* @return The number of prefixes that are present in the argument map.
* @param predicates The list of predicates to combine. Must not be null or empty.
* @return A combined predicate that represents the logical AND of all provided predicates.
* @throws IllegalArgumentException if the list of predicates is empty.
*/
private long countPrefixesUsed(ArgumentMultimap argMultimap, Prefix... prefixes) {
return Arrays.stream(prefixes)
.filter(prefix -> argMultimap.getValue(prefix).isPresent())
.count();
private Predicate<Person> combinePredicates(List<Predicate<Person>> predicates) {
Objects.requireNonNull(predicates, "Predicates list must not be null");
if (predicates.isEmpty()) {
throw new IllegalArgumentException("Predicates list must not be empty");
}

return new CombinedPredicate(predicates);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package seedu.address.model.person.predicates;

import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

import seedu.address.model.person.Person;

/**
* Represents a predicate that combines multiple other predicates using the AND operator.
* This class allows you to test multiple conditions on a {@link Person} object in a way that
* ensures all provided predicates must evaluate to true for the overall predicate to return true.
*
* <p>This is particularly useful for filtering when you need to apply multiple conditions
* simultaneously, such as filtering by name, phone number, email, etc.</p>
*/
public class CombinedPredicate implements Predicate<Person> {
private final List<Predicate<Person>> predicates;

/**
* Constructs a {@code CombinedPredicate} with a list of predicates.
* Each predicate in the list must evaluate to {@code true} for this combined predicate
* to return {@code true} when tested.
*
* @param predicates the list of predicates to combine
*/
public CombinedPredicate(List<Predicate<Person>> predicates) {
this.predicates = Objects.requireNonNull(predicates, "Predicates list must not be null");
}

/**
* Tests the given {@code Person} object against all the combined predicates.
* This method returns {@code true} if all predicates return {@code true} for the given person.
*
* @param person the {@code Person} to be tested
* @return {@code true} if all predicates return {@code true}; {@code false} otherwise
*/
@Override
public boolean test(Person person) {
for (Predicate<Person> predicate : predicates) {
if (!predicate.test(person)) {
return false;
}
}
return true;
}

/**
* Checks whether this combined predicate is equal to another object.
* Two {@code CombinedPredicate} objects are considered equal if their underlying
* list of predicates are equal. This method is used for ease of testing without dealing with lambdas.
*
* @param other the object to compare to
* @return {@code true} if this combined predicate is equal to the other object; {@code false} otherwise
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}

if (!(other instanceof CombinedPredicate)) {
return false;
}

CombinedPredicate that = (CombinedPredicate) other;
return this.predicates.equals(that.predicates);
}

// This method is used for ease of testing without dealing with lambdas.
@Override
public int hashCode() {
return predicates.hashCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import static seedu.address.testutil.Assert.assertThrows;
import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import org.junit.jupiter.api.Test;

import seedu.address.logic.commands.AddCommand;
Expand All @@ -23,11 +27,13 @@
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.Person;
import seedu.address.model.person.Remark;
import seedu.address.model.person.predicates.CombinedPredicate;
import seedu.address.model.person.predicates.NameContainsSubstringPredicate;
import seedu.address.testutil.EditPersonDescriptorBuilder;
import seedu.address.testutil.PersonBuilder;
import seedu.address.testutil.PersonUtil;


public class AddressBookParserTest {

private final AddressBookParser parser = new AddressBookParser();
Expand Down Expand Up @@ -70,12 +76,18 @@ public void parseCommand_exit() throws Exception {
@Test
public void parseCommand_filter() throws Exception {
String substring = "foo";
System.out.println(FilterCommand.COMMAND_WORD + " "
+ CliSyntax.PREFIX_NAME.getPrefix() + " " + substring);
FilterCommand command = (FilterCommand) parser.parseCommand(
FilterCommand.COMMAND_WORD + " "
+ CliSyntax.PREFIX_NAME.getPrefix() + " " + substring);
assertEquals(new FilterCommand(new NameContainsSubstringPredicate(substring)), command);

// Build expected command
List<Predicate<Person>> expectedPredicates = new ArrayList<>();
expectedPredicates.add(new NameContainsSubstringPredicate(substring));
CombinedPredicate combinedPredicate = new CombinedPredicate(expectedPredicates);
FilterCommand expectedCommand = new FilterCommand(combinedPredicate);

// Build actual command
FilterCommand actualCommand = (FilterCommand) parser.parseCommand(FilterCommand.COMMAND_WORD
+ " " + CliSyntax.PREFIX_NAME.getPrefix() + " " + substring);

assertEquals(expectedCommand, actualCommand);
}

@Test
Expand Down
Loading

0 comments on commit ed277a1

Please sign in to comment.