diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/AddIngredientController.java b/src/main/java/no/ntnu/idatt1002/demo/controller/AddIngredientController.java new file mode 100644 index 0000000000000000000000000000000000000000..108f13437ba6d6a412f927335b558b88cae4c6cd --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/AddIngredientController.java @@ -0,0 +1,91 @@ +package no.ntnu.idatt1002.demo.controller; + +import javafx.animation.FadeTransition; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.event.ActionEvent; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.ResourceBundle; +import java.util.stream.Collectors; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.util.Duration; +import no.ntnu.idatt1002.demo.data.recipes.FileHandler; +import no.ntnu.idatt1002.demo.data.recipes.FoodItem; +import no.ntnu.idatt1002.demo.data.recipes.IngredientsAtHand; + +public class AddIngredientController implements Initializable { + + private ObservableList<String> ingredients; + private String[] ingredientsList; + + @FXML + private Button addBtn; + + @FXML + private ListView<String> listView; + + @FXML + private TextField searchBar; + + @FXML + private Button searchBtn; + + @FXML + private Label status; + + private String statusText = "Added: "; + + @FXML + void addToFridge(ActionEvent event) throws IOException { + IngredientsAtHand ingredientsAtHand = FileHandler.readIngredientsAtHand("Fridge"); + FoodItem item = FoodItem.valueOf(listView.getSelectionModel().getSelectedItem().replace(" ", "_").toUpperCase()); + + assert ingredientsAtHand != null; + if(!ingredientsAtHand.atHand(item)) { + ingredientsAtHand.addIngredient(item); + FileHandler.writeIngredientsAtHand(ingredientsAtHand, "Fridge"); + + + if(status.isVisible() && status.getText().isBlank()) { + statusText += String.format("%s", item.label); + } else if (status.isVisible()){ + statusText += String.format(", %s", item.label); + } + status.setText(statusText); // Only if not already in list!! + + } + } + + @FXML + void search() { + listView.getItems().clear(); + listView.getItems().addAll(searchList(searchBar.getText(), + Arrays.stream(FoodItem.values()).toList().stream().map(value -> value.label).toArray(String[]::new))); // String[] + } + + + private List<String> searchList(String searchWords, String[] listOfStrings) { + String[] searchWordsArray = searchWords.trim().split(" "); + return Arrays.stream(listOfStrings).filter((in) -> { + return Arrays.stream(searchWordsArray).allMatch((word) -> + in.toLowerCase().contains(word.toLowerCase())); + }).collect(Collectors.toList()); + } + + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + listView.setItems(FXCollections.observableArrayList(Arrays.stream(FoodItem.values()).map(value -> value.label).toList())); + Platform.runLater(() -> searchBar.requestFocus()); + status.setWrapText(true); + } +} diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/AllRecipesController.java b/src/main/java/no/ntnu/idatt1002/demo/controller/AllRecipesController.java new file mode 100644 index 0000000000000000000000000000000000000000..8c4a90b13a60671d0c63c1c8ddf28e84a20012d6 --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/AllRecipesController.java @@ -0,0 +1,115 @@ +package no.ntnu.idatt1002.demo.controller; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import no.ntnu.idatt1002.demo.data.recipes.*; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.ResourceBundle; + +public class AllRecipesController implements Initializable { + + IngredientsAtHand ingredientsAtHand; + RecipeRegister recipeRegister; + + @FXML + private Button goBackBtn; + + @FXML + private ListView<String> allList; + + + private ObservableList<String> recipes; + + private String selectedRecipeName; + + + @FXML + private void goBack(ActionEvent event) throws IOException { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/SuggestRecipes.fxml")); + + Parent root = loader.load(); + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + private void showRecipe(String recipeName) throws IOException { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/Recipe.fxml")); + + Recipe recipeOfInterest = recipeRegister.getRecipe(recipeName); + + Parent root = loader.load(); + + RecipeController recipeController = loader.getController(); + + recipeController.setData(recipeOfInterest); + + Stage stage = (Stage)allList.getParent().getScene().getWindow(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + +private float percent(int a, int b) { + if(b != 0 && a != 0) { + return (float) a / b; + } else { + return 0; + } + + +} + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + + ingredientsAtHand = FileHandler.readIngredientsAtHand("Fridge"); + recipeRegister = FileHandler.readRecipeRegister("Recipes"); + + int numberOfRecipes = recipeRegister.getRecipes().size(); + + ArrayList<Recipe> sortedRecipes = recipeRegister.pickBestFits(numberOfRecipes, ingredientsAtHand); + + recipes = FXCollections.observableArrayList(sortedRecipes.stream().map(recipe -> { + return String.format("# %s - %d missing ingredients (%.2f %%)", recipe.getName(), recipe.getMissingIngredients(), percent(recipe.getIngredientList().size() - recipe.getMissingIngredients(),recipe.getIngredientList().size())); + }).toList()); + + allList.setItems(recipes); + + allList.setOnMouseClicked(new EventHandler<MouseEvent>() { + + @Override + public void handle(MouseEvent mouseEvent) { + selectedRecipeName = allList.getSelectionModel() + .getSelectedItem().split("-|#")[1].strip(); + + try { + showRecipe(selectedRecipeName); + + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + }); + + } +} diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/IngredientTileController.java b/src/main/java/no/ntnu/idatt1002/demo/controller/IngredientTileController.java new file mode 100644 index 0000000000000000000000000000000000000000..ba5e90e775dbf8900e64ea52e87af6b2e0bc371f --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/IngredientTileController.java @@ -0,0 +1,34 @@ +package no.ntnu.idatt1002.demo.controller; + +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import no.ntnu.idatt1002.demo.data.recipes.RecipeIngredient; +import java.net.URL; +import java.util.ResourceBundle; + +public class IngredientTileController implements Initializable { + + + @FXML + private Label text; + + @FXML + private Pane ingredientPane; + + + public void setData(RecipeIngredient ingredient) { + StringBuilder sb = new StringBuilder(); + sb.append("# ").append(ingredient.getFoodType().label.substring(0,1).toUpperCase()) + .append(ingredient.getFoodType().label.substring(1)); + sb.append(" ").append(ingredient.getAmount()).append(" ").append(ingredient.getUnit().label); + text.setText(String.valueOf(sb)); + } + + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + + } +} diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/MainMenu.java b/src/main/java/no/ntnu/idatt1002/demo/controller/MainMenu.java index b8230ee080fc0a6d6f779716effaaddf5cbfe1ca..7e37ae10765388d4b137cb627a2c19130365e49e 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/controller/MainMenu.java +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/MainMenu.java @@ -236,7 +236,7 @@ public class MainMenu { private void switchScene(ActionEvent event) throws IOException { FXMLLoader loader = new FXMLLoader(); if (event.getSource() == foodBtn) { - System.out.println("Food button pressed"); + loader.setLocation(getClass().getResource("/view/SuggestRecipes.fxml")); } else if (event.getSource() == expenseBtn) { loader.setLocation(getClass().getResource("/view/IncomeAndExpenses.fxml")); } else if (event.getSource() == incomeBtn) { diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/RecipeController.java b/src/main/java/no/ntnu/idatt1002/demo/controller/RecipeController.java new file mode 100644 index 0000000000000000000000000000000000000000..c4ba3654e9cbe7b3f949a4d96249e44453dbf7a8 --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/RecipeController.java @@ -0,0 +1,111 @@ +package no.ntnu.idatt1002.demo.controller; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import no.ntnu.idatt1002.demo.data.recipes.*; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class RecipeController implements Initializable { + + @FXML + private Label recipeName; + + @FXML + private Text instructions; + + @FXML + private Button goBackBtn; + + @FXML + private VBox ingredientList; + + + @FXML + private ObservableList<RecipeIngredient> ingredients; + + @FXML + private Pane ingredientPane; + + @FXML + private Button allRecipesBtn; + + + private Recipe recipe; + + + public void setData(Recipe recipeOfInterest) { + recipe = recipeOfInterest; + + recipeName.setText(recipe.getName()); + instructions.setText(recipe.getInstructions()); + ingredients = FXCollections.observableArrayList(recipe.getIngredientList()); + + setIngredientTiles(); + } + + + private void setIngredientTiles() { + + for(RecipeIngredient ri : ingredients) { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/IngredientTile.fxml")); + + try { + Pane pane = loader.load(); + IngredientTileController ingredientTileController = loader.getController(); //Todo: is null + ingredientTileController.setData(ri); + + ingredientList.getChildren().add(pane); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + + @FXML + private void goBack(ActionEvent event) throws IOException { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/SuggestRecipes.fxml")); + + Parent root = loader.load(); + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + @FXML + private void toAllRecipes(ActionEvent event) throws IOException { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/AllRecipes.fxml")); + + Parent root = loader.load(); + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + // ingredients = FXCollections.observableArrayList(recipe.getIngredientList()); + + } +} diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/RecipeTileController.java b/src/main/java/no/ntnu/idatt1002/demo/controller/RecipeTileController.java new file mode 100644 index 0000000000000000000000000000000000000000..0e832a026e76472bbc3fce207e30464082ff11ca --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/RecipeTileController.java @@ -0,0 +1,65 @@ +package no.ntnu.idatt1002.demo.controller; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import no.ntnu.idatt1002.demo.data.recipes.FileHandler; +import no.ntnu.idatt1002.demo.data.recipes.Recipe; +import no.ntnu.idatt1002.demo.data.recipes.RecipeRegister; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class RecipeTileController implements Initializable { + + @FXML + private Button nameTag; + + @FXML + private Label missingTag; + + @FXML + private VBox recipeTile; + + private RecipeRegister recipeRegister; + + + @FXML + private void tileClick(ActionEvent event) throws IOException { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/Recipe.fxml")); + + String recipeName = this.nameTag.getText(); + Recipe recipeOfInterest = recipeRegister.getRecipe(recipeName); + + Parent root = loader.load(); + + RecipeController recipeController = loader.getController(); + + recipeController.setData(recipeOfInterest); + + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + public void setData(Recipe recipe) { + nameTag.setText(recipe.getName()); + missingTag.setText(Integer.toString(recipe.getMissingIngredients())); + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + recipeRegister = FileHandler.readRecipeRegister("Recipes"); + nameTag.setWrapText(true); + } +} diff --git a/src/main/java/no/ntnu/idatt1002/demo/controller/SuggestRecipesController.java b/src/main/java/no/ntnu/idatt1002/demo/controller/SuggestRecipesController.java new file mode 100644 index 0000000000000000000000000000000000000000..49ef7c7ffb84056d562131ce4233c8c1632f3e01 --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/controller/SuggestRecipesController.java @@ -0,0 +1,202 @@ +package no.ntnu.idatt1002.demo.controller; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import no.ntnu.idatt1002.demo.data.recipes.*; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Optional; +import java.util.ResourceBundle; + +public class SuggestRecipesController implements Initializable { + + IngredientsAtHand ingredientsAtHand; + RecipeRegister recipeRegister; + + + @FXML + private Button addToFridgeBtn; + + @FXML + private Button removeBtn; + + @FXML + private Button showAllBtn; + + @FXML + private Button goBackBtn; + + @FXML + private ListView<String> fridgeList; + + @FXML + private GridPane recipeGrid; + + @FXML + private Label missingList; + + @FXML + private VBox recipeTile; + + private ObservableList<String> fridge; + + private ObservableList<Recipe> recipes; + + private final int NUMBER_OF_TILES = 4; + + private final ArrayList<VBox> currentRecipeTiles = new ArrayList<>(4); + + + + @FXML + private void addIngredient(ActionEvent event) throws IOException { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/AddIngredient.fxml")); + DialogPane addIngredientPane = loader.load(); + + Dialog<ButtonType> dialog = new Dialog<>(); + dialog.setDialogPane(addIngredientPane); + dialog.setTitle("Add ingredient to fridge"); + + Optional<ButtonType> clickedButton = dialog.showAndWait(); + + if (clickedButton.isPresent() && clickedButton.get() == ButtonType.CLOSE) { + // Refresh ingredientsAtHand. + readIngredientsAtHand(); + setRecipeTiles(); + } + } + + @FXML + private void removeFromFridge(ActionEvent event) throws IOException { + String toRemove = fridgeList.getSelectionModel().getSelectedItem(); + //TODO: If anything selected! + if(toRemove != null) { + ingredientsAtHand.removeIngredient(FoodItem.valueOf(toRemove.replace(" ", "_").toUpperCase())); + storeIngredientsAtHand(); + setRecipeTiles(); + } + + } + + + + private void setRecipeTiles() { + // Ingredeints at hand and recipesRegister + ArrayList<Recipe> recipes = recipeRegister.pickBestFits(NUMBER_OF_TILES, ingredientsAtHand); + + int i = 0; + int j = 0; + int counter = 0; + + for (Recipe r : recipes) { + FXMLLoader loader = new FXMLLoader(); + loader.setLocation(getClass().getResource("/view/RecipeTile.fxml")); + + if (i > 1) { + j++; + i = 0; + } + + try { + VBox vBox = loader.load(); + RecipeTileController recipeTileController = loader.getController(); + recipeTileController.setData(r); + + if (currentRecipeTiles.size() < recipes.size()) { + currentRecipeTiles.add(vBox); + } else { + recipeGrid.getChildren().remove(currentRecipeTiles.get(counter)); + currentRecipeTiles.set(counter, vBox); + } + + setHoverEffect(vBox, r); + + recipeGrid.add(vBox, i, j); + + + } catch (IOException e) { + throw new RuntimeException(e); + } + i++; + counter++; + } + } + + + private void setHoverEffect(VBox vBox, Recipe recipe) { + vBox.setOnMouseEntered(event -> { + if(recipe.getMissingIngredients()==0) { + missingList.setText(""); + missingList.setVisible(false); + } else { + missingList.setText("Missing: " + String.join(", ", recipe.getMissingList())); + missingList.setVisible(true); + } + + }); + + vBox.setOnMouseExited(event -> { + missingList.setText(""); + missingList.setVisible(false); + }); + } + + + @FXML + private void switchScene(ActionEvent event) throws IOException { + FXMLLoader loader = new FXMLLoader(); + if (event.getSource() == showAllBtn) { + loader.setLocation(getClass().getResource("/view/AllRecipes.fxml")); + } else if (event.getSource() == goBackBtn) { + loader.setLocation(getClass().getResource("/view/MainMenuNew.fxml")); + } + Parent root = loader.load(); + Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow(); + Scene scene = new Scene(root); + stage.setScene(scene); + stage.setResizable(false); + stage.show(); + } + + public void readIngredientsAtHand() { + // If no ingredients at hand file exsists, add one and let it be empty. //TODO + ingredientsAtHand = FileHandler.readIngredientsAtHand("Fridge"); + fridge = FXCollections.observableArrayList(ingredientsAtHand.getIngredientsAtHand().stream().map(foodItem -> foodItem.label).toList()); + //List<String> fridgeLabels = fridge; + fridgeList.setItems(fridge.sorted()); + } + + public void storeIngredientsAtHand() throws IOException { + FileHandler.writeIngredientsAtHand(ingredientsAtHand, "Fridge"); + fridge = FXCollections.observableArrayList(ingredientsAtHand.getIngredientsAtHand().stream().map(foodItem -> foodItem.label).toList()); + fridgeList.setItems(fridge.sorted()); + } + + + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + + readIngredientsAtHand(); + recipeRegister = FileHandler.readRecipeRegister("Recipes"); + recipes = FXCollections.observableArrayList(recipeRegister.getRecipes()); + missingList.setVisible(false); + + // Get the number from FX-grid available? + setRecipeTiles(); + } +} diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FileHandler.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FileHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..88f4251800d886fd008c7c11682110a348bfc85e --- /dev/null +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FileHandler.java @@ -0,0 +1,218 @@ +package no.ntnu.idatt1002.demo.data.recipes; + +import java.io.*; +import java.util.List; +import java.util.Scanner; + +/** + * The FileHandler class is a static class that handles reading and writing to the .register files for storing + * dinner recipes and ingredients at hand. Files of this class are stored at src/main/resources/recipes. + * + * @author hannesofie + */ +public class FileHandler { + + private static final String fileType = ".register"; + private static final String filePath = "src/main/resources/recipes/"; + + + /** + * The method takes a RecipeRegister object and a String as parameters. The recipe register is then written + * to a file named after the provided string, with the file-type ".register" in the /main/recourses/recipes folder. + * The file is written at the following format and is only sensitive to new-lines within the instructions-String: + * # Recipe name + * - Ingredient 1 | amount | unit + * - Ingredient 2 | amount | unit + * - ... + * Instructions + * An IllegalArgumentException is thrown if the recipe register is null. IOExceptions may occur upon writing to + * file, in which case the stacktrace is printed to terminal. + * + * @param recipeRegister A recipe register object that is to be written to file. + * @param title The title by which to name the .register-file. + */ + public static void writeRegister(RecipeRegister recipeRegister, String title) { + if (recipeRegister == null) { + throw new IllegalArgumentException("Only a valid register object can be written to file."); + } + + try (FileWriter fileWriter = new FileWriter(filePath + title + fileType)) { + recipeRegister.getRecipes().forEach((recipe) -> + { + try { + fileWriter.write(formatRecipe(recipe).toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * The method supports the method for writing a recipe register to file by taking in a single recipe object + * and return it as a String at the correct format for storage. + * + * @param recipe A recipe object to format into a String. + * @return A String representation of the recipe at the specified format for file storage. + */ + public static StringBuilder formatRecipe(Recipe recipe) { + StringBuilder sb = new StringBuilder(); + sb.append("# ") + .append(recipe.getName()) + .append("\n") + .append(formatIngredientList(recipe.getIngredientList())) + .append("\n") + .append(recipe.getInstructions()) + .append("\n\n"); + return sb; + } + + /** + * The method supports the 'formatRecipe' method by receiving a list of ingredient objects and returning a + * String at the correct format for the writing to file. + * + * @param ingredientList A list of ingredients to be formatted into a String. + * @return A String of the ingredients at the correct format for writing to file. + */ + public static StringBuilder formatIngredientList(List<RecipeIngredient> ingredientList) { + StringBuilder sb = new StringBuilder(); + + ingredientList.forEach((ingredient) -> sb.append("- ") + .append(ingredient.getFoodType()) + .append(" | ") + .append(ingredient.getAmount()) + .append(" | ") + .append(ingredient.getUnit()) + .append("\n")); + return sb; + } + + /** + * The method reads a recipe register from file and returns the recipe register object. The title of the file + * is provided as a String. If the file doesn't exist, a message is written to the terminal and null is returned + * instead of a recipe register. Each recipe is separated by a '#'-sign and passed on to the method 'readRecipe' + * that reads and returns each Recipe object for the register. + * + * @param title Title of the .register file at which the recipe register is saved. + * @return A recipe register object read from file. + */ + public static RecipeRegister readRecipeRegister(String title) { + File file = new File(filePath + title + fileType); + + RecipeRegister register = new RecipeRegister(); + + try (Scanner sc = new Scanner(file)) { + sc.useDelimiter("#"); + String line; + + while (sc.hasNext()) { + line = sc.next(); + if (!line.isBlank()) { + register.addRecipe(readRecipe(line)); + } + } + } catch (FileNotFoundException e) { + System.out.println("The file was not found."); + return null; + } + return register; + } + + /** + * The method supports the readRecipeRegister method by receiving a string containing the information needed to + * create one specific recipe object. The method reads the needed information from this String and returns + * the Recipe object based on this. The beginning of each ingredient line is recognized by a hyphen ('-'), while + * the instructions are all lines, including internal new-lines, that do not start with a hyphen. + * + * @param readRecipe A String representation of one recipe as read from file. + * @return A recipe object based on the provided string representation. + */ + public static Recipe readRecipe(String readRecipe) { + Scanner sc = new Scanner(readRecipe); + + Recipe recipe; + String instructions = "None"; + String recipeName = sc.nextLine().strip(); + StringBuilder sb = new StringBuilder(); + + String line; + recipe = new Recipe(recipeName, instructions); + + while (sc.hasNextLine()) { + line = sc.nextLine(); + + if (line.startsWith("-")) { + String[] ingredientParts = line.split("\\|"); + + FoodItem ingredientType = FoodItem.valueOf(ingredientParts[0].replaceFirst("-", "").strip()); + double ingredientAmount = Double.parseDouble(ingredientParts[1].strip()); + MeasuringUnit ingredientUnit = MeasuringUnit.valueOf(ingredientParts[2].strip()); + + recipe.addIngredient(ingredientType, ingredientAmount, ingredientUnit); + } else { + sb.append(line).append("\n"); + } + } + recipe.setInstructions(String.valueOf(sb).strip()); + return recipe; + } + + + /** + * The method takes in an IngredientsAtHand object and writes it to a .register-file with the provided + * String as title. The file contains no other information than a simple list of FoodItem constants + * separated by new-line. The resulting file is stored at /main/recourses/recipes/. + * An IllegalArgumentException is thrown if the ingredients at hand object is null. + * IOExceptions may occur upon writing to file, in which case the stacktrace is printed to terminal. + * + * @param ingredientsAtHand An IngredientsAtHand object that holds a collection of constants of the + * FoodItem enum class. + * @param title The title by which to name the file that the ingredients at hand are written to. + */ + public static void writeIngredientsAtHand(IngredientsAtHand ingredientsAtHand, String title) throws IOException { + StringBuilder sb = new StringBuilder(); + + try (FileWriter fileWriter = new FileWriter(filePath + title + fileType)) { + if (ingredientsAtHand == null) { + fileWriter.write(""); + }else { + ingredientsAtHand.getIngredientsAtHand().forEach((ingredient) -> sb.append(ingredient).append("\n")); + try { + fileWriter.write(String.valueOf(sb)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + + /** + * The method reads an IngredientsAtHand object from the .register file with the name provided as a parameter + * and stored at /main/recourses/recipes/. If the file is not found, a FileNotFoundException is thrown and + * null is returned instead of a IngredientsAtHand object. + * @param title Title of the file to read the IngredientsAtHand object from. + * @return An IngredientsAtHand object based on the provided .register file. + */ + public static IngredientsAtHand readIngredientsAtHand(String title) { + File file = new File(filePath + title + fileType); + IngredientsAtHand ingredientsAtHand = new IngredientsAtHand(); + + try (Scanner sc = new Scanner(file)) { + String line; + + while (sc.hasNext()) { + line = sc.next(); + if (!line.isBlank()) { + ingredientsAtHand.addIngredient(FoodItem.valueOf(line)); + } + } + } catch (FileNotFoundException e) { + return null; + } + return ingredientsAtHand; + } +} \ No newline at end of file diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FoodItem.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FoodItem.java index 94c956d7149385d2a83597e69b90c4849081cf11..8dfa3710b2945177d353b71dcce3a34c22645d71 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FoodItem.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/FoodItem.java @@ -1,21 +1,86 @@ package no.ntnu.idatt1002.demo.data.recipes; +/** + * The FoodItem enum class defines a set of food types that may be serving as ingredients in recipes and that the + * user may, or may not, have available in the cupboard, fridge or freezer. The label is a strict lower case + * version of the constant value where the underscore is replaced by a space. This value is used at the front-end + * to represent each enum value. + * + * @author hannesofie + */ public enum FoodItem { ONION("onion"), MINCED_MEAT("minced meat"), - POTATO("potatoes"), + POTATO("potato"), YELLOW_CHEESE("yellow cheese"), WHEAT_FLOUR("wheat flour"), MILK("milk"), TOMATO("tomato"), ORANGE("orange"), LEMON("lemon"), - SALSA_SAUCE("salsa sauce") - ; + SALSA_SAUCE("salsa sauce"), + CUCUMBER("cucumber"), + SALAD("salad"), + SPINACH("spinach"), + SPRING_ROLL("spring roll"), + BELL_PEPPER("bell pepper"), + CHICKPEAS("chickpeas"), + SPAGHETTI("spaghetti"), + PASTA("pasta"), + CREAM("cream"), + HONEY("honey"), + VINEGAR("vinegar"), + TOMATO_PASTE("tomato paste"), + CHILLI("chilli"), + EGG("egg"), + OLIVE_OIL("olive oil"), + HAM("ham"), + PARMESAN("parmesan"), + SNAP_PEA("snap pea"), + MACARONI("macaroni"), + SALMON("salmon"), + FISH("fish"), + CARROT("carrot"), + BUTTER("butter"), + LEEK("leek"), + BREADCRUMBS("breadcrumbs"), + OIL("oil"), + SUMMER_CUTLET("summer cutlet"), + RED_ONION("red onion"), + AVOCADO("avocado"), + LEMON_JUICE("lemon juice"), + DRY_THYME("dry thyme"), + FRESH_YEAST("fresh yeast"), + GARLIC_CLOVE("garlic clove"), + GINGER("ginger"), + CANNED_TOMATO("canned tomato"), + DRY_BASIL("dry basil"), + FRESH_BASIL("fresh basil"), + CELERY("celery"), + BROTH("broth"), + BAY_LEAF("bay leaf"), + CHILLI_BEANS("chilli beans"), + CHILLI_POWDER("chilli powder"), + CUMIN_POWDER("cumin powder"), + PIE_DOUGH("pie dough"), + BROCCOLI("broccoli"), + LAM("lam"), + SUGAR("sugar"), + SHALLOT("shallot"), + RED_WINE("red wine"), + WHITE_BEANS("white beans"), + FROZEN_GREEN_PEAS("frozen green peas"), + SAUSAGE("sausage"), + DRY_OREGANO("dry oregano"); public final String label; + /** + * The constructor of the enum constants takes in a string label and assigns it to its respective constant. + * The label is used for representation in texts and lists at the frontend of the application. + * @param label A lower-case and readable string representation of the enum constant. + */ FoodItem(String label) { this.label = label; } diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Ingredient.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Ingredient.java index 50c850574dc549e12891bc41ed2c5f512225c8bb..76a5b9e78d1f1f5d105d6b928821c3fc373606f3 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Ingredient.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Ingredient.java @@ -1,7 +1,5 @@ package no.ntnu.idatt1002.demo.data.recipes; -import java.util.Objects; - /** * The Ingredient class represents an ingredient that can be part of a recipe in real life and/or * be available to the user in real life. When the ingredient is part of a recipe, the subclass called @@ -42,6 +40,12 @@ public class Ingredient { return foodType; } + /** + * The method takes in a value of the FoodItem enum and sets the ingredient's foodType field equal to this constant. + * If null is given an IllegalArgumentException is thrown. + * + * @param foodType The type of food to assign the ingredient to. + */ public void setFoodType(FoodItem foodType) { if(foodType == null) { throw new IllegalArgumentException("The food type must be set to a valid value of FoodItem."); @@ -116,8 +120,4 @@ public class Ingredient { return Double.compare(that.amount, amount) == 0 && foodType == that.foodType && unit == that.unit; } -/* @Override - public int hashCode() { - return Objects.hash(foodType, amount, unit); - }*/ } diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHand.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHand.java index 72318cd4f29e3228c6300bcd17bba64beb585805..5c4d029f3efab2cd34521180d92223024dcbe022 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHand.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHand.java @@ -4,81 +4,55 @@ import java.util.ArrayList; import java.util.List; /** - * The IngredientsAtHand class contains a collection of Ingredient objects that are currently available to the user. - * Only one instance of each ingredient type may exist in the collection. If an ingredient is already present in the - * collection, the old will be replaced by the new. + * The class holds a collection of FoodItem constants in an ArrayList that represents the groceries that the + * user has available in real life. The class provides methods to add and remove food types from the collection + * as well as checking if a given food type is available, i.e. is part of the collection. * * @author hannesofie */ public class IngredientsAtHand { - private final List<Ingredient> ingredientsAtHand = new ArrayList<>(); + private final ArrayList<FoodItem> ingredientsAtHand = new ArrayList<>(); /** - * The method returns the collection of ingredients at hand as an arraylist of ingredient objects. - * @return The collection of ingredients at hand to the user. + * The method returns the collection of ingredients at hand as an arraylist of FoodItem enum constants. + * @return The collection of food types at hand to the user. */ - public List<Ingredient> getIngredientsAtHand() { + public List<FoodItem> getIngredientsAtHand() { return ingredientsAtHand; } /** * The method takes in an ingredient object and adds it to the collection of ingredients at hand. - * @param ingredient The ingredient object to add to the collection of ingredients at hand. + * @param ingredient The food type to add to the collection of ingredients at hand. */ - public void addIngredient(Ingredient ingredient) { - this.ingredientsAtHand.add(ingredient); + public void addIngredient(FoodItem ingredient) { + if(!this.atHand(ingredient) && ingredient != null) { + this.ingredientsAtHand.add(ingredient); + } } /** - * Returns null if no ingredient of the requested type is found in the collection. - * @param ingredientType What type of food the ingredient is. - * @return The ingredient of the specified type found among the ingredients at hand, null otherwise. + * The method takes in a constant of the FoodType enum class and checks if it is present in the collection. + * If it is present, true is returned, otherwise false. + * @param foodItem A constant value of the FoodItem enum class to check for in the collection + * of ingredients at hand. + * @return True if the food type is at hand, otherwise false. */ - public Ingredient getIngredient(FoodItem ingredientType) { - if(ingredientType == null) return null; - return this.getIngredientsAtHand().stream() - .filter((ingredient) -> ingredient.getFoodType() == ingredientType) - .findFirst().orElse(null); + public boolean atHand(FoodItem foodItem) { + return ingredientsAtHand.stream().anyMatch( (in) -> in.equals(foodItem)); } - /** - * The method takes in three parameters. The method first checks if the Ingredient is at hand in the first place. - * If it is, the old amount and unit of this ingredient are replaced by the provided amount and unit if they - * differ. If not, the ingredient is left as is. If the ingredient is not in the collection, - * @param ingredientType What type of food the ingredient is. - * @param amount The amount of the ingredient. - * @return True if Ingredient is successfully altered or added, false if not. - */ - public boolean alterIngredient(FoodItem ingredientType, double amount, MeasuringUnit unit) { - //TODO: Consider handling exceptions differently. - if(ingredientsAtHand.stream().anyMatch((ingredient) -> ingredient.getFoodType() == ingredientType)) { - try { - getIngredient(ingredientType).setAmount(amount); - getIngredient(ingredientType).setUnit(unit); - - } catch (IllegalArgumentException e) { - return false; - } - } else { - try { - addIngredient(new Ingredient(ingredientType, amount, unit)); - } catch (IllegalArgumentException e) { - return false; - } - } - return true; - } /** * The method takes in a value of the FoodItem enum as a parameter and removes it from the collection of * ingredients at hand if it exists and returns true. If no ingredient of the given type was found in the * collection, false is returned. - * @param ingredientType What type of food the ingredient is. + * @param ingredientType What type of food to remove from the list of ingredients at hand. * @return True if the ingredient was found among the ingredients at hand and removed, false otherwise. */ public boolean removeIngredient(FoodItem ingredientType) { - return ingredientsAtHand.removeIf((ingredient) -> ingredient.getFoodType() == ingredientType); + return ingredientsAtHand.remove(ingredientType); } } diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/MeasuringUnit.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/MeasuringUnit.java index 83f664cd4fc99285c6fc18fffc0b910c633b7412..5f0165adde0f6bc15cd79a5580679b0cc4e035a5 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/MeasuringUnit.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/MeasuringUnit.java @@ -1,5 +1,13 @@ package no.ntnu.idatt1002.demo.data.recipes; +/** + * The enum class defines a set of valid units of measurements related to the ingredients and recipes. + * The label property of each constant provides a lower-case representation of each unit that can be used in + * presentation to the user. + * + * @author hannesofie + */ + public enum MeasuringUnit { DL("dl."), @@ -8,10 +16,19 @@ public enum MeasuringUnit { TBS("tbs."), GR("gr."), KG("kg."), - PC("pieces"); + PC("pieces"), + CAN("can"), + SLICE("slice"), + PKG("pack"), + CB("cubes"); public final String label; + /** + * The constructor of each enum constant takes in a string representation of the constant that is more suited for + * presentation at the frontend. + * @param label Lower case representation of the unit of measure. + */ MeasuringUnit(String label) { this.label = label; } diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Recipe.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Recipe.java index dd6330f177983aa20d7ae9f548983214d4014f1a..528fd5a93711d958ce0bef0614c261404052aa78 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Recipe.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/Recipe.java @@ -4,29 +4,56 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -// Method in case we want to check if also the amount is enough for the recipe. -// Recipes without any ingredients can be showed among all ingredients, but not in suggestion based on ingredients -// at hand. -//TODO: Record? +/** + * The recipe class represents a dinner recipe for the user. It has a name, a textual instruction and + * holds a collection of recipe ingredient items. Each recipe ingredient has a food type, amount, unit of measure and + * a status(true/false) of whether it is at hand or not. + * Apart for methods to get and alter fields, the class provides the following specialized methods: + * - updateIngredientStatus: takes in a list of food types at hand and updates the 'atHand' property of each + * of its ingredients accordingly. + * - getMissingIngredients: returns the number of ingredients in the recipe that are not at hand. + * - getMissingList: returns an ArrayList of the FoodItem labels (String) that are not at hand. + * specific food type + * When the Recipe has not been compared to a collection of ingredients at hand, the number of missing + * ingredients is set to 0 and the list of missing ingredients is set to an empty ArrayList. + * + * @author hannesofie + */ public class Recipe { - private String name = ""; - private List<RecipeIngredient> ingredientList = new ArrayList<>(); - private String instructions = ""; + private String name; + private final ArrayList<RecipeIngredient> ingredientList = new ArrayList<>(); + private String instructions; - public Recipe(String name, String description ) { - if(name.isBlank() | description.isBlank()) { + private int missingIngredients = 0; + private ArrayList<String> missingList = new ArrayList<>(); + + /** + * The constructor method creates a new Recipe object by setting the name and instructions as Strings. + * @param name Name of the recipe. + * @param instructions The instructions of the recipe. + */ + public Recipe(String name, String instructions ) { + if(name.isBlank() | instructions.isBlank()) { throw new IllegalArgumentException("The recipe must have a name and a description."); } this.name = name; - this.instructions = description; + this.instructions = instructions; } - + /** + * The method returns the name of the recipe. + * @return The name of the recipe as a String. + */ public String getName() { return name; } + /** + * The method takes in a String to which the name of the recipe is set. If a blank String is given, an + * IllegalArgumentException is thrown. + * @param name New name for the recipe. + */ public void setName(String name) { if(name.isBlank()) { throw new IllegalArgumentException("The recipe name cannot be left blank."); @@ -34,14 +61,27 @@ public class Recipe { this.name = name; } + /** + * The method returns the list of RecipeIngredients that the recipe consists of. + * @return A list of RecipeIngredients belonging to the recipe. + */ public List<RecipeIngredient> getIngredientList() { return ingredientList; } + /** + * The method returns the instructions of the recipe. + * @return The instructions of the recipe as a String. + */ public String getInstructions() { return instructions; } + /** + * The method takes a String as a parameter to which the recipe instructions are set, provided that the String + * is not blank, in which case an IllegalArgumentException is thrown instead. + * @param instructions The new instructions of the recipe as a String. + */ public void setInstructions(String instructions) { if(instructions.isBlank()) { throw new IllegalArgumentException("The recipe instructions cannot be left blank."); @@ -49,13 +89,12 @@ public class Recipe { this.instructions = instructions; } - - /* public void addIngredients(ArrayList<RecipeIngredient> ingredients) { - ingredients.forEach((ingredient) -> this.ingredientList.add(ingredient)); - }*/ - - //TODO: Make interface for "collects ingredients" and do deep copy. - + /** + * The method takes in a constant of the FoodItem enum class and searches for it among the recipe's ingredients. + * If it is found, the recipe ingredient object is returned, if it is not found, null is returned. + * @param ingredientType A constant value defined by the FoodItem enum class. + * @return The recipe ingredient if it is present in the recipe ingredient list, null otherwise. + */ public RecipeIngredient getIngredient(FoodItem ingredientType) { if(ingredientType == null) return null; return this.getIngredientList().stream() @@ -63,7 +102,17 @@ public class Recipe { .findFirst().orElse(null); } - + /** + * The method adds an ingredient to the recipe if it is not already in the recipe, in which case the existing + * ingredient of the same food type is updated with the parameters provided to this method. The parameters are + * a constant of the FoodItem enum class to provide the food type, a double value representing the amount of + * the ingredient and the measuring unit given as a constant of the MeasuringUnit enum class. If the + * ingredient is neither added nor updated successfully, false is returned, otherwise true is returned. + * @param ingredientType The type of food the ingredient belongs to. + * @param amount The amount of the ingredient when part of this recipe. + * @param unit The measuring unit of the ingredient part of this recipe. + * @return True if the ingredient is successfully added or altered, otherwise false. + */ public boolean addIngredient(FoodItem ingredientType, double amount, MeasuringUnit unit) { if(ingredientList.stream().anyMatch((ingredient) -> ingredient.getFoodType() == ingredientType)) { try { @@ -84,34 +133,85 @@ public class Recipe { } /** - * The functionality may be expanded upon in order to also check if the amount is sufficient for the current recipe. - * @param ingredientsAtHand + * The method takes in an object of the IngredientsAtHand class which defines which foods the user has at hand. + * If the IngredientsAtHand object is null, and IllegalArgumentException is thrown. Otherwise, the recipe's + * ingredient list is iterated through, and the 'atHand' property of each ingredient is updated to true if + * it is present in the ingredients at hand collection, or false if it is not. + * In addition, the recipe's parameters "missingIngredients" and "missingList" are updated with the final + * number of ingredients in the recipe that are not at hand and the name of those recipes as Strings in an + * ArrayList, respectively. + * @param ingredientsAtHand An IngredientsAtHand object holding a collection of the food types that the + * user has available. */ public void updateIngredientStatus(IngredientsAtHand ingredientsAtHand) { - // Will need a supporting class for converting between units to be accurate. - if(ingredientsAtHand == null) { - throw new NullPointerException("The ingredients at hand object must exist"); - } else if (ingredientsAtHand.getIngredientsAtHand().size() < 1) { - throw new IllegalArgumentException("The collection of ingredients at hand is empty."); - } else { - ingredientList.forEach((inRecipe) -> { - ingredientsAtHand.getIngredientsAtHand().forEach((atHand) -> { - if(inRecipe.getFoodType() == atHand.getFoodType()) { - inRecipe.setAtHand(true); - } - }); - }); - } + if (ingredientsAtHand == null) { + throw new NullPointerException("The ingredients at hand object must exist"); + } else { + missingList = new ArrayList<>(); + missingIngredients = (int) ingredientList.stream().filter((inRecipe) -> !ingredientsAtHand.atHand(inRecipe.getFoodType())).count(); + + ingredientList.forEach((inRecipe) -> { + inRecipe.setAtHand(ingredientsAtHand.atHand(inRecipe.getFoodType())); + if(!ingredientsAtHand.atHand(inRecipe.getFoodType())) { + missingList.add(inRecipe.getFoodType().label); + } + }); + } + } + + + /** + * The method returns the property 'missingIngredients' of the recipe. This is the number of ingredients + * as an int, contained in teh recipe, that is not part of the collection of ingredients at hand last updated for. + * @return The number of ingredients in the recipe that are not currently at hand. + */ + public int getMissingIngredients() { + return missingIngredients; + } + + /** + * The method returns an ArrayList of Strings representing each of the food types that the recipe is missing + * relative to the collection of ingredients at hand last updated for. + * @return The ingredients in this recipe that are not currently at hand as a list of Strings. + */ + public ArrayList<String> getMissingList() { + return missingList; } + /** + * The method returns a String representation of the recipe, listing its name, ingredients and instructions. + * @return A String representation of the recipe. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(this.name).append("\n"); + this.getIngredientList().forEach((ingredient) -> sb.append(ingredient).append("\n")); + sb.append(this.getInstructions()); + + return String.valueOf(sb); + } + + /** + * The method takes in another Object and checks whether it is equal to the current recipe object. It first checks + * if the object reference is the same, in which case true is returned. If not, it checks that the object is not + * null and whether its class is dissimilar, for which false is also returned. Finally, the object is cast as a + * Recipe and compared by name. If the two recipe's names are the same, they are considered the same. + * @param o A general Object. + * @return True if the object is equal to the current recipe, false otherwise. + */ @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Recipe recipe = (Recipe) o; + if (!(o instanceof Recipe recipe)) return false; return Objects.equals(name, recipe.name); } + /** + * The method returns a standard hash-code based on the recipe's name, ingredient list and instructions. + * @return A hash-code as an integer for the recipe. + */ @Override public int hashCode() { return Objects.hash(name, ingredientList, instructions); diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredient.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredient.java index 36850d072b18816f5bdd231400eedef305e8cf84..127586fb6f0ff5a3ef3856531f467c29f295dd47 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredient.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredient.java @@ -35,10 +35,24 @@ public class RecipeIngredient extends Ingredient{ /** * The method sets the value of the atHand field for the ingredient to either true or false. - * @param atHand + * @param atHand A boolean value to set the 'atHand' status of the ingredient. */ public void setAtHand(boolean atHand) { this.atHand = atHand; } + /** + * The method returns a String representation of a Recipe Ingredient object, listing its type, amount, unit + * and whether it is at hand or not. + * @return A String representation of the recipe ingredient object. + */ + @Override + public String toString() { + return "Ingredient{" + + "foodType=" + this.getFoodType().label + + ", amount=" + this.getAmount() + + ", unit=" + this.getUnit().label + + ", at hand=" + this.isAtHand() + + '}'; + } } diff --git a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegister.java b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegister.java index 1ec98e95879698ac7e6088f46f18e54a2bf67062..dd6954090704c381ca11224e6effb7df86dabc45 100644 --- a/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegister.java +++ b/src/main/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegister.java @@ -1,31 +1,82 @@ package no.ntnu.idatt1002.demo.data.recipes; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; +/** + * The RecipeRegister class holds a collection of Recipes in a list and provides methods to access the recipes, + * add recipes to the register as well as to pick a number of recipe's best fitting a list of food types + * provided as a IngredientsAtHand object. The register is instantiated using the default constructor. + * + * @author hannesofie + */ public class RecipeRegister { - private final List<Recipe> recipes = new ArrayList<>(); + private ArrayList<Recipe> recipes = new ArrayList<>(); - //TODO: Copy-constructor - - public List<Recipe> getRecipes() { - return recipes; + /** + * The method returns the list of recipes in the Register. + * @return A list of recipes that belong to this register. + */ + public ArrayList<Recipe> getRecipes() { + return this.recipes; } + /** + * The method takes in a Recipe object. If the recipe is already in the register, the old recipe will be replaced. + * If the recipe is null, an IllegalArgumentException is thrown. Otherwise, the new recipe is added to the + * register's collection of recipes. + * @param recipe A new recipe to add to the register. + */ public void addRecipe (Recipe recipe) { - if(recipes.contains(recipe)) { - throw new IllegalArgumentException("The recipe already exists in the register."); // Or just replace? + if(this.recipes.contains(recipe)) { + recipes.remove(getRecipe(recipe.getName())); + recipes.add(recipe); } else if (recipe == null) { throw new NullPointerException("The recipe cannot be null."); + } else { + this.recipes.add(recipe); } - this.recipes.add(recipe); } + + /** + * The method takes in a string with the name of a recipe and returns that recipe object if it is in the + * recipe register. If it is not found, null is returned instead. + * @param name The name of a recipe as a String. + * @return The recipe matching the provided name if it is present in the recipe register. + */ public Recipe getRecipe(String name) { return recipes.stream(). filter((recipe) -> recipe.getName().equals(name)) .findFirst().orElse(null); } + + /** + * The method takes in a number and an IngredientsAtHand object and returns an ArrayList containing that + * number of recipes that, based on the ingredients at hand, require the fewest extra ingredients apart + * from the ones at hand. If the number of recipes in the register is fewer than the provided number, the list + * will contain as many as the register can provide. The resulting list is sorted by the number of ingredients + * required apart from those ingredients at hand. If zero recipes are requested, and empty ArrayList is returned. + * @param number Number of recipes to pick from the recipe register. + * @param atHand A IngredientsAtHand object providing a collection of the food types at hand to the user. + * @return A list of recipes of sorted by available ingredients and limited to the provided number or the + * number of recipes in the register. + */ + public ArrayList<Recipe> pickBestFits(int number, IngredientsAtHand atHand) { + if(number == 0) { + return new ArrayList<>(); + } + ArrayList<Recipe> recipes; + this.recipes.forEach(r -> r.updateIngredientStatus(atHand)); + + recipes = this.recipes.stream() + .sorted(Comparator.comparingInt(Recipe::getMissingIngredients)) + .limit(number).collect(Collectors.toCollection(ArrayList::new)); + return recipes; + } + } diff --git a/src/main/resources/Economics/Expense.register b/src/main/resources/Economics/Expense.register index 6f1ef622ca9752f0e2b9887fde1ac4a3d76e97ee..6435f9469d30071fc836f95e7337b9dd542af9a1 100644 --- a/src/main/resources/Economics/Expense.register +++ b/src/main/resources/Economics/Expense.register @@ -9,3 +9,9 @@ amount=200.0 isRecurring=Not recurring category=FOOD +date=2023-04-15 +description=iphone +amount=5000.0 +isRecurring=Not recurring +category=OTHER + diff --git a/src/main/resources/recipes/Fridge.register b/src/main/resources/recipes/Fridge.register new file mode 100644 index 0000000000000000000000000000000000000000..a8ed5c2825afdeece81d384305cf49b49829b260 --- /dev/null +++ b/src/main/resources/recipes/Fridge.register @@ -0,0 +1,9 @@ +MILK +YELLOW_CHEESE +MINCED_MEAT +ONION +HAM +TOMATO +WHEAT_FLOUR +ORANGE +OIL diff --git a/src/main/resources/recipes/Recipes.register b/src/main/resources/recipes/Recipes.register new file mode 100644 index 0000000000000000000000000000000000000000..c7ddcb813722e52aaaa0e9b83f44e09c38844448 --- /dev/null +++ b/src/main/resources/recipes/Recipes.register @@ -0,0 +1,275 @@ +# Simple Pasta Salad + +- PASTA | 300 | GR +- OLIVE_OIL | 2 | TBS +- HAM | 10 | SLICE +- PARMESAN | 100 | GR +- SNAP_PEA | 50 | GR + +1. Kok pasta som anvist på pakken. + +2. Hell av vannet og tilsett salt og olivenolje. Avkjøl pastaen. + +3. Skjær skinke, ost og sukkererter(snap pea) i små biter og tilsett pastaen. Rør rundt. + +4. Smak til med salt og pepper og drypp over litt olivenolje. + +https://www.matprat.no/oppskrifter/rask/enkel-pastasalat/ + +# Fish gratin + +- MACARONI | 75 | GR +- BUTTER | 2 | TBS +- WHEAT_FLOUR | 3 | TBS +- MILK | 4 | DL +- LEEK | 0.25 | PC +- FISH | 400 | GR +- EGG | 4 | PC +- YELLOW_CHEESE | 1 | DL +- BREADCRUMBS | 0.5 | DL + + +1. Kok makaroni som anvist på pakken, hvis ikke du har rester med ferdig kokt pasta. Ca. 250 g kokt pasta tilsvarer 75 g ukokt makaroni. + +2. Smelt smør i en kjele ved svak varme. Tilsett hvetemel og rør blandingen jevn. Skru ned varmen. Tilsett litt og litt av melken og rør klumpfritt mellom hver gang. Kok opp mens du rører godt. La sausen småkoke i ca. 5 minutter. + +3. Ta sausen av platen og tilsett kokt makaroni, tynne purreringer, salt og pepper. Ha i biter med kokt fisk. Både fersk, salt og røkt fisk kan brukes. + +4. Skill eggene, og stivpisk eggehvitene. Bland først eggeplommene inn i sausen. Vend deretter inn eggehvitene litt forsiktig for å beholde luften i eggehvitene. + +Det beste er å bruke romtempererte egg. Da blir det mer luft i eggehvitene når du pisker dem. Pass på at det ikke kommer eggeplomme opp i eggehviten når du skiller eggene. Eggeplommen inneholder fett og gjør at det blir vanskelig å stivpiske eggehvitene. + +5. Fordel gratengrøren i en godt smurt ildfast form og strø revet ost og griljermel på toppen. Stek i stekeovnen ved 180 °C i ca. 40 minutter til gratengen har hevet seg og blitt gyllen. + +Server fiskegrateng med råkost og kokte poteter. +Steketiden vil variere etter hva slags form du bruker. Bruker du en flat form blir steketiden kortere enn om du fyller en form med høyere kanter. + + +https://www.matprat.no/oppskrifter/familien/fiskegrateng/ + + +# Summer Cutlets with Crispy Pasta Salad + +- SUMMER_CUTLET | 4 | PC +- OIL | 2 | TBS +- PASTA | 150 | GR +- SNAP_PEA | 150 | GR +- CARROT | 2 | PC +- RED_ONION | 1 | PC +- AVOCADO | 1 | PC +- LEMON_JUICE | 3 | TBS +- OLIVE_OIL | 2 | TBS +- DRY_THYME | 1 | TSP + +1. Kok pasta etter anvisning på pakken. Hell den i et dørslag og skyll med kaldt vann. + +2. Rens og finsnitt sukkererter og del skrelte gulrøtter, rødløk og avokado i små biter. Hell på sitronsaft, olivenolje, timian, salt og pepper. Bland godt og tilsett makaronien. + +3. Pensle kotelettene med olje. Dryss på krydder. + +4. Grill kotelettene 4-5 minutter på hver side. + + +https://www.matprat.no/oppskrifter/familien/sommerkoteletter-med-spro-pastasalat/ + + +# Basic Squared pizza + +- WHEAT_FLOUR | 7 | DL +- OIL | 4 | TBS +- FRESH_YEAST | 0.5 | PKG +- MINCED_MEAT | 400 | GR +- ONION | 1 | PC +- GARLIC_CLOVE | 2 | PC +- TOMATO_PASTE | 2 | TBS +- CANNED_TOMATO | 1 | CAN +- DRY_BASIL | 1 | TSP +- YELLOW_CHEESE | 300 | GR +- BELL_PEPPER | 1 | PC + +1. Rør ut gjær i 3 dl lunkent vann. Tilsett olje, salt og mel. Elt deigen godt sammen, og la den heve til dobbel størrelse. + +2. Brun kjøttdeig i olje på sterk varme i to omganger. Ha kjøttdeigen tilbake i stekepannen. + +3. Tilsett hakket løk, hvitløk, tomatpuré, hermetiske tomater og ca. 1 dl vann. La sausen småkoke i ca. 10 minutter til den tykner. Smak til med salt, pepper og basilikum (alt. 1 ss fersk basilikum). + +4. Kjevle ut deigen. Ha den over på en bakepapirkledd langpanne og fordel kjøttsausen utover deigen. Dryss over revet ost, og ha på paprika. + +5. Stek pizzaen midt i ovn på 225 °C i ca. 20 minutter. + +Server toppet med oregano og gjerne en frisk salat med en god dressing som tilbehør til pizzaen. + + +https://www.matprat.no/oppskrifter/kos/langpannepizza/ + +# Pasta Bolognese + +- MINCED_MEAT | 600 | GR +- OIL | 4 | TBS +- ONION | 1 | PC +- GARLIC_CLOVE | 3 | PC +- CARROT | 1 | PC +- CELERY | 1 | PC +- CANNED_TOMATO | 2 | CAN +- BROTH | 6 | DL +- BAY_LEAF | 1 | PC +- SPAGHETTI | 500 | GR + +1. Skrell og finhakk løk og hvitløk. Vask og finhakk gulrot og stilkselleri. + +2. Stek kjøttdeigen i flere omganger i en varm panne med olje. Ha kjøttdeigen over i en gryte og ha i de finhakkede grønnsakene. Ha i tomater, 0,5 dl rødvin om du har det for hånden, kjøttbuljong og laurbærblad. + +3. La det hele småkoke under lokk i ca. 1 ½ time. Tilsett mer væske om nødvendig. Smak til med salt og pepper. + +4. Kok pasta etter anvisning på pakken. Vend litt av sausen inn i pastaen rett før servering, da holder pastaen seg bedre og vil holde seg varmere. + +I Bologna finhakker de kjøttet til den ekte bolognesen. Bytt ut kjøttdeig med flatbiff av storfe. Følg samme fremgangsmåte, men la Bolognesen koke lengre. Etter to timer er kjøttet veldig mørt. Tilsett mer væske om nødvendig. + + +https://www.matprat.no/oppskrifter/gjester/pasta-bolognese/ + +# Chilli con Carne + +- MINCED_MEAT | 400 |GR +- OIL | 2 | TBS +- ONION | 1 | PC +- CHILLI | 1 | PC +- GARLIC_CLOVE | 2 | PC +- CANNED_TOMATO | 1 | CAN +- BELL_PEPPER | 2 | PC +- CHILLI_BEANS | 1 | CAN +- CHILLI_POWDER | 1.5 | TSP +- CUMIN_POWDER | 0.5 | TSP + +Bruk gjerne paprika i ulike farger! + +1. Ha margarin eller olje i en varm gryte. Vent til margarinen slutter å bruse, og brun kjøttdeig i to omganger sammen med løk, hvitløk og chili. + +2. Hell over hermetisk tomat og la det surre i 3-4 minutter. + +3. Bland inn chilibønner, paprika og krydder. La det koke et par minutter og smak til med salt. + +Server gjerne gryteretten med salat og grove minibaguetter. + + +https://www.matprat.no/oppskrifter/rask/chili-con-carne/ + +# Ham Pie + +- PIE_DOUGH | 1 | PKG +- BROCCOLI | 100 | GR +- GARLIC_CLOVE | 1 | PC +- LEEK | 0.25 | PC +- EGG | 4 | PC +- MILK | 2 | DL +- YELLOW_CHEESE | 1.5 | DL + +Forvarm stekeovnen til 200 °C. + +1. Legg deigen i en liten paiform (20-22 cm) og klem den godt inn til kantene. La det øverste papiret som deigen er pakket inn i sitte på, og fyll formen med tørkede erter eller ris. + +2. Sett paiformen midt i stekeovnen og forstek paiskallet i 12-15 minutter, til kantene begynner å få farge. Hell erter/ris ut av formen (ta vare på dem til neste gang du aker pai) og fjern papiret. + +3. Del i mellomtiden brokkoli i små buketter og forvell dem i kokende vann i 2-3 minutter. Skyll med kaldt vann og la dem renne godt av seg. + +4. Ha skinke, hvitløk, purre og brokkolibuketter over bunnen. + +5. Visp sammen egg og melk (ev. fløte). Krydre med salt og pepper. Hell eggeblandingen over og dryss revet ost på toppen. + +6. Senk varmen i stekeovnen til 180 °C og stek paien midt i ovnen i 25-30 minutter, eller til eggeblandingen har stivnet og paien har fått en fin, gyllenbrun farge. + +Server paien nystekt eller lun, gjerne med en god, frisk salat ved siden av. +Har du ikke akkurat de ingrediensene som står her, kan du ha nesten hva som helst i en pai. Pølse- eller kjøttrester i terninger, og masse forskjellige grønnsaker smaker godt. Husk å forvelle harde grønnsaker som løk og brokkoli. + + +https://www.matprat.no/oppskrifter/familien/skinkepai/ + + + +# French Lam Stew + +- LAM | 1 | KG +- BUTTER | 3 | TBS +- SHALLOT | 10 | PC +- GARLIC_CLOVE | 2 | PC +- RED_WINE | 2.5 | DL +- CANNED_TOMATO | 1 | CAN +- TOMATO_PASTE | 1 | DL +- CARROT | 2 | PC +- DRY_THYME | 1 | TSP +- BAY_LEAF | 1 |PC +- WHITE_BEANS | 1 | CAN + +1. Varm en stekepanne med smør og stek kjøttet på alle sider. Legg det over i en vid gryte. Stek sjalottløk raskt i samme panne og ha over i gryten. Hell i vin, tomater, tomatpuré, gulrot i biter og krydder. Tilsett nok vann til at det akkurat dekker. Kok opp, senk varmen og sett på lokk. La gryten småkoke til kjøttet er helt mørt, ca. 1 time. + +2. Skyll bønnene (ferdig kokt, ca 150gr) godt og ha dem i gryten. Smak til med sukker, salt og pepper. + +3. Server gryten rykende varm med godt, ferskt brød og/eller poteter. + +Bytt gjerne ut rødvin med hvitvin. Dette er en fin rett du kan lage om du har rester av lammestek. Kombiner gjerne med smakfulle pølser. + +Du kan gjerne bruke fårikålkjøtt av får istedet for av lam. Beregn i så fall lengre koketid. Fårekjøtt trenger ca. 3 timers koketid. + + +https://www.matprat.no/oppskrifter/gjester/fransk-lammegryte/ + + +# Tomato Soup with Egg + +- EGG | 4 | PC +- ONION | 1 | PC +- GARLIC_CLOVE | 2 | PC +- OLIVE_OIL | 2 | TBS +- TOMATO_PASTE | 4 | TBS +- CANNED_TOMATO | 2 | CAN +- BROTH | 2 | CB +- SUGAR | 1 | TBS +- DRY_BASIL | 1 | TSP + +1. Kok egg i 8-10 minutter. Avkjøl i kaldt vann og skrell dem. + +2. Fres løk og hvitløk myk og blank i en kjele med litt olje. + +3. Tilsett tomatpuré, og la det surre ett minutt. Ha deretter i hakket tomat, ca. 8 dl. vann, grønnsaksbuljong og sukker. Kok opp og la det småkoke i ca. 10 minutter. + +4. Kjør suppen til ønsket konsistens med stavmikser eller i hurtigmikser. Tilsett basilikum (av. en halv potte fersk), og smak til med pepper. + +Server suppen med kokte egg. +Prøv deg fram med ulike urter – oregano, timian, rosmarin, persille og kjørvel passer sammen med tomat. Tilsett gjerne en fløteskvett om du vil ha suppen litt mildere. + + +https://www.matprat.no/oppskrifter/familien/tomatsuppe-med-kokt-egg/ + + +# Sousage Pasta Pot + +- SAUSAGE | 400 | GR +- BROCCOLI | 0.5 | PC +- ONION | 1 | PC +- GARLIC_CLOVE | 1 | PC +- PASTA | 400 | GR +- CANNED_TOMATO | 2 | CAN +- DRY_OREGANO | 1 | TSP +- YELLOW_CHEESE | 100 | GR +- CREAM | 2 | DL +- SUGAR | 1 | TBS +- FROZEN_GREEN_PEAS | 100 | GR + +Du kan velge mellom å bruke vann eller utblandet buljong i denne oppskriften. + +1. Fjern skinnet på pølsene, og skjær dem i 1/2 cm tykke skiver. Kutt brokkoli i små buketter. Husk også å ta med stilken - kutt den i grove biter. Sett pølser og brokkoli til side. + +2. Rens løk og hvitløk. Grovhakk løken og finhakk hvitløken. + +3. Ha pastaskruer, hermetiske tomater, 4 dl buljong/vann og oregano i en stor gryte sammen med løk og hvitløk. Rør litt rundt så alt blander seg. Sett gryten på platen , og kok opp. Sett på lokk, og la det koke i ca. 10 minutter. Rør innimellom slik at det ikke svir seg i bunnen av gryten. Dersom du syns det er litt lite væske kan du ha i mer vann. + +4. Når gryten har småkokt under lokk i ca. 10 minutter, har du i pølser og brokkoli. Kok til alt er gjennomvarmt, og pastaen er "al dente", ca. 5 minutter til. + +5. Rør inn revet hvitost og fløte. Smak til med salt, pepper og eventuelt litt sukker. + +6. Ha i erter helt til slutt. Da får de akkurat nok varme, uten å miste den fine grønnfargen sin. + +Dryss eventuelt over litt finhakket bladpersille og litt grovkvernet pepper til pynt. + + +https://www.matprat.no/oppskrifter/familien/polsegryte/ \ No newline at end of file diff --git a/src/main/resources/recipes/testReadRegisterFromFile.register b/src/main/resources/recipes/testReadRegisterFromFile.register new file mode 100644 index 0000000000000000000000000000000000000000..2a49055f25cfbe1ddbbe9e59d32c5e0a4c8ae074 --- /dev/null +++ b/src/main/resources/recipes/testReadRegisterFromFile.register @@ -0,0 +1,14 @@ +# Meat, cheese and potatoes +- MINCED_MEAT | 500.0 | GR +- POTATO | 750.0 | GR +- YELLOW_CHEESE | 2.0 | DL + +Instructions. +Testing another line of instructions! + +# Meat, cheese and lemons +- MINCED_MEAT | 500.0 | GR +- LEMON | 750.0 | GR +- YELLOW_CHEESE | 2.0 | DL + +Instructions. \ No newline at end of file diff --git a/src/main/resources/style.css b/src/main/resources/style.css new file mode 100644 index 0000000000000000000000000000000000000000..87a78c9e242433109d514c5550f18ed3cd2afdaa --- /dev/null +++ b/src/main/resources/style.css @@ -0,0 +1,81 @@ + +.button-style { + //-fx-background-color: linear-gradient(#8ca45b, #1e5b5b); + -fx-background-color: white; + -fx-text-fill: black; + -fx-background-radius: 30; + -fx-background-insets: 0; + + -fx-font-size: 16; + //-fx-text-fill: white; +} + +.button-style:hover { + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; + -fx-scale-z: 1.05; +} + +.recipe-tile { + -fx-background-color: rgba(255, 213, 0, 0.86); + -fx-background-radius: 25; + -fx-border-color: white; + -fx-border-width: 6px; + -fx-border-radius: 25; +} + +.recipe-tile:hover { + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; + -fx-scale-z: 1.05; +} + +.dialog-pane { + -fx-background-color: rgba(151, 175, 151, 0.8); + +} + +.list-cell:filled:selected:focused, .list-cell:filled:selected { + -fx-background-color: #1e5b5b; + -fx-text-fill: white; +} + +.list-cell{ + -fx-font-size:18.0; +} + +.list-cell:even { /* <=== changed to even */ + -fx-background-color: white; +} + +.list-cell:odd { /* <=== changed to even */ + -fx-background-color: rgba(190, 217, 190, 0.8); +} + +.list-cell:filled:hover { + -fx-background-color: #1e5b5b; + -fx-text-fill: white; +} + +.tile-button { + -fx-background-color: transparent; + -fx-alignment: center; +} + +.ingredient:hover { + -fx-scale-x: 1.05; + -fx-scale-y: 1.05; + -fx-scale-z: 1.05; +} + +.recipe-instructions { + -fx-font-size: 16; + -fx-font-style: italic; + -fx-spacing: 1.5; +} + +.information-label { + -fx-background-color: rgba(255, 255, 255, 0.65); + -fx-text-fill: black; + -fx-border-radius: 20; +} \ No newline at end of file diff --git a/src/main/resources/view/AddIngredient.fxml b/src/main/resources/view/AddIngredient.fxml new file mode 100644 index 0000000000000000000000000000000000000000..675a26f9f2ccf35729df39cc2eb5f6b5b2211efe --- /dev/null +++ b/src/main/resources/view/AddIngredient.fxml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<DialogPane id="dialog-pane" expanded="true" prefHeight="524.0" prefWidth="614.0" stylesheets="@../style.css" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.AddIngredientController"> + <content> + <AnchorPane prefHeight="400.0" prefWidth="600.0"> + <padding> + <Insets top="20.0" /> + </padding> + <children> + <VBox prefHeight="406.0" prefWidth="614.0"> + <children> + <HBox prefHeight="100.0" prefWidth="200.0" spacing="20.0"> + <children> + <TextField fx:id="searchBar" prefHeight="36.0" prefWidth="438.0" promptText="Search for ingredient"> + <padding> + <Insets bottom="10.0" left="20.0" right="20.0" top="10.0" /> + </padding> + <HBox.margin> + <Insets /> + </HBox.margin> + </TextField> + <Button id="button-style" fx:id="searchBtn" alignment="CENTER" contentDisplay="CENTER" defaultButton="true" mnemonicParsing="false" onAction="#search" styleClass="button-style" stylesheets="@../style.css" text="Search" textAlignment="CENTER"> + <font> + <Font size="14.0" /> + </font> + <HBox.margin> + <Insets /> + </HBox.margin> + </Button> + </children> + <VBox.margin> + <Insets left="25.0" right="25.0" /> + </VBox.margin> + </HBox> + <ListView id="list-cell" fx:id="listView" prefHeight="330.0" prefWidth="564.0" stylesheets="@../style.css"> + <padding> + <Insets bottom="10.0" left="20.0" right="20.0" top="10.0" /> + </padding> + <VBox.margin> + <Insets left="25.0" right="25.0" /> + </VBox.margin> + </ListView> + <Pane prefHeight="100.0" prefWidth="614.0"> + <children> + <Button id="button-style" fx:id="addBtn" layoutX="275.0" layoutY="7.0" mnemonicParsing="false" onAction="#addToFridge" styleClass="button-style" stylesheets="@../style.css" text="ADD"> + <font> + <Font size="14.0" /> + </font> + </Button> + </children> + </Pane> + <Pane prefHeight="100.0" prefWidth="614.0"> + <children> + <Label fx:id="status" layoutX="28.0" layoutY="6.0" prefHeight="44.0" prefWidth="558.0" textAlignment="CENTER" wrapText="true"> + <font> + <Font size="14.0" /> + </font></Label> + </children> + </Pane> + </children> + </VBox> + </children></AnchorPane> + </content> + <header> + <Label fx:id="addIngredientPane" alignment="CENTER" contentDisplay="CENTER" text="Add an ingredient to the fridge"> + <font> + <Font size="24.0" /> + </font> + <padding> + <Insets bottom="25.0" top="20.0" /> + </padding> + </Label> + </header> + <buttonTypes> + <ButtonType fx:constant="CLOSE" /> + </buttonTypes> +</DialogPane> diff --git a/src/main/resources/view/AllRecipes.fxml b/src/main/resources/view/AllRecipes.fxml new file mode 100644 index 0000000000000000000000000000000000000000..b58fa895c0942d96f5b2bac6b055fc551245034f --- /dev/null +++ b/src/main/resources/view/AllRecipes.fxml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<AnchorPane prefHeight="695.0" prefWidth="1130.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.AllRecipesController"> + <children> + <ImageView fitHeight="695.0" fitWidth="1130.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../Images/backgroundMini.jpg" /> + </image> + </ImageView> + <BorderPane prefHeight="695.0" prefWidth="1130.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <top> + <HBox prefHeight="125.0" prefWidth="1130.0" BorderPane.alignment="CENTER"> + <children> + <Pane prefHeight="125.0" prefWidth="284.0"> + <children> + <Button fx:id="goBackBtn" layoutX="60.0" layoutY="38.0" mnemonicParsing="false" onAction="#goBack" styleClass="button-style" stylesheets="@../style.css" text="Back to Suggestions" /> + </children> + </Pane> + <Pane prefHeight="100.0" prefWidth="623.0"> + <children> + <Label layoutX="115.0" layoutY="24.0" text="All recipes"> + <font> + <Font size="48.0" /> + </font> + </Label> + </children></Pane> + <Pane prefHeight="200.0" prefWidth="200.0" /> + </children></HBox> + </top> + <center> + <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="526.0" prefWidth="819.0"> + <children> + <ListView fx:id="allList" prefHeight="526.0" prefWidth="819.0" styleClass="list-cell" stylesheets="@../style.css" /> + </children> + </AnchorPane> + </center> + <left> + <Pane prefHeight="595.0" prefWidth="148.0" BorderPane.alignment="CENTER" /> + </left> + <right> + <Pane prefHeight="557.0" prefWidth="152.0" BorderPane.alignment="CENTER" /> + </right> + <bottom> + <Pane prefHeight="38.0" prefWidth="1130.0" BorderPane.alignment="CENTER" /> + </bottom> + </BorderPane> + </children> +</AnchorPane> diff --git a/src/main/resources/view/IngredientTile.fxml b/src/main/resources/view/IngredientTile.fxml new file mode 100644 index 0000000000000000000000000000000000000000..0d01e38bd7c1b21dc39222e4dfdbc352c20880f7 --- /dev/null +++ b/src/main/resources/view/IngredientTile.fxml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<Pane fx:id="ingredientPane" styleClass="ingredient" stylesheets="@../style.css" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.IngredientTileController"> + <children> + <Label fx:id="text" prefHeight="40.0" prefWidth="250.0" text="Ingredient"> + <font> + <Font name="System Italic" size="14.0" /> + </font> + <padding> + <Insets left="15.0" /> + </padding> + </Label> + </children> +</Pane> diff --git a/src/main/resources/view/MainMenuNew.fxml b/src/main/resources/view/MainMenuNew.fxml index bf3e93837c42caf2898f69d64926f13043b88507..cfce53f6a77a4b563d436b135d0cfcc1b89c4f94 100644 --- a/src/main/resources/view/MainMenuNew.fxml +++ b/src/main/resources/view/MainMenuNew.fxml @@ -1,25 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> -<?import javafx.geometry.Insets?> -<?import javafx.scene.control.Button?> -<?import javafx.scene.control.DatePicker?> -<?import javafx.scene.control.Label?> -<?import javafx.scene.control.ProgressBar?> -<?import javafx.scene.image.Image?> -<?import javafx.scene.image.ImageView?> -<?import javafx.scene.layout.AnchorPane?> -<?import javafx.scene.layout.BorderPane?> -<?import javafx.scene.layout.ColumnConstraints?> -<?import javafx.scene.layout.GridPane?> -<?import javafx.scene.layout.Pane?> -<?import javafx.scene.layout.RowConstraints?> -<?import javafx.scene.layout.StackPane?> -<?import javafx.scene.layout.TilePane?> -<?import javafx.scene.layout.VBox?> -<?import javafx.scene.text.Font?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> - -<AnchorPane fx:id="root" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.MainMenu"> +<AnchorPane fx:id="root" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.MainMenu"> <children> <ImageView fitHeight="719.0" fitWidth="1130.0" layoutY="-3.0" pickOnBounds="true" preserveRatio="true"> <image> @@ -189,7 +176,7 @@ </BorderPane> <TilePane alignment="CENTER" hgap="50.0" prefHeight="215.0" prefWidth="1131.0"> <children> - <Button fx:id="foodBtn" alignment="TOP_CENTER" contentDisplay="TOP" mnemonicParsing="false" prefHeight="100.0" prefWidth="150.0" text="Food"> + <Button fx:id="foodBtn" alignment="TOP_CENTER" contentDisplay="TOP" mnemonicParsing="false" onAction="#switchScene" prefHeight="100.0" prefWidth="150.0" text="Food"> <graphic> <ImageView fitHeight="75.0" fitWidth="75.0" pickOnBounds="true" preserveRatio="true"> <image> diff --git a/src/main/resources/view/Recipe.fxml b/src/main/resources/view/Recipe.fxml new file mode 100644 index 0000000000000000000000000000000000000000..45c0e797062d5dd8804f7ddca268d83cbc65c0c6 --- /dev/null +++ b/src/main/resources/view/Recipe.fxml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<AnchorPane prefHeight="695.0" prefWidth="1130.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.RecipeController"> + <children> + <ImageView fitHeight="695.0" fitWidth="1130.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../Images/backgroundMini.jpg" /> + </image></ImageView> + <BorderPane prefHeight="695.0" prefWidth="1130.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <top> + <HBox prefHeight="164.0" prefWidth="1130.0" BorderPane.alignment="CENTER"> + <children> + <Pane prefHeight="136.0" prefWidth="411.0"> + <children> + <Button id="button-stye" fx:id="goBackBtn" layoutX="81.0" layoutY="30.0" mnemonicParsing="false" onAction="#goBack" styleClass="button-style" stylesheets="@../style.css" text="Recipe Suggestions"> + <font> + <Font size="14.0" /> + </font></Button> + <Button fx:id="allRecipesBtn" layoutX="81.0" layoutY="68.0" mnemonicParsing="false" onAction="#toAllRecipes" styleClass="button-style" stylesheets="@../style.css" text="All Recipes" /> + </children> + </Pane> + <Pane prefHeight="103.0" prefWidth="853.0"> + <children> + <Label fx:id="recipeName" alignment="CENTER" contentDisplay="CENTER" layoutX="44.0" layoutY="38.0" text="RecipeName" textAlignment="CENTER"> + <font> + <Font size="36.0" /> + </font> + </Label> + </children> + </Pane> + </children> + </HBox> + </top> + <center> + <HBox prefHeight="100.0" prefWidth="200.0" spacing="50.0" BorderPane.alignment="CENTER"> + <children> + <ScrollPane prefHeight="431.0" prefWidth="271.0" stylesheets="@../style.css"> + <content> + <AnchorPane prefHeight="449.0" prefWidth="262.0" styleClass="ingredient-pane" stylesheets="@../style.css"> + <children> + <VBox fx:id="ingredientList" styleClass="ingredient-list" stylesheets="@../style.css"> + <padding> + <Insets left="15.0" top="20.0" /> + </padding></VBox> + </children> + </AnchorPane> + </content> + </ScrollPane> + <ScrollPane prefHeight="429.0" prefWidth="632.0"> + <content> + <Pane id="recipe-instructions" stylesheets="@../style.css"> + <children> + <Text fx:id="instructions" layoutX="50.0" layoutY="54.0" lineSpacing="10.0" strokeType="OUTSIDE" strokeWidth="0.0" styleClass="recipe-instructions" text="Instructions" wrappingWidth="506.7294921875" /> + </children> + <opaqueInsets> + <Insets /> + </opaqueInsets> + </Pane> + </content> + </ScrollPane> + </children> + </HBox> + </center> + <left> + <Pane prefHeight="457.0" prefWidth="75.0" BorderPane.alignment="CENTER" /> + </left> + <bottom> + <Pane prefHeight="77.0" prefWidth="1130.0" BorderPane.alignment="CENTER" /> + </bottom> + <right> + <Pane prefHeight="454.0" prefWidth="122.0" BorderPane.alignment="CENTER" /> + </right></BorderPane> + </children> +</AnchorPane> diff --git a/src/main/resources/view/RecipeTile.fxml b/src/main/resources/view/RecipeTile.fxml new file mode 100644 index 0000000000000000000000000000000000000000..b4f2c0704ecb6174aee5efd6cdba1f2f6c951f11 --- /dev/null +++ b/src/main/resources/view/RecipeTile.fxml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<VBox fx:id="recipeTile" prefHeight="220.0" prefWidth="280.0" styleClass="recipe-tile" stylesheets="@../style.css" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.RecipeTileController"> + <children> + <Pane fx:id="namePane" prefHeight="178.0" prefWidth="268.0"> + <children> + <Button fx:id="nameTag" mnemonicParsing="false" onAction="#tileClick" prefHeight="149.0" prefWidth="268.0" styleClass="tile-button" stylesheets="@../style.css" text="nameOfRecipe"> + <font> + <Font size="24.0" /> + </font> + </Button> + </children> + </Pane> + <HBox prefHeight="88.0" prefWidth="500.0"> + <children> + <Label alignment="CENTER" prefHeight="63.0" prefWidth="292.0" text="Ingredients missing:"> + <font> + <Font size="18.0" /> + </font> + <opaqueInsets> + <Insets left="20.0" right="20.0" /> + </opaqueInsets> + </Label> + <Label id="noMissingIngredients" fx:id="missingTag" alignment="CENTER" contentDisplay="CENTER" prefHeight="69.0" prefWidth="127.0" text="# missing"> + <font> + <Font size="24.0" /> + </font> + </Label> + </children> + <opaqueInsets> + <Insets left="20.0" right="20.0" /> + </opaqueInsets> + </HBox> + </children> +</VBox> diff --git a/src/main/resources/view/SuggestRecipes.fxml b/src/main/resources/view/SuggestRecipes.fxml new file mode 100644 index 0000000000000000000000000000000000000000..cb8ab9438642ad0c190b6fd6de3c0c676e7b8033 --- /dev/null +++ b/src/main/resources/view/SuggestRecipes.fxml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="695.0" prefWidth="1130.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="no.ntnu.idatt1002.demo.controller.SuggestRecipesController"> + <children> + <ImageView fitHeight="695.0" fitWidth="1130.0" pickOnBounds="true"> + <image> + <Image url="@../Images/backgroundMini.jpg" /> + </image> + </ImageView> + <BorderPane prefHeight="400.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <right> + <VBox id="fridge" prefHeight="577.0" prefWidth="421.0" BorderPane.alignment="CENTER"> + <children> + <Pane prefHeight="64.0" prefWidth="421.0"> + <children> + <Label layoutX="101.0" layoutY="17.0" text="Food in the Fridge" underline="true"> + <font> + <Font size="24.0" /> + </font> + <padding> + <Insets right="20.0" /> + </padding> + </Label> + </children> + </Pane> + <ListView id="list-cell" fx:id="fridgeList" prefHeight="470.0" prefWidth="378.0" stylesheets="@../style.css"> + <VBox.margin> + <Insets right="20.0" /> + </VBox.margin></ListView> + <Pane prefHeight="47.0" prefWidth="421.0"> + <children> + <Button id="button-style" fx:id="addToFridgeBtn" layoutX="138.0" layoutY="9.0" mnemonicParsing="false" onAction="#addIngredient" styleClass="button-style" stylesheets="@../style.css" text="Add to fridge" textAlignment="CENTER"> + <font> + <Font size="14.0" /> + </font> + </Button> + <Button id="button-style" fx:id="removeBtn" layoutX="33.0" layoutY="8.0" mnemonicParsing="false" onAction="#removeFromFridge" styleClass="button-style" stylesheets="@../style.css" text="Remove"> + <font> + <Font size="14.0" /> + </font> + </Button> + </children> + </Pane> + </children> + <padding> + <Insets right="20.0" /> + </padding></VBox> + </right> + <bottom> + <Pane prefHeight="42.0" prefWidth="600.0" BorderPane.alignment="CENTER"> + <children> + <Pane layoutX="72.0" prefHeight="45.0" prefWidth="891.0"> + <children> + <Label fx:id="missingList" styleClass="information-label" stylesheets="@../style.css"> + <font> + <Font name="System Bold" size="14.0" /> + </font> + <padding> + <Insets left="20.0" right="20.0" /> + </padding></Label> + </children> + </Pane> + </children></Pane> + </bottom> + <top> + <BorderPane prefHeight="76.0" prefWidth="600.0" BorderPane.alignment="CENTER"> + <left> + <Pane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER"> + <children> + <Button id="button-style" fx:id="goBackBtn" alignment="CENTER" contentDisplay="CENTER" layoutX="62.0" layoutY="24.0" mnemonicParsing="false" onAction="#switchScene" styleClass="button-style" stylesheets="@../style.css" text="Go Back" textAlignment="CENTER"> + <font> + <Font size="14.0" /> + </font> + </Button> + </children> + </Pane> + </left> + <center> + <Label text="Recipe Suggestions" BorderPane.alignment="CENTER"> + <font> + <Font size="48.0" /> + </font> + </Label> + </center> + <right> + <VBox alignment="CENTER_LEFT" prefHeight="400.0" prefWidth="405.0" BorderPane.alignment="CENTER"> + <children> + <Button fx:id="showAllBtn" mnemonicParsing="false" onAction="#switchScene" styleClass="button-style" stylesheets="@../style.css" text="Or show all recipes" textAlignment="CENTER"> + <VBox.margin> + <Insets left="80.0" /> + </VBox.margin> + </Button> + </children></VBox> + </right> + </BorderPane> + </top> + <center> + <GridPane fx:id="recipeGrid" hgap="20.0" prefWidth="603.0" vgap="20.0" BorderPane.alignment="CENTER"> + <columnConstraints> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" /> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" /> + </columnConstraints> + <rowConstraints> + <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> + <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> + </rowConstraints> + <BorderPane.margin> + <Insets bottom="20.0" left="50.0" right="50.0" top="70.0" /> + </BorderPane.margin> + </GridPane> + </center></BorderPane> + </children> +</AnchorPane> diff --git a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/FileHandlerTest.java b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/FileHandlerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..248985c463aca4c94e938eb1cf6ebe620db94922 --- /dev/null +++ b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/FileHandlerTest.java @@ -0,0 +1,151 @@ +package no.ntnu.idatt1002.demo.data.recipes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +class FileHandlerTest { + + RecipeRegister recipeRegister = new RecipeRegister(); + IngredientsAtHand ingredientsAtHand = new IngredientsAtHand(); + Recipe recipe1; + ArrayList<RecipeIngredient> ingredientsOfRecipe1 = new ArrayList<>(); + + @BeforeEach + void beforeEach() { + recipe1 = new Recipe("Meat, cheese and potatoes", "Instructions"); + + ingredientsOfRecipe1.add(new RecipeIngredient(FoodItem.MINCED_MEAT, 500, MeasuringUnit.GR)); + ingredientsOfRecipe1.add(new RecipeIngredient(FoodItem.POTATO, 750, MeasuringUnit.GR)); + ingredientsOfRecipe1.add(new RecipeIngredient(FoodItem.YELLOW_CHEESE, 2, MeasuringUnit.DL)); + recipe1.addIngredient(FoodItem.MINCED_MEAT, 500, MeasuringUnit.GR); + recipe1.addIngredient(FoodItem.POTATO, 750, MeasuringUnit.GR); + recipe1.addIngredient(FoodItem.YELLOW_CHEESE, 2, MeasuringUnit.DL); + } + + @Nested + @DisplayName("Test supporting methods for wring register to file.") + class testFormattingClasses { + + @Test + @DisplayName("Test that ingredients are correctly formatted before writing to file. ") + void testWritingIngredients() { + String correctFormat = "- MINCED_MEAT | 500.0 | GR\n- POTATO | 750.0 | GR\n- YELLOW_CHEESE | 2.0 | DL\n"; + assertEquals(correctFormat, String.valueOf(FileHandler.formatIngredientList(ingredientsOfRecipe1))); + } + + @Test + @DisplayName("Test behavior for empty ingredient list.") + void testWritingEmptyIngredientList() { + ArrayList<RecipeIngredient> emptyList = new ArrayList<>(); + assertEquals("", String.valueOf(FileHandler.formatIngredientList(emptyList))); + } + + + @Test + @DisplayName("Test that recipes are formatted correctly before writing to file.") + void testFormatRecipe() { + + String correctFormat = """ + # Meat, cheese and potatoes + - MINCED_MEAT | 500.0 | GR + - POTATO | 750.0 | GR + - YELLOW_CHEESE | 2.0 | DL + + Instructions + + """; + assertEquals(correctFormat, String.valueOf(FileHandler.formatRecipe(recipe1))); + } + + } + + + @Nested + @DisplayName("Test writing a recipe register to file.") + class testWritingRecipeRegister { + + @Test + @DisplayName("Test that writing empty register to file creates blank file.") + void writeEmptyRegister() { + RecipeRegister emptyRegister = new RecipeRegister(); + assertAll(() -> FileHandler.writeRegister(emptyRegister, "emptyRegister")); + } + + @Test + @DisplayName("Test that writing null register to file throws exception.") + void writeNullRegister() { + assertThrows(IllegalArgumentException.class, () -> FileHandler.writeRegister(null, "noRegister")); + } + + @Test + @DisplayName("Write recipe register correctly to file as text.") + void writeRecipeRegisterToFile() { + assertAll(() -> FileHandler.writeRegister(recipeRegister, "RecipeRegister")); + } + } + + + + @Nested + @DisplayName("Test reading a recipe register from file.") + class testReadRecipeRegister { + + @Test + @DisplayName("Read recipe register correctly from file to object.") + void readRecipeRegisterFromFile() { + assertAll(() -> FileHandler.readRecipeRegister("testReadRegisterFromFile")); + + RecipeRegister registerReadFromTestFile = FileHandler.readRecipeRegister("testReadRegisterFromFile"); + assert registerReadFromTestFile != null; + + assertEquals(new RecipeIngredient(FoodItem.MINCED_MEAT, 500, MeasuringUnit.GR), + registerReadFromTestFile.getRecipe("Meat, cheese and potatoes").getIngredient(FoodItem.MINCED_MEAT)); + assertEquals(new RecipeIngredient(FoodItem.LEMON, 750, MeasuringUnit.GR), + registerReadFromTestFile.getRecipe("Meat, cheese and lemons").getIngredient(FoodItem.LEMON)); + } + } + + + + @Nested + @DisplayName("Test writing and reading an IngredientsAtHand object to and from file.") + class testIngredientsAtHandStorage { + + @BeforeEach + void setUpIngredientsAtHand() { + ingredientsAtHand.addIngredient(FoodItem.POTATO); + ingredientsAtHand.addIngredient(FoodItem.MILK); + ingredientsAtHand.addIngredient(FoodItem.LEMON); + ingredientsAtHand.addIngredient(FoodItem.MINCED_MEAT); + } + + @Test + @DisplayName("Write empty ingredients at hand to file.") + void writeEmptyIngredientsAtHandToFile() { + IngredientsAtHand emptyAtHand = new IngredientsAtHand(); + assertAll(() -> FileHandler.writeIngredientsAtHand(emptyAtHand, "EmptyAtHandRegister")); + assertEquals(0, Objects.requireNonNull(FileHandler.readIngredientsAtHand("EmptyAtHandRegister")).getIngredientsAtHand().size()); + + } + + @Test + @DisplayName("Write ingredients at hand to file.") + void writeIngredientsAtHandToFile() { + assertAll(() -> FileHandler.writeIngredientsAtHand(ingredientsAtHand, "AtHandRegister")); + } + + @Test + @DisplayName("Read ingredients at hand from file.") + void readIngredientsAtHandFromFile() { + assertAll(() -> FileHandler.writeIngredientsAtHand(ingredientsAtHand, "AtHandRegister")); + assertEquals(ingredientsAtHand.getIngredientsAtHand().size(), + Objects.requireNonNull(FileHandler.readIngredientsAtHand("AtHandRegister")).getIngredientsAtHand().size()); + } + } +} \ No newline at end of file diff --git a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientTest.java b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientTest.java index 79dd0439298817a773fc8ac9c6c70f19575c0957..322b967efbf7da712f1a9d3c62579af4e073f415 100644 --- a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientTest.java +++ b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientTest.java @@ -2,6 +2,7 @@ package no.ntnu.idatt1002.demo.data.recipes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -15,62 +16,86 @@ class IngredientTest { testIngredient = new Ingredient(FoodItem.ONION, 0.5f, MeasuringUnit.KG); } - @Test - @DisplayName("The constructor creates an ingredient object successfully") - void constructValidIngredient() { - Ingredient validIngredient = new Ingredient(FoodItem.ONION, 1, MeasuringUnit.KG); - assertEquals(validIngredient, new Ingredient(FoodItem.ONION, 1, MeasuringUnit.KG)); - } + @Nested + @DisplayName("Test creation of an Ingredient") + class testCreation{ - @Test - @DisplayName("The constructor throws exceptions for illegal input.") - void constructorThrowsExceptionsWhenItShould() { - assertThrows(IllegalArgumentException.class, () -> new Ingredient(null, 2, MeasuringUnit.DL)); - assertThrows(IllegalArgumentException.class, () -> new Ingredient(FoodItem.ONION, -5, MeasuringUnit.DL)); - assertThrows(IllegalArgumentException.class, () -> new Ingredient(FoodItem.ONION, 0, MeasuringUnit.DL)); - assertThrows(IllegalArgumentException.class, () -> new Ingredient(FoodItem.ONION, 2, null)); - } + // Indirectly tests equals method. + @Test + @DisplayName("The constructor creates an ingredient object successfully") + void constructValidIngredient() { + Ingredient validIngredient = new Ingredient(FoodItem.ONION, 1, MeasuringUnit.KG); + assertEquals(validIngredient, new Ingredient(FoodItem.ONION, 1, MeasuringUnit.KG)); + } - @Test - @DisplayName("Change of food type works for valid food type.") - void setFoodTypeWorksForValidType() { - testIngredient.setFoodType(FoodItem.LEMON); - assertEquals(new Ingredient(FoodItem.LEMON, 0.5f, MeasuringUnit.KG), testIngredient); + @Test + @DisplayName("The constructor throws exceptions for illegal input.") + void constructorThrowsExceptionsWhenItShould() { + assertThrows(IllegalArgumentException.class, () -> new Ingredient(null, 2, MeasuringUnit.DL)); + assertThrows(IllegalArgumentException.class, () -> new Ingredient(FoodItem.ONION, -5, MeasuringUnit.DL)); + assertThrows(IllegalArgumentException.class, () -> new Ingredient(FoodItem.ONION, 0, MeasuringUnit.DL)); + assertThrows(IllegalArgumentException.class, () -> new Ingredient(FoodItem.ONION, 2, null)); + } } - @Test - @DisplayName("Change of food type to invalid type throws exception.") - void setFoodTypeThrowsException() { - assertThrows(IllegalArgumentException.class, () -> testIngredient.setFoodType(null)); - } + @Nested + @DisplayName("Test alternation of Ingredient object") + class testAlternations{ - @Test - @DisplayName("Change of food amount works for valid amount.") - void setAmountWorksForValidAmount() { - testIngredient.setAmount(2.5); - assertEquals(new Ingredient(FoodItem.ONION, 2.5f, MeasuringUnit.KG), testIngredient); - } + @Nested + @DisplayName("Valid alternations") + class testValidAlternations { + @Test + @DisplayName("Change of food type works for valid food type.") + void setFoodTypeWorksForValidType() { + testIngredient.setFoodType(FoodItem.LEMON); + assertEquals(new Ingredient(FoodItem.LEMON, 0.5f, MeasuringUnit.KG), testIngredient); + } - @Test - @DisplayName("Change of food amount to invalid amount throws exception.") - void setAmountThrowsException() { - assertThrows(IllegalArgumentException.class, () -> testIngredient.setAmount(0)); - assertThrows(IllegalArgumentException.class, () -> testIngredient.setAmount(-1)); - } + @Test + @DisplayName("Change of food amount works for valid amount.") + void setAmountWorksForValidAmount() { + testIngredient.setAmount(2.5); + assertEquals(new Ingredient(FoodItem.ONION, 2.5f, MeasuringUnit.KG), testIngredient); + } + + @Test + @DisplayName("Change of measuring unit works for valid unit.") + void setUnitWorksForValidUnit() { + testIngredient.setUnit(MeasuringUnit.TBS); + assertEquals(new Ingredient(FoodItem.ONION, 0.5f, MeasuringUnit.TBS), testIngredient); + } - @Test - @DisplayName("Change of measuring unit works for valid unit.") - void setUnitWorksForValidUnit() { - testIngredient.setUnit(MeasuringUnit.TBS); - assertEquals(new Ingredient(FoodItem.ONION, 0.5f, MeasuringUnit.TBS), testIngredient); - } - @Test - @DisplayName("Change of measuring to invalid unit throws exception.") - void setUnitThrowsException() { - assertThrows(IllegalArgumentException.class, () -> testIngredient.setUnit(null)); + } + + @Nested + @DisplayName("Invalid alternations") + class testInvalidAlternations { + @Test + @DisplayName("Change of food type to invalid type throws exception.") + void setFoodTypeThrowsException() { + assertThrows(IllegalArgumentException.class, () -> testIngredient.setFoodType(null)); + } + + @Test + @DisplayName("Change of food amount to invalid amount throws exception.") + void setAmountThrowsException() { + assertThrows(IllegalArgumentException.class, () -> testIngredient.setAmount(0)); + assertThrows(IllegalArgumentException.class, () -> testIngredient.setAmount(-1)); + } + + @Test + @DisplayName("Change of measuring to invalid unit throws exception.") + void setUnitThrowsException() { + assertThrows(IllegalArgumentException.class, () -> testIngredient.setUnit(null)); + } + } + } + + //TODO: Test for equals method? } \ No newline at end of file diff --git a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHandTest.java b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHandTest.java index 369d7c43de2309fda39160a9cbe50b566075d800..d2cee2ad1bcf310265c4df61e3e37e77b380e237 100644 --- a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHandTest.java +++ b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/IngredientsAtHandTest.java @@ -2,6 +2,7 @@ package no.ntnu.idatt1002.demo.data.recipes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -12,81 +13,76 @@ class IngredientsAtHandTest { @BeforeEach void beforeEach() { - ingredientsAtHand.addIngredient(new Ingredient(FoodItem.LEMON, 3, MeasuringUnit.PC)); - ingredientsAtHand.addIngredient(new Ingredient(FoodItem.MILK, 1.5f, MeasuringUnit.L)); - ingredientsAtHand.addIngredient(new Ingredient(FoodItem.MINCED_MEAT, 400, MeasuringUnit.GR)); - - } - - @Test - @DisplayName("The getIngredient method returns the correct ingredient.") - void getIngredientReturnsSuccessfully() { - assertEquals(new Ingredient(FoodItem.LEMON, 3, MeasuringUnit.PC), - ingredientsAtHand.getIngredient(FoodItem.LEMON)); - - } - - @Test - @DisplayName("The getIngredient method returns null if the ingredient is not in the collection.") - void getIngredientReturnsNullWhenNotFoundOrNull(){ - assertNull(ingredientsAtHand.getIngredient(FoodItem.ONION)); - assertNull(ingredientsAtHand.getIngredient(null)); + ingredientsAtHand.addIngredient(FoodItem.LEMON); + ingredientsAtHand.addIngredient(FoodItem.MILK); + ingredientsAtHand.addIngredient(FoodItem.MINCED_MEAT); } - @Test - @DisplayName("Altering ingredient successfully and return true.") - void alterIngredientSuccessfully() { - assertNotEquals(new Ingredient(FoodItem.ONION, 500, MeasuringUnit.GR), - ingredientsAtHand.getIngredient(FoodItem.ONION)); - - assertTrue(ingredientsAtHand.alterIngredient(FoodItem.ONION, 500, MeasuringUnit.GR)); - - assertEquals(new Ingredient(FoodItem.ONION, 500, MeasuringUnit.GR), - ingredientsAtHand.getIngredient(FoodItem.ONION)); - + @Nested + @DisplayName("Test addition of food types to the collection.") + class testAddIngredient { + int numberOfIngredientsBefore; + + @BeforeEach + void getNumberOfIngredientsAtHand() { + numberOfIngredientsBefore = ingredientsAtHand.getIngredientsAtHand().size(); + } + + @Test + @DisplayName("A valid food type is added successfully.") + void addValidFoodType() { + ingredientsAtHand.addIngredient(FoodItem.POTATO); + assertEquals(numberOfIngredientsBefore+1, ingredientsAtHand.getIngredientsAtHand().size()); + } + + @Test + @DisplayName("An invalid(null) food type is not added to the collection.") + void notAddNullFoodType() { + ingredientsAtHand.addIngredient(null); + assertEquals(numberOfIngredientsBefore, ingredientsAtHand.getIngredientsAtHand().size()); + } + + @Test + @DisplayName("An duplicate food type is not added to the collection.") + void notAddDuplicateFoodType() { + ingredientsAtHand.addIngredient(FoodItem.LEMON); + assertEquals(numberOfIngredientsBefore, ingredientsAtHand.getIngredientsAtHand().size()); + } } - @Test - @DisplayName("Altering ingredient that does not yet exist in collection adds it and returns true.") - void alterNewIngredientAddsIt() { - int ingredientsAtStart = ingredientsAtHand.getIngredientsAtHand().size(); - assertTrue(ingredientsAtHand.alterIngredient(FoodItem.ORANGE, 8, MeasuringUnit.PC)); - assertEquals(ingredientsAtStart + 1, ingredientsAtHand.getIngredientsAtHand().size()); + @Nested + @DisplayName("Test the atHand method of the ingredients at hand class. ") + class testAtHandMethod { + @Test + @DisplayName("The atHand method returns true correctly when the food type is at hand.") + void atHandReturnsTrueSuccessfully() { + assertTrue(ingredientsAtHand.atHand(FoodItem.LEMON)); + } + + @Test + @DisplayName("The atHand method returns false correctly when the food type is not at hand.") + void atHandReturnsFalseSuccessfully() { + assertFalse(ingredientsAtHand.atHand(FoodItem.YELLOW_CHEESE)); + } } - @Test - @DisplayName("Attempting to alter ingredient in illegal way does not alter the collection and returns false.") - void alterIngredientUnchangedForInvalidChange() { - assertFalse(ingredientsAtHand.alterIngredient(null, 350, MeasuringUnit.GR)); - assertEquals(new Ingredient(FoodItem.LEMON, 3, MeasuringUnit.PC), - ingredientsAtHand.getIngredient(FoodItem.LEMON)); + @Nested + @DisplayName("Test the removeIngredient method of the ingredients at hand class. ") + class testRemoveIngredientMethod { + @Test + @DisplayName("Ingredient is removed successfully and true is returned.") + void removeIngredientSuccessfully() { + int ingredientsAtStart = ingredientsAtHand.getIngredientsAtHand().size(); + assertTrue(ingredientsAtHand.removeIngredient(FoodItem.LEMON)); + assertEquals(ingredientsAtStart-1, ingredientsAtHand.getIngredientsAtHand().size()); + } + + @Test + @DisplayName("Removing ingredient that is not in the collection, leaves it unchanged and returns false.") + void removeIngredientNotInCollection() { + assertFalse(ingredientsAtHand.removeIngredient(FoodItem.SALSA_SAUCE)); + } } - - @Test - @DisplayName("Altering an ingredients without changing values, leaves no change on the collection and returns true.") - void alterNothingLeavesCollectionIntact() { - int ingredientsAtStart = ingredientsAtHand.getIngredientsAtHand().size(); - - assertTrue(ingredientsAtHand.alterIngredient(FoodItem.LEMON, 3, MeasuringUnit.PC)); - - assertEquals(ingredientsAtStart, ingredientsAtHand.getIngredientsAtHand().size()); - } - - @Test - @DisplayName("Ingredient is removed successfully and true is returned.") - void removeIngredientSuccessfully() { - int ingredientsAtStart = ingredientsAtHand.getIngredientsAtHand().size(); - assertTrue(ingredientsAtHand.removeIngredient(FoodItem.LEMON)); - assertEquals(ingredientsAtStart-1, ingredientsAtHand.getIngredientsAtHand().size()); - } - - @Test - @DisplayName("Removing ingredient that is not in the collection, leaves it unchanged and returns false.") - void removeIngredientNotInCollection() { - - assertFalse(ingredientsAtHand.removeIngredient(FoodItem.SALSA_SAUCE)); - } - } \ No newline at end of file diff --git a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredientTest.java b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..31c7400cf3fa20be2233c0ae5e171800dc252ec4 --- /dev/null +++ b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeIngredientTest.java @@ -0,0 +1,41 @@ +package no.ntnu.idatt1002.demo.data.recipes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RecipeIngredientTest { + + RecipeIngredient testIngredient; + + @BeforeEach + void beforeEach() { + testIngredient = new RecipeIngredient(FoodItem.ONION, 0.5f, MeasuringUnit.KG); + } + + @Test + @DisplayName("Check that recipe ingredient is initiated not at hand") + void initiateAsNotAtHand() { + assertFalse(testIngredient.isAtHand()); + } + + @Test + @DisplayName("AtHand correctly set") + void setAtHandCorrectly() { + assertFalse(testIngredient.isAtHand()); + testIngredient.setAtHand(false); + assertFalse(testIngredient.isAtHand()); + } + + @Test + @DisplayName("Validate that two ingredients are equal despite at hand status.") + void equalIngredientsDespiteStatus() { + RecipeIngredient secondTestIngredient = new RecipeIngredient(FoodItem.ONION, 0.5f, MeasuringUnit.KG); + testIngredient.setAtHand(true); + + assertEquals(testIngredient, secondTestIngredient); + } + +} \ No newline at end of file diff --git a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegisterTest.java b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegisterTest.java index 4176a2451f7ed7ba28f9653b5f77ef1ba00abd49..4b274f3b99e14c6ec6c3af5b25d48f192c6cb962 100644 --- a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegisterTest.java +++ b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeRegisterTest.java @@ -2,6 +2,7 @@ package no.ntnu.idatt1002.demo.data.recipes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -9,37 +10,160 @@ import static org.junit.jupiter.api.Assertions.*; class RecipeRegisterTest { RecipeRegister myRecipes; + int numberOfRecipes; @BeforeEach void beforeEach() { myRecipes = new RecipeRegister(); - myRecipes.addRecipe(new Recipe("Pasta al Limone", "Instructions")); + myRecipes.addRecipe(new Recipe("Lemon pasta", "Instructions")); + numberOfRecipes = myRecipes.getRecipes().size(); } - @Test - @DisplayName("Adding recipe to register successfully.") - void addRecipeToRegister() { - int numberOfRecipes = myRecipes.getRecipes().size(); - myRecipes.addRecipe(new Recipe("Carbonara", "Instructions")); - assertEquals(numberOfRecipes + 1, myRecipes.getRecipes().size()); - } - @Test - @DisplayName("Throw exception when adding already registered recipe.") - void recipeAlreadyInRegister() { - assertThrows(IllegalArgumentException.class, () -> myRecipes.addRecipe( new Recipe("Pasta al Limone", "Instructions"))); - } + @Nested + @DisplayName("Test that addRecipe method works as intended.") + class testAddRecipe { + + @Test + @DisplayName("Adding recipe to register successfully.") + void addRecipeToRegister() { + assertAll(() -> myRecipes.addRecipe(new Recipe("Meat stew", "Instructions"))); + assertEquals(numberOfRecipes + 1, myRecipes.getRecipes().size()); + } + + @Test + @DisplayName("When adding already registered recipe, replace the old one.") + void recipeAlreadyInRegister() { + assertAll(() -> myRecipes.addRecipe( new Recipe("Lemon pasta", "Instructions 2"))); + assertEquals(numberOfRecipes, myRecipes.getRecipes().size()); + assertEquals("Instructions 2", myRecipes.getRecipe("Lemon pasta").getInstructions()); + } - @Test - @DisplayName("Throw exception when adding null recipe.") - void addNullRecipe() { - assertThrows(NullPointerException.class, () -> myRecipes.addRecipe(null)); + @Test + @DisplayName("Throw exception when adding null recipe.") + void addNullRecipe() { + assertThrows(NullPointerException.class, () -> myRecipes.addRecipe(null)); + } } - @Test - @DisplayName("Returns correct recipe based on name.") - void getRecipeByName() { - assertEquals(new Recipe("Pasta al Limone", "Instructions"), myRecipes.getRecipe("Pasta al Limone")); + + @Nested + @DisplayName("Test functionality of 'getRecipe' method of register.") + class testGetRecipe { + + @Test + @DisplayName("Returns correct recipe based on name.") + void getRecipeByName() { + assertEquals(new Recipe("Lemon pasta", "Instructions"), myRecipes.getRecipe("Lemon pasta")); + } + + @Test + @DisplayName("Returns null for empty string.") + void getRecipeByNullName() { + assertNull(myRecipes.getRecipe(null)); + } + + @Test + @DisplayName("Returns null for recipe not present in register.") + void getNoRecipeByName() { + assertNull(myRecipes.getRecipe("Pancakes")); + } } + + @Nested + @DisplayName("Test functionality of 'pickBestFits' method of register class.") + class testBestPick { + + IngredientsAtHand ingredientsAtHand; + RecipeRegister newRecipes; + Recipe recipe1; + Recipe recipe2; + Recipe recipe3; + Recipe recipe4; + + + @BeforeEach + void setUpRecipes() { + ingredientsAtHand = new IngredientsAtHand(); + ingredientsAtHand.addIngredient(FoodItem.LEMON); + + newRecipes = new RecipeRegister(); + + recipe1 = new Recipe("Recipe with 2 ingredients", "Instructions"); + recipe1.addIngredient(FoodItem.LEMON, 2, MeasuringUnit.PC); + recipe1.addIngredient(FoodItem.ONION, 2, MeasuringUnit.PC); + + recipe2 = new Recipe("Recipe with 3 ingredients", "Instructions"); + recipe2.addIngredient(FoodItem.POTATO, 2, MeasuringUnit.PC); + recipe2.addIngredient(FoodItem.ONION, 2, MeasuringUnit.PC); + recipe2.addIngredient(FoodItem.BROCCOLI, 2, MeasuringUnit.PC); + + recipe3 = new Recipe("Recipe with 4 ingredients", "Instructions"); + recipe3.addIngredient(FoodItem.POTATO, 2, MeasuringUnit.PC); + recipe3.addIngredient(FoodItem.ONION, 2, MeasuringUnit.PC); + recipe3.addIngredient(FoodItem.BROCCOLI, 2, MeasuringUnit.PC); + recipe3.addIngredient(FoodItem.OIL, 2, MeasuringUnit.PC); + + recipe4 = new Recipe("Recipe with 5 ingredients", "Instructions"); + recipe4.addIngredient(FoodItem.POTATO, 2, MeasuringUnit.PC); + recipe4.addIngredient(FoodItem.ONION, 2, MeasuringUnit.PC); + recipe4.addIngredient(FoodItem.BROCCOLI, 2, MeasuringUnit.PC); + recipe4.addIngredient(FoodItem.OIL, 2, MeasuringUnit.PC); + recipe4.addIngredient(FoodItem.BELL_PEPPER, 2, MeasuringUnit.PC); + + newRecipes.addRecipe(recipe4); + newRecipes.addRecipe(recipe3); + newRecipes.addRecipe(recipe2); + newRecipes.addRecipe(recipe1); + } + + @Test + @DisplayName("Pick one recipe correctly.") + void pickOneBestRecipe() { + assertEquals(recipe1, newRecipes.pickBestFits(1, ingredientsAtHand).get(0)); + assertEquals(1, newRecipes.pickBestFits(1, ingredientsAtHand).size()); + } + + @Test + @DisplayName("Attempt at picking more recipes than in register. ") + void pickMoreThanAvailableRecipes() { + assertAll(() -> newRecipes.pickBestFits(6, ingredientsAtHand)); + assertEquals(4, newRecipes.pickBestFits(6, ingredientsAtHand).size()); + } + + @Test + @DisplayName("Pick based on no ingredients at hand.") + void pickWithNoIngredientsAtHand() { + IngredientsAtHand noIngredientsAtHand = new IngredientsAtHand(); + assertAll(() -> newRecipes.pickBestFits(3, noIngredientsAtHand)); + assertEquals(recipe1, newRecipes.pickBestFits(3, noIngredientsAtHand).get(0) ); + assertEquals(3, newRecipes.pickBestFits(3, noIngredientsAtHand).size() ); + } + + @Test + @DisplayName("Pick zero recipes returns empty ArrayList.") + void pickNoRecipes() { + assertAll(() -> newRecipes.pickBestFits(0, ingredientsAtHand)); + assertEquals(0, newRecipes.pickBestFits(0, ingredientsAtHand).size() ); + } + + @Test + @DisplayName("Correct first suggestion after reorder of best fit.") + void suggestionsInCorrectOrder() { + assertAll(() -> newRecipes.pickBestFits(4, ingredientsAtHand)); + assertEquals(recipe1, newRecipes.pickBestFits(4, ingredientsAtHand).get(0)); + assertEquals(recipe2, newRecipes.pickBestFits(4, ingredientsAtHand).get(1)); + assertEquals(recipe3, newRecipes.pickBestFits(4, ingredientsAtHand).get(2)); + assertEquals(recipe4, newRecipes.pickBestFits(4, ingredientsAtHand).get(3)); + + ingredientsAtHand.removeIngredient(FoodItem.LEMON); + ingredientsAtHand.addIngredient(FoodItem.BROCCOLI); + ingredientsAtHand.addIngredient(FoodItem.ONION); + + assertEquals(recipe2, newRecipes.pickBestFits(4, ingredientsAtHand).get(0)); + assertEquals(recipe4, newRecipes.pickBestFits(4, ingredientsAtHand).get(3)); + + } + } } \ No newline at end of file diff --git a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeTest.java b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeTest.java index 46d4955720b089e6391f3e5f0aaa5f70fb014b06..f6cb6cd3d5e68469d342a8525d93bbfea604ef21 100644 --- a/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeTest.java +++ b/src/test/java/no/ntnu/idatt1002/demo/data/recipes/RecipeTest.java @@ -2,6 +2,7 @@ package no.ntnu.idatt1002.demo.data.recipes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -17,101 +18,144 @@ class RecipeTest { recipe.addIngredient(FoodItem.MINCED_MEAT, 500, MeasuringUnit.GR); recipe.addIngredient(FoodItem.POTATO, 750, MeasuringUnit.GR); recipe.addIngredient(FoodItem.YELLOW_CHEESE, 2, MeasuringUnit.DL); - - } - - @Test - @DisplayName("Constructor creates Recipe successfully.") - void constructorCreatesRecipe() { - assertEquals(recipe, new Recipe("Meat, cheese and potatoes", "Instructions")); - // Recipes are equal if name and Instructions are equal. } - @Test - @DisplayName("Constructor throws exception for empty name or instruction.") - void constructorThrowsException() { - assertThrows(IllegalArgumentException.class, () -> new Recipe("", "Instructions")); - assertThrows(IllegalArgumentException.class, () -> new Recipe("Name", "")); + @Nested + @DisplayName("Test functionality of the Recipe constructor.") + class testConstructor { - } + @Test + @DisplayName("Constructor creates Recipe successfully.") + void constructorCreatesRecipe() { + // Two recipes are considered equal if their name's match. + assertEquals(recipe, new Recipe("Meat, cheese and potatoes", "Instructions")); - @Test - @DisplayName("Can change name of recipe successfully.") - void setNameSuccessfully() { - String newName = "Meat and cheese"; - recipe.setName(newName); - assertEquals(newName, recipe.getName()); - } + assertEquals("Meat, cheese and potatoes", recipe.getName()); + assertEquals(3, recipe.getIngredientList().size()); + assertEquals("Instructions", recipe.getInstructions()); + } - @Test - @DisplayName("Throws exception when name set to empty string.") - void setNameEmptyThrowsException() { - assertThrows(IllegalArgumentException.class, () -> recipe.setName(" ")); - } + @Test + @DisplayName("Constructor throws exception for empty name or instruction.") + void constructorThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new Recipe("", "Instructions")); + assertThrows(IllegalArgumentException.class, () -> new Recipe("Name", "")); + } - @Test - @DisplayName("Can change instructions of recipe successfully.") - void setInstructionsSuccessfully() { - String newInstruction = "New instructions"; - recipe.setInstructions(newInstruction); - assertEquals(newInstruction, recipe.getInstructions()); } - @Test - @DisplayName("Throws exception when Instructions set to empty string.") - void setInstructionsEmptyThrowsException() { - assertThrows(IllegalArgumentException.class, () -> recipe.setInstructions("")); - } + @Nested + @DisplayName("Test methods to set name and instructions for recipe.") + class testSetNameAndInstructions { + + @Test + @DisplayName("Can change name of recipe successfully.") + void setNameSuccessfully() { + String newName = "Meat and cheese"; + recipe.setName(newName); + assertEquals(newName, recipe.getName()); + } + + @Test + @DisplayName("Throws exception when name set to empty string.") + void setNameEmptyThrowsException() { + assertThrows(IllegalArgumentException.class, () -> recipe.setName(" ")); + } + + @Test + @DisplayName("Can change instructions of recipe successfully.") + void setInstructionsSuccessfully() { + String newInstruction = "New instructions"; + recipe.setInstructions(newInstruction); + assertEquals(newInstruction, recipe.getInstructions()); + } + + @Test + @DisplayName("Throws exception when Instructions set to empty string.") + void setInstructionsEmptyThrowsException() { + assertThrows(IllegalArgumentException.class, () -> recipe.setInstructions("")); + } - @Test - @DisplayName("Ingredient is added to recipe successfully.") - void addNewIngredient() { - int ingredientsInRecipe = recipe.getIngredientList().size(); - assertTrue(recipe.addIngredient(FoodItem.LEMON, 1, MeasuringUnit.PC)); - assertEquals(ingredientsInRecipe + 1, recipe.getIngredientList().size()); } - @Test - @DisplayName("Ingredient already in recipe does not alter and returns true.") - void addAlreadyIncludedIngredient() { - int ingredientsInRecipe = recipe.getIngredientList().size(); - assertTrue(recipe.addIngredient(FoodItem.MINCED_MEAT, 500, MeasuringUnit.GR)); - assertEquals(ingredientsInRecipe, recipe.getIngredientList().size()); - } - - @Test - @DisplayName("Invalid ingredient change is not made and false is returned.") - void addInvalidIngredientReturnsFalse() { - int ingredientsInRecipe = recipe.getIngredientList().size(); - assertFalse(recipe.addIngredient(null, 500, MeasuringUnit.GR)); - assertEquals(ingredientsInRecipe, recipe.getIngredientList().size()); - } - - - @Test - @DisplayName("Updating ingredient status works as expected.") - void updateIngredientStatus() { - IngredientsAtHand inFridge = new IngredientsAtHand(); - inFridge.addIngredient(new Ingredient(FoodItem.MINCED_MEAT, 400, MeasuringUnit.GR)); - inFridge.addIngredient(new Ingredient(FoodItem.YELLOW_CHEESE, 500, MeasuringUnit.GR)); + @Nested + @DisplayName("Test method to add or alter ingredients in the recipe.") + class testAddIngredient { + + + @Test + @DisplayName("New Ingredient is added to recipe successfully.") + void addNewIngredient() { + int ingredientsInRecipe = recipe.getIngredientList().size(); + assertTrue(recipe.addIngredient(FoodItem.LEMON, 1, MeasuringUnit.PC)); + assertEquals(ingredientsInRecipe + 1, recipe.getIngredientList().size()); + } + + @Test + @DisplayName("Ingredient already in recipe does not alter and returns true.") + void addAlreadyIncludedIngredient() { + int ingredientsInRecipe = recipe.getIngredientList().size(); + assertTrue(recipe.addIngredient(FoodItem.MINCED_MEAT, 500, MeasuringUnit.GR)); + assertEquals(ingredientsInRecipe, recipe.getIngredientList().size()); + } + + @Test + @DisplayName("Ingredient already in recipe with different amount alters and returns true.") + void addAlteredIngredient() { + int ingredientsInRecipe = recipe.getIngredientList().size(); + assertEquals(500, recipe.getIngredient(FoodItem.MINCED_MEAT).getAmount()); + assertTrue(recipe.addIngredient(FoodItem.MINCED_MEAT, 10, MeasuringUnit.DL)); + assertEquals(ingredientsInRecipe, recipe.getIngredientList().size()); + assertEquals(10, recipe.getIngredient(FoodItem.MINCED_MEAT).getAmount()); + } + + @Test + @DisplayName("Invalid ingredient change is not made and false is returned.") + void addInvalidIngredientReturnsFalse() { + int ingredientsInRecipe = recipe.getIngredientList().size(); + assertFalse(recipe.addIngredient(null, 500, MeasuringUnit.GR)); + assertEquals(ingredientsInRecipe, recipe.getIngredientList().size()); + } - recipe.getIngredientList().forEach((ingredient) -> assertFalse(ingredient.isAtHand())); - recipe.updateIngredientStatus(inFridge); - - assertTrue(recipe.getIngredient(FoodItem.MINCED_MEAT).isAtHand()); - assertTrue(recipe.getIngredient(FoodItem.YELLOW_CHEESE).isAtHand()); - assertFalse(recipe.getIngredient(FoodItem.POTATO).isAtHand()); } - @Test - @DisplayName("Update of ingredient status based on empty or null ingredients at hand collection.") - void nullOrEmptyIngredientsAtHand() { - IngredientsAtHand emptyFridge = new IngredientsAtHand(); - - assertThrows(NullPointerException.class, () -> recipe.updateIngredientStatus(null)); - assertThrows(IllegalArgumentException.class, () -> recipe.updateIngredientStatus(emptyFridge)); + @Nested + @DisplayName("Test update method for ingredient status of Recipe class") + class testUpdateIngredientStatus { + + + @Test + @DisplayName("Updating ingredient status works as expected.") + void updateIngredientStatus() { + IngredientsAtHand inFridge = new IngredientsAtHand(); + inFridge.addIngredient(FoodItem.MINCED_MEAT); + inFridge.addIngredient(FoodItem.YELLOW_CHEESE); + + recipe.getIngredientList().forEach((ingredient) -> assertFalse(ingredient.isAtHand())); + recipe.updateIngredientStatus(inFridge); + + assertTrue(recipe.getIngredient(FoodItem.MINCED_MEAT).isAtHand()); + assertTrue(recipe.getIngredient(FoodItem.YELLOW_CHEESE).isAtHand()); + assertFalse(recipe.getIngredient(FoodItem.POTATO).isAtHand()); + } + + @Test + @DisplayName("Update of ingredient status based on empty or null ingredients at hand collection.") + void nullOrEmptyIngredientsAtHand() { + assertThrows(NullPointerException.class, () -> recipe.updateIngredientStatus(null)); + } + + @Test + @DisplayName("Update of ingredients status when ingredients at hand is empty.") + void emptyIngredientsAtHand() { + IngredientsAtHand emptyFridge = new IngredientsAtHand(); + assertAll(() -> recipe.updateIngredientStatus(emptyFridge)); + + assertFalse(recipe.getIngredient(FoodItem.MINCED_MEAT).isAtHand()); + assertFalse(recipe.getIngredient(FoodItem.YELLOW_CHEESE).isAtHand()); + assertFalse(recipe.getIngredient(FoodItem.POTATO).isAtHand()); + } } - } \ No newline at end of file