Skip to content

Commit

Permalink
Merge pull request #95 from itsme-zeix/add-filtering-by-income
Browse files Browse the repository at this point in the history
Implement filtering by income
  • Loading branch information
itsme-zeix authored Oct 20, 2024
2 parents 3ee1011 + a7cf4e8 commit 11d4661
Show file tree
Hide file tree
Showing 10 changed files with 480 additions and 61 deletions.
26 changes: 20 additions & 6 deletions docs/UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,9 @@ This feature allows users to update the details of an existing customer in the d
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 one or more flags (indicating the fields to search) and the corresponding search terms.
Searches are **case-insensitive** and use [**substring-matching**](#substring-matching), **except for Tier**, which must start with the specified substring.
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), **except for [Tier](#filtering-by-tier) and [Income](#filtering-by-income)**, which have their own specific matching criteria detailed below.

- **Command Format:**
```
Expand All @@ -288,10 +289,10 @@ To perform a search, use the `filter` command followed by one or more flags (ind

#### Parameters

| Parameter | Expected Format | Explanation |
|-------------|--------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| FLAG | Refer to the list of supported flags detailed below. | Identifies the field to search (e.g., `n/` for name, `j/` for job). | |
| SEARCH TERM | Refer to the syntax constraints in the [parameter subsection of the `add` command](#add-command-parameters). | The value to search for in the specified field (e.g., "doctor" for job, "TAN LESHEW" for name). |
| Parameter | Expected Format | Explanation |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| FLAG | Refer to the list of supported flags detailed below. | Identifies the field to search. <br/><br/> e.g., `n/` for name, `j/` for job. | |
| SEARCH TERM | Follows the syntax for [each field's expected input](#add-command-parameters). <br/><br/>**Income** requires a numeric value with a comparison operator (`=`, `>`, `<`), while **Tier** allows for partial (prefix) matching. Other fields follow substring matching. | The value to search for in the specified field. <br/><br/> e.g., `doctor` for job, `>5000` for income). |

#### Supported flags:
- `n/` for Name
Expand All @@ -306,6 +307,19 @@ To perform a search, use the `filter` command followed by one or more flags (ind
- 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 `Gordon Moore`, the search term `Gordon`, `Moore`, or `Gordon Moore` will match, but `Moore Gordon` will not.

#### Filtering By Tier
- **Prefix Matching:** Tier searches use **prefix matching**, meaning the search term must match the beginning of the tier exactly.
- If a customer has a tier labeled `Gold`, a search for `t/ G` or `t/ Gold` will match, but `t/ ld` or `t/ Gold Premium` will not.

#### Filtering By Income
- **Comparison Operators:** Filtering by income allows numeric comparisons using operators `=`, `>`, or `<` to find customers whose income meets certain criteria.
- **Equal to (`=`):** Use `=` to find customers with a specific income.
- `i/ =5000` will match customers with an income of exactly 5000.
- **Greater than (`>`):** Use `>` to find customers with an income higher than the specified threshold.
- `i/ >5000` will match customers with incomes greater than 5000.
- **Less than (`<`):** Use `<` to find customers with an income lower than the specified threshold.
- `i/ <5000` will match customers with incomes below 5000.

#### What to Expect
- **If Successful:**
- Message: "`x` person listed!", where `x` is the number of matching results.
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/seedu/address/logic/parser/FilterCommandParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@
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.IncomeComparisonPredicate;
import seedu.address.model.person.predicates.JobContainsSubstringPredicate;
import seedu.address.model.person.predicates.NameContainsSubstringPredicate;
import seedu.address.model.person.predicates.PhoneContainsSubstringPredicate;
import seedu.address.model.person.predicates.RemarkContainsSubstringPredicate;
import seedu.address.model.person.predicates.TierStartsWithSubstringPredicate;
import seedu.address.model.util.IncomeComparisonOperator;

/**
* Parses input arguments and creates a new FilterCommand object
Expand Down Expand Up @@ -110,6 +112,15 @@ private List<Predicate<Person>> collectPredicates(ArgumentMultimap argMultimap)
String substring = ParserUtil.parseJob(argMultimap.getValue(PREFIX_JOB).get()).value;
predicates.add(new JobContainsSubstringPredicate(substring));
}
if (argMultimap.getValue(PREFIX_INCOME).isPresent()) {
String operatorAndIncome = argMultimap.getValue(PREFIX_INCOME).get();

IncomeComparisonOperator operator =
ParserUtil.parseIncomeComparisonOperator(operatorAndIncome.substring(0, 1));
int income = ParserUtil.parseIncome(operatorAndIncome.substring(1)).value;

predicates.add(new IncomeComparisonPredicate(operator, income));
}
if (argMultimap.getValue(PREFIX_REMARK).isPresent()) {
String substring = ParserUtil.parseRemark(argMultimap.getValue(PREFIX_REMARK).get()).value;
predicates.add(new RemarkContainsSubstringPredicate(substring));
Expand Down
40 changes: 28 additions & 12 deletions src/main/java/seedu/address/logic/parser/ParserUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import seedu.address.model.person.Phone;
import seedu.address.model.person.Remark;
import seedu.address.model.tier.Tier;
import seedu.address.model.util.IncomeComparisonOperator;

/**
* Contains utility methods used for parsing strings in the various *Parser classes.
Expand Down Expand Up @@ -141,10 +142,25 @@ public static Remark parseRemark(String remark) throws ParseException {
}

/**
* Parses a {@code String tag} into a {@code Tag}.
* Parses a {@code String remark} into a {@code Remark}.
* Leading and trailing whitespaces are trimmed.
*
* @throws ParseException if the given {@code remark} is invalid.
*/
public static Remark parseNewRemark(String remark) throws ParseException {
requireNonNull(remark);
String trimmedRemark = remark.trim();
if (!Remark.isValidRemark(trimmedRemark)) {
throw new ParseException(Remark.MESSAGE_CONSTRAINTS);
}
return new Remark(trimmedRemark);
}

/**
* Parses a {@code String tier} into a {@code Tier}.
* Leading and trailing whitespaces will be trimmed.
*
* @throws ParseException if the given {@code tag} is invalid.
* @throws ParseException if the given {@code tier} is invalid.
*/
public static Tier parseTier(String tag) throws ParseException {
requireNonNull(tag);
Expand All @@ -159,19 +175,19 @@ public static Tier parseTier(String tag) throws ParseException {
}

/**
* Parses a {@code String remark} into a {@code Remark}.
* Leading and trailing whitespaces are trimmed.
* Parses a {@code String operator} into a {@code IncomeComparisonOperator}.
* Leading and trailing whitespaces will be trimmed.
*
* @throws ParseException if the given {@code remark} is invalid.
* @throws ParseException if the given {@code operator} is invalid.
*/
public static Remark parseNewRemark(String remark) throws ParseException {
requireNonNull(remark);
String trimmedRemark = remark.trim();
if (!Remark.isValidRemark(trimmedRemark)) {
System.out.println("exception thrown");
throw new ParseException(Remark.MESSAGE_CONSTRAINTS);
public static IncomeComparisonOperator parseIncomeComparisonOperator(String operator) throws ParseException {
requireNonNull(operator);
String trimmedOperator = operator.trim();
if (!IncomeComparisonOperator.isValidComparisonOperator(trimmedOperator)) {
System.out.println(("HERE"));
throw new ParseException(IncomeComparisonOperator.MESSAGE_CONSTRAINTS);
}
return new Remark(trimmedRemark);
return new IncomeComparisonOperator(trimmedOperator);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package seedu.address.model.person.predicates;

import static java.util.Objects.requireNonNull;

import java.util.function.Predicate;

import seedu.address.commons.util.ToStringBuilder;
import seedu.address.model.person.Person;
import seedu.address.model.util.IncomeComparisonOperator;

/**
* Predicate that compares a {@code Person}'s income against a threshold using a specified comparison operator.
*/
public class IncomeComparisonPredicate implements Predicate<Person> {
private final int incomeThreshold;
private final IncomeComparisonOperator incomeComparisonOperator;

/**
* Constructs an {@code IncomeComparisonPredicate}.
*
* @param incomeComparisonOperator The operator used to compare the person's income with the threshold.
* @param incomeThreshold The threshold income to compare against.
*/
public IncomeComparisonPredicate(IncomeComparisonOperator incomeComparisonOperator, int incomeThreshold) {
requireNonNull(incomeComparisonOperator);
checkPositiveIncomeThreshold(incomeThreshold);
this.incomeThreshold = incomeThreshold;
this.incomeComparisonOperator = incomeComparisonOperator;
}

@Override
public boolean test(Person person) {
int personIncome = person.getIncome().value;

switch (incomeComparisonOperator.comparisonOperator) {
case "=":
return personIncome == incomeThreshold;
case ">":
return personIncome > incomeThreshold;
case "<":
return personIncome < incomeThreshold;
default:
return false;
}
}

@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}

// instanceof handles nulls
if (!(other instanceof IncomeComparisonPredicate)) {
return false;
}

IncomeComparisonPredicate otherIncomeComparisonPredicate =
(IncomeComparisonPredicate) other;
return incomeThreshold == otherIncomeComparisonPredicate.incomeThreshold
&& incomeComparisonOperator.equals(otherIncomeComparisonPredicate.incomeComparisonOperator);
}

@Override
public String toString() {
return new ToStringBuilder(this)
.add("incomeThreshold", incomeThreshold)
.add("incomeComparisonOperator", incomeComparisonOperator)
.toString();
}

/**
* Ensures that the income threshold is positive.
*
* @param incomeThreshold The threshold to check.
* @throws IllegalArgumentException if {@code incomeThreshold} is not greater than 1.
*/
private void checkPositiveIncomeThreshold(int incomeThreshold) {
if (incomeThreshold < 0) {
throw new IllegalArgumentException("Income threshold cannot be less than 0.");
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package seedu.address.model.util;

import static java.util.Objects.requireNonNull;
import static seedu.address.commons.util.AppUtil.checkArgument;

import java.util.Objects;

/**
* Represents a comparison operator used in income comparisons.
* The operator can only be one of '=', '>', or '<'.
* Guarantees: comparisonOperator is validated upon creation.
*/
public class IncomeComparisonOperator {
public static final String MESSAGE_CONSTRAINTS = "Income comparison operators can only be '=', '>' or '<'";

public final String comparisonOperator;

/**
* Constructs a {@code IncomeComparisonOperator}.
*
* @param comparisonOperator A comparisonOperator
*/
public IncomeComparisonOperator(String comparisonOperator) {
requireNonNull(comparisonOperator);
checkArgument(isValidComparisonOperator(comparisonOperator), MESSAGE_CONSTRAINTS);
this.comparisonOperator = comparisonOperator;
}

/**
* Returns true if a given string is a valid comparison operator.
*/
public static boolean isValidComparisonOperator(String test) {
if (test.trim().isEmpty()) {
return false;
}

return (test.equals("=") || test.equals(">") || test.equals("<"));
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof IncomeComparisonOperator) {
IncomeComparisonOperator otherIncomeComparisonOperator = (IncomeComparisonOperator) other;
return comparisonOperator.equals(otherIncomeComparisonOperator.comparisonOperator);
}
return false;
}

@Override
public int hashCode() {
return Objects.hash(comparisonOperator);
}

@Override
public String toString() {
return comparisonOperator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
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.IncomeComparisonPredicate;
import seedu.address.model.person.predicates.JobContainsSubstringPredicate;
import seedu.address.model.person.predicates.NameContainsSubstringPredicate;
import seedu.address.model.person.predicates.PhoneContainsSubstringPredicate;
import seedu.address.model.person.predicates.RemarkContainsSubstringPredicate;
import seedu.address.model.person.predicates.TierStartsWithSubstringPredicate;
import seedu.address.model.util.IncomeComparisonOperator;

public class FilterCommandParserTest {

Expand Down Expand Up @@ -58,13 +60,24 @@ public void parse_emailFlag_returnsEmailFilterCommand() {
assertParseSuccess(parser, " e/ [email protected]", expectedFilterCommand);
}

@Test
public void parse_incomeFlag_returnsIncomeFilterCommand() {
List<Predicate<Person>> expectedPredicates = new ArrayList<>();
expectedPredicates.add(new EmailContainsSubstringPredicate("[email protected]"));
FilterCommand expectedFilterCommand = new FilterCommand(new CombinedPredicate(expectedPredicates));

assertParseSuccess(parser, " e/ [email protected]", expectedFilterCommand);
}

@Test
public void parse_jobFlag_returnsRemarkFilterCommand() {
List<Predicate<Person>> expectedPredicates = new ArrayList<>();
expectedPredicates.add(new JobContainsSubstringPredicate("Software Engineer"));
IncomeComparisonOperator operator = new IncomeComparisonOperator(">");
expectedPredicates.add(new IncomeComparisonPredicate(operator, 5000));

FilterCommand expectedFilterCommand = new FilterCommand(new CombinedPredicate(expectedPredicates));

assertParseSuccess(parser, " j/ Software Engineer", expectedFilterCommand);
assertParseSuccess(parser, " i/ >5000", expectedFilterCommand);
}

@Test
Expand Down Expand Up @@ -112,13 +125,15 @@ public void parse_validMultipleArgs_returnsFilterCommand() {
expectedPredicates.add(new EmailContainsSubstringPredicate("[email protected]"));
expectedPredicates.add(new AddressContainsSubstringPredicate("Block 123"));
expectedPredicates.add(new JobContainsSubstringPredicate("Software Engineer"));
IncomeComparisonOperator operator = new IncomeComparisonOperator(">");
expectedPredicates.add(new IncomeComparisonPredicate(operator, 5000));
expectedPredicates.add(new RemarkContainsSubstringPredicate("is a celebrity"));
expectedPredicates.add(new TierStartsWithSubstringPredicate("GOLD"));

FilterCommand expectedFilterCommand = new FilterCommand(new CombinedPredicate(expectedPredicates));

assertParseSuccess(parser, " n/ Alice p/ 91112222 e/ [email protected] a/ Block 123 "
+ "j/ Software Engineer r/ is a celebrity t/ gold", expectedFilterCommand);
+ "j/ Software Engineer i/ >5000 r/ is a celebrity t/ gold", expectedFilterCommand);
}

@Test
Expand Down
Loading

0 comments on commit 11d4661

Please sign in to comment.