diff --git a/.gitignore b/.gitignore index 2873e189e1..30b1a9d588 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,11 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +GroceryList.log +GroceryList.log.lck + +/data + +/.vscode +/META-INF diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..733b5072eb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "markis.code-coverage" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..c5f3f6b9c7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..c95a1c268d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to Grocery in Time + +First off, thank you for considering contributing to Grocery in Time. It’s people like you that make Grocery in Time such a great tool. + +## Where do I go from here? + +If you've noticed a bug or have a question, [search the issue tracker](https://github.com/AY2324S2-CS2113-T12-2/tp/issues) to see if someone else in the community has already created a ticket. If not, go ahead and make one! + +## Contributing Code + +If you'd like to contribute code with a new feature or a fix, please make sure to check out [our issues](https://github.com/AY2324S2-CS2113-T12-2/tp/issues). It's a good start to see what has been reported or suggested before diving in. + +### Getting Started + +* Make sure you have a [GitHub account](https://github.com/signup/free) +* Submit a ticket for your issue, assuming one does not already exist. +* Fork the repository on GitHub. + +### Making Changes + +* Create a topic branch from where you want to base your work. +* Make commits of logical units. +* Make sure your commit messages are in the proper format (see below). +* Push your changes to a topic branch in your fork of the repository. + +### Commit Messages + +Please follow these guidelines for commit messages: + +- Use the present tense ("Add feature" not "Added feature"). +- Use the imperative mood ("Move cursor to..." not "Moves cursor to..."). +- Limit the first line to 72 characters or less. +- Reference issues and pull requests liberally after the first line. + + +### Submitting Changes + +* Push your changes to a topic branch in your fork of the repository. +* Submit a pull request to [the repository](https://github.com/AY2324S2-CS2113-T12-2/tp) in the organization. +* After feedback has been given, we expect responses within two weeks. After two weeks, we may close the pull request if it isn't showing any activity. + +## Coding conventions + +Start reading our code and you'll get the hang of it. We optimize for readability: + +* We indent using four spaces (tabs) +* We ALWAYS put spaces after list items and method parameters (`[1, 2, 3]`, not `[1,2,3]`), and around operators (`x += 1`, not `x+=1`). +* This project is built with Java, so refer to our `.editorconfig` file and Checkstyle rules for more conventions. +* Write tests for new features or fixes. We are using JUnit for unit tests. + +## Reporting a bug + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/AY2324S2-CS2113-T12-2/tp/issues). +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/AY2324S2-CS2113-T12-2/tp/issues). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +## Pull Request Process + +1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. +2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations, and container parameters. +3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +4. The Pull Request will be merged in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. + + +Thank you for contributing! diff --git a/GroceryList.log.1 b/GroceryList.log.1 new file mode 100644 index 0000000000..615e552e92 --- /dev/null +++ b/GroceryList.log.1 @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2024-04-12T08:14:04.754275Z + 1712909644754 + 275000 + 0 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added tomato (fruit), amount: 0, expiration date not set, cost: $0.00, location: null, remark not set + + + 2024-04-12T08:14:04.783381200Z + 1712909644783 + 381200 + 1 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added banana (fruit), amount: 4 pieces, expiration: 2024-04-30, cost: $2.00, location: pantry, remark not set + + + 2024-04-12T08:14:04.785381500Z + 1712909644785 + 381500 + 2 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added milk (beverage), amount: 300 ml, expiration date not set, cost: $0.00, location: fridge, remark not set + + + 2024-04-12T08:14:04.787382100Z + 1712909644787 + 382100 + 3 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added ma (as), amount: 0, expiration date not set, cost: $0.00, location: null, remark not set + + + + + + + 2024-04-15T07:28:00.146164Z + 1713166080146 + 164000 + 0 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added milk (BEVERAGE) + + + 2024-04-15T07:28:00.162164300Z + 1713166080162 + 164300 + 1 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added milk tea (BEVERAGE) + + + 2024-04-15T07:28:00.163166800Z + 1713166080163 + 166800 + 2 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added tomato (VEGETABLE) + + + 2024-04-15T07:28:00.164165600Z + 1713166080164 + 165600 + 3 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added banana (FRUIT) + + + 2024-04-15T07:28:00.165166300Z + 1713166080165 + 166300 + 4 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added eepy (NOT SLEEPY) + + diff --git a/GroceryList.log.2 b/GroceryList.log.2 new file mode 100644 index 0000000000..8aa40c0af5 --- /dev/null +++ b/GroceryList.log.2 @@ -0,0 +1,88 @@ + + + + + 2024-04-15T07:28:00.571022100Z + 1713166080571 + 22100 + 0 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added milk (BEVERAGE) + + + 2024-04-15T07:28:00.582021200Z + 1713166080582 + 21200 + 1 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added milk tea (BEVERAGE) + + + 2024-04-15T07:28:00.591018600Z + 1713166080591 + 18600 + 2 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added tomato (VEGETABLE) + + + 2024-04-15T07:28:00.592019200Z + 1713166080592 + 19200 + 3 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added banana (FRUIT) + + + 2024-04-15T07:28:00.593020400Z + 1713166080593 + 20400 + 4 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added eepy (NOT SLEEPY) + + + 2024-04-15T07:29:00.741576500Z + 1713166140741 + 576500 + 5 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added beans, amount: 0 + + + 2024-04-15T07:29:39.987825200Z + 1713166179987 + 825200 + 6 + grocery.GroceryList + INFO + grocery.GroceryList + addGrocery + 1 + Added egg, amount: 5 + + diff --git a/License.md b/License.md new file mode 100644 index 0000000000..11068a64e2 --- /dev/null +++ b/License.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2024] [CS2113-T12-02] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..fbfebb43e1 --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: git.Git + diff --git a/README.md b/README.md index f82e2494b7..76074b3fe7 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,120 @@ -# Duke project template - -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. - -## Setting up in Intellij - -Prerequisites: JDK 11 (use the exact version), update Intellij to the most recent version. - -1. **Ensure Intellij JDK 11 is defined as an SDK**, as described [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk) -- this step is not needed if you have used JDK 11 in a previous Intellij project. -1. **Import the project _as a Gradle project_**, as described [here](https://se-education.org/guides/tutorials/intellijImportGradleProject.html). -1. **Verify the set up**: After the importing is complete, locate the `src/main/java/seedu/duke/Duke.java` file, right-click it, and choose `Run Duke.main()`. If the setup is correct, you should see something like the below: - ``` - > Task :compileJava - > Task :processResources NO-SOURCE - > Task :classes - - > Task :Duke.main() - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - - What is your name? - ``` - Type some word and press enter to let the execution proceed to the end. +# GiT - Grocery in Time + +``` + ______ _ _________ + .' ___ | (_)| _ _ | +/ .' \_| __ |_/ | | \_| +| | ____[ | | | +\ `.___] || | _| |_ + `._____.'[___] |_____| + ``` + + +## Introduction +Welcome to GiT, Grocery in Time, a Java application designed for efficient grocery management. This tool helps users monitor their groceries, including tracking expiration dates, managing inventory quantities, and setting alerts for low stock or soon-to-expire items. + +## Table of Contents +- [Getting Started](#Getting-started) +- [Features](#features) + - [Common Commands](#common-commands) + - [Grocery Management](#grocery-management) + - [Calories Management](#calories-management) + - [Profile Management](#profile-management) + - [Recipe Management](#recipe-management) +- [Data Management](#data-management) +- [Command Summary](#command-summary) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [License](#license) + +## Getting Started + +### Prerequisites +- Java JDK 11: Ensure you have Java Development Kit (JDK) 11 installed on your system. It is essential for running the application. You can download it from [Oracle's JDK download page](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html). + +### Installation +1. **Download the latest release** + - Download the `GiT.jar` file from the Releases section on the project's GitHub page or from the distribution email/website. + +2. **Run the application** + - Open a terminal or command prompt. + - Navigate to the directory where `GiT.jar` is located. + - Execute the following command to run the application: + ```bash + java -jar GiT.jar + ``` + +## Features +### Inovative Four Mode Application +- **Groceries Management**: Add, edit, and delete grocery items with detailed commands. Manage your inventory by setting categories, amounts, expiration dates, and storage locations. Examples include `add GROCERY`, `del GROCERY`, and `edit GROCERY`. +- **Calorie Management**: Track calorie intake by logging food consumption and viewing total calories. Commands like `eat FOOD` and `view` help maintain dietary goals. +- **Recipe Management**: Add, view, and manage recipes. Store detailed recipes including ingredients and cooking steps, and find recipes using keywords with commands such as `add RECIPE`, `view RECIPE`, and `find KEYWORD`. +- **Profile Management**: Customize user profiles to support calorie management based on individual dietary needs. Update personal information and view user details with commands like `update` and `view`. + +## How to Use +Upon launching GiT, you will be greeted with a simple text-based user interface. + +### Common Commands +- **Switch Mode**: `switch` + - Switches the application between different modes (grocery, profile, calories, recipe). +- **Exit**: `exit` + - Closes the application. + +### Grocery Management +Manage your grocery items effectively using these commands: +- **Add Grocery**: `add GROCERY` +- **Edit Grocery**: Multiple commands to set category, amount, location, etc. +- **Delete Grocery**: `del GROCERY` +- **List Groceries**: Multiple listing options based on category, price, expiration, etc. +- **Find Grocery**: `find KEYWORD` +- **Grocery Details**: `view GROCERY` + +### Calories Management +Track and manage your daily calorie intake: +- **Add Food Consumption**: `eat FOOD` +- **View Calorie Intake**: `view` + + +### Profile Management +Manage user profile for personalized calorie tracking: +- **Update Profile**: `update` +- **View Profile**: `view` + + +### Recipe Management +Store and manage recipes: +- **Add Recipe**: `add` +- **View Recipes**: Multiple commands to view, list, find, and edit recipes. +- **Delete Recipe**: `delete RECIPE` + + +## Data Management +GiT automatically saves your data in the `/data` folder located in the same directory as the JAR file. The data includes separate files for groceries, calories, profile, and recipes. + +### Caution +Modifying data files manually can corrupt them. Always back up your data before making manual changes. + +## Command Summary + +| Command | Description | Format | +| --- | --- | --- | +| **Common** | | | +| Switch | Switch application mode | `switch` | +| Exit | Close the application | `exit` | +| **Grocery Management** | | | +| Add | Add a grocery item | `add GROCERY` | +| Delete | Delete a grocery item | `del GROCERY` | +| List | List groceries | Multiple formats | +| **Calories Management** | | | +| Eat | Log food consumption | `eat FOOD` | +| View | View calorie intake | `view` | +| **Profile Management** | | | +| Update | Update user profile | `update` | +| View | View user profile | `view` | +| **Recipe Management** | | | +| Add | Add a new recipe | `add RECIPE` | +| Delete | Delete a recipe | `delete RECIPE` | + ## Build automation using Gradle @@ -55,10 +145,8 @@ The project uses [GitHub actions](https://github.com/features/actions) for CI. W `/docs` folder contains a skeleton version of the project documentation. -Steps for publishing documentation to the public: -1. If you are using this project template for an individual project, go your fork on GitHub.
- If you are using this project template for a team project, go to the team fork on GitHub. -1. Click on the `settings` tab. -1. Scroll down to the `GitHub Pages` section. -1. Set the `source` as `master branch /docs folder`. -1. Optionally, use the `choose a theme` button to choose a theme for your documentation. +## Contributing +Interested in contributing? Great! Please fork the project and submit a pull request with your proposed changes. Detailed instructions on setting up your development environment and the contribution guidelines can be found in the [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## License +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. diff --git a/build.gradle b/build.gradle index ea82051fab..7ffe22c927 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation 'com.sun.mail:jakarta.mail:2.0.1' + implementation 'org.jline:jline:3.20.0' } test { @@ -29,11 +31,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("git.Git") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("Git") archiveClassifier.set("") } @@ -43,4 +45,5 @@ checkstyle { run{ standardInput = System.in -} + enableAssertions = true; +} \ No newline at end of file diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..2dd15e111a 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Display | Name | Github Profile | Portfolio | +|-----------------------------------------------------------------------------------------------|:------------------:|:--------------------------------------------:|:------------------------------------:| +| Zi Hui's Github avatar | Luo Zi Hui | [Github](https://github.com/luozihui2003) | [Portfolio](team/luozihui2003.md) | +| Siyi's Github avatar | Liu Siyi | [Github](https://github.com/64-1) | [Portfolio](team/64-1.md) | +| Willson's Github avatar | Willson Han Zhekai | [Github](https://github.com/wallywallywally) | [Portfolio](team/wallywallywally.md) | +| Sharlyn's Github avatar | Sharlyn Lui | [Github](https://github.com/SharlynLui) | [Portfolio](team/sharlynlui.md) | +| Luo Yu's Github avatar | Luo Yu | [Github](https://github.com/luoyu-uwu) | [Portfolio](team/luoyu-uwu.md) | \ No newline at end of file diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..840749efc1 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,320 @@ # Developer Guide +- [Acknowledgements](#acknowledgements) +- [Design & Implementation](#design--implementation) + - [Designs](#_designs_) + - [1. Execute different commands based on the modes](#1-execute-different-commands-based-on-the-modes) + - [2. Calories Management Mode](#2-calories-management-mode) + - [3. Profile Management Mode](#3-profile-management-mode) + - [4. Grocery Management Mode](#4-grocery-management-mode) + - [4.1 addOrDelGrocery](#41-addordelgrocery) + - [4.2 editGrocery](#42-editgrocery) + - [4.3 handleLocationCommands](#43-handlelocationcommands) + - [4.4 handleListOrHelp](#44-handlelistorhelp) + - [Implementation](#_implementation_) + - [1. View all groceries added](#1-view-all-groceries-added) + - [2. List the groceries by price in descending order](#2-list-the-groceries-by-price-in-descending-order) + - [3. Input category for each grocery added](#3-input-category-for-each-grocery-added) + - [4. Input amount for each grocery added](#4-input-amount-for-each-grocery-added) + - [5. Input the location of where each grocery is stored](#5-input-the-location-of-where-each-grocery-is-stored) + - [6. Edit grocery amount](#6-edit-grocery-amount) + - [7. Edit the cost of a grocery after adding](#7-edit-the-cost-of-a-grocery-after-adding) + - [8. Edit the threshold amount of a grocery after adding](#8-edit-the-threshold-amount-of-a-grocery-after-adding) + - [9. View a list of groceries low in stock](#9-view-a-list-of-groceries-low-in-stock) + - [10. Input expiration date of each grocery when added](#10-input-expiration-date-of-each-grocery-when-added) + - [11. Editing expiration date after it is added](#11-editing-expiration-date-after-it-is-added) + - [12. Storing a grocery in a storage location](#12-storing-a-grocery-in-a-storage-location) +- [Product Scope](#product-scope) + - [Target user profile](#target-user-profile) + - [Target user profile](#value-proposition) +- [User Stories](#user-stories) +- [Non-functional Requirements](#non-functional-requirements) +- [Glossary](#glossary) +- [Instructions for manual testing](#instructions-for-manual-testing) + ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +Grocery in Time (GiT) makes use of several open-source software and libraries. We acknowledge and are grateful to the community for these contributions: + +### Libraries + +1. **JUnit 5 (Jupiter API and Engine)** + JUnit 5 is used for writing and running repeatable tests in Java. It's a fundamental part of our testing framework, ensuring our application functions as intended. + - **Version**: 5.10.0 + - [JUnit 5 Documentation](https://junit.org/junit5/docs/current/user-guide/) + +2. **Jakarta Mail (formerly JavaMail)** + Jakarta Mail API is used for constructing and sending emails directly from our application, which is critical for notification features. + - **Version**: 2.0.1 + - [Jakarta Mail Documentation](https://eclipse-ee4j.github.io/mail/) + +3. **JLine 3** + JLine 3 is a library for handling console input, improving the user interaction experience in command-line applications by providing features like line editing, history, or tab completion. + - **Version**: 3.25.0 + - [JLine 3 GitHub Repository](https://github.com/jline/jline3) + +### Tools + +4. **Gradle Shadow Plugin** + The Gradle Shadow Plugin is used to create a single distributable JAR file containing all dependencies, simplifying deployment and execution. + - **Version**: 7.1.2 + - [Gradle Shadow Plugin Documentation](https://imperceptiblethoughts.com/shadow/) + +5. **Checkstyle** + Checkstyle is a development tool to help programmers write Java code that adheres to a coding standard. It automates the process of checking Java code, which is helpful in maintaining code quality. + - **Version**: 10.2 + - [Checkstyle Documentation](https://checkstyle.sourceforge.io/) + +### Development Environment + +6. **Gradle** + Gradle is our chosen build automation tool which simplifies compiling, testing, and packaging the code. + - [Gradle Documentation](https://gradle.org/guides/) + +We would like to thank the developers and contributors of these projects for their efforts in maintaining such useful resources. Their hard work and dedication make software development more efficient and error-free. + + +## Design & Implementation + +## _Designs_ +### 1. Execute different commands based on the modes +![Execute different commands](./diagrams/executeCommand.png) + +* When `executeCommand` is executed in `Parser`, different methods will be self invoked based on the selected mode. + * If mode is `grocery`, execute `groceryManagement`. + * If mode is `calories`, execute `caloriesManagement`. + * If mode is `profile`, execute `profileManagement`. + * If mode is `recipe`, execute `recipeManagement`. + +The following is a class diagram containing Food, FoodList and UserInfo. + +![Food, FoodList, UserInfo](./diagrams/UserInfo.png) + +### 2. Calories Management Mode +![Commands for managing calories](./diagrams/caloriesManagement.png) + +* When `caloriesManagement` is executed in `Parser`, different actions will be carried out based on the commands. + * If `eat`, store the name and calories of the input food. + * If `view`, display all the foods consumed. + +### 3. Profile Management Mode +![Commands for managing profile](./diagrams/profileManagement.png) + +* When `profileManagement` is executed in `Parser`, different actions will be carried out based on the commands. + * If `update`, store the user data required for calories calculation. + * If `view`, display user information. + +### 4. Grocery Management Mode + +Below is a class diagram showing the associations between the `Grocery`, `GroceryList`, `Location`, and `LocationList` classes. + +![Grocery, GroceryList, Location, LocationList](diagrams/Grocery.png) + +When `Parser` gets a user input related to the Grocery Management Mode, it executes `Parser-groceryManagement(commandPanrts)`. + +![Commands for managing grocery](./diagrams/groceryManagement.png) + +Different methods in `Parser` will be self invoked based on the index of the command in enum class GroceryCommand. + +#### 4.1 addOrDelGrocery +![addOrDelGrocery](./diagrams/addOrDelGrocery.png) + +To add groceries or delete an existing grocery. -## Design & implementation +#### 4.2 editGrocery +![editDelGrocery](./diagrams/editGrocery.png) -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +To edit the information of an existing grocery. +#### 4.3 handleLocationCommands +![handleLocationCommands](./diagrams/handleLocationCommands.png) -## Product scope +`LOC` and `DELLOC` adds and deletes storage locations. +`LISTLOC [LOCATION]` shows all locations or groceries at a given location, depending on whether a location was passed. + +#### 4.4 handleListOrHelp +![handleListOrHelp](./diagrams/handleListOrHelp.png) + +To list groceries according to different parameters, view help, switch modes, or exit from GiT. + +  +## _Implementation_ + +### 1. View all groceries added + * When the command entered is `list`, `listGroceries()` in GroceryList will be executed. + * If the current grocery list, `groceries`, is empty, execute `printNoGrocery()` in GroceryUi. + * Else, execute `printGroceryList(groceries)` in GroceryUi. + +### 2. List the groceries by price in descending order + * When the command entered is `listcost`, `sortByCost()` in GroceryList will be executed. + * If the current grocery list, `groceries`, is empty, execute `printNoGrocery()` in GroceryUi. + * Else, create a new array list name `groceriesByCost` with type `Grocery`. + * Assign all the values in current grocery list `groceries` to `groceriesByCost`. + * Execute `sort` in `groceriesByCost` with a lambda function that compares the `getCost()` value of each Grocery in the list. + * Then execute `Collections.reverse(groceriesByCost)` to reverse the list so that the cost is sorted in descending order. + * Lastly, execute `printGroceryList(groceriesByCost)` in GroceryUi. + +### 3. Input category for each grocery added + * In Grocery class, modified the Grocery constructor to accept the 'category' parameter. + * In Parser class executeCommand method, modified the add command to prompt the user for the category of the grocery. Passed the category as a parameter when creating a new Grocery object. + * In Ui class, added a new method promptForCategory to prompt the user for the category of the grocery. + * In Grocery class, modified the printGrocery method to include the category information in the output string. + +### 4. Input amount for each grocery added + * In Grocery class, modified the Grocery constructor to accept the 'amount' parameter. + * In Parser class executeCommand method, modified the add command to prompt the user for the amount of grocery. Passed the amount as a parameter when creating a new Grocery object. + * In Ui class, added a new method promptForAmount to prompt the user for the amount of grocery. + * In Grocery class, modified the printGrocery method to print different units of measurement for different categories. + +### 5. Input the location of where each grocery is stored + * In Grocery class, modified the Grocery class to include location (String) as an attribute. + * In Grocery class, modified the Grocery constructor to accept the 'location' parameter. + * In Grocery class, under printGrocery, added locationString to format location. + * In Parser class executeCommand method, modified the add command to prompt the user for where the grocery is stored. Passed the location as a parameter when creating a new Grocery object. + * In Ui class, added promptForLocation method to take in user input for location of the grocery. + * In Ui class, modified the printGrocery method to print the 'location' of the grocery alongside the grocery name. + * Alternative considered: Can possibly add location as enumeration however different people might store groceries in different places thus better to set as String so that user is free to input location details however specific they want. + +### 6. Edit grocery amount + * A `Grocery` stores its `amount` as an attribute. All `Grocery` objects are then stored in an ArrayList in `GroceryList`, which entirely handles the editing of the `amount`. + +![Class diagram for editAmount](./diagrams/GroceryAmt.png) + + * `GroceryList+editAmount()` is used to either decrease or directly set the `amount` of a `Grocery`. It takes in 2 parameters: + 1. details: String — User input read from `Scanner`. + 2. isUse: boolean — `false` directly sets the `amount`, while `true` decreases it + * To set the `amount` of a `Grocery`, the user inputs `amt GROCERY a/AMOUNT`. + * To edit the `amount` after using a `Grocery`, the user inputs `use GROCERY a/AMOUNT`. + * Our app then executes `GroceryList+editAmount()` with parameter `use = false` or `true` respectively, as illustrated by the following sequence diagram. + +![editAmount sequence diagram](./diagrams/useAmt.png) + + * Additional checks specific to `use` ensure that the user only inputs a valid `int`, or that the `amount` must not be 0 beforehand. + * Any exceptions thrown come with a message to help the user remedy their specific issue, as displayed by the `Ui`. + +### 7. Edit the cost of a grocery after adding +* when the command entered is `cost`, `editCost` in GroceryList will be executed. + ![editCost sequence diagram](./diagrams/editCost.png) +* Additional checks ensure that the user only inputs a valid `positive numeric` value. +* Any exceptions thrown come with a message to help the user remedy their specific issue, as displayed by the `Ui`. + +### 8. Edit the threshold amount of a grocery after adding +* when the command entered is `th`, `editThreshold` in GroceryList will be executed. + ![editThreshold sequence diagram](./diagrams/editThreshold.png) +* Additional checks ensure that the user only inputs a valid `positive integer`. +* Any exceptions thrown come with a message to help the user remedy their specific issue, as displayed by the `Ui`. + +### 9. View a list of groceries low in stock +* When the command entered is `low`, `listLowStocks` in GroceryList will be executed. +* This will create a new array list called `lowStockGroceries` with type `Grocery` +* For each grocery in the current grocery list, `groceries`, execute `grocery.isLow()`. Add the grocery into `lowStockGroceries` if the return value is true. +* Execute `printLowStocks(lowStockGroceries)` in GroceryUi to print out the list. + +### 10. Input expiration date of each grocery when added + * In Grocery class, the expiration field in the Grocery class was changed from a String to a LocalDate to standardize date handling. + * In Grocery class, the setExpiration method was updated to accept a String input, convert it to a LocalDate using a specified format ("yyyy-MM-dd"), and then store this date. + * In UI class, the UI now includes a multistep process to prompt the user for the year, month, and day of the grocery item's expiration date. This process ensures that the date is captured in a user-friendly manner and stored accurately. + * In GroceryList class, a new method, sortByExpiration, was added to allow sorting the list of groceries by their expiration dates in ascending order. This method utilizes the Collections.sort method with a lambda expression comparing the expiration dates of Grocery items. + +### 11. Editing expiration date after it is added + * In GroceryList class, modified the editExpiration method to parse String into LocalDate. + * `GroceryList+editExpiration()` is used to directly set the `exp` of a `Grocery`. It takes in 1 parameter: + 1. details: String — User input read from `Scanner`. + * To edit the `exp` after using a `Grocery`, the user inputs `use GROCERY d/EXPIRATION_DATE`. + +![editExpiration sequence diagram](./diagrams/GroceryList_editExpiration.png) + +### 12. Storing a grocery in a storage location +* A `Grocery` stores its location by referencing a `Location` object. All `Locations` are stored in a `LocationList` class. + +![Class diagram for editLocation](./diagrams/GroceryLocation.png) + +* `GroceryList+editLocation()` handles the editing of a grocery's location. + 1. If the grocery is not stored anywhere, its `Location` will be set. + 2. If the grocery is to be stored in a different location, its `Location` will be changed. +* This method takes in 1 parameter: + 1. details: String — User input read from `Scanner`. +* The user enters `store GROCERY l/LOCATION` to store the grocery in the desired location. Our app then executes `GroceryList+editLocation()`, as illustrated by the following sequence diagram. + +![editLocation sequence diagram](./diagrams/editLocation.png) + +* If the target `Location` does not exist, our app automatically creates it and stores the grocery there. +* If the target `Location` is the same as the current `Location`, our app throws a `SameLocationException()`, informing the user of this error. + + +  +## Product Scope ### Target user profile -{Describe the target user profile} +Our target user is someone who regularly goes grocery shopping, and would like to track and manage their inventory of groceries. +Our target user is also health-conscious and interested in keeping track of their calorie consumption. +Additionally, other than grocery shopping, our target user will likely be cooking as well, thus they will want to create and management their own recipes. ### Value proposition -{Describe the value proposition: what problem does it solve?} +Grocery in Time aims to act as an easy-to-use central database for all the user's groceries. Managing many groceries stored at different locations around the house can get confusing, +therefore our app will allow users to track their groceries easily. + +Users are able to edit and manage the category, amount, expiration date, and storage location of their groceries. +When groceries are running low, the app can generate a shopping list to remind users of what they need to buy. +Furthermore, the app can generate a list of items that are expiring soon, reminding users to consume their groceries as soon as possible. + +GiT also comes with other modes for recipe management, profile management and calorie tracking. + +In recipe management mode, users will be able to create and record their own recipes with details such as title, ingredients and steps. +User will be able to look for recipes using a keyword, view and edit existing recipes. +In profile management mode, users can store information such as weight, height and gender to calculate and manage their calories intake according to their goals. +Lastly, in calorie tracker, users can add the food they have eaten along with the calories for GiT to calculate user's total intake. ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|--------------------------------|-----------------------------------------------------|---------------------------------------------------------| +| v1.0 | new user | see instructions on how to use the app | refer to them when I forget how to use the application | +| v1.0 | user | add groceries to the app | manage all my groceries | +| v1.0 | user | view all my groceries | know what I have bought | +| v1.0 | user | delete groceries from the list | stop tracking those groceries | +| v1.0 | user | add the amount of a grocery | keep track of the amount of that item I have | +| v1.0 | user | add the expiration date of the grocery | keep track of when my items expire easily | +| v2.0 | user who consumes groceries | track the usage of my groceries | know how much I have left | +| v2.0 | financially-aware user | track and view the cost of my groceries | know how much I am spending | +| v2.0 | health-conscious user | categorise my groceries | know what types of groceries I have | +| v2.0 | user with many storage spaces | add the location of where an item is stored | see where I keep my groceries | +| v2.0 | user with many storage spaces | find out what groceries are stored in each location | know where to find my groceries | +| v2.0 | forgetful user | get a list of groceries that are low in stock | remind myself to buy them on my next grocery trip | +| v2.0 | forgetful user | find my groceries by name | know if I have tracked that grocery | +| v2.0 | user who replenishes groceries | set threshold amount for the groceries | know what groceries I should top up | +| v2.0 | user who cooks with recipes | create and keep my own version of recipes | refer to my own recipes when I cook | +| v2.0 | user who cooks with recipes | list all my recipes | know what recipes I have created | +| v2.0 | user who cooks with recipes | view all the details of a recipe | use the recipe to recreate the dish | +| v2.0 | user who cook with recipes | delete a recipe | remove recipes I no longer want | +| v2.0 | health-conscious user | store the calories of the food I consumed | track my calories intake and know how much I should eat | +| v2.0 | environmentally-conscious user | get a list of items that are expiring soon | prioritise using them to reduce food waste | +| v2.0 | reviewer | rate and review products | know if I like them | +| v2.0 | meticulous user | add remarks to my groceries | know extra information about my groceries | +| v2.1 | user who cooks with recipes | edit my old recipes | update the recipes to new preferences and methods | +| v2.1 | user | store my past grocery information | access information about the groceries I am tracking | ## Non-Functional Requirements -{Give non-functional requirements} +* GiT is able to handle large amounts of data, stored in `/data/groceryList.txt`. +* GiT should be easy for a new user to grasp, and allow experienced users to use different functionalities quickly. ## Glossary -* *glossary item* - Definition +* *Java* - Object-oriented programming language used to create Grocery in Time. +* *Command Line Interface* - Text-based user interface to allow users to interact with Grocery in Time. ## Instructions for manual testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +First, testers can install GiT by following these instructions: + +1. Ensure that you have Java 11 or above installed. +2. Down the latest version of `Grocery in Time` from [here](https://github.com/AY2324S2-CS2113-T12-2/tp/releases). +3. Open a command terminal, `cd` into the folder where the JAR file is + and use `java -jar Git.jar` to run Grocery in Time. + +To get you started, we have provided some sample data [here](https://github.com/AY2324S2-CS2113-T12-2/tp/tree/master/sampleData). +To load this data into GiT, simply move `groceryList.txt` into the `/data` directory that will be created in the same directory as `Git.jar` after running it at least once. + +Do check out our [User Guide](UserGuide.md) to see what functionalities GiT offers. Happy testing! \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..bd1538f5a3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,9 @@ -# Duke +# Grocery in Time -{Give product intro here} +![Grocery in Time logo](images/GitStartup.png) + +Grocery in Time (GiT) is a **grocery tracker app**, optimised for use via a Command Line Interface (CLI). +It allows users to track and manage their groceries around their home easily. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..f36a6a3e4a 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,953 @@ -# User Guide +# User Guide to ***Grocery in Time*** + +![Grocery in Time logo](images/GitStartup.png) ## Introduction -{Give a product intro} +Grocery in Time (GiT) is a **grocery tracker app**, optimised for use via a Command Line Interface (CLI). +It allows users to track and manage their groceries around their home easily. -## Quick Start -{Give steps to get started quickly} +- [Quick start](#quick-start) +- [Features](#features) + - [Switching between different modes: `switch`](#switching-between-different-modes-switch) + - [A. Grocery management mode](#a-grocery-management-mode) + - [Viewing help: `help`](#viewing-help-help) + - [Add / Edit / Delete Groceries](#add--edit--delete-groceries) + - [Adding a new grocery: `add`](#adding-a-new-grocery-add) + - [Adding multiple groceries: `addmulti`](#adding-multiple-groceries-addmulti) + - [Setting the category of a grocery: `cat`](#setting-the-category-of-a-grocery-cat) + - [Setting the amount of a grocery: `amt`](#setting-the-amount-of-a-grocery-amt) + - [Using a grocery: `use`](#using-a-grocery-use) + - [Storing a grocery in a storage location: `store`](#storing-a-grocery-in-a-storage-location-store) + - [Setting the expiration date of a grocery: `exp`](#setting-the-expiration-date-of-a-grocery-exp) + - [Setting the cost of a grocery: `cost`](#setting-the-cost-of-a-grocery-cost) + - [Setting the threshold of a grocery: `th`](#setting-the-threshold-of-a-grocery-th) + - [Adding a remark for a grocery: `remark`](#adding-a-remark-for-a-grocery-remark) + - [Adding rating and review of a grocery: `rate`](#adding-rating-and-review-of-a-grocery-rate) + - [Deleting a grocery: `del`](#deleting-a-grocery-del) + - [Find / View / List Groceries](#find--view--list-groceries) + - [Finding groceries: `find`](#finding-groceries-find) + - [Viewing a grocery: `view`](#viewing-a-grocery-view) + - [Viewing groceries that are low in stock: `low`](#viewing-groceries-that-are-low-in-stock-low) + - [Viewing groceries expiring in the next 3 days: `expiring`](#viewing-groceries-expiring-in-the-next-3-days-expiring) + - [Listing all groceries: `list`](#listing-all-groceries-list) + - [Listing all groceries by category: `listcat`](#listing-all-groceries-by-category-listcat) + - [Listing all groceries by price: `listcost`](#listing-all-groceries-by-price-listcost) + - [Listing all groceries by expiration date: `listexp`](#listing-all-groceries-by-expiration-date-listexp) + - [Listing storage locations and their groceries: `listloc`](#listing-storage-locations-and-their-groceries-listloc) + - [Manage Storage Locations](#manage-storage-locations) + - [Adding a storage location: `loc`](#adding-a-storage-location-loc) + - [Removing a storage location: `delloc`](#removing-a-storage-location-delloc) + - [B. Calories management mode](#b-calories-management-mode) + - [Adding eaten food: `eat`](#adding-eaten-food-eat) + - [Viewing all food and calories intake: `view`](#viewing-all-food-and-calories-intake-view) + - [C. Profile management mode](#c-profile-management-mode) + - [Updating user information: `update`](#updating-user-information-update) + - [Viewing user details: `view`](#viewing-user-details-view) + - [D. Recipe management mode](#d-recipe-management-mode) + - [Adding a new recipe: `add`](#adding-a-new-recipe-add) + - [Listing all recipes: `list`](#listing-all-recipes-list) + - [Viewing a recipe: `view`](#viewing-a-recipe-view) + - [Finding recipe(s): `find`](#finding-recipes-find) + - [Editing a recipe: `edit`](#editing-a-recipe-edit) + - [Deleting a recipe: `delete`](#deleting-a-recipe-delete) + - [Exiting the program: `exit`](#exiting-the-program-exit) +- [Data saving and loading](#data-saving-and-loading) +- [Command summary](#command-summary) +## Quick Start 1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Down the latest version of `Grocery in Time` from [here](https://github.com/AY2324S2-CS2113-T12-2/tp/releases). +3. Open a command terminal, `cd` into the folder where the JAR file is + and use `java -jar Git.jar` to run Grocery in Time. + +  +# Features + +> #### Notes about the command format +> * **Do not use command words / tag to name grocery / recipe etc.** +>
e.g. Do not name grocery "a/". +> * Words in `UPPERCASE` are parameters to be supplied by the user. +>
e.g. In `find KEYWORD`, `KEYWORD` is a parameter to be supplied: `find cheese`. + +  +## Switching between different modes: `switch` +Switches between profile, calories, grocery or recipe mode. +GiT comes in different modes and will prompt the user to choose their desired mode. + +Format: `switch` + +* No other word is to be entered after `switch`. Instead, wait for GiT to prompt you to enter the desired mode to switch to. + +Example of usage: + +``` +>> switch + +What mode would you like to enter? +Please select a mode: grocery, profile, calories or recipe: + +>> grocery + +Here are some ways you can maange your groceries! +... +``` + + +  +## A. Grocery management mode + +> #### Notes about this mode +> * Features requiring the `GROCERY` or `LOCATION` inputs are case-insensitive. +>
e.g. `amt GROCERY a/AMOUNT` will set the amount of `milk` or `MILK`. +>
e.g. `store GROCERY l/LOCATION` works the same using `freezer` or `FREEZER`. +> * The actual output may differ slightly from the examples due to the addition of lines for better user readability. + +### Viewing help: `help` +Prints all commands and a short description of what they do. + +Format: `help` + +* No other word is to be entered after `help`. + +Example of usage: + +``` +>> help + +Here are some ways you can manage your groceries! +... +``` + +  +### Add / Edit / Delete Groceries + +### Adding a new grocery: `add` +Adds a grocery and any desired additional details. + +Format: `add GROCERY` + +* `GROCERY` must be a valid String. +* Duplicate groceries will not be added. +* After executing `add GROCERY`, GiT will ask if the user wishes to include additional details. + * If so, the user has to enter the numbers corresponding to the details they wish to add. + * Multiple numbers can be entered in any order and spaces between numbers are ignored. + * Details are prompted for in the order their numbers are entered. + * If `8` is entered, another menu explaining what each detail means will always be displayed first. + * Invalid values are ignored. + * This step can be skipped by inputting nothing. +* Any details not included here can be edited using other commands. + * The only detail can cannot be included here is the rating, which is edited using [rate](#adding-rating-and-review-of-a-grocery-rate). + +Example of usage: +``` +>> add milk + +Before adding milk, do you want to include the following details? +1. Category +2. Amount +3. Location +4. Expiration Date +5. Cost +6. Threshold Amount +7. Remark +8. Help +Please enter the number of the details you want to include: +You may enter multiple numbers. (e.g. 1234) +To skip this step, do not enter any values. + +>> 23 + +Including Amount +Please enter the amount (e.g. 3): + +>> 5 + +Including Location +Please enter the location (e.g. freezer first compartment) + +>> cabinet + +milk added! +``` + + +  +### Adding multiple groceries: `addmulti` +Adds multiple groceries and any desired additional details. + +Format: `addmulti` + +* The grocery name cannot be empty. +* Duplicate groceries will not be added. +* After executing `addmulti`, GiT will prompt for various details. + 1. Number of groceries to add + 2. Additional details to include + * The interface is the same as the one for [add](#adding-a-new-grocery-add). + +Example of usage: + +``` +>> addmulti + +How many groceries would you like to add? + +>> 2 + +Adding item 1 of 2 +Please enter the name of the grocery: + +>> beans + +Do you want to include additional details for beans? (Y/N) + +>> Y + +... +``` + + +  +### Setting the category of a grocery: `cat` +Sets the category of a grocery. + +Format: `cat GROCERY c/CATEGORY` + +* `CATEGORY` must be a valid String. +* `CATEGORY` will be stored in uppercase. + +Example of usage: +``` +>> cat milk c/dairy + +milk is now a dairy +``` + + +  +### Setting the amount of a grocery: `amt` +Sets the amount of a grocery. + +Format: `amt GROCERY a/AMOUNT` + +* `AMOUNT` must be a valid integer. + +Example of usage: +``` +>> amt milk a/5 + +milk: 5 +``` + + +  +### Using a grocery: `use` +Reduce the amount of a grocery after using it. + +Format: `use GROCERY a/AMOUNT` + +* `AMOUNT` must be a valid integer. +* If `AMOUNT` is greater than what the `GROCERY` has in stock, its amount will be reduced to 0. +* If the amount of the `GROCERY` is already 0 or is not set, GiT will let the user know it is out of stock. + +Example of usage: + +* Amount used is less than amount stored. + +``` +>> use meat a/4 + +meat: 56 +``` + +* Amount used is greater than amount stored. + +``` +>> use meat a/60 + +meat is now out of stock! +``` + + +  +### Storing a grocery in a storage location: `store` +Store a grocery in a given storage location. + +Format: `store GROCERY l/LOCATION` + +* If `LOCATION` does not exist, GiT will create the storage location and store the `GROCERY` there automatically. +* More information on storage locations can be found [here](#manage-storage-locations). + +Example of usage: + +* `LOCATION` exists + +``` +>> store paprika l/spice rack + +paprika stored in spice rack +``` + +* `LOCATION` does not exist + +``` +>> store onion l/cabinet + +New location added: cabinet +onion stored in cabinet +``` + + +  +### Setting the expiration date of a grocery: `exp` +Sets the expiration date of a grocery. + +Format: `exp GROCERY d/EXPIRATION_DATE` + +* `EXPIRATION_DATE` must be in yyyy-MM-dd format. + +Example of usage: + +`exp milk d/2024-07-20` + + +  +### Setting the cost of a grocery: `cost` +Sets the cost of a grocery. + +Format: `cost GROCERY $PRICE` + +* `PRICE` must be a valid numerical value. + +Example of usage: + +``` +>> cost milk $1.20 + +milk is now $1.20 +``` + + +  +### Setting the threshold of a grocery: `th` +Sets the threshold amount of a grocery. +The user should be reminded to top up the stock if amount falls below the threshold amount. + +Format: `th GROCERY a/AMOUNT` + +* `AMOUNT` must be a valid integer. + +Example of usage: + +``` +>> th milk a/1 + +milk's threshold is now 1 +``` + + +  +### Adding a remark for a grocery: `remark` +Adds a remark for existing grocery. +The remark for the grocery will be displayed in `list` and `view`. + +Format: `remark GROCERY r/REMARK` + +Example of usage: +`remark milk r/save some for next week` + + +  +### Adding rating and review of a grocery: `rate` +Adds rating and review of an existing grocery + +Format: `rate GROCERY` + +Example of usage: + +`rate milk` + + +  +### Deleting a grocery: `del` +Delete a grocery. + +Format: `del GROCERY` + +* If the `GROCERY` was stored in a location, it would be removed from that location. + +Example of usage: + +``` +>> del milk + +This grocery is removed: +milk (dairy), amount: 0, expiration: 2024-04-10, cost: $0.00, location: fridge +You now have 2 groceries left +``` + +  +## Find / View / List Groceries + +### Finding groceries: `find` +Find groceries containing a given keyword in their name. + +Format: `find KEYWORD` + +* The search is case-insensitive. +* If a phrase is passed, the entire phrase is searched for. + +Example of usage: + +``` +>> find cheese + +Here are the groceries containg: cheese +- cheese (dairy), amount: 50 +- red cheddar cheese (dairy), amount: 4 +``` + + +  +### Viewing a grocery: `view` +Shows all the details of the grocery. + +Format: `view GROCERY` + +Example of usage: + +``` +>> view apple + +These are the details of fuji apple: +Amount: 5 +Expiry date: not set +Category: Fruit +Cost: not set +Location: Fridge +Rating: 4 +Review: buy the same brand next time +Remark: not set +``` + + +  +### Viewing groceries that are low in stock: `low` +Shows a list of groceries below the threshold amount. + +Format: `low` + +* No other word is to be entered after `low`. + +Example of usage: + +``` +>> low +Time to top up these groceries! + - milk only left: 0 + - apple only left: 0 +``` -## Features -{Give detailed description of each feature} +  +### Viewing groceries expiring in the next 3 days: `expiring` +Shows a list of groceries that are expiring in the next 3 days. +Sends an email notification if needed. -### Adding a todo: `todo` -Adds a new item to the list of todo items. +Format: `expiring` -Format: `todo n/TODO_NAME d/DEADLINE` +* No other word is to be entered after `expiring`. -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Example of usage: -Example of usage: +``` +>> expiring -`todo n/Write the rest of the User Guide d/next week` +Checking for groceries nearing expiration... +Milk is nearing expiration on 2024-04-10 +Do you wish to send a notification email? (y/n) -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +>> y -## FAQ +Please enter your email to receive notifications: -**Q**: How do I transfer my data to another computer? +>> example@gamil.com -**A**: {your answer here} +Sending notification email... +Email sent successfully to example@gmail.com +``` +  +### Listing all groceries: `list` +Shows a list of all groceries you have. + +Format: `list` + +* No other word is to be entered after `list`. + +Example of usage: + +``` +>> list + +Here are your groceries! + - coke (SODA), cost: $1.00 + - milk (DAIRY), amount: 0 units, cost: $1.20 + - apple (FRUIT), amount: 0 units, cost: $2.00 +``` + + +  +### Listing all groceries by category: `listcat` +Shows a list of all groceries you have, sorted by category alphabetically. + +Format: `listcat` + +* No other word is to be entered after `listcat`. +* Grocery list is sorted, causing the order shown by `list` to change. + +Example of usage: + +``` +>> listcat + +Here are your groceries! + - milk (DAIRY), amount: 0 units, cost: $1.20 + - apple (FRUIT), amount: 0 units, cost: $2.00 + - coke (SODA), cost: $1.00 +``` + + +  +### Listing all groceries by price: `listcost` +Shows a list of all groceries you have, sorted by descending price. + +Format: `listcost` + +* No other word is to be entered after `listcost`. +* Grocery list is sorted, causing the order shown by `list` to change. + +Example of usage: + +``` +>> listcost + +Here are your groceries! + - apple, amount: 0 units, cost: $2.00 + - milk, amount: 0 units, cost: $1.20 + - coke, cost: $1.00 +``` + + +  +### Listing all groceries by expiration date: `listexp` +Shows a list of all groceries you have, sorted by expiration date +i.e. earliest expiring item at the top. + +Format: `listexp` + +* No other word is to be entered after `listexp`. +* Grocery list is sorted, causing the order shown by `list` to change. + +Example of usage: + +``` +>> listexp + +Here are your groceries! + - meat (MEAT), expiration: 2024-04-10 + - cheese (DAIRY), expiration: 2025-12-12 +``` + + +  +### Listing storage locations and their groceries: `listloc` +View all storage locations being tracked, or the groceries stored in a given location + +Format: `listloc [LOCATION]` + +* `LOCATION` is an optional parameter. + * Without `LOCATION`, all storage locations will be displayed. + * With `LOCATION`, all groceries in the given `LOCATION` will be displayed. +* More information on storage locations can be found [here](#manage-storage-locations). + +Example of usage: + +* `listloc`: All storage locations are displayed. + +``` +>> listloc + +Here's all the locations you are tracking: +- spice rack +- freezer +- cubby +``` + +* `listloc cubby`: All groceries in `cubby` are displayed. + +``` +>> listloc cubby + +Viewing location: cubby +Here are your groceries! +- cheese (dairy), amount: 50, location: cubby +- pasta (carbs), cost: $2.95, location: cubby +``` + + +  +## Manage Storage Locations + +### Adding a storage location: `loc` +Add a storage location to be tracked. + +Format: `loc LOCATION` + +* Duplicate locations will not be added. + +Example of usage: + +``` +>> loc freezer + +New location added: freezer +``` + + +  +### Removing a storage location: `delloc` +Remove a storage location from tracking. + +Format: `delloc LOCATION` + +Example of usage: + +``` +>> delloc cabinet + +Location: freezer has been removed from tracking! +``` + + +  +## B. Calories management mode + +### Adding eaten food: `eat` +Adds the food eaten and store its calories. + +Format: `eat FOOD` + +Example of usage: +``` +>> eat burger +Please enter the calories of the food in kcal: +>> 400 +burger, with 400.0 calories was consumed! +``` + +  +### Viewing all food and calories intake: `view` +Shows all the food consumed so far and their calories. + +Format: `view` + +Example of usage: + +``` +>> view +Here are the food you have consumed today: + - burger, with 400.0 calories + - apple, with 52.0 calories +You have consumed 452.0 calories for today +``` + + +  +## C. Profile management mode + +### Updating user information: `update` +Stores information needed to calculate and manage calories intake. + +Format: `update` + +User will be prompt for the following details: + * `name`: A non-empty string is expected + * `weight`: A positive numeric value is expected + * `height`: A positive numeric value is expected + * `age`: A positive integer is expected + * `gender`: F / M / others + * Input is case-insensitive + * If `other` is entered, calculations done will be based on `M` + * `activeness`: inactive/light/moderate/active/very + * Input must be one of these options to be valid. + * Inactive - little or no exercise + * Light - light exercise 1-3 days per week + * Moderate - moderate exercise 3-5 days per week + * Active - hard exercise 6-7 days a week + * Very - very hard exercise 6-7 days a week + * `aim`: lose/maintain/gain + * Input must be one of these options to be valid. + +Example of usage: +``` +>> update +Please enter your name +>> Alice +Please enter your weight in KG: +>> 50 +Please enter your height in cm: +>> 165 +Please enter your age in years (nearest whole number): +>> 22 +Please enter your gender (M / F / Others): +>> f +Please enter your activeness (e.g. inactive/light/moderate/active/very): +>> moderate +Please enter your weight aim (e.g. lose/maintain/gain): +>> lose +Your target calories intake a day should be 1655 +``` + +  +### Viewing user details: `view` +Shows the user profile details. + +Format: `view` + +Example of usage: +``` +>> view +Name: Alice +Height: 165.0 +Weight: 50.0 +Age: 22 +Gender: f +Target calories intake: 1655 +``` + + +  +## D. Recipe management mode + +> #### Notes about this mode +> * All keywords passed in are not case-sensitive. +> * Duplicated recipe title with different capitalization will not be not accepted. +> * Commands with command word + something will still be processed so that users can continue to use the feature even if they typed in extra details, as long as they follow the instructions from the prompt. +> * e.g. If command word is `add`, `add something` will also be accepted. + +### Adding a new recipe: `add` +Adds new recipe, ingredient and steps. + +Format: `add` + +Example: +``` +>> add + +Please enter the title of the recipe: + +>> Fried Egg + +Please enter the ingredients for this recipe in one line: + +>> egg, salt + +Please enter the steps for this recipe in one line: + +>> Fry the egg. Add salt. Serve. + +Fried Egg added! +``` + +  +### Listing all recipes: `list` +Shows all the recipe titles. + +Format: `list` + +Example: +``` +>> list + +Here are your recipe titles! + +1. fried egg +- - - - - +``` + +  +### Viewing a recipe: `view` +Shows the recipe ingredients and steps. + +Format: `view` `RECIPE` + +Example: +``` +>> view + +Please enter the title of the recipe: + +>> Fried Egg +``` + +  +### Finding recipes: `find` +Find the relevant recipe(s) with given keyword + +Format: `find` `KEYWORD` + +Example: +``` +>> find + +Please enter the title of the recipe: + +>> fried egg + +Here are the recipe(s) containing: fried egg +- fried egg with chili +- fried egg with vegetable +``` + +  +### Editing a recipe: `edit` +Shows the recipe ingredients and steps. + +Format: `edit` `RECIPE` `TITLE/INGREDIENTS/STEPS` + +Example: +``` +>> edit + +Please enter the title of the recipe: + +>> Fried Egg + +Please enter the part of the recipe to be edited. +Only ONE part can be edited (Title / Ingredients / Steps): + +>> title + +Please enter the title of the recipe (e.g. fried egg): + +>> Fried Egg with Chilli +``` + +  +### Deleting a recipe: `delete` +Shows the recipe ingredients and steps. + +Format: `delete` `RECIPE` + +Example: +``` +>> delete + +Please enter the title of the recipe: + +>> Fried Egg + +Fried Egg is removed from the recipe list. +``` + +  +## Exiting the program: `exit` +Exits GiT, regardless of which mode you are in. + +Format: `exit` + +* No other word is to be entered after `exit`. + +Example of usage: + +``` +>> exit + +bye bye! +``` + + +  +## Data saving and loading + +GiT's data is automatically saved in `/data` in the same directory as `Git.jar`. +When GiT starts up, it will automatically load the data. + +### Editing data + +Data for different modes is saved in different files. +For instance, grocery data is stored in `groceryList.txt`. + +> #### CAUTION +> Any changes that invalidate the data format will corrupt the entire data file. +> In this case, GiT will **wipe** all previously stored data. +>
+> It is recommended to make a backup +> before editing. + + +  ## Command Summary -{Give a 'cheat sheet' of commands here} +### For all modes + +| Command | Format | +|--------------|----------| +| Switch modes | `switch` | +| Exit | `exit` | + +### Grocery management mode + +| Command | Format | +|--------------------------------------------------------------|---------------------------------| +| View help | `help` | +| Add grocery | `add GROCERY` | +| Add multiple groceries | `addmulti` | +| Set grocery category | `cat GROCERY c/CATEGORY` | +| Set grocery amount | `amt GROCERY a/AMOUNT` | +| Use grocery | `use GROCERY a/AMOUNT` | +| Store grocery | `store GROCERY l/LOCATION` | +| Set grocery expiration date | `exp GROCERY d/EXPIRATION_DATE` | +| Set grocery cost | `cost GROCERY $PRICE` | +| Set grocery threshold amount | `th GROCERY a/AMOUNT` | +| Add or edit remark | `remark GROCERY r/REMARK` | +| Add grocery rating and review | `rate GROCERY` | +| Delete grocery | `del GROCERY` | +| Find groceries | `find KEYWORD` | +| View grocery details | `view GROCERY` | +| View groceries that are low in stock | `low` | +| View groceries expiring in the next 3 days | `expiring` | +| List groceries | `list` | +| List groceries by category | `listcat` | +| List groceries by price | `listcost` | +| List groceries by expiration date | `listexp` | +| List storage locations
List groceries in given location | `listloc [LOCATION]` | +| Add storage location | `loc LOCATION` | +| Remove storage location | `delloc LOCATION` | + +### Calorie management mode + +| Command | Format | +|-------------------------------|------------| +| Add eaten food | `eat FOOD` | +| View food and calories intake | `view` | + +### Profile management mode + +| Command | Format | +|-------------------------|----------| +| Update user information | `update` | +| View user details | `view` | + +### Recipe management mode -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Command | Format | +|----------------|-------------------------------------------| +| Add recipe | `add` `TITLE` `INGREDIENTS` `STEPS` | +| List recipes | `list` | +| View recipe | `view` `TITLE` | +| Find recipe(s) | `find` `KEYWORD` | +| Edit recipe | `edit` `RECIPE` `TITLE/INGREDIETNS/STEPS` | +| Delete recipe | `delete` `TITLE` | \ No newline at end of file diff --git a/docs/diagrams/Grocery.png b/docs/diagrams/Grocery.png new file mode 100644 index 0000000000..4191cb1144 Binary files /dev/null and b/docs/diagrams/Grocery.png differ diff --git a/docs/diagrams/Grocery.puml b/docs/diagrams/Grocery.puml new file mode 100644 index 0000000000..17edf1cd8f --- /dev/null +++ b/docs/diagrams/Grocery.puml @@ -0,0 +1,59 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class GroceryList { + +addGrocery(grocery: Grocery) + +removeGrocery(name: String) + +getGrocery(name: String) + +edit...() + +findGroceries(key: String) + +viewGrocery(grocery: String) + +listGroceries() + +listLowStocks() + +sortByCost() + +sortByCategory() + +sortByExpiration() +} +note left: Each Grocery field can be\nedited using its respective edit...() + +class Grocery { + -name: String + -amount: int + -threshold: int + -expiration: LocalDate + -category: String + -unit: String + -cost: double + -rating: int + -review: String + -remark: String + -isSetCost: boolean + -isSetAmount: boolean + + +Grocery(name: String, amount: int, threshold: int, expiration: LocalDate, category: String, cost: double, location: Location) + +isLow(): boolean + +printGrocery(): String + +toSaveFormat(): String +} + +class Location { + -name: String + +addGrocery(grocery: Grocery) + +removeGrocery(grocery: Grocery) + +listGroceries() + +clearLocation() +} + +class LocationList { + +addLocation(name: String) + +removeLocation(name: String) + +findLocation(name: String) + +listLocation() +} + +GroceryList "1" --> "*" Grocery : stores > +LocationList "1" -> "*" Location : contains > +Grocery "*" <---> "1" Location : stored in > + +@enduml \ No newline at end of file diff --git a/docs/diagrams/GroceryAmt.png b/docs/diagrams/GroceryAmt.png new file mode 100644 index 0000000000..1a1c26e509 Binary files /dev/null and b/docs/diagrams/GroceryAmt.png differ diff --git a/docs/diagrams/GroceryAmt.puml b/docs/diagrams/GroceryAmt.puml new file mode 100644 index 0000000000..28530b7a39 --- /dev/null +++ b/docs/diagrams/GroceryAmt.puml @@ -0,0 +1,14 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class GroceryList { + +editAmount(details: String, isUse: boolean) +} + +class Grocery { + -amount: int +} + +GroceryList "1" ---> "*" Grocery : stores > +@enduml \ No newline at end of file diff --git a/docs/diagrams/GroceryList_editExpiration.png b/docs/diagrams/GroceryList_editExpiration.png new file mode 100644 index 0000000000..c20e884046 Binary files /dev/null and b/docs/diagrams/GroceryList_editExpiration.png differ diff --git a/docs/diagrams/GroceryLocation.png b/docs/diagrams/GroceryLocation.png new file mode 100644 index 0000000000..7e7e923434 Binary files /dev/null and b/docs/diagrams/GroceryLocation.png differ diff --git a/docs/diagrams/GroceryLocation.puml b/docs/diagrams/GroceryLocation.puml new file mode 100644 index 0000000000..ee39e4596a --- /dev/null +++ b/docs/diagrams/GroceryLocation.puml @@ -0,0 +1,29 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class GroceryList { + +editLocation(details: String) +} + +class Grocery { + +} + +class LocationList { + +addLocation(name: String) + +removeLocation(name: String) + +findLocation(name: String) +} + +class Location { + -name: String + +addGrocery(grocery: Grocery) + +removeGrocery(grocery: Grocery) +} + +GroceryList "1" -> "*" Grocery : stores > +LocationList "1" -> "*" Location : contains > +Grocery "*" <--> "1" Location : stored in > + +@enduml \ No newline at end of file diff --git a/docs/diagrams/UserInfo.png b/docs/diagrams/UserInfo.png new file mode 100644 index 0000000000..13a074e0ae Binary files /dev/null and b/docs/diagrams/UserInfo.png differ diff --git a/docs/diagrams/UserInfo.puml b/docs/diagrams/UserInfo.puml new file mode 100644 index 0000000000..e0e95f03e9 --- /dev/null +++ b/docs/diagrams/UserInfo.puml @@ -0,0 +1,49 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle +class FoodList { + +FoodList() + +addFood(food: Food): Void + +printFoods(): Void +} +class Food { + -name: String + -calories: Double + +Food(name: String, calories: Double) + +getName(): String + +getCalories: Double + +print(): String +} +FoodList *--> "*" Food: contains > + +class UserInfo { + -name: String + -weight: Double + -height: Double + -age: Integer + -gender: String + -aim: String + -activeness: String + -BMR: Double + -AMR: Double + -caloriesCap: Integer + -currentCalories: Integer + -calBMR(): Void + -calAMR(): Void + -setCaloriesCap(): Void + +UserInfo() + +setName(name: String): Void + +setWeight(weight: Double): Void + +setHeight(height: Double): Void + +setAge(age: Integer): Void + +setGender(gender: String): Void + +setAim(aim: String): Void + +setActiveness(activeness: String): Void + +getCurrentCalories(): Double + +updateInfo(name: String, weight: Double, height: Double, age: Integer, + gender: String, activeness: String, aim: String): Void + +viewProfile(): String + -consumptionOfCalories(food: Food): Void +} +UserInfo ..> Food +@enduml \ No newline at end of file diff --git a/docs/diagrams/addOrDelGrocery.png b/docs/diagrams/addOrDelGrocery.png new file mode 100644 index 0000000000..da97244002 Binary files /dev/null and b/docs/diagrams/addOrDelGrocery.png differ diff --git a/docs/diagrams/addOrDelGrocery.puml b/docs/diagrams/addOrDelGrocery.puml new file mode 100644 index 0000000000..55deec860a --- /dev/null +++ b/docs/diagrams/addOrDelGrocery.puml @@ -0,0 +1,39 @@ +@startuml + +participant ":Parser" as p +participant "groceryList:GroceryList" as gl +participant "grocery:Grocery" as g +participant ":System.out" as sys +participant "groceryUi:GroceryUi" as ui + + -> p : addOrDelGrocery(command, commandParts) + + alt command == ADD + opt name == null + p -> sys : println(GitException.getMessage()) + end + p -> gl : isGroceryExists(name) + gl --> p : isExist + opt isExist == true + p -> sys : println(GitException.getMessage()) + end + create g + p -> g : Grocery(commandParts[1]) + g --> p : grocery + p -> ui : promptAddMenu(grocery) + p -> gl : addGrocery(grocery) + else command == ADDMULTI + p -> ui : promptAddMultipleMenu() + ui --> p : groceries + loop all grocery g in groceries + p -> gl : addGrocery(g) + end + + else command == DEL + p -> gl : removeGrocery(commandParts[1]) + + else else + p -> sys : println(GitException.getMessage()) + end + +@enduml \ No newline at end of file diff --git a/docs/diagrams/caloriesManagement.png b/docs/diagrams/caloriesManagement.png new file mode 100644 index 0000000000..85c87aab14 Binary files /dev/null and b/docs/diagrams/caloriesManagement.png differ diff --git a/docs/diagrams/caloriesManagement.puml b/docs/diagrams/caloriesManagement.puml new file mode 100644 index 0000000000..e9752371e1 --- /dev/null +++ b/docs/diagrams/caloriesManagement.puml @@ -0,0 +1,44 @@ +@startuml + +participant ":Parser" as p +participant "command:CalCommand" as cal +participant "caloriesUi:CaloriesUi" as cui +participant "ui:Ui" as ui +participant "food:Food" as f +participant "userInfo:UserInfo" as info +participant "foodList:foodList" as fl +participant ":System.out" as sys + + -> p : caloriesManagement(commandParts) +create cal +p -> cal : valueOf(commandParts[0].toUpperCase()) +cal --> p : command + +alt command == EAT + p -> cui : promptForCalories() + ui --> p : calories + create f + p -> f : Food(commandParts[1],calories) + f --> p : food + p -> fl : addFood(food) + p -> info : consumptionOfCalories(foodList.getFoods()) +else command == VIEW + p -> fl: printFoods() +else command == switch + p -> ui : switchMode() + ui --> p : currentMode +else command == HELP + p -> ui : displayHelpForCal() +else command == EXIT + alt commandParts[1].isEmpty() + p -> sys : println("bye bye") + else else + p -> sys : println(GitException.getMessage()) + end +else else + p -> sys : println(GitException.getMessage()) +end + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/editCost.png b/docs/diagrams/editCost.png new file mode 100644 index 0000000000..82036bac8a Binary files /dev/null and b/docs/diagrams/editCost.png differ diff --git a/docs/diagrams/editCost.puml b/docs/diagrams/editCost.puml new file mode 100644 index 0000000000..c3794b5ee6 --- /dev/null +++ b/docs/diagrams/editCost.puml @@ -0,0 +1,37 @@ +@startuml + +participant "groceryList:GroceryList" as gl +participant "grocery:Grocery" as g +participant "groceryUi:GroceryUi" as ui +participant "storage:Storage" as s + +activate gl +activate g +activate ui +activate s +-> gl : editCost(details) +gl -> gl:checkDetails(details, "cost", "$") +activate gl +return costParts + +gl -> gl: getGrocery(costParts[0].strip()) +activate gl +return Grocery + +gl -> g : setCost(cost) +note right +Ensures the input cost is valid +end note + +gl->ui:printCostSet(grocery) + +gl -> gl : getGroceries() +activate gl +return groceries + +gl -> s :saveGroceryFile(groceries) + + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/editGrocery.png b/docs/diagrams/editGrocery.png new file mode 100644 index 0000000000..69d7301cc4 Binary files /dev/null and b/docs/diagrams/editGrocery.png differ diff --git a/docs/diagrams/editGrocery.puml b/docs/diagrams/editGrocery.puml new file mode 100644 index 0000000000..f611b69f89 --- /dev/null +++ b/docs/diagrams/editGrocery.puml @@ -0,0 +1,28 @@ +@startuml + +participant ":Parser" as p +participant "groceryList:GroceryList" as gl +participant ":System.out" as sys + + -> p : addOrDelGrocery(command, commandParts) + + alt command == EXP + p -> gl : editExpiration(commandParts[1]) + else command == CAT + p -> gl : editCategory(commandParts[1]) + else command == AMT or command == USE + p -> gl : editAmount(commandParts[1], commandParts[0].equals("use")) + + else command == TH + p -> gl : editThreshold(commandParts[1]) + else command == COST + p -> gl : editCost(commandParts[1]) + else command == RATE + p -> gl : editRatingAndReview(commandParts[1]) + else command == STORE + p -> gl : editLocation(commandParts[1]) + else else + p -> sys : println(GitException.getMessage()) + end + +@enduml \ No newline at end of file diff --git a/docs/diagrams/editLocation.png b/docs/diagrams/editLocation.png new file mode 100644 index 0000000000..1a8029678d Binary files /dev/null and b/docs/diagrams/editLocation.png differ diff --git a/docs/diagrams/editLocation.puml b/docs/diagrams/editLocation.puml new file mode 100644 index 0000000000..88f207085b --- /dev/null +++ b/docs/diagrams/editLocation.puml @@ -0,0 +1,65 @@ +@startuml + +participant "groceryList:GroceryList" as gl +participant ":LocationList" as ll +participant "grocery:Grocery" as g +participant "oldLocation:Location" as ol +participant "location:Location" as nl + + -> gl : editLocation() +activate gl + +gl -> gl : checkDetails() +note right +Ensures user input is valid +end note +activate gl +return locationParts + +gl -> gl : getGrocery() +activate gl +return grocery + +alt Location exists + gl -> ll : findLocation() + activate ll + return location +else Location does not exist + gl -> ll : findLocation() + activate ll + ll -> ll : addLocation() + + note right + Creates new Location + end note + + activate ll + return Location + return location +end + +gl -> g : getLocation() +activate g +return oldLocation + +opt Target location == Old location + <- gl: throw SameLocationException() + +else Grocery previously stored + gl -> ol : removeGrocery() + activate ol + return +end + +gl -> g : setLocation() +activate g +return + +gl -> nl : addGrocery() +activate nl +return + +deactivate gl + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/editThreshold.png b/docs/diagrams/editThreshold.png new file mode 100644 index 0000000000..359f0fd743 Binary files /dev/null and b/docs/diagrams/editThreshold.png differ diff --git a/docs/diagrams/editThreshold.puml b/docs/diagrams/editThreshold.puml new file mode 100644 index 0000000000..b336d4d9ed --- /dev/null +++ b/docs/diagrams/editThreshold.puml @@ -0,0 +1,41 @@ +@startuml + +participant "groceryList:GroceryList" as gl +participant "grocery:Grocery" as g +participant "groceryUi:GroceryUi" as ui +participant "storage:Storage" as s + +activate gl +activate g +activate ui +activate s +-> gl : editThreshold(details) +gl -> gl:checkDetails(details, "th", "a/") +activate gl +return amtParts + +gl -> gl: getGrocery(costParts[0].strip()) +activate gl +return Grocery + +gl -> gl: checkAmount(thresholdString) +activate gl +return threshold + +gl -> g : setThreshold(threshold) +note right +Ensures the input threshold is valid +end note + +gl->ui:printThresholdSet(grocery + +gl -> gl : getGroceries() +activate gl +return groceries + +gl -> s :saveGroceryFile(groceries) + + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/executeCommand.png b/docs/diagrams/executeCommand.png new file mode 100644 index 0000000000..04d015bfc9 Binary files /dev/null and b/docs/diagrams/executeCommand.png differ diff --git a/docs/diagrams/executeCommand.puml b/docs/diagrams/executeCommand.puml new file mode 100644 index 0000000000..3c6148aec2 --- /dev/null +++ b/docs/diagrams/executeCommand.puml @@ -0,0 +1,35 @@ +@startuml + +participant ":Parser" as p +participant "ui:Ui" as ui +participant "mode:Mode" as m +participant ":System.out" as sys + +-> p : executeCommand(commandParts, selectedMode) +create m +p -> m : valueOf(currentMode.toUpperCase()) +m --> p : mode + +alt mode == GROCERY + p -> p : groceryManagement(commandParts) + ref over p, ui, sys : groceryManagement +else mode == CALORIES + p -> p : caloriesManagement(commandParts) + ref over p, ui, sys : caloriesManagement +else mode == PROFILE + p -> p : profileManagement(commandParts) + ref over p, ui, sys : profileManagement +else mode == RECIPE + p -> p : recipeManagement(commandParts) + ref over p, ui, sys : recipeManagement +else mode == MODE + p -> ui : switchMode() + ui --> p : mode +else mode == HELP + p -> ui : displayHelp() +else mode == EXIT + p -> sys : println("bye bye") +else else + p -> sys : println(GitException.getMessage()) +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/groceryManagement.png b/docs/diagrams/groceryManagement.png new file mode 100644 index 0000000000..f7d52c48bc Binary files /dev/null and b/docs/diagrams/groceryManagement.png differ diff --git a/docs/diagrams/groceryManagement.puml b/docs/diagrams/groceryManagement.puml new file mode 100644 index 0000000000..5c55b67a02 --- /dev/null +++ b/docs/diagrams/groceryManagement.puml @@ -0,0 +1,44 @@ +@startuml + +participant ":Parser" as p +participant "command:GroceryCommand" as g +participant "groceryList:GroceryList" as gl + + -> p : groceryManagement(commandParts) +create g +p -> g : valueOf(commandParts[0].toUpperCase()) +g --> p : command + +p -> g : ordinal() +g --> p : index +p -> g: DEL.ordinal() +g --> p : indexOfDel +p -> g: STORE.ordinal() +g --> p : indexOfStore +p -> g: LISTLOC.ordinal() +g --> p : indexOfListloc +p -> g: FIND.ordinal() +g --> p : indexOfFind +p -> g: VIEW.ordinal() +g --> p : indexOfView +alt index <= indexOfDel + p -> p : addOrDelGrocery(command, commandParts) + ref over p : caloriesManagement +else index <= indexOfStore + p -> p : editGrocery(command, commandParts) + ref over p : editGrocery +else index <= indexOfListloc + p -> p : handleLocationCommands(command, commandParts[1]) + ref over p : handleLocationCommands +else index == indexOfFind + p -> gl : findGroceries(commandParts[1]) + ref over p, gl : findGroceries +else index == indexOfView + p -> gl : viewGrocery(commandParts[1]) + ref over p, gl : viewGrocery +else else + p -> p : handleListOrHelp(command) + ref over p : handleListOrHelp +end + +@enduml \ No newline at end of file diff --git a/docs/diagrams/handleListOrHelp.png b/docs/diagrams/handleListOrHelp.png new file mode 100644 index 0000000000..0c386c37f4 Binary files /dev/null and b/docs/diagrams/handleListOrHelp.png differ diff --git a/docs/diagrams/handleListOrHelp.puml b/docs/diagrams/handleListOrHelp.puml new file mode 100644 index 0000000000..b3ef40357e --- /dev/null +++ b/docs/diagrams/handleListOrHelp.puml @@ -0,0 +1,37 @@ +@startuml + +participant ":Parser" as p +participant "groceryList:GroceryList" as gl +participant ":System.out" as sys +participant "ui:Ui" as ui + + -> p : handleListOrHelp(command, commandParts) + +p -> p : checkListCommand(commandParts) + + + alt command == LIST + p -> gl : listGroceries() + else command == LISTCAT + p -> gl : sortByCategory() + else command == LISTCOST + p -> gl : sortByCost() + else command == LISTEXP + p -> gl : sortByExpiration() + else command == EXPIRING + p -> gl : displayGroceriesExpiringInNext3Days() + else command == LOW + p -> gl : listLowStocks() + else command == HELP + p -> ui : displayHelpForGrocery() + else command == SWITCH + p -> ui : switchMode() + ui --> p : currentMode + else command == EXIT + p -> sys : println("bye bye!") + else else + p -> sys : println(GitException.getMessage()) + end + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/handleLocationCommands.png b/docs/diagrams/handleLocationCommands.png new file mode 100644 index 0000000000..1c216a442a Binary files /dev/null and b/docs/diagrams/handleLocationCommands.png differ diff --git a/docs/diagrams/handleLocationCommands.puml b/docs/diagrams/handleLocationCommands.puml new file mode 100644 index 0000000000..6a87379aab --- /dev/null +++ b/docs/diagrams/handleLocationCommands.puml @@ -0,0 +1,28 @@ +@startuml + +participant ":Parser" as p +participant ":LocationList" as ll +participant ":GroceryUi" as gui +participant "location:Location" as l +participant ":System.out" as sys + + -> p : handleLocationCommands(command, name) + +alt command == LOC + p -> ll : addLocation(name) + p -> gui : printLocationAdded(name) +else command == LISTLOC + alt LISTLOC + p -> ll : listLocations() + else LISTLOC LOCATION + p -> ll : findLocation(name) + ll -> l : Location + l -> l : listGroceries() + end +else command == DELLOC + p -> ll : removeLocation(name) +else else + p -> sys : println(GitException.getMessage()) +end + +@enduml \ No newline at end of file diff --git a/docs/diagrams/profileAndCalories.puml b/docs/diagrams/profileAndCalories.puml new file mode 100644 index 0000000000..89ce2856f9 --- /dev/null +++ b/docs/diagrams/profileAndCalories.puml @@ -0,0 +1,21 @@ +@startuml +skinparam classAttributeIconSize 0 + +class FoodList { + +FoodList() + +addFood(food: Food) : void + +printFoods() : void +} +class Food { + -name: String + -calories: Double + +Food(name: String, calories: double) + +getName(): String + +getCalories(): Double + +print(): String +} + +FoodList *--> "*" Food : contains + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/profileManagement.png b/docs/diagrams/profileManagement.png new file mode 100644 index 0000000000..dd7c0b009c Binary files /dev/null and b/docs/diagrams/profileManagement.png differ diff --git a/docs/diagrams/profileManagement.puml b/docs/diagrams/profileManagement.puml new file mode 100644 index 0000000000..150f12ee3a --- /dev/null +++ b/docs/diagrams/profileManagement.puml @@ -0,0 +1,51 @@ +@startuml + +participant ":Parser" as p +participant "command:ProfileCommand" as prof +participant "profileUi:ProfileUi" as pui +participant "userInfo:UserInfo" as info +participant "ui:Ui" as ui +participant ":System.out" as sys + + -> p : profileManagement(commandParts) +create prof +p -> prof : valueOf(commandParts[0].toUpperCase()) +prof --> p : command + +alt command == UPDATE + p -> pui : promptForName() + pui --> p : name + p -> pui : promptForWeight() + pui --> p : Weight + p -> pui : promptForHeight() + pui --> p : Height + p -> pui : promptForAge() + pui --> p : Age + p -> pui : promptForGender() + pui --> p : Gender + p -> pui : promptForActiveness() + pui --> p : Activeness + p -> pui : promptForAim() + pui --> p : Aim + p -> info : updateInfo(name, weight,height,age,gender,activeness,aim) +else command == VIEW + p -> info: viewProfile() + info --> p : profile + p -> sys : println(profile) +else command == SWITCH + p -> ui : switchMode() + ui --> p : currentMode +else command == HELP + p -> ui : displayHelpForProf() +else command == EXIT + alt commandParts[1].isEmpty() + p -> sys : println("bye bye") + else else + p -> sys : println(GitException.getMessage()) + end +else else + p -> sys : println(GitException.getMessage()) +end + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/useAmt.png b/docs/diagrams/useAmt.png new file mode 100644 index 0000000000..40e712bb97 Binary files /dev/null and b/docs/diagrams/useAmt.png differ diff --git a/docs/diagrams/useAmt.puml b/docs/diagrams/useAmt.puml new file mode 100644 index 0000000000..3d92c37369 --- /dev/null +++ b/docs/diagrams/useAmt.puml @@ -0,0 +1,42 @@ +@startuml + +participant "groceryList:GroceryList" as gl +participant "grocery:Grocery" as g +participant "ui:Ui" as ui + + -> gl : editAmount() +activate gl + +gl -> gl : checkDetails() +note right +Ensures user input is valid +end note +activate gl +return amtParts + +gl -> gl : getGrocery() +activate gl +return grocery + +gl -> gl : checkAmount() +note right +Ensures integer is valid +end note +activate gl +return amount + +gl -> g : setAmount() +activate g +return + +alt finalAmount == 0 + gl -> ui : printAmtDepleted() + activate ui + deactivate ui +else else + gl -> ui : printAmtSet() + activate ui + deactivate ui +end + +@enduml \ No newline at end of file diff --git a/docs/images/GitStartup.png b/docs/images/GitStartup.png new file mode 100644 index 0000000000..ee918626d3 Binary files /dev/null and b/docs/images/GitStartup.png differ diff --git a/docs/team/64-1.md b/docs/team/64-1.md new file mode 100644 index 0000000000..b2bc47398e --- /dev/null +++ b/docs/team/64-1.md @@ -0,0 +1,54 @@ +# LIU SIYI's Project Portfolio Page + +## Project: Grocery in Time +Grocery in Time is a desktop application used for keeping track of groceries. +The user interacts with it using a CLI. It is written in Java and has about 4 kLoC. + +## Summary of contributions +* New Feature: Added the "Add Grocery" function. +* `add GROCEY` + *What it does: enables users to add groceries directly into the grocery list. + *Justification: This feature streamlines the process of managing grocery inventories, making it easier for users to keep their lists up-to-date. + +* New Feature: Added the "AddMulti" function. +* `addmulti` + *What it does: enables user to add multiple groceries into the grocery list. + *Justification: This feature allows user to do less repeated work when adding multiple groceies at a time. + +* Improvement: Amended the "Edit Expiry Date" function. +* `exp GROCERY d/EXPIRATION_DATE` + *What it does: split into two distinct parts for enhanced usability. The first part prompts users for an expiry date when adding a grocery, while the second allows users to edit the expiry date at a later stage. + *Justification: This modification improves the flexibility and accuracy of managing grocery expiry dates, ensuring users can easily update their groceries’ shelf life. + +* Technical Update: Changed time format to LocalDateTime. + What it does: switches the time format from string to LocalDateTime across the application. + Justification: This update standardizes time representation within the app, enhancing data consistency and supporting more complex time-based functionalities. + +* New Feature: Added an "Add Grocery Menu" function. + What it does: prompts the user for additional details after adding a new grocery item, ensuring a comprehensive entry is made. + Justification: This feature enriches the grocery addition process by capturing detailed information from the outset, leading to better inventory management. + +* New Method: Added a method for sorting groceries by expiry date. +* `listexp` + What it does: allows users to sort their grocery list based on the expiry dates of items, in either ascending or descending order. + Justification: Enhances the user's ability to prioritize groceries based on expiry, helping to minimize waste and manage groceries more efficiently. + +* New Method: Added a method to display groceries expiring in 3 days. +* `expiring` + What it does: shows users a list of groceries that are expiring within the next three days and send user email notifications. + Justification: This method significantly aids in reducing waste by alerting users to consume or replace items nearing their expiry, improving overall grocery management. + +* Code Contribution: [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=64-1&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +## Documentation +* User Guide: + * Added documentation for the features `add`, `exp`, `rate`, `listexp`, and `expiring`. +* Developer Guide: + * Added implementation details of the `add`, `exp`, and `expiring` feature. + +* Review/mentoring contributions: + [#19](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/19), + [#21](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/21), + [#46](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/46), + [#49](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/49), + [#73](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/73). diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/luoyu-uwu.md b/docs/team/luoyu-uwu.md new file mode 100644 index 0000000000..4926767aec --- /dev/null +++ b/docs/team/luoyu-uwu.md @@ -0,0 +1,41 @@ +# Luo Yu's Project Portfolio Page + +## Project: Grocery in Time +Grocery in Time is a desktop application used for keeping track of groceries. +The user interacts with it using a CLI. It is written in Java and has nearly 7 kLoC. + +Given below are my contributions to the project. +* New Feature: Added the ability to store grocery `prices`. + * What it does: allows the user to view the cost of a grocery and sort groceries by price in descending order. + * Justification: This feature improves the product significantly as user can track how much they are spending. +* New Feature: Added the ability to store grocery `threshold` amount. + * What it does: allows the user to view low-stock groceries and receive reminders when consumption dips below set thresholds. + * Justification: This feature greatly enhances the product by keeping users informed about which groceries need replenishing. +* New Feature: Added the ability to select `different modes` and switch between them. + * What it does: allows the users to seamlessly switch between grocery, profile, calories, and recipe management modes. + * Justification: This feature enhances user experience by facilitating easy navigation between various functions. +* New Feature: Added the ability to consumed `food` and manage `calories intake`. + * What it does: allows the user to input their details to calculate target calorie intake, track consumed calories, + and receive reminders if they exceed their target intake. + * Justification: + This feature significantly enhances the product by enabling users to monitor their calorie intake effectively. + +* Code Contribution: [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=luoyu-uwu&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +* Documentation + * User Guide: + * Added documentation for the features `switch`, `cost`, `th`, `low`, `eat`, `view` and `update`. + * Developer Guide: + * Added implementation details of the `list`, `listcost`, `th`, `cost` and `low` feature. + * Added `Execute Command`, `Calories Management Mode`, `Profile Management Mode` and `Grocery Management Mode` sequence diagram. + * Added `addOrDelGrocery`, `editGrocery`, and `viewListOrHelp` sequence diagram. + * Added `editCost` and `editThreshold` sequence diagram. + * Added `class diagram` for Calories and Profile Management Mode. + +* Review/mentoring contributions: + * [#18](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/18) , + [#49](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/49), + [#60](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/60), + [#73](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/73), + [#153](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/153), + [#154](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/154). \ No newline at end of file diff --git a/docs/team/luozihui2003.md b/docs/team/luozihui2003.md new file mode 100644 index 0000000000..fb4ea9a2df --- /dev/null +++ b/docs/team/luozihui2003.md @@ -0,0 +1,26 @@ +# Zi Hui's Project Portfolio Page +## Project: Grocery in Time +Grocery in Time is a desktop application used for keeping track of groceries. +The user interacts with it using a CLI. It is written in Java and has about 7 kLoC. +## Summary of contributions +* New Feature: Added the "Delete Grocery" function. + * What it does: enables users to delete groceries from the list of groceries. + * Justification: This feature aids user in deleting the grocery that they no longer need. + +* New Feature: Add category parameter for Grocery class + * What it does: Add the category of the grocery, and automatically assigns the unit to the item by its category (e.g. beverage is assigned ml). + * Justification: This feature enables clarity for the user, by adding a unit behind the amount. + +* New Feature: Add Save and Load file feature for Grocery + * What it does: Saves the grocery list, with all its details into a text file, and loads it. + * Justification: This feature enables users to leave the program, and still be able to see all their groceries stored when they return. +* New Feature: Add Save and Load file feature for Recipe + * What it does: Saves the recipe list, with all its details into a text file, and loads it. + * Justification: This feature enables users to leave the program, and still be able to see all their recipes stored when they return. +* New Feature: Add Save and Load file feature for Profile + * What it does: Saves the User Profile, with all its details into a text file, and loads it. + * Justification: This feature enables users to leave the program, and be recognised as an existing user when they return. +* Improvement: Wipes saved file if user corrupts it + * What it does: Resets the saved file to contain nothing if a user removes a line or divider. + * Justification: This feature ensures that the saved file can be parsed correctly when loading. +* Code Contribution: [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=luozihui2003&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code&tabOpen=false) diff --git a/docs/team/sharlynlui.md b/docs/team/sharlynlui.md new file mode 100644 index 0000000000..19477cdccc --- /dev/null +++ b/docs/team/sharlynlui.md @@ -0,0 +1,41 @@ +# Sharlyn's Project Portfolio Page + +## Project: Grocery in Time +Grocery in Time is a desktop application used for keeping track of groceries. +The user interacts with it using a CLI. It is written in Java and has about 7 kLoC. + +## Summary of contributions +* **New Class:** Added the Recipe class and RecipeList class. + * What it does: allow users to manage recipes with title, ingredients and steps in the application. + * Justification: users will be able to record their own recipes for easy viewing. +* **New Feature:** Added `View` Recipe, `List` Recipe, `Add` Recipe, `Edit` Recipe, `Delete` Recipe, `Find` Recipe and `View` Recipe. + * Justification: allow users to manage recipes in the application. +* **New Feature:** Added `View` Grocery. + * What it does: users can view all the details of the specific grocery. + * Justification: users might not remember what they have recorded for the grocery and might want to check the details. +* **New Feature:** Added `Remark` and `Edit` Remark. + * What it does: allow users to add a remark attached to the grocery to be displayed when viewed or listed. + * Justification: users can remind themselves instructions for these groceries e.g. to keep it for next week. +* **Code Contributed:** [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=sharlynlui&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) +* **Project management:** + * Manged releases v1.0-v2.1 on GitHub +* **Enhancements to existing features:** + * Extracted Calories Ui, GroceryUi, ProfileUi, RecipeUi from Ui. + * Updated recipe feature to only add valid recipes and disallow duplicated recipes. + * Updated rate grocery feature to only accept valid inputs. + * Updated greetings to only accept valid input as username. + * Cosmetic improvement made to the display of view recipe feature. + * Improved defensiveness with test cases and assertions. +* **Documentation:** + * User Guide: + * Added documentation for Recipe Management: `add`, `list`, `view`, `find`, `edit`, `delete`. ([#85](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/85/commits), [#175](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/175)) + * Added documentation for Grocery Management: `view`. + * Categories the commands into 3 main category. ([#179](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/179)) + * Developer Guide: + * Contributed to the target user profile, value proposition and user stories. +* **Community:** + * PRs reviewed (example [1](https://github.com/nus-cs2113-AY2324S2/tp/pull/24), [2](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/24)). + * Reported bugs and suggestions for other teams in the class. + * Fixed bugs on our team project. ([#142](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/142),[#138](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/138), [#139](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/139), [#117](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/117), [#102](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/102), [#101](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/101), [#100](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/100), [#91](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/91)) + * Identity potential bugs on our team project. ([#158](https://github.com/AY2324S2-CS2113-T12-2/tp/issues/158)) + diff --git a/docs/team/wallywallywally.md b/docs/team/wallywallywally.md new file mode 100644 index 0000000000..8b9306bf00 --- /dev/null +++ b/docs/team/wallywallywally.md @@ -0,0 +1,146 @@ +# Willson Han Zhekai - Project Portfolio Page + +## Overview +Grocery in Time (GiT) is a **grocery tracker app**, optimised for use via a Command Line Interface (CLI). +It allows users to track and manage their groceries around their home easily. + + + +### Summary of Contributions + +#### Code contributed: [RepoSense link of contributions](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=wallywallywally&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +#### New features and enhancements +1. Ability to **edit** the amount of a grocery + - `amt GROCERY a/AMOUNT`: Set amount + - `use GROCERY a/AMOUNT`: Decrease amount after using +2. Functionalities related to **storage locations** + - `loc LOCATION`: Add location to be tracked + - `delloc LOCATION`: Remove tracked location + - `store GROCERY l/LOCATION`: Store grocery in a given location + - `listloc [LOCATION]` + - Without `LOCATION`: View all tracked locations + - With `LOCATION` View groceries stored in given `LOCATION` + - Loading storage location data +3. Ability to **find** groceries by name: + - `find KEYWORD` +4. Improved defensiveness + - Created custom exceptions with helpful error messages + - Tested and fixed various bugs + + +#### Contributions to documentation +1. #### User Guide + - Documentation for various enhancements + - `add`, `addmulti`, `cat`, `amt`, `use`, `store`, `del`, `find`, `listcat`, `listloc`, `loc`, `delloc` + - General information + - Introduction, Quick Start, Command Summary + +2. #### Developer Guide + - Design and implementation details + - For features `amt`/`use` and `store` + - Class diagram for `Grocery`, `GroceryList`, `Location`, `LocationList` + - Sequence diagram for `handleLocationCommands`, `editAmount`, `editLocation` + - Product scope + - Target user profile, Value proposition, User stories + - General information + - Non-functional requirements, Glossary, Instructions for manual testing + + +#### Contributions to Team-Based Tasks +- Set up GitHub organisation and repository +- General code enhancements regarding readability, exception handling +- Enhancements to overall formatting and readability for User and Developer Guides + - Table of Contents +- Maintained issue tracker and milestones + - Created and delegated issues +- Released v2.0 + + +#### Review/mentoring contributions +As shown in the following PRs: +[#19](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/19), +[#52](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/52), +[#89](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/89), +[#151](https://github.com/AY2324S2-CS2113-T12-2/tp/pull/151) + + +#### Contributions beyond the project team +* Reviewed another team's Developer Guide: [SuperTracker](https://github.com/nus-cs2113-AY2324S2/tp/pull/41) +* Reported bugs for another team's program during the Practical Exam Dry Run: [BinBash](https://github.com/AY2324S2-CS2113T-T09-2/tp) + + +
+ +## Examples of documentation contributions + +## 1. Extracts from the User Guide + +### [Listing storage locations and their groceries: `listloc`](../UserGuide.md#listing-storage-locations-and-their-groceries-listloc) +View all storage locations being tracked, or the groceries stored in a given location + +Format: `listloc [LOCATION]` + +* `LOCATION` is an optional parameter. + * Without `LOCATION`, all storage locations will be displayed. + * With `LOCATION`, all groceries in the given `LOCATION` will be displayed. +* More information on storage locations can be found [here](../UserGuide.md#manage-storage-locations). + +Example of usage: + +* `listloc`: All storage locations are displayed. + +``` +>> listloc + +Here's all the locations you are tracking: +- spice rack +- freezer +- cubby +``` + +* `listloc cubby`: All groceries in `cubby` are displayed. + +``` +>> listloc cubby + +Viewing location: cubby +Here are your groceries! +- cheese (dairy), amount: 50, location: cubby +- pasta (carbs), cost: $2.95, location: cubby +``` + +
+ +## 2. Extracts from the Developer Guide + +### [Grocery Management Mode](../DeveloperGuide.md#4-grocery-management-mode) + +Below is a class diagram showing the associations between the `Grocery`, `GroceryList`, `Location`, and `LocationList` classes. + +![Grocery, GroceryList, Location, LocationList](../diagrams/Grocery.png) + + +### [handleLocationCommands](../DeveloperGuide.md#43-handlelocationcommands) +![handleLocationCommands](../diagrams/handleLocationCommands.png) + +`LOC` and `DELLOC` adds and deletes storage locations. +`LISTLOC [LOCATION]` shows all locations or groceries at a given location, depending on whether a location was passed. + + +### [Edit grocery amount](../DeveloperGuide.md#6-edit-grocery-amount) +* A `Grocery` stores its `amount` as an attribute. All `Grocery` objects are then stored in an ArrayList in `GroceryList`, which entirely handles the editing of the `amount`. + +![Grocery (showing amount) and GroceryList class diagram](../diagrams/GroceryAmt.png) + +* `GroceryList+editAmount()` is used to either decrease or directly set the `amount` of a `Grocery`. It takes in 2 parameters: + 1. details: String — User input read from `Scanner`. + 2. use: boolean — `true` decreases the `amount`, while `false` directly sets it. +* To set the `amount` of a `Grocery`, the user inputs `amt GROCERY a/AMOUNT`. +* To edit the `amount` after using a `Grocery`, the user inputs `use GROCERY a/AMOUNT`. +* Our app then executes `GroceryList+editAmount()` with parameter `use = false` or `true` respectively, as illustrated by the following sequence diagram. + +![useAmt sequence diagram](../diagrams/useAmt.png) + +* Additional checks specific to `use` ensure that the user only inputs a valid `int`, or that the `amount` must not be 0 beforehand. +* Any exceptions thrown come with a message to help the user remedy their specific issue, as displayed by the `Ui`. \ No newline at end of file diff --git a/sampleData/groceryList.txt b/sampleData/groceryList.txt new file mode 100644 index 0000000000..0b55a012e0 --- /dev/null +++ b/sampleData/groceryList.txt @@ -0,0 +1,12 @@ +Meat | 0 | null | 2024-04-14 | MEAT | 0.00 | bottom freezer +milk | 50 | 6 | 2024-12-10 | DAIRY | 5.00 | fridge +mustard | 10 | 2 | 2024-12-10 | SAUCE | 3.75 | fridge +ketchup | 5 | 2 | 2025-11-10 | SAUCE | 3.75 | fridge +eggs | 20 | 2 | 2024-05-01 | SAUCE | 15.00 | fridge +chicken breast | 15 | 10 | 2024-05-01 | MEAT | 6.25 | bottom freezer +paprika | 100 | 20 | 2026-01-01 | SPICES | 5.49 | cabinet +star anise | 25 | 20 | 2026-01-01 | SPICES | 5.49 | cabinet +cinnamon sticks | 100 | 20 | 2026-01-01 | SPICES | 5.49 | cabinet +pasta | 500 | 200 | 2025-01-01 | CARBS | 3.35 | shelf +rice | 2000 | 500 | 2025-01-01 | CARBS | 7.57 | shelf +soup cans | 6 | 2 | 2027-01-01 | SOUP | 3.35 | shelf \ No newline at end of file diff --git a/src/main/java/email/EmailNotifier.java b/src/main/java/email/EmailNotifier.java new file mode 100644 index 0000000000..6b292fce3b --- /dev/null +++ b/src/main/java/email/EmailNotifier.java @@ -0,0 +1,61 @@ +package email; + +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import java.util.Properties; + +/** + * Represents an email notifier that sends email notifications. + */ +public class EmailNotifier { + + // Constants + private static final String USERNAME = "liuli.shisuo.5511@gmail.com"; + private static final String APP_PASSWORD = "scwy avwe xvyy qyzw"; + + /** + * Sends an email to the recipient. + * + * @param recipient Recipient's email address. + * @param subject Subject of the email. + * @param content Content of the email. + */ + public static void sendEmail(String recipient, String subject, String content) { + Properties props = new Properties(); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.host", "smtp.gmail.com"); + props.put("mail.smtp.port", "587"); + + Session session = Session.getInstance(props, new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(USERNAME, APP_PASSWORD); + } + }); + + try { + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(USERNAME)); + message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipient)); + message.setSubject(subject); + message.setText(content); + + Transport.send(message); + System.out.println("Email sent successfully to " + recipient); + } catch (AuthenticationFailedException e) { + System.err.println("Authentication failed: Check your username and app-specific password."); + e.printStackTrace(); + } catch (MessagingException e) { + System.err.println("Failed to send email: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/enumerations/CalCommand.java b/src/main/java/enumerations/CalCommand.java new file mode 100644 index 0000000000..6c9619c446 --- /dev/null +++ b/src/main/java/enumerations/CalCommand.java @@ -0,0 +1,5 @@ +package enumerations; + +public enum CalCommand { + EAT, VIEW, SWITCH, HELP, EXIT +} diff --git a/src/main/java/enumerations/GroceryCommand.java b/src/main/java/enumerations/GroceryCommand.java new file mode 100644 index 0000000000..dedf3d96b7 --- /dev/null +++ b/src/main/java/enumerations/GroceryCommand.java @@ -0,0 +1,8 @@ +package enumerations; + +public enum GroceryCommand { + // order of where command is placed affects the function it calls, refer to groceryManagement + ADD, ADDMULTI, DEL, EXP, CAT, AMT, TH, RATE, USE, COST, REMARK, STORE, LOC, DELLOC, + LISTLOC, FIND, VIEW, LIST, LISTCAT, LISTCOST, LISTEXP, EXPIRING, LOW, HELP, SWITCH, EXIT + +} diff --git a/src/main/java/enumerations/Mode.java b/src/main/java/enumerations/Mode.java new file mode 100644 index 0000000000..26e3e782bb --- /dev/null +++ b/src/main/java/enumerations/Mode.java @@ -0,0 +1,5 @@ +package enumerations; + +public enum Mode { + GROCERY, PROFILE, CALORIES, RECIPE, MODE, EXIT, HELP +} diff --git a/src/main/java/enumerations/ProfileCommand.java b/src/main/java/enumerations/ProfileCommand.java new file mode 100644 index 0000000000..6a4f3e5b38 --- /dev/null +++ b/src/main/java/enumerations/ProfileCommand.java @@ -0,0 +1,5 @@ +package enumerations; + +public enum ProfileCommand { + UPDATE, VIEW, SWITCH, HELP, EXIT +} diff --git a/src/main/java/enumerations/RecipeCommand.java b/src/main/java/enumerations/RecipeCommand.java new file mode 100644 index 0000000000..67dfae386d --- /dev/null +++ b/src/main/java/enumerations/RecipeCommand.java @@ -0,0 +1,5 @@ +package enumerations; + +public enum RecipeCommand { + ADD, LIST, VIEW, DELETE, SWITCH, EXIT, HELP, FIND, EDIT; +} diff --git a/src/main/java/exceptions/CannotUseException.java b/src/main/java/exceptions/CannotUseException.java new file mode 100644 index 0000000000..f0437c4689 --- /dev/null +++ b/src/main/java/exceptions/CannotUseException.java @@ -0,0 +1,13 @@ +package exceptions; + +/** + * Represents the exception thrown when command "use" should not be run due to the grocery an amount of 0. + */ +public class CannotUseException extends GitException { + /** + * Constructs CannotUseException. + */ + public CannotUseException() { + message = "The grocery you want to use is already out of stock - time to replenish!"; + } +} diff --git a/src/main/java/exceptions/DuplicateException.java b/src/main/java/exceptions/DuplicateException.java new file mode 100644 index 0000000000..136a1dfe7e --- /dev/null +++ b/src/main/java/exceptions/DuplicateException.java @@ -0,0 +1,13 @@ +package exceptions; + +/** + * Represents the exception thrown when the user tries to add a duplicate of an item. + */ +public class DuplicateException extends GitException { + /** + * Constructs DuplicateException. + */ + public DuplicateException(String item, String input) { + message = "The " + item + " (" + input + ") already exists, a duplicate will not be added."; + } +} diff --git a/src/main/java/exceptions/FailToCalculateCalories.java b/src/main/java/exceptions/FailToCalculateCalories.java new file mode 100644 index 0000000000..0cca119c14 --- /dev/null +++ b/src/main/java/exceptions/FailToCalculateCalories.java @@ -0,0 +1,12 @@ +package exceptions; + +public class FailToCalculateCalories extends GitException { + + /** + * Constructs failToCalculateCalories. + */ + public FailToCalculateCalories() { + message = "Failed to calculate target calories. \n" + + "Please check if sufficient information has been given." ; + } +} diff --git a/src/main/java/exceptions/GitException.java b/src/main/java/exceptions/GitException.java new file mode 100644 index 0000000000..ed27413251 --- /dev/null +++ b/src/main/java/exceptions/GitException.java @@ -0,0 +1,20 @@ +package exceptions; + +/** + * Represents abstract superclass for GiT-specific exceptions. + */ +public abstract class GitException extends Exception { + protected String message; + + /** + * Constructs GiTException. + */ + public GitException() {} + + /** + * Returns error message. + */ + public String getMessage() { + return message; + } +} diff --git a/src/main/java/exceptions/InsufficientInfoException.java b/src/main/java/exceptions/InsufficientInfoException.java new file mode 100644 index 0000000000..027574411a --- /dev/null +++ b/src/main/java/exceptions/InsufficientInfoException.java @@ -0,0 +1,15 @@ +package exceptions; + +/** + * Represents the exception thrown when the information given is insufficient to calculate BMR. + */ +public class InsufficientInfoException extends GitException { + + /** + * Constructs InsufficientInfoException. + */ + public InsufficientInfoException() { + message = "User's information is insufficient to calculate BMR," + + " please check the current information"; + } +} diff --git a/src/main/java/exceptions/InvalidCommandException.java b/src/main/java/exceptions/InvalidCommandException.java new file mode 100644 index 0000000000..454005fd1d --- /dev/null +++ b/src/main/java/exceptions/InvalidCommandException.java @@ -0,0 +1,14 @@ +package exceptions; + +/** + * Represents the exception thrown when the command is invalid. + */ +public class InvalidCommandException extends GitException { + /** + * Constructs InvalidCommandException. + */ + public InvalidCommandException() { + message = "Unknown command. Type 'help' for a list of commands."; + } + +} diff --git a/src/main/java/exceptions/InvalidDateException.java b/src/main/java/exceptions/InvalidDateException.java new file mode 100644 index 0000000000..f204a38298 --- /dev/null +++ b/src/main/java/exceptions/InvalidDateException.java @@ -0,0 +1,8 @@ +package exceptions; + +public class InvalidDateException extends RuntimeException { + public InvalidDateException(String message) { + super(message); + } +} + diff --git a/src/main/java/exceptions/LocalDateWrongFormatException.java b/src/main/java/exceptions/LocalDateWrongFormatException.java new file mode 100644 index 0000000000..4595501bc3 --- /dev/null +++ b/src/main/java/exceptions/LocalDateWrongFormatException.java @@ -0,0 +1,13 @@ +package exceptions; + +/** + * Represents the exception thrown when the LocalDate format is wrong. + */ +public class LocalDateWrongFormatException extends GitException { + /** + * Constructs LocalDateWrongFormatException. + */ + public LocalDateWrongFormatException() { + message = "Expiration date is in the wrong format. Please use yyyy-MM-dd."; + } +} diff --git a/src/main/java/exceptions/PastExpirationDateException.java b/src/main/java/exceptions/PastExpirationDateException.java new file mode 100644 index 0000000000..256b996bee --- /dev/null +++ b/src/main/java/exceptions/PastExpirationDateException.java @@ -0,0 +1,13 @@ +package exceptions; + +/** + * Represents the exception thrown when the grocery has already expired. + */ +public class PastExpirationDateException extends GitException{ + /** + * Constructs PastExpirationDateException. + */ + public PastExpirationDateException() { + message = "The grocery has already expired!"; + } +} diff --git a/src/main/java/exceptions/SameLocationException.java b/src/main/java/exceptions/SameLocationException.java new file mode 100644 index 0000000000..2490c987a1 --- /dev/null +++ b/src/main/java/exceptions/SameLocationException.java @@ -0,0 +1,13 @@ +package exceptions; + +/** + * Represents the exception thrown when the user tries to store a grocery in the same grocery. + */ +public class SameLocationException extends GitException { + /** + * Constructs SameLocationException. + */ + public SameLocationException(String grocery, String location) { + message = grocery + " is already stored in " + location + "."; + } +} diff --git a/src/main/java/exceptions/commands/CommandWrongFormatException.java b/src/main/java/exceptions/commands/CommandWrongFormatException.java new file mode 100644 index 0000000000..18cbc1f321 --- /dev/null +++ b/src/main/java/exceptions/commands/CommandWrongFormatException.java @@ -0,0 +1,24 @@ +package exceptions.commands; + +import exceptions.GitException; + +/** + * Represents the exception thrown when the command does not follow the proper format. + */ +public class CommandWrongFormatException extends GitException { + /** + * Constructs CommandWrongFormatException. + */ + public CommandWrongFormatException(String command, String parameter) { + message = printWrongFormatFix(command, parameter); + } + + /** + * Creates a message that reminds the user of the proper command format. + */ + public String printWrongFormatFix(String command, String parameter) { + return "Command is in the wrong format, type \"help\" for more information." + + System.lineSeparator() + + command + " needs '" + parameter + "'"; + } +} diff --git a/src/main/java/exceptions/commands/IncompleteParameterException.java b/src/main/java/exceptions/commands/IncompleteParameterException.java new file mode 100644 index 0000000000..397fd1e428 --- /dev/null +++ b/src/main/java/exceptions/commands/IncompleteParameterException.java @@ -0,0 +1,15 @@ +package exceptions.commands; + +import exceptions.GitException; + +/** + * Represents the exception thrown when the format is correct, but parameter input is empty. + */ +public class IncompleteParameterException extends GitException { + /** + * Constructs IncompleteParameterException. + */ + public IncompleteParameterException(String parameter) { + message = parameter + " cannot be empty!"; + } +} diff --git a/src/main/java/exceptions/emptyinput/EmptyInputException.java b/src/main/java/exceptions/emptyinput/EmptyInputException.java new file mode 100644 index 0000000000..f659e9c3f9 --- /dev/null +++ b/src/main/java/exceptions/emptyinput/EmptyInputException.java @@ -0,0 +1,15 @@ +package exceptions.emptyinput; + +import exceptions.GitException; + +/** + * Represents the exception thrown when the input is not given after the command. + */ +public class EmptyInputException extends GitException { + /** + * Constructs EmptyInputException. + */ + public EmptyInputException(String input) { + message = "A " + input + " needs to be specified!"; + } +} diff --git a/src/main/java/exceptions/invalidinput/InvalidAmountException.java b/src/main/java/exceptions/invalidinput/InvalidAmountException.java new file mode 100644 index 0000000000..0274bf6ca9 --- /dev/null +++ b/src/main/java/exceptions/invalidinput/InvalidAmountException.java @@ -0,0 +1,15 @@ +package exceptions.invalidinput; + +import exceptions.GitException; + +/** + * Represents the exception thrown when the amount inputted by the user is invalid. + */ +public class InvalidAmountException extends GitException { + /** + * Constructs InvalidAmountException. + */ + public InvalidAmountException() { + message = "Please input a valid integer that is greater than 0!"; + } +} diff --git a/src/main/java/exceptions/invalidinput/InvalidCostException.java b/src/main/java/exceptions/invalidinput/InvalidCostException.java new file mode 100644 index 0000000000..433da9a41a --- /dev/null +++ b/src/main/java/exceptions/invalidinput/InvalidCostException.java @@ -0,0 +1,17 @@ +package exceptions.invalidinput; + +import exceptions.GitException; + +/** + * Represents the exception thrown when the cost inputted by the user is invalid. + */ +public class InvalidCostException extends GitException { + /** + * Constructs InvalidCostException. + */ + public InvalidCostException() { + + message = "Cost entered is invalid!\n" + + "Please enter the cost (e.g., $1.20):"; + } +} diff --git a/src/main/java/exceptions/nosuch/NoSuchObjectException.java b/src/main/java/exceptions/nosuch/NoSuchObjectException.java new file mode 100644 index 0000000000..a42b1bfc20 --- /dev/null +++ b/src/main/java/exceptions/nosuch/NoSuchObjectException.java @@ -0,0 +1,16 @@ +package exceptions.nosuch; + + +import exceptions.GitException; + +/** + * Represents the exception thrown when the Object the code is looking for does not exist. + */ +public class NoSuchObjectException extends GitException { + /** + * Constructs NoSuchObjectException. + */ + public NoSuchObjectException(String object) { + message = "The " + object + " does not exist!"; + } +} diff --git a/src/main/java/food/Food.java b/src/main/java/food/Food.java new file mode 100644 index 0000000000..80ba5b74fb --- /dev/null +++ b/src/main/java/food/Food.java @@ -0,0 +1,34 @@ +package food; + +public class Food { + private String name; + private double calories; + + /** + * Constructs the food to store name and calories. + * + * @param name Name of the food. + * @param calories Calories of the food. + */ + public Food(String name, double calories) { + this.name = name; + this.calories = calories; + } + + public String getName() { + return name; + } + + public double getCalories() { + return calories; + } + + /** + * Stores the food's name and calories as a string + * @return A string containing the food's name and calories. + */ + public String print() { + assert !(this.name.isEmpty()) : "Name should not be empty. Food constructed wrongly."; + return this.name + ", with " + this.calories + " calories"; + } +} diff --git a/src/main/java/food/FoodList.java b/src/main/java/food/FoodList.java new file mode 100644 index 0000000000..ab9b27b029 --- /dev/null +++ b/src/main/java/food/FoodList.java @@ -0,0 +1,51 @@ +package food; + +import git.GroceryUi; +import java.util.ArrayList; +import java.util.List; + +public class FoodList { + private List foods; + + /** + * Constructs FoodList. + */ + public FoodList() { + foods = new ArrayList<>(); + } + + public List getFoods() { + return foods; + } + + /** + * Adds a new food into the list of food. + * + * @param food New consumed food. + */ + public void addFood(Food food) { + try { + foods.add(food); + GroceryUi.printFoodAdded(food); + assert foods.contains(food) : "Food should be added to the list"; + } catch (NullPointerException e) { + System.out.println("Failed to add food: the food collection is null."); + } catch (Exception e) { + System.out.println("An unexpected error occurred while adding the food: " + e.getMessage()); + } + } + + /** + * Prints the list of food in the list. + */ + public void printFoods() { + if (foods.isEmpty()) { + System.out.println("You have not consumed any food today"); + } else { + System.out.println("Here are the food you have consumed today:"); + for (Food food : foods) { + System.out.println(" - " + food.print()); + } + } + } +} diff --git a/src/main/java/git/CaloriesUi.java b/src/main/java/git/CaloriesUi.java new file mode 100644 index 0000000000..e767d2730a --- /dev/null +++ b/src/main/java/git/CaloriesUi.java @@ -0,0 +1,51 @@ +package git; + +import java.util.Scanner; + +public class CaloriesUi { + + // ATTRIBUTES + public static final String DIVIDER = "- - - - -"; + private static Scanner in; + + // METHODS + /** + * Constructs Ui and initialises Scanner to read input. + */ + public CaloriesUi() { + in = new Scanner(System.in); + } + + //@@author LuoYu-uwu + /** + * Prompts user for calories of the food. + * + * @return The calories of the consumed food. + */ + public double promptForCalories() { + System.out.println("Please enter the calories of the food in kcal:"); + double calories = 0; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + try { + calories = Double.parseDouble(input); + if (calories > 0 ){ + break; + } else { + calories = 0; + System.out.println("Calories entered is invalid!"); + } + } catch (NumberFormatException nfe) { + System.out.println("Calories entered is invalid!"); + } + if(i == 4) { + System.out.println("Failed to enter valid calories, " + + "food will not be stored"); + } else { + System.out.println("Please enter the calories of the food in kcal:"); + } + } + return calories; + } + //@@author LuoYu-uwu +} diff --git a/src/main/java/git/Git.java b/src/main/java/git/Git.java new file mode 100644 index 0000000000..94e417656f --- /dev/null +++ b/src/main/java/git/Git.java @@ -0,0 +1,78 @@ +package git; + +import exceptions.GitException; +import user.UserInfo; + +/** + * Represents the Grocery in Time (GiT) program, allowing users to store and track their groceries! + */ +public class Git { + private Ui ui; + private GroceryUi groceryUi; + private boolean isRunning; + private Parser parser; + private Storage storage; + private UserInfo userInfo; + + /** + * Constructs Git. + */ + public Git() { + ui = Ui.getInstance(); + groceryUi = GroceryUi.getInstance(); + parser = new Parser(ui); + isRunning = true; + storage = new Storage(); + userInfo = storage.loadProfileFile(); + } + + /** + * Runs Git. + */ + private void run() { + String username; + if (storage.isProfileSaved() && userInfo.getName() != null){ + username = ui.printWelcomeToExistingUser(); + } else { + username = ui.printWelcome(); + } + parser.setUsername(username); + + String mode = null; + boolean isInitialised = false; + while (!isInitialised) { + try { + mode = Ui.switchMode(); + isInitialised = true; + } catch (GitException e) { + Ui.printLine(); + } + } + + while (isRunning) { + try { + String[] commandParts; + if (!mode.equals("exit")) { + commandParts = parser.processCommandParts(); + } else { + commandParts = new String[]{"exit", ""}; + } + parser.executeCommand(commandParts, mode); + isRunning = parser.getIsRunning(); + mode = parser.getCurrentMode(); + } catch (GitException e) { + System.out.println(e.getMessage()); + } finally { + Ui.printLine(); + } + } + } + + /** + * Main for GiT. + */ + public static void main(String[] args) { + new Git().run(); + } + +} diff --git a/src/main/java/git/GroceryUi.java b/src/main/java/git/GroceryUi.java new file mode 100644 index 0000000000..b902019cb6 --- /dev/null +++ b/src/main/java/git/GroceryUi.java @@ -0,0 +1,770 @@ +package git; + +import exceptions.DuplicateException; +import exceptions.GitException; +import exceptions.invalidinput.InvalidCostException; +import exceptions.PastExpirationDateException; +import exceptions.nosuch.NoSuchObjectException; +import food.Food; +import grocery.Grocery; +import grocery.GroceryList; +import grocery.location.Location; +import grocery.location.LocationList; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.util.List; +import java.util.HashSet; +import java.util.Scanner; + +public class GroceryUi { + // ATTRIBUTES + public static final String DIVIDER = "- - - - -"; + private static GroceryUi singleGroceryUi = null; + private static Scanner in; + + // METHODS + /** + * Constructs Ui and initialises Scanner to read input. + */ + public GroceryUi() { + in = new Scanner(System.in); + } + + /** + * Returns the single instance of Ui. + */ + public static GroceryUi getInstance() { + if (singleGroceryUi == null) { + singleGroceryUi = new GroceryUi(); + } + return singleGroceryUi; + } + + public static void printLine() { + System.out.println(DIVIDER); + } + + /** + * Prompts user for additional details when adding a grocery. + * + * @param grocery The grocery to be added. + */ + //@@author wallywallywally + public void promptAddMenu(Grocery grocery) { + printAddMenu(grocery.getName()); + String rawInput = in.nextLine().replaceAll(" ", ""); + + // Help is always shown first + if (rawInput.contains("8")) { + System.out.println("Displaying help:"); + singleGroceryUi.displayAddHelp(); + printLine(); + rawInput = rawInput.replaceAll("8",""); + } + + // Remove duplicates + StringBuilder addNums = new StringBuilder(); + for (char choice : rawInput.toCharArray()) { + if (!addNums.toString().contains(String.valueOf(choice))) { + addNums.append(choice); + } + } + + processAddMenu(grocery, addNums.toString()); + } + + /** + * Prompts user for multiple grocery names. + * + * @return the array of the groceries. + * @throws DuplicateException Thrown when the grocery to add already exists. + */ + public Grocery[] promptAddMultipleMenu() throws DuplicateException { + System.out.println("\nHow many groceries would you like to add?"); + int num; + while (true) { + try { + num = Integer.parseInt(in.nextLine().trim()); + if (num <= 0) { + System.out.println("\nPlease enter a positive number."); + } else if (num > 20) { + System.out.println("\nWow, that's too many."); + } else { + break; // Break loop if input is a positive integer + } + } catch (NumberFormatException e) { + System.out.println("\nInvalid input. Please enter a number."); + } + } + + Grocery[] groceries = new Grocery[num]; + Storage storage = new Storage(); + GroceryList groceryList = storage.loadGroceryFile(); + HashSet existingGroceryNames = new HashSet<>(); + + for (int i = 0; i < num; i++) { + String name; + while (true) { + System.out.println("\nAdding item " + (i + 1) + " of " + num); + System.out.println("\nPlease enter the name of the grocery:"); + name = in.nextLine().trim(); + if (groceryList.isGroceryExists(name)){ + throw new DuplicateException("grocery", name); + } + if (name.isEmpty()) { + System.out.println("\nInvalid input. Please enter a non-empty grocery name."); + } else if (existingGroceryNames.contains(name)) { + System.out.println("\nThis grocery has already been added. Please enter a different grocery name."); + } else { + existingGroceryNames.add(name); + break; + } + } + + Grocery grocery = new Grocery(name); + + while (true) { + System.out.println("\nDo you want to include additional details for " + grocery.getName() + "? (Y/N)"); + String choice = in.nextLine().trim().toUpperCase(); + if (choice.equals("Y")) { + promptAddMenu(grocery); // Assuming you have this method implemented elsewhere + break; + } else if (choice.equals("N")) { + System.out.println("\nNo additional details will be added for " + grocery.getName()); + break; + } else { + System.out.println("\nInvalid input. Please enter 'Y' for yes or 'N' for no."); + } + } + + groceries[i] = grocery; + } + + return groceries; + } + + /** + * Prints output when a location is added to LocationList. + * + * @param name Location name. + */ + public static void printLocationAdded(String name) { + System.out.println("New location added: " + name); + } + + /** + * Prints output when a location is removed from LocationList. + * + * @param name Location name. + */ + public static void printLocationRemoved(String name) { + System.out.println("Location: " + name + " has been removed from tracking!"); + } + + /** + * Prints all locations. + * + * @param locations List of locations. + */ + public static void printLocationList(List locations) { + if (locations.isEmpty()) { + System.out.println("No locations are currently being tracked!"); + } else { + System.out.println("Here's all the locations you are tracking:"); + for (Location loc : locations) { + System.out.println(" - " + loc.getName()); + } + } + } + + /** + * Prints the new location set for the selected grocery. + * + * @param grocery The grocery that should be updated. + */ + public static void printLocationSet(Grocery grocery) { + assert !grocery.getLocation().getName().isEmpty() : "Grocery location should not be empty"; + System.out.println(grocery.getName() + " stored in " + grocery.getLocation().getName()); + } + + /** + * Prints the all the grocery found containing the keyword. + * + * @param groceries The list of groceries. + * @param key The keyword to search for. + */ + public static void printGroceriesFound(List groceries, String key) { + if (groceries.isEmpty()) { + System.out.println("No groceries contain: " + key); + } else { + System.out.println("Here are the groceries containing: " + key); + for (Grocery grocery: groceries) { + System.out.println(" - " + grocery.printGrocery()); + } + } + } + + /** + * Prints output after a grocery's amount is set to 0. + * + * @param grocery The grocery that is depleted. + */ + public static void printAmtDepleted(Grocery grocery) { + System.out.println(grocery.getName() + " is now out of stock!"); + } + + /** + * Display help message for the user when adding grocery. + */ + public void displayAddHelp() { + System.out.println( + "Here are some details you can include when adding a grocery:\n" + + "1. Category - what type of grocery is it.\n" + + "2. Amount - how much of the grocery is stored.\n" + + "3. Location - where the grocery is stored.\n" + + "4. Expiration Date - when the grocery expires.\n" + + "5. Cost - how much did the grocery cost.\n" + + "6. Threshold Amount - the minimum amount of the grocery that sets reminder.\n" + + "7. Remark - extra information about the grocery.\n"); + } + + /** + * Prompts the user to enter the location of the grocery. + * If the location is new, it is automatically created + * If left blank, location is set to null. + * + * @return Location of selected grocery. + */ + public Location promptForLocation() { + System.out.println("Please enter the location (e.g. freezer first compartment)"); + String name = in.nextLine().strip(); + + while (name.isBlank()) { + System.out.println("The location cannot be empty!"); + name = in.nextLine().strip(); + } + + Location location; + try { + location = LocationList.findLocation(name); + } catch (NoSuchObjectException e1) { + try { + LocationList.addLocation(name); + GroceryUi.printLocationAdded(name.strip()); + location = LocationList.findLocation(name); + } catch (GitException e2) { + location = null; + } + } + + return location; + } + + //@@author luozihui2003 + + /** + * Prompts user for category. + */ + public String promptForCategory(){ + System.out.println("Please enter the category (e.g. fruit):"); + String category = in.nextLine().trim(); + if (category.isBlank()) { + System.out.println("No category specified - will be left empty."); + } + return category; + } + + /** + * Prompts user for amount. + */ + public int promptForAmount(){ + System.out.println("Please enter a valid integer for the amount (e.g. 3):"); + int amount = 0; + for (int i = 0; i < 5; i++){ + try { + amount = Integer.parseInt(in.nextLine().trim()); + break; + } catch (NumberFormatException e){ + System.out.println("Amount entered is invalid!"); + if (i == 4){ + System.out.println("Failed to enter valid amount. Amount will be stored as 0."); + } else { + System.out.println("Please enter a valid integer."); + } + } + } + return amount; + } + //@@author LuoYu-uwu + /** + * Prompts the user to enter the cost of the grocery for at most 5 times. + * If invalid value is entered for the 6th time, auto set the cost to 0. + * + * @return the cost to be set for the grocery. + */ + public double promptForCost() { + System.out.println("Please enter the cost (e.g., $1.20) or nil:"); + double cost = 0; + for (int i = 0; i < 5; i++) { + String price = in.nextLine().trim(); + if (price.equals("nil")) { + break; + } + try { + cost = convertCost(price); + if (cost >= 0 ){ + break; + } else { + cost = 0; + System.out.println("Cost entered is invalid!"); + System.out.println("Please enter the cost (e.g., $1.20) or nil:"); + } + } catch (GitException e) { + System.out.println(e.getMessage()); + } + } + return cost; + } + + /** + * Removes dollar sign from input cost and convert to double. + * + * @param price Input cost entered by user. + * @return Cost in desired format. + * @throws GitException If there is no Dollar sign or cost entered is not numeric. + */ + private double convertCost(String price) throws GitException{ + if(price.contains("$")) { + String formattedPrice = price.replace("$", ""); + try { + return Double.parseDouble(formattedPrice); + } catch (NumberFormatException nfe) { + throw new InvalidCostException(); + } + } else { + throw new InvalidCostException(); + } + } + + /** + * Prompts user for threshold amount. + */ + public int promptForThreshold(){ + System.out.println("Please enter the threshold amount (e.g. 3) or nil:"); + int threshold = 0; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + if (input.equals("nil")) { + break; + } + try { + threshold = Integer.parseInt(input); + if (threshold >= 0 ){ + break; + } else { + threshold = 0; + System.out.println("Amount entered is invalid!"); + System.out.println("Please enter the threshold amount (e.g. 3) or nil:"); + } + } catch (NumberFormatException nfe) { + System.out.println("Amount entered is invalid!"); + System.out.println("Please enter the threshold amount (e.g. 3) or nil:"); + } + } + return threshold; + } + + /** + * Prints all groceries with amount less than threshold set. + * + * @param groceries An array list of groceries. + */ + public static void printLowStocks(List groceries) { + int size = groceries.size(); + if (size == 0) { + System.out.println("There are no items low in stock :)"); + } else { + System.out.println("Time to top up these groceries!"); + for (Grocery grocery : groceries) { + System.out.println(" - " + grocery.getName() + + " only left: " + grocery.getAmount()); + } + } + } + + public static void lowStockAlert(Grocery grocery) { + System.out.println(grocery.getName() + " is low in stock!"); + System.out.println("There's only " +grocery.getAmount() + " left"); + } + + /** + * Prints output when the selected grocery is removed. + * + * @param grocery The grocery that is removed. + * @param groceries The array list of groceries. + */ + public static void printGroceryRemoved(Grocery grocery, List groceries) { + assert grocery!=null : "Grocery does not exist"; + System.out.println("This grocery is removed:"); + System.out.println(grocery.printGrocery()); + System.out.println("You now have " + groceries.size() + " groceries left"); + } + + /** + * Prints the new threshold set for the selected grocery. + * + * @param grocery The grocery that should be updated. + */ + public static void printThresholdSet(Grocery grocery) { + String unit; + if (grocery.getUnit() == null) { + unit = ""; + } else { + unit = " " + grocery.getUnit(); + } + System.out.println(grocery.getName() + "'s threshold is now " + + grocery.getThreshold() + unit); + } + + /** + * Prints output after editing the selected grocery's cost. + * + * @param grocery The grocery that should be updated. + */ + public static void printCostSet(Grocery grocery) { + assert (grocery.getCost()!= 0): "grocery cost should not be empty"; + double cost = grocery.getCost(); + String price = "$" + String.format("%.2f", cost); + System.out.println(grocery.getName() + " is now " + price); + } + //@@author LuoYu-uwu + + //@@author lsiyi + /** + * Prints the additional details menu. + */ + public void printAddMenu(String name) { + System.out.println("Before adding " + name + ", do you want to include the following details?"); + System.out.println("1. Category"); + System.out.println("2. Amount"); + System.out.println("3. Location"); + System.out.println("4. Expiration Date"); + System.out.println("5. Cost"); + System.out.println("6. Threshold Amount"); + System.out.println("7. Remark"); + System.out.println("8. Help"); + System.out.println("Please enter the numbers of the details you want to include:"); + System.out.println("You may enter multiple numbers. (e.g. 1234)"); + System.out.println("To skip this step, do not enter any values."); + } + + /** + * Processes the additional details of the grocery to be added. + * + * @param grocery The grocery to be added. + * @param addNums String containing the numbers of the additional details to be added. + */ + public void processAddMenu (Grocery grocery, String addNums) { + for (char choice : addNums.toCharArray()) { + switch (choice) { + case '1': + System.out.println("Including Category"); + String category = singleGroceryUi.promptForCategory(); + grocery.setCategory(category.toUpperCase()); + break; + + case '2': + System.out.println("Including Amount"); + int amount = singleGroceryUi.promptForAmount(); + grocery.setAmount(amount); + break; + + case '3': + System.out.println("Including Location"); + Location location = singleGroceryUi.promptForLocation(); + grocery.setLocation(location); + if (location != null) { + location.addGrocery(grocery); + } + break; + + case '4': + System.out.println("Including Expiration Date"); + String expiration = singleGroceryUi.promptForExpiration(); + try { + grocery.setExpiration(expiration); + } catch (PastExpirationDateException e) { + e.printStackTrace(); + } + break; + + case '5': + System.out.println("Including Cost"); + double cost = singleGroceryUi.promptForCost(); + grocery.setCost(cost); + break; + + case '6': + System.out.println("Including Threshold Amount"); + int threshold = singleGroceryUi.promptForThreshold(); + grocery.setThreshold(threshold); + break; + + case '7': + System.out.println("Including Remark"); + String remark = singleGroceryUi.promptForRemark(); + grocery.setRemark(remark); + break; + + default: + System.out.println("Invalid choice: " + choice); + break; + } + + printLine(); + } + } + + /** + * Prompts user for expiration date. + * Validates the input date for correct format and future dates. + * + * @return Formatted expiration date in the format YYYY-MM-DD. + */ + public String promptForExpiration() { + LocalDate expirationDate = null; + while (expirationDate == null) { + try { + System.out.println("Please enter the year of expiry (e.g. 2024):"); + int year = Integer.parseInt(in.nextLine().trim()); + + System.out.println("Please enter the month of expiry (e.g. July or 07):"); + String monthInput = in.nextLine().trim(); + String monthString = convertMonthToNumber(monthInput); + int month = Integer.parseInt(monthString); + + System.out.println("Please enter the date of expiry (e.g. 19):"); + int day = Integer.parseInt(in.nextLine().trim()); + + // Attempt to create a date from the input. + expirationDate = LocalDate.of(year, month, day); + + // Check if the date is in the past. + if (expirationDate.isBefore(LocalDate.now())) { + System.out.println("The expiration date cannot be in the past. Please try again."); + expirationDate = null; // Reset to null to re-prompt the user. + } + } catch (DateTimeException | NumberFormatException e) { + System.out.println("Invalid date. Please ensure the year, " + + "month, and day are correct and try again."); + } + } + return expirationDate.toString(); // Formats to YYYY-MM-DD by default. + } + + /** + * Prompts user for rating and review. + * + * @param grocery for rate and review. + */ + public static void promptForRatingAndReview(Grocery grocery) { + System.out.println("Please enter the rating from 1 to 5:"); + int rating = 0; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + try { + rating = Integer.parseInt(input); + if (rating > 0 && rating <= 5){ + break; + } else { + rating = 0; + System.out.println("Rating entered is invalid!"); + System.out.println("Please enter the rating (e.g. 5):"); + } + } catch (NumberFormatException nfe) { + System.out.println("Rating entered is invalid!"); + System.out.println("Please enter the rating in integer(e.g. 5):"); + } + } + grocery.setRating(rating); + + System.out.println("Please enter the review:"); + String review = in.nextLine().trim(); + grocery.setReview(review); + } + + /** + * Reads expiration date from user input. + * + * @param month Month of expiration. + * @return Month in numerical format. + */ + private String convertMonthToNumber(String month) { + // Convert month from name to number (e.g., "July" to "07") + String[] monthNames = {"January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"}; + String[] monthNumbers = {"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"}; + for (int i = 0; i < monthNames.length; i++) { + if (month.equalsIgnoreCase(monthNames[i]) || month.equals(monthNumbers[i])) { + return monthNumbers[i]; // Found a match, return the month number + } + } + // If no match found or input is already in numeric format, return original input + // This part can be enhanced to handle invalid months. + return month; + } + //@@author lsiyi + + //@@author SharlynLui + /** + * Prints grocery details for view command. + * + * @param grocery The grocery that should be printed. + */ + public static void printViewGrocery(Grocery grocery) { + assert !(grocery.getName().isEmpty()): "grocery name should not be empty"; + System.out.println("These are the details of " + grocery.getName() + ": "); + if (grocery.getAmount() != 0) { + System.out.println("Amount: " + grocery.getAmount()); + } else if (grocery.getIsSetAmount()){ + System.out.println("Amount:" + grocery.getAmount()); + } else { + System.out.println("Amount: not set"); + } + if (grocery.getExpiration() != null) { + System.out.println("Expiry date: " + grocery.getExpiration()); + } else { + System.out.println("Expiry date: not set"); + } + if (!grocery.getCategory().isEmpty()) { + System.out.println("Category: " + grocery.getCategory()); + } else { + System.out.println("Category: not set"); + } + if (grocery.getCost() != 0) { + System.out.println("Cost: " + grocery.getCost()); + } else if (grocery.getIsSetCost()) { + System.out.println("Cost: " + grocery.getCost()); + } else { + System.out.println("Cost: not set"); + } + if (grocery.getLocation() != null) { + System.out.println("Location: " + grocery.getLocation().getName()); + } else { + System.out.println("Location: not set"); + } + if (grocery.getRating() != 0) { + System.out.println("Rating: " + grocery.getRating()); + } else { + System.out.println("Rating: not set"); + } + if (!grocery.getReview().isEmpty()) { + System.out.println("Review: " + grocery.getReview()); + } else { + System.out.println("Review: not set"); + } + if (!grocery.getRemark().isEmpty()) { + System.out.println("Remark: " + grocery.getRemark()); + } else { + System.out.println("Remark: not set"); + } + } + + /** + * Inform user that Grocery does not exist. + * + */ + public static void printGroceriesNotFound() { + System.out.println("Grocery not found. Please check if the name is correct or try another name."); + } + + /** + * Prints output after setting the selected grocery's remark. + * + */ + public static void printRemarkSet(Grocery grocery) { + assert !(grocery.getRemark().isEmpty()): "grocery remark should not be empty"; + System.out.println("remark:" + grocery.getRemark()); + } + + /** + * Prints output after setting the selected grocery's expiration date. + * + * @param grocery The grocery that should be updated. + */ + public static void printExpSet(Grocery grocery) { + assert !(grocery.getName().isEmpty()): "grocery name should not be empty"; + System.out.println(grocery.getName() + " will expire on: " + grocery.getExpiration()); + } + /** + * Prints output after editing the selected grocery's category. + * + * @param grocery The grocery that should be updated. + */ + public static void printCategorySet(Grocery grocery){ + assert !(grocery.getCategory().isEmpty()): "grocery category should not be empty"; + System.out.println(grocery.getName() + " is now a " + grocery.getCategory()); + } + + + /** + * Prints output after adding a grocery. + * + * @param grocery Grocery added. + */ + public static void printGroceryAdded(Grocery grocery) { + assert !(grocery.getName().isEmpty()): "grocery name should not be empty"; + System.out.println(grocery.getName() + " added!"); + } + + /** + * Prints the new amount set for the selected grocery. + * + * @param grocery The grocery that should be updated. + */ + public static void printAmtSet(Grocery grocery) { + assert grocery.getAmount() >= 0 : "grocery amount should not be empty"; + System.out.println(grocery.getName() + ": " + grocery.getAmount()); + } + + + /** + * Prints out when there are no groceries. + */ + public static void printNoGrocery() { + System.out.println("There's no groceries!"); + } + + /** + * Prints all groceries. + * + * @param groceries An array list of groceries. + */ + public static void printGroceryList(List groceries) { + assert !groceries.isEmpty() : "grocery list should not be empty"; + System.out.println("Here are your groceries!"); + for (Grocery grocery: groceries) { + System.out.println(" - " + grocery.printGrocery()); + } + } + + + public static void printFoodAdded(Food food) { + assert !(food.getName().isEmpty()): "food name should not be empty"; + System.out.println(food.print() + " was consumed!"); + } + + /** + * Prompts user for remark for grocery. + * + * @return the remark to be added. + */ + public String promptForRemark() { + System.out.println("Please enter the remark for this grocery:"); + return in.nextLine().trim(); + } + //@@author SharlynLui + + //@@author luozihui +} diff --git a/src/main/java/git/Parser.java b/src/main/java/git/Parser.java new file mode 100644 index 0000000000..afbe9029de --- /dev/null +++ b/src/main/java/git/Parser.java @@ -0,0 +1,575 @@ +package git; + + +import enumerations.CalCommand; +import enumerations.GroceryCommand; +import enumerations.Mode; +import enumerations.ProfileCommand; +import enumerations.RecipeCommand; +import exceptions.DuplicateException; +import exceptions.GitException; +import exceptions.InvalidCommandException; +import exceptions.emptyinput.EmptyInputException; +import exceptions.nosuch.NoSuchObjectException; +import food.Food; +import food.FoodList; +import grocery.ExpirationChecker; +import grocery.Grocery; +import grocery.GroceryList; +import grocery.location.Location; +import grocery.location.LocationList; +import recipe.Recipe; +import recipe.RecipeList; +import user.UserInfo; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Deals with commands entered by user. + */ +public class Parser { + private GroceryList groceryList; + private FoodList foodList; + private UserInfo userInfo; + private Ui ui; + private GroceryUi groceryUi; + private RecipeUi recipeUi; + private ProfileUi profileUi; + private CaloriesUi caloriesUi; + private RecipeList recipeList; + private Storage storage; + + private boolean isRunning; + private String currentMode; + + + /** + * Constructs Parser. + * + * @param ui Ui object. + */ + public Parser(Ui ui) { + this.storage = new Storage(); + groceryList = storage.loadGroceryFile(); + foodList = new FoodList(); + userInfo = storage.loadProfileFile(); + recipeUi = new RecipeUi(); + groceryUi = new GroceryUi(); + profileUi = new ProfileUi(); + caloriesUi = new CaloriesUi(); + recipeList = storage.loadRecipeFile(); + this.ui = ui; + isRunning = true; + } + + /** + * Processes a command and its details into a valid format for executing relevant code. + * + * @return Array of the fragments of the commands. + */ + public String[] processCommandParts() { + String[] commandParts = ui.processInput(); + if (commandParts.length == 1) { + return new String[]{commandParts[0], ""}; + } else { + return commandParts; + } + } + + /** + * Handles all the user's commands depending on the selected mode. + * + * @param commandParts Fragments of the command entered by the user. + * @param selectedMode Mode of GiT as selected by the user. + * @throws GitException Exception thrown depending on specific error. + */ + public void executeCommand(String[] commandParts, String selectedMode) throws GitException { + this.currentMode = selectedMode; + Mode mode; + try { + mode = Mode.valueOf(currentMode.toUpperCase()); + } catch (Exception e) { + throw new InvalidCommandException(); + } + + switch (mode) { + case GROCERY: + groceryManagement(commandParts); + break; + + case CALORIES: + caloriesManagement(commandParts); + break; + + case PROFILE: + profileManagement(commandParts); + break; + + case RECIPE: + recipeManagement(commandParts); + break; + + case MODE: + currentMode = Ui.switchMode(); + break; + + case HELP: + Ui.displayHelp(); + break; + + case EXIT: + if (commandParts[1].isEmpty()) { + System.out.println("bye bye!"); + isRunning = false; + } else { + throw new InvalidCommandException(); + } + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Handles commands related to calorie tracking. + * + * @param commandParts Fragments of the command entered by the user. + * @throws GitException Exception thrown depending on specific error. + */ + public void caloriesManagement(String[] commandParts) throws GitException { + CalCommand command; + try { + command = CalCommand.valueOf(commandParts[0].toUpperCase()); + } catch (Exception e) { + throw new InvalidCommandException(); + } + + switch (command) { + case EAT: + String name = commandParts[1]; + if (name == null || name.isBlank() || !name.matches("[a-zA-Z]+")) { + throw new EmptyInputException("valid food name"); + } + double calories = caloriesUi.promptForCalories(); + if (calories == 0) { + throw new EmptyInputException("valid calories value"); + } + Food food = new Food(name, calories); + foodList.addFood(food); + userInfo.consumptionOfCalories(foodList.getFoods()); + break; + + case VIEW: + foodList.printFoods(); + System.out.println("You have consumed " + userInfo.getCurrentCalories() + " calories for today"); + break; + + case SWITCH: + currentMode = Ui.switchMode(); + break; + + case HELP: + Ui.displayHelpForCal(); + break; + + case EXIT: + if (commandParts[1].isEmpty()) { + System.out.println("bye bye!"); + isRunning = false; + } else { + throw new InvalidCommandException(); + } + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Sets username after user input. + */ + public void setUsername(String username) { + userInfo.setName(username); + storage.saveProfileFile(userInfo); + } + + /** + * Handles commands related to the user's profile. + * + * @param commandParts Fragments of the command entered by the user. + * @throws GitException Exception thrown depending on specific error. + */ + public void profileManagement(String[] commandParts) throws GitException { + ProfileCommand command; + try { + command = ProfileCommand.valueOf(commandParts[0].toUpperCase()); + } catch (Exception e) { + throw new InvalidCommandException(); + } + + switch (command) { + case UPDATE: + String name = profileUi.promptForName(); + double weight = profileUi.promptForWeight(); + double height = profileUi.promptForHeight(); + int age = profileUi.promptForAge(); + String gender = profileUi.promptForGender(); + String activeness = profileUi.promptForActiveness(); + String aim = profileUi.promptForAim(); + userInfo.updateInfo(name, weight,height,age,gender,activeness,aim); + storage.saveProfileFile(userInfo); + break; + + case VIEW: + System.out.println(userInfo.viewProfile()); + break; + + case SWITCH: + currentMode = Ui.switchMode(); + break; + + case HELP: + Ui.displayHelpForProf(); + break; + + case EXIT: + if (commandParts[1].isEmpty()) { + System.out.println("bye bye!"); + isRunning = false; + } else { + throw new InvalidCommandException(); + } + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Handles commands related to recipe management. + * + * @param commandParts Fragments of the command entered by the user. + * @throws GitException Exception thrown depending on specific error. + */ + public void recipeManagement(String[] commandParts) throws GitException { + RecipeCommand command; + try { + command = RecipeCommand.valueOf(commandParts[0].toUpperCase()); + } catch (Exception e) { + throw new InvalidCommandException(); + } + + switch (command) { + case ADD: + String title = recipeUi.promptForTitle(); + if (title.isEmpty()) { + throw new EmptyInputException("title"); + } + if (recipeList.isRecipeExists(title)) { + throw new DuplicateException("recipe", title); + } + String ingredients = recipeUi.promptForIngredients(); + String[] ingredientsList = ingredients.split("[,]"); + ArrayList ingredientsArr = new ArrayList<>(Arrays.asList(ingredientsList)); + String steps = recipeUi.promptForSteps(); + String[] stepsList = steps.split("[.]"); + ArrayList stepsArr = new ArrayList<>(Arrays.asList(stepsList)); + recipeList.addRecipe(new Recipe(title, ingredientsArr, stepsArr)); + break; + + case LIST: + recipeList.listRecipes(); + break; + + case VIEW: + String titleView = recipeUi.promptForTitle(); + Recipe recipeToView = recipeList.getRecipe(titleView); + recipeToView.viewRecipe(); + break; + + case FIND: + String recipeToFind = recipeUi.promptForTitle(); + recipeList.findRecipe(recipeToFind); + break; + + case EDIT: + String recipeToEdit = recipeUi.promptForTitle(); + if (recipeToEdit.isEmpty()) { + throw new EmptyInputException("title"); + } + if (!recipeList.isRecipeExists(recipeToEdit)) { + throw new NoSuchObjectException("recipe"); + } + String editPart = recipeUi.promptForEdit(); + if (editPart.equalsIgnoreCase("title")) { + String editLine = recipeUi.promptForTitle(); + recipeList.editRecipe(recipeToEdit, editPart, editLine); + } else if (editPart.equalsIgnoreCase("ingredients")) { + String editLine = recipeUi.promptForIngredients(); + recipeList.editRecipe(recipeToEdit, editPart, editLine); + } else if (editPart.equalsIgnoreCase("steps")) { + String editLine = recipeUi.promptForSteps(); + recipeList.editRecipe(recipeToEdit, editPart, editLine); + } + break; + + case DELETE: + String recipeTitle = recipeUi.promptForTitle(); + recipeList.removeRecipe(recipeTitle); + break; + + case SWITCH: + currentMode = Ui.switchMode(); + executeCommand(commandParts, currentMode); + break; + + case EXIT: + if (commandParts[1].isEmpty()) { + System.out.println("bye bye!"); + isRunning = false; + } else { + throw new InvalidCommandException(); + } + break; + + case HELP: + Ui.displayHelpForRecipe(); + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Handles commands related to grocery management. + * + * @param commandParts Fragments of the command entered by the user. + * @throws GitException Exception thrown depending on specific error. + */ + public void groceryManagement(String[] commandParts) throws GitException { + assert commandParts.length == 2 : "Command passed in wrong format"; + + GroceryCommand command; + try { + command = GroceryCommand.valueOf(commandParts[0].toUpperCase()); + } catch (Exception e) { + throw new InvalidCommandException(); + } + + int index = command.ordinal(); + if (index <= GroceryCommand.DEL.ordinal()) { + addOrDelGrocery(command, commandParts); + } else if (index <= GroceryCommand.STORE.ordinal()) { + editGrocery(command, commandParts); + } else if (index <= GroceryCommand.LISTLOC.ordinal()) { + handleLocationCommands(command, commandParts[1]); + } else if (index == GroceryCommand.FIND.ordinal()) { + groceryList.findGroceries(commandParts[1]); + } else if (index == GroceryCommand.VIEW.ordinal()) { + groceryList.viewGrocery(commandParts[1]); + } else { + handleListOrHelp(command, commandParts); + } + } + + /** + * Handles commands related to adding or deleting a grocery. + * + * @param command Command keyword of data type Enum. + * @param commandParts Fragments of the command entered by user. + * @throws GitException Exception thrown depending on specific error. + */ + private void addOrDelGrocery(GroceryCommand command, String[] commandParts) throws GitException { + switch (command) { + case ADD: + String name = commandParts[1]; + if (name == null || name.isBlank()) { + throw new EmptyInputException("grocery"); + } + + if (groceryList.isGroceryExists(name)) { + throw new DuplicateException("grocery", name); + } + + Grocery grocery = new Grocery(commandParts[1]); + groceryUi.promptAddMenu(grocery); + groceryList.addGrocery(grocery); + GroceryUi.printGroceryAdded(grocery); + break; + + case ADDMULTI: + Grocery[] groceries = groceryUi.promptAddMultipleMenu(); + for (Grocery g : groceries) { + groceryList.addGrocery(g); + GroceryUi.printGroceryAdded(g); + } + break; + + case DEL: + groceryList.removeGrocery(commandParts[1]); + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Handles commands related to editing a grocery. + * + * @param command Command keyword of data type Enum. + * @param commandParts Fragments of the command entered by user. + * @throws GitException Exception thrown depending on specific error. + */ + private void editGrocery(GroceryCommand command, String[] commandParts) throws GitException { + switch (command) { + case EXP: + groceryList.editExpiration(commandParts[1]); + break; + + case CAT: + groceryList.editCategory(commandParts[1]); + break; + + case AMT: + case USE: + groceryList.editAmount(commandParts[1], commandParts[0].equals("use")); + break; + + case TH: + groceryList.editThreshold(commandParts[1]); + break; + + case COST: + groceryList.editCost(commandParts[1]); + break; + + case RATE: + groceryList.editRatingAndReview(commandParts[1]); + break; + + case STORE: + groceryList.editLocation(commandParts[1]); + break; + + case REMARK: + groceryList.editRemark(commandParts[1]); + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Handles commands related to locations. + * + * @param command Command keyword of data type Enum. + * @param name Location name. + * @throws GitException Exception thrown depending on specific error. + */ + private void handleLocationCommands(GroceryCommand command, String name) throws GitException { + switch (command) { + case LOC: + LocationList.addLocation(name); + GroceryUi.printLocationAdded(name); + break; + + case LISTLOC: + if (name.isBlank()) { + LocationList.listLocations(); + } else { + Location location = LocationList.findLocation(name); + location.listGroceries(); + } + break; + + case DELLOC: + LocationList.removeLocation(name); + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Checks if the user input for `list` commands are valid i.e. no other words. + * + * @param commandParts User inputs. + * @throws InvalidCommandException Thrown when there is another word after `list`. + */ + private void checkListCommand(String[] commandParts) throws InvalidCommandException { + if (!commandParts[1].isBlank()) { + throw new InvalidCommandException(); + } + } + + /** + * Handles commands related to listing the grocery list, getting help, or switching modes. + * + * @param command Command keyword of data type Enum. + * @param commandParts User inputs. + * @throws GitException Exception thrown depending on specific error. + */ + private void handleListOrHelp(GroceryCommand command, String[] commandParts) throws GitException { + checkListCommand(commandParts); + + switch (command) { + case LIST: + groceryList.listGroceries(); + break; + + case LISTCAT: + groceryList.sortByCategory(); + break; + + case LISTCOST: + groceryList.sortByCost(); + break; + + case LISTEXP: + groceryList.sortByExpiration(); + break; + + case EXPIRING: + ExpirationChecker expirationChecker = new ExpirationChecker(groceryList); + expirationChecker.run(); + break; + + case LOW: + groceryList.listLowStocks(); + break; + + case HELP: + Ui.displayHelpForGrocery(); + break; + + case SWITCH: + currentMode = Ui.switchMode(); + break; + + case EXIT: + System.out.println("bye bye!"); + isRunning = false; + break; + + default: + throw new InvalidCommandException(); + } + } + + // Getters + public boolean getIsRunning() { + return isRunning; + } + + public String getCurrentMode() { + return currentMode; + } +} diff --git a/src/main/java/git/ProfileUi.java b/src/main/java/git/ProfileUi.java new file mode 100644 index 0000000000..3b4faebfe0 --- /dev/null +++ b/src/main/java/git/ProfileUi.java @@ -0,0 +1,233 @@ +package git; + +import java.util.Scanner; + +public class ProfileUi { + // ATTRIBUTES + public static final String DIVIDER = "- - - - -"; + private static Scanner in; + private static final double MAX_HEIGHT = 280; + private static final double MAX_WEIGHT = 370; + private static final double MAX_AGE = 160; + + // METHODS + /** + * Constructs Ui and initialises Scanner to read input. + */ + public ProfileUi() { + in = new Scanner(System.in); + } + + //@@author LuoYu-uwu + /** + * Prompts user for a name. + * + * @return The entered valid name or empty. + */ + public String promptForName() { + System.out.println("Please enter your name"); + String name = ""; + for (int i = 0; i < 5; i++) { + name = in.nextLine().trim(); + if (name.isBlank()) { + if (i == 4) { + System.out.println("Failed to enter valid name, " + + "name will be stored as empty"); + } else { + System.out.println("Please enter a valid name"); + } + } else { + break; + } + } + return name; + } + + /** + * Prompts user for weight. + * + * @return The entered valid weight or 0. + */ + public double promptForWeight() { + System.out.println("Please enter your weight in KG:"); + double weight = 0; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + try { + weight = Double.parseDouble(input); + if (weight > 0 && weight < MAX_WEIGHT) { + break; + } else { + weight = 0; + System.out.println("Weight entered is invalid!"); + System.out.println("Please enter your weight in KG:"); + } + } catch (NumberFormatException nfe) { + System.out.println("Weight entered is invalid!"); + if(i == 4) { + System.out.println("Failed to enter valid weight, " + + "weight will be stored as 0"); + } else { + System.out.println("Please enter your weight in KG:"); + } + } + } + + return weight; + } + + /** + * Prompts user for height. + * + * @return The entered valid height or 0. + */ + public double promptForHeight() { + System.out.println("Please enter your height in cm:"); + double height = 0; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + try { + height = Double.parseDouble(input); + if (height > 0 && height < MAX_HEIGHT){ + break; + } else { + height = 0; + System.out.println("Height entered is invalid!"); + System.out.println("Please enter your height in cm:"); + } + } catch (NumberFormatException nfe) { + System.out.println("Height entered is invalid!"); + if(i == 4) { + System.out.println("Failed to enter valid height, " + + "height will be stored as 0"); + } else { + System.out.println("Please enter your height in cm:"); + } + } + } + return height; + } + + /** + * Prompts user for age. + * + * @return The entered valid age or 0. + */ + public int promptForAge() { + System.out.println("Please enter your age in years (nearest whole number):"); + int age = 0; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + try { + age = Integer.parseInt(input); + if (age > 0 && age < MAX_AGE){ + break; + } else { + age = 0; + System.out.println("Age entered is invalid!"); + System.out.println("Please enter your age in years (nearest whole number):"); + } + } catch (NumberFormatException nfe) { + System.out.println("Age entered is invalid!"); + if(i == 4) { + System.out.println("Failed to enter valid age, " + + "age will be stored as 0"); + } else { + System.out.println("Please enter your age in years " + + "(nearest whole number):"); + } + } + } + return age; + } + + /** + * Prompts user for gender. + * + * @return The entered valid gender or empty. + */ + public String promptForGender() { + System.out.println("Please enter your gender (M / F / Others):"); + String gender = ""; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + if (input.equalsIgnoreCase("F") + || input.equalsIgnoreCase("M") + || input.equalsIgnoreCase("Others") + ) { + gender = input; + break; + } else { + System.out.println("Gender entered is invalid!"); + if (i == 4) { + System.out.println("Failed to enter valid gender, " + + "gender will be stored as empty"); + } else { + System.out.println("Please enter your gender (M / F / Others):"); + } + } + } + return gender; + } + + /** + * Prompts user for aim. + * + * @return The entered valid aim or empty. + */ + public String promptForAim() { + System.out.println("Please enter your weight aim (e.g. lose/maintain/gain):"); + String aim = ""; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + if (input.equalsIgnoreCase("lose") + || input.equalsIgnoreCase("maintain") + || input.equalsIgnoreCase("gain")) { + aim = input; + break; + } else { + System.out.println("Aim entered is invalid!"); + if (i == 4) { + System.out.println("Failed to enter valid aim, " + + "aim will be stored as empty"); + } else { + System.out.println("Please enter your aim (e.g. lose/maintain/gain):"); + } + } + } + return aim; + } + + /** + * Prompts user for activeness. + * + * @return The entered valid activeness or empty. + */ + public String promptForActiveness() { + System.out.println("Please enter your activeness " + + "(e.g. inactive/light/moderate/active/very):"); + String activeness = ""; + for (int i = 0; i < 5; i++) { + String input = in.nextLine().trim(); + if (input.equalsIgnoreCase("inactive") + || input.equalsIgnoreCase("light") + || input.equalsIgnoreCase("moderate") + || input.equalsIgnoreCase("active") + || input.equalsIgnoreCase("very")) { + activeness = input; + break; + } else { + System.out.println("Activeness entered is invalid!"); + if (i == 4) { + System.out.println("Failed to enter valid activeness, " + + "activeness will be stored as empty"); + } else { + System.out.println("Please enter your activeness " + + "(e.g. inactive/light/moderate/active/very):"); + } + } + } + return activeness; + } + //@@author LuoYu-uwu +} diff --git a/src/main/java/git/RecipeUi.java b/src/main/java/git/RecipeUi.java new file mode 100644 index 0000000000..1e8e6eeee8 --- /dev/null +++ b/src/main/java/git/RecipeUi.java @@ -0,0 +1,164 @@ +package git; + +import recipe.Recipe; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * Deals with interactions with the user in Recipe mode. + */ +public class RecipeUi { + // ATTRIBUTES + public static final String DIVIDER = "- - - - -"; + private static Scanner in; + + // METHODS + + /** + * Constructs Ui and initialises Scanner to read input. + */ + public RecipeUi() { + in = new Scanner(System.in); + } + + + /** + * Prompts user for title when adding recipe in RECIPE mode. + * + * @return the title of the recipe + */ + public String promptForTitle() { + String title = null; + while (title == null) { + System.out.println("Please enter the title of the recipe (e.g. fried egg):"); + title = in.nextLine().trim(); + if (title.isEmpty()) { + System.out.println("Invalid input. Title cannot be empty."); + title = null; + } + } + return title; + } + + /** + * Prompts user for ingredients when adding recipe in RECIPE mode. + * + * @return the ingredients in a single line, trimmed only + */ + public String promptForIngredients() { + String ingredients = null; + while (ingredients == null) { + System.out.println("Please enter the ingredients for this recipe in one line (e.g. egg, salt):"); + ingredients = in.nextLine().trim(); + if (ingredients.isEmpty()) { + System.out.println("Invalid input. Ingredients cannot be empty."); + ingredients = null; + } + } + return ingredients; + } + + /** + * Prompts user for steps when adding recipe in RECIPE mode. + * + * @return the steps in a single line, trimmed only + */ + public String promptForSteps() { + String steps = null; + while (steps == null) { + System.out.println("Please enter the steps for this recipe in one line " + + "(e.g. Fry the egg. Add salt. Serve.):"); + steps = in.nextLine().trim(); + if (steps.isEmpty()) { + System.out.println("Invalid input. Steps cannot be empty."); + steps = null; + } + } + return steps; + } + + /** + * Prompts user for which part of the recipe to edit. + * + * @return The part to be edited. + */ + public String promptForEdit() { + String part = null; + while (part == null) { + System.out.println("Please edit the part of the recipe to be edited.\nOnly ONE part can be edited" + + " (Title / Ingredients / Steps): "); + part = in.nextLine().trim(); + if (part.isEmpty()) { + System.out.println("Invalid input. Steps cannot be empty."); + part = null; + } + if (! (part.equalsIgnoreCase("title") || part.equalsIgnoreCase("ingredients") || + part.equalsIgnoreCase("steps"))) { + System.out.println("Invalid parameter. Please enter Title / Ingredients / Steps."); + part = null; + } + } + return part; + } + + /** + * Informs the user that the recipe has been added to the recipe list. + * + * @param recipe Recipe added. + */ + public static void printRecipeAdded(Recipe recipe) { + assert !(recipe.getTitle().isEmpty()) : "grocery name should not be empty"; + System.out.println(recipe.getTitle() + " added!"); + } + + /** + * Prints out when there are no recipe. + */ + public static void printNoRecipe() { + System.out.println("There's no recipe!"); + } + + /** + * Prints all recipes. + * + * @param recipeArr An array list of groceries. + */ + public static void printRecipeList(ArrayList recipeArr) { + assert !recipeArr.isEmpty() : "recipe list should not be empty"; + System.out.println("Here are your recipe titles!"); + int num = 1; + for (Recipe recipe : recipeArr) { + System.out.println(num + ". " + recipe.getTitle()); + num += 1; + } + } + + /** + * Prints output when the selected recipe is removed. + * + * @param recipe The recipe that is removed. + */ + public static void printRecipeRemoved(Recipe recipe) { + assert recipe != null : "Recipe does not exist"; + System.out.println(recipe.getTitle() + " is removed from the recipe list."); + } + + /** + * Prints the all the recipes found containing the keyword. + * + * @param relevantRecipe The list of recipe. + * @param key The keyword to search for. + */ + public static void printRecipesFound(List relevantRecipe, String key) { + if (relevantRecipe.isEmpty()) { + System.out.println("There is no recipe containing: " + key); + } else { + System.out.println("Here are the recipe(s) containing: " + key); + for (Recipe currRecipe: relevantRecipe) { + System.out.println(" - " + currRecipe.getTitle()); + } + } + } +} diff --git a/src/main/java/git/Storage.java b/src/main/java/git/Storage.java new file mode 100644 index 0000000000..3b7458ab6c --- /dev/null +++ b/src/main/java/git/Storage.java @@ -0,0 +1,335 @@ +package git; + +import exceptions.GitException; +import exceptions.nosuch.NoSuchObjectException; +import grocery.Grocery; +import grocery.GroceryList; +import grocery.location.LocationList; +import recipe.Recipe; +import recipe.RecipeList; +import user.UserInfo; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +import grocery.location.Location; + + +/** + * Handles loading from and saving tasks to a file. + */ +public class Storage { + /** + * Saves the current list of groceries to the file. + * @param groceries The list of groceries to save. + */ + public void saveGroceryFile(List groceries) { + try { + File directory = new File("./data"); + if (!directory.exists()) { + directory.mkdirs(); + } + FileWriter writer = new FileWriter("./data/groceryList.txt"); + for (Grocery grocery : groceries) { + writer.write(grocery.toSaveFormat() + "\n"); + } + writer.close(); + } catch (IOException e) { + System.out.println("An error occurred while saving groceries."); + e.printStackTrace(); + } + } + + /** + * Loads groceries from the file. + * @return groceryList loaded from the file. If file does not exist, returns an empty groceryList. + * If file is corrupted, wipe file. + * + */ + public GroceryList loadGroceryFile(){ + GroceryList groceryList = new GroceryList(); + try { + File file = new File("./data/groceryList.txt"); + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + try { + Grocery grocery = parseGrocery(line); + Location location = grocery.getLocation(); + if (location != null){ + location.addGrocery(grocery); + } + groceryList.addGrocery(grocery); + } catch (Exception e) { + wipeFile(file); + return new GroceryList(); + } + } + scanner.close(); + } catch (FileNotFoundException e) { + //System.out.println("No saved groceries found.\n "); + } + return groceryList; + } + + /** + * Parses a string from the file into a grocery object. + * + * @param line The string to parse. + * @return The parsed grocery object. Returns null if file is corrupted. + */ + public Grocery parseGrocery(String line) { + String[] parts = line.split(" \\| "); + if (parts.length != 7) { + return null; + } else { + String name = parts[0].trim(); + int amount = parts[1].equalsIgnoreCase("null") ? -1 : Integer.parseInt(parts[1].trim()); + int threshold = parts[2].equalsIgnoreCase("null") ? -1 : Integer.parseInt(parts[2].trim()); + String expiration = parts[3].equals("null") ? "" : parts[3].trim(); + String category = parts[4].equalsIgnoreCase("") ? "" : parts[4].trim().toUpperCase(); + double cost = parts[5].equalsIgnoreCase("null") ? -1 : Double.parseDouble(parts[5].trim()); + Location location = parseGroceryLocation(parts[6].strip()); + + Grocery grocery = new Grocery(name); + if (amount != -1) { + grocery.setAmount(amount); + grocery.setIsSetAmount(true); + } + if (threshold != -1) { + grocery.setThreshold(threshold); + } + if (cost != -1) { + grocery.setCost(cost); + grocery.setIsSetCost(true); + } + if (!expiration.isBlank()) { + grocery.setExpirationOnLoad(expiration); + } + grocery.setCategory(category); + grocery.setLocation(location); + + return grocery; + } + } + + /** + * Parses the String containing location information into the location, if there is one. + * + * @param locString String containing information about a location. + * @return Location object. + */ + public Location parseGroceryLocation(String locString) { + Location location = null; + if (!locString.equalsIgnoreCase("null")) { + try { + location = LocationList.findLocation(locString); + } catch (NoSuchObjectException e) { + try { + LocationList.addLocation(locString); + location = LocationList.findLocation(locString); + } catch (GitException ignore) { + assert !locString.isBlank() : "No empty strings at this point."; + } + } + } + + return location; + } + + /** + * Wipes the contents of the specified file. + * + * @param file The file to wipe. + */ + private void wipeFile(File file) { + try { + FileWriter writer = new FileWriter(file); + writer.write(""); + writer.close(); + } catch (IOException e) { + System.out.println("An error occurred while wiping the file: " + file.getName()); + e.printStackTrace(); + } + } + + /** + * Saves the current list of recipes to the file. + * @param recipeArr The list of recipes to save. + */ + public void saveRecipeFile(ArrayList recipeArr) { + try { + File directory = new File("./data"); + if (!directory.exists()) { + directory.mkdirs(); + } + FileWriter writer = new FileWriter("./data/recipeList.txt"); + for (Recipe recipe : recipeArr) { + writer.write(recipe.toRecipeSaveFormat() + "\n"); + } + writer.close(); + } catch (IOException e) { + System.out.println("An error occurred while saving recipes."); + e.printStackTrace(); + } + } + + /** + * Loads recipes from the file. + * @return recipeList loaded from the file. If file does not exist, returns an empty recipeList. + */ + public RecipeList loadRecipeFile(){ + RecipeList recipeList = new RecipeList(); + try { + File file = new File("./data/recipeList.txt"); + Scanner scanner = new Scanner(file); + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + try { + Recipe recipe = parseRecipe(line); + if (recipe != null) { + recipeList.addRecipe(recipe); + } else { + wipeFile(file); + return new RecipeList(); + } + } catch (Exception e) { + wipeFile(file); + return new RecipeList(); + } + } + scanner.close(); + } catch (FileNotFoundException e) { + //System.out.println("No saved recipes found.\n "); + } + return recipeList; + } + + /** + * Parses a string from the file into a Recipe object. + * + * @param line The string to parse. + * @return The parsed recipe object. + */ + private Recipe parseRecipe(String line) { + String[] parts = line.split(" \\| "); + if (parts.length != 3) { + return null; + } else { + String title = parts[0].trim(); + String[] ingredientsArray = parts[1].equalsIgnoreCase("null") ? null : parts[1].split(", "); + ArrayList ingredientsList = new ArrayList<>(Arrays.asList(ingredientsArray)); + String[] stepsArray = parts[2].equalsIgnoreCase("null") ? null : parts[2].split(". "); + ArrayList stepsList = new ArrayList<>(Arrays.asList(stepsArray)); + return new Recipe(title, ingredientsList, stepsList); + } + } + + /** + * Saves the current user profile to the file. + * @param userInfo The user profile to save. + */ + public void saveProfileFile(UserInfo userInfo) { + try { + File directory = new File("./data"); + if (!directory.exists()) { + directory.mkdirs(); + } + FileWriter writer = new FileWriter("./data/userProfile.txt"); + writer.write(userInfo.toProfileSaveFormat()); + writer.close(); + } catch (IOException e) { + System.out.println("An error occurred while saving recipes."); + e.printStackTrace(); + } + } + + /** + * Loads the user profile from the file. + * @return userInfo loaded from the file. If file does not exist, returns an empty userInfo. + */ + public UserInfo loadProfileFile(){ + UserInfo userInfo = new UserInfo(); + try { + File file = new File("./data/userProfile.txt"); + Scanner scanner = new Scanner(file); + int lineCount = 0; + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + lineCount ++; + if (!parseProfile(line, userInfo)) { //if corrupted + wipeFile(file); + return new UserInfo(); + } + } + scanner.close(); + if (lineCount != 8){ + wipeFile(file); + return new UserInfo(); + } + } catch (FileNotFoundException e) { + //System.out.println("No saved recipes found.\n "); + } catch (GitException e) { + throw new RuntimeException(e); + } + return userInfo; + } + + /** + * Parses a string from the file into a userInfo object. + * + * @param line The string to parse. + * @param userInfo The UserInfo object to store the parsed information. + */ + private boolean parseProfile(String line, UserInfo userInfo) throws GitException { + String[] parts = line.split(": "); + if (parts.length != 2) { + return false; // Line is corrupted + } + switch (parts[0]) { + case "Name": + userInfo.setName(parts[1]); + break; + case "Height": + userInfo.setHeight(Double.parseDouble(parts[1])); + break; + case "Weight": + userInfo.setWeight(Double.parseDouble(parts[1])); + break; + case "Age": + userInfo.setAge(Integer.parseInt(parts[1])); + break; + case "Gender": + userInfo.setGender(parts[1]); + break; + case "Aim": + userInfo.setAim(parts[1]); + break; + case "Activeness": + userInfo.setActiveness(parts[1]); + break; + case "Calories": + userInfo.setCaloriesCapFromLoad(Integer.parseInt(parts[1])); + break; + default: + break; + } + return true; + } + + /** + * Checks if the user's profile file exists. + * + * @return True if the profile file exists, false otherwise. + */ + public boolean isProfileSaved() { + File profileFile = new File("./data/userProfile.txt"); + return profileFile.exists(); + } +} diff --git a/src/main/java/git/Ui.java b/src/main/java/git/Ui.java new file mode 100644 index 0000000000..d6f848c13d --- /dev/null +++ b/src/main/java/git/Ui.java @@ -0,0 +1,288 @@ +package git; + +import java.util.Scanner; +import exceptions.GitException; +import exceptions.InvalidCommandException; +import enumerations.Mode; +import user.UserInfo; + + +/** + * Deals with interactions with the user. + */ +public class Ui { + public static final String DIVIDER = "- - - - -"; + private static final double MAX_HEIGHT = 280; + private static final double MAX_WEIGHT = 370; + private static final double MAX_AGE = 160; + private static Ui singleUi = null; + private static Scanner in; + private static String userName; + private Storage storage; + private UserInfo userInfo; + + /** + * Constructs Ui and initialises Scanner to read input. + */ + private Ui() { + in = new Scanner(System.in); + storage = new Storage(); + userInfo = storage.loadProfileFile(); + } + + /** + * Returns the single instance of Ui. + */ + public static Ui getInstance() { + if (singleUi == null) { + singleUi = new Ui(); + } + return singleUi; + } + + public static String getUserName() { + return userName; + } + + /** + * Prints welcome message. + */ + public String printWelcome() { + final String gitlogo = + " ______ _ _________\n" + + " .' ___ | (_)| _ _ |\n" + + "/ .' \\_| __ |_/ | | \\_|\n" + + "| | ____[ | | |\n" + + "\\ `.___] || | _| |_\n" + + " `._____.'[___] |_____|"; + + System.out.println(gitlogo + System.lineSeparator()); + + System.out.println("Hello from GiT"); + userName = null; + while (userName == null) { + System.out.println("What is your name?"); + printLine(); + userName = in.nextLine(); + if (userName.isEmpty()) { + System.out.println("Invalid input. Please enter a valid name."); + userName = null; + } + } + printHello(userName); + displayWelcomeMessage(); + displayHelp(); + + return userName; + } + /** + * Prints welcome message to an existing user. + */ + public String printWelcomeToExistingUser() { + final String gitlogo = + " ______ _ _________\n" + + " .' ___ | (_)| _ _ |\n" + + "/ .' \\_| __ |_/ | | \\_|\n" + + "| | ____[ | | |\n" + + "\\ `.___] || | _| |_\n" + + " `._____.'[___] |_____|"; + + System.out.println(gitlogo + System.lineSeparator()); + + System.out.println("Hello from GiT"); + userName = userInfo.getName(); + printHello(userName); + displayWelcomeMessage(); + displayHelp(); + + return userName; + } + + /** + * Prints Hello with user's name. + * + * @param userName User's name. + */ + public void printHello(String userName) { + System.out.println("Hello " + userName + "!"); + printLine(); + } + + public static void displayWelcomeMessage() { + System.out.println("========================================================================"); + System.out.println("Welcome to GiT - Grocery in Time!"); + System.out.println("GiT is your reliable assistant for managing your grocery inventory efficiently.\n"); + System.out.println("Keep track of your grocery items, "+ + "monitor expiration dates, and never run out of your essentials again."); + System.out.println("We are here to help you manage your groceries better, save time, and reduce waste.\n"); + System.out.println("Type 'help' anytime you need assistance with commands."); + System.out.println("Thank you for choosing GiT - Your groceries organized, on time, every time!"); + System.out.println("========================================================================"); + } + + public static void displayCommands(String selectedMode) throws GitException { + Mode mode; + try { + mode = Mode.valueOf(selectedMode.toUpperCase());; + } catch (Exception e) { + throw new InvalidCommandException(); + } + switch (mode) { + case GROCERY: + displayHelpForGrocery(); + System.out.println("Enter command:"); + break; + + case CALORIES: + displayHelpForCal(); + System.out.println("Enter command:"); + break; + + case PROFILE: + displayHelpForProf(); + System.out.println("Enter command:"); + break; + + case RECIPE: + displayHelpForRecipe(); + System.out.println("Enter command:"); + break; + + case EXIT: + // Do nothing + break; + + default: + throw new InvalidCommandException(); + } + } + + /** + * Processes user input into a command and its details for Parser. + * + * @return Array of the fragments of the command. + */ + public String[] processInput() { + String commandLine = in.nextLine(); + String[] commandParts = commandLine.strip().split(" ", 2); + assert commandParts.length > 0 : "Failed to read user input"; + + return commandParts; + } + + public static String switchMode() throws GitException { + System.out.println("What mode would you like to enter?"); + System.out.println("Please select a mode: " + "grocery, profile, calories or recipe:"); + String newMode = in.nextLine().trim(); + Mode mode; + while (true) { + try { + mode = Mode.valueOf(newMode.toUpperCase()); + break; + } catch (Exception e) { + System.out.println("Please enter a valid mode:"); + newMode = in.nextLine().trim(); + } + } + displayCommands(newMode); + return newMode; + } + + /** + * Displays help message containing all possible commands for grocery management. + */ + public static void displayHelpForGrocery() { + System.out.println( + "Here are some ways you can manage your groceries!\n" + + "add GROCERY: adds the item GROCERY.\n" + + "addmulti: adds multiple items GROCERIES.\n" + + "cat GROCERY c/CATEGORY: edits the category for GROCERY.\n" + + "amt GROCERY a/AMOUNT: sets the amount of GROCERY.\n" + + "use GROCERY a/AMOUNT: updates the total amount after using a GROCERY.\n" + + "store GROCERY l/LOCATION: sets the location of GROCERY.\n" + + "exp GROCERY d/EXPIRATION_DATE: edits the expiration date for GROCERY.\n" + + "cost GROCERY $PRICE: edits the price of GROCERY.\n" + + "th GROCERY a/AMOUNT: edits the threshold amount of GROCERY.\n" + + "remark GROCERY r/REMARK: updates the remark of the GROCERY.\n" + + "rate GROCERY: rates and reviews GROCERY.\n" + + "del GROCERY: deletes GROCERY.\n" + + "find KEYWORD: finds all groceries containing the KEYWORD.\n" + + "view GROCERY: view all the details of GROCERY.\n" + + "low: shows a list of groceries that are low in stock.\n" + + "expiring: shows a list of groceries that are expiring soon.\n" + + "list: shows list of all groceries you have.\n" + + "listcat: shows the list sorted by category.\n" + + "listcost: shows the list sorted by price.\n" + + "listexp: shows the list sorted by expiration date.\n" + + "listloc [LOCATION]: shows all locations, or all groceries stored in [LOCATION].\n" + + "loc LOCATION: adds a LOCATION to track.\n" + + "delloc LOCATION: removes LOCATION from tracking.\n" + + "switch: switches the mode.\n" + + "help: view all the possible commands.\n" + + "exit: exits the program." + ); + } + + /** + * Displays help message containing all possible commands for calories management. + */ + public static void displayHelpForCal() { + System.out.println( + "Here are some ways you can manage your calories intake!\n" + + "eat FOOD: adds the food that you have eaten.\n" + + "view: shows the food you have eaten and total calories intake.\n" + + "switch: switches the mode.\n" + + "exit: exits the program.\n" + + "help: view all the possible commands for calories management." + ); + } + + /** + * Displays help message containing all possible commands for profile management. + */ + public static void displayHelpForProf() { + System.out.println( + "Here are some ways you can manage your profile!\n" + + "update: stores information needed to manage your calories intake.\n" + + "view: view your profile details.\n" + + "switch: switches the mode.\n" + + "exit: exits the program.\n" + + "help: view all the possible commands for profile management." + ); + } + + public static void displayHelpForRecipe() { + System.out.println( + "Here are some ways you can manage your recipes!\n" + + "add: add a new recipe. \n" + + "list: list all your recipes. \n" + + "view: view your recipes details.\n" + + "find: list the recipe(s) with given key.\n" + + "delete: delete the recipe. \n" + + "switch: switches the mode.\n" + + "exit: exits the program.\n" + + "help: view all the possible commands for recipes management." + ); + } + + /** + * Displays help message containing all possible commands for this app. + */ + public static void displayHelp() { + System.out.println( + "Here are some ways you can use our app!\n" + + "grocery: manages your groceries.\n" + + "calories: manages your calories intake.\n" + + "profile: manages your profile.\n" + + "recipe: manages your recipe. \n" + + "exit: exits the program.\n" + ); + } + + /** + * Prints divider for user readability. + */ + public static void printLine() { + System.out.println(DIVIDER); + } +} diff --git a/src/main/java/grocery/ExpirationChecker.java b/src/main/java/grocery/ExpirationChecker.java new file mode 100644 index 0000000000..5cdd159ade --- /dev/null +++ b/src/main/java/grocery/ExpirationChecker.java @@ -0,0 +1,103 @@ +package grocery; + +import email.EmailNotifier; + +import java.time.LocalDate; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/** + * Represents a checker that checks for groceries nearing expiration. + */ +public class ExpirationChecker { + private GroceryList groceryList; + + /** + * Constructs an ExpirationChecker. + * + * @param groceryList GroceryList to check for expiration. + */ + public ExpirationChecker(GroceryList groceryList) { + this.groceryList = groceryList; + } + + /** + * Runs the expiration checker. + */ + public void run() { + StringBuilder emailContent = new StringBuilder("Items nearing expiration:\n\n"); + boolean hasExpiringItems = false; + + System.out.println("Checking for groceries nearing expiration..."); + List groceries = groceryList.getGroceries(); + LocalDate today = LocalDate.now(); + for (Grocery grocery : groceries) { + LocalDate expirationDate = grocery.getExpiration(); + if (expirationDate != null && expirationDate.isBefore(today.plusDays(3))) { + System.out.println(grocery.getName() + " is nearing expiration on " + expirationDate); + emailContent.append(grocery.getName()).append(" expires on ").append(expirationDate).append("\n"); + System.out.println("Do you wish to send a notification email? (y/n)"); + Scanner scanner = new Scanner(System.in); + String response = scanner.nextLine().trim().toLowerCase(); + if (response.equals("y")) { + hasExpiringItems = true; + } else if(response.equals("n")){ + continue; + } else { + System.out.println("Invalid input. Please enter 'y' or 'n'."); + continue; + } + } + } + + if (hasExpiringItems) { + sendExpiryNotification(emailContent.toString()); + } else { + System.out.println("No items are nearing expiration within the next 3 days."); + } + } + + /** + * Sends an email notification to the user. + * + * @param content Content of the email. + */ + private void sendExpiryNotification(String content) { + Scanner scanner = new Scanner(System.in); + try { + System.out.println("Please enter your email to receive notifications:"); + String recipient = scanner.nextLine(); + + // Validate the email address + if (!isValidEmail(recipient)) { + System.out.println("The email address entered is invalid."); + return; + } + + System.out.println("Sending notification email..."); + + String subject = "Grocery Expiry Notification"; + EmailNotifier.sendEmail(recipient, subject, content); // Send the email + } catch (Exception e) { + System.err.println("An error occurred while sending the email notification: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Validates the email address. + * + * @param email Email address to validate. + * @return True if the email address is valid, false otherwise. + */ + private boolean isValidEmail(String email) { + String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + Pattern pattern = Pattern.compile(emailRegex); + Matcher matcher = pattern.matcher(email); + return matcher.matches(); + } + + +} diff --git a/src/main/java/grocery/Grocery.java b/src/main/java/grocery/Grocery.java new file mode 100644 index 0000000000..821a38406f --- /dev/null +++ b/src/main/java/grocery/Grocery.java @@ -0,0 +1,366 @@ +package grocery; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import exceptions.PastExpirationDateException; +import grocery.location.Location; + +/** + * Represents a grocery. + */ +public class Grocery { + public static final String FRUIT = "FRUIT"; + public static final String VEGETABLE = "VEGETABLE"; + public static final String MEAT = "MEAT"; + public static final String BEVERAGE = "BEVERAGE"; + private String name; + private int amount; + private int threshold; + private LocalDate expiration; + private String category; + private String unit; + private double cost; + private Location location; + private int rating; + private String review; + private String remark; + private boolean isSetCost; + private boolean isSetAmount; + + /** + * Constructs a Grocery. + * + * @param name Name. + * @param amount Measurement of grocery. + * @param expiration When grocery expires. + * @param category Category of grocery. + * @param location Location of where the grocery is stored. + */ + public Grocery(String name, int amount, int threshold, + LocalDate expiration, String category, double cost, Location location) { + this.name = name; + this.amount = amount; + this.threshold = threshold; + this.expiration = expiration; + this.category = category; + setUnit(category); + this.cost = cost; + this.isSetCost = true; + this.isSetAmount = true; + this.location = location; + this.rating = 0; + this.review = ""; + this.remark = ""; + } + + /** + * Basic constructor for Grocery. + * + * @param name Name. + */ + public Grocery(String name) { + this.name = name; + this.amount = 0; + this.expiration = null; + this.category = ""; + this.cost = 0; + this.isSetCost = false; + this.isSetAmount = false; + this.location = null; + this.rating = 0; + this.review = ""; + this.remark = ""; + } + + // Getters and setters + public String getName() { + return name; + } + + public int getAmount() { + return amount; + } + + public LocalDate getExpiration() { + return expiration; + } + + public String getCategory() { + return category; + } + + public double getCost() { + return this.cost; + } + + public Location getLocation() { + return this.location; + } + + public int getThreshold() { + return this.threshold; + } + + public int getRating() { + return this.rating; + } + + public String getReview() { + return this.review; + } + + public String getRemark() { + return remark; + } + + public boolean getIsSetCost() { + return isSetCost; + } + + public boolean getIsSetAmount() { + return isSetAmount; + } + + public void setName(String name) { + this.name = name; + } + + public void setCategory(String category) { + this.category = category; + setUnit(category); + } + + public void setAmount(int amount) { + assert amount >= 0 : "Amount entered is invalid!"; + this.amount = amount; + this.isSetAmount = true; + } + + public void setThreshold(int threshold) { + this.threshold = threshold; + } + + public String getUnit() { + return unit; + } + + public void setRating(int rating) { + this.rating = rating; + } + + public void setReview(String review) { + this.review = review; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public void setIsSetCost(boolean isSetCost) { + this.isSetCost = isSetCost; + } + + public void setIsSetAmount(boolean isSetAmount) { + this.isSetAmount = isSetAmount; + } + + /** + * Checks if the grocery is low in stock. + * + * @return True if current amount is lesser than threshold, or amount is 0. + */ + public boolean isLow() { + if (this.amount == 0) { + return true; + } else { + return this.amount < this.threshold; + } + } + /** + * Set unit of the grocery based on its category. + * + * @param category Category of the grocery. + */ + public void setUnit(String category) { + switch (category){ + case FRUIT: + this.unit = "pieces"; + break; + case VEGETABLE: + case MEAT: + this.unit = "grams"; + break; + case BEVERAGE: + this.unit = "ml"; + break; + default: + this.unit = ""; + break; + } + } + + /** + * Formats the expiration date from type string to local date. + * + * @param expiration The expiration date of the grocery. + * @throws PastExpirationDateException + */ + public void setExpiration(String expiration) throws PastExpirationDateException { + assert !(expiration.isEmpty()) : "Expiration date entered is invalid!"; + if (expiration == null||expiration.isEmpty()) { + throw new IllegalArgumentException("Expiration date entered is invalid!"); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate expirationDate = LocalDate.parse(expiration, formatter); + + if (expirationDate.isBefore(LocalDate.now())) { + throw new PastExpirationDateException(); + } + + this.expiration = LocalDate.parse(expiration, formatter); + } + + /** + * Sets the expiration date when loading saved data. + * + * @param expiration The expiration date of the grocery as saved in the data. + */ + public void setExpirationOnLoad(String expiration) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + this.expiration = LocalDate.parse(expiration, formatter); + } + + /** + * Sets the cost of the grocery. + * + * @param cost The cost of the grocery as a double. + */ + public void setCost(double cost) { + assert cost >= 0 : "Cost entered is invalid!"; // Ensure that the cost is non-negative. + this.cost = cost; + this.isSetCost = true; + } + + public void setLocation(Location location) { + this.location = location; + } + + /** + * Returns details that are set in the grocery. + * + * @return String representation of the Grocery. + */ + public String printGrocery() { + assert !(this.name.isEmpty()) : "Grocery does not exist!!"; + + String categoryString = ""; + if (!category.isBlank()) { + categoryString = " (" + category + ")"; + } + + String locationString = ""; + if (this.location != null) { + locationString = ", location: " + this.location.getName(); + } + + String amountString = ""; + if (isSetAmount) { + if (amount != 0) { + amountString = ", amount: " + amount; + } else { + amountString = ", amount: 0"; + } + } + + String unitString = ""; + if (isSetAmount) { + if (unit != null) { + unitString = " " + unit; + } + } + + String exp = ""; + if (expiration != null) { + exp = ", expiration: " + expiration.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + + String price = ""; + if (isSetCost) { + if (cost != 0) { + price = ", cost: $" + String.format("%.2f", cost); + } else { + price = ", cost: $0.00"; + } + } + + String remarkString = ""; + if (!remark.isEmpty()) { + remarkString = ", remark: " + remark; + } + + return this.name + categoryString + amountString + unitString + exp + price + + locationString + remarkString; + + } + + /** + * Returns the name, amount, threshold, expiration date, category, cost and location of the grocery for saving. + * + * @return String representation of the Grocery. + */ + public String toSaveFormat() { + assert !(this.name.isEmpty()) : "Grocery does not exist!!"; + + String amountString; + if (isSetAmount) { + amountString = "| " + amount + " "; + } else { + amountString = "| null "; + } + + String locationString; + if (this.location != null) { + locationString = "| " + this.location.getName() + " "; + } else { + locationString = "| null "; + } + + String thresholdString; + if (this.threshold != 0){ + thresholdString = "| " + threshold + " "; + } else { + thresholdString = "| null "; + } + + String exp; + if (expiration != null) { + exp = "| " + expiration.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " "; + } else { + exp = "| null "; + } + + String categoryString; + if (category != null) { + categoryString = "| " + this.category + " "; + } else { + categoryString = "| null "; + } + + String price; + if (isSetCost) { + price = "| " + String.format("%.2f", cost) + " "; + } else { + price = "| null "; + } + + String remarkString; + if (remark != null) { + remarkString = "| " + remark + " "; + } else { + remarkString = "| null "; + } + return this.name + " " + amountString + thresholdString + exp + categoryString + price + locationString; + + } +} + diff --git a/src/main/java/grocery/GroceryList.java b/src/main/java/grocery/GroceryList.java new file mode 100644 index 0000000000..e6b23834ae --- /dev/null +++ b/src/main/java/grocery/GroceryList.java @@ -0,0 +1,546 @@ +package grocery; + +import exceptions.GitException; +import exceptions.invalidinput.InvalidAmountException; +import exceptions.LocalDateWrongFormatException; +import exceptions.PastExpirationDateException; +import exceptions.emptyinput.EmptyInputException; +import exceptions.CannotUseException; +import exceptions.invalidinput.InvalidCostException; +import exceptions.SameLocationException; +import git.Storage; +import git.GroceryUi; +import exceptions.nosuch.NoSuchObjectException; +import exceptions.commands.IncompleteParameterException; +import exceptions.commands.CommandWrongFormatException; +import grocery.location.Location; +import grocery.location.LocationList; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.Collections; +import java.util.stream.Collectors; + + +/** + * Stores all the user's groceries in a main list. + */ +public class GroceryList { + private List groceries; + private Logger logger; + private Storage storage; + + /** + * Constructs GroceryList. + */ + public GroceryList() { + groceries = new ArrayList<>(); + LoggerGroceryList.setupLogger(); + logger = Logger.getLogger(GroceryList.class.getName()); + this.storage = new Storage(); + } + + /** + * Adds a grocery. + * + * @param grocery Grocery to be added. + */ + public void addGrocery(Grocery grocery) { + + try { + groceries.add(grocery); + storage.saveGroceryFile(getGroceries()); + assert groceries.contains(grocery) : "Grocery should be added to the list"; + } catch (NullPointerException e) { + System.out.println("Failed to add grocery: the grocery is null."); + } catch (Exception e) { + System.out.println("An unexpected error occurred while adding the grocery: " + e.getMessage()); + } + + logger.log(Level.INFO, "Added " + grocery.printGrocery()); + + } + + /** + * Checks if a grocery exists. + * + * @param name Name of the grocery. + * @return True if the grocery exists, false otherwise. + */ + public boolean isGroceryExists(String name) { + for (Grocery grocery : groceries) { + if (grocery.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + /** + * Returns the desired grocery. + * + * @param name Name of the grocery. + * @return The needed grocery. + * @throws NoSuchObjectException If the selected grocery does not exist. + */ + public Grocery getGrocery(String name) throws NoSuchObjectException { + int index = -1; + for (Grocery grocery : groceries) { + if(grocery.getName().equalsIgnoreCase(name)) { + index = groceries.indexOf(grocery); + break; + } + } + + if (index != -1) { + assert groceries != null : "Found grocery should not be null"; + return groceries.get(index); + } else { + throw new NoSuchObjectException("grocery (" + name + ")"); + } + } + /** + * Returns the desired groceries. + * + * @return The needed groceries. + */ + public List getGroceries(){ + return groceries; + } + + /** + * Checks whether details are valid, else throw GitException accordingly. + * + * @param details User input. + * @param command Command word. + * @param parameter Parameter for the command. + * @return String array of valid details. + * @throws GitException Exception thrown depending on error. + */ + private String[] checkDetails(String details, String command, String parameter) throws GitException { + if (details.isEmpty()) { + throw new EmptyInputException("grocery"); + } + + // Split the input into the grocery name and the detail part. + String[] detailParts; + if (command.equals("cost")) { + detailParts = details.split("\\$", 2); + } else { + detailParts = details.split(parameter, 2); + } + + // Check iin the grocery exists + if (!isGroceryExists(detailParts[0].strip())) { + throw new NoSuchObjectException("grocery (" + detailParts[0].strip() + ")"); + } + + // Missing parameter + if (detailParts.length < 2) { + throw new CommandWrongFormatException(command, parameter); + } + + String attribute = detailParts[1].strip(); + if (attribute.isEmpty()) { + throw new IncompleteParameterException(parameter); + } + + return new String[] {detailParts[0].strip(), attribute}; + } + + /** + * Sets the expiration date of an existing grocery. + * + * @param details A string containing grocery name and details. + * @throws GitException Exception thrown depending on error. + */ + public void editExpiration(String details) throws GitException { + String[] expParts = checkDetails(details, "exp", "d/"); + Grocery grocery = getGrocery(expParts[0].strip()); + + // Parse the date string to LocalDate + LocalDate date; + try { + date = LocalDate.parse(expParts[1].strip(), DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } catch (DateTimeParseException e) { + throw new LocalDateWrongFormatException(); + } + + // Convert LocalDate back to String to match the setExpiration signature + String dateString = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + try { + grocery.setExpiration(dateString); + } catch (PastExpirationDateException e) { + System.out.println(e.getMessage()); + } + + // Verification and UI feedback + GroceryUi.printExpSet(grocery); + storage.saveGroceryFile(getGroceries()); + } + + /** + * Sets the category of an existing grocery. + * + * @param details User input. + * @throws GitException Exception thrown depending on error. + */ + public void editCategory(String details) throws GitException { + String[] catParts = checkDetails(details, "cat", "c/"); + Grocery grocery = getGrocery(catParts[0].strip()); + String newCategory = catParts[1].strip(); + + grocery.setCategory(newCategory.toUpperCase()); + GroceryUi.printCategorySet(grocery); + storage.saveGroceryFile(getGroceries()); + } + + /** + * Checks whether the amount inputted by the user is valid. + * + * @param amountString String of amount inputted by the user. + * @return Valid amount. + * @throws InvalidAmountException Thrown if the amount is not a valid integer that is greater than 0 + */ + private int checkAmount (String amountString) throws InvalidAmountException { + int amount; + try { + amount = Integer.parseInt(amountString); + } catch (NumberFormatException e) { + throw new InvalidAmountException(); + } + + if (amount <= 0) { + throw new InvalidAmountException(); + } + + return amount; + } + + /** + * Sets the amount of an existing grocery. + * + * @param details User input. + * @param isUse True to reduce the amount of a grocery, false to set a new amount. + * @throws GitException Exception thrown depending on error. + */ + public void editAmount(String details, boolean isUse) throws GitException { + String [] amtParts; + if (isUse) { + amtParts = checkDetails(details, "use", "a/"); + } else { + amtParts = checkDetails(details, "amt", "a/"); + } + Grocery grocery = getGrocery(amtParts[0].strip()); + String amountString = amtParts[1].strip(); + int amount = checkAmount(amountString); + + if (isUse && grocery.getAmount() == 0) { + throw new CannotUseException(); + } else if (isUse) { + amount = Math.max(0, grocery.getAmount() - amount); + } + + grocery.setAmount(amount); + storage.saveGroceryFile(getGroceries()); + if (amount == 0) { + GroceryUi.printAmtDepleted(grocery); + } else if (grocery.isLow()){ + GroceryUi.lowStockAlert(grocery); + } else { + GroceryUi.printAmtSet(grocery); + } + } + + /** + * Updates the remark of an existing grocery. + * + * @param details A string containing grocery new remark. + * @throws GitException is input is not valid + */ + public void editRemark(String details) throws GitException { + // Assuming the format is "remark GROCERY r/REMARK" + String[] remarkParts = checkDetails(details, "remark", "r/"); + Grocery grocery = getGrocery(remarkParts[0].strip()); + String remark = remarkParts[1].strip(); + + grocery.setRemark(remark); + if (remark.isEmpty()) { + throw new EmptyInputException("remark"); + } + GroceryUi.printRemarkSet(grocery); + storage.saveGroceryFile(getGroceries()); + } + + /** + * Updates the cost of an existing grocery. + * + * @param details A string containing grocery name and details. + * @throws GitException If the input new cost is not numeric. + */ + public void editCost(String details) throws GitException { + String[] costParts = checkDetails(details, "cost", "$"); + Grocery grocery = getGrocery(costParts[0].strip()); + String price = costParts[1].strip(); + + try { + double cost = Double.parseDouble(price); + if (cost < 0) { + throw new InvalidCostException(); + } + grocery.setCost(cost); + GroceryUi.printCostSet(grocery); + storage.saveGroceryFile(getGroceries()); + } catch (NumberFormatException e) { + throw new InvalidCostException(); + } + } + + /** + * Updates the threshold of an existing grocery. + * + * @param details A string containing grocery name and details. + * @throws GitException If the input new cost is not numeric. + */ + public void editThreshold(String details) throws GitException { + String [] amtParts = checkDetails(details, "th", "a/"); + Grocery grocery = getGrocery(amtParts[0].strip()); + String thresholdString = amtParts[1].strip(); + int threshold; + try { + threshold = Integer.parseInt(thresholdString); + } catch (NumberFormatException e) { + throw new InvalidAmountException(); + } + grocery.setThreshold(threshold); + GroceryUi.printThresholdSet(grocery); + storage.saveGroceryFile(getGroceries()); + } + + /** + * Updates the location of an existing grocery. + * + * @param details A string containing grocery name and details. + * @throws GitException Thrown if given location name is empty. + */ + public void editLocation(String details) throws GitException { + String[] locationParts = checkDetails(details, "store", "l/"); + Grocery grocery = getGrocery(locationParts[0].strip()); + String name = locationParts[1].strip(); + + Location location; + try { + location = LocationList.findLocation(name); + } catch (NoSuchObjectException e) { + LocationList.addLocation(name); + GroceryUi.printLocationAdded(name.strip()); + location = LocationList.findLocation(name); + } + + Location oldLocation = grocery.getLocation(); + if (oldLocation == location) { + throw new SameLocationException(grocery.getName(), location.getName()); + } else if (oldLocation != null) { + oldLocation.removeGrocery(grocery); + } + grocery.setLocation(location); + location.addGrocery(grocery); + GroceryUi.printLocationSet(grocery); + storage.saveGroceryFile(getGroceries()); + } + + /** + * Searches for groceries containing the given keyword. + */ + public void findGroceries(String key) throws EmptyInputException { + if (key.isEmpty()) { + throw new EmptyInputException("keyword"); + } + + List relevantGroceries = new ArrayList<>(); + for (Grocery grocery : groceries) { + if(grocery.getName().toLowerCase().contains(key.toLowerCase())) { + relevantGroceries.add(grocery); + } + } + + GroceryUi.printGroceriesFound(relevantGroceries, key); + } + + //@@author SharlynLui + /** + * Display all the details of the grocery. + */ + public void viewGrocery(String grocery) throws EmptyInputException { + if (grocery.isEmpty()) { + throw new EmptyInputException("grocery"); + } + + int exists = 0; + + for (Grocery item : groceries) { + if(item.getName().toLowerCase().equals(grocery.trim())) { + GroceryUi.printViewGrocery(item); + exists = 1; + break; + } + } + + if (exists == 0) { + GroceryUi.printGroceriesNotFound(); + } + } + //@@author SharlynLui + + /** + * Updates the rating and review of an existing grocery. + * + * @param details A string containing grocery name and details. + * @throws GitException If the input grocery is empty. + */ + public void editRatingAndReview(String details) throws GitException { + if (details.isEmpty()) { + throw new EmptyInputException("grocery"); + } + Grocery grocery = getGrocery(details); + GroceryUi.promptForRatingAndReview(grocery); + storage.saveGroceryFile(getGroceries()); + } + + /** + * Lists all the user's groceries. + */ + public void listGroceries() { + int size = groceries.size(); + if (size == 0) { + GroceryUi.printNoGrocery(); + } else { + GroceryUi.printGroceryList(groceries); + } + } + + /** + * Lists all the groceries that are low in stock. + */ + public void listLowStocks() { + List lowStockGroceries = new ArrayList<>(); + for (Grocery grocery: groceries) { + if (grocery.isLow()) { + lowStockGroceries.add(grocery); + } + } + GroceryUi.printLowStocks(lowStockGroceries); + } + + /** + * Sorts the groceries by expiration date. Groceries without an expiration date are sorted to the end. + */ + public void sortByExpiration() { + int size = groceries.size(); + if (size == 0) { + GroceryUi.printNoGrocery(); + } else { + Collections.sort(groceries, (g1, g2) -> { + LocalDate exp1 = g1.getExpiration(); + LocalDate exp2 = g2.getExpiration(); + if (exp1 == null && exp2 == null) { + // If both groceries have no expiration date, they are equal + return 0; + } + if (exp1 == null) { + // If only the first grocery has no expiration date, it is greater + return 1; + } + if (exp2 == null) { + // If only the second grocery has no expiration date, it is greater + return -1; + } + return exp1.compareTo(exp2); + }); + GroceryUi.printGroceryList(groceries); + } + } + + /** + * Gets a list of groceries expiring in the next 3 days. + * + * @return A list of groceries expiring within the next 3 days. + */ + public List getGroceriesExpiringInNext3Days() { + LocalDate today = LocalDate.now(); + LocalDate threeDaysFromNow = today.plusDays(3); + + return groceries.stream() + .filter(grocery -> { + LocalDate expirationDate = grocery.getExpiration(); + return !expirationDate.isBefore(today) && !expirationDate.isAfter(threeDaysFromNow); + }) + .collect(Collectors.toList()); + } + + /** + * display the groceries that are expiring in the next 3 days. + */ + public void displayGroceriesExpiringInNext3Days() { + List groceriesExpiringInNext3Days = getGroceriesExpiringInNext3Days(); + if (groceriesExpiringInNext3Days.isEmpty()) { + GroceryUi.printNoGrocery(); + } else { + System.out.println("Here are the groceries expiring in the next 3 days:"); + GroceryUi.printGroceryList(groceriesExpiringInNext3Days); + } + } + + /** + * Sorts the groceries by descending cost. + */ + public void sortByCost() { + int size = groceries.size(); + if (size == 0) { + GroceryUi.printNoGrocery(); + } else { + List groceriesByCost = groceries; + groceriesByCost.sort((g1, g2) -> Double.compare(g1.getCost(), g2.getCost())); + Collections.reverse(groceriesByCost); + GroceryUi.printGroceryList(groceriesByCost); + } + } + /** + * Sorts the groceries by category. + */ + public void sortByCategory(){ + int size = groceries.size(); + if (size == 0) { + GroceryUi.printNoGrocery(); + } else { + Collections.sort(groceries, Comparator.comparing(Grocery::getCategory)); + GroceryUi.printGroceryList(groceries); + } + } + /** + * Removes a grocery. + * + * @param name Grocery name from user input. + * @throws GitException If grocery is empty. + */ + public void removeGrocery(String name) throws GitException { + if (name.isEmpty()) { + throw new EmptyInputException("grocery"); + } + + // Assuming the format is "del GROCERY" + Grocery grocery = getGrocery(name); + groceries.remove(grocery); + Location location = grocery.getLocation(); + if (location != null) { + location.removeGrocery(grocery); + } + GroceryUi.printGroceryRemoved(grocery, groceries); + storage.saveGroceryFile(getGroceries()); + } +} diff --git a/src/main/java/grocery/LoggerGroceryList.java b/src/main/java/grocery/LoggerGroceryList.java new file mode 100644 index 0000000000..885bde817b --- /dev/null +++ b/src/main/java/grocery/LoggerGroceryList.java @@ -0,0 +1,37 @@ +package grocery; + +import java.io.IOException; + +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; + +/** + * Logs metadata about the GroceryList class. + */ +public class LoggerGroceryList { + /** + * Configure logger for the GroceryList class. + */ + public static void setupLogger() { + Logger loggerGL = Logger.getLogger(GroceryList.class.getName()); + + LogManager.getLogManager().reset(); + loggerGL.setLevel(Level.ALL); + + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.SEVERE); + loggerGL.addHandler(ch); + + try { + FileHandler fh = new FileHandler("GroceryList.log", true); + fh.setLevel(Level.INFO); + loggerGL.addHandler(fh); + } catch (IOException e) { + loggerGL.log(Level.SEVERE, "Logger for GroceryList class fails", e); + } + } + +} diff --git a/src/main/java/grocery/location/Location.java b/src/main/java/grocery/location/Location.java new file mode 100644 index 0000000000..e1b7d31ecf --- /dev/null +++ b/src/main/java/grocery/location/Location.java @@ -0,0 +1,65 @@ +package grocery.location; + +import git.GroceryUi; +import grocery.Grocery; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a location to store groceries. + */ +public class Location { + private String name; + private List groceries; + + /** + * Constructs Location. + */ + public Location (String name) { + this.name = name; + groceries = new ArrayList<>(); + } + /** + * Adds a grocery to this location. + */ + public void addGrocery(Grocery grocery) { + groceries.add(grocery); + } + + /** + * Removes the specified grocery from this location. + */ + public void removeGrocery(Grocery grocery) { + groceries.remove(grocery); + } + + /** + * Lists all groceries stored at this location. + */ + public void listGroceries() { + System.out.println("Viewing location: " + name); + if (groceries.isEmpty()) { + GroceryUi.printNoGrocery(); + } else { + GroceryUi.printGroceryList(groceries); + } + } + + /** + * Sets the location attribute of all stored groceries to NULL. + * Called when a location is being removed from tracking. + */ + public void clearLocation() { + for (Grocery grocery : groceries) { + grocery.setLocation(null); + } + } + + /** + * Gets name of the location. + */ + public String getName() { + return name; + } +} diff --git a/src/main/java/grocery/location/LocationList.java b/src/main/java/grocery/location/LocationList.java new file mode 100644 index 0000000000..89d6d9e77d --- /dev/null +++ b/src/main/java/grocery/location/LocationList.java @@ -0,0 +1,88 @@ +package grocery.location; + +import exceptions.DuplicateException; +import exceptions.emptyinput.EmptyInputException; +import exceptions.nosuch.NoSuchObjectException; +import git.GroceryUi; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stores all the user's saved locations. + */ +public class LocationList { + private static List locations = new ArrayList<>(); + + /** + * Adds a new location. + * + * @param name Name of location. + * @throws EmptyInputException Exception thrown if user does not input a location name. + */ + public static void addLocation(String name) throws EmptyInputException, DuplicateException { + try { + findLocation(name); + throw new DuplicateException("location", name); + } catch (NoSuchObjectException e) { + if (name == null || name.isBlank()) { + throw new EmptyInputException("location"); + } + + Location location = new Location(name.strip()); + locations.add(location); + } + } + + /** + * Removes a location from tracking. + * + * @param name Name of location. + * @throws EmptyInputException Thrown if user does not input a location name. + * @throws NoSuchObjectException Thrown if the location does not exist. + */ + public static void removeLocation(String name) throws EmptyInputException, NoSuchObjectException { + if (name == null || name.isBlank()) { + throw new EmptyInputException("location"); + } + + Location location = findLocation(name); + location.clearLocation(); + locations.remove(location); + GroceryUi.printLocationRemoved(name.strip()); + } + + /** + * Returns the desired location. + * + * @param name Name of location + * @throws NoSuchObjectException Thrown if the location does not exist. + */ + public static Location findLocation(String name) throws NoSuchObjectException { + if (locations.isEmpty()) { + throw new NoSuchObjectException("location (" + name + ")"); + } + + int index = -1; + for (Location loc : locations) { + if(loc.getName().equalsIgnoreCase(name)) { + index = locations.indexOf(loc); + break; + } + } + + if (index != -1) { + return locations.get(index); + } else { + throw new NoSuchObjectException("location (" + name + ")"); + } + } + + /** + * Lists all locations being tracked. + */ + public static void listLocations() { + GroceryUi.printLocationList(locations); + } + +} diff --git a/src/main/java/recipe/Recipe.java b/src/main/java/recipe/Recipe.java new file mode 100644 index 0000000000..0294986dbb --- /dev/null +++ b/src/main/java/recipe/Recipe.java @@ -0,0 +1,88 @@ +package recipe; + +import java.util.ArrayList; + +public class Recipe { + private String title; + private ArrayList ingredients; + + private ArrayList steps; + + /** + * Constructs a Recipe. + * + * @param title Title of the recipe. + * @param ingredients Ingredients to be stored in the recipe. + * @param steps Steps to be stored in the recipe. + */ + public Recipe(String title, ArrayList ingredients, ArrayList steps) { + this.title = title; + this.ingredients = ingredients; + this.steps = steps; + } + + public String getTitle() { + return title; + } + + /** + * Edits the existing title with new title. + */ + public void editTitle(String newTitle) { + title = newTitle; + } + + /** + * Edits the existing title with new title. + */ + public void editIngredients(ArrayList newIngredients) { + ingredients = newIngredients; + } + + /** + * Edits the existing title with new title. + */ + public void editSteps(ArrayList newSteps) { + steps = newSteps; + } + + /** + * Prints the title, ingredients and steps of the recipe. + */ + public void viewRecipe() { + System.out.println("Recipe title: " + title + "\n"); + System.out.println("Ingredients: "); + for (String currIngredient : ingredients) { + System.out.println("- " + currIngredient.trim()); + } + System.out.println("\nSteps: "); + int index = 1; + for (String currStep : steps) { + System.out.println(index + ": " + currStep.trim()); + index += 1; + } + } + /** + * Returns the title, ingredients and steps of the recipe for saving. + * + * @return String representation of the Recipe. + */ + public String toRecipeSaveFormat (){ + assert !(this.title.isEmpty()) : "Recipe does not exist!!"; + + String ingredientsString; + if (this.ingredients != null) { + ingredientsString = String.join(", ", ingredients); + } else { + ingredientsString = "null"; + } + + String stepsString; + if (this.steps != null){ + stepsString = String.join(". ", steps); + } else { + stepsString = "null"; + } + return this.title + " | " + ingredientsString + " | " + stepsString; + } +} diff --git a/src/main/java/recipe/RecipeList.java b/src/main/java/recipe/RecipeList.java new file mode 100644 index 0000000000..1611e3d694 --- /dev/null +++ b/src/main/java/recipe/RecipeList.java @@ -0,0 +1,152 @@ +package recipe; + +import exceptions.GitException; +import exceptions.emptyinput.EmptyInputException; +import exceptions.nosuch.NoSuchObjectException; +import git.RecipeUi; +import git.Storage; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class RecipeList { + private ArrayList recipeArr; + private Storage storage; + /** + * Constructs RecipeList with recipe as an empty ArrayList. + */ + public RecipeList() { + recipeArr = new ArrayList<>(); + this.storage = new Storage(); + + } + + /** + * Adds a recipe to the recipe list. + * Parser will not allow duplicated recipe. + * @param recipe Recipe to be added. + */ + public void addRecipe(Recipe recipe) { + try { + recipeArr.add(recipe); + RecipeUi.printRecipeAdded(recipe); + storage.saveRecipeFile(recipeArr); + assert recipeArr.contains(recipe) : "Grocery should be added to the list"; + } catch (NullPointerException e) { + System.out.println("Failed to add recipe: the recipe is null."); + } catch (Exception e) { + System.out.println("An unexpected error occurred while adding the recipe: " + e.getMessage()); + } + } + + /** + * Lists all the user's recipes. + */ + public void listRecipes() { + int size = recipeArr.size(); + if (size == 0) { + RecipeUi.printNoRecipe(); + } else { + RecipeUi.printRecipeList(recipeArr); + } + } + + /** + * Returns the desired recipe. + * + * @param title Title of the recipe. + * @return The specific recipe. + * @throws NoSuchObjectException If the selected grocery does not exist. + */ + public Recipe getRecipe(String title) throws NoSuchObjectException { + int index = -1; + for (Recipe recipe : recipeArr) { + if(recipe.getTitle().equalsIgnoreCase(title)) { + index = recipeArr.indexOf(recipe); + break; + } + } + + if (index != -1) { + assert recipeArr != null : "Found grocery should not be null"; + return recipeArr.get(index); + } else { + throw new NoSuchObjectException("recipe"); + } + } + + /** + * Removes a recipe. + * + * @param title Recipe title from user input. + * @throws GitException If recipe is empty. + */ + public void removeRecipe(String title) throws GitException { + if (title.isEmpty()) { + throw new EmptyInputException("recipe"); + } + + Recipe currRecipe = getRecipe(title); + recipeArr.remove(currRecipe); + RecipeUi.printRecipeRemoved(currRecipe); + storage.saveRecipeFile(recipeArr); + } + + /** + * Searches for recipes containing the given keyword. + */ + public void findRecipe(String key) throws EmptyInputException { + if (key.isEmpty()) { + throw new EmptyInputException("keyword"); + } + + List relevantRecipe = new ArrayList<>(); + for (Recipe currRecipe : recipeArr) { + if(currRecipe.getTitle().toLowerCase().contains(key.toLowerCase())) { + relevantRecipe.add(currRecipe); + } + } + + RecipeUi.printRecipesFound(relevantRecipe, key); + } + + /** + * Checks if a recipe exists. + * + * @param title Title of the recipe + * @return True if the recipe exists, false otherwise. + */ + public boolean isRecipeExists(String title) { + for (Recipe currRecipe : recipeArr) { + if (currRecipe.getTitle().equalsIgnoreCase(title)) { + return true; + } + } + return false; + } + + /** + * Updates an existing grocery. + * + * @param title The title of the recipe to be edited. + * @throws GitException is input is not valid + */ + public void editRecipe(String title, String editPart, String editLine) throws GitException { + Recipe currRecipe = getRecipe(title); + if (editPart.equalsIgnoreCase("title")) { + currRecipe.editTitle(editLine); + } else if (editPart.equalsIgnoreCase("ingredients")) { + String[] ingredientsList = editLine.split("[,]"); + ArrayList ingredientsArr = new ArrayList(Arrays.asList(ingredientsList)); + currRecipe.editIngredients(ingredientsArr); + } else if (editPart.equalsIgnoreCase("steps")) { + String[] stepsList = editLine.split("[.]"); + ArrayList stepsArr = new ArrayList(Arrays.asList(stepsList)); + currRecipe.editSteps(stepsArr); + } + System.out.println("This is the edited recipe:"); + currRecipe.viewRecipe(); + storage.saveRecipeFile(recipeArr); + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/main/java/user/UserInfo.java b/src/main/java/user/UserInfo.java new file mode 100644 index 0000000000..b7caefb5b0 --- /dev/null +++ b/src/main/java/user/UserInfo.java @@ -0,0 +1,251 @@ +package user; + +import exceptions.GitException; +import exceptions.FailToCalculateCalories; +import exceptions.InsufficientInfoException; +import food.Food; +import git.Storage; + +import java.util.List; + +public class UserInfo { + private String name; + private double weight; + private double height; + private int age; + private String gender; + private String aim; + private String activeness; + private double BMR; + private double AMR; + private int caloriesCap; + private int currentCalories; + private Storage storage; + + public UserInfo() { + this.name = null; + this.weight = 0; + this.height = 0; + this.age = 0; + this.BMR = 0; + this.AMR = 0; + this.currentCalories = 0; + this.storage = new Storage(); + } + + public void setName(String name) { + this.name = name; + } + + /** + * Sets weight in KG. + * + * @param weight User's weight. + */ + public void setWeight(double weight) { + assert weight >= 0 : "User should not be allowed to input negative weight"; + this.weight = weight; + } + + /** + * Sets height in cm. + * + * @param height User's height. + */ + public void setHeight(double height) { + assert height >= 0 : "User should not be allowed to input negative height"; + this.height = height; + } + + /** + * Sets age in years. + * + * @param age User's age. + */ + public void setAge(int age) { + assert age >= 0: "User should not be allowed to input negative age"; + this.age = age; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public void setAim(String aim) { + this.aim = aim; + } + + public void setActiveness(String activeness) { + this.activeness = activeness; + } + + public double getCurrentCalories() { + return currentCalories; + } + + /** + * Updates the user's information using the given input. + * + * @param name Entered name. + * @param weight Entered weight. + * @param height Entered height. + * @param age Entered age. + * @param gender Entered gender. + * @param activeness Entered activeness. + * @param aim Entered aim. + */ + public void updateInfo(String name, double weight, double height, int age, + String gender, String activeness, String aim) { + setName(name); + setWeight(weight); + setHeight(height); + setAge(age); + setGender(gender.toLowerCase()); + setAim(aim.toLowerCase()); + setActiveness(activeness.toLowerCase()); + try { + calBMR(); + calAMR(); + setCaloriesCap(); + System.out.println("Your target calories intake a day should be " + + this.caloriesCap); + } catch (GitException e) { + System.out.println(e.getMessage()); + } + + } + + /** + * Calculate's the user's Basal metabolic rate if there is sufficient information. + * + * @throws InsufficientInfoException When there is not enough information for calculation. + */ + public void calBMR() throws InsufficientInfoException { + if (this.weight == 0 || this.height == 0 || this.age == 0) { + throw new InsufficientInfoException(); + } + double result; + if(gender.equalsIgnoreCase("F")) { + result = 655 + (9.56 * this.weight) + (1.85 * this.height) - (4.68 * this.age); + } else { + result = 66.47 + (13.75 * this.weight) + (5 * this.height) - (6.76 * this.age); + } + this.BMR = result; + } + + /** + * Calculate's the user's Active Metabolic Rate given the activeness. + * + * @throws FailToCalculateCalories When invalid activeness was given. + */ + public void calAMR() throws FailToCalculateCalories { + switch (this.activeness) { + case "inactive": + this.AMR = this.BMR * 1.2; + break; + case "light": + this.AMR = this.BMR * 1.38; + break; + case "moderate": + this.AMR = this.BMR * 1.55; + break; + case "active": + this.AMR = this.BMR * 1.73; + break; + case "very": + this.AMR = this.BMR * 1.9; + break; + default: + throw new FailToCalculateCalories(); + } + } + + /** + * Calculate's the user's target calories given the aim. + * + * @throws FailToCalculateCalories When invalid aim was given. + */ + public void setCaloriesCap() throws FailToCalculateCalories { + switch (this.aim) { + case "lose": + this.caloriesCap = (int)(this.AMR*0.8); + break; + case "maintain": + this.caloriesCap = (int)(AMR); + break; + case "gain": + this.caloriesCap = (int)(this.AMR*1.2); + break; + default: + throw new FailToCalculateCalories(); + } + } + + /** + * Calculates the total calories consumed. + * Only check if it has exceeded the target calories if sufficient information was given. + * + * @param foods The list of consumed food. + * @throws InsufficientInfoException When insufficient information about the user was given. + */ + public void consumptionOfCalories(List foods) throws InsufficientInfoException{ + assert !(foods.isEmpty()) : "Food should be added into list before storing consumed calories"; + this.currentCalories = 0; + for (Food food : foods) { + this.currentCalories = (int)(food.getCalories() + this.currentCalories); + } + if (this.weight == 0 || this.height == 0 || this.age == 0 || + this.gender.isEmpty() || this.aim.isEmpty() || this.activeness.isEmpty()) { + throw new InsufficientInfoException(); + } + if (this.currentCalories > this.caloriesCap) { + System.out.println("You have exceeded your calories intake!"); + System.out.println("You have consumed " + currentCalories + "kcal"); + System.out.println("when your target is " + caloriesCap + "kcal"); + } + } + + /** + * Stores user details as a string. + * + * @return A string containing all the user's details. + */ + public String viewProfile(){ + String userName = "Name: " + this.name + "\n"; + String height = "Height: " + this.height + "\n"; + String weight = "Weight: " + this.weight + "\n"; + String age = "Age: " + this.age + "\n"; + String gender = "Gender: " + this.gender + "\n"; + String target = "Target calories intake: " + this.caloriesCap; + return userName + height + weight + age + gender + target; + } + + /** + * Stores user details as a string in format for saving. + * + * @return A string containing all the user's details. + */ + public String toProfileSaveFormat(){ + assert !(this.name.isEmpty()) : "User does not exist!!"; + String userName = "Name: " + this.name + "\n"; + String height = "Height: " + this.height + "\n"; + String weight = "Weight: " + this.weight + "\n"; + String age = "Age: " + this.age + "\n"; + String gender = "Gender: " + this.gender + "\n"; + String aim = "Aim: " + this.aim + "\n"; + String activeness = "Activeness: " + this.activeness + "\n"; + String caloriesCap = "Calories: " + this.caloriesCap + "\n"; + return userName + height + weight + age + gender + aim + activeness + caloriesCap; + } + /** + * Sets calories cap. + * + * @param caloriesCap Loaded from saved file. + */ + public void setCaloriesCapFromLoad(int caloriesCap){ + this.caloriesCap = caloriesCap; + } + public String getName(){ + return this.name; + } +} diff --git a/src/test/java/food/FoodListTest.java b/src/test/java/food/FoodListTest.java new file mode 100644 index 0000000000..330ac60446 --- /dev/null +++ b/src/test/java/food/FoodListTest.java @@ -0,0 +1,15 @@ +package food; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FoodListTest { + @Test + void addFood_success() { + FoodList fl= new FoodList(); + Food food = new Food("apple", 52); + fl.addFood(food); + assertTrue(fl.getFoods().contains(food), "Food should be added to the list."); + } +} diff --git a/src/test/java/food/FoodTest.java b/src/test/java/food/FoodTest.java new file mode 100644 index 0000000000..0fa50963c8 --- /dev/null +++ b/src/test/java/food/FoodTest.java @@ -0,0 +1,22 @@ +package food; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FoodTest { + + @Test + public void print_success() { + Food food = new Food("apple", 52); + assertEquals("apple, with 52.0 calories", food.print()); + } + + @Test + public void print_foodHasInvalidName_throwsAssertionError() { + Food food = new Food("", 52); + assertThrows(AssertionError.class, food::print, + "Should throw AssertionError for negative cost."); + } +} diff --git a/src/test/java/git/GitTest.java b/src/test/java/git/GitTest.java new file mode 100644 index 0000000000..3d15a7f0e4 --- /dev/null +++ b/src/test/java/git/GitTest.java @@ -0,0 +1,21 @@ +package git; + +import exceptions.GitException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class GitTest { + @Test + public void executeCommand_invalidCommand_success() { + try { + Ui ui = Ui.getInstance(); + Parser parser = new Parser(ui); + String[] commandParts = {"nonsense", ""}; + parser.executeCommand(commandParts, "grocery"); + } catch (GitException e) { + assertEquals("Unknown command. Type 'help' for a list of commands.", e.getMessage());; + } + } +} diff --git a/src/test/java/git/StorageTest.java b/src/test/java/git/StorageTest.java new file mode 100644 index 0000000000..2770ed3ba1 --- /dev/null +++ b/src/test/java/git/StorageTest.java @@ -0,0 +1,54 @@ +package git; + +import exceptions.GitException; +import grocery.Grocery; +import grocery.location.Location; +import grocery.location.LocationList; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +public class StorageTest { + @Test + public void parseGrocery_success() { + String groceryString = "Meat | 0 | null | 2024-04-14 | Meat | 0.00 | bottom freezer "; + Storage storage = new Storage(); + Grocery grocery = storage.parseGrocery(groceryString); + String expectedGrocery = "Meat (MEAT), amount: 0 grams, expiration: 2024-04-14, " + + "cost: $0.00, location: bottom freezer"; + assertEquals(expectedGrocery, grocery.printGrocery()); + } + + @Test + public void parseGrocery_invalidFormat_returnsNull() { + String groceryString = "Meat | 0 | null | 2024 | bottom freezer "; + Storage storage = new Storage(); + Grocery grocery = storage.parseGrocery(groceryString); + assertNull(grocery); + } + + @Test + public void parseGroceryLocation_success() { + String locString = "burger stand"; + try { + LocationList.addLocation(locString); + Location expectedLocation = LocationList.findLocation(locString); + + Storage storage = new Storage(); + Location actualLocation = storage.parseGroceryLocation(locString); + assertEquals(expectedLocation.getName(), actualLocation.getName()); + } catch (GitException ignore) { + fail("parseGroceryLocation should not fail."); + } + } + + @Test + public void parseGroceryLocation_noLocation_returnsNull() { + String locString = "null"; + Storage storage = new Storage(); + Location location = storage.parseGroceryLocation(locString); + assertNull(location); + } +} diff --git a/src/test/java/grocery/GroceryListTest.java b/src/test/java/grocery/GroceryListTest.java new file mode 100644 index 0000000000..969ba33693 --- /dev/null +++ b/src/test/java/grocery/GroceryListTest.java @@ -0,0 +1,296 @@ +package grocery; + +import exceptions.SameLocationException; +import exceptions.commands.CommandWrongFormatException; +import exceptions.CannotUseException; +import exceptions.GitException; + +import exceptions.emptyinput.EmptyInputException; +import exceptions.invalidinput.InvalidAmountException; +import exceptions.nosuch.NoSuchObjectException; +import grocery.location.Location; +import grocery.location.LocationList; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class GroceryListTest { + @Test + public void addGrocery_success() { + GroceryList gl = new GroceryList(); + Grocery grocery = new Grocery("Apples", 10, 5, LocalDate.of(2024, 12, 31), "Fruit", 2.99, null); + gl.addGrocery(grocery); + assertTrue(gl.getGroceries().contains(grocery), "Grocery should be added to the list."); + } + + @Test + public void addGrocery_throwNULL_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery(null)); + fail("Expected IllegalArgumentException was not thrown."); + } catch (NullPointerException e) { + assertNull(e.getMessage()); + } + } + + @Test + public void isGroceryExists_true() { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Bananas")); + assertTrue(gl.isGroceryExists("Bananas"), "Grocery should exist in the list."); + } + + @Test + public void isGroceryExists_false() { + GroceryList gl = new GroceryList(); + assertFalse(gl.isGroceryExists("Bananas"), "Grocery should not exist in the list."); + } + + @Test + public void editExpiration_success() { + GroceryList gl = new GroceryList(); + try { + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(),"Meat", 0, new Location("Freezer"))); + gl.editExpiration("Meat d/2024-07-19"); + } catch (GitException e) { + fail("editExpiration should not throw an exception"); + } + } + + @Test + public void listGroceries_emptyList() { + GroceryList gl = new GroceryList(); + gl.listGroceries(); + assertTrue(gl.getGroceries().isEmpty(), "There should be no groceries to list."); + } + + @Test + public void listGroceries_containsItems() { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Potatoes", 50, 20, LocalDate.of(2024, 11, 30), "Vegetable", 0.25, null)); + gl.listGroceries(); + assertFalse(gl.getGroceries().isEmpty(), "Grocery list should contain items."); + } + + @Test + public void editExpiration_noSuchGrocery_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.editExpiration("nonexistentGrocery d/2024-07-19"); + fail("Expected NoSuchGroceryException not thrown"); + } catch (NoSuchObjectException e) { + assertEquals("The grocery (nonexistentGrocery) does not exist!", e.getMessage()); + } catch (GitException e) { + fail("Expected NoSuchGroceryException, but another GitException was thrown"); + } + } + + @Test + public void editExpiration_wrongFormat_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0, new Location("Freezer"))); + gl.editExpiration("Meat d/2024-07-19"); + } catch (GitException e) { + String message = "Command is in the wrong format, type \"help\" for more information." + + System.lineSeparator() + + "exp needs 'd/'"; + assertEquals(message, e.getMessage()); + } + } + + @Test + public void editCategory_nonExistingGrocery_throwsException() { + GroceryList gl = new GroceryList(); + Exception exception = assertThrows(NoSuchObjectException.class, () -> gl.editCategory("Milk c/Beverage")); + assertTrue(exception.getMessage().contains("does not exist"), + "Expected NoSuchObjectException for non-existing grocery."); + } + + + @Test + public void removeGrocery_groceryDelete_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("fooood", 0, 0, null, "Meat", 0,new Location("Freezer"))); + gl.removeGrocery("food"); + fail("Expected NoSuchGroceryException not thrown"); + } catch (GitException e) { + // NoSuchGroceryException + assertEquals("The grocery (food) does not exist!", e.getMessage()); + } + } + + @Test + public void editAmount_success() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0, new Location("Freezer"))); + gl.editAmount("Meat a/5", false); + assertEquals(gl.getGrocery("Meat").getAmount(), 5); + } catch (GitException e) { + fail("Test should not fail."); + } + } + + @Test + public void editAmount_noSuchGrocery_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.editAmount("Meat", false); + } catch (NoSuchObjectException e) { + String expectedMessage = "The grocery (Meat) does not exist!"; + assertEquals(expectedMessage.trim(), e.getMessage().trim()); + } catch (GitException e) { + fail("Expected a NoSuchObjectException, but another GitException was thrown"); + } + } + + @Test + public void editAmount_wrongFormat_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0, new Location("Freezer"))); + gl.editAmount("Meat", false); + fail("Expected a WrongFormatException to be thrown"); + } catch (CommandWrongFormatException e) { + String expectedMessage = "Command is in the wrong format, type \"help\" for more information." + + System.lineSeparator() + + "amt needs 'a/'"; + assertEquals(expectedMessage.trim(), e.getMessage().trim()); + } catch (GitException e) { + fail("Expected a WrongFormatException, but another GitException was thrown"); + } + } + + @Test + public void editAmount_negativeInteger_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0, new Location("Freezer"))); + gl.editAmount("Meat a/-5", false); + } catch (InvalidAmountException e) { + String expectedMessage = "Please input a valid integer that is greater than 0!"; + assertEquals(expectedMessage.trim(), e.getMessage().trim()); + } catch (GitException e) { + fail("Expected a InvalidAmountException, but another GitException was thrown"); + } + } + + @Test + public void editAmountUseTrue_amountReaches0_success() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Meat", 5, 0, LocalDate.now(), "Meat", 0, new Location("Freezer"))); + gl.editAmount("Meat a/5", true); + } catch (GitException e) { + fail("editAmount_useTrue should not throw an exception"); + } + } + + + @Test + public void editAmountUseTrue_noAmountCannotUse_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0,new Location("Freezer"))); + gl.editAmount("Meat a/5", true); + fail("Expected a CannotUseException to be thrown"); + } catch (CannotUseException e) { + String expectedMessage = "The grocery you want to use is already out of stock - time to replenish!"; + assertEquals(expectedMessage, e.getMessage()); + } catch (GitException e) { + fail("Expected a CannotUseException, but another GitException was thrown"); + } + } + + @Test + public void editLocation_noSuchLocation_success() { + try { + GroceryList gl = new GroceryList(); + LocationList.addLocation("Cold part"); + Location location = LocationList.findLocation("Cold part"); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0, location)); + gl.editLocation("Meat l/bottom freezer"); + String finalLocation = gl.getGrocery("Meat").getLocation().getName(); + assertEquals(finalLocation, "bottom freezer"); + } catch (GitException ignore) { + fail("Location should be automatically created, so no exception thrown."); + } + } + + @Test + public void editLocation_sameLocation_exceptionThrown() { + try { + GroceryList gl = new GroceryList(); + LocationList.addLocation("Freezer"); + Location location = LocationList.findLocation("Freezer"); + gl.addGrocery(new Grocery("Meat", 0, 0, LocalDate.now(), "Meat", 0, location)); + gl.editLocation("Meat l/freezer"); + } catch (SameLocationException e) { + String expectedMessage = "Meat is already stored in Freezer."; + assertEquals(expectedMessage, e.getMessage()); + } catch (GitException ignore) { + fail("Should throw SameLocationException."); + } + } + + @Test + public void testSortByExpiration() { + // Create a grocery list instance + GroceryList gl = new GroceryList(); + + // Create and add groceries with various expiration dates + Grocery grocery1 = new Grocery("Milk", 0, 0, LocalDate.parse("2024-04-10"), "dairy", 0, null); + Grocery grocery2 = new Grocery("Bread", 0, 0, LocalDate.parse("2024-04-20"), "baked", 0, null); + Grocery grocery3 = new Grocery("Eggs"); // No expiration date set + + gl.addGrocery(grocery1); + gl.addGrocery(grocery2); + gl.addGrocery(grocery3); + + // Sort the groceries by expiration + gl.sortByExpiration(); + + // Get the sorted list of groceries + List sortedGroceries = gl.getGroceries(); + + // Assertions to check if the groceries are sorted correctly + assertEquals(grocery1, sortedGroceries.get(0), "Milk should be first as it expires first."); + assertEquals(grocery2, sortedGroceries.get(1), "Bread should be second as it expires next."); + assertEquals(grocery3, sortedGroceries.get(2), "Eggs should be last as it has no expiration date."); + } + + @Test + public void findGroceries_emptyInput_exceptionThrown() { + GroceryList gl = new GroceryList(); + assertThrows(EmptyInputException.class, () -> gl.findGroceries("")); + } + + @Test + public void removeGrocery_success() throws GitException { + GroceryList gl = new GroceryList(); + Grocery grocery = new Grocery("Oranges", 20, 10, LocalDate.of(2024, 12, 31), "Fruit", 0.99, null); + gl.addGrocery(grocery); + gl.removeGrocery("Oranges"); + assertFalse(gl.getGroceries().contains(grocery), "Grocery should be removed from the list."); + } + + @Test + public void removeGrocery_nonExisting_throwsException() { + GroceryList gl = new GroceryList(); + assertThrows(NoSuchObjectException.class, () -> gl.removeGrocery("Oranges"), + "Should throw an exception for non-existing grocery."); + } + +} diff --git a/src/test/java/grocery/GroceryTest.java b/src/test/java/grocery/GroceryTest.java new file mode 100644 index 0000000000..fa1c83ea70 --- /dev/null +++ b/src/test/java/grocery/GroceryTest.java @@ -0,0 +1,126 @@ +package grocery; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import grocery.location.Location; +import org.junit.jupiter.api.Test; + +import exceptions.PastExpirationDateException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GroceryTest { + @Test + public void setExpiration_validDate() throws PastExpirationDateException { + Grocery grocery = new Grocery("Milk"); + grocery.setExpiration("2024-12-31"); + assertEquals(LocalDate.parse("2024-12-31"), grocery.getExpiration(), + "Expiration date should be set correctly."); + } + + @Test + public void setExpiration_pastDate_throwsException() { + Grocery grocery = new Grocery("Milk"); + // Adjust the exception type to match the actual implementation + assertThrows(PastExpirationDateException.class, () -> grocery.setExpiration("2020-01-01"), + "Should throw PastExpirationDateException for past dates."); + } + + @Test + public void setAmount_positiveAmount() { + Grocery grocery = new Grocery("Water", 0, 0, LocalDate.parse("2024-12-31"), "Beverage", 1.00, null); + grocery.setAmount(10); + assertEquals(10, grocery.getAmount(), "Amount should be set correctly."); + } + + @Test + public void setAmount_negativeAmount_throwsAssertionError() { + Grocery grocery = new Grocery("Water"); + assertThrows(AssertionError.class, () -> grocery.setAmount(-5), + "Should throw AssertionError for negative amount."); + } + + @Test + public void setCost_positiveValue() { + Grocery grocery = new Grocery("Butter", 5, 3, LocalDate.now(), "Dairy", 0, null); + grocery.setCost(3.99); + assertEquals(3.99, grocery.getCost(), "Cost should be set correctly."); + } + + @Test + public void setCost_negativeValue_throwsAssertionError() { + Grocery grocery = new Grocery("Butter"); + assertThrows(AssertionError.class, () -> grocery.setCost(-1.00), + "Should throw AssertionError for negative cost."); + } + + @Test + public void isLow_belowThreshold_true() { + Grocery grocery = new Grocery("Eggs", 1, 5, LocalDate.parse("2024-12-31"), "Poultry", 1.50, null); + assertTrue(grocery.isLow(), "Should return true as the amount is below the threshold."); + } + + @Test + public void isLow_aboveThreshold_false() { + Grocery grocery = new Grocery("Eggs", 10, 5, LocalDate.parse("2024-12-31"), "Poultry", 1.50, null); + assertFalse(grocery.isLow(), "Should return false as the amount is above the threshold."); + } + + @Test + public void printGrocery_noAmountNoExpiration_leaveEmpty() { + Grocery grocery = new Grocery("apple", 0, 0, null, "fruit", 0, new Location("Pantry")); + String message = "apple (fruit), amount: 0 , " + + "cost: $0.00, location: Pantry"; + assertEquals(message, grocery.printGrocery()); + } + + @Test + public void printGrocery_costWrongFormat_formattedCost() { + Grocery grocery = new Grocery("chicken", 1, 0, LocalDate.now().plusDays(1), "meat",1,new Location("Pantry")); + String message = "chicken (meat)" + ", amount: 1 " + ", expiration: " + + LocalDate.now().plusDays(1) + ", cost: $1.00, location: Pantry"; + assertEquals(message, grocery.printGrocery()); + } + + @Test + public void printGrocery_correctAmtAndExpAndCost() { + Grocery grocery = new Grocery("chicken", 1, 0, LocalDate.now().plusDays(1), "meat",1.20,new Location("Pantry")); + String message = "chicken (meat)" + ", amount: 1 " + ", expiration: " + + LocalDate.now().plusDays(1) + ", cost: $1.20, location: Pantry"; + assertEquals(message, grocery.printGrocery()); + } + + @Test + public void toSaveFormat_success() { + Grocery grocery = new Grocery("chicken", 1, 0, LocalDate.now().plusDays(1), "meat",1.20,new Location("Pantry")); + String formattedGrocery = grocery.toSaveFormat(); + String expectedFormat = "chicken | 1 | null | " + LocalDate.now().plusDays(1) + " | meat | 1.20 | Pantry "; + assertEquals(expectedFormat, formattedGrocery); + } + + + @Test + public void toSaveFormat_noAmountOrCost_success() { + Grocery grocery = new Grocery("burger"); + String formattedGrocery = grocery.toSaveFormat(); + String expectedFormat = "burger | null | null | null | | null | null "; + assertEquals(expectedFormat, formattedGrocery); + } + + @Test + public void setExpirationOnLoad_success() { + Grocery grocery = new Grocery("airplane food"); + String expString = "2024-10-11"; + grocery.setExpirationOnLoad(expString); + + LocalDate actualDate = grocery.getExpiration(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String actualDateString = actualDate.format(formatter); + String expectedDateString = "2024-10-11"; + assertEquals(expectedDateString, actualDateString); + } +} diff --git a/src/test/java/grocery/location/LocationListTest.java b/src/test/java/grocery/location/LocationListTest.java new file mode 100644 index 0000000000..5d50f36f57 --- /dev/null +++ b/src/test/java/grocery/location/LocationListTest.java @@ -0,0 +1,52 @@ +package grocery.location; + +import exceptions.DuplicateException; +import exceptions.GitException; +import exceptions.emptyinput.EmptyInputException; +import exceptions.nosuch.NoSuchObjectException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class LocationListTest { + @Test + public void addLocation_findLocation_success() { + try { + LocationList.addLocation("back of freezer"); + LocationList.findLocation("back of freezer"); + } catch (GitException e) { + fail("findLocation should be successful."); + } + } + + @Test + public void addLocation_emptyInput_exceptionThrown() { + assertThrows(EmptyInputException.class, () -> LocationList.addLocation("")); + } + + @Test + public void addLocation_duplicate_exceptionThrown() { + assertThrows(DuplicateException.class, () -> LocationList.addLocation("BACK of freezer")); + } + + @Test + public void findLocation_noSuchLocation_exceptionThrown() { + assertThrows(NoSuchObjectException.class, () -> LocationList.findLocation("Nuclear reactor")); + } + + @Test + public void removeLocation_success() { + try { + LocationList.addLocation("front of freezer"); + LocationList.removeLocation("front of freezer"); + } catch (GitException e) { + fail("removeLocation should be successful."); + } + } + + @Test + public void removeLocation_noSuchLocation_exceptionThrown() { + assertThrows(NoSuchObjectException.class, () -> LocationList.removeLocation("cubby")); + } +} diff --git a/src/test/java/recipe/RecipeListTest.java b/src/test/java/recipe/RecipeListTest.java new file mode 100644 index 0000000000..f82020f927 --- /dev/null +++ b/src/test/java/recipe/RecipeListTest.java @@ -0,0 +1,23 @@ +package recipe; + +import exceptions.nosuch.NoSuchObjectException; +import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecipeListTest { + @Test + public void addRecipe_success() throws NoSuchObjectException { + String title = "Egg"; + ArrayList ingredients = new ArrayList(); + ingredients.add("egg"); + ingredients.add("salt"); + ArrayList steps = new ArrayList(); + steps.add("Fry." ); + steps.add("Serve."); + Recipe recipe = new Recipe(title, ingredients, steps); + RecipeList recipeArr = new RecipeList(); + recipeArr.addRecipe(recipe); + assertEquals(recipe, recipeArr.getRecipe("Egg")); + } +} diff --git a/src/test/java/recipe/RecipeTest.java b/src/test/java/recipe/RecipeTest.java new file mode 100644 index 0000000000..6199444c67 --- /dev/null +++ b/src/test/java/recipe/RecipeTest.java @@ -0,0 +1,34 @@ +package recipe; + +import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecipeTest { + @Test + public void getTitle_validDetails() { + String title = "Egg"; + ArrayList ingredients = new ArrayList(); + ingredients.add("egg"); + ingredients.add("salt"); + ArrayList steps = new ArrayList(); + steps.add("Fry." ); + steps.add("Serve."); + Recipe recipe = new Recipe(title, ingredients, steps); + assertEquals("Egg", recipe.getTitle()); + } + + @Test + public void editTitle_validDetails() { + String title = "Egg"; + ArrayList ingredients = new ArrayList(); + ingredients.add("egg"); + ingredients.add("salt"); + ArrayList steps = new ArrayList(); + steps.add("Fry." ); + steps.add("Serve."); + Recipe recipe = new Recipe(title, ingredients, steps); + recipe.editTitle("Not egg"); + assertEquals("Not egg", recipe.getTitle()); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} diff --git a/src/test/java/user/UserInfoTest.java b/src/test/java/user/UserInfoTest.java new file mode 100644 index 0000000000..77fc771209 --- /dev/null +++ b/src/test/java/user/UserInfoTest.java @@ -0,0 +1,280 @@ +package user; + +import exceptions.FailToCalculateCalories; +import exceptions.InsufficientInfoException; +import food.Food; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class UserInfoTest { + + @Test + void setName_success() { + UserInfo user = new UserInfo(); + user.setName("Tom"); + assertEquals(user.getName(), "Tom"); + } + + @Test + void setWeight_negativeWeight_throwsAssertionError() { + UserInfo user = new UserInfo(); + assertThrows(AssertionError.class, () ->user.setWeight(-50), + "Should throw AssertionError for negative weight."); + } + + @Test + void setHeight_negativeHeight_throwsAssertionError() { + UserInfo user = new UserInfo(); + assertThrows(AssertionError.class, () ->user.setHeight(-1), + "Should throw AssertionError for negative height."); + } + + @Test + void setAge_negativeAge_throwsAssertionError() { + UserInfo user = new UserInfo(); + assertThrows(AssertionError.class, () ->user.setAge(-1), + "Should throw AssertionError for negative age."); + } + + @Test + void calBMR_heightIsZero_exceptionThrown(){ + try { + UserInfo user = new UserInfo(); + user.setHeight(0); + user.calBMR(); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void calBMR_weightIsZero_exceptionThrown(){ + try { + UserInfo user = new UserInfo(); + user.setWeight(0); + user.calBMR(); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void calBMR_ageIsZero_exceptionThrown(){ + try { + UserInfo user = new UserInfo(); + user.setAge(0); + user.calBMR(); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void calBMR_validInput_success(){ + try { + UserInfo user = new UserInfo(); + user.setWeight(50); + user.setHeight(165); + user.setAge(21); + user.setGender("F"); + user.calBMR(); + } catch (InsufficientInfoException e) { + fail("Exception should not be thrown"); + } + } + + @Test + void calAMR_invalidActiveness_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + user.setActiveness("no"); + user.calAMR(); + fail("Expected a FailToCalculateCalories to be thrown"); + } catch (FailToCalculateCalories e) { + String expectedMessage = "Failed to calculate target calories. \n" + + "Please check if sufficient information has been given." ; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void calAMR_validActiveness_success() { + try { + UserInfo user = new UserInfo(); + user.setActiveness("active"); + user.calAMR(); + } catch (FailToCalculateCalories e) { + fail("Exception should not be thrown"); + } + } + + @Test + void setCaloriesCap_invalidAim_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + user.setAim("nil"); + user.setCaloriesCap(); + fail("Expected a FailToCalculateCalories to be thrown"); + } catch (FailToCalculateCalories e) { + String expectedMessage = "Failed to calculate target calories. \n" + + "Please check if sufficient information has been given." ; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void setCaloriesCap_validAim_success() { + try { + UserInfo user = new UserInfo(); + user.setAim("maintain"); + user.setCaloriesCap(); + } catch (FailToCalculateCalories e) { + fail("Exception should not be thrown"); + } + } + + @Test + void consumptionOfCalories_emptyFoodList_throwsAssertionError() { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + assertThrows(AssertionError.class, () ->user.consumptionOfCalories(foods), + "Should throw AssertionError for empty food list."); + } + + @Test + void consumptionOfCalories_weightIsZero_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 0, 165, 22, "f" , "active", "lose"); + user.consumptionOfCalories(foods); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void consumptionOfCalories_heightIsZero_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 50, 0, 22, "f" , "active", "lose"); + user.consumptionOfCalories(foods); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void consumptionOfCalories_ageIsZero_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 50, 165, 0, "f" , "active", "lose"); + user.consumptionOfCalories(foods); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void consumptionOfCalories_genderIsEmpty_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 50, 165, 21, "" , "active", "lose"); + user.consumptionOfCalories(foods); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void consumptionOfCalories_activenessIsEmpty_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 50, 165, 21, "f" , "", "lose"); + user.consumptionOfCalories(foods); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void consumptionOfCalories_aimIsEmpty_exceptionThrown() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 50, 165, 21, "f" , "active", ""); + user.consumptionOfCalories(foods); + fail("Expected a InsufficientInfoException to be thrown"); + } catch (InsufficientInfoException e) { + String expectedMessage = "User's information is insufficient to calculate BMR," + + " please check the current information"; + assertEquals(expectedMessage, e.getMessage()); + } + } + + @Test + void consumptionOfCalories_validDetails_success() { + try { + UserInfo user = new UserInfo(); + List foods = new ArrayList<>(); + foods.add(new Food("apple", 52)); + user.updateInfo("Alice", 50, 165, 21, "f" , "active", "lose"); + user.consumptionOfCalories(foods); + } catch (InsufficientInfoException e) { + fail("Exception should not be thrown"); + } + } + + @Test + void viewProfile() { + UserInfo user = new UserInfo(); + user.updateInfo("Alice", 50, 165, 21, "f" , "active", "lose"); + String details = "Name: Alice\n" + + "Height: 165.0\n" + +"Weight: 50.0\n" + +"Age: 21\n" + +"Gender: f\n" + +"Target calories intake: 1854"; + assertEquals(user.viewProfile(), details); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..3695aa4d43 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,33 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + ______ _ _________ + .' ___ | (_)| _ _ | +/ .' \_| __ |_/ | | \_| +| | ____[ | | | +\ `.___] || | _| |_ + `._____.'[___] |_____| +Hello from GiT What is your name? -Hello James Gosling +- - - - - +Hello James Gosling! +- - - - - +======================================================================== +Welcome to GiT - Grocery in Time! +GiT is your reliable assistant for managing your grocery inventory efficiently. + +Keep track of your grocery items, monitor expiration dates, and never run out of your essentials again. +We are here to help you manage your groceries better, save time, and reduce waste. + +Type 'help' anytime you need assistance with commands. +Thank you for choosing GiT - Your groceries organized, on time, every time! +======================================================================== +Here are some ways you can use our app! +grocery: manages your groceries. +calories: manages your calories intake. +profile: manages your profile. +recipe: manages your recipe. +exit: exits the program. + +What mode would you like to enter? +Please select a mode: grocery, profile, calories or recipe: +bye bye! +- - - - - diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..a998c92f2b 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,3 @@ -James Gosling \ No newline at end of file +James Gosling +exit +exit \ No newline at end of file