Repository: AY2425S2-CS2103T-W12-3/tp
Pull request #126: Branch implement filter tag
Author: login: ShiJianXi
Labels: name: priority.Medium
description: Nice to have
Created: 2025-03-25T18:03:22Z | Merged: 2025-03-27T05:59:45Z

Fixes #110 

Linked issue #110: Feature: Filter by tags (year, specialisation etc.)

Diff:
diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java
index e1fa0aad845..fd4e8b9d70c 100644
--- a/src/main/java/seedu/address/logic/commands/FilterCommand.java
+++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java
@@ -1,27 +1,35 @@
 package seedu.address.logic.commands;
 
 import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PROJECT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
+
+import java.util.function.Predicate;
 
 import seedu.address.commons.util.ToStringBuilder;
 import seedu.address.logic.Messages;
 import seedu.address.model.Model;
-import seedu.address.model.person.StudentUnderProjectPredicate;
+import seedu.address.model.person.Person;
 
 /**
- * Filters and list all students in Prof-iler whose project contain the given keyword.
- * Keyword matching is case insensitive.
+ * Filters and list all students in Prof-iler whose project contain the given keyword
+ * or whose tag contain the given keyword.
+ * Keyword 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 for the student who is under the "
-            + "specified project based on the keywords (case-insensitive)"
+            + "specified project based on the keywords (case-insensitive) "
             + "and displays them as a list with index numbers. \n"
-            + "Parameters: KEYWORD\n"
-            + "Example: " + COMMAND_WORD + "Prof-iler";
+            + "Parameters: \n"
+            + PREFIX_PROJECT + "KEYWORD [MORE KEYWORDS] \n"
+            + PREFIX_TAG + "KEYWORD [MORE KEYWORDS] \n"
+            + "Example: " + COMMAND_WORD + " " + PREFIX_PROJECT + " Prof-iler \n"
+            + "Example: " + COMMAND_WORD + " " + PREFIX_TAG + "W12" + " CS2103T";
 
-    private final StudentUnderProjectPredicate predicate;
+    private final Predicate<Person> predicate;
 
-    public FilterCommand(StudentUnderProjectPredicate predicate) {
+    public FilterCommand(Predicate<Person> predicate) {
         this.predicate = predicate;
     }
 
diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
index 2aa9cd7dae7..de3ff08066d 100644
--- a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
+++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java
@@ -1,12 +1,18 @@
 package seedu.address.logic.parser;
 
+import static java.util.Objects.requireNonNull;
 import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PROJECT;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG;
 
 import java.util.Arrays;
+import java.util.function.Predicate;
 
 import seedu.address.logic.commands.FilterCommand;
 import seedu.address.logic.parser.exceptions.ParseException;
-import seedu.address.model.person.StudentUnderProjectPredicate;
+import seedu.address.model.person.Person;
+import seedu.address.model.person.ProjectContainsKeywordsPredicate;
+import seedu.address.model.person.TagContainsKeywordsPredicate;
 
 /**
  * Parses input arguments and creates a new FilterCommand object
@@ -21,15 +27,57 @@ public class FilterCommandParser implements Parser<FilterCommand> {
      * @throws ParseException if the user input does not conform the expected format
      */
     public FilterCommand parse(String args) throws ParseException {
-        String trimmedArgs = args.trim();
-        if (trimmedArgs.isEmpty()) {
-            throw new ParseException(
-                    String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        requireNonNull(args);
+        ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PROJECT, PREFIX_TAG);
+
+        if (!hasRequiredPrefix(argumentMultimap)) {
+            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        }
+
+        argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PROJECT, PREFIX_TAG);
+
+        String[] projectKeywords = new String[0];
+        Predicate<Person> filteredStudent = null;
+        filteredStudent = getStudentPredicate(argumentMultimap, filteredStudent);
+
+        return new FilterCommand(filteredStudent);
+    }
+
+    private Predicate<Person> getStudentPredicate(ArgumentMultimap argumentMultimap, Predicate<Person> filteredStudent)
+            throws ParseException {
+        String[] projectKeywords;
+        if (argumentMultimap.getValue(PREFIX_PROJECT).isPresent()) {
+            checkFilterCondition(argumentMultimap, PREFIX_PROJECT);
+            projectKeywords = argumentMultimap.getValue(PREFIX_PROJECT).get().split("\\s+");
+            filteredStudent = new ProjectContainsKeywordsPredicate(Arrays.asList(projectKeywords));
         }
 
-        String[] projectKeyword = trimmedArgs.split("\\s+");
+        if (argumentMultimap.getValue(PREFIX_TAG).isPresent()) {
+            checkFilterCondition(argumentMultimap, PREFIX_TAG);
+            projectKeywords = argumentMultimap.getValue(PREFIX_TAG).get().split("\\s+");
+            filteredStudent = new TagContainsKeywordsPredicate(Arrays.asList(projectKeywords));
+        }
+        return filteredStudent;
+    }
+
+    private boolean isPrefixPresent(ArgumentMultimap argumentMultimap, Prefix prefix) {
+        return !argumentMultimap.getAllValues(prefix).isEmpty();
+    }
+
+    private void checkFilterCondition(ArgumentMultimap argumentMultimap, Prefix prefix) throws ParseException {
+        if (isPrefixPresent(argumentMultimap, prefix == PREFIX_PROJECT ? PREFIX_TAG : PREFIX_PROJECT)
+                || argumentMultimap.getValue(prefix).isEmpty()
+                || !argumentMultimap.getPreamble().isEmpty()
+                || argumentMultimap.getValue(prefix).get().isEmpty()) {
+            throw new ParseException(String.format(
+                    MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE
+            ));
+        }
+    }
 
-        return new FilterCommand(new StudentUnderProjectPredicate(Arrays.asList(projectKeyword)));
+    private boolean hasRequiredPrefix(ArgumentMultimap argumentMultimap) {
+        return !argumentMultimap.getAllValues(PREFIX_TAG).isEmpty()
+                || !argumentMultimap.getAllValues(PREFIX_PROJECT).isEmpty();
     }
 }
 
diff --git a/src/main/java/seedu/address/model/person/StudentUnderProjectPredicate.java b/src/main/java/seedu/address/model/person/ProjectContainsKeywordsPredicate.java
similarity index 61%
rename from src/main/java/seedu/address/model/person/StudentUnderProjectPredicate.java
rename to src/main/java/seedu/address/model/person/ProjectContainsKeywordsPredicate.java
index 99f5341df07..74881b4f9a6 100644
--- a/src/main/java/seedu/address/model/person/StudentUnderProjectPredicate.java
+++ b/src/main/java/seedu/address/model/person/ProjectContainsKeywordsPredicate.java
@@ -7,13 +7,13 @@
 import seedu.address.commons.util.ToStringBuilder;
 
 /**
- * Tests that a {@code Student}'s {@code Project} matches any of the keywords given.
+ * Tests that a {@code Person}'s {@code Project} matches any of the keywords given.
  */
-public class StudentUnderProjectPredicate implements Predicate<Person> {
+public class ProjectContainsKeywordsPredicate implements Predicate<Person> {
 
     private final List<String> keywords;
 
-    public StudentUnderProjectPredicate(List<String> keywords) {
+    public ProjectContainsKeywordsPredicate(List<String> keywords) {
         this.keywords = keywords;
     }
 
@@ -30,12 +30,12 @@ public boolean equals(Object other) {
         }
 
         // instaceof handles nulls
-        if (!(other instanceof StudentUnderProjectPredicate)) {
+        if (!(other instanceof ProjectContainsKeywordsPredicate)) {
             return false;
         }
-
-        StudentUnderProjectPredicate otherStudentUnderProjectPredicate = (StudentUnderProjectPredicate) other;
-        return keywords.equals(otherStudentUnderProjectPredicate.keywords);
+        ProjectContainsKeywordsPredicate otherProjectContainsKeywordsPredicate =
+                (ProjectContainsKeywordsPredicate) other;
+        return keywords.equals(otherProjectContainsKeywordsPredicate.keywords);
     }
 
     @Override
diff --git a/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java
new file mode 100644
index 00000000000..938665b8c68
--- /dev/null
+++ b/src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java
@@ -0,0 +1,57 @@
+package seedu.address.model.person;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.StringUtil;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Tests that a {@code Person}'s {@code Project} matches any of the keywords given.
+ */
+public class TagContainsKeywordsPredicate implements Predicate<Person> {
+
+    private final List<String> keywords;
+
+    public TagContainsKeywordsPredicate(List<String> keywords) {
+        this.keywords = keywords;
+    }
+
+    @Override
+    public boolean test(Person person) {
+        Set<Tag> tags = person.getTags();
+        boolean tagContainsKeywords = false;
+        for (Tag tag : tags) {
+            String tagName = tag.tagName;
+            tagContainsKeywords = keywords.stream().anyMatch(
+                    keyword -> StringUtil.containsWordIgnoreCase(tagName, keyword));
+            if (tagContainsKeywords) {
+                break;
+            }
+        }
+        return tagContainsKeywords;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        }
+
+        // instaceof handles nulls
+        if (!(other instanceof TagContainsKeywordsPredicate)) {
+            return false;
+        }
+
+        TagContainsKeywordsPredicate otherTagContainsKeywordsPredicate = (TagContainsKeywordsPredicate) other;
+        return keywords.equals(otherTagContainsKeywordsPredicate.keywords);
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).add("Tag", keywords).toString();
+    }
+
+}
diff --git a/src/test/java/seedu/address/logic/commands/FilterCommandTest.java b/src/test/java/seedu/address/logic/commands/FilterCommandTest.java
index 04553c626dc..c7b3f6bca44 100644
--- a/src/test/java/seedu/address/logic/commands/FilterCommandTest.java
+++ b/src/test/java/seedu/address/logic/commands/FilterCommandTest.java
@@ -15,7 +15,7 @@
 import seedu.address.model.Model;
 import seedu.address.model.ModelManager;
 import seedu.address.model.UserPrefs;
-import seedu.address.model.person.StudentUnderProjectPredicate;
+import seedu.address.model.person.ProjectContainsKeywordsPredicate;
 
 /**
  * Contains integration tests (interaction with the Model) for {@code FilterCommand}.
@@ -26,10 +26,10 @@ public class FilterCommandTest {
 
     @Test
     public void equals() {
-        StudentUnderProjectPredicate firstPredicate =
-                new StudentUnderProjectPredicate(Collections.singletonList("first"));
-        StudentUnderProjectPredicate secondPredicate =
-                new StudentUnderProjectPredicate(Collections.singletonList("second"));
+        ProjectContainsKeywordsPredicate firstPredicate =
+                new ProjectContainsKeywordsPredicate(Collections.singletonList("first"));
+        ProjectContainsKeywordsPredicate secondPredicate =
+                new ProjectContainsKeywordsPredicate(Collections.singletonList("second"));
 
         FilterCommand filterFirstCommand = new FilterCommand(firstPredicate);
         FilterCommand filterSecondCommand = new FilterCommand(secondPredicate);
@@ -54,7 +54,7 @@ public void equals() {
     @Test
     public void execute_zeroKeywords_noProjectFound() {
         String expectedMessage = String.format(MESSAGE_PROJECTS_FILTERED_OVERVIEW, 0);
-        StudentUnderProjectPredicate predicate = preparePredicate(" ");
+        ProjectContainsKeywordsPredicate predicate = preparePredicate(" ");
         FilterCommand command = new FilterCommand(predicate);
         expectedModel.updateFilteredPersonList(predicate);
         assertCommandSuccess(command, model, expectedMessage, expectedModel);
@@ -63,7 +63,7 @@ public void execute_zeroKeywords_noProjectFound() {
 
     @Test
     public void execute_multipleKeywords_multipleProjectsFound() {
-        StudentUnderProjectPredicate predicate = new StudentUnderProjectPredicate(Arrays.asList("keyword"));
+        ProjectContainsKeywordsPredicate predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("keyword"));
         FilterCommand filterCommand = new FilterCommand(predicate);
         String expected = FilterCommand.class.getCanonicalName() + "{predicate=" + predicate + "}";
         assertEquals(expected, filterCommand.toString());
@@ -71,7 +71,7 @@ public void execute_multipleKeywords_multipleProjectsFound() {
 
     @Test
     public void toStringMethod() {
-        StudentUnderProjectPredicate predicate = new StudentUnderProjectPredicate(Arrays.asList("keyword"));
+        ProjectContainsKeywordsPredicate predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("keyword"));
         FilterCommand filterCommand = new FilterCommand(predicate);
         String expected = FilterCommand.class.getCanonicalName() + "{predicate=" + predicate + "}";
         assertEquals(expected, filterCommand.toString());
@@ -81,7 +81,7 @@ public void toStringMethod() {
     /**
      * Parses {@code userInput} into a {@code StudentUnderProjectPredicate} {}
      */
-    private StudentUnderProjectPredicate preparePredicate(String userInput) {
-        return new StudentUnderProjectPredicate(Arrays.asList(userInput.split("\\s+")));
+    private ProjectContainsKeywordsPredicate preparePredicate(String userInput) {
+        return new ProjectContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+")));
     }
 }
diff --git a/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java
index ed7b9dfafc8..f5e6a367cfc 100644
--- a/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java
+++ b/src/test/java/seedu/address/logic/parser/FilterCommandParserTest.java
@@ -9,24 +9,52 @@
 import org.junit.jupiter.api.Test;
 
 import seedu.address.logic.commands.FilterCommand;
-import seedu.address.model.person.StudentUnderProjectPredicate;
+import seedu.address.model.person.ProjectContainsKeywordsPredicate;
+import seedu.address.model.person.TagContainsKeywordsPredicate;
 
 public class FilterCommandParserTest {
     private FilterCommandParser parser = new FilterCommandParser();
 
     @Test
     public void parse_emptyArgs_throwsParseException() {
+        // empty input
         assertParseFailure(parser, "    ", String.format(MESSAGE_INVALID_COMMAND_FORMAT,
                 FilterCommand.MESSAGE_USAGE));
+
+        // missing prefix
+        assertParseFailure(parser, "filter test", String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+                FilterCommand.MESSAGE_USAGE));
+
+        // missing value for project
+        assertParseFailure(parser, "filter pr/ ", String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+                FilterCommand.MESSAGE_USAGE));
+
+        // missing value for tag
+        assertParseFailure(parser, "filter t/ ", String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+                FilterCommand.MESSAGE_USAGE));
+
+        // having both prefix for tag and project
+        assertParseFailure(parser, "filter t/ pr/ ", String.format(MESSAGE_INVALID_COMMAND_FORMAT,
+                FilterCommand.MESSAGE_USAGE));
+    }
+
+    @Test
+    public void parse_validArgsForProject_returnsFilterCommand() {
+        FilterCommand expectedFilterCommand =
+                new FilterCommand(new ProjectContainsKeywordsPredicate(Arrays.asList("Project", "Profiler")));
+        assertParseSuccess(parser, " pr/ Project Profiler", expectedFilterCommand);
+
+        // multiple whitespaces between keywords
+        assertParseSuccess(parser, " pr/ \n Project \n \t Profiler \t", expectedFilterCommand);
     }
 
     @Test
-    public void parse_validArgs_returnsFilterCommand() {
+    public void parse_validArgsForTag_returnsFilterCommand() {
         FilterCommand expectedFilterCommand =
-                new FilterCommand(new StudentUnderProjectPredicate(Arrays.asList("Project", "Profiler")));
-        assertParseSuccess(parser, "Project Profiler", expectedFilterCommand);
+                new FilterCommand(new TagContainsKeywordsPredicate(Arrays.asList("CS2103T", "CS2101")));
+        assertParseSuccess(parser, " t/ CS2103T CS2101", expectedFilterCommand);
 
         // multiple whitespaces between keywords
-        assertParseSuccess(parser, "\n Project \n \t Profiler \t", expectedFilterCommand);
+        assertParseSuccess(parser, " t/ \n CS2103T \n \t CS2101 \t", expectedFilterCommand);
     }
 }
diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java
index 1fbce2f0d5f..319c33278ad 100644
--- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java
+++ b/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java
@@ -68,7 +68,7 @@ public void test_nameDoesNotContainKeywords_returnsFalse() {
         predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol"));
         assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build()));
 
-        // Keywords match phone, email and address, but does not match name
+        // Keywords match phone, email and project, but does not match name
         predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street"));
         assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345")
                 .withEmail("alice@email.com").withProject("Main Street").build()));
diff --git a/src/test/java/seedu/address/model/person/ProjectContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/ProjectContainsKeywordsPredicateTest.java
new file mode 100644
index 00000000000..85d27025d2e
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/ProjectContainsKeywordsPredicateTest.java
@@ -0,0 +1,90 @@
+package seedu.address.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.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.testutil.PersonBuilder;
+
+public class ProjectContainsKeywordsPredicateTest {
+
+    @Test
+    public void equals() {
+        List<String> firstPredicateKeywordList = Collections.singletonList("first");
+        List<String> secondPredicateKeywordList = Arrays.asList("first", "second");
+
+        ProjectContainsKeywordsPredicate firstPredicate = new
+                ProjectContainsKeywordsPredicate(firstPredicateKeywordList);
+        ProjectContainsKeywordsPredicate secondPredicate = new
+                ProjectContainsKeywordsPredicate(secondPredicateKeywordList);
+
+        // same object -> returns true
+        assertTrue(firstPredicate.equals(firstPredicate));
+
+        // same value -> returns true
+        ProjectContainsKeywordsPredicate firstPredicateCopy = new
+                ProjectContainsKeywordsPredicate(firstPredicateKeywordList);
+        assertTrue(firstPredicate.equals(firstPredicateCopy));
+
+        // different types -> returns false;
+        assertFalse(firstPredicate.equals(1));
+
+        // null -> returns false
+        assertFalse(firstPredicate.equals(null));
+
+        // different project -> returns false
+        assertFalse(firstPredicate.equals(secondPredicate));
+    }
+
+    @Test
+    public void test_projectContainsKeywords_returnsTrue() {
+        // One keyword
+        ProjectContainsKeywordsPredicate predicate = new
+                ProjectContainsKeywordsPredicate(Collections.singletonList("Prof-iler"));
+        assertTrue(predicate.test(new PersonBuilder().withProject("Prof-iler").build()));
+
+        // Multiple keywords
+        predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("Project", "Rex"));
+        assertTrue(predicate.test(new PersonBuilder().withProject("Project Rex").build()));
+
+        // Only one matching keyword
+        predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("Project", "Bob"));
+        assertTrue(predicate.test(new PersonBuilder().withProject("Bob Builder").build()));
+
+        // Mix-case keywords
+        predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("pRoJECt", "bOB"));
+        assertTrue(predicate.test(new PersonBuilder().withProject("Alice Bob").build()));
+    }
+
+    @Test
+    public void test_projectDoesNotContainKeywords_returnFalse() {
+        // Zero keywords
+        ProjectContainsKeywordsPredicate predicate = new
+                ProjectContainsKeywordsPredicate(Collections.emptyList());
+        assertFalse(predicate.test(new PersonBuilder().withProject("Prof-iler").build()));
+
+        // Non-matching keyword
+        predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("Project", "Rex"));
+        assertFalse(predicate.test(new PersonBuilder().withProject("Prof-iler").build()));
+
+        // Keywords match name,phone, email but does not match project
+        predicate = new ProjectContainsKeywordsPredicate(Arrays.asList("Project", "12345"));
+        assertFalse(predicate.test(new PersonBuilder().withName("Project").withPhone("12345")
+                .withEmail("Project@email.com").withProject("Bob").build()));
+    }
+
+    @Test
+    public void toStringMethod() {
+        List<String> keywords = List.of("keyword1", "keyword2");
+        ProjectContainsKeywordsPredicate predicate = new ProjectContainsKeywordsPredicate(keywords);
+
+        String expected = ProjectContainsKeywordsPredicate.class.getCanonicalName() + "{Project=" + keywords + "}";
+        assertEquals(expected, predicate.toString());
+    }
+}
diff --git a/src/test/java/seedu/address/model/person/StudentUnderProjectPredicateTest.java b/src/test/java/seedu/address/model/person/StudentUnderProjectPredicateTest.java
deleted file mode 100644
index b010e1d2be3..00000000000
--- a/src/test/java/seedu/address/model/person/StudentUnderProjectPredicateTest.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package seedu.address.model.person;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.junit.jupiter.api.Test;
-
-public class StudentUnderProjectPredicateTest {
-
-    @Test
-    public void equals() {
-        List<String> firstPredicateKeywordList = Collections.singletonList("first");
-        List<String> secondPredicateKeywordList = Arrays.asList("first", "second");
-
-        StudentUnderProjectPredicate firstPredicate = new StudentUnderProjectPredicate(firstPredicateKeywordList);
-        StudentUnderProjectPredicate secondPredicate = new StudentUnderProjectPredicate(secondPredicateKeywordList);
-
-        // same object -> returns true
-        assertTrue(firstPredicate.equals(firstPredicate));
-
-        // same value -> returns true
-        StudentUnderProjectPredicate firstPredicateCopy = new StudentUnderProjectPredicate(firstPredicateKeywordList);
-        assertTrue(firstPredicate.equals(firstPredicateCopy));
-
-        // different types -> returns false;
-        assertFalse(firstPredicate.equals(1));
-
-        // null -> returns false
-        assertFalse(firstPredicate.equals(null));
-
-        // different project -> returns false
-        assertFalse(firstPredicate.equals(secondPredicate));
-    }
-}
diff --git a/src/test/java/seedu/address/model/person/TagContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/TagContainsKeywordsPredicateTest.java
new file mode 100644
index 00000000000..6052dd3c501
--- /dev/null
+++ b/src/test/java/seedu/address/model/person/TagContainsKeywordsPredicateTest.java
@@ -0,0 +1,84 @@
+package seedu.address.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.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.address.testutil.PersonBuilder;
+
+public class TagContainsKeywordsPredicateTest {
+
+    @Test
+    public void equals() {
+        List<String> firstPredicateKeywordList = Collections.singletonList("first");
+        List<String> secondPredicateKeywordList = Arrays.asList("first", "second");
+
+        TagContainsKeywordsPredicate firstPredicate = new TagContainsKeywordsPredicate(firstPredicateKeywordList);
+        TagContainsKeywordsPredicate secondPredicate = new TagContainsKeywordsPredicate(secondPredicateKeywordList);
+
+        // same object -> returns true
+        assertTrue(firstPredicate.equals(firstPredicate));
+
+        // same value -> returns true
+        TagContainsKeywordsPredicate firstPredicateCopy = new TagContainsKeywordsPredicate(firstPredicateKeywordList);
+        assertTrue(firstPredicate.equals(firstPredicateCopy));
+
+        // different types -> returns false;
+        assertFalse(firstPredicate.equals(1));
+
+        // null -> returns false
+        assertFalse(firstPredicate.equals(null));
+
+        // different project -> returns false
+        assertFalse(firstPredicate.equals(secondPredicate));
+    }
+
+    @Test
+    public void test_tagContainsKeywords_returnsTrue() {
+        // One keyword
+        TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.singletonList("CS2103T"));
+        assertTrue(predicate.test(new PersonBuilder().withTags("CS2103T").build()));
+
+        // Multiple keywords
+        predicate = new TagContainsKeywordsPredicate(Arrays.asList("CS2103T", "CS2101"));
+        assertTrue(predicate.test(new PersonBuilder().withTags("CS2103T", "CS2101").build()));
+
+        // Only one matching keyword
+        predicate = new TagContainsKeywordsPredicate(Arrays.asList("CS2103T"));
+        assertTrue(predicate.test(new PersonBuilder().withTags("CS2103T", "CS2101").build()));
+
+        // Mixed-case keywords
+        predicate = new TagContainsKeywordsPredicate(Arrays.asList("cS2103t", "cS2101"));
+        assertTrue(predicate.test(new PersonBuilder().withTags("CS2103T", "CS2101").build()));
+    }
+
+    @Test
+    public void test_tagDoesNotContainKeywords_returnFalse() {
+        // Zero keywords
+        TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.emptyList());
+        assertFalse(predicate.test(new PersonBuilder().withTags("CS2103T").build()));
+
+        // Non-matching keyword
+        predicate = new TagContainsKeywordsPredicate(Arrays.asList("CS2105"));
+        assertFalse(predicate.test(new PersonBuilder().withTags("CS2103T", "CS2101").build()));
+
+        // Keywords match name, email, phone and project but does not match tag
+        predicate = new TagContainsKeywordsPredicate(Arrays.asList("12345", "CS2103T"));
+        assertFalse(predicate.test(new PersonBuilder().withName("CS2103T").withPhone("12345")
+                .withEmail("CS2103T@email.com").withProject("CS2103T").withTags("Y4").build()));
+    }
+
+    @Test
+    public void toStringMethod() {
+        List<String> keywords = List.of("keyword1", "keyword2");
+        TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(keywords);
+        String expected = TagContainsKeywordsPredicate.class.getCanonicalName() + "{Tag=" + keywords + "}";
+        assertEquals(expected, predicate.toString());
+    }
+}


Reviews:
user: login: gekjunxu
body: Overall looks good, other than an area that could be possibly abstracted in the future iteration.
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T14:36:11Z
user: login: weiwong834
body: Overall LGTM
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T14:38:44Z
user: login: swatirajesh277
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T00:26:38Z
user: login: dragonesejosh
body: Few minor nits, but apart from them LGTM!
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T02:30:34Z
user: login: ShiJianXi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T03:54:45Z
user: login: ShiJianXi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T04:04:13Z
user: login: ShiJianXi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T04:20:02Z
user: login: ShiJianXi
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T04:20:24Z

Review comments:
diff_hunk: @@ -21,15 +27,57 @@ public class FilterCommandParser implements Parser<FilterCommand> {
      * @throws ParseException if the user input does not conform the expected format
      */
     public FilterCommand parse(String args) throws ParseException {
-        String trimmedArgs = args.trim();
-        if (trimmedArgs.isEmpty()) {
-            throw new ParseException(
-                    String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        requireNonNull(args);
+        ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PROJECT, PREFIX_TAG);
+
+        if (!hasRequiredPrefix(argumentMultimap)) {
+            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        }
+
+        argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PROJECT, PREFIX_TAG);
+
+        String[] projectKeywords = new String[0];
+        Predicate<Person> filteredStudent = null;
+        if (argumentMultimap.getValue(PREFIX_PROJECT).isPresent()) {
+            checkProjectFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_PROJECT).get().split("\\s+");
+            filteredStudent = new ProjectContainsKeywordsPredicate(Arrays.asList(projectKeywords));
         }
 
-        String[] projectKeyword = trimmedArgs.split("\\s+");
+        if (argumentMultimap.getValue(PREFIX_TAG).isPresent()) {
+            checkTagFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_TAG).get().split("\\s+");
+            filteredStudent = new TagContainsKeywordsPredicate(Arrays.asList(projectKeywords));
+        }
+
+        return new FilterCommand(filteredStudent);
+    }
+
+    private boolean isPrefixPresent(ArgumentMultimap argumentMultimap, Prefix prefix) {
+        return !argumentMultimap.getAllValues(prefix).isEmpty();
+    }
+
+    private void checkProjectFilterCondition(ArgumentMultimap argumentMultimap) throws ParseException {
path: src/main/java/seedu/address/logic/parser/FilterCommandParser.java
user: login: gekjunxu
body: seems to be repeated code here, can consider abstracting out in future iterations. 
Maybe can do something like `checkFilterCondition(argumentmultimap, type PREFIX` )
created_at: 2025-03-26T14:34:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,57 @@
+package seedu.address.model.person;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import seedu.address.commons.util.StringUtil;
+import seedu.address.commons.util.ToStringBuilder;
+import seedu.address.model.tag.Tag;
+
+/**
+ * Tests that a {@code Person}'s {@code Project} matches any of the keywords given.
+ */
+public class TagContainsKeywordsPredicate implements Predicate<Person> {
+
+    private final List<String> keywords;
+
+    public TagContainsKeywordsPredicate(List<String> keywords) {
+        this.keywords = keywords;
+    }
+
+    @Override
+    public boolean test(Person person) {
+        Set<Tag> tags = person.getTags();
+        boolean tagContainsKeywords = false;
path: src/main/java/seedu/address/model/person/TagContainsKeywordsPredicate.java
user: login: swatirajesh277
body: LGTM! Nice implementation of code
created_at: 2025-03-27T00:26:34Z
author_association: NONE
diff_hunk: @@ -1,27 +1,32 @@
 package seedu.address.logic.commands;
 
 import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PROJECT;
+
+import java.util.function.Predicate;
 
 import seedu.address.commons.util.ToStringBuilder;
 import seedu.address.logic.Messages;
 import seedu.address.model.Model;
-import seedu.address.model.person.StudentUnderProjectPredicate;
+import seedu.address.model.person.Person;
 
 /**
- * Filters and list all students in Prof-iler whose project contain the given keyword.
- * Keyword matching is case insensitive.
+ * Filters and list all students in Prof-iler whose project contain the given keyword
+ * or whose tag contain the given keyword.
+ * Keyword 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 for the student who is under the "
             + "specified project based on the keywords (case-insensitive)"
             + "and displays them as a list with index numbers. \n"
-            + "Parameters: KEYWORD\n"
-            + "Example: " + COMMAND_WORD + "Prof-iler";
+            + "Parameters: "
+            + PREFIX_PROJECT + "KEYWORD [MORE KEYWORDS] "
+            + "Example: " + COMMAND_WORD + " pr/ Prof-iler";
path: src/main/java/seedu/address/logic/commands/FilterCommand.java
user: login: dragonesejosh
body: Should probably replace "pr/" with PREFIX_PROJECT
Can also add PREFIX_TAG before "[MORE KEYWORDS]" if it is supposed to be tags, then add to example as well.
created_at: 2025-03-27T02:20:32Z
author_association: NONE
diff_hunk: @@ -21,15 +27,57 @@ public class FilterCommandParser implements Parser<FilterCommand> {
      * @throws ParseException if the user input does not conform the expected format
      */
     public FilterCommand parse(String args) throws ParseException {
-        String trimmedArgs = args.trim();
-        if (trimmedArgs.isEmpty()) {
-            throw new ParseException(
-                    String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        requireNonNull(args);
+        ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PROJECT, PREFIX_TAG);
+
+        if (!hasRequiredPrefix(argumentMultimap)) {
+            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        }
+
+        argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PROJECT, PREFIX_TAG);
+
+        String[] projectKeywords = new String[0];
+        Predicate<Person> filteredStudent = null;
+        if (argumentMultimap.getValue(PREFIX_PROJECT).isPresent()) {
+            checkProjectFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_PROJECT).get().split("\\s+");
+            filteredStudent = new ProjectContainsKeywordsPredicate(Arrays.asList(projectKeywords));
         }
 
-        String[] projectKeyword = trimmedArgs.split("\\s+");
+        if (argumentMultimap.getValue(PREFIX_TAG).isPresent()) {
+            checkTagFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_TAG).get().split("\\s+");
+            filteredStudent = new TagContainsKeywordsPredicate(Arrays.asList(projectKeywords));
+        }
path: src/main/java/seedu/address/logic/parser/FilterCommandParser.java
user: login: dragonesejosh
body: Seems to be repeated code here as well, can consider abstracting out, although not urgent to do so now.
created_at: 2025-03-27T02:21:56Z
author_association: NONE
diff_hunk: @@ -7,13 +7,13 @@
 import seedu.address.commons.util.ToStringBuilder;
 
 /**
- * Tests that a {@code Student}'s {@code Project} matches any of the keywords given.
+ * Tests that a {@code Person}'s {@code Project} matches any of the keywords given.
  */
-public class StudentUnderProjectPredicate implements Predicate<Person> {
+public class ProjectContainsKeywordsPredicate implements Predicate<Person> {
 
     private final List<String> keywords;
path: src/main/java/seedu/address/model/person/ProjectContainsKeywordsPredicate.java
user: login: dragonesejosh
body: Can consider storing as a Set, so that ("first", "second") and ("second", "first") will be the same.
Also in TagContainsKeywordsPredicate.
If it doesn't affect normal usage then it's fine to leave as is
created_at: 2025-03-27T02:27:33Z
author_association: NONE
diff_hunk: @@ -21,15 +27,57 @@ public class FilterCommandParser implements Parser<FilterCommand> {
      * @throws ParseException if the user input does not conform the expected format
      */
     public FilterCommand parse(String args) throws ParseException {
-        String trimmedArgs = args.trim();
-        if (trimmedArgs.isEmpty()) {
-            throw new ParseException(
-                    String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        requireNonNull(args);
+        ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PROJECT, PREFIX_TAG);
+
+        if (!hasRequiredPrefix(argumentMultimap)) {
+            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        }
+
+        argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PROJECT, PREFIX_TAG);
+
+        String[] projectKeywords = new String[0];
+        Predicate<Person> filteredStudent = null;
+        if (argumentMultimap.getValue(PREFIX_PROJECT).isPresent()) {
+            checkProjectFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_PROJECT).get().split("\\s+");
+            filteredStudent = new ProjectContainsKeywordsPredicate(Arrays.asList(projectKeywords));
         }
 
-        String[] projectKeyword = trimmedArgs.split("\\s+");
+        if (argumentMultimap.getValue(PREFIX_TAG).isPresent()) {
+            checkTagFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_TAG).get().split("\\s+");
+            filteredStudent = new TagContainsKeywordsPredicate(Arrays.asList(projectKeywords));
+        }
+
+        return new FilterCommand(filteredStudent);
+    }
+
+    private boolean isPrefixPresent(ArgumentMultimap argumentMultimap, Prefix prefix) {
+        return !argumentMultimap.getAllValues(prefix).isEmpty();
+    }
+
+    private void checkProjectFilterCondition(ArgumentMultimap argumentMultimap) throws ParseException {
path: src/main/java/seedu/address/logic/parser/FilterCommandParser.java
user: login: ShiJianXi
body: Good suggestion, will implement this

created_at: 2025-03-27T03:54:45Z
author_association: NONE
diff_hunk: @@ -1,27 +1,32 @@
 package seedu.address.logic.commands;
 
 import static java.util.Objects.requireNonNull;
+import static seedu.address.logic.parser.CliSyntax.PREFIX_PROJECT;
+
+import java.util.function.Predicate;
 
 import seedu.address.commons.util.ToStringBuilder;
 import seedu.address.logic.Messages;
 import seedu.address.model.Model;
-import seedu.address.model.person.StudentUnderProjectPredicate;
+import seedu.address.model.person.Person;
 
 /**
- * Filters and list all students in Prof-iler whose project contain the given keyword.
- * Keyword matching is case insensitive.
+ * Filters and list all students in Prof-iler whose project contain the given keyword
+ * or whose tag contain the given keyword.
+ * Keyword 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 for the student who is under the "
             + "specified project based on the keywords (case-insensitive)"
             + "and displays them as a list with index numbers. \n"
-            + "Parameters: KEYWORD\n"
-            + "Example: " + COMMAND_WORD + "Prof-iler";
+            + "Parameters: "
+            + PREFIX_PROJECT + "KEYWORD [MORE KEYWORDS] "
+            + "Example: " + COMMAND_WORD + " pr/ Prof-iler";
path: src/main/java/seedu/address/logic/commands/FilterCommand.java
user: login: ShiJianXi
body: Agree, missed that out will implement it!
created_at: 2025-03-27T04:04:13Z
author_association: NONE
diff_hunk: @@ -21,15 +27,57 @@ public class FilterCommandParser implements Parser<FilterCommand> {
      * @throws ParseException if the user input does not conform the expected format
      */
     public FilterCommand parse(String args) throws ParseException {
-        String trimmedArgs = args.trim();
-        if (trimmedArgs.isEmpty()) {
-            throw new ParseException(
-                    String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        requireNonNull(args);
+        ArgumentMultimap argumentMultimap = ArgumentTokenizer.tokenize(args, PREFIX_PROJECT, PREFIX_TAG);
+
+        if (!hasRequiredPrefix(argumentMultimap)) {
+            throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE));
+        }
+
+        argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_PROJECT, PREFIX_TAG);
+
+        String[] projectKeywords = new String[0];
+        Predicate<Person> filteredStudent = null;
+        if (argumentMultimap.getValue(PREFIX_PROJECT).isPresent()) {
+            checkProjectFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_PROJECT).get().split("\\s+");
+            filteredStudent = new ProjectContainsKeywordsPredicate(Arrays.asList(projectKeywords));
         }
 
-        String[] projectKeyword = trimmedArgs.split("\\s+");
+        if (argumentMultimap.getValue(PREFIX_TAG).isPresent()) {
+            checkTagFilterCondition(argumentMultimap);
+            projectKeywords = argumentMultimap.getValue(PREFIX_TAG).get().split("\\s+");
+            filteredStudent = new TagContainsKeywordsPredicate(Arrays.asList(projectKeywords));
+        }
path: src/main/java/seedu/address/logic/parser/FilterCommandParser.java
user: login: ShiJianXi
body: Agree implemented
created_at: 2025-03-27T04:20:02Z
author_association: NONE
diff_hunk: @@ -7,13 +7,13 @@
 import seedu.address.commons.util.ToStringBuilder;
 
 /**
- * Tests that a {@code Student}'s {@code Project} matches any of the keywords given.
+ * Tests that a {@code Person}'s {@code Project} matches any of the keywords given.
  */
-public class StudentUnderProjectPredicate implements Predicate<Person> {
+public class ProjectContainsKeywordsPredicate implements Predicate<Person> {
 
     private final List<String> keywords;
path: src/main/java/seedu/address/model/person/ProjectContainsKeywordsPredicate.java
user: login: ShiJianXi
body: I think I will leave it as List<String> as of now.
created_at: 2025-03-27T04:20:24Z
author_association: NONE

PR comments:
user: login: codecov[bot]
created_at: 2025-03-25T18:08:10Z
body: ## [Codecov](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3) Report
Attention: Patch coverage is `96.15385%` with `2 lines` in your changes missing coverage. Please review.

| [Files with missing lines](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3) | Patch % | Lines |
|---|---|---|
| [...eedu/address/logic/parser/FilterCommandParser.java](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fseedu%2Faddress%2Flogic%2Fparser%2FFilterCommandParser.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3#diff-c3JjL21haW4vamF2YS9zZWVkdS9hZGRyZXNzL2xvZ2ljL3BhcnNlci9GaWx0ZXJDb21tYW5kUGFyc2VyLmphdmE=) | 92.59% | [0 Missing and 2 partials :warning: ](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3) |

| [Files with missing lines](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...va/seedu/address/logic/commands/FilterCommand.java](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fseedu%2Faddress%2Flogic%2Fcommands%2FFilterCommand.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3#diff-c3JjL21haW4vamF2YS9zZWVkdS9hZGRyZXNzL2xvZ2ljL2NvbW1hbmRzL0ZpbHRlckNvbW1hbmQuamF2YQ==) | `100.00% <100.00%> (ø)` | `7.00 <1.00> (+1.00)` | |
| [...model/person/ProjectContainsKeywordsPredicate.java](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fseedu%2Faddress%2Fmodel%2Fperson%2FProjectContainsKeywordsPredicate.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3#diff-c3JjL21haW4vamF2YS9zZWVkdS9hZGRyZXNzL21vZGVsL3BlcnNvbi9Qcm9qZWN0Q29udGFpbnNLZXl3b3Jkc1ByZWRpY2F0ZS5qYXZh) | `100.00% <100.00%> (ø)` | `7.00 <1.00> (?)` | |
| [...ess/model/person/TagContainsKeywordsPredicate.java](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fseedu%2Faddress%2Fmodel%2Fperson%2FTagContainsKeywordsPredicate.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3#diff-c3JjL21haW4vamF2YS9zZWVkdS9hZGRyZXNzL21vZGVsL3BlcnNvbi9UYWdDb250YWluc0tleXdvcmRzUHJlZGljYXRlLmphdmE=) | `100.00% <100.00%> (ø)` | `9.00 <9.00> (?)` | |
| [...eedu/address/logic/parser/FilterCommandParser.java](https://app.codecov.io/gh/AY2425S2-CS2103T-W12-3/tp/pull/126?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fseedu%2Faddress%2Flogic%2Fparser%2FFilterCommandParser.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AY2425S2-CS2103T-W12-3#diff-c3JjL21haW4vamF2YS9zZWVkdS9hZGRyZXNzL2xvZ2ljL3BhcnNlci9GaWx0ZXJDb21tYW5kUGFyc2VyLmphdmE=) | `92.85% <92.59%> (-7.15%)` | `15.00 <14.00> (+12.00)` | :arrow_down: |

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: swatirajesh277
created_at: 2025-03-27T00:26:47Z
body: LGTM
author_association: NONE