diff --git a/build.gradle b/build.gradle index 5420e266a91..0a32ffdedde 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,10 @@ checkstyle { toolVersion = '8.29' } +run { + enableAssertions = true +} + test { useJUnitPlatform() finalizedBy jacocoTestReport diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index e3bc5f3f4dc..1d591a3f91d 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -211,6 +211,138 @@ The following sequence diagram shows how eat recipe operation works when `execut ##### Aspect 1: Concern while adding a new feature * Workflow must be consistent with other listing commands e.g. fridge and calories. +### Add Ingredient feature + +#### Implementation +This feature allows users to add ingredients into the fridge. + +Substitutability is used in Command and Parser: +* `AddIngredientCommand` extends `Command` +* `AddIngredientCommandParser` implements `Parser` + +Given below is an example usage scenario and how the mechanism behaves at each step. + +![AddIngredientSequence](images/AddIngredientSequence.png) + +Step 1: +User inputs the add ingredient command to add ingredients into the fridge. + +Step 2: +After successful parsing of user input, the `AddIngredientCommand#execute(Model model)` method is called. + +Step 3: +The ingredients that the user has input will be saved into Wishful Shrinking's fridge of ingredients. + +Step 4: +After the successful adding of the ingredient, a `CommandResult` object is instantiated and returned to `LogicManager`. + +#### Design Considerations +##### Aspect 1: Concern while adding a new feature +* Workflow must be consistent with other adding commands e.g. add recipe and eat recipe for consumption. + +##### Aspect 2: How do we successfully parse the ingredients the user has added with the optional ingredient quantity +* **Alternative 1 (current choice):** Add a quantity field in the Ingredient class as well as a IngredientParser class that parses the user ingredients that the user has input into an arraylist of Ingredient objects + * Pros: Easy to implement. + * Cons: The parser may confuse ingredients that have prefixes that Wishful Shrinking uses to identify fields in the names, eg "-" or "," + +### Search Recipe feature + +#### Implementation +This feature allows users search for recipes in the recipe list based on the name, tags or ingredients. + +Substitutability is used in Command and Parser: +* `SearchRecipeCommand` extends `Command` +* `SearchRecipeCommandParser` implements `Parser` + +Given below is an example usage scenario and how the mechanism behaves at each step. + +![SearchRecipeSequence](images/SearchRecipeSequence.png) + +Step 1: +User inputs the search recipe command to search for the recipes they want. + +Step 2: +After successful parsing of user input, the `SearchRecipeCommand#execute(Model model)` method is called. + +Step 3: +The list of recipes that fit the user's search will be returned to the user. + +Step 4: +After the successful searching of the recipes, a `CommandResult` object is instantiated and returned to `LogicManager`. + +#### Design Considerations +##### Aspect 1: Concern while adding a new feature +* Workflow must be consistent with other searching commands e.g. search recipe. + +##### Aspect 2: How do we successfully search and filter the recipes based on the user' search +* **Alternative 1 (current choice):** User can only search for recipes based on one fields at a time + * Pros: Easy to implement. + * Cons: User's cannot filter the recipes by two or three fields at once + +* **Alternative 2:** User can only search for recipes by all fields at once + * Pros: Harder to implement. + * Cons: User's can filter the recipes by two or three fields at once + +### Delete Consumption feature + +#### Implementation +This feature allows users to delete the recipes they have eaten in the calorie tracker. + +Substitutability is used in Command and Parser: +* `DeleteConsumptionCommand` extends `Command` +* `DeleteConsumptionCommandParser` implements `Parser` + +Given below is an example usage scenario and how the mechanism behaves at each step. + +![DeleteConsumptionSequence](images/DeleteConsumptionSequence.png) + +Step 1: +User inputs the delete consumption command to delete the recipes eaten in the calorie tracker. + +Step 2: +After successful parsing of user input, the `DeleteConsumptionCommand#execute(Model model)` method is called. + +Step 3: +The recipe that the user has specified will be deleted from the consumption list. + +Step 4: +After the successful deleting of recipes, a `CommandResult` object is instantiated and returned to `LogicManager`. + +#### Design Considerations +##### Aspect 1: Concern while adding a new feature +* Workflow must be consistent with other deleting commands e.g. delete recipe and delete ingredient. + +### Recommend feature + +#### Implementation +This feature allows users to get recommended recipes that they are able to make with the ingredients in their fridge. + +Substitutability is used in Command: +* `RecommendCommand` extends `Command` + +Given below is an example usage scenario and how the mechanism behaves at each step. + +![RecommendSequence](images/RecommendSequence.png) + +Step 1: +User inputs the recommend command to get the recommended recipes. + +Step 2: +After successful parsing of user input, the `RecommendCommand#execute(Model model)` method is called. + +Step 3: +The list of recommended recipes will be returned to the user. + +Step 4: +After the successful recommending of recipes, a `CommandResult` object is instantiated and returned to `LogicManager`. + +#### Design Considerations +##### Aspect : How do we quickly and accurately compare ingredients in each recipe and the user's fridge +* **Alternative 1 (current choice):** Compare the exact ingredients in each recipe to the users ingredients in the fridge + * Pros: Easy to implement + * Cons: Slow to compare, the ingredients might not match if the spellings are different, or if the ingredient has similar names, eg mozarella and cheese. Other than that, if users do not input basic ingredients into their fridge, eg salt and pepper, the recipe might not get recommended to them. + + -------------------------------------------------------------------------------------------------------------------- ## **Documentation, logging, testing, configuration, dev-ops** diff --git a/docs/diagrams/AddIngredientSequence.puml b/docs/diagrams/AddIngredientSequence.puml new file mode 100644 index 00000000000..8e420d43e4f --- /dev/null +++ b/docs/diagrams/AddIngredientSequence.puml @@ -0,0 +1,89 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":WishfulShrinkingParser" as WishfulShrinkingParser LOGIC_COLOR +participant ":AddIngredientCommandParser" as AddIngredientCommandParser LOGIC_COLOR +participant "command :AddIngredientCommand" as AddIngredientCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":WishfulShrinking" as WishfulShrinking MODEL_COLOR +participant ":UniqueIngredientList" as UniqueIngredientList MODEL_COLOR +end box + +[-> LogicManager : execute("addF i/tomato") +activate LogicManager + +LogicManager -> WishfulShrinkingParser : parseCommand("addF i/tomato") +activate WishfulShrinkingParser + +create AddIngredientCommandParser +WishfulShrinkingParser -> AddIngredientCommandParser +activate AddIngredientCommandParser + +AddIngredientCommandParser --> WishfulShrinkingParser +deactivate AddIngredientCommandParser + +WishfulShrinkingParser -> AddIngredientCommandParser : parse("i/tomato") +activate AddIngredientCommandParser + +create AddIngredientCommand +AddIngredientCommandParser -> AddIngredientCommand +activate AddIngredientCommand + +AddIngredientCommand --> AddIngredientCommandParser : command +deactivate AddIngredientCommand + +AddIngredientCommandParser --> WishfulShrinkingParser : command +deactivate AddIngredientCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddIngredientCommandParser -[hidden]-> WishfulShrinkingParser +destroy AddIngredientCommandParser + +WishfulShrinkingParser --> LogicManager : command +deactivate WishfulShrinkingParser + +LogicManager -> AddIngredientCommand : execute() +activate AddIngredientCommand + +AddIngredientCommand -> Model : updateFilteredIngredientList(predicate) +activate Model + +Model --> AddIngredientCommand +deactivate Model + +AddIngredientCommand -> Model : addIngredient("tomato") +activate Model + +Model -> WishfulShrinking : addIngredient("tomato") +activate WishfulShrinking + +WishfulShrinking --> UniqueIngredientList: add("tomato") +activate UniqueIngredientList + +UniqueIngredientList --> WishfulShrinking +deactivate UniqueIngredientList + +WishfulShrinking --> Model +deactivate WishfulShrinking + +Model --> AddIngredientCommand +deactivate Model + +create CommandResult +AddIngredientCommand -> CommandResult +activate CommandResult + +CommandResult --> AddIngredientCommand +deactivate CommandResult + +AddIngredientCommand --> LogicManager : result +deactivate AddIngredientCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteConsumptionSequence.puml b/docs/diagrams/DeleteConsumptionSequence.puml new file mode 100644 index 00000000000..dbdd4bb844a --- /dev/null +++ b/docs/diagrams/DeleteConsumptionSequence.puml @@ -0,0 +1,89 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":WishfulShrinkingParser" as WishfulShrinkingParser LOGIC_COLOR +participant ":DeleteConsumptionCommandParser" as DeleteConsumptionCommandParser LOGIC_COLOR +participant "d:DeleteConsumptionCommand" as DeleteConsumptionCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":WishfulShrinking" as WishfulShrinking MODEL_COLOR +participant ":ConsumptionList" as ConsumptionList MODEL_COLOR +end box + +[-> LogicManager : execute("deleteC 1") +activate LogicManager + +LogicManager -> WishfulShrinkingParser : parseCommand("deleteC 1") +activate WishfulShrinkingParser + +create DeleteConsumptionCommandParser +WishfulShrinkingParser -> DeleteConsumptionCommandParser +activate DeleteConsumptionCommandParser + +DeleteConsumptionCommandParser --> WishfulShrinkingParser +deactivate DeleteConsumptionCommandParser + +WishfulShrinkingParser -> DeleteConsumptionCommandParser : parse("1") +activate DeleteConsumptionCommandParser + +create DeleteConsumptionCommand +DeleteConsumptionCommandParser -> DeleteConsumptionCommand +activate DeleteConsumptionCommand + +DeleteConsumptionCommand --> DeleteConsumptionCommandParser : command +deactivate DeleteConsumptionCommand + +DeleteConsumptionCommandParser --> WishfulShrinkingParser : command +deactivate DeleteConsumptionCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteConsumptionCommandParser -[hidden]-> WishfulShrinkingParser +destroy DeleteConsumptionCommandParser + +WishfulShrinkingParser --> LogicManager : command +deactivate WishfulShrinkingParser + +LogicManager -> DeleteConsumptionCommand : execute() +activate DeleteConsumptionCommand + +DeleteConsumptionCommand -> Model : getFilteredConsumptionList() +activate Model + +Model --> DeleteConsumptionCommand +deactivate Model + +DeleteConsumptionCommand -> Model : deleteConsumption(target) +activate Model + +Model -> WishfulShrinking : removeConsumption(key) +activate WishfulShrinking + +WishfulShrinking --> ConsumptionList: remove(toRemove) +activate ConsumptionList + +ConsumptionList --> WishfulShrinking +deactivate ConsumptionList + +WishfulShrinking --> Model +deactivate WishfulShrinking + +Model --> DeleteConsumptionCommand +deactivate Model + +create CommandResult +DeleteConsumptionCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteConsumptionCommand +deactivate CommandResult + +DeleteConsumptionCommand --> LogicManager : result +deactivate DeleteConsumptionCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/RecommendSequence.puml b/docs/diagrams/RecommendSequence.puml new file mode 100644 index 00000000000..b1e9bc5117d --- /dev/null +++ b/docs/diagrams/RecommendSequence.puml @@ -0,0 +1,58 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":WishfulShrinkingParser" as WishfulShrinkingParser LOGIC_COLOR +participant "command :RecommendCommand" as RecommendCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute(recommend) +activate LogicManager + +LogicManager -> WishfulShrinkingParser : parseCommand(recommend) +activate WishfulShrinkingParser + +create RecommendCommand +WishfulShrinkingParser -> RecommendCommand +activate RecommendCommand + +RecommendCommand --> WishfulShrinkingParser : command +deactivate RecommendCommand + +WishfulShrinkingParser --> LogicManager : command +deactivate WishfulShrinkingParser + +LogicManager -> RecommendCommand : execute() +activate RecommendCommand + +RecommendCommand -> Model : updateFilteredRecipeList(predicate) +activate Model + +Model --> RecommendCommand +deactivate Model + +RecommendCommand -> Model : getFilteredRecipeList() +activate Model + +Model --> RecommendCommand +deactivate Model + +create CommandResult +RecommendCommand -> CommandResult +activate CommandResult + +CommandResult --> RecommendCommand +deactivate CommandResult + +RecommendCommand --> LogicManager : result +deactivate RecommendCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/SearchRecipeSequence.puml b/docs/diagrams/SearchRecipeSequence.puml new file mode 100644 index 00000000000..32d7766c91a --- /dev/null +++ b/docs/diagrams/SearchRecipeSequence.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":WishfulShrinkingParser" as WishfulShrinkingParser LOGIC_COLOR +participant ":SearchRecipeCommandParser" as SearchRecipeCommandParser LOGIC_COLOR +participant "command :SearchRecipeCommand" as SearchRecipeCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("searchR n/burger") +activate LogicManager + +LogicManager -> WishfulShrinkingParser : parseCommand("searchR n/burger") +activate WishfulShrinkingParser + +create SearchRecipeCommandParser +WishfulShrinkingParser -> SearchRecipeCommandParser +activate SearchRecipeCommandParser + +SearchRecipeCommandParser --> WishfulShrinkingParser +deactivate SearchRecipeCommandParser + +WishfulShrinkingParser -> SearchRecipeCommandParser : parse("n/burger") +activate SearchRecipeCommandParser + +create SearchRecipeCommand +SearchRecipeCommandParser -> SearchRecipeCommand +activate SearchRecipeCommand + +SearchRecipeCommand --> SearchRecipeCommandParser : command +deactivate SearchRecipeCommand + +SearchRecipeCommandParser --> WishfulShrinkingParser : command +deactivate SearchRecipeCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SearchRecipeCommandParser -[hidden]-> WishfulShrinkingParser +destroy SearchRecipeCommandParser + +WishfulShrinkingParser --> LogicManager : command +deactivate WishfulShrinkingParser + +LogicManager -> SearchRecipeCommand : execute() +activate SearchRecipeCommand + +SearchRecipeCommand -> Model : updateFilteredRecipeList(predicate) +activate Model + +Model --> SearchRecipeCommand +deactivate Model + +create CommandResult +SearchRecipeCommand -> CommandResult +activate CommandResult + +CommandResult --> SearchRecipeCommand +deactivate CommandResult + +SearchRecipeCommand --> LogicManager : result +deactivate SearchRecipeCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/images/AddIngredientSequence.png b/docs/images/AddIngredientSequence.png new file mode 100644 index 00000000000..570137fcdea Binary files /dev/null and b/docs/images/AddIngredientSequence.png differ diff --git a/docs/images/DeleteConsumptionSequence.png b/docs/images/DeleteConsumptionSequence.png new file mode 100644 index 00000000000..7ce481b5ada Binary files /dev/null and b/docs/images/DeleteConsumptionSequence.png differ diff --git a/docs/images/RecommendSequence.png b/docs/images/RecommendSequence.png new file mode 100644 index 00000000000..615fd92fe27 Binary files /dev/null and b/docs/images/RecommendSequence.png differ diff --git a/docs/images/SearchRecipeSequence.png b/docs/images/SearchRecipeSequence.png new file mode 100644 index 00000000000..9608473d520 Binary files /dev/null and b/docs/images/SearchRecipeSequence.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 68ee3bc49b6..41b8b2cdcc9 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index e5a3f452a1e..616a0fcc92d 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -1,5 +1,6 @@ package seedu.address.logic; +import java.io.IOException; import java.nio.file.Path; import javafx.collections.ObservableList; @@ -23,7 +24,7 @@ public interface Logic { * @throws CommandException If an error occurs during command execution. * @throws ParseException If an error occurs during parsing. */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws CommandException, ParseException, IOException; /** * Returns the WishfulShrinking. diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 8586aca843f..84b7e14dea1 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -40,7 +40,7 @@ public LogicManager(Model model, Storage storage) { } @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { + public CommandResult execute(String commandText) throws CommandException, ParseException, IOException { logger.info("----------------[USER COMMAND][" + commandText + "]"); CommandResult commandResult; diff --git a/src/main/java/seedu/address/logic/commands/ListRecipesCommand.java b/src/main/java/seedu/address/logic/commands/ListRecipesCommand.java index 5a21f43fae8..38a0049df0e 100644 --- a/src/main/java/seedu/address/logic/commands/ListRecipesCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListRecipesCommand.java @@ -24,7 +24,8 @@ public CommandResult execute(Model model) { ObservableList recipes = model.getFilteredRecipeList(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < recipes.size(); i++) { - builder.append((i + 1) + ". " + recipes.get(i).toString() + "\n"); + assert(recipes.get(i).getName().toString().length() != 0); + builder.append((i + 1) + ". " + recipes.get(i).getName() + "\n"); } return new CommandResult(MESSAGE_SUCCESS + builder.toString(), false, false, true, false, false, false, false); diff --git a/src/main/java/seedu/address/logic/commands/RecommendCommand.java b/src/main/java/seedu/address/logic/commands/RecommendCommand.java index d3fc3241632..57f3a248cff 100644 --- a/src/main/java/seedu/address/logic/commands/RecommendCommand.java +++ b/src/main/java/seedu/address/logic/commands/RecommendCommand.java @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javafx.collections.ObservableList; import seedu.address.model.Model; @@ -21,17 +23,20 @@ public class RecommendCommand extends Command { public static final String MESSAGE_SUCCESS = "Recommended recipes (according to fridge)" + "\n"; + private static Logger logger = Logger.getLogger("RecommendLogger"); @Override public CommandResult execute(Model model) { + logger.log(Level.INFO, "going to start recommending"); requireNonNull(model); RecommendPredicate pred = new RecommendPredicate(getIngredients(model)); model.updateFilteredRecipeList(pred); ObservableList recipes = model.getFilteredRecipeList(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < recipes.size(); i++) { - builder.append((i + 1) + ". " + recipes.get(i).toString() + "\n"); + builder.append((i + 1) + ". " + recipes.get(i).getName() + "\n"); } + logger.log(Level.INFO, "end of recommending"); return new CommandResult(MESSAGE_SUCCESS + builder.toString(), false, false, true, false, false, false, false); } diff --git a/src/main/java/seedu/address/logic/parser/AddIngredientCommandParser.java b/src/main/java/seedu/address/logic/parser/AddIngredientCommandParser.java index 03f9b7dd725..63a6c4774a3 100644 --- a/src/main/java/seedu/address/logic/parser/AddIngredientCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddIngredientCommandParser.java @@ -29,7 +29,8 @@ public AddIngredientCommand parse(String args) throws ParseException { } String ingredientString = ParserUtil.parseIngredient(argMultimap.getValue(PREFIX_INGREDIENT).get()); - System.out.println(ingredientString); + assert ingredientString.length() != 0 : "ingredientString should not be empty"; + ArrayList ingredients = IngredientParser.parse(ingredientString); return new AddIngredientCommand(ingredients); diff --git a/src/main/java/seedu/address/logic/parser/AddRecipeCommandParser.java b/src/main/java/seedu/address/logic/parser/AddRecipeCommandParser.java index 4913827a3ca..2a825892f51 100644 --- a/src/main/java/seedu/address/logic/parser/AddRecipeCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddRecipeCommandParser.java @@ -8,6 +8,7 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_RECIPE_IMAGE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.io.IOException; import java.util.ArrayList; import java.util.Set; import java.util.stream.Stream; @@ -29,7 +30,7 @@ public class AddRecipeCommandParser implements Parser { * and returns an AddRecipeCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public AddRecipeCommand parse(String args) throws ParseException { + public AddRecipeCommand parse(String args) throws ParseException, IOException { ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_INGREDIENT, PREFIX_CALORIES, PREFIX_INSTRUCTION, PREFIX_RECIPE_IMAGE, PREFIX_TAG); @@ -48,10 +49,40 @@ public AddRecipeCommand parse(String args) throws ParseException { String instruction = argMultimap.getValue(PREFIX_INSTRUCTION).get(); String recipeImage = argMultimap.getValue(PREFIX_RECIPE_IMAGE).get(); + assert(recipeImage.length() != 0); + if (recipeImage.length() < 13) { + recipeImage = "images/default.jpg"; + } else if (!recipeImage.substring(0, 6).equals("images") && !recipeImage.substring(0, 4).equals("http")) { + recipeImage = "images/default.jpg"; + } + /* + String filename = ""; + for (int i = recipeImage.length() - 1; i >= 0; i--) { + if (recipeImage.charAt(i) == '/') { + filename = recipeImage.substring(i + 1); + break; + } + } + URL url = new URL(recipeImage); + InputStream in = new BufferedInputStream(url.openStream()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int n = 0; + while (-1 != (n = in.read(buf))) { + out.write(buf, 0, n); + } + out.close(); + in.close(); + byte[] response = out.toByteArray(); + recipeImage = this.getClass().getResource("/images").getPath() + filename; + FileOutputStream fos = new FileOutputStream(recipeImage); + fos.write(response); + fos.close(); + } + */ Recipe recipe = new Recipe(name, instruction, recipeImage, ingredients, calories, tagList); - return new AddRecipeCommand(recipe); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteConsumptionCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteConsumptionCommandParser.java index 85ffc895777..28e95bc84a8 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteConsumptionCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteConsumptionCommandParser.java @@ -19,6 +19,7 @@ public class DeleteConsumptionCommandParser implements Parser 0 : "Index should be bigger than 0"; return new DeleteConsumptionCommand(index); } catch (ParseException pe) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, diff --git a/src/main/java/seedu/address/logic/parser/DeleteRecipeCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteRecipeCommandParser.java index c5c3521251c..f960074d05b 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteRecipeCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteRecipeCommandParser.java @@ -19,6 +19,7 @@ public class DeleteRecipeCommandParser implements Parser { public DeleteRecipeCommand parse(String args) throws ParseException { try { Index index = ParserUtil.parseIndex(args); + assert(index.getZeroBased() >= 0); return new DeleteRecipeCommand(index); } catch (ParseException pe) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index d6551ad8e3f..58880f07648 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -1,5 +1,7 @@ package seedu.address.logic.parser; +import java.io.IOException; + import seedu.address.logic.commands.Command; import seedu.address.logic.parser.exceptions.ParseException; @@ -12,5 +14,5 @@ public interface Parser { * Parses {@code userInput} into a command and returns it. * @throws ParseException if {@code userInput} does not conform the expected format */ - T parse(String userInput) throws ParseException; + T parse(String userInput) throws ParseException, IOException; } diff --git a/src/main/java/seedu/address/logic/parser/SearchIngredientCommandParser.java b/src/main/java/seedu/address/logic/parser/SearchIngredientCommandParser.java index a66fd073de9..bbf035b1426 100644 --- a/src/main/java/seedu/address/logic/parser/SearchIngredientCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/SearchIngredientCommandParser.java @@ -32,6 +32,7 @@ public SearchIngredientCommand parse(String args) throws ParseException { } String[] ingredientKeywords = trimmedArgs.split("\\s+"); + assert ingredientKeywords.length != 0 : "ingredientKeywords should not be empty"; return new SearchIngredientCommand(new IngredientContainsKeywordsPredicate(Arrays.asList(ingredientKeywords))); } diff --git a/src/main/java/seedu/address/logic/parser/SearchRecipeCommandParser.java b/src/main/java/seedu/address/logic/parser/SearchRecipeCommandParser.java index 2171d298a48..6131dbbb484 100644 --- a/src/main/java/seedu/address/logic/parser/SearchRecipeCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/SearchRecipeCommandParser.java @@ -76,12 +76,15 @@ private RecipeContainsKeywordsPredicate parsePredicates(String trimmedName, Stri if (!trimmedName.isEmpty()) { String[] nameKeywords = trimmedName.split("\\s+"); + assert nameKeywords.length != 0 : "nameKeywords should not be empty"; return new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords)); } else if (!trimmedTag.isEmpty()) { String[] tagKeywords = trimmedTag.split("\\s+"); + assert tagKeywords.length != 0 : "tagKeywords should not be empty"; return new TagContainsKeywordsPredicate(Arrays.asList(tagKeywords)); } else if (!trimmedIngredient.isEmpty()) { String[] ingredientKeywords = trimmedIngredient.split("\\s+"); + assert ingredientKeywords.length != 0 : "ingredientKeywords should not be empty"; return new RecipeContainsIngredientsPredicate(Arrays.asList(ingredientKeywords)); } else { throw new ParseException( diff --git a/src/main/java/seedu/address/logic/parser/WishfulShrinkingParser.java b/src/main/java/seedu/address/logic/parser/WishfulShrinkingParser.java index d7e0d9da4f0..8f5b5efba89 100644 --- a/src/main/java/seedu/address/logic/parser/WishfulShrinkingParser.java +++ b/src/main/java/seedu/address/logic/parser/WishfulShrinkingParser.java @@ -3,6 +3,7 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; +import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,7 +44,7 @@ public class WishfulShrinkingParser { * @return the command based on the user input * @throws ParseException if the user input does not conform the expected format */ - public Command parseCommand(String userInput) throws ParseException { + public Command parseCommand(String userInput) throws ParseException, IOException { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); if (!matcher.matches()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); diff --git a/src/main/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicate.java b/src/main/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicate.java index 988714b77b2..47557b6f357 100644 --- a/src/main/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicate.java +++ b/src/main/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicate.java @@ -19,8 +19,14 @@ public RecipeContainsIngredientsPredicate(List keywords) { @Override public boolean test(Recipe recipe) { String ingredients = recipe.getIngredient().stream().map(Object::toString).collect(Collectors.joining(",")); + + if (keywords.isEmpty()) { + return false; + } + return keywords.stream() .allMatch(keyword -> StringUtil.containsWordIgnoreCase(ingredients, keyword)); + } @Override diff --git a/src/main/java/seedu/address/model/recipe/RecommendPredicate.java b/src/main/java/seedu/address/model/recipe/RecommendPredicate.java index 6b434a56208..bdba328cb38 100644 --- a/src/main/java/seedu/address/model/recipe/RecommendPredicate.java +++ b/src/main/java/seedu/address/model/recipe/RecommendPredicate.java @@ -25,6 +25,11 @@ public boolean test(Recipe recipe) { .collect(toList()); String str = keywords.stream().map(Object::toString).map(x -> x.replaceAll("\\s+", "")) .collect(Collectors.joining(" ")); + + if (keywords.isEmpty()) { + return false; + } + return ingredients.stream() .allMatch(ingredient -> StringUtil.containsWordIgnoreCase(str, ingredient)); } diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index c3d211647a4..f003a2ecdc2 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -74,7 +74,7 @@ public static Recipe[] getSampleRecipes() { } private static String getRecipeName(String str) { - return str.substring(10, str.length() - 3); + return str.substring(10, str.length() - 2); } private static String getTag(String str) { @@ -86,7 +86,7 @@ private static String getRecipeInstructions(String str) { } private static ArrayList getRecipeIngredients(String str) { - String ingts = str.substring(16, str.length() - 3); + String ingts = str.substring(16, str.length() - 2); String[] ingredients = ingts.split(", "); ArrayList ingredientList = new ArrayList<>(); for (String ingredient: ingredients) { diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 110fcc729c9..025eb4b8c2c 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,5 +1,7 @@ package seedu.address.ui; +import java.io.IOException; + import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; @@ -35,8 +37,11 @@ public CommandBox(CommandExecutor commandExecutor) { * Handles the Enter button pressed event. */ @FXML - private void handleCommandEntered() { + private void handleCommandEntered() throws IOException, CommandException, ParseException { try { + commandExecutor.execute(commandTextField.getText()); + commandTextField.setText(""); + } catch (IOException e) { CommandResult commandResult = commandExecutor.execute(commandTextField.getText()); if (commandResult.isEditRecipe() || commandResult.isEditIngredient()) { commandTextField.setText(commandResult.getCommandBox()); @@ -78,7 +83,7 @@ public interface CommandExecutor { * * @see seedu.address.logic.Logic#execute(String) */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws CommandException, ParseException, IOException; } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index c1bba5d3039..00eeecc3add 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,5 +1,6 @@ package seedu.address.ui; +import java.io.IOException; import java.util.logging.Logger; import com.jfoenix.assets.JFoenixResources; @@ -211,7 +212,7 @@ public RecipeListPanel getRecipeListPanel() { * * @see seedu.address.logic.Logic#execute(String) */ - private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + private CommandResult executeCommand(String commandText) throws CommandException, ParseException, IOException { try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); @@ -241,7 +242,7 @@ private CommandResult executeCommand(String commandText) throws CommandException } return commandResult; - } catch (CommandException | ParseException e) { + } catch (CommandException | ParseException | IOException e) { logger.info("Invalid command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); throw e; diff --git a/src/main/resources/images/default.jpg b/src/main/resources/images/default.jpg new file mode 100644 index 00000000000..d47aec45120 Binary files /dev/null and b/src/main/resources/images/default.jpg differ diff --git a/src/main/resources/images/defaultrecipe.jpg b/src/main/resources/images/defaultrecipe.jpg new file mode 100644 index 00000000000..0f3062c6cf8 Binary files /dev/null and b/src/main/resources/images/defaultrecipe.jpg differ diff --git a/src/test/data/JsonSerializableWishfulShrinkingTest/typicalRecipesWishfulShrinking.json b/src/test/data/JsonSerializableWishfulShrinkingTest/typicalRecipesWishfulShrinking.json index 1e2bb8d9dea..a2c74bea834 100644 --- a/src/test/data/JsonSerializableWishfulShrinkingTest/typicalRecipesWishfulShrinking.json +++ b/src/test/data/JsonSerializableWishfulShrinkingTest/typicalRecipesWishfulShrinking.json @@ -49,7 +49,7 @@ "quantity" : "15 Ounce " }], "calories": "40", - "tagged": [ "healthy" ] + "tagged": [ "healthy", "low calories" ] }, { "name": "Easter Eggs", "instruction": "In a large saucepan, melt butter over low heat. Add marshmallows and stir until melted. Remove from heat, then add rice cereal and stir until well coated. Lightly spray interior of the plastic eggs with non-stick cooking spray. If mixture is too sticky, you can also spray your hands. Fill both sides of the plastic egg with rice cereal mixture, slightly over-filling one side. Press chocolate egg in the center on one side of the egg, then close the plastic egg to shape it. (It should be full enough to meet with a little resistance as you close it.) Gently release the rice cereal egg from the mold, decorate with your choice of sprinkles and set aside in egg crate until set.", @@ -69,6 +69,6 @@ "quantity" : "8 slices " }], "calories": "40", - "tagged": [ "healthy" ] + "tagged": [ "healthy", "low calories" ] } ] } diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index 955bef1c9b6..2bb44498b5f 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -104,7 +104,7 @@ public void getFilteredRecipeList_modifyList_throwsUnsupportedOperationException * @see #assertCommandFailure(String, Class, String, Model) */ private void assertCommandSuccess(String inputCommand, String expectedMessage, - Model expectedModel) throws CommandException, ParseException { + Model expectedModel) throws CommandException, ParseException, IOException { CommandResult result = logic.execute(inputCommand); assertEquals(expectedMessage, result.getFeedbackToUser()); assertEquals(expectedModel, model); diff --git a/src/test/java/seedu/address/logic/commands/ListRecipesCommandTest.java b/src/test/java/seedu/address/logic/commands/ListRecipesCommandTest.java index 5e264e31fd6..37e2a97a3c5 100644 --- a/src/test/java/seedu/address/logic/commands/ListRecipesCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ListRecipesCommandTest.java @@ -33,7 +33,7 @@ public void execute_listIsNotFiltered_showsSameList() { ObservableList recipes = model.getFilteredRecipeList(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < recipes.size(); i++) { - builder.append((i + 1) + ". " + recipes.get(i).toString() + "\n"); + builder.append((i + 1) + ". " + recipes.get(i).getName() + "\n"); } assertCommandSuccess(new ListRecipesCommand(), model, ListRecipesCommand.MESSAGE_SUCCESS + builder.toString(), expectedModel); @@ -45,7 +45,7 @@ public void execute_listIsFiltered_showsEverything() { ObservableList recipes = expectedModel.getFilteredRecipeList(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < recipes.size(); i++) { - builder.append((i + 1) + ". " + recipes.get(i).toString() + "\n"); + builder.append((i + 1) + ". " + recipes.get(i).getName() + "\n"); } assertCommandSuccess(new ListRecipesCommand(), model, ListRecipesCommand.MESSAGE_SUCCESS + builder.toString(), expectedModel); diff --git a/src/test/java/seedu/address/logic/commands/RecommendCommandTest.java b/src/test/java/seedu/address/logic/commands/RecommendCommandTest.java new file mode 100644 index 00000000000..7b2c3de7b42 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/RecommendCommandTest.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showRecipeAtIndex; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_INGREDIENTS; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_RECIPE; +import static seedu.address.testutil.TypicalRecipes.getTypicalWishfulShrinking; + +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javafx.collections.ObservableList; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.recipe.Ingredient; +import seedu.address.model.recipe.Recipe; +import seedu.address.model.recipe.RecommendPredicate; + +public class RecommendCommandTest { + private Model model; + private Model expectedModel; + private ArrayList keywords; + + @BeforeEach + public void setUp() { + model = new ModelManager(getTypicalWishfulShrinking(), new UserPrefs()); + expectedModel = new ModelManager(model.getWishfulShrinking(), new UserPrefs()); + model.updateFilteredIngredientList(PREDICATE_SHOW_ALL_INGREDIENTS); + ObservableList ingredients = model.getFilteredIngredientList(); + keywords = new ArrayList<>(); + for (int i = 0; i < ingredients.size(); i++) { + keywords.add(ingredients.get(i).getValue()); + } + } + + @Test + public void execute_listIsNotFiltered_showsSameList() { + RecommendPredicate pred = new RecommendPredicate(keywords); + expectedModel.updateFilteredRecipeList(pred); + ObservableList recipes = expectedModel.getFilteredRecipeList(); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < recipes.size(); i++) { + builder.append((i + 1) + ". " + recipes.get(i).toString() + "\n"); + } + assertCommandSuccess(new RecommendCommand(), model, + RecommendCommand.MESSAGE_SUCCESS + builder.toString(), expectedModel); + } + + @Test + public void execute_listIsFiltered_showsEverything() { + showRecipeAtIndex(model, INDEX_FIRST_RECIPE); + RecommendPredicate pred = new RecommendPredicate(keywords); + expectedModel.updateFilteredRecipeList(pred); + ObservableList recipes = expectedModel.getFilteredRecipeList(); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < recipes.size(); i++) { + builder.append((i + 1) + ". " + recipes.get(i).toString() + "\n"); + } + assertCommandSuccess(new RecommendCommand(), model, + RecommendCommand.MESSAGE_SUCCESS + builder.toString(), expectedModel); + } +} diff --git a/src/test/java/seedu/address/logic/commands/SearchRecipeCommandTest.java b/src/test/java/seedu/address/logic/commands/SearchRecipeCommandTest.java index edc02cc27c9..58b6a593de5 100644 --- a/src/test/java/seedu/address/logic/commands/SearchRecipeCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/SearchRecipeCommandTest.java @@ -7,6 +7,7 @@ import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; import static seedu.address.testutil.TypicalRecipes.EGGS; import static seedu.address.testutil.TypicalRecipes.ENCHILADAS; +import static seedu.address.testutil.TypicalRecipes.PATTY; import static seedu.address.testutil.TypicalRecipes.PORK; import static seedu.address.testutil.TypicalRecipes.getTypicalWishfulShrinking; @@ -19,6 +20,8 @@ import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; import seedu.address.model.recipe.NameContainsKeywordsPredicate; +import seedu.address.model.recipe.RecipeContainsIngredientsPredicate; +import seedu.address.model.recipe.TagContainsKeywordsPredicate; /** * Contains integration tests (interaction with the Model) for {@code FindCommand}. @@ -55,9 +58,9 @@ public void equals() { } @Test - public void execute_zeroKeywords_noRecipeFound() { + public void execute_zeroNameKeywords_noRecipeFound() { String expectedMessage = String.format(MESSAGE_RECIPES_LISTED_OVERVIEW, 0); - NameContainsKeywordsPredicate predicate = preparePredicate(" "); + NameContainsKeywordsPredicate predicate = prepareNamePredicate(" "); SearchRecipeCommand command = new SearchRecipeCommand(predicate); expectedModel.updateFilteredRecipeList(predicate); assertCommandSuccess(command, model, expectedMessage, expectedModel); @@ -65,19 +68,73 @@ public void execute_zeroKeywords_noRecipeFound() { } @Test - public void execute_multipleKeywords_multipleRecipesFound() { + public void execute_multipleNameKeywords_multipleRecipesFound() { String expectedMessage = String.format(MESSAGE_RECIPES_LISTED_OVERVIEW, 3); - NameContainsKeywordsPredicate predicate = preparePredicate("Pork Egg Enchiladas"); + NameContainsKeywordsPredicate predicate = prepareNamePredicate("Pork Egg Enchiladas"); SearchRecipeCommand command = new SearchRecipeCommand(predicate); expectedModel.updateFilteredRecipeList(predicate); assertCommandSuccess(command, model, expectedMessage, expectedModel); assertEquals(Arrays.asList(PORK, ENCHILADAS, EGGS), model.getFilteredRecipeList()); } + @Test + public void execute_zeroTagKeywords_noRecipeFound() { + String expectedMessage = String.format(MESSAGE_RECIPES_LISTED_OVERVIEW, 0); + TagContainsKeywordsPredicate predicate = prepareTagPredicate(" "); + SearchRecipeCommand command = new SearchRecipeCommand(predicate); + expectedModel.updateFilteredRecipeList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredRecipeList()); + } + + @Test + public void execute_multipleTagKeywords_multipleRecipesFound() { + String expectedMessage = String.format(MESSAGE_RECIPES_LISTED_OVERVIEW, 2); + TagContainsKeywordsPredicate predicate = prepareTagPredicate("low calories"); + SearchRecipeCommand command = new SearchRecipeCommand(predicate); + expectedModel.updateFilteredRecipeList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(ENCHILADAS, PATTY), model.getFilteredRecipeList()); + } + + @Test + public void execute_zeroIngredientKeywords_noRecipeFound() { + String expectedMessage = String.format(MESSAGE_RECIPES_LISTED_OVERVIEW, 0); + RecipeContainsIngredientsPredicate predicate = prepareIngredientsPredicate(" "); + SearchRecipeCommand command = new SearchRecipeCommand(predicate); + expectedModel.updateFilteredRecipeList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Collections.emptyList(), model.getFilteredRecipeList()); + } + + @Test + public void execute_multipleIngredientKeywords_multipleRecipesFound() { + String expectedMessage = String.format(MESSAGE_RECIPES_LISTED_OVERVIEW, 1); + RecipeContainsIngredientsPredicate predicate = prepareIngredientsPredicate("Egg"); + SearchRecipeCommand command = new SearchRecipeCommand(predicate); + expectedModel.updateFilteredRecipeList(predicate); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(Arrays.asList(EGGS), model.getFilteredRecipeList()); + } + /** * 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+"))); + } + + /** + * Parses {@code userInput} into a {@code RecipeContainsKeywordsPredicate}. + */ + private RecipeContainsIngredientsPredicate prepareIngredientsPredicate(String userInput) { + return new RecipeContainsIngredientsPredicate(Arrays.asList(userInput.split("\\s+"))); + } } diff --git a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java b/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java index e4c33515768..3a44bb298ae 100644 --- a/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java +++ b/src/test/java/seedu/address/logic/parser/CommandParserTestUtil.java @@ -2,6 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; + import seedu.address.logic.commands.Command; import seedu.address.logic.parser.exceptions.ParseException; @@ -18,7 +20,7 @@ public static void assertParseSuccess(Parser parser, String userInput, Command e try { Command command = parser.parse(userInput); assertEquals(expectedCommand, command); - } catch (ParseException pe) { + } catch (ParseException | IOException pe) { throw new IllegalArgumentException("Invalid userInput.", pe); } } @@ -31,7 +33,7 @@ public static void assertParseFailure(Parser parser, String userInput, String ex try { parser.parse(userInput); throw new AssertionError("The expected ParseException was not thrown."); - } catch (ParseException pe) { + } catch (ParseException | IOException pe) { assertEquals(expectedMessage, pe.getMessage()); } } diff --git a/src/test/java/seedu/address/logic/parser/SearchRecipeCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SearchRecipeCommandParserTest.java index 0c1b4846dc5..e1e8178c6bf 100644 --- a/src/test/java/seedu/address/logic/parser/SearchRecipeCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/SearchRecipeCommandParserTest.java @@ -1,7 +1,9 @@ package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_INGREDIENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; @@ -11,6 +13,8 @@ import seedu.address.logic.commands.SearchRecipeCommand; import seedu.address.model.recipe.NameContainsKeywordsPredicate; +import seedu.address.model.recipe.RecipeContainsIngredientsPredicate; +import seedu.address.model.recipe.TagContainsKeywordsPredicate; public class SearchRecipeCommandParserTest { @@ -24,13 +28,29 @@ public void parse_emptyArg_throwsParseException() { @Test public void parse_validArgs_returnsFindCommand() { - // no leading and trailing whitespaces + // no leading and trailing whitespaces in name SearchRecipeCommand expectedSearchRecipeCommand = - new SearchRecipeCommand(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); - assertParseSuccess(parser, " " + PREFIX_NAME + "Alice Bob", expectedSearchRecipeCommand); + new SearchRecipeCommand(new NameContainsKeywordsPredicate(Arrays.asList("Pork", "Sandwich"))); + assertParseSuccess(parser, " " + PREFIX_NAME + "Pork Sandwich", expectedSearchRecipeCommand); - // multiple whitespaces between keywords - assertParseSuccess(parser, " " + PREFIX_NAME + " \n Alice \n \t Bob \t", expectedSearchRecipeCommand); + // multiple whitespaces between keywords in name + assertParseSuccess(parser, " " + PREFIX_NAME + " \n Pork \n \t Sandwich \t", expectedSearchRecipeCommand); + + // no leading and trailing whitespaces in ingredients + expectedSearchRecipeCommand = + new SearchRecipeCommand(new RecipeContainsIngredientsPredicate(Arrays.asList("Potato", "Bread"))); + assertParseSuccess(parser, " " + PREFIX_INGREDIENT + "Potato Bread", expectedSearchRecipeCommand); + + // multiple whitespaces between ingredients keywords + assertParseSuccess(parser, " " + PREFIX_INGREDIENT + " \n Potato \n \t Bread \t", expectedSearchRecipeCommand); + + // no leading and trailing whitespaces in tag + expectedSearchRecipeCommand = + new SearchRecipeCommand(new TagContainsKeywordsPredicate(Arrays.asList("low", "calories"))); + assertParseSuccess(parser, " " + PREFIX_TAG + "low calories", expectedSearchRecipeCommand); + + // multiple whitespaces between tag keywords + assertParseSuccess(parser, " " + PREFIX_TAG + " \n low \n \t calories \t", expectedSearchRecipeCommand); } } diff --git a/src/test/java/seedu/address/logic/parser/WishfulShrinkingParserTest.java b/src/test/java/seedu/address/logic/parser/WishfulShrinkingParserTest.java index 9953648876f..5c0e6c821bd 100644 --- a/src/test/java/seedu/address/logic/parser/WishfulShrinkingParserTest.java +++ b/src/test/java/seedu/address/logic/parser/WishfulShrinkingParserTest.java @@ -29,6 +29,7 @@ import seedu.address.logic.commands.ListConsumptionCommand; import seedu.address.logic.commands.ListIngredientsCommand; import seedu.address.logic.commands.ListRecipesCommand; +import seedu.address.logic.commands.RecommendCommand; import seedu.address.logic.commands.SearchIngredientCommand; import seedu.address.logic.commands.SearchRecipeCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -144,6 +145,12 @@ public void parseCommand_listConsumption() throws Exception { assertTrue(parser.parseCommand(ListConsumptionCommand.COMMAND_WORD + " 3") instanceof ListConsumptionCommand); } + @Test + public void parseCommand_recommend() throws Exception { + assertTrue(parser.parseCommand(RecommendCommand.COMMAND_WORD) instanceof RecommendCommand); + assertTrue(parser.parseCommand(RecommendCommand.COMMAND_WORD + " 3") instanceof RecommendCommand); + } + @Test public void parseCommand_eatRecipe() throws Exception { EatRecipeCommand command = (EatRecipeCommand) parser.parseCommand( diff --git a/src/test/java/seedu/address/model/recipe/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/recipe/NameContainsKeywordsPredicateTest.java index 65f535e6924..616d0b65261 100644 --- a/src/test/java/seedu/address/model/recipe/NameContainsKeywordsPredicateTest.java +++ b/src/test/java/seedu/address/model/recipe/NameContainsKeywordsPredicateTest.java @@ -41,34 +41,37 @@ public void equals() { @Test public void test_nameContainsKeywords_returnsTrue() { // One keyword - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.singletonList("Alice")); - assertTrue(predicate.test(new RecipeBuilder().withName("Alice Bob").build())); + NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.singletonList("Pasta")); + assertTrue(predicate.test(new RecipeBuilder().withName("Pasta Sandwich").build())); // Multiple keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob")); - assertTrue(predicate.test(new RecipeBuilder().withName("Alice Bob").build())); + predicate = new NameContainsKeywordsPredicate(Arrays.asList("Sandwich", "Pasta")); + assertTrue(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); // Only one matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Bob", "Carol")); - assertTrue(predicate.test(new RecipeBuilder().withName("Alice Carol").build())); + predicate = new NameContainsKeywordsPredicate(Arrays.asList("Pasta", "Pork")); + assertTrue(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); // Mixed-case keywords - predicate = new NameContainsKeywordsPredicate(Arrays.asList("aLIce", "bOB")); - assertTrue(predicate.test(new RecipeBuilder().withName("Alice Bob").build())); + predicate = new NameContainsKeywordsPredicate(Arrays.asList("sANdWich", "pasTa")); + assertTrue(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); } @Test public void test_nameDoesNotContainKeywords_returnsFalse() { // Zero keywords NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Collections.emptyList()); - assertFalse(predicate.test(new RecipeBuilder().withName("Alice").build())); + assertFalse(predicate.test(new RecipeBuilder().withName("Sandwich").build())); // Non-matching keyword - predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol")); - assertFalse(predicate.test(new RecipeBuilder().withName("Alice Bob").build())); - - // Keywords match ingredients, email and address, but does not match name - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); - assertFalse(predicate.test(new RecipeBuilder().withName("Alice").withIngredient("12345", "2g") - .build())); + predicate = new NameContainsKeywordsPredicate(Arrays.asList("Pork")); + assertFalse(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); + + // Keywords match ingredients and calories but does not match name + predicate = new NameContainsKeywordsPredicate( + Arrays.asList("Kaiser", "Rolls", "Or", "Other", "Bread", "2", "whole", "70")); + assertFalse(predicate.test(new RecipeBuilder().withName("Sandwich") + .withIngredient("Kaiser Rolls Or Other Bread", "2 whole") + .withCalories(70) + .build())); } } diff --git a/src/test/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicateTest.java b/src/test/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicateTest.java new file mode 100644 index 00000000000..9a0b6b5f010 --- /dev/null +++ b/src/test/java/seedu/address/model/recipe/RecipeContainsIngredientsPredicateTest.java @@ -0,0 +1,81 @@ +package seedu.address.model.recipe; + +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.RecipeBuilder; + +public class RecipeContainsIngredientsPredicateTest { + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + RecipeContainsIngredientsPredicate firstPredicate = + new RecipeContainsIngredientsPredicate(firstPredicateKeywordList); + RecipeContainsIngredientsPredicate secondPredicate = + new RecipeContainsIngredientsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + RecipeContainsIngredientsPredicate firstPredicateCopy = + new RecipeContainsIngredientsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different recipe -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_recipesContainsIngredients_returnsTrue() { + // One ingredient keyword + RecipeContainsIngredientsPredicate predicate = + new RecipeContainsIngredientsPredicate(Collections.singletonList("Bread")); + assertTrue(predicate.test(new RecipeBuilder().withIngredient("White Bread", "").build())); + + // Multiple ingredient keywords + predicate = new RecipeContainsIngredientsPredicate(Arrays.asList("Potato", "Bread")); + assertTrue(predicate.test(new RecipeBuilder().withIngredient("Potato Bread", "").build())); + + // Mixed-case ingredient keywords + predicate = new RecipeContainsIngredientsPredicate(Arrays.asList("PoTaTo", "BRead")); + assertTrue(predicate.test(new RecipeBuilder().withIngredient("Potato Bread", "").build())); + } + + @Test + public void test_recipeDoesNotContainIngredients_returnsFalse() { + // Zero ingredient keywords + RecipeContainsIngredientsPredicate predicate = new RecipeContainsIngredientsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new RecipeBuilder().withIngredient("Potato", "").build())); + + // Only one matching ingredient keyword + predicate = new RecipeContainsIngredientsPredicate(Arrays.asList("Potato", "Grape")); + assertFalse(predicate.test(new RecipeBuilder().withIngredient("Potato Bread", "").build())); + + // Non-matching ingredient keyword + predicate = new RecipeContainsIngredientsPredicate(Arrays.asList("Grape")); + assertFalse(predicate.test(new RecipeBuilder().withIngredient("Potato Bread", "").build())); + + // Keywords match name and calories, but does not match ingredient + predicate = new RecipeContainsIngredientsPredicate( + Arrays.asList("Sandwich", "70")); + assertFalse(predicate.test(new RecipeBuilder() + .withName("Sandwich") + .withCalories(70) + .build())); + } +} diff --git a/src/test/java/seedu/address/model/recipe/RecipeContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/recipe/RecipeContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..444ecf07525 --- /dev/null +++ b/src/test/java/seedu/address/model/recipe/RecipeContainsKeywordsPredicateTest.java @@ -0,0 +1,80 @@ +package seedu.address.model.recipe; + +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.RecipeBuilder; + +public class RecipeContainsKeywordsPredicateTest { + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + RecipeContainsKeywordsPredicate firstPredicate = + new RecipeContainsKeywordsPredicate(firstPredicateKeywordList); + RecipeContainsKeywordsPredicate secondPredicate = + new RecipeContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + RecipeContainsKeywordsPredicate firstPredicateCopy = + new RecipeContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different recipe -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_nameContainsKeywords_returnsTrue() { + // One keyword + RecipeContainsKeywordsPredicate predicate = + new RecipeContainsKeywordsPredicate(Collections.singletonList("Pasta")); + assertTrue(predicate.test(new RecipeBuilder().withName("Pasta Sandwich").build())); + + // Multiple keywords + predicate = new RecipeContainsKeywordsPredicate(Arrays.asList("Sandwich", "Pasta")); + assertTrue(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); + + // Only one matching keyword + predicate = new RecipeContainsKeywordsPredicate(Arrays.asList("Pasta", "Pork")); + assertTrue(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); + // Mixed-case keywords + predicate = new RecipeContainsKeywordsPredicate(Arrays.asList("sANdWich", "pasTa")); + assertTrue(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); + } + + @Test + public void test_nameDoesNotContainKeywords_returnsFalse() { + // Zero keywords + RecipeContainsKeywordsPredicate predicate = new RecipeContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new RecipeBuilder().withName("Sandwich").build())); + + // Non-matching keyword + predicate = new RecipeContainsKeywordsPredicate(Arrays.asList("Pork")); + assertFalse(predicate.test(new RecipeBuilder().withName("Sandwich Pasta").build())); + + // Keywords match ingredients and calories but does not match name + predicate = new RecipeContainsKeywordsPredicate( + Arrays.asList("Kaiser", "Rolls", "Or", "Other", "Bread", "2", "whole", "70")); + assertFalse(predicate.test(new RecipeBuilder().withName("Sandwich") + .withIngredient("Kaiser Rolls Or Other Bread", "2 whole") + .withCalories(70) + .build())); + } +} diff --git a/src/test/java/seedu/address/model/recipe/RecommendPredicateTest.java b/src/test/java/seedu/address/model/recipe/RecommendPredicateTest.java new file mode 100644 index 00000000000..759204a45f5 --- /dev/null +++ b/src/test/java/seedu/address/model/recipe/RecommendPredicateTest.java @@ -0,0 +1,81 @@ +package seedu.address.model.recipe; + +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.RecipeBuilder; + +public class RecommendPredicateTest { + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + RecommendPredicate firstPredicate = + new RecommendPredicate(firstPredicateKeywordList); + RecommendPredicate secondPredicate = + new RecommendPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + RecommendPredicate firstPredicateCopy = + new RecommendPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different recipe -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_recipesContainsIngredients_returnsTrue() { + // ingredient match without quantity + RecommendPredicate predicate = + new RecommendPredicate(Collections.singletonList("Bread")); + assertTrue(predicate.test(new RecipeBuilder().withIngredient("Bread", "").build())); + + // ingredient match with quantity + predicate = new RecommendPredicate(Arrays.asList("Bread")); + assertTrue(predicate.test(new RecipeBuilder().withIngredient("Bread", "2 whole").build())); + + // Mixed-case ingredient keywords + predicate = new RecommendPredicate(Arrays.asList("BrEad")); + assertTrue(predicate.test(new RecipeBuilder().withIngredient("Bread", "2 whole").build())); + } + + @Test + public void test_recipeDoesNotContainIngredients_returnsFalse() { + // Zero ingredient keywords + RecommendPredicate predicate = new RecommendPredicate(Collections.emptyList()); + assertFalse(predicate.test(new RecipeBuilder().withIngredient("Potato", "").build())); + + // Only one matching ingredient keyword + predicate = new RecommendPredicate(Arrays.asList("Potato", "Bread")); + assertFalse(predicate.test(new RecipeBuilder().withIngredient("Potato Bread", "").build())); + + // Non-matching ingredient keyword + predicate = new RecommendPredicate(Arrays.asList("Grape")); + assertFalse(predicate.test(new RecipeBuilder().withIngredient("Potato Bread", "").build())); + + // Keywords match name and calories, but does not match ingredient + predicate = new RecommendPredicate( + Arrays.asList("Sandwich", "70")); + assertFalse(predicate.test(new RecipeBuilder() + .withName("Sandwich") + .withCalories(70) + .build())); + } +} diff --git a/src/test/java/seedu/address/model/recipe/TagContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/recipe/TagContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..2d60dab5226 --- /dev/null +++ b/src/test/java/seedu/address/model/recipe/TagContainsKeywordsPredicateTest.java @@ -0,0 +1,78 @@ +package seedu.address.model.recipe; + +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.RecipeBuilder; + +public class TagContainsKeywordsPredicateTest { + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + TagContainsKeywordsPredicate firstPredicate = new TagContainsKeywordsPredicate(firstPredicateKeywordList); + TagContainsKeywordsPredicate secondPredicate = new TagContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> 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 recipe -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_tagContainsKeywords_returnsTrue() { + // One keyword + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.singletonList("healthy")); + assertTrue(predicate.test(new RecipeBuilder().withTags("super healthy").build())); + + // Multiple keywords + predicate = new TagContainsKeywordsPredicate(Arrays.asList("healthy", "low calories")); + assertTrue(predicate.test(new RecipeBuilder().withTags("low calories healthy").build())); + + // Only one matching keyword + predicate = new TagContainsKeywordsPredicate(Arrays.asList("healthy", "high calories")); + assertTrue(predicate.test(new RecipeBuilder().withTags("low calories healthy").build())); + // Mixed-case keywords + predicate = new TagContainsKeywordsPredicate(Arrays.asList("heAltHy", "lOw calOrieS")); + assertTrue(predicate.test(new RecipeBuilder().withTags("low calories healthy").build())); + } + + @Test + public void test_tagDoesNotContainKeywords_returnsFalse() { + // Zero keywords + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new RecipeBuilder().withTags("Sandwich").build())); + + // Non-matching keyword + predicate = new TagContainsKeywordsPredicate(Arrays.asList("Pork")); + assertFalse(predicate.test(new RecipeBuilder().withTags("Sandwich Pasta").build())); + + // Keywords match name, ingredients, calories but does not match tags + predicate = new TagContainsKeywordsPredicate( + Arrays.asList("Sandwich", "Kaiser", "Rolls", "Or", "Other", "Bread", "2", "whole", "70")); + assertFalse(predicate.test(new RecipeBuilder() + .withName("Sandwich") + .withIngredient("Kaiser Rolls Or Other Bread", "2 whole") + .withCalories(70) + .withTags("healthy") + .build())); + } +} diff --git a/src/test/java/seedu/address/testutil/TypicalRecipes.java b/src/test/java/seedu/address/testutil/TypicalRecipes.java index 663dbb1956d..a52ac916e75 100644 --- a/src/test/java/seedu/address/testutil/TypicalRecipes.java +++ b/src/test/java/seedu/address/testutil/TypicalRecipes.java @@ -105,7 +105,7 @@ public class TypicalRecipes { + "In the oven (on a baking sheet) or microwave, melt cheese all over " + "the top of each tortilla so that it covers most of the surface area.") .withRecipeImage("images/enchilada1.jpeg") - .withTags("healthy") + .withTags("healthy", "low calories") .build(); public static final Recipe EGGS = new RecipeBuilder().withName("Easter Eggs") @@ -140,7 +140,7 @@ public class TypicalRecipes { + "Cook the patties on both sides until totally done in the middle. " + "Assemble patty melts.") .withRecipeImage("images/party.jpeg") - .withTags("healthy") + .withTags("healthy", "low calories") .build(); // Manually added