diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 251a5944745..3cdf1f48280 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -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:**
```
@@ -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.
e.g., `n/` for name, `j/` for job. | |
+| SEARCH TERM | Follows the syntax for [each field's expected input](#add-command-parameters).
**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.
e.g., `doctor` for job, `>5000` for income). |
#### Supported flags:
- `n/` for Name
@@ -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.
diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
index f649da17008..1e2dbfcc1ba 100644
--- a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
+++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
@@ -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
@@ -110,6 +112,15 @@ private List> 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));
diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java
index c9ac8ca6743..61283eb8269 100644
--- a/src/main/java/seedu/address/logic/parser/ParserUtil.java
+++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java
@@ -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.
@@ -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);
@@ -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);
}
}
diff --git a/src/main/java/seedu/address/model/person/predicates/IncomeComparisonPredicate.java b/src/main/java/seedu/address/model/person/predicates/IncomeComparisonPredicate.java
new file mode 100644
index 00000000000..37f2485d5be
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/predicates/IncomeComparisonPredicate.java
@@ -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 {
+ 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.");
+ }
+ }
+}
+
diff --git a/src/main/java/seedu/address/model/person/predicates/IncomeContainsSubstringPredicate.java b/src/main/java/seedu/address/model/person/predicates/IncomeContainsSubstringPredicate.java
deleted file mode 100644
index 098e76836e0..00000000000
--- a/src/main/java/seedu/address/model/person/predicates/IncomeContainsSubstringPredicate.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package seedu.address.model.person.predicates;
-
-import seedu.address.commons.util.StringUtil;
-import seedu.address.commons.util.ToStringBuilder;
-import seedu.address.model.person.Person;
-
-/**
- * Tests that a {@code Person}'s {@code Income} contains a specified substring.
- */
-public class IncomeContainsSubstringPredicate extends ContainsSubstringPredicate {
- public IncomeContainsSubstringPredicate(String substring) {
- super(substring);
- }
-
- @Override
- public boolean test(Person person) {
- return StringUtil.containsSubstringIgnoreCase(String.valueOf(person.getIncome().value), substring);
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) {
- return true;
- }
-
- // instanceof handles nulls
- if (!(other instanceof IncomeContainsSubstringPredicate)) {
- return false;
- }
-
- IncomeContainsSubstringPredicate otherIncomeContainsSubstringPredicate =
- (IncomeContainsSubstringPredicate) other;
- return substring.equals(otherIncomeContainsSubstringPredicate.substring);
- }
-
- @Override
- public String toString() {
- return new ToStringBuilder(this).add("substring", substring).toString();
- }
-}
diff --git a/src/main/java/seedu/address/model/util/IncomeComparisonOperator.java b/src/main/java/seedu/address/model/util/IncomeComparisonOperator.java
new file mode 100644
index 00000000000..ed865bee5e8
--- /dev/null
+++ b/src/main/java/seedu/address/model/util/IncomeComparisonOperator.java
@@ -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;
+ }
+}
diff --git a/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java
index 8e83a9f90e1..1f7d7608a1c 100644
--- a/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java
@@ -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 {
@@ -58,13 +60,24 @@ public void parse_emailFlag_returnsEmailFilterCommand() {
assertParseSuccess(parser, " e/ alice@hello.com", expectedFilterCommand);
}
+ @Test
+ public void parse_incomeFlag_returnsIncomeFilterCommand() {
+ List> expectedPredicates = new ArrayList<>();
+ expectedPredicates.add(new EmailContainsSubstringPredicate("alice@hello.com"));
+ FilterCommand expectedFilterCommand = new FilterCommand(new CombinedPredicate(expectedPredicates));
+
+ assertParseSuccess(parser, " e/ alice@hello.com", expectedFilterCommand);
+ }
+
@Test
public void parse_jobFlag_returnsRemarkFilterCommand() {
List> 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
@@ -112,13 +125,15 @@ public void parse_validMultipleArgs_returnsFilterCommand() {
expectedPredicates.add(new EmailContainsSubstringPredicate("alice@example.com"));
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/ alice@example.com 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
diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
index 28f9d76d4ae..4e3eab7c986 100644
--- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
+++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java
@@ -10,23 +10,32 @@
import seedu.address.logic.parser.exceptions.ParseException;
import seedu.address.model.person.Address;
import seedu.address.model.person.Email;
+import seedu.address.model.person.Income;
import seedu.address.model.person.Name;
import seedu.address.model.person.Phone;
import seedu.address.model.tier.Tier;
+import seedu.address.model.util.IncomeComparisonOperator;
public class ParserUtilTest {
private static final String INVALID_NAME = "R@chel";
private static final String INVALID_PHONE = "+651234";
private static final String INVALID_ADDRESS = " ";
private static final String INVALID_EMAIL = "example.com";
+ private static final String INVALID_INCOME = "one thousand";
private static final String INVALID_TAG = "#friend";
+ private static final String INVALID_INCOME_COMPARISON_OPERATOR_1 = "==";
+ private static final String INVALID_INCOME_COMPARISON_OPERATOR_2 = "!";
private static final String VALID_NAME = "Rachel Walker";
private static final String VALID_PHONE = "91234567";
private static final String VALID_ADDRESS = "123 Main Street #0505";
private static final String VALID_EMAIL = "rachel@example.com";
+ private static final String VALID_INCOME = "1000";
private static final String VALID_TAG_1 = "BRONZE";
private static final String VALID_TAG_2 = "SILVER";
+ private static final String VALID_INCOME_COMPARISON_OPERATOR_EQUAL = ">";
+ private static final String VALID_INCOME_COMPARISON_OPERATOR_GREATER_THAN = ">";
+ private static final String VALID_INCOME_COMPARISON_OPERATOR_LESS_THAN = "<";
private static final String WHITESPACE = " \t\r\n";
@@ -142,6 +151,29 @@ public void parseEmail_validValueWithWhitespace_returnsTrimmedEmail() throws Exc
assertEquals(expectedEmail, ParserUtil.parseEmail(emailWithWhitespace));
}
+ @Test
+ public void parseIncome_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> ParserUtil.parseIncome((String) null));
+ }
+
+ @Test
+ public void parseIncome_invalidValue_throwsParseException() {
+ assertThrows(ParseException.class, () -> ParserUtil.parseIncome(INVALID_INCOME));
+ }
+
+ @Test
+ public void parseIncome_validValueWithoutWhitespace_returnsEmail() throws Exception {
+ Income expectedIncome = new Income(Integer.parseInt(VALID_INCOME));
+ assertEquals(expectedIncome, ParserUtil.parseIncome(VALID_INCOME));
+ }
+
+ @Test
+ public void parseIncome_validValueWithWhitespace_returnsTrimmedIncome() throws Exception {
+ String incomeWithWhitespace = WHITESPACE + VALID_INCOME + WHITESPACE;
+ Income expectedIncome = new Income(Integer.parseInt(VALID_INCOME));
+ assertEquals(expectedIncome, ParserUtil.parseIncome(incomeWithWhitespace));
+ }
+
@Test
public void parseTier_null_throwsNullPointerException() {
assertThrows(NullPointerException.class, () -> ParserUtil.parseTier(null));
@@ -165,4 +197,53 @@ public void parseTier_validValueWithWhitespace_returnsTrimmedTag() throws Except
assertEquals(expectedTier, ParserUtil.parseTier(tagWithWhitespace));
}
+ @Test
+ public void parseIncomeComparisonOperator_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> ParserUtil.parseIncomeComparisonOperator(null));
+ }
+
+ @Test
+ public void parseIncomeComparisonOperator_invalidValue_throwsParseException() {
+ assertThrows(ParseException.class, () ->
+ ParserUtil.parseIncomeComparisonOperator(INVALID_INCOME_COMPARISON_OPERATOR_1));
+ assertThrows(ParseException.class, () ->
+ ParserUtil.parseIncomeComparisonOperator(INVALID_INCOME_COMPARISON_OPERATOR_2));
+ }
+
+ @Test
+ public void parseIncomeComparisonOperator_validValueWithoutWhitespace_returnsIncomeComparisonOperator()
+ throws Exception {
+ IncomeComparisonOperator equalOperator = new IncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_EQUAL);
+ IncomeComparisonOperator greaterThanOperator = new IncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_GREATER_THAN);
+ IncomeComparisonOperator lessThanOperator = new IncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_LESS_THAN);
+
+ assertEquals(equalOperator, ParserUtil.parseIncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_EQUAL));
+ assertEquals(greaterThanOperator, ParserUtil.parseIncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_GREATER_THAN));
+ assertEquals(lessThanOperator, ParserUtil.parseIncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_LESS_THAN));
+ }
+
+ @Test
+ public void parseIncomeComparisonOperator_validValueWithWhitespace_returnsIncomeComparisonOperator()
+ throws Exception {
+ IncomeComparisonOperator equalOperator = new IncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_EQUAL);
+ IncomeComparisonOperator greaterThanOperator = new IncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_GREATER_THAN);
+ IncomeComparisonOperator lessThanOperator = new IncomeComparisonOperator(
+ VALID_INCOME_COMPARISON_OPERATOR_LESS_THAN);
+
+ assertEquals(equalOperator, ParserUtil.parseIncomeComparisonOperator(
+ WHITESPACE + VALID_INCOME_COMPARISON_OPERATOR_EQUAL + WHITESPACE));
+ assertEquals(greaterThanOperator, ParserUtil.parseIncomeComparisonOperator(
+ WHITESPACE + VALID_INCOME_COMPARISON_OPERATOR_GREATER_THAN + WHITESPACE));
+ assertEquals(lessThanOperator, ParserUtil.parseIncomeComparisonOperator(
+ WHITESPACE + VALID_INCOME_COMPARISON_OPERATOR_LESS_THAN + WHITESPACE));
+ }
+
}
diff --git a/src/test/java/seedu/address/model/person/predicates/IncomeComparisonPredicateTest.java b/src/test/java/seedu/address/model/person/predicates/IncomeComparisonPredicateTest.java
new file mode 100644
index 00000000000..a4028c79f26
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/predicates/IncomeComparisonPredicateTest.java
@@ -0,0 +1,88 @@
+package seedu.address.model.person.predicates;
+
+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 org.junit.jupiter.api.Test;
+
+import seedu.address.model.util.IncomeComparisonOperator;
+import seedu.address.testutil.PersonBuilder;
+
+public class IncomeComparisonPredicateTest {
+
+ @Test
+ public void equals() {
+ IncomeComparisonOperator operatorEqual = new IncomeComparisonOperator("=");
+ IncomeComparisonOperator operatorGreater = new IncomeComparisonOperator(">");
+
+ IncomeComparisonPredicate firstPredicate = new IncomeComparisonPredicate(operatorEqual, 5000);
+ IncomeComparisonPredicate secondPredicate = new IncomeComparisonPredicate(operatorGreater, 10000);
+
+ // same object -> returns true
+ assertTrue(firstPredicate.equals(firstPredicate));
+
+ // same values -> returns true
+ IncomeComparisonPredicate firstPredicateCopy = new IncomeComparisonPredicate(operatorEqual, 5000);
+ assertTrue(firstPredicate.equals(firstPredicateCopy));
+
+ // different types -> returns false
+ assertFalse(firstPredicate.equals(1));
+
+ // null -> returns false
+ assertFalse(firstPredicate.equals(null));
+
+ // different predicate -> returns false
+ assertFalse(firstPredicate.equals(secondPredicate));
+ }
+
+ @Test
+ public void test_incomeEqualComparison_returnsTrue() {
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(new IncomeComparisonOperator("="), 5000);
+ assertTrue(predicate.test(new PersonBuilder().withIncome(5000).build()));
+ }
+
+ @Test
+ public void test_incomeEqualComparison_returnsFalse() {
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(new IncomeComparisonOperator("="), 5000);
+ assertFalse(predicate.test(new PersonBuilder().withIncome(6000).build()));
+ assertFalse(predicate.test(new PersonBuilder().withIncome(4000).build()));
+ }
+
+ @Test
+ public void test_incomeGreaterThanComparison_returnsTrue() {
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(new IncomeComparisonOperator(">"), 5000);
+ assertTrue(predicate.test(new PersonBuilder().withIncome(6000).build()));
+ }
+
+ @Test
+ public void test_incomeGreaterThanComparison_returnsFalse() {
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(new IncomeComparisonOperator(">"), 5000);
+ assertFalse(predicate.test(new PersonBuilder().withIncome(4000).build()));
+ assertFalse(predicate.test(new PersonBuilder().withIncome(5000).build()));
+ }
+
+ @Test
+ public void test_incomeLessThanComparison_returnsTrue() {
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(new IncomeComparisonOperator("<"), 5000);
+ assertTrue(predicate.test(new PersonBuilder().withIncome(4000).build()));
+ }
+
+ @Test
+ public void test_incomeLessThanComparison_returnsFalse() {
+ // Income is not less than the threshold
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(new IncomeComparisonOperator("<"), 5000);
+ assertFalse(predicate.test(new PersonBuilder().withIncome(6000).build()));
+ assertFalse(predicate.test(new PersonBuilder().withIncome(5000).build()));
+ }
+
+ @Test
+ public void toStringMethod() {
+ IncomeComparisonOperator operator = new IncomeComparisonOperator("=");
+ IncomeComparisonPredicate predicate = new IncomeComparisonPredicate(operator, 5000);
+
+ String expected = IncomeComparisonPredicate.class.getCanonicalName()
+ + "{incomeThreshold=5000, incomeComparisonOperator==}";
+ assertEquals(expected, predicate.toString());
+ }
+}
diff --git a/src/test/java/seedu/address/model/util/IncomeComparisonOperatorTest.java b/src/test/java/seedu/address/model/util/IncomeComparisonOperatorTest.java
new file mode 100644
index 00000000000..243920102a3
--- /dev/null
+++ b/src/test/java/seedu/address/model/util/IncomeComparisonOperatorTest.java
@@ -0,0 +1,89 @@
+package seedu.address.model.util;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static seedu.address.testutil.Assert.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+public class IncomeComparisonOperatorTest {
+ @Test
+ public void constructor_null_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> new IncomeComparisonOperator(null));
+ }
+
+ @Test
+ public void constructor_invalidOperator_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> new IncomeComparisonOperator("!"));
+ assertThrows(IllegalArgumentException.class, () -> new IncomeComparisonOperator("=="));
+ assertThrows(IllegalArgumentException.class, () -> new IncomeComparisonOperator(""));
+ }
+
+ @Test
+ public void constructor_validOperator_success() {
+ assertDoesNotThrow(() -> new IncomeComparisonOperator("="));
+ assertDoesNotThrow(() -> new IncomeComparisonOperator(">"));
+ assertDoesNotThrow(() -> new IncomeComparisonOperator("<"));
+ }
+
+ @Test
+ public void isValidComparisonOperator() {
+ // Test valid operators
+ assertTrue(IncomeComparisonOperator.isValidComparisonOperator("="));
+ assertTrue(IncomeComparisonOperator.isValidComparisonOperator(">"));
+ assertTrue(IncomeComparisonOperator.isValidComparisonOperator("<"));
+
+ // Test invalid operators
+ assertFalse(IncomeComparisonOperator.isValidComparisonOperator("!"));
+ assertFalse(IncomeComparisonOperator.isValidComparisonOperator("=="));
+ assertFalse(IncomeComparisonOperator.isValidComparisonOperator(" "));
+ assertFalse(IncomeComparisonOperator.isValidComparisonOperator(""));
+ assertFalse(IncomeComparisonOperator.isValidComparisonOperator(">="));
+ }
+
+ @Test
+ public void equals_sameObject_returnsTrue() {
+ IncomeComparisonOperator operator = new IncomeComparisonOperator("=");
+ assertEquals(operator, operator);
+ }
+
+ @Test
+ public void equals_differentObjectsSameValue_returnsTrue() {
+ IncomeComparisonOperator operator1 = new IncomeComparisonOperator(">");
+ IncomeComparisonOperator operator2 = new IncomeComparisonOperator(">");
+ assertEquals(operator1, operator2);
+ }
+
+ @Test
+ public void equals_differentObjectsDifferentValue_returnsFalse() {
+ IncomeComparisonOperator operator1 = new IncomeComparisonOperator(">");
+ IncomeComparisonOperator operator2 = new IncomeComparisonOperator("<");
+ assertNotEquals(operator1, operator2);
+ }
+
+ @Test
+ public void hashCode_sameValue_returnsSameHashCode() {
+ // Test that objects with the same value return the same hash code
+ IncomeComparisonOperator operator1 = new IncomeComparisonOperator("<");
+ IncomeComparisonOperator operator2 = new IncomeComparisonOperator("<");
+ assertEquals(operator1.hashCode(), operator2.hashCode());
+ }
+
+ @Test
+ public void hashCode_differentValues_returnsDifferentHashCodes() {
+ // Test that objects with different values return different hash codes
+ IncomeComparisonOperator operator1 = new IncomeComparisonOperator("=");
+ IncomeComparisonOperator operator2 = new IncomeComparisonOperator(">");
+ assertNotEquals(operator1.hashCode(), operator2.hashCode());
+ }
+
+ @Test
+ public void toString_returnsCorrectString() {
+ // Test that the toString method returns the correct string representation
+ IncomeComparisonOperator operator = new IncomeComparisonOperator("=");
+ assertEquals("=", operator.toString());
+ }
+}