From 77995039b7390829696864cfcc73fccf1b91dbff Mon Sep 17 00:00:00 2001 From: Dominic-Khoo Date: Mon, 21 Oct 2024 17:25:38 +0800 Subject: [PATCH] Add find tag feature to FindCommand --- .../address/logic/commands/FindCommand.java | 15 +++-- .../logic/parser/FindCommandParser.java | 26 +++++-- .../model/TagContainsKeywordsPredicate.java | 29 ++++++++ .../typicalPersonsAddressBook.json | 4 +- .../logic/commands/FindCommandTest.java | 67 +++++++++++++------ .../logic/parser/FindCommandParserTest.java | 19 +++++- .../address/testutil/TypicalPersons.java | 2 + 7 files changed, 126 insertions(+), 36 deletions(-) create mode 100644 src/main/java/seedu/address/model/TagContainsKeywordsPredicate.java diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..bc738cda537 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -2,27 +2,29 @@ import static java.util.Objects.requireNonNull; +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.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all persons in address book whose name or tags contain any of the argument keywords. * Keyword matching is case insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names or tags contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + + "Example: " + COMMAND_WORD + " alice bob charlie (for names) OR find t/diabetic t/G6PD-Def"; - private final NameContainsKeywordsPredicate predicate; + private final Predicate predicate; - public FindCommand(NameContainsKeywordsPredicate predicate) { + public FindCommand(Predicate predicate) { this.predicate = predicate; } @@ -40,7 +42,6 @@ public boolean equals(Object other) { return true; } - // instanceof handles nulls if (!(other instanceof FindCommand)) { return false; } diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..cab099c926b 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -3,31 +3,45 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import java.util.Arrays; +import java.util.List; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.TagContainsKeywordsPredicate; /** - * Parses input arguments and creates a new FindCommand object + * Parses input arguments and creates a new FindCommand object. */ public class FindCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the FindCommand * and returns a FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format + * + * @throws ParseException if the user input does not conform to the expected format. */ public FindCommand parse(String args) throws ParseException { String trimmedArgs = args.trim(); + + // If the user provides a tag search prefix (e.g., "t/diabetic"), search by tags + if (trimmedArgs.startsWith("t/")) { + String tagKeywords = trimmedArgs.replace("t/", "").trim(); + + if (tagKeywords.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + List tagKeywordList = Arrays.asList(tagKeywords.split("\\s+")); + return new FindCommand(new TagContainsKeywordsPredicate(tagKeywordList)); + } + + // Otherwise, search by name if (trimmedArgs.isEmpty()) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + List nameKeywords = Arrays.asList(trimmedArgs.split("\\s+")); + return new FindCommand(new NameContainsKeywordsPredicate(nameKeywords)); } - } diff --git a/src/main/java/seedu/address/model/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/TagContainsKeywordsPredicate.java new file mode 100644 index 00000000000..b31470f7dbd --- /dev/null +++ b/src/main/java/seedu/address/model/TagContainsKeywordsPredicate.java @@ -0,0 +1,29 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code Tag} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> person.getTags().stream() + .anyMatch(tag -> tag.tagName.equalsIgnoreCase(keyword))); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((TagContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 82d7ad52e3e..37e72238934 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -39,7 +39,7 @@ "nric": "S1234567E", "address" : "michegan ave", "remark" : "", - "tags" : [ ] + "tags" : [ "Diabetic", "G6PD" ] }, { "name" : "Fiona Kunz", "phone" : "9482427", @@ -47,7 +47,7 @@ "nric": "S1234567F", "address" : "little tokyo", "remark" : "", - "tags" : [ ] + "tags" : [ "Diabetic", "G6PD" ] }, { "name" : "George Best", "phone" : "9482442", diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java index b8b7dbba91a..78297fb7aeb 100644 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java @@ -19,6 +19,7 @@ import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.TagContainsKeywordsPredicate; /** * Contains integration tests (interaction with the Model) for {@code FindCommand}. @@ -29,37 +30,46 @@ public class FindCommandTest { @Test public void equals() { - NameContainsKeywordsPredicate firstPredicate = + NameContainsKeywordsPredicate firstNamePredicate = new NameContainsKeywordsPredicate(Collections.singletonList("first")); - NameContainsKeywordsPredicate secondPredicate = + NameContainsKeywordsPredicate secondNamePredicate = new NameContainsKeywordsPredicate(Collections.singletonList("second")); + TagContainsKeywordsPredicate firstTagPredicate = + new TagContainsKeywordsPredicate(Collections.singletonList("diabetic")); + TagContainsKeywordsPredicate secondTagPredicate = + new TagContainsKeywordsPredicate(Collections.singletonList("allergy")); - FindCommand findFirstCommand = new FindCommand(firstPredicate); - FindCommand findSecondCommand = new FindCommand(secondPredicate); + FindCommand findFirstNameCommand = new FindCommand(firstNamePredicate); + FindCommand findSecondNameCommand = new FindCommand(secondNamePredicate); + FindCommand findFirstTagCommand = new FindCommand(firstTagPredicate); + FindCommand findSecondTagCommand = new FindCommand(secondTagPredicate); // same object -> returns true - assertTrue(findFirstCommand.equals(findFirstCommand)); + assertTrue(findFirstNameCommand.equals(findFirstNameCommand)); // same values -> returns true - FindCommand findFirstCommandCopy = new FindCommand(firstPredicate); - assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + FindCommand findFirstNameCommandCopy = new FindCommand(firstNamePredicate); + assertTrue(findFirstNameCommand.equals(findFirstNameCommandCopy)); // different types -> returns false - assertFalse(findFirstCommand.equals(1)); + assertFalse(findFirstNameCommand.equals(1)); // null -> returns false - assertFalse(findFirstCommand.equals(null)); + assertFalse(findFirstNameCommand.equals(null)); // different person -> returns false - assertFalse(findFirstCommand.equals(findSecondCommand)); + assertFalse(findFirstNameCommand.equals(findSecondNameCommand)); + + // different predicate type -> returns false + assertFalse(findFirstNameCommand.equals(findFirstTagCommand)); } @Test public void execute_zeroKeywords_noPersonFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); - NameContainsKeywordsPredicate predicate = preparePredicate(" "); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); + NameContainsKeywordsPredicate namePredicate = prepareNamePredicate(" "); + FindCommand command = new FindCommand(namePredicate); + expectedModel.updateFilteredPersonList(namePredicate); assertCommandSuccess(command, model, expectedMessage, expectedModel); assertEquals(Collections.emptyList(), model.getFilteredPersonList()); } @@ -67,25 +77,42 @@ public void execute_zeroKeywords_noPersonFound() { @Test public void execute_multipleKeywords_multiplePersonsFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); - NameContainsKeywordsPredicate predicate = preparePredicate("Kurz Elle Kunz"); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); + NameContainsKeywordsPredicate namePredicate = prepareNamePredicate("Kurz Elle Kunz"); + FindCommand command = new FindCommand(namePredicate); + expectedModel.updateFilteredPersonList(namePredicate); assertCommandSuccess(command, model, expectedMessage, expectedModel); assertEquals(Arrays.asList(CARL, ELLE, FIONA), model.getFilteredPersonList()); } + @Test + public void execute_tagSearch_personsWithTagsFound() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 2); + TagContainsKeywordsPredicate tagPredicate = prepareTagPredicate("Diabetic G6PD"); + FindCommand command = new FindCommand(tagPredicate); + expectedModel.updateFilteredPersonList(tagPredicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ELLE, FIONA), model.getFilteredPersonList()); + } + @Test public void toStringMethod() { - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Arrays.asList("keyword")); - FindCommand findCommand = new FindCommand(predicate); - String expected = FindCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; + NameContainsKeywordsPredicate namePredicate = new NameContainsKeywordsPredicate(Arrays.asList("keyword")); + FindCommand findCommand = new FindCommand(namePredicate); + String expected = FindCommand.class.getCanonicalName() + "{predicate=" + namePredicate + "}"; assertEquals(expected, findCommand.toString()); } /** * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. */ - private NameContainsKeywordsPredicate preparePredicate(String userInput) { + private NameContainsKeywordsPredicate prepareNamePredicate(String userInput) { return new NameContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); } + + /** + * Parses {@code userInput} into a {@code TagContainsKeywordsPredicate}. + */ + private TagContainsKeywordsPredicate prepareTagPredicate(String userInput) { + return new TagContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); + } } diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java index d92e64d12f9..28bef2f7ed6 100644 --- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java @@ -10,6 +10,7 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.TagContainsKeywordsPredicate; public class FindCommandParserTest { @@ -21,7 +22,7 @@ public void parse_emptyArg_throwsParseException() { } @Test - public void parse_validArgs_returnsFindCommand() { + public void parse_validNameArgs_returnsFindCommand() { // no leading and trailing whitespaces FindCommand expectedFindCommand = new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); @@ -31,4 +32,20 @@ public void parse_validArgs_returnsFindCommand() { assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedFindCommand); } + @Test + public void parse_validTagArgs_returnsFindCommand() { + // Test valid tag-based search + FindCommand expectedFindCommand = + new FindCommand(new TagContainsKeywordsPredicate(Arrays.asList("Diabetic", "G6PD"))); + assertParseSuccess(parser, "t/Diabetic t/G6PD", expectedFindCommand); + + // multiple whitespaces between tag keywords + assertParseSuccess(parser, " \n t/Diabetic \n \t t/G6PD \t", expectedFindCommand); + } + + @Test + public void parse_invalidArgs_throwsParseException() { + // Test invalid argument (e.g., empty string) + assertParseFailure(parser, "t/", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } } diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index a163a72694c..ff53bff1c9c 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -68,6 +68,7 @@ public class TypicalPersons { .withNric("S1234567E") .withEmail("werner@example.com") .withAddress("michegan ave") + .withTags("Diabetic", "G6PD") .withAppointment("10-11-2024 05:31") .build(); public static final Person FIONA = new PersonBuilder() @@ -76,6 +77,7 @@ public class TypicalPersons { .withNric("S1234567F") .withEmail("lydia@example.com") .withAddress("little tokyo") + .withTags("Diabetic", "G6PD") .withAppointment("08-11-2024 13:22") .build(); public static final Person GEORGE = new PersonBuilder()