diff --git a/.gitignore b/.gitignore index 2873e189e1..3f105975d5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,4 @@ src/main/resources/docs/ *.iml bin/ -/text-ui-test/ACTUAL.TXT -text-ui-test/EXPECTED-UNIX.TXT + 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/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..b4c8b7e66b --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: seedu.healthmate.HealthMate + diff --git a/README.md b/README.md index e243ece764..2c0fa6c781 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Duke project template +# HealthMate 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. diff --git a/build.gradle b/build.gradle index ea82051fab..cf15a94ff8 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.healthmate.HealthMate") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("healthmate") archiveClassifier.set("") } @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/data/meal_entries.csv b/data/meal_entries.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/meal_options.csv b/data/meal_options.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/user_data.csv b/data/user_data.csv new file mode 100644 index 0000000000..fd59a3532f --- /dev/null +++ b/data/user_data.csv @@ -0,0 +1 @@ +180.0,80.0,true,20,BULKING,2674.0,2024-11-11 21:46:53,true diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..def4be6220 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 +--------|:---------------:|:-------------------------------------:|:---------: +![](https://via.placeholder.com/100.png?text=Photo) | Ryan Tan | [Github](https://github.com/ryan-txn) | [Portfolio](https://github.com/ryan-txn) +![](https://via.placeholder.com/100.png?text=Photo) | Jonas Seet | [Github](https://github.com/dri-water) | [Portfolio](https://github.com/dri-water) +![](https://via.placeholder.com/100.png?text=Photo) | Ryan Leong | [Github](https://github.com/ryryry-3302) | [Portfolio](https://github.com/ryryry-3302) +![](https://via.placeholder.com/100.png?text=Photo) | Kenneth Styppa | [Github](https://github.com/kennethSty) | [Portfolio](https://github.com/kennethSty) +![](https://via.placeholder.com/100.png?text=Photo) | Deepan Krishnaa | [Github](https://github.com/DarkDragoon2002) | [Portfolio](https://github.com/DarkDragoon2002) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..417abca6f7 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,372 @@ # Developer Guide ## Acknowledgements +ChatParser structure inspired by: +[this repository](https://github.com/kennethSty/ip). -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +Calorie consumption bar inspired by +[this blogpost](https://medium.com/javarevisited/how-to-display-progressbar-on-the-standard-console-using-java-18f01d52b30e). ## Design & implementation +The overall project is structured into six packages: +- Command: Classes used for abstracting user commands +- Core: Core classes +- Exceptions: Customized exceptions +- Recommender: TBD +- Services: Key logic and services that make use of core classes +- Utils: Helper functions -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### High Level Class Design +The main classes of this implementation are: +- HealthMate +- ChatParser +- User +- MealList +- MealEntriesList +- Meal +- MealEntry +- HistoryTracker +- UI +In each class we focused on maintaining a tight abstraction barrier between classes. +This specifically includes adherance to the "Tell Don't Ask" principle which was enforced by +making most attributes of all classes above private and avoiding getter methods if possible. +The following diagram illustrates the resulting associations, methods and attributes. +For the sake of clarity, classes which are not central corner stones for a high-level overview are ommitted. + +![High Level CD](images/highLevelClassDiagram.jpg) + +#### HealthMate +Entry point to the application is the main function of HealthMate. +The HealthMate class contains a private ChatParser attribute. This attribute's run function initiates, +after an initial greeting to the user, the interaction process. In this process, +the user enters commands with additional information into his command line application. +The content of these commands is parsed by the ChatParser. + +#### ChatParser +The ChatParser class, instantiated once per application run, manages HealthMates overal usage +flow through its main run() method. + +It has two primary attributes: +- A `MealEntriesList` object called `mealEntries` + - Contains tracked calorie consumption +- A `MealList` object called `mealOptions` + - Contains meals that are presaved by the user for quick selection to track commonly consumed meals + - the command `meal menu` is used to display the current mealOptions. The implementation of this command is shown in the UML diagram below. +![Meal Menu SD](images/mealMenuSD.png) + +These objects represent the application's underlying data with which the user interacts through the command line. +To ensure no unintended changes are done, the ChatParser class orchestrates the effects of the users prompts. +For saving these changes the ChatParser class makes use the `HistoryTracker` which facilitates the process of +storing (and loading) User data, mealEntries data and mealOptions data to their corresponding files. + +More details on the implementation of ChatParser follows in the Feature Section. + +#### MealList +The MealList class contains a private ArrayList of Meal object. +Further, it encapsulates behaviour to operate on this list of meals. Most notably, +this adding or deleting a Meal to/from the list. + +#### MealEntriesList +The MealEntriesList class extends the MealList class. It overwrites the extractAndAppendMeal(...) method, +and additionally includes methods specifically tailored to providing helpul user feedback, as the MealEntries stored +within its instance, signify the users calorie consumption. +As a MealEntry object differs from a Meal object by the additional timestamp attribute, this includes +computations based on the time dimension. More specifically, the printDaysConsumptionBar() uses the UIs class' +methods in the background to visualize the percentage of a certain days total consumption versus the idaeal consumption +of a User class. + +#### Meal +The Meal class encapsulates the concept of a meal. As the purpose of this application +is to track calorie consumption, this consists of a mandatory calorie entry. The meal's name attribute, +is however an Optional allowing a case, where no meaningful label can be attached to a certain consumption. +![User SD](images/addMeal.svg) + +#### MealEntry +The MealEntry class extends the meal class and contains an additional field timestamp. +This distinction was made, as objects of the Meal class will represent possible meal options to choose form, +while a mealEntry is a concrete calorie consumption the user wants to track. The latter makes a timestamp indispensible. + +#### HealthGoal +The HealthGoal class manages a user's health goal and calculates target calorie intake based on user data such as height, weight, age, and gender. +It offers three main health goals: weight loss, steady state, and bulking, each with a corresponding calorie modification factor. +The class supports setting, storing, and retrieving the health goal. +Additionally, it provides a method to compute target calories using the Harris-Benedict Equation, modified by the current health goal. +This allows the class to adapt the calorie calculations according to the user's health objectives, making it versatile for various fitness plans. + +This is a condensed diagram on how the HealthGoal class interacts with other Classes: +![HealtGoal CD](images/HealthGoalClassDiagram.png) + +The implementation of setting of a healthGoal with a String input for example is shown below: + +![Set Health Goal SD](images/setHealthGoalSD.png) + +#### User +The user class encapsulates all necessary information for computing an ideal daily calorie consumption. +This includes: +- Height (in cm): Double +- Weight (in cm): Double +- Age: Integer +- Gender: Boolean +- Health goal: HealthGoal +- Ideal calorie intake: Computed based on the information above +- Date: LocalDateTime specifying the date of the above information +More details follow in the Features section of this guide. + +#### HistoryTracker +The HistoryTracker class is responsible for managing the persistence of data in the HealthMate application. It handles the saving and loading of user data, meal entries, and meal options to and from files. This class plays a crucial role in maintaining the application's state across different sessions. + +Key features of the HistoryTracker class include: + +1. Data Persistence: + - Saves user data, meal entries, and meal options to separate files. + - Loads existing data from these files when a new usage session is initiated. + +2. File Management: + - Creates and manages the necessary files for storing application data. + - Handles file I/O operations, ensuring data integrity during read and write processes. + +3. Data Formatting: + - Converts complex objects (like User, MealEntry, and Meal) into a format suitable for file storage. + - Deserializes stored data back into usable objects when loading the application state. + +HistoryTracker allows for the persistance of user inputted data between sessions by storing it in a local csv file. + +## Features + +This section will document the contributions made by each team member regarding the implementation or planned feature enhancements, detailing the design and thought processes behind them. + +--- +### Creating a User Profile +To create or load a user profile the `UserHistoryTracker` class provides the method `checkForUserData` which loads +saved user information if available from an existing file or prompts the user +to input new information for creating a new profile as shown in the sequence diagram below. +![User SD](images/userSequenceDiagram.jpg) +Reference diagrams used +![loadUserEntries SD](images/loadUserEntriesSD.png) +![askForUserData SD](images/askForUserData.drawio.svg) +![createFileIfNotExists](images/createFileIfNotExists.drawio.png) + + +### ChatParser Input Handling +The 'ChatParser' class has the responsibility of parsing user input to steer the +application logic based on predefined commands specified in the `CommandMap' class. +Therefore, the ChatParser class acts as the main interface between user input and command execution. +This includes extracting and routing commands, as well as exception handling for +false input. + +#### Feature Implementation +The `ChatParser class:` +1. Accepts user input. I.e. it reads input from the command line +2. Tokenizes commands and identifys one- and two-token commands +3. Routes commands based on the identified command tokens which are specified in the `CommandMap` class. This is done +using methods such as `multiCommandParsing` for 2-token commands and `run` which encapsulates the main loop of +user interaction until the exit command "bye" terminates the application. +4. Logs command routing and its effects on a high level to enable tracking of the application's activity. + +#### Why It Is Implemented This Way +The ChatParser class was implemented in the above manner for three reasons: +1. It allows create one abstract unit for handling the responsibility of orchestrating usage flow. +2. As a high-level abstraction layer it improves readability by bundliing the overall application logic in one place. +3. Its modularity allows for easy extensions or modifications to `CommandMap` and `multiCommandParsing`. + +#### Alternatives considered +Direct command handling in the main loop. Reduces the depth of the application, +but comes at the cost of reduced readability and higher cohesion. + +#### Future additions +Separating the responsibilities of reading and preprocessing user input from the responsibility +to steer command routing. This could improve the maintainability of the ChatParser class in the future. + +### Command Handling with CommandMap Class + +#### Overview + +The `CommandMap` feature enhances the system's command handling by centralizing the lookup, and +storage of commands. It allows users to efficiently view commands usage within the HealthMate application. + +#### Feature Implementation + +The `CommandMap` class in the `seedu.healthmate.command` package maps command names to their corresponding +`Command` objects using a `HashMap`. This ensures fast retrieval and allows users to explore +commands with ease. + +#### Why It Is Implemented This Way + +Using a `HashMap` allows efficient command lookups with a constant time complexity of O(1). Centralizing all +commands within `CommandMap` simplifies the system's command handling process and makes it more maintainable as new +commands are added. + +#### Alternatives Considered + +An alternative was storing commands in a list and iterating through them sequentially to find the matching command. +However, this approach was less efficient for frequent lookups compared to the `HashMap`. + +#### Proposed additions for future + +The `CommandMap` can be built upon to support saving and usage of user created scripts as commands. For example +using a user could possibly create an add morningRoutine command by creating a command that runs multiple add +mealEntry commands of their regular breakfast as well as triggering the updateUser data command. + +#### Sequence Diagram +![loadUserEntries SD](images/listCommands.svg) + +1. **Command Lookup Process**: Illustrate the flow from when a user enters a command to when `CommandMap. +getCommandByName()` retrieves the command and the UI displays the results. + - Components: `UI`, `ChatParser`, `CommandMap`. + - Highlight how `CommandMap` retrieves the appropriate command based on user input. + + + +### Delete Meal and MealEntry Commands + +#### Overview + +The `delete` commands allow users to be able to delete any `meal` or `mealEntry` they may have put in. +It allows users to be able to delete any erroneous entry they may have put in. + +#### Feature Implementation + +The `delete meal` and `delete mealEntry` Commands are classes in the `seedu.healthmate.command` package. +When called by the user from ChatParser the `executeCommand` method will be called +executing the command as necessary. +This is also how all other commands are executed. + +To delete a `meal` or `mealEntry` the respective index of it must be included after the command. + +#### Why It Is implemented This Way + +Inline with the `Single-Responsibility Principle (SRP)` the execution commands were abstracted +and were put into the respective Commands. This allowed for neater and more readable code while also +allowing for less nesting inline with the `SLAP Principle`. + +The index of `meal` or `mealEntry` was used to delete it as it had less edge cases while also being +more consistent from the user's point of view. + +#### Alternatives Considered + +Initially the code to execute the commands were in the `ChatParser` class but this made it messy and +caused a lot of unnecessary nesting. Debugging the `ChatParser` class was also much harder due to this. +Therefore, the execution code and it's helper functions were shifted to the various command classes. + +The deletion of a `meal` option by using it's name was also considered. However this was found to be +less intuitive from a user point of view. Some larger `meal` names (eg. `Hawaiian Pizza with Mushrooms`) +may be harder for the user to input correctly in order to delete. From a usability perspective, +just requiring the index makes it simpler for the user. + +#### Future Additions + +A find command could be implemented that could help users find the index and details of +the `meal` or `mealEntry` that they would like to delete. Would be helpful for the user as +too many entries may make it hard for the user to find the one they would like to delete. + + +--- ## Product scope ### Target user profile -{Describe the target user profile} +The target user profile for HealthMate includes: +- Health-conscious individuals who want to monitor their daily calorie intake +- Users comfortable with command lines +- People trying to lose weight, maintain a healthy weight or increase muscle growth +- Fitness enthusiasts who want to balance their calorie consumption with their exercise routines +- Individuals with specific dietary requirements or restrictions +- Busy individuals who need a quick way to log meals and monitor progress. +- Data-Oriented Users: Users interested in gathering data about their habits to improve them +- Anyone interested in developing better eating habits and nutritional awareness + +A typical HealthMate user is Martin. +He is a 30-year-old software engineer who wants to maintain a healthy lifestyle with the help of technology. +Additionally, he has the following characteristics: +* Health-conscious individual: Martin want to pay attention to his diet. He believes in the importance of monitoring calorie intake to achieve optimal health. +* Command-line expertise: As a software engineer, Tom is extremely comfortable using command-line interfaces and appreciates the simplicity and efficiency they offer. +* Fitness enthusiast: Martin regularly engages in workouts. He aims to build muscle mass while maintaining a healthy body weight, which he achieves by balancing his calorie intake with his exercise routine. +* Data-oriented self-improvement mindset: Martin loves leveraging data to optimize his meal plans. He sets goals, tracks progress, and analyzes trends to continuously improve his habits. ### Value proposition -{Describe the value proposition: what problem does it solve?} +HealthMate solves the following problems: +- Difficulty in tracking daily calorie intake: Users can easily log their meals and snacks +- Lack of awareness about ideal calorie consumption: Based on the users health goal and his physical attributes, the app calculates the ideal daily calorie intake for this specific user. +- Lack of motivation: By visually showing the daily progress towards the users ideal calorie consumption, the app motivates to reach this goal on a daily basis. +- Inconvenience of manual calorie calculations: Pre-saved meal options make tracking quicker and more efficient +- Inability to see patterns in eating habits: Historical data allows users to analyze their consumption over time +- Struggle to maintain consistent healthy eating habits: Regular tracking encourages mindful eating and helps users stay accountable to their health goals ## 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 usage instructions | refer to them when I forget how to use the application | +| v1.0 | user | save frequently eaten meals | quickly add them in the future without re-entering details | +| v1.0 | user | log my daily meals | track my calorie intake | +| v1.0 | user | see my calorie consumption | know if I'm meeting my daily goals | +| v1.0 | user | delete a previously logged consumption | undo mistakes when tracking my meals | +| v1.0 | user | set my health goals | have a target to work towards | +| v2.0 | user | see my meal history | analyze my eating patterns over time | +| v2.0 | user | know how much calories I should consume per day based on my health goal and my physical properties | plan my future eating habits accordingly | +| v2.0 | user | see visual representations of how much of my ideal daily consumption I have eaten | better plan the rest of my daily consumption | +| v2.0 | motivated user | get a visual overview on my daily actual vs. ideal calorie intake over a specified timerange | better track my progress of developing a better eating habit | +| v2.0 | user | specify the amount of portions I eat of a meal | enter the consumption of a meal only once innstead of multiple times | +| v2.0 | user | export my data | back up my records or analyze them elsewhere | +| v2.0 | user | create custom meal combinations | quickly log common meal combinations | +| v2.0 | forgetful user | add calorie consumptions for past days | keep track of my consumption even if I forgot to enter it first | ## Non-Functional Requirements -{Give non-functional requirements} +1. Usability: The command-line interface should be intuitive and easy to use, even for non-technical users. +2. Reliability: The application should not lose any user data during normal operation or unexpected shutdowns. +3. Compatibility: The application should run on common operating systems (Windows, macOS, Linux). +4. Maintainability: The code should be well-documented and follow clean code principles for easy future enhancements. +5. Portability: User data should be easily exportable and importable for backup purposes or switching devices. +6. UX: The application should make use of intuitive visuals to help the user get insights into his eathing habits. +7. Performance: The application should respond to all user commands without negatively noticable delay. +7. Scientific: The app should calculate ideal calorie consumption based on evidence from science. ## Glossary -* *glossary item* - Definition +* *Meal* - A food item or combination of food items consumed at one time, with associated calorie information. +* *MealEntry* - A record of a meal consumed by the user, including the meal details and a timestamp. +* *MealList* - A collection of pre-saved meals that users can quickly select from when logging their food intake. +* *MealEntriesList* - A chronological list of all meals consumed by the user. +* *ChatParser* - The component responsible for interpreting user commands and executing the appropriate actions. +* *HistoryTracker* - The component that manages the storage and retrieval of meal entries, and meal options. +* *UserHistoryTracker* - The component that manages the storage and retrieval of user data. +* *HealthGoal* - The class that manages functions pertaining to setting of user health goal as well calculating ideal caloric intake based on various factors. ## 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} +1. Installation and Setup: + - Ensure Java Runtime Environment (JRE) is installed on your system. + - Download the HealthMate application JAR file. + - Open a terminal or command prompt and navigate to the directory containing the JAR file. + +2. Running the Application: + - Execute the command: `java -jar HealthMate.jar` + - Verify that the application starts and displays a welcome message. + +3. Testing Basic Commands: + - Do note our commands are not case-sensitive except for our special parameters (/c,/p,/t) + - Try entering the command `list commands` and verify that usage instructions are displayed. + - Test the `bye` command to ensure the application exits properly. + +4. Saving a Meal: + - Use the command `save meal [name] [calories]` (e.g., `save meal Chicken Salad /c350`) + - Verify that the meal is added successfully and displayed in the meal list using `meal menu` + +5. Adding a Meal Entry: + - Use the command `add mealEntry [meal from menu/ standalone meal name] [calories if standalone meal] [portion]` + (e.g., `add mealEntry Chicken Salad /p3`) or (`add mealEntry newmeal /c300 /p1`) + - Check that the meal entry is recorded with the current timestamp. + +6. Show Historic Caloric Trend: + - Use the command `show historicCalories [no. of days]` (e.g., `show historicCalories 5`) + - Check that the calories added previously are shown and that all the stats displayed are correct + +7. Testing Data Persistence: + - Exit the application using the `bye` command. + - Restart the application and check if previously added meals and logged entries are still present. + +8. Error Handling: + - Try entering invalid commands or data to ensure the application handles errors gracefully and provides helpful error messages. diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..db164a0b9e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,8 @@ -# Duke +# HealthMate -{Give product intro here} +HealthMate is a meal and calorie tracking application designed to help users manage their dietary intake, +monitor their weight, and track their overall health goals. +The app enables users to log meals, track calories, and observe their progress towards a healthier lifestyle. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d6cf4c3b3a..229a8d2b2a 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,521 @@ -# User Guide +# User Guide for HealthMate ## Introduction -{Give a product intro} +**HealthMate** is a meal and calorie tracking application designed to help users manage their dietary intake, monitor their weight, and track their overall health goals. The app enables users to log meals, track calories, and observe their progress towards a healthier lifestyle. -## Quick Start +## Contents -{Give steps to get started quickly} +- [Quick Start Guide](#quick-start-guide) +- [Features](#features) + - [Exit](#exit-bye) + - [List Commands](#list-commands) + - [Meal Management Commands](#meal-management-commands) + - [Save Meal to Meal Menu](#save-meal-to-meal-menu-save-meal-name-of-meal-cnumber-of-calories) + - [Overwrite Saved Meal in Meal Menu](#overwrite-saved-meal-in-meal-menu-save-meal-name-of-existing-meal-cnumber-of-calories) + - [Add Meal Entry for Tracking](#add-meal-entry-for-trackingadd-mealentry-meal-cnumber-of-calories-pportions-tyyyy-mm-ddor-add-mealentry-meal-from-meal-menu-or) + - [Delete meal from meal menu](#delete-meal-from-meal-menu-delete-meal-index-of-meal-in-meal-menu) + - [Show List of Available Meal Options](#show-list-of-available-meal-options-meal-menu) + - [Weight Timeline](#weight-timeline-weight-timeline) + - [Meal Recommender](#meal-recommender-meal-recommendations) +- [Meal Log Commands](#meal-log-commands) + - [Show Meal History](#show-meal-history-log-meals) + - [Delete meal from meal log](#delete-meal-from-meal-log-delete-mealentry-index-of-meal-in-the-meal-log) +- [Calorie Progress Commands](#calorie-progress-commands) + - [Show Calorie Progress for Today](#show-calorie-progress-for-today-show-todaycalories) + - [Show Historic Calorie Progress](#show-historic-calorie-progress-show-historiccalories-number-of-days-inclu-today) +- [Update your data](#update-your-data) +- [FAQ](#faq) +- [Command Summary](#command-summary) +- [Data Storage and Persistence](#data-storage-and-persistence) + - [Data Security](#data-security) -1. Ensure that you have Java 17 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). -## Features +## Quick Start Guide -{Give detailed description of each feature} +- Welcome to HealthMate! Follow these steps to get started quickly: +- Do note all commands are not case-sensitive, except for special parameters (/c, /p, /t) -### Adding a todo: `todo` -Adds a new item to the list of todo items. +**Entering the App** +- Open CLI and Navigate to location of `HealthMate.jar` +- Run the following Command: `java -jar HealthMate.jar` -Format: `todo n/TODO_NAME d/DEADLINE` +**Create your user profile** +Input data about yourself needed to compute your ideal daily calorie intake. +This includes: +- Your height in cm (e.g. 180) +- Your weight in kg (e.g. 80) +- Your gender (either male or female) +- Your age in years (e.g. 20) +- Your personal health goal: + - `1` WEIGHT_LOSS + - `2` STEADY_STATE + - `3` BULKING +- Whether you your system can print the special characters "░" and "█" +(The UI of the consumption bar feature will be customized based on your answer). -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +**Log Your First Meal** +- Enter the meal name and calories. +- Example: `add mealEntry burger /c300` +- The printed consumption bar shows you how much more (or less) you should consume to reach your health goal. -Example of usage: +``` +add mealEntry burger /c300 -`todo n/Write the rest of the User Guide d/next week` + _____________________________________________________________________________ + Tracked: burger with 300 calories (at: 2024-10-29T22:00) + _____________________________________________________________________________ + % of Expected Calorie Intake Consumed: + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11% |░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-29) + _____________________________________________________________________________ +``` +**Get an overview on your calorie consumption** +- Use the "meal log" feature to view your meal entries and track your daily caloric intake. +- Example: Add your first mealEntry and then use `meal log` to assess the meals tracked so far. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` + ``` + meal log + _____________________________________________________________________________ + 1: burger with 300 calories (at: 2024-10-29T22:00) + _____________________________________________________________________________ + ``` + +**Track your daily progress** +- Based on your user data and your health goal we computed your ideal daily calorie intake. +- When you add (or delete) a mealEntry, we print a consumption bar showing how much more/less you should consume. +- Interpreting the consumption bar: + - The percentage in the middle shows how much percent of your ideal calorie consumption you have consumed so far. + - The date on the right shows you the date for which this percentage is calculated. +- Example: On the 29th of October 2024 you have consumed 11% of your ideal calorie intake. + +``` + % of Expected Calorie Intake Consumed: + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11% |░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-29) +``` + + +**Store Common Meals** +- Save frequently eaten meals to your meal menu for quick access +- Example: `save meal pizza /c800` saves pizza with 800 calories to your meal menu +- Add meals from the meal menu without specifying the calories +- Example: `add mealEntry pizza` + +**Track Meal Menu** +- Use the `meal menu` command to view your saved meal options +- Example: `meal menu` + + + +Enjoy your journey towards a healthier lifestyle with HealthMate! +## Features +### Legend +[] = optional parameter +{} = parameter +### Exit: `bye` +- Exits program + +``` +bye + Stay healthy! + _____________________________________________________________________________ +``` + +### List Commands +Use the `list commands [{command}]` command to view all valid commands and their formats. +- Example usage to list all commands + +``` +list commands + _____________________________________________________________________________ + Use `list commands ` to view a command's syntax + _____________________________________________________________________________ + update userdata + update userdata + _____________________________________________________________________________ + current userdata + current userdata + _____________________________________________________________________________ + list commands + list commands + _____________________________________________________________________________ + meal log + meal log + _____________________________________________________________________________ + add mealEntry + add mealEntry {meal name from menu} OR add mealEntry [{name}] /c{calories} [/p{portions}] [/t{Date in YYYY-MM-DD}] + _____________________________________________________________________________ + delete mealEntry + delete mealEntry {index of meal in the meal log} + _____________________________________________________________________________ + meal menu + meal menu + _____________________________________________________________________________ + save meal + save meal {meal name} /c{number of calories} + _____________________________________________________________________________ + delete meal + delete meal {index of meal in meal menu} + _____________________________________________________________________________ + show todayCalories + show todayCalories + _____________________________________________________________________________ + show historicCalories + show historicCalories {Number of Days inclu. Today} + _____________________________________________________________________________ + meal recommendations + meal recommendations + _____________________________________________________________________________ + weight timeline + weight timeline + _____________________________________________________________________________ + bye + bye + _____________________________________________________________________________ +``` + +List Commands with command parameter: + +``` +list commands delete meal + _____________________________________________________________________________ + Command: delete meal + Format: delete meal {index of meal in meal menu} + Description: Deletes meal option at the specified index from the meal menu +``` + +### Meal Management Commands: + +#### Save Meal to Meal Menu: `save meal {Name of Meal} /c{Number of calories}` +- Allows user to store a meal with its calories to be used with the add mealEntry command +- Example usage to store a meal of pizza with 300 calories + +``` +save meal pizza /c300 + _____________________________________________________________________________ + Added to options: pizza with 300 calories + _____________________________________________________________________________ +``` + +#### Overwrite Saved Meal in Meal Menu: `save meal {Name of existing Meal} /c{Number of calories}` +- Allows user to update an existing meal option with a new calorie number + +``` +save meal soup /c300 + _____________________________________________________________________________ + Added to options: soup with 300 calories + _____________________________________________________________________________ +save meal soup /c200 + _____________________________________________________________________________ + Duplicate meal found: soup + Updated existing meal with new meal specifics! + _____________________________________________________________________________ +meal menu + _____________________________________________________________________________ + 1: pizza with 400 calories + 2: soup with 200 calories + _____________________________________________________________________________ +``` + +#### Add Meal Entry for Tracking:`add mealEntry {meal} /c{Number of calories} /p{portions} /t{YYYY-MM-DD}`or `add mealEntry {meal from meal menu}` or +- Adds a meal from the saved meal options to your daily caloric intake. +- After adding the meal, the app will show how the meal affects your progress towards your daily caloric goal. + +Log a meal with calories: + +``` +add mealEntry grapes /c100 + _____________________________________________________________________________ + Tracked: grapes with 100 calories (at: 2024-11-03) + _____________________________________________________________________________ + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + Current Calories Consumed: 100 + % of Expected Calorie Intake Consumed: + █░░░░░░░░░░░░░░░░░░░░░░░░░░░░| 4%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-11-03) + _____________________________________________________________________________ +``` + +Log a meal for a specific day in the past: + +``` +add mealEntry pizza /c300 /t2024-10-30 + _____________________________________________________________________________ + Tracked: pizza with 300 calories (at: 2024-10-30) + _____________________________________________________________________________ + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + Current Calories Consumed: 300 + % of Expected Calorie Intake Consumed: + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-30) + _____________________________________________________________________________ +``` + +Shorcut: log a meal without a name and only calories. + +``` +add mealEntry /c200 + _____________________________________________________________________________ + Tracked: Meal with 200 calories (at: 2024-11-11) + _____________________________________________________________________________ + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + Current Calories Consumed: 1300 + % of Expected Calorie Intake Consumed: + █████████████░░░░░░░░░░░░░░░░| 46%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-11-11) + _____________________________________________________________________________ + +``` + +Shortcut: log a presaved meal from the list of meal options (no calories needed). + +``` +add mealEntry pizza +Getting info from meal options... + _____________________________________________________________________________ + Tracked: pizza with 300 calories (at: 2024-11-03) + _____________________________________________________________________________ + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + Current Calories Consumed: 400 + % of Expected Calorie Intake Consumed: + ████░░░░░░░░░░░░░░░░░░░░░░░░░| 14%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-11-03) + _____________________________________________________________________________ +``` -## FAQ -**Q**: How do I transfer my data to another computer? +#### Delete meal from meal menu: `delete meal {index of meal in meal menu}` +- Deletes meal option from the meal menu at the specified index +- To identify the right index consider running `meal menu` beforehand +- Example usage -**A**: {your answer here} +``` +delete meal 1 + _________________________________________________________________________ + Deleted option: pizza with 400 calories + _________________________________________________________________________ +``` +#### Show List of Available Meal Options: `meal menu` +- Lists all the saved meal options for quick selection when logging your meals. +``` +meal menu + _________________________________________________________________________ + 1: pizza with 400 calories + 2: ciffbar with 300 calories + _________________________________________________________________________ +``` + +#### Weight Timeline: `weight timeline` +- Returns a graph of weight changes over time +- Requires users to have saved sufficient weight entries of different values using the `update userdata` command +before being able to display a graph of weight in kilograms over time. Graph is normalized to the Max and minimum + weight values hence resolution of graph decreases if the gap between minimum and maximum weight is unrealistically + large + +``` +weight timeline +Weight Timeline +170.0 | * +165.5 | * +161.0 | * +156.5 | * +152.0 | * +147.5 | * +143.0 | * +138.5 | * +134.0 | * +129.5 | * +125.0 | * +120.5 | * +116.0 | * * +111.5 | * * +107.0 | * * +102.5 | * * + 98.0 | * * * + 93.5 | * * * + 89.0 | * * * + 84.5 | * * * * + 80.0 | * * * * * * * * * * + ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + 11-09 11-09 11-09 11-09 11-09 11-09 11-09 11-09 11-09 11-09 +``` + + +#### Meal Recommender: `meal recommendations` +- Returns recipes that suit your health goal + +``` +meal recommendations + _____________________________________________________________________________ + Recommended recipes for your health goal + Veggie Wrap with Hummus: 361 calories + Protein: 12g + Carbs: 50g + Fat: 14g + Fiber: 8g + 1 teaspoon extra-virgin olive oil + ½ small zucchini, sliced + ½ medium red bell pepper, sliced + ¼ small red onion, sliced + ½ teaspoon dried oregano + Pinch of salt + 2 whole-grain wraps + ¼ cup hummus + ½ cup baby spinach + 2 tablespoons crumbled feta cheese + 4 black olives, sliced +``` + +### Meal Log Commands: + +- Meal entries are managed in the meal log. With it user's can view their tracked meals. + +#### Show Meal History: `meal log` +- Displays the log of all meal entries along with their Timestamp in Date Time format. +- Example Usage + +``` +meal log + _____________________________________________________________________________ + 1: pizza with 300 calories (at: 2024-11-01) + 2: late dinner with 300 calories (at: 2024-11-02) + 3: salad with 200 calories (at: 2024-11-03) + 4: supper with 100 calories (at: 2024-11-04) + _____________________________________________________________________________ + +``` + +#### Delete meal from meal log: `delete mealEntry {index of meal in the meal log}` +- Deletes meal entry from the meal log at the specified index +- Note: depending on your system the bars might look different +- Example Usage + +``` +delete mealEntry 1 + _____________________________________________________________________________ + Deleted entry: Cheeseburger with 900 calories (at: 2024-10-30T11:00) + _____________________________________________________________________________ + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + Current Calories Consumed: 500 + % of Expected Calorie Intake Consumed: + █████░░░░░░░░░░░░░░░░░░░░░░░░| 18% |░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-30) + _____________________________________________________________________________ +``` +### Calorie Progress Commands: +#### Show Calorie Progress for Today: `show todayCalories` +- Prints a Calorie Progress Bar to represent Today Calorie Progress +- Note: depending on your system the bars might look different +- Example Usage: + +``` +show todayCalories + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + Current Calories Consumed: 900 + % of Expected Calorie Intake Consumed: + █████████░░░░░░░░░░░░░░░░░░░░| 32% |░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-29) + _____________________________________________________________________________ +``` + +#### Show Historic Calorie Progress: `show historicCalories {Number of Days inclu. Today}` +- Prints Calorie Progress Bars & Various Stats to represent Historical Calorie Progress +- Combines global and local view on eating patterns via the progress bar and details such as the meal with the highest calories. +- Example Usage (Note: depending on your system the bars might look different): + +``` +show historicCalories 10 + _____________________________________________________________________________ + Ideal Daily Caloric Intake: 2865 + _____________________________________________________________________________ + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-25) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-26) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-27) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-28) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-29) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-30) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-10-31) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-11-01) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-11-02) + ███░░░░░░░░░░░░░░░░░░░░░░░░░░| 11%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (2024-11-03) + Stats over past 10 days + Total Calories Consumed: 3000 + Total Ideal Calories: 28650 + Percentage of Total Ideal Calories : 10.0% + Day With Heaviest Meal: 2024-10-24 + Heaviest Meal Consumed: burger with 300 calories (at: 2024-10-25) + Meals Consumption's Percentage of Daily Ideal Calories: 10.0% + _____________________________________________________________________________ +``` + + +### Update your data +Your goal or your body weight changed? By running the `update userdata` you can update the specifics of your profile. + +``` +_____________________________________________________________________________ +update userdata + Create your profile: please enter... + Height in cm (e.g. 180): +``` + +If you want to see the specifics for which your ideal calorie consumption is calculated +run the `current userdata` command. + +``` +current userdata + _____________________________________________________________________________ + Here is your current user data: + Height: 182.0cm + Weight: 80.0kg + Gender: male + Age: 20 + Health Goal: BULKING + Ideal Daily Caloric Intake: 2688.0 + Recorded at: 2024-11-12 09:01:41 + Is able to see special chars: true + _____________________________________________________________________________ + +``` + +## FAQ ## Command Summary -{Give a 'cheat sheet' of commands here} +### Legend +[] = optional parameter +{} = parameter + +| Command | Syntax | Description | +|--------------------------------------------|--------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| +| List all commmands | `list commands` or `list commands [{command}]` | Lists all available commands and the proper formatting. +| Save meal to meal options | `save meal {meal} /c{Number of calories}` | Prompts for meal name, calories, then confirms saving the meal. | +| Add meal entry for tracking | `add mealEntry {meal} [/c{Number of calories}] [/p{Number of portions}] [/t{timestamp in YYYY-MM-DD}]` | Adds meal to daily caloric intake and shows progress toward goal. | +| Show list of available meal options | `meal menu` | Prints all available meals from the saved options list. | +| Show past meals | `meal log` | Displays history of meals with timestamp and calories. | +| Delete meal from meal menu | `delete meal {index of meal in meal menu}` | Deletes meal option from the meal menu at the specified index. | +| Delete meal entry from meal log | `delete mealEntry {index of meal in the meal log}` | Deletes meal entry from the meal log at the specified index and shows effect on the days progress toward goal. | +| Show Calorie Progress for Today | `show todayCalories` | Prints a Calorie Progress Bar to represent Today Calorie Progress | +| Show Historic Calorie Progress | `show historicCalories {Number of Days inclu. Today}` | Prints Calorie Progress Bars & Various Stats to represent Historical Calorie Progress | +| Add and Update new User Entry to Save File | `update userdata` | Asks user for new User data to update in save file. | +| Show Most Recent User Data Entry | `current userdata` | Prints the most recent User Data from the save file. Prints an error if none found. | +| Exit | `bye` | Closes program after saving data | +| Display weight timeline | `weight timeline` | Creates a graph of up to the last 10 weight entries over time if there is significant changes. | +| Meal recommendation command | `meal recommendations` | Returns a list of ready recipes for a user based on their HealthGoal | + +## Data Storage and Persistence +HealthMate stores your meal logs, meal options, and user profile data in CSV files located in a folder named `data` within the application directory. +This allows your data to persist between sessions, so you won’t lose your progress when you close the application. + +Here’s how HealthMate manages your data in detail: +1. Meal Entries and Options: Your logged meals and saved meal options are stored in `meal_entries.csv` and `meal_options.csv`. +2. User Profile: Your profile data, including height, weight, age, gender, and health goal, is saved in `user_data.csv`. -* Add todo `todo n/TODO_NAME d/DEADLINE` +### Data Security +To ensure no data is lost DO NOT manually modify these files or move them out of the directory. +In case you need to transfer your data, we recommend making a copy instead. \ No newline at end of file diff --git a/docs/images/HealthGoalClassDiagram.png b/docs/images/HealthGoalClassDiagram.png new file mode 100644 index 0000000000..46dc6c5b88 Binary files /dev/null and b/docs/images/HealthGoalClassDiagram.png differ diff --git a/docs/images/UserClassDiagram.jpg b/docs/images/UserClassDiagram.jpg new file mode 100644 index 0000000000..b74c118255 Binary files /dev/null and b/docs/images/UserClassDiagram.jpg differ diff --git a/docs/images/addMeal.svg b/docs/images/addMeal.svg new file mode 100644 index 0000000000..12397a08d5 --- /dev/null +++ b/docs/images/addMeal.svg @@ -0,0 +1,4 @@ + + + +
run()
[Command is add meal]
historyTracker:HistoryTracker
saveMealToFile(...)
chatParser:ChatParser
multiCommandParsing(...)
mealOptions:MealList
load meal options
ref
read user input from scanner
ref
alt
extractAndAppendMeal(...)
creates a meal object 
from string and 
appends it
saveMealOptions(...)
\ No newline at end of file diff --git a/docs/images/askForUserData.drawio.svg b/docs/images/askForUserData.drawio.svg new file mode 100644 index 0000000000..3eb0c4c2de --- /dev/null +++ b/docs/images/askForUserData.drawio.svg @@ -0,0 +1,4 @@ + + + +
<<class>>
User
<<class>>...
<<class>>
UI
<<class>>...
printString("Age (e.g. 20):")
printString("Age (e.g. 20):")
printString("Profile creation Successful!")
printString("Profile creation Successful!")
printString("Great! You can now begin to use the app!")
printString("Great! You can now begin to use the app!")
Prompt user for data, scan input via Scanner, create and save user to UserDataFile
Prompt user for data, scan input via Scanner, create and save user to UserDataFi...
s: Scanner
s: Scanner
nextLine()
nextLine()
 int age
 int age
printString("Create your profile: please enter...")
printString("Create your profile: please enter...")
nextLine()
nextLine()
double height
double height
boolean isMale
boolean isMale
nextLine()
nextLine()
printString("Height in cm (e.g. 180):")
printString("Height in cm (e.g. 180):")
printString("Weight in kg (e.g. 80):")
printString("Weight in kg (e.g. 80):")
askForUserData()
askForUserData()
double weight
double weight
printString("Gender (male or female):")
printString("Gender (male or female):")
nextLine()
nextLine()
printString("Health Goal (WEIGHT_LOSS, STEADY_STATE, BULKING):")
printString("Health Goal (WEIGHT_LOSS, STEADY_STATE, BULKING):")
nextLine()
nextLine()
String healthGoal
String healthGoal
User(height, weight, isMale, age, healthGoal)
User(height, weight, isMale, age, healthGoal)
User user
User user
:userHistoryTracker
:userHistoryTracker
UserHistoryTracker()
UserHistoryTracker()
saveUserToFile(User user)
saveUserToFile(User user)
User user
User user
\ No newline at end of file diff --git a/docs/images/createFileIfNotExists.drawio.png b/docs/images/createFileIfNotExists.drawio.png new file mode 100644 index 0000000000..6e868650bb Binary files /dev/null and b/docs/images/createFileIfNotExists.drawio.png differ diff --git a/docs/images/highLevelClassDiagram.jpg b/docs/images/highLevelClassDiagram.jpg new file mode 100644 index 0000000000..bb8db66fe1 Binary files /dev/null and b/docs/images/highLevelClassDiagram.jpg differ diff --git a/docs/images/listCommands.svg b/docs/images/listCommands.svg new file mode 100644 index 0000000000..9c4ca66b00 --- /dev/null +++ b/docs/images/listCommands.svg @@ -0,0 +1,4 @@ + + + +
<<class>>
UI
getCommands(userInput, command)
alt
<<class>>
CommandMap
getAllCommands()
List<Command> commands
List<Command> commands
[commandToFind is empty || commandToFind does not exist]
[commandToFind exists in map]
getCommandByName(commandToFind)
List<Command> commands
chatParser:ChatParser
multiCommandParsing(...)
printCommands(commands)
alt
[command is list commands]
\ No newline at end of file diff --git a/docs/images/loadUserEntriesSD.png b/docs/images/loadUserEntriesSD.png new file mode 100644 index 0000000000..d605c63513 Binary files /dev/null and b/docs/images/loadUserEntriesSD.png differ diff --git a/docs/images/mealMenuSD.png b/docs/images/mealMenuSD.png new file mode 100644 index 0000000000..ca0d75f782 Binary files /dev/null and b/docs/images/mealMenuSD.png differ diff --git a/docs/images/setHealthGoalSD.png b/docs/images/setHealthGoalSD.png new file mode 100644 index 0000000000..056b3a207b Binary files /dev/null and b/docs/images/setHealthGoalSD.png differ diff --git a/docs/images/userSequenceDiagram.jpg b/docs/images/userSequenceDiagram.jpg new file mode 100644 index 0000000000..e84ad887fe Binary files /dev/null and b/docs/images/userSequenceDiagram.jpg differ diff --git a/docs/team/darkdragoon2002.md b/docs/team/darkdragoon2002.md new file mode 100644 index 0000000000..98816e723b --- /dev/null +++ b/docs/team/darkdragoon2002.md @@ -0,0 +1,78 @@ +# Project Portfolio Page - DarkDragoon2002 + +## Overview +HealthMate is a meal and calorie tracking application designed to help users manage their dietary intake, monitor their weight, and track their overall health goals. The app enables users to log meals, track calories, and observe their progress towards a healthier lifestyle. + +## Summary of Contributions + +### Code Contributed +[Link to code contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=darkdragoon2002&breakdown=true) + +### Enhancements Implemented + +1. **Implemented `HealthGoal` Class** + - Implemented `HealthGoal` Class allowing the `User` Class to save the health goal of the user + - Implemented Target Calories Calculation, depending on data such as height, weight, age, gender and health goal + - Created a Function to update it + - Created a Command that can be called by the user but has since been integrated into update user command + +2. **Implemented Deletion of `Meal` & `MealEntry`** + - Implemented Deletion of Meal & MealEntry Commands and Functions + - Integrated both into the `ChatParser` Class as well + +3. **Implemented `DateTimeUtils` Class** + - Implemented `DateTimeUtils` Class in order to allow for easier and more compact use of `LocalDateTime` and `LocalDate` packages + - Used in the implementation of `show todayCalories` & `show historic Calories {No. of Days}` commands + +4. **Implemented `todayCalories` & `historicCalories` Commands and Methods** + - Implemented `todayCalories` & `historicCalories` methods and Commands + - Used `kennethSty`'s `ConsumptionBars` Code to print out consumption bars for the various days + - Used `DateTimeUtils` to better get the date and times necessary + - Implemented the statistics and necessary code that was used by `historicCalories` which was then abstracted out by `kennethSty` into the `ConsumptionStatistics` Class + +5. **Migrated Command Code from `ChatParser` to respective Command Classes** + - Migrated the Code to various `executeCommand` Methods from ChatParser + - Was initially in a large `switch`, `case` statement + - Migration allowed for Increased Readability + - Inline with Single Responsibility Principle + - Added Assertions and other checks in other Classes to prevent bugs + + + +6. **Unit Testing** + - Created Unit Tests for sections I worked on + - Created `HealthGoalTest` + - Added Testing Functions to ChatParser for: + - `delete meal` + - `delete mealEntry` + - `show todayCalories` + - Major Changes to `show historicCalories` + - Added Multiple Helper Functions to aid with testing as well +### Contributions to the User Guide +* Added entries for the following commands: + - Delete meal from meal menu + - Delete meal from meal log + - Show Calorie Progress for Today + - Show Historic Calorie Progress +* Added Entering App section in Quick Start Guide +* Updated User Guide post Bug Fixing + +### Contributions to the Developer Guide +* Added HealthGoal and various references to it + * Added Class Diagram for it + * Added Sequence Diagram for it +* Add Step in Manual Testing to show HistoricCaloricTrend +* Added Details on Deletion Commands + +### Contributions to Team Tasks +* Set up the Repo +* Set up various permissions, tags, roles and rules in the repo +* Contributed to creation of issues +* Fixed IO Testing issues +* Major Bug Fixing post PE Dry Run +* Created and release v2.0 + +### Review/Mentoring Contributions +* Reviewed and approved multiple PRs +* Helped troubleshoot integration issues between different components +* Helped to resolve merge conflicts for multiple PRs \ No newline at end of file diff --git a/docs/team/dri-water.md b/docs/team/dri-water.md new file mode 100644 index 0000000000..af76804411 --- /dev/null +++ b/docs/team/dri-water.md @@ -0,0 +1,59 @@ +# Project Portfolio Page - Dri-water + +## Overview +HealthMate is a meal and calorie tracking application designed to help users manage their dietary intake, monitor their weight, and track their overall health goals. The app enables users to log meals, track calories, and observe their progress towards a healthier lifestyle. + +## Summary of Contributions + +### Code Contributed +[Link to code contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=authorship&tabAuthor=Dri-water&tabRepo=AY2425S1-CS2113-W12-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented +1. **Enhanced Meal Menu System** + * Implemented the meal saving functionality that allows users to store frequently eaten meals with their calorie counts using the `save meal` command (details in mealSaver class) + * Added ability to retrieve saved meals when logging new meal entries using `add mealEntry`, improving user experience by eliminating the need to remember and re-enter calorie information + * Modified `mealEntry` class to accommodate retrieval of saved meals + +2. **Implemented MealSaver Class** + * This class handles the logic of writing and overwriting the `mealOptions` + * This abstracts the logic of saving meals from the main logic of the program, making the code more modular and easier to understand + +3. **Implemented History Tracking System** + * Added a `HistoryTracker` class + * Developed the meal history tracking system that maintains a chronological record of all meal entries that persists beyond the current session + * Writes to a csv file the current `meal_entries` and `meal_options` within the /data folder + +4. **Implemented manual mealEntry timestamp** + * Added functionality to allow users to add a mealEntry with a custom specified timestamp + * Modified parsing logic to accept timestamp input + * Added error-handling for invalid timestamp inputs + +### Contributions to the User Guide + * Added Quick start guide + * Added details for `add mealEntry` command + + +### Contributions to the Developer Guide +* Added implementation details for: + * `Meal Menu` feature + * `HistoryTracker` class + * UML diagram for `meal menu` command +* Added instructions for: + 1. Installation and Setup + 2. Running the application + 3. Testing basic commands + 4. Adding a mealEntry + 5. Testing data persistence + 6. Error Handling +* Added Non-functional requirements for project + +### Contributions to Team Tasks +* Helped in setting up the initial project structure +* Contributed to creation of issues +* Helped coordinate version control and release management + +### Review/Mentoring Contributions +* Reviewed and approved multiple PRs +* Helped troubleshoot integration issues between different components +* Helped to resolve merge conflicts for multiple PRs + 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/kennethsty.md b/docs/team/kennethsty.md new file mode 100644 index 0000000000..0031be9cd3 --- /dev/null +++ b/docs/team/kennethsty.md @@ -0,0 +1,93 @@ +# Project Portfolio Page - kennethSty + +## Overview +HealthMate is a meal and calorie tracking application designed to help users manage their dietary intake, +monitor their weight, and track their overall health goals. +The app enables users to log meals, track calories, and observe their progress towards a healthier lifestyle. + +## Summary of Contributions + +### Code Contributed +[Link to code contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=Kenneth&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=authorship&tabAuthor=kennethSty&tabRepo=AY2425S1-CS2113-W12-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +*Created foundational application design, including packaging, and contributed to the following classes:* +- `Meal` and `MealEntry` - initial version and design improvements +- `MealList` and `MealEntriesList` - initial version and refinements +- `HistoryTracker` - bug fixes and minor contributions +- `UI`, `Logging`, `User`, `UserHistoryTracker`, and `ConsumptionStatistics` - initial implementations and design enhancements +- `Pair` and `CommandPair` - initial implementations + +### Enhancements Implemented + +1. **`Meal` and `MealEntry` Classes** + - Established inheritance structure for `Meal` and `MealEntry`. + - Added `Optional` handling for unnamed entries and low-level input parsing. + +2. **`MealList` and `MealEntriesList` Classes** + - Set up classes with polymorphism for `Meal` and `MealEntry` to enable efficient list handling. + +3. **User- & Storage-Related Enhancements (`User`, `UserHistoryTracker`, `HistoryTracker`)** + - Created `User` class structure, added `checkForUserData()` and `loadUserEntries()` for user initialization. + - Refactored `askForUserData` to improve exception handling and support re-entry without restarting. + - Assisted in refactoring `UserHistoryTracker` to handle data corruption. + - Enhanced `HistoryTracker` to allow users to revert corrupted data. + +4. **`ChatParser` Class Contributions** + - Designed `ChatParser` and implemented command preprocessing, command routing (via `run` and `multiCommandParsing`) and command logging. + - Developed command logic for `meal log`, `meal menu`, `save meal`, and `add mealentry`. + +5. **`UI` Class Implementation** + - Built static methods for standardized output, structured feedback, and added methods for displaying logged meals. + - Implemented the consumption bar and its applications to visualize user progress toward health goals, with system-dependent customization for character support. + - Customized `UI` for system compatibility with special characters for a visual consumption bar. + +6. **Unit Testing** + - Created `ChatParserTest` with foundational test methods (`setOutputStream`, `restoreStream`, `compareChatParserOutput`). + - Added 7 tests for input handling, meal tracking, historical data retrieval, and timestamp validation. + + +### Contributions to the User Guide +Added the sections: +- Get an overview on your calorie consumption +- Track your daily progress +- Create your user profile +- Data Storage and Persistence +- Data Security +- Add Meal Entry for Tracking (both 'shortcut' subsections) + +Overall proofreading and improvements. + + +### Contributions to the Developer Guide +* In `Design & Implementation` I added the following subsections: + - `High Level Class design` (incl. class diagram) + - `HealthMate` + - `ChatParser` + - `MealList` + - `MealEntriesList` + - `Meal` (including the sequence diagram) + - `MealEntry` + - `User` + +* In the `Features` section the subsections I added are: + - `Creating a User Profile` and the sequence diagram `userSequenceDiagram.jpg` + - `ChatParser Input Handling` including the subsections +* Other contributions: + - Adding the target user profile and personification. + - Added non-functional requirements. + - Helped define value propositions. + +### Contributions to Team Tasks +* Took over responsibility for managing and creating issues for steering team progress (opened 30 issues, closed 46 myself). +* Created the first POC application for tracking meal entries and meal options to kickstart the project. +* Fixed IO Testing issues. +* Resolved 18 issues after the Dry-PE run. +* Will help to do the product demo. +* Performed testing. + +### Review/Mentoring Contributions +* Reviewed, approved or if necessary requested changes to several PRs every week (authored 42 PRs, reviewed 30) +* Helped to finish the user profile creation procedure when the previous approach got stuck. +* Helped to SLAP code (e.g. the feature to print historic consumption bars). +* Initiated calls to help my teammates solve final bugs before creating the final Jar. +* Simply had a good time :-) diff --git a/docs/team/ryan-txn.md b/docs/team/ryan-txn.md new file mode 100644 index 0000000000..00932757ca --- /dev/null +++ b/docs/team/ryan-txn.md @@ -0,0 +1,47 @@ +# Project Portfolio Page - ryan-txn + +## Overview +HealthMate is a meal and calorie tracking application designed to help users manage their dietary intake, monitor their weight, and track their overall health goals. The app enables users to log meals, track calories, and observe their progress towards a healthier lifestyle. + +## Summary of Contributions + +### Code Contributed +[Link to code contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=ryan-txn&sort=totalCommits%20dsc&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=authorship&tabAuthor=ryan-txn&tabRepo=AY2425S1-CS2113-W12-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented +1. **Implement User Data Format** + * Created a data structure to store user information including height, weight, gender, age, timestamp, and health goal + * Enables usage for displaying weight changes over time ([ryryry-3302](ryryry-3302.md)) and calculating ideal daily calories ([DarkDragoon](darkdragoon2002)) + +2. **Implement Saving and Loading of User Data into CSV File** + * Developed the `UserHistoryTracker` class with methods for: + - Printing user data + - Converting user data into comma-separated strings for saving + - Parsing saved data and converting it to a `UserEntryList` format for easy retrieval and display + +3. **Add Test Cases for Saving and Loading of User Data** + * Created unit tests to ensure correct functionality for saving and loading user data + * Validates proper conversion to and from CSV format, ensuring data integrity during file operations + +4. **Strengthen User Save File from Manual Changes** + * Added validation checks for each user attribute to detect tampering with the save file + * Ensures that loaded data adheres to expected formats for all fields, protecting against erroneous or malicious modifications to user data + + +### Contributions to the User Guide +* Added contents page +* Added details for add mealEntry command + + +### Contributions to the Developer Guide +* Added Sequence Diagrams for: + * loadUserEntries() method + * createFileIfNotExists() method + +### Contributions to Team Tasks +* Handled user data architecture +* Contributed to creation of issues + +### Review/Mentoring Contributions +* Reviewed and approved multiple PRs +* Resolved merge conflicts where necessary diff --git a/docs/team/ryryry-3302.md b/docs/team/ryryry-3302.md new file mode 100644 index 0000000000..9ceacd1d2b --- /dev/null +++ b/docs/team/ryryry-3302.md @@ -0,0 +1,60 @@ +# Project Portfolio Page - ryryry-3302 + +## Overview +HealthMate is a meal and calorie tracking application designed to help users manage their dietary intake, monitor +their weight, and track their overall health goals. The app enables users to log meals, track calories, and observe +their progress towards a healthier lifestyle. + +## Summary of Contributions +[Link to code contributions](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=ryryry-3302&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=authorship&tabAuthor=ryryry-3302&tabRepo=AY2425S1-CS2113-W12-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=functional-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +Given below are my contributions to the project. +1. **Command Class** + - Abstract class used to serve as the base class child command classes + - Used to store command info such as, command name, syntax and description + - Convenient design allowing us to store future command information added later on and for easy retrieval with a + common toString() method +2. **CommandMap Class** + - Used to store all possible commands for use with the `list commands` command + - Allows users to quickly see a brief overview of all commands possible + - Hashmap allows fast retrieval of all information of a specific command in O(1) time +3. **Parameter enum** + - Used to store different parameters we use for different commands instead of magic literals + - Has methods for retrieving specific parameter values as well as throwing exceptions for bad formatting/ + missing values +4. **Enhanced add meal command** + - Modified extractAndAppendMeal to include optional parameters from the Parameter class + - Added exception handling for Bad formatting/ Missing values accompanied by a UI reply +5. **Weight timeline command** + - Made WeightEntryDisplay class to generate a graph of the last 10 weight entries +6. **Meal recommendations command** + - Made Recipe parent class and child recipe classes + - Made Recipe map to store recipes to return the right recipes based on user's health goal +6. **Unit Testing** + - Implemented all tests Parameters + - Implemented all tests UI + - Updated ChatParser tests + +### Contributions to the User Guide +Wrote documentation for the following commands including description, sample usage and console output +1. `list commands` +2. `save meal` +3. `add mealEntry` +4. `delete meal` +5. `meal menu` +6. `meal log` +7. `meal recommendations` +7. `delete mealEntry` +8. `weight timline` +9. Summary table + +### Contributions to the Developer Guide +- Added implementation details of the `CommandMap` class and `listCommands` +- Created UML diagrams for `askForUserData`, `addMeal`, `listCommands` + +### Contributions to team based tasks +- Setting up milestones and maintaining issues +- Enable assertions in build gradle +- Reminding of weekly deliverables and deadlines + +### Community +- Forum responses [1](https://github.com/nus-cs2113-AY2425S1/forum/issues/2#issuecomment-2294732154) \ No newline at end of file diff --git a/logs/seedu.healthmate.ChatParser.log b/logs/seedu.healthmate.ChatParser.log new file mode 100644 index 0000000000..0d13b27090 --- /dev/null +++ b/logs/seedu.healthmate.ChatParser.log @@ -0,0 +1,18 @@ +Nov 03, 2024 1:30:08 PM seedu.healthmate.ChatParser +INFO: Initializing HistoryTracker +Nov 03, 2024 1:30:08 PM seedu.healthmate.ChatParser +INFO: Loaded MealEntries +Nov 03, 2024 1:30:08 PM seedu.healthmate.ChatParser +INFO: Loaded MealOptions +Nov 03, 2024 1:30:08 PM seedu.healthmate.ChatParser run +INFO: Checking if user data exists +Nov 03, 2024 1:30:08 PM seedu.healthmate.ChatParser run +INFO: Getting next user input line +Nov 03, 2024 1:30:11 PM seedu.healthmate.ChatParser multiCommandParsing +INFO: User command is: add mealEntry +Nov 03, 2024 1:30:11 PM seedu.healthmate.ChatParser multiCommandParsing +INFO: Executing command to add a meal to mealEntries +Nov 03, 2024 1:30:11 PM seedu.healthmate.ChatParser run +INFO: User input contains more than 1 token +Nov 03, 2024 1:30:11 PM seedu.healthmate.ChatParser run +INFO: Getting next user input line diff --git a/logs/seedu.healthmate.ChatParser.log.lck b/logs/seedu.healthmate.ChatParser.log.lck new file mode 100644 index 0000000000..e69de29bb2 diff --git a/logs/seedu.healthmate.services.ChatParser.log b/logs/seedu.healthmate.services.ChatParser.log new file mode 100644 index 0000000000..ce4f833339 --- /dev/null +++ b/logs/seedu.healthmate.services.ChatParser.log @@ -0,0 +1,32 @@ +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser +INFORMATION: Initialized HistoryTracker +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealEntries +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealOptions +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser +INFORMATION: Initializing UserHistoryTracker +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser +INFORMATION: ChatParser correctly initialized. +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser run +INFORMATION: Checking if user data exists +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser run +INFORMATION: User is: 180.0,80.0,true,20,BULKING,2674.0,2024-11-11 21:46:53,true +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: show todayCalories +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Start multicCommandParsing +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Main Command: show todayCalories Other: [Ljava.lang.String;@16293aa2 +Nov. 12, 2024 9:28:00 AM seedu.healthmate.command.commands.TodayCalorieProgressCommand executeCommands +INFORMATION: Executing command to print daily progress bar +Nov. 12, 2024 9:28:00 AM seedu.healthmate.command.commands.TodayCalorieProgressCommand executeCommands +INFORMATION: Finish executing command to print daily progress bar +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: bye +Nov. 12, 2024 9:28:00 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User closes application diff --git a/logs/seedu.healthmate.services.ChatParser.log.1 b/logs/seedu.healthmate.services.ChatParser.log.1 new file mode 100644 index 0000000000..9dd1a8b767 --- /dev/null +++ b/logs/seedu.healthmate.services.ChatParser.log.1 @@ -0,0 +1,83 @@ +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser +INFORMATION: Initialized HistoryTracker +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealEntries +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealOptions +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser +INFORMATION: Initializing UserHistoryTracker +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser +INFORMATION: ChatParser correctly initialized. +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser run +INFORMATION: Checking if user data exists +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser run +INFORMATION: User is: 180.0,80.0,true,20,BULKING,2865.603,2024-11-09 20:30:46,true +Nov. 11, 2024 8:38:45 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 11, 2024 8:38:58 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: log meals +Nov. 11, 2024 8:38:58 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Start multicCommandParsing +Nov. 11, 2024 8:38:58 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Main Command: log meals Other: [Ljava.lang.String;@31221be2 +Nov. 11, 2024 8:38:58 AM seedu.healthmate.command.commands.LogMealsCommand executeCommand +INFORMATION: Executing command to show meal history +Nov. 11, 2024 8:38:58 AM seedu.healthmate.command.commands.LogMealsCommand executeCommand +INFORMATION: Finish executing command to show meal history +Nov. 11, 2024 8:38:58 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 11, 2024 8:39:15 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: log meals +Nov. 11, 2024 8:39:15 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Start multicCommandParsing +Nov. 11, 2024 8:39:15 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Main Command: log meals Other: [Ljava.lang.String;@68de145 +Nov. 11, 2024 8:39:15 AM seedu.healthmate.command.commands.LogMealsCommand executeCommand +INFORMATION: Executing command to show meal history +Nov. 11, 2024 8:39:15 AM seedu.healthmate.command.commands.LogMealsCommand executeCommand +INFORMATION: Finish executing command to show meal history +Nov. 11, 2024 8:39:15 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 11, 2024 8:39:26 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: show historicCalories +Nov. 11, 2024 8:39:26 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Start multicCommandParsing +Nov. 11, 2024 8:39:26 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Main Command: show historicCalories Other: [Ljava.lang.String;@27fa135a +Nov. 11, 2024 8:39:26 AM seedu.healthmate.command.commands.HistoricCalorieProgressCommand executeCommand +INFORMATION: Executing command to print historic calorie bar +Nov. 11, 2024 8:39:26 AM seedu.healthmate.command.commands.HistoricCalorieProgressCommand executeCommand +INFORMATION: Finish executing command to print historic calorie bar +Number of past days entered: +Nov. 11, 2024 8:39:26 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 11, 2024 8:39:33 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: show historicCalories -1 +Nov. 11, 2024 8:39:33 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Start multicCommandParsing +Nov. 11, 2024 8:39:33 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Main Command: show historicCalories Other: [Ljava.lang.String;@506c589e +Nov. 11, 2024 8:39:33 AM seedu.healthmate.command.commands.HistoricCalorieProgressCommand executeCommand +INFORMATION: Executing command to print historic calorie bar +Nov. 11, 2024 8:39:33 AM seedu.healthmate.command.commands.HistoricCalorieProgressCommand executeCommand +INFORMATION: Finish executing command to print historic calorie bar +Number of past days entered: +Nov. 11, 2024 8:39:33 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 11, 2024 8:39:38 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: show historicCalories 10 +Nov. 11, 2024 8:39:38 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Start multicCommandParsing +Nov. 11, 2024 8:39:38 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Main Command: show historicCalories Other: [Ljava.lang.String;@446cdf90 +Nov. 11, 2024 8:39:38 AM seedu.healthmate.command.commands.HistoricCalorieProgressCommand executeCommand +INFORMATION: Executing command to print historic calorie bar +Nov. 11, 2024 8:39:38 AM seedu.healthmate.command.commands.HistoricCalorieProgressCommand executeCommand +INFORMATION: Finish executing command to print historic calorie bar +Number of past days entered: 10 +Nov. 11, 2024 8:39:38 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 11, 2024 8:39:43 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input is: bye +Nov. 11, 2024 8:39:43 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User closes application diff --git a/logs/seedu.healthmate.services.ChatParser.log.2 b/logs/seedu.healthmate.services.ChatParser.log.2 new file mode 100644 index 0000000000..30b0333796 --- /dev/null +++ b/logs/seedu.healthmate.services.ChatParser.log.2 @@ -0,0 +1,22 @@ +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser +INFORMATION: Initializing HistoryTracker +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealEntries +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealOptions +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser +INFORMATION: Initializing UserHistoryTracker +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser run +INFORMATION: Checking if user data exists +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: User commands are: Pair of add mealEntry, [Ljava.lang.String;@3d74bf60 +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser multiCommandParsing +INFORMATION: Executing command to add a meal to mealEntries +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User input contains more than 1 token +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: Getting next user input line +Nov. 05, 2024 10:36:43 AM seedu.healthmate.services.ChatParser parseUserInput +INFORMATION: User closes application diff --git a/seedu.healthmate.ChatParser.log b/seedu.healthmate.ChatParser.log new file mode 100644 index 0000000000..8a72e8eabb --- /dev/null +++ b/seedu.healthmate.ChatParser.log @@ -0,0 +1,12 @@ +Okt. 16, 2024 3:47:16 PM seedu.healthmate.services.ChatParser +INFORMATION: Initializing HistoryTracker +Okt. 16, 2024 3:47:16 PM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealEntries +Okt. 16, 2024 3:47:16 PM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealOptions +Okt. 16, 2024 3:47:16 PM seedu.healthmate.services.ChatParser run +INFORMATION: Checking if user data exists +Okt. 16, 2024 3:47:16 PM seedu.healthmate.services.ChatParser run +INFORMATION: Getting next user input line +Okt. 16, 2024 3:47:23 PM seedu.healthmate.services.ChatParser run +INFORMATION: User closes application 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/seedu/healthmate/HealthMate.java b/src/main/java/seedu/healthmate/HealthMate.java new file mode 100644 index 0000000000..bfe7579288 --- /dev/null +++ b/src/main/java/seedu/healthmate/HealthMate.java @@ -0,0 +1,15 @@ +package seedu.healthmate; + +import seedu.healthmate.services.ChatParser; +import seedu.healthmate.services.UI; + +public class HealthMate { + private static ChatParser chatParser = new ChatParser(); + + public static void main(String[] args) { + UI.printGreeting(); + chatParser.run(); + + } +} + diff --git a/src/main/java/seedu/healthmate/command/Command.java b/src/main/java/seedu/healthmate/command/Command.java new file mode 100644 index 0000000000..49315898fc --- /dev/null +++ b/src/main/java/seedu/healthmate/command/Command.java @@ -0,0 +1,55 @@ +package seedu.healthmate.command; + +/** + * Represents an abstract command with a specific command string, format, and description. + * This class serves as a base for creating different types of commands, each having its own format + * and description for user interaction. + */ +public abstract class Command { + public static final String INDENTATION = " "; + + public final String command; + private String commandLower; + private final String format; + private final String description; + + /** + * Constructs a Command instance with the specified details. + * + * @param command The command string that triggers the command. + * @param format The format for using the command. + * @param description A description of what the command does. + */ + public Command(String command, String format, String description) { + this.format = format; + this.command = command; + this.description = description; + } + + /** + * Returns a string representation of the command, including its name, format, and description. + * + * @return A formatted string containing the command details. + */ + @Override + public String toString() { + return "Command: " + command + "\n" + + INDENTATION + "Format: " + format + "\n" + + INDENTATION + "Description: " + description; + } + + public String shortDescription() { + return command + " \n " + INDENTATION + format; + } + + /** + * Returns the command string. + * + * @return The command string. + */ + public String getCommand() { + return command; + } + + +} diff --git a/src/main/java/seedu/healthmate/command/CommandMap.java b/src/main/java/seedu/healthmate/command/CommandMap.java new file mode 100644 index 0000000000..cce819dc10 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/CommandMap.java @@ -0,0 +1,89 @@ +package seedu.healthmate.command; + +import seedu.healthmate.command.commands.AddMealEntryCommand; +import seedu.healthmate.command.commands.DeleteMealCommand; +import seedu.healthmate.command.commands.DeleteMealEntryCommand; +import seedu.healthmate.command.commands.HistoricCalorieProgressCommand; +import seedu.healthmate.command.commands.ListCommandsCommand; +import seedu.healthmate.command.commands.MealLogCommand; +import seedu.healthmate.command.commands.MealMenuCommand; +import seedu.healthmate.command.commands.SaveMealCommand; +import seedu.healthmate.command.commands.TodayCalorieProgressCommand; +import seedu.healthmate.command.commands.UpdateUserDataCommand; +import seedu.healthmate.command.commands.CurrentUserDataCommand; +import seedu.healthmate.command.commands.MealRecommendationsCommand; +import seedu.healthmate.command.commands.WeightTimelineCommand; +import seedu.healthmate.command.commands.ByeCommand; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + + +public class CommandMap { + private static final Logger logger = Logger.getLogger(CommandMap.class.getName()); + private static final Map COMMANDSMAP = new LinkedHashMap<>(); + + static { + COMMANDSMAP.put(UpdateUserDataCommand.COMMAND_LOWER, new UpdateUserDataCommand()); + COMMANDSMAP.put(CurrentUserDataCommand.COMMAND_LOWER, new CurrentUserDataCommand()); + + COMMANDSMAP.put(ListCommandsCommand.COMMAND_LOWER, new ListCommandsCommand()); + + COMMANDSMAP.put(MealLogCommand.COMMAND_LOWER, new MealLogCommand()); + COMMANDSMAP.put(AddMealEntryCommand.COMMAND_LOWER, new AddMealEntryCommand()); + COMMANDSMAP.put(DeleteMealEntryCommand.COMMAND_LOWER, new DeleteMealEntryCommand()); + + COMMANDSMAP.put(MealMenuCommand.COMMAND_LOWER, new MealMenuCommand()); + COMMANDSMAP.put(SaveMealCommand.COMMAND_LOWER, new SaveMealCommand()); + COMMANDSMAP.put(DeleteMealCommand.COMMAND_LOWER, new DeleteMealCommand()); + + COMMANDSMAP.put(TodayCalorieProgressCommand.COMMAND_LOWER, new TodayCalorieProgressCommand()); + COMMANDSMAP.put(HistoricCalorieProgressCommand.COMMAND_LOWER, new HistoricCalorieProgressCommand()); + + COMMANDSMAP.put(MealRecommendationsCommand.COMMAND_LOWER, new MealRecommendationsCommand()); + COMMANDSMAP.put(WeightTimelineCommand.COMMAND_LOWER, new WeightTimelineCommand()); + + COMMANDSMAP.put(ByeCommand.COMMAND_LOWER, new ByeCommand()); + } + + /** + * Retrieves a list of commands based on user input and specified command. + * If the user input matches the command exactly, returns all commands. + * Otherwise, returns a list with a single command matching the user input, + * or all commands if no specific command is found. + * + * @param userInput The input provided by the user. + * @param command The command to check against. + * @return A list of matching commands. + */ + // Retrieve a command by its name + public static List getCommands(String userInput, String command) { + if(userInput.equalsIgnoreCase(command)) { + return getAllCommands(); + } + String commandToFind = userInput.substring(command.length()).trim(); + List commands = new ArrayList<>(); + if(!COMMANDSMAP.containsKey(commandToFind.toLowerCase())) { + return commands; + } + commands.add(getCommandByName(commandToFind.toLowerCase())); + return commands; + + } + + private static Command getCommandByName(String commandName) { + return COMMANDSMAP.get(commandName); + } + + private static List getAllCommands() { + assert !COMMANDSMAP.isEmpty() : "Command map should not be empty"; + List commands = new ArrayList<>(COMMANDSMAP.values()); + + logger.info("Retrieved " + commands.size() + " commands from the CommandMap"); + + return commands; + } +} diff --git a/src/main/java/seedu/healthmate/command/CommandPair.java b/src/main/java/seedu/healthmate/command/CommandPair.java new file mode 100644 index 0000000000..507b4f7ef2 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/CommandPair.java @@ -0,0 +1,38 @@ +package seedu.healthmate.command; + +import seedu.healthmate.utils.Pair; + +/** + * Represents a combination of commands with a hierarchy of a main command and additional subcommands. + * Extends {@code Pair} with the first element as the main command and + * the second as an array of subcommands. + */ +public class CommandPair extends Pair { + + public CommandPair(String twoTokenCommand, String[] additionalCommands){ + super(twoTokenCommand, additionalCommands); + } + + /** + * Returns the main command. + * @return main command as a {@code String} + */ + public String getMainCommand() { + return super.t(); + } + + /** + * Returns the subcommand at the specified index. + * @param index the index of the subcommand + * @return subcommand at the specified index + */ + public String getCommandByIndex(int index) { + return super.u()[index]; + } + + @Override + public String toString() { + return "Main Command: " + super.t() + " Other: " + super.u().toString(); + } + +} diff --git a/src/main/java/seedu/healthmate/command/commands/AddMealEntryCommand.java b/src/main/java/seedu/healthmate/command/commands/AddMealEntryCommand.java new file mode 100644 index 0000000000..77d20b0b2a --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/AddMealEntryCommand.java @@ -0,0 +1,71 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.MealList; +import seedu.healthmate.core.User; +import seedu.healthmate.services.HistoryTracker; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to add a meal entry to the user's meal log. + * The meal entry can be added from an existing meal in the meal menu or + * as a new meal with a specified number of calories. + */ +public class AddMealEntryCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "add mealEntry"; + public static final String COMMAND_LOWER = "add mealentry"; + /** Command format for adding a meal entry with proper parameters. */ + private static final String FORMAT = + "add mealEntry {meal name from menu} OR add mealEntry [{name}] /c{calories} [/p{portions}] [/t{Date in " + + "YYYY-MM-DD}]"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = + "Adds a meal entry to the meal log either from a pre-existing meal in the meal menu \n" + + INDENTATION + "or a new meal with a specified amount of calories, portions and date.\n" + + INDENTATION + "Optional parameters (name, portions, date) are indicated by []"; + + /** + * Constructs an {@code AddMealEntryCommand} object with a predefined command keyword, + * format, and description. + */ + public AddMealEntryCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the add meal entry command by appending a new meal entry to the user's meal log. + * The meal entry can be an existing meal from the meal options or a new meal entry with specified calories. + * Saves the updated meal entries to the history tracker. + * + * @param historyTracker The history tracker to save the updated meal entries. + * @param mealOptions The list of predefined meal options. + * @param mealEntries The list of meal entries to which the new meal will be added. + * @param user The user for whom the meal entry is being added. + * @param userInput The input provided by the user, containing the meal name and optional calories. + * @param command The specific command keyword issued by the user. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand( + HistoryTracker historyTracker, MealList mealOptions, MealEntriesList mealEntries, + User user, String userInput, String command, Logger logger) { + + assert mealOptions != null : "Meal options list should not be null"; + assert mealEntries != null : "Meal entries list should not be null"; + + logger.log(Level.INFO, "Executing command to add a meal to mealEntries" + System.lineSeparator() + + "Number of tracked meals is: " + mealEntries.size()); + + // Adds the meal entry and updates history + mealEntries.extractAndAppendMeal(userInput, command, mealOptions, user); + historyTracker.saveMealEntries(mealEntries); + + logger.log(Level.INFO, "Finish executing command to add a meal to mealEntries" + System.lineSeparator() + + "Number of tracked meals is: " + mealEntries.size()); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/ByeCommand.java b/src/main/java/seedu/healthmate/command/commands/ByeCommand.java new file mode 100644 index 0000000000..1a8af4ec6d --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/ByeCommand.java @@ -0,0 +1,28 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; + + +public class ByeCommand extends Command { + /** Command keyword to invoke this action. */ + public static final String COMMAND = "bye"; + public static final String COMMAND_LOWER = "bye"; + + /** Command format for adding a meal entry with proper parameters. */ + private static final String FORMAT = + "bye"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = + "Exits the program"; + + /** + * Constructs an {@code AddMealEntryCommand} object with a predefined command keyword, + * format, and description. + */ + public ByeCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + +} diff --git a/src/main/java/seedu/healthmate/command/commands/ClearUserDataCommand.java b/src/main/java/seedu/healthmate/command/commands/ClearUserDataCommand.java new file mode 100644 index 0000000000..d5f05073b0 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/ClearUserDataCommand.java @@ -0,0 +1,55 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.UserEntryList; +import seedu.healthmate.services.UserHistoryTracker; +import seedu.healthmate.services.UI; + + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to clear the user's personal data. + */ +public class ClearUserDataCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "clear userdata"; + + public static final String COMMAND_LOWER = "clear userdata"; + + + /** Command format for clearing user data. */ + private static final String FORMAT = "clear userdata"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Clears all userdata"; + + /** + * Constructs an {@code ClearUserDataCommand} object with a predefined command keyword, + * format, and description. + */ + public ClearUserDataCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the clear user data command deleting all file data + * Logs the command execution and asserts that the new user data is valid. + * + * @param userHistoryTracker The user history tracker used to save and display updated user information. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(UserHistoryTracker userHistoryTracker, Logger logger) { + logger.log(Level.INFO, "Executing command to update user data"); + + userHistoryTracker.clearSaveFile(); + Optional updatedUserEntryList = userHistoryTracker.loadUserEntries(); + assert updatedUserEntryList.isEmpty() : "New user entry list should be empty"; + + UI.printReply("User Data has been cleared!", "Success: "); + logger.log(Level.INFO, "Finish executing command to clear user data"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/CurrentUserDataCommand.java b/src/main/java/seedu/healthmate/command/commands/CurrentUserDataCommand.java new file mode 100644 index 0000000000..67f180814c --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/CurrentUserDataCommand.java @@ -0,0 +1,60 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.User; +import seedu.healthmate.services.UI; +import seedu.healthmate.services.UserHistoryTracker; + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to update the user's personal data. + * This command triggers prompts to gather and update the user's height, weight, gender, age, and health goal. + */ +public class CurrentUserDataCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "current userdata"; + public static final String COMMAND_LOWER = "current userdata"; + /** Command format for updating user data. */ + private static final String FORMAT = "current userdata"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Prints most recent userdata or error if none found"; + + /** + * Constructs an {@code CurrentUserDataCommand} object with a predefined command keyword, + * format, and description. + */ + public CurrentUserDataCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the update user data command by triggering prompts to collect new data for + * the user's profile, including height, weight, gender, age, and health goal. + * Logs the command execution and asserts that the new user data is valid. + * + * @param userHistoryTracker The user history tracker used to save and display updated user information. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(UserHistoryTracker userHistoryTracker, Logger logger) { + Optional lastUser = userHistoryTracker.getLatestUser(); + logger.log(Level.INFO, "Executing command to return current user data"); + + // Prompts the user to enter new data + lastUser.ifPresentOrElse( + user -> { + UI.printSeparator(); + UI.printString("Here is your current user data:"); + user.printUIString(); + UI.printSeparator(); + }, + () -> UI.printReply("Hit ENTER to set up a new user.", "") + ); + + logger.log(Level.INFO, "Finish executing command to return current user data"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/DeleteMealCommand.java b/src/main/java/seedu/healthmate/command/commands/DeleteMealCommand.java new file mode 100644 index 0000000000..18fa373ba2 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/DeleteMealCommand.java @@ -0,0 +1,64 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealList; +import seedu.healthmate.services.HistoryTracker; +import seedu.healthmate.services.UI; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to delete a meal option from the meal menu. + * The meal is deleted based on its specified index in the meal menu. + */ +public class DeleteMealCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "delete meal"; + public static final String COMMAND_LOWER = "delete meal"; + /** Command format for deleting a meal option by specifying its index. */ + private static final String FORMAT = COMMAND + " {index of meal in meal menu}"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Deletes meal option at the specified index from the meal menu"; + + /** + * Constructs a {@code DeleteMealCommand} object with a predefined command keyword, + * format, and description. + */ + public DeleteMealCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the delete meal command by removing a meal option from the meal menu at the specified index. + * Saves the updated meal options list to the history tracker. + * + * @param historyTracker The history tracker to save the updated meal options. + * @param mealOptions The list of meal options from which the specified meal will be deleted. + * @param userInput The input provided by the user, containing the index of the meal to be deleted. + * @param command The specific command keyword issued by the user. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand( + HistoryTracker historyTracker, MealList mealOptions, + String userInput, String command, Logger logger) { + + assert mealOptions != null : "Meal options list should not be null"; + + logger.log(Level.INFO, "Executing command to delete a meal from meal options." + System.lineSeparator() + + "Number of meal options is: " + mealOptions.size()); + + if (mealOptions.size() <= 0) { + UI.printReply("No Meal Options", "Error: "); + return; + } + + // Removes the specified meal from the meal options and updates history + mealOptions.extractAndRemoveMeal(userInput, command); + historyTracker.saveMealOptions(mealOptions); + logger.log(Level.INFO, "Finished executing command to delete a meal." + System.lineSeparator() + + "Number of meal options is: " + mealOptions.size()); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/DeleteMealEntryCommand.java b/src/main/java/seedu/healthmate/command/commands/DeleteMealEntryCommand.java new file mode 100644 index 0000000000..e7f0dd4865 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/DeleteMealEntryCommand.java @@ -0,0 +1,67 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.User; +import seedu.healthmate.services.HistoryTracker; +import seedu.healthmate.services.UI; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to delete a meal entry from the user's meal log. + * The meal entry is deleted based on its specified index in the meal log. + */ +public class DeleteMealEntryCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "delete mealEntry"; + public static final String COMMAND_LOWER = "delete mealentry"; + /** Command format for deleting a meal entry by specifying its index in the meal log. */ + private static final String FORMAT = COMMAND + " {index of meal in the meal log}"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Used to delete a meal at a specified index in the meal log"; + + /** + * Constructs a {@code DeleteMealEntryCommand} object with a predefined command keyword, + * format, and description. + */ + public DeleteMealEntryCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the delete meal entry command by removing a meal entry from the user's meal log + * at the specified index. Saves the updated meal entries list to the history tracker. + * + * @param historyTracker The history tracker to save the updated meal entries. + * @param mealEntries The list of meal entries from which the specified meal entry will be deleted. + * @param user The user whose meal entry is being deleted. + * @param userInput The input provided by the user, containing the index of the meal entry to be deleted. + * @param command The specific command keyword issued by the user. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand( + HistoryTracker historyTracker, MealEntriesList mealEntries, + User user, String userInput, String command, Logger logger) { + + assert mealEntries != null : "Meal entries list should not be null"; + + logger.log(Level.INFO, "Executing command to delete a tracked meal." + System.lineSeparator() + + "Number of meals tracked is: " + mealEntries.size()); + + if (mealEntries.size() <= 0) { + UI.printReply("No Meal Entries", "Error: "); + return; + } + + // Removes the specified meal entry from the meal log and updates history + mealEntries.extractAndRemoveMeal(userInput, command, user); + historyTracker.saveMealEntries(mealEntries); + + logger.log(Level.INFO, "Finish executing command to delete a tracked meal." + System.lineSeparator() + + "Number of meals tracked is: " + mealEntries.size()); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/HistoricCalorieProgressCommand.java b/src/main/java/seedu/healthmate/command/commands/HistoricCalorieProgressCommand.java new file mode 100644 index 0000000000..2f0c4dc1f9 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/HistoricCalorieProgressCommand.java @@ -0,0 +1,87 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.command.CommandPair; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.User; +import seedu.healthmate.services.UI; + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to display historical calorie progress by showing calorie progress bars and statistics + * for a specified number of days including today. + */ +public class HistoricCalorieProgressCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "show historicCalories"; + public static final String COMMAND_LOWER = "show historiccalories"; + /** Command format specifying the number of days to include in the historic calorie progress. */ + private static final String FORMAT = "show historicCalories {Number of Days inclu. Today}"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = + "Prints Calorie Progress Bars & Various Stats to represent Historical Calorie Progress"; + + /** + * Constructs a {@code HistoricCalorieProgressCommand} object with a predefined command keyword, + * format, and description. + */ + public HistoricCalorieProgressCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the historic calorie progress command by displaying calorie progress bars and statistics + * for a specified number of days including today. If a valid number of days is specified, the command + * shows historical calorie data. + * + * @param mealEntries The list of meal entries to use for displaying calorie progress. + * @param commandPair The command pair containing the main command and additional parameters. + * @param user The user whose calorie progress is being displayed. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand( + MealEntriesList mealEntries, CommandPair commandPair, User user, Logger logger) { + + assert mealEntries != null : "Meal entries list should not be null"; + + logger.log(Level.INFO, "Executing command to print historic calorie bar"); + + Optional pastDays = parseDaysFromCommand(commandPair, 0); + + pastDays.ifPresent(days -> mealEntries.printHistoricConsumptionBars(user, days)); + + logger.log(Level.INFO, "Finish executing command to print historic calorie bar" + System.lineSeparator() + + "Number of past days entered: " + pastDays.map(integer -> integer.toString()).orElse("")); + } + + /** + * Attempts to parse a specific command token as an integer, representing the number of days for + * which calorie progress should be displayed. + * + * @param commandPair The command pair containing the commands and additional parameters. + * @param index The index of the parameter within the additional commands array. + * @return An {@code Optional} representing the number of days, if parsed successfully. + */ + private static Optional parseDaysFromCommand(CommandPair commandPair, int index) { + assert commandPair != null : "CommandPair should not be null"; + assert index >= 0 : "Index should be non-negative"; + + try { + int days = Integer.parseInt(commandPair.getCommandByIndex(index)); + if (days <= 0) { + throw new NumberFormatException(); + } + return Optional.of(days); + } catch (NumberFormatException e) { + UI.printReply(commandPair.getCommandByIndex(index), "The following is not a valid day count: "); + } catch (IndexOutOfBoundsException s) { + UI.printReply("Specify the number of days you want to look into the past", "Missing input: "); + } + return Optional.empty(); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/ListCommandsCommand.java b/src/main/java/seedu/healthmate/command/commands/ListCommandsCommand.java new file mode 100644 index 0000000000..bc6eb3e4c5 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/ListCommandsCommand.java @@ -0,0 +1,49 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.command.CommandMap; +import seedu.healthmate.services.UI; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to list all available commands in the application. + */ +public class ListCommandsCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "list commands"; + public static final String COMMAND_LOWER = "list commands"; + /** Command format for listing all available commands. */ + private static final String FORMAT = "list commands"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Lists out all available commands"; + + /** + * Constructs a {@code ListCommandsCommand} object with a predefined command keyword, + * format, and description. + */ + public ListCommandsCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the list commands command by retrieving and displaying all available commands + * to the user. Logs the command execution and asserts that the commands list is not null. + * + * @param userInput The input provided by the user. + * @param command The specific command keyword issued by the user. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(String userInput, String command, Logger logger) { + List commands = CommandMap.getCommands(userInput, command); + assert commands != null : "Commands list should not be null"; + + logger.log(Level.INFO, "Executing command to show all available commands"); + UI.printCommands(commands); + logger.log(Level.INFO, "Finish executing command to show all available commands"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/MealLogCommand.java b/src/main/java/seedu/healthmate/command/commands/MealLogCommand.java new file mode 100644 index 0000000000..78b87b0991 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/MealLogCommand.java @@ -0,0 +1,48 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.services.UI; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to display the log of all meal entries. + * Each meal entry is shown with its timestamp in date-time format. + */ +public class MealLogCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "meal log"; + public static final String COMMAND_LOWER = "meal log"; + /** Command format for displaying the meal log. */ + private static final String FORMAT = "meal log"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = + "Displays the log of all meal entries along with their Timestamp in Date Time format"; + + /** + * Constructs a {@code LogMealsCommand} object with a predefined command keyword, + * format, and description. + */ + public MealLogCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the log meals command by displaying the user's meal log. + * Logs the command execution and asserts that the meal entries list is not null. + * + * @param mealEntries The list of meal entries to display. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(MealEntriesList mealEntries, Logger logger) { + assert mealEntries != null : "Meal entries list should not be null"; + + logger.log(Level.INFO, "Executing command to show meal history"); + UI.printMealEntries(mealEntries); // Displays the log of all meal entries + logger.log(Level.INFO, "Finish executing command to show meal history"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/MealMenuCommand.java b/src/main/java/seedu/healthmate/command/commands/MealMenuCommand.java new file mode 100644 index 0000000000..d9d96ace8f --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/MealMenuCommand.java @@ -0,0 +1,46 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealList; +import seedu.healthmate.services.UI; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to display the menu of previously saved food items along with their calorie counts. + */ +public class MealMenuCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "meal menu"; + public static final String COMMAND_LOWER = "meal menu"; + /** Command format for displaying the meal menu. */ + private static final String FORMAT = "meal menu"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Displays the menu of previously saved food along with their calories"; + + /** + * Constructs a {@code MealMenuCommand} object with a predefined command keyword, + * format, and description. + */ + public MealMenuCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the meal menu command by displaying the list of saved food options along with their calorie information. + * Logs the command execution and asserts that the meal options list is not null. + * + * @param mealOptions The list of meal options to display. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(MealList mealOptions, Logger logger) { + assert mealOptions != null : "Meal options list should not be null"; + + logger.log(Level.INFO, "Executing meal menu command to show meal options. "); + UI.printMealOptions(mealOptions); + logger.log(Level.INFO, "Executing meal menu command to show meal options"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/MealRecommendationsCommand.java b/src/main/java/seedu/healthmate/command/commands/MealRecommendationsCommand.java new file mode 100644 index 0000000000..f37d3effdd --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/MealRecommendationsCommand.java @@ -0,0 +1,54 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.User; +import seedu.healthmate.recommender.Recipe; +import seedu.healthmate.recommender.RecipeMap; +import seedu.healthmate.services.UI; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to display meal recommendations based on the user's health goal. + */ +public class MealRecommendationsCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "meal recommendations"; + public static final String COMMAND_LOWER = "meal recommendations"; + /** Command format for displaying meal recommendations. */ + private static final String FORMAT = "meal recommendations"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Displays meal recommendations based on User's health goal"; + + /** + * Constructs a {@code MealRecommendationsCommand} object with a predefined command keyword, + * format, and description. + */ + public MealRecommendationsCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the meal recommendations command by displaying meal recommendations that align with + * the user's health goal. Retrieves relevant recipes from the RecipeMap and displays them. + * Logs the command execution and asserts that the user's health goal and recipes list are valid. + * + * @param user The user for whom meal recommendations are being generated. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(User user, Logger logger) { + assert user.getHealthGoal() != null : "User health goal should not be null"; + + // Retrieves and verifies recipes based on the user's health goal + List recipes = RecipeMap.getRecipesByGoal(user.getHealthGoal()); + assert recipes != null && !recipes.isEmpty() : "Recipes should not be null or empty"; + + logger.log(Level.INFO, "Executing command to list meal recommendation"); + UI.printRecommendation(recipes); + logger.log(Level.INFO, "Finish executing command to list meal recommendation"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/SaveMealCommand.java b/src/main/java/seedu/healthmate/command/commands/SaveMealCommand.java new file mode 100644 index 0000000000..ff990e5d5c --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/SaveMealCommand.java @@ -0,0 +1,63 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.Meal; +import seedu.healthmate.core.MealList; +import seedu.healthmate.services.HistoryTracker; +import seedu.healthmate.services.MealSaver; + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to save a meal to the meal menu for future use with the "add mealEntry" command. + * This command allows users to save meal details, including the name and calorie count. + */ +public class SaveMealCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "save meal"; + public static final String COMMAND_LOWER = "save meal"; + /** Command format for saving a meal with a specified name and calorie count. */ + private static final String FORMAT = "save meal {meal name} /c{number of calories}"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = + "Saves a meal to the meal menu for later use with the add mealEntry command"; + + /** + * Constructs a {@code SaveMealCommand} object with a predefined command keyword, + * format, and description. + */ + public SaveMealCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the save meal command by extracting meal details from the user input and + * saving the meal to the meal menu. Logs the command execution and asserts that the necessary + * components are available and valid. + * + * @param historyTracker The history tracker used to save the meal to persistent storage. + * @param mealOptions The list of meal options to which the new meal will be added. + * @param userInput The input provided by the user, containing the meal name and calorie count. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand( + HistoryTracker historyTracker, MealList mealOptions, String userInput, Logger logger) { + + assert historyTracker != null : "HistoryTracker should not be null"; + logger.log(Level.INFO, "Executing command to save a meal to meal options." + System.lineSeparator() + + "Number of meal options is: " + mealOptions.size()); + + // Initializes MealSaver and extracts meal details from user input + MealSaver mealSaver = new MealSaver(historyTracker); + Optional mealToSave = mealSaver.extractMealFromUserInput(userInput); + + // Saves the meal to the meal options list if valid + mealToSave.ifPresent(meal -> mealSaver.saveMeal(meal, mealOptions)); + logger.log(Level.INFO, "Finished executing save meal command to save the (optional) meal: " + mealToSave + + System.lineSeparator() + "Number of meal options is: " + mealOptions.size()); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/TodayCalorieProgressCommand.java b/src/main/java/seedu/healthmate/command/commands/TodayCalorieProgressCommand.java new file mode 100644 index 0000000000..ad7e931d68 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/TodayCalorieProgressCommand.java @@ -0,0 +1,50 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.User; + +import java.time.LocalDateTime; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to display today's calorie progress by printing a calorie progress bar + * that reflects the user's calorie consumption for the current day. + */ +public class TodayCalorieProgressCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "show todayCalories"; + public static final String COMMAND_LOWER = "show todaycalories"; + /** Command format for displaying today's calorie progress. */ + private static final String FORMAT = "show todayCalories"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Prints a Calorie Progress Bar to represent Today Calorie Progress"; + + /** + * Constructs a {@code TodayCalorieProgressCommand} object with a predefined command keyword, + * format, and description. + */ + public TodayCalorieProgressCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the today calorie progress command by displaying a calorie progress bar that + * shows the user's calorie consumption for the current day. Logs the command execution and + * asserts that the meal entries list is not null. + * + * @param mealEntries The list of meal entries used to calculate today's calorie consumption. + * @param user The user whose calorie progress is being displayed. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommands(MealEntriesList mealEntries, User user, Logger logger) { + assert mealEntries != null : "Meal entries list should not be null"; + + logger.log(Level.INFO, "Executing command to print daily progress bar"); + mealEntries.printDaysConsumptionBar(user, LocalDateTime.now()); + logger.log(Level.INFO, "Finish executing command to print daily progress bar"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/UpdateUserDataCommand.java b/src/main/java/seedu/healthmate/command/commands/UpdateUserDataCommand.java new file mode 100644 index 0000000000..5f059738a0 --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/UpdateUserDataCommand.java @@ -0,0 +1,52 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.User; +import seedu.healthmate.services.UserHistoryTracker; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to update the user's personal data. + * This command triggers prompts to gather and update the user's height, weight, gender, age, and health goal. + */ +public class UpdateUserDataCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "update userdata"; + public static final String COMMAND_LOWER = "update userdata"; + /** Command format for updating user data. */ + private static final String FORMAT = "update userdata"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "Triggers prompts for asking height, weight, gender, age and health goal"; + + /** + * Constructs an {@code UpdateUserDataCommand} object with a predefined command keyword, + * format, and description. + */ + public UpdateUserDataCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the update user data command by triggering prompts to collect new data for + * the user's profile, including height, weight, gender, age, and health goal. + * Logs the command execution and asserts that the new user data is valid. + * + * @param userHistoryTracker The user history tracker used to save and display updated user information. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(UserHistoryTracker userHistoryTracker, Logger logger) { + logger.log(Level.INFO, "Executing command to update user data"); + + // Prompts the user to enter new data + User newUser = User.askForUserData(); + assert newUser != null : "New user data should not be null"; + + // Displays all user entries after update + userHistoryTracker.printAllUserEntries(); + logger.log(Level.INFO, "Finish executing command to update user data"); + } +} diff --git a/src/main/java/seedu/healthmate/command/commands/WeightTimelineCommand.java b/src/main/java/seedu/healthmate/command/commands/WeightTimelineCommand.java new file mode 100644 index 0000000000..a7cd79bbfb --- /dev/null +++ b/src/main/java/seedu/healthmate/command/commands/WeightTimelineCommand.java @@ -0,0 +1,53 @@ +package seedu.healthmate.command.commands; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.UserEntryList; +import seedu.healthmate.services.UserHistoryTracker; +import seedu.healthmate.core.WeightEntryDisplay; + +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to view a timeline of the user's last 10 weight updates. + * This command displays the weight entries in a timeline format. + */ +public class WeightTimelineCommand extends Command { + + /** Command keyword to invoke this action. */ + public static final String COMMAND = "weight timeline"; + public static final String COMMAND_LOWER = "weight timeline"; + /** Command format for displaying the weight timeline. */ + private static final String FORMAT = "weight timeline"; + + /** Description of the command functionality. */ + private static final String DESCRIPTION = "View a timeline of the last 10 weight updates"; + + /** + * Constructs a {@code WeightTimelineCommand} object with a predefined command keyword, + * format, and description. + */ + public WeightTimelineCommand() { + super(COMMAND, FORMAT, DESCRIPTION); + } + + /** + * Executes the weight timeline command by retrieving and displaying the user's weight history. + * Logs the command execution and asserts that the user history data is available and valid. + * + * @param userHistoryTracker The tracker that stores and manages the user's historical data. + * @param logger The logger used for logging command execution steps. + */ + public static void executeCommand(UserHistoryTracker userHistoryTracker, Logger logger) { + + // Retrieves and verifies user weight history data + Optional userHistoryData = userHistoryTracker.loadUserEntries(); + assert userHistoryData != null && userHistoryData.isPresent() : "User history data should not be null or empty"; + + // Displays the weight timeline if data is available + logger.log(Level.INFO, "Executing command to print weight timeline"); + WeightEntryDisplay.display(userHistoryData); + logger.log(Level.INFO, "Finish executing command to print weight timeline."); + } +} diff --git a/src/main/java/seedu/healthmate/core/HealthGoal.java b/src/main/java/seedu/healthmate/core/HealthGoal.java new file mode 100644 index 0000000000..dc564a5a0f --- /dev/null +++ b/src/main/java/seedu/healthmate/core/HealthGoal.java @@ -0,0 +1,128 @@ +package seedu.healthmate.core; + +import seedu.healthmate.services.UI; + +/** + * Manages a user's health goal (weight loss, steady state, bulking) and + * calculates target calorie intake based on user data and selected goal. + */ +public class HealthGoal { + + private static final String WEIGHT_LOSS = "WEIGHT_LOSS"; + private static final String STEADY_STATE = "STEADY_STATE"; + private static final String BULKING = "BULKING"; + + private static final double WEIGHT_LOSS_MODIFIER = 0.9; + private static final double STEADY_STATE_MODIFIER = 1.1; + private static final double BULKING_MODIFIER = 1.4; + + private static final String[] healthGoals = {WEIGHT_LOSS, STEADY_STATE, BULKING}; + + private String currentHealthGoal; + + + /** + * Constructor for HealthGoal. + * Initializes the health goal with the provided input. + * + * @param healthGoalInput the desired health goal. + */ + public HealthGoal(int healthGoalInput) { + saveHealthGoal(healthGoalInput); + } + + public HealthGoal(String healthGoalInput) { + saveHealthGoal(healthGoalInput); + } + + /** + * Saves the current health goal based on input. + * + * @param healthGoalInput the input health goal (e.g., WEIGHT_LOSS). + */ + public void saveHealthGoal(int healthGoalInput) { + + if (1 > healthGoalInput | healthGoalInput > 3) { + UI.printReply("Invalid Health Goal", "Save Health Goal Error: "); + return; + } + + currentHealthGoal = healthGoals[healthGoalInput - 1]; + + } + + public void saveHealthGoal(String healthGoalInput) { + if (healthGoalInput.equals("")) { + UI.printReply("Empty Health Goal", "Save Health Goal Error: "); + } + assert healthGoalInput != null : "Health goal input cannot be null"; + switch (healthGoalInput) { + case WEIGHT_LOSS: + this.currentHealthGoal = WEIGHT_LOSS; + break; + case STEADY_STATE: + this.currentHealthGoal = STEADY_STATE; + break; + case BULKING: + this.currentHealthGoal = BULKING; + break; + default: + UI.printReply("Invalid Health Goal", "Save Health Goal Error: "); + } + } + + /** + * Gets the current health goal. + * + * @return the current health goal as a String. + */ + public String getCurrentHealthGoal() { + return currentHealthGoal; + } + + /** + * Calculates the target calories based on user data and health goal. + * + * @param height the user's height in cm. + * @param weight the user's weight in kg. + * @param isMale true if the user is male, false if female. + * @param age the user's age. + * @return the target calories based on the health goal and user data. + */ + public int getTargetCalories(double height, double weight, boolean isMale, int age) { + assert height > 0 : "Height must be positive"; + assert weight > 0 : "Weight must be positive"; + assert age > 0 : "Age must be positive"; + + double rawCaloriesTarget; + if (isMale) { + rawCaloriesTarget = 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age); + } else { + rawCaloriesTarget = 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age); + } + + assert currentHealthGoal != null : "Current health goal must be set"; + + switch (currentHealthGoal) { + case WEIGHT_LOSS: + return (int)(rawCaloriesTarget * WEIGHT_LOSS_MODIFIER); + case STEADY_STATE: + return (int)(rawCaloriesTarget * STEADY_STATE_MODIFIER); + case BULKING: + return (int)(rawCaloriesTarget * BULKING_MODIFIER); + default: + return -1; + } + } + + /** + * Returns a string representation of the current health goal. + * + * @return the current health goal as a String. + */ + @Override + public String toString() { + assert currentHealthGoal != null : "Current health goal cannot be null when converting to string"; + return currentHealthGoal; + } +} diff --git a/src/main/java/seedu/healthmate/core/Meal.java b/src/main/java/seedu/healthmate/core/Meal.java new file mode 100644 index 0000000000..8a8fea64fb --- /dev/null +++ b/src/main/java/seedu/healthmate/core/Meal.java @@ -0,0 +1,91 @@ +package seedu.healthmate.core; + +import java.time.LocalDateTime; +import java.util.Optional; + +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.EmptyCalorieException; +import seedu.healthmate.utils.Parameter; + +public class Meal { + + public static final int MAX_DESCRIPTION_LENGTH = 50; + + private final Optional name; + private final int calories; + + public Meal(Optional name, int calories) { + this.name = name; + this.calories = calories; + } + + /** + * Extracts a Meal object from a string input. + * @param input The input string containing meal information + * @param command The command string to parse from + * @return A new Meal object with the extracted description and calories + * @throws EmptyCalorieException if no calorie value is specified + * @throws BadCalorieException if the calorie format is invalid + */ + public static Meal extractMealFromString(String input, + String command) throws EmptyCalorieException, BadCalorieException { + Optional mealDescription = extractMealDescription(input, command); + int calories = Parameter.getCalories(input); + Meal meal = new Meal(mealDescription, calories); + return meal; + } + + /** + * Extracts the meal description from the input string. + * @param input The input string containing the meal description + * @param command The command string to parse from + * @return An Optional containing the extracted meal description, or empty if none exists + */ + public static Optional extractMealDescription(String input, String command) { + int mealDescriptionIndex = input.indexOf(command) + command.length(); + int signallerIndex = input.indexOf(Parameter.EMPTY_SIGNALLER.getPrefix()); + if (signallerIndex == -1) { + signallerIndex = input.length(); + } + String mealDescrition = input.substring(mealDescriptionIndex, signallerIndex).trim().toLowerCase(); + if (mealDescrition.strip().length() > 0) { + return Optional.of(mealDescrition); + } else { + return Optional.empty(); + } + } + + public boolean descriptionIsEmpty() { + return this.name.orElse("").length() == 0; + } + + public boolean descriptionWithinMaxLength() { + return this.name.orElse("").length() <= MAX_DESCRIPTION_LENGTH; + } + + public boolean isBeforeEqualDate(LocalDateTime timestamp) { + return false; + } + + public boolean isAfterEqualDate(LocalDateTime timestamp) { + return false; + } + + public String toSaveString() { + return this.name.orElse("Meal") + "," + this.getCalories(); + } + + public Optional getName() { + return this.name; + } + + public int getCalories() { + return this.calories; + } + + @Override + public String toString() { + return this.name.orElse("Meal") + " with " + this.calories + " calories"; + } +} + diff --git a/src/main/java/seedu/healthmate/core/MealEntriesList.java b/src/main/java/seedu/healthmate/core/MealEntriesList.java new file mode 100644 index 0000000000..48897f2b2e --- /dev/null +++ b/src/main/java/seedu/healthmate/core/MealEntriesList.java @@ -0,0 +1,260 @@ +package seedu.healthmate.core; + + +import static seedu.healthmate.core.MealEntry.extractMealEntryFromString; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import seedu.healthmate.services.ConsumptionStatistics; +import seedu.healthmate.services.UI; +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.BadPortionException; +import seedu.healthmate.exceptions.BadTimestampException; +import seedu.healthmate.exceptions.EmptyCalorieException; +import seedu.healthmate.exceptions.MealNotFoundException; +import seedu.healthmate.utils.DateTimeUtils; +import seedu.healthmate.utils.Parameter; + + +public class MealEntriesList extends MealList { + + public MealEntriesList() { + super(); + } + + public MealEntriesList(ArrayList mealList) { + super(mealList); + } + + /** + * Creates a new mealEntry from user input and appends it to the list of mealEntries + * @param userInput the original user input + * @param command the command that triggered this action + * @param mealOptions the list of available presaved meals + * @param user the user's profile + */ + @Override + public void extractAndAppendMeal(String userInput, String command, MealList mealOptions, User user) { + try { + if (userInput.contains(",")) { + UI.printReply("No Commas Allowed", "Retry: "); + return; + } + int portions = Parameter.getPortions(userInput); + MealEntry meal = extractMealEntryFromString(userInput, command, mealOptions); + if (!meal.descriptionWithinMaxLength()) { + UI.printReply( + "Keep description to less than " + Meal.MAX_DESCRIPTION_LENGTH + " characters", + "Retry: "); + return; + } + addPortionsOfMeal(meal, portions); + printDaysConsumptionBar(user, meal.getTimestamp()); + } catch (EmptyCalorieException | BadCalorieException e) { + List messages = List.of("Every meal needs a calorie integer (e.g. /c120).", + "Note: The integer has to be within the range of 0 and 2147483647"); + UI.printMultiLineReply(messages); + } catch (StringIndexOutOfBoundsException s) { + UI.printReply("Do not forget to use /c{Integer} mark the following integer as calories", + "Retry: "); + } catch (MealNotFoundException e) { + List messages = List.of("Please save this meal to the meal menu first,", + "or use /c and /p to include calories and portion sizes"); + UI.printMultiLineReply(messages); + } catch (BadPortionException e) { + List messages = List.of("Retry: Please use a rightly formatted nonzero integer " + + "to specify portion size.", "E.g for 2 portions {/p2})."); + UI.printMultiLineReply(messages); + } catch (BadTimestampException e) { + UI.printReply("Please include a timestamp for your meal (e.g for 2024-10-30 {/t2024-10-30}).", + "Retry: "); + } + } + + /** + * Removes a meal from the list of tracked meal consumption + * Prints out visual feedback to highlight the + * resulting change interms of today's calorie consumption bar + * @param userInput The user input causing the this remove process + * @param command The identified command + * @param user The user's profile + */ + public void extractAndRemoveMeal(String userInput, String command, User user) { + try { + int mealNumber = Integer.parseInt(userInput.replaceAll(command, "").strip()); + LocalDateTime mealEntryDate = this.getDateOfMealEntry(mealNumber); + deleteMeal(mealNumber); + printDaysConsumptionBar(user, mealEntryDate); + } catch (NumberFormatException n) { + UI.printReply("Meal Entry index needs to be an integer", "Error: "); + } catch (IndexOutOfBoundsException s) { + UI.printReply("Meal Entry index needs to be within range", "Error: "); + } + } + + /** + * Given portions `p`, adds the mealEntry p times to the list of mealEntries + * @param mealEntry the meal to be added to the {@code MealEntriesList} + * @param portion the portions consumed of the {@code mealEntry} + */ + public void addPortionsOfMeal(Meal mealEntry, int portion) { + IntStream.range(0, Math.max(1, portion)) + .forEach(i -> this.addMeal(mealEntry)); + } + + /** + * Adds a mealEntry to the mealEntriesList + * @param mealEntry the mealEntry to be added to the {@code MealEntriesList} + */ + @Override + public void addMeal(Meal mealEntry) { + super.mealList.add(mealEntry); + UI.printReply(mealEntry.toString(), "Tracked: "); + } + + /** + * Deletes a mealEntry by its index in the log meals overview + * @param mealNumber Index of the meal to be deleted + */ + //@@author DarkDragoon2002 + @Override + public void deleteMeal(int mealNumber) { + Meal mealToDelete = this.mealList.get(mealNumber - 1); + super.mealList.remove(mealNumber - 1); + UI.printReply(mealToDelete.toString(), "Deleted entry: "); + } + //@@author + + public List getMealEntries() { + return new ArrayList<>(super.mealList); + } + + /** + * Computes actual calorie consumption and delegates the construction and actual printing of the + * consumption bar to the user instance which forwards it to the UI class + * @param user User profile for which the ideal calorie consumption + * will be compared with the actual consumption + * @param dateTime The date for which actual consumption is calculated and compared to the target. + */ + public void printDaysConsumptionBar(User user, LocalDateTime dateTime) { + assert user != null : "User cannot be null"; + assert dateTime != null: "Date needs to be specified to print todays consumption bar"; + + LocalDate date = dateTime.toLocalDate(); + LocalDateTime todayStartOfDay = DateTimeUtils.startOfDayLocalDateTime(date); + LocalDateTime todayEndOfDay = DateTimeUtils.endOfDayLocalDateTime(date); + + MealEntriesList mealsConsumedToday = this.getMealEntriesByDate(todayStartOfDay, todayEndOfDay); + int caloriesConsumed = mealsConsumedToday.getTotalCaloriesConsumed(); + Integer targetCalories = user.getTargetCalories(); + boolean useSpecialChars = user.isAbleToSeeSpecialChars(); + + UI.printReply(targetCalories.toString(), "Ideal Daily Caloric Intake: "); + UI.printString("Current Calories Consumed: " + caloriesConsumed); + UI.printConsumptionBar("% of Expected Calorie Intake Consumed: ", + targetCalories, + caloriesConsumed, + date, + useSpecialChars); + } + + + /** + * Prints the historic consumption bars for a specified number of days. + * + * @param user the User whose consumption history is to be printed + * @param days the number of days to include in the consumption history + * @throws IllegalArgumentException if user is null or days is negative + */ + public void printHistoricConsumptionBars(User user, int days) { + assert user != null : "User cannot be null"; + assert days >= 0 : "Days cannot be negative"; + + Integer targetCalories = user.getTargetCalories(); + UI.printReply(targetCalories.toString(), "Ideal Daily Caloric Intake: "); + ConsumptionStatistics consumptionStats = ConsumptionStatistics.computeStats(user, days, this); + this.printHistoricBarPerDay(days, user); + consumptionStats.printStats(days); + } + + /** + * Computes the total calories consumed from all meals in the list. + * @return The sum of calories for all meals in {@code MealEntriesList}. + */ + public int getTotalCaloriesConsumed() { + return this.mealList.stream() + .map(meal -> meal.getCalories()) + .reduce(0, (accumulator, calorie) -> accumulator + calorie); + } + + /** + * Retrieves the meal entry with the maximum calories in {@code MealEntriesList}. + * @return An {@code Optional} containing the {@code MealEntry} with the highest calorie count, + * or an empty {@code Optional} if {@code mealList} is empty. + */ + public Optional getMaxCaloriesConsumed() { + return this.mealList.stream() + .map(meal -> (MealEntry) meal) + .reduce((meal1, meal2) -> meal1.getCalories() > meal2.getCalories() ? meal1 : meal2); + } + + /** + * Collects a list of meal entries within a specified date range into a new MealEntriesList. + * @param lowerDateBound The inclusive lower bound of the date range. + * @param upperDateBound The inclusive upper bound of the date range. + * @return A {@code MealEntriesList} containing meals that fall within the specified date range. + */ + public MealEntriesList getMealEntriesByDate(LocalDateTime lowerDateBound, LocalDateTime upperDateBound) { + ArrayList filteredMeals = super.mealList.stream() + .filter(meal -> meal.isBeforeEqualDate(upperDateBound)) + .filter(meal -> meal.isAfterEqualDate(lowerDateBound)) + .collect(Collectors.toCollection(() -> new ArrayList())); + return new MealEntriesList(filteredMeals); + } + + /** + * Returns the number of mealEntries tracked in this MealEntriesList + * @return Integer the size of the List of meals stored in this instance + */ + @Override + public int size() { + return super.size(); + } + + //@@author DarkDragoon2002 + /** + * Iterates daily over this list of mealEntries and prints daily consumption bar + * @param days number of days to go back in time + * @param user user profile for which the progress bar is built + */ + private void printHistoricBarPerDay(int days, User user) { + LocalDate today = DateTimeUtils.currentDate(); + + for (int i = days - 1; i >= 0; i--) { + LocalDate printDate = today.minusDays(i); + LocalDateTime upperDateBound = DateTimeUtils.endOfDayLocalDateTime(printDate); + LocalDateTime lowerDateBound = DateTimeUtils.startOfDayLocalDateTime(printDate); + + MealEntriesList mealsConsumed = this.getMealEntriesByDate(lowerDateBound, upperDateBound); + int caloriesConsumed = mealsConsumed.getTotalCaloriesConsumed(); + int targetCalories = user.getTargetCalories(); + boolean useSpecialChars = user.isAbleToSeeSpecialChars(); + + UI.printHistoricConsumptionBar(targetCalories, caloriesConsumed, printDate, useSpecialChars); + + } + } + //@@author + + private LocalDateTime getDateOfMealEntry(int mealNumber) { + MealEntry mealEntry = (MealEntry) this.mealList.get(mealNumber - 1); + return mealEntry.getTimestamp(); + } + +} diff --git a/src/main/java/seedu/healthmate/core/MealEntry.java b/src/main/java/seedu/healthmate/core/MealEntry.java new file mode 100644 index 0000000000..ba42cc9a15 --- /dev/null +++ b/src/main/java/seedu/healthmate/core/MealEntry.java @@ -0,0 +1,141 @@ +package seedu.healthmate.core; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Optional; + +import seedu.healthmate.services.UI; +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.BadTimestampException; +import seedu.healthmate.exceptions.EmptyCalorieException; +import seedu.healthmate.exceptions.EmptyTimestampException; +import seedu.healthmate.exceptions.MealNotFoundException; +import seedu.healthmate.utils.DateTimeUtils; +import seedu.healthmate.utils.Parameter; + +/** + * Represents a meal entry in the HealthMate application. + * A meal entry extends the Meal class and includes timestamp information. + */ +public class MealEntry extends Meal{ + private final LocalDateTime timestamp; + + /** + * Constructs a MealEntry with the current timestamp. + * + * @param name The name/description of the meal + * @param calories The caloric content of the meal + */ + public MealEntry(Optional name, int calories) { + super(name, calories); + this.timestamp = LocalDateTime.now(); + } + + /** + * Constructs a MealEntry with a specified timestamp. + * + * @param name The name/description of the meal + * @param calories The caloric content of the meal + * @param timestamp The timestamp of when the meal was consumed + */ + public MealEntry(Optional name, int calories, LocalDateTime timestamp) { + super(name, calories); + this.timestamp = timestamp; + } + + /** + * Extracts a MealEntry object from a string input. + * + * @param input The input string to parse + * @param command The command associated with the input + * @param mealOptions List of predefined meal options + * @return A new MealEntry object based on the input + * @throws EmptyCalorieException If calories information is missing + * @throws BadCalorieException If calories value is invalid + * @throws MealNotFoundException If referenced meal is not found in options + * @throws BadTimestampException If timestamp format is invalid + */ + public static MealEntry extractMealEntryFromString(String input, String command, MealList mealOptions) + throws EmptyCalorieException, BadCalorieException, MealNotFoundException, BadTimestampException { + + int calories; + Optional mealDescription = extractMealDescription(input, command); + + try { + calories = Parameter.getCalories(input); + } catch (EmptyCalorieException e) { + UI.printSeparator(); + UI.printString("Getting info from meal options..."); + Optional optionalCalories = mealOptions.getCaloriesByMealName(mealDescription.orElse("")); + if (!optionalCalories.isPresent() && !(mealDescription.orElse("").equals(""))) { + UI.printMealNotFound(); + throw new MealNotFoundException(); + } + calories = optionalCalories.orElseThrow(() -> new EmptyCalorieException()); + } + + try { + LocalDate timestamp = Parameter.getTimestamp(input); + if (timestamp.isAfter(DateTimeUtils.currentDate())) { + UI.printString("DATE ERROR: NO FUTURE DATES"); + throw new BadTimestampException(); + } + return new MealEntry(mealDescription, calories, timestamp.atStartOfDay()); + } catch (EmptyTimestampException e) { + return new MealEntry(mealDescription, calories); + } catch (BadTimestampException e) { + throw new BadTimestampException(); + } + } + + /** + * Gets the timestamp of the meal entry. + * + * @return The timestamp when the meal was consumed + */ + public LocalDateTime getTimestamp() { + return this.timestamp; + } + + /** + * Converts the meal entry to a string format for saving. + * + * @return String representation of the meal entry for storage + */ + @Override + public String toSaveString() { + return super.toSaveString() + ", " + this.timestamp; + } + + /** + * Checks if the meal entry's timestamp is before or equal to the given timestamp. + * + * @param timestamp The timestamp to compare against + * @return true if this entry's timestamp is before or equal to the given timestamp + */ + @Override + public boolean isBeforeEqualDate(LocalDateTime timestamp) { + return this.timestamp.isBefore(timestamp) || this.timestamp.isEqual(timestamp); + } + + /** + * Checks if the meal entry's timestamp is after or equal to the given timestamp. + * + * @param timestamp The timestamp to compare against + * @return true if this entry's timestamp is after or equal to the given timestamp + */ + @Override + public boolean isAfterEqualDate(LocalDateTime timestamp) { + return this.timestamp.isAfter(timestamp) || this.timestamp.isEqual(timestamp); + } + + /** + * Returns a string representation of the meal entry. + * + * @return String representation of the meal entry including timestamp + */ + @Override + public String toString() { + return super.toString() + " (at: " + this.timestamp.toLocalDate() + ")"; + } +} diff --git a/src/main/java/seedu/healthmate/core/MealList.java b/src/main/java/seedu/healthmate/core/MealList.java new file mode 100644 index 0000000000..6db41a9013 --- /dev/null +++ b/src/main/java/seedu/healthmate/core/MealList.java @@ -0,0 +1,158 @@ +package seedu.healthmate.core; +import static seedu.healthmate.core.Meal.extractMealFromString; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import seedu.healthmate.services.UI; +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.EmptyCalorieException; + +/** + * Represents a list of meals that can be manipulated. + * Provides methods to add, remove, and manage meals in the list. + */ +public class MealList { + + protected ArrayList mealList; + + /** + * Constructs an empty MealList. + */ + public MealList() { + this.mealList = new ArrayList(); + } + + /** + * Constructs a MealList with an existing list of meals. + * @param mealList The ArrayList of meals to initialize with + */ + public MealList(ArrayList mealList) { + this.mealList = mealList; + } + + /** + * Extracts meal information from user input and adds it to the list. + * @param userInput The raw input string from user + * @param command The command that triggered this action + * @param mealOptions List of available meal options + * @param user The current user + */ + public void extractAndAppendMeal(String userInput, String command, MealList mealOptions, User user) { + try { + if (userInput.contains(",")) { + UI.printReply("Meal entry should not include commas", "Retry: "); + return; + } + Meal meal = extractMealFromString(userInput, command); + + if (meal.descriptionIsEmpty()) { + UI.printReply("Meal options require a name", "Retry: "); + } else if (!meal.descriptionWithinMaxLength()) { + UI.printReply( + "Keep description to less than " + Meal.MAX_DESCRIPTION_LENGTH + " characters", + "Retry: "); + } else { + this.addMeal(meal); + } + } catch (EmptyCalorieException | BadCalorieException e) { + UI.printReply("Every meal needs a calorie integer. (e.g. 120)", ""); + } catch (StringIndexOutOfBoundsException s) { + UI.printReply("Do not forget to use /c mark the following integer as calories", + "Retry: "); + } + } + + /** + * Removes a meal from the list based on user input. + * @param userInput The raw input string from user + * @param command The command that triggered this action + */ + public void extractAndRemoveMeal(String userInput, String command) { + try { + int mealNumber = Integer.parseInt(userInput.replaceAll(command, "").strip()); + deleteMeal(mealNumber); + } catch (NumberFormatException n) { + UI.printReply("Meal index needs to be an integer", "Error: "); + } catch (IndexOutOfBoundsException s) { + UI.printReply("Meal index needs to be within range", "Error: "); + } + } + + /** + * Adds a meal to the list without displaying CLI messages. + * @param meal The meal to be added + */ + public void addMealWithoutCLIMessage(Meal meal) { + this.mealList.add(meal); + } + + /** + * Adds a meal to the list and displays a confirmation message. + * @param meal The meal to be added + */ + public void addMeal(Meal meal) { + this.mealList.add(meal); + UI.printReply(meal.toString(), "Added to options: "); + } + + + /** + * Deletes a meal from the list by its index. + * @param mealNumber The 1-based index of the meal to delete + */ + //@@author DarkDragoon2002 + public void deleteMeal(int mealNumber) { + Meal mealToDelete = this.mealList.get(mealNumber - 1); + this.mealList.remove(mealNumber - 1); + UI.printReply(mealToDelete.toString(), "Deleted option: "); + } + //@@author + + public List getMealList() { + return new ArrayList<>(mealList); + } + + /** + * Retrieves the calories for a meal by its name. + * @param mealName The name of the meal to look up + * @return Optional containing the calories if found, empty otherwise + */ + public Optional getCaloriesByMealName(String mealName) { + for (Meal meal : mealList) { + if (meal.getName().isPresent() && meal.getName().get().equalsIgnoreCase(mealName)) { + return Optional.of(meal.getCalories()); + } + } + return Optional.empty(); + } + + /** + * Returns the string representation of a meal at the given index. + * @param mealIndex The index of the meal + * @return String representation of the meal + */ + public String toMealStringByIndex(int mealIndex) { + return this.mealList.get(mealIndex).toString(); + } + + public int size() { + return this.mealList.size(); + } + + /** + * Updates an existing meal in the list with new information. + * @param newMeal The meal containing updated information + */ + public void updateMeal(Meal newMeal) { + for (int i = 0; i < mealList.size(); i++) { + if (mealList.get(i).getName().equals(newMeal.getName())) { + mealList.remove(i); + mealList.add(i, newMeal); + break; + } + } + } + +} diff --git a/src/main/java/seedu/healthmate/core/User.java b/src/main/java/seedu/healthmate/core/User.java new file mode 100644 index 0000000000..79a8d4eecf --- /dev/null +++ b/src/main/java/seedu/healthmate/core/User.java @@ -0,0 +1,288 @@ +package seedu.healthmate.core; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Scanner; + +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.services.UI; +import seedu.healthmate.services.UserHistoryTracker; + +/** + * Represents a user record captured at a specific date and time. + */ +public class User { + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final double idealCalories; + private final double heightEntry; + private final double weightEntry; + private final boolean isMale; + private final int age; + private final HealthGoal healthGoal; + private final LocalDateTime localDateTime; + private final boolean isAbleToSeeSpecialChars; + + /** + * Constructs a new User object with the specified details. This constructor + * is used when collecting data directly from the user. + * + * @param height User's height in centimeters. + * @param weight User's weight in kilograms. + * @param isMale True if the user is male, false otherwise. + * @param age User's age in years. + * @param healthGoal The health goal for the user (e.g., weight loss, muscle gain). + */ + public User(double height, double weight, boolean isMale, + int age, HealthGoal healthGoal, boolean isAbleToSeeSpecialChars) { + this.heightEntry = height; + this.weightEntry = weight; + this.isMale = isMale; + this.age = age; + this.healthGoal = healthGoal; + this.idealCalories = this.healthGoal.getTargetCalories(height, weight, isMale, age); + this.localDateTime = LocalDateTime.now(); + this.isAbleToSeeSpecialChars = isAbleToSeeSpecialChars; + } + + /** + * Constructs a User object from previously saved data. This constructor + * is used when loading user information from storage. + * + * @param height User's height in centimeters. + * @param weight User's weight in kilograms. + * @param isMale True if the user is male, false otherwise. + * @param age User's age in years. + * @param healthGoal The health goal for the user (e.g., weight loss, muscle gain). + * @param idealCalories The previously calculated ideal calorie intake for the user. + * @param localDateTime The timestamp of the last user data entry in ISO-8601 format. + */ + public User(double height, double weight, boolean isMale, int age, + String healthGoal, double idealCalories, String localDateTime, boolean isAbleToSeeSpecialChars) { + this.heightEntry = height; + this.weightEntry = weight; + this.isMale = isMale; + this.age = age; + this.healthGoal = new HealthGoal(healthGoal); + this.idealCalories = idealCalories; + this.localDateTime = LocalDateTime.parse(localDateTime, DATE_TIME_FORMATTER); + this.isAbleToSeeSpecialChars = isAbleToSeeSpecialChars; + } + + /** + * Asks user to input specifics for creating a new User instance + * @return A new user instance created with the data inputted by user. + */ + public static User askForUserData() { + try { + Scanner scanner = new Scanner(System.in); + + UI.printString("Create your profile: please enter..."); + + double height = askForHeight(scanner); + double weight = askForWeight(scanner); + boolean isMale = askForGender(scanner); + int age = askForAge(scanner); + HealthGoal healthGoal = askForHealthGoal(scanner); + boolean isAbleToSeeSpecialChars = askForSpecialChars(scanner); + + User user = new User(height, weight, isMale, age, healthGoal, isAbleToSeeSpecialChars); + UI.printString("Profile creation Successful!"); + UI.printReply("Great! You can now begin to use the app!", ""); + + UserHistoryTracker userHistoryTracker = new UserHistoryTracker(); + userHistoryTracker.saveUserToFile(user); + + return user; + } catch (Exception exception) { + UI.printReply("Wrong user input: " + exception.getMessage(), "Retry: "); + return askForUserData(); + } + } + + /** + * Returns ideal calories to be consumed by this user instance + * @return double Ideal calorie consumption + */ + public int getTargetCalories() { + return (int) this.idealCalories; + } + + public boolean isAbleToSeeSpecialChars() { + return this.isAbleToSeeSpecialChars; + } + + /** + * Creates a specific user profile for isolated testing. + * @return User profile + */ + public static User createUserStub() { + HealthGoal bulkGoal = new HealthGoal("BULKING"); + return new User(180, 80.0, true, 20, bulkGoal, true); + } + + public static User createAlternativeUserStub() { + HealthGoal steadyGoal = new HealthGoal("STEADY_STATE"); + return new User(200, 200, false, 82, steadyGoal, true); + } + + + @Override + public String toString() { + return heightEntry + "," + + weightEntry + "," + + isMale + "," + + age + "," + + healthGoal.toString() + "," + + idealCalories + "," + + localDateTime.format(DATE_TIME_FORMATTER) + "," + + isAbleToSeeSpecialChars; + } + + public void printUIString() { + UI.printString("Height: " + heightEntry + "cm"); + UI.printString("Weight: " + weightEntry + "kg"); + UI.printString("Gender: " + (isMale ? "male" : "female")); + UI.printString("Age: " + age); + UI.printString("Health Goal: " + healthGoal.toString()); + UI.printString("Ideal Daily Caloric Intake: " + idealCalories); + UI.printString("Recorded at: " + localDateTime.format(DATE_TIME_FORMATTER)); + UI.printString("Is able to see special chars: " + isAbleToSeeSpecialChars); + } + + public Goals getHealthGoal() { + return Goals.valueOf(this.healthGoal.getCurrentHealthGoal()); + } + + public LocalDateTime getLocalDateTime() { + return this.localDateTime; + } + + public double getWeight() { + return this.weightEntry; + } + + private static Double askForHeight(Scanner scanner) { + UI.printString("Height in cm (e.g. 180):"); + try { + double height = Double.parseDouble(scanner.nextLine()); + if (height <= 0){ + List messages = List.of("Invalid height detected - entered height <= 0", "Retry:"); + UI.printMultiLineReply(messages); + return askForHeight(scanner); + } else if (height >= 270) { + List messages = List.of("Invalid height detected - entered height <= 270", "Retry:"); + UI.printMultiLineReply(messages); + return askForHeight(scanner); + } else { + return height; + } + } catch (NumberFormatException n) { + List messages = List.of("Enter a valid height such that 0 < height < 270", "Retry:"); + UI.printMultiLineReply(messages); + return askForHeight(scanner); + } + } + + private static Double askForWeight(Scanner scanner) { + UI.printString("Weight in kg (e.g. 80):"); + try { + double weight = Double.parseDouble(scanner.nextLine()); + if (weight <= 0){ + List messages = List.of("Invalid weight detected. Entered weight <= 0", "Retry: "); + UI.printMultiLineReply(messages); + return askForWeight(scanner); + } else if (weight >= 650) { + List messages = List.of("Invalid weight detected. Entered weight >= 650", "Retry: "); + UI.printMultiLineReply(messages); + return askForWeight(scanner); + } else { + return weight; + } + } catch (NumberFormatException n) { + List messages = List.of("Invalid weight detected. Enter a weight s.t. 0 < weight < 650", "Retry: "); + UI.printMultiLineReply(messages); + return askForWeight(scanner); + } + } + + private static boolean askForGender(Scanner scanner) { + UI.printString("Gender (male or female):"); + String gender = scanner.nextLine(); + if (gender.equals("male")) { + return true; + } else if (gender.equals("female")) { + return false; + } else { + List messages = List.of("Gender does not fit the biological categories. " + + "Please select from: male, female", "Retry: "); + UI.printMultiLineReply(messages); + return askForGender(scanner); + } + } + + private static int askForAge(Scanner scanner) { + UI.printString("Age (e.g. 20):"); + try { + int age = Integer.parseInt(scanner.nextLine()); + if (age <= 0){ + List messages = List.of("Invalid age detected. Age is <= 0.", "Retry: "); + UI.printMultiLineReply(messages); + return askForAge(scanner); + } else if (age >= 600) { + List messages = List.of("Invalid age detected. Age is >= 600.", "Retry: "); + UI.printMultiLineReply(messages); + return askForAge(scanner); + } else { + return age; + } + } catch (NumberFormatException n) { + List messages = List.of("Invalid age detected. Has to be an integer between 0 and 600.", "Retry: "); + UI.printMultiLineReply(messages); + return askForAge(scanner); + } + } + + private static HealthGoal askForHealthGoal(Scanner scanner) { + + List messages = List.of("Enter a health goal:", + "Choose one of the following:", + "1. WEIGHT_LOSS", + "2. STEADY_STATE", + "3. BULKING", + "Enter the necessary number (1,2,3) to select"); + UI.printMultiLineReply(messages); + try { + int healthGoal = Integer.parseInt(scanner.nextLine().strip()); + boolean inputIsInvalid = healthGoal < 1 | healthGoal > 3; + if (inputIsInvalid) { + UI.printString("INVALID HEALTH GOAL: TRY AGAIN"); + return askForHealthGoal(scanner); + } else { + return new HealthGoal(healthGoal); + } + } catch (NumberFormatException n) { + UI.printString("INVALID HEALTH GOAL: TRY AGAIN"); + return askForHealthGoal(scanner); + } + + } + + private static boolean askForSpecialChars(Scanner scanner) { + List initMessages = List.of("Does progressbar below look well formatted as a questionmark? ", + "Enter: {y} if it looks good. Enter: {n} if it contains weird characters such as '?'."); + UI.printMultiLineReply(initMessages); + UI.printString(UI.progressBarStringBuilder(100, 25, true)); + String input = scanner.nextLine().strip().toLowerCase(); + boolean inputIsInvalid = !input.equals("y") && !input.equals("n"); + if (inputIsInvalid) { + List messages = List.of("Invalid Input. Please enter 'y' or 'n'", "Retry"); + UI.printMultiLineReply(messages); + return askForSpecialChars(scanner); + } else { + return input.equals("y") ? true : false; + } + } +} + diff --git a/src/main/java/seedu/healthmate/core/UserEntryList.java b/src/main/java/seedu/healthmate/core/UserEntryList.java new file mode 100644 index 0000000000..b1023f409b --- /dev/null +++ b/src/main/java/seedu/healthmate/core/UserEntryList.java @@ -0,0 +1,63 @@ +package seedu.healthmate.core; + +import java.util.ArrayList; + +/** + * Represents a list of user entries in the HealthMate application. + * This class manages a collection of User objects and provides methods to manipulate and access the list. + */ +public class UserEntryList { + private ArrayList userEntryList; + + /** + * Constructs an empty UserEntryList. + */ + public UserEntryList() { + this.userEntryList = new ArrayList<>(); + } + + /** + * Adds a new user entry to the list. + * + * @param user The User object to be added to the list + */ + public void addUserEntry(User user) { + userEntryList.add(user); + } + + /** + * Returns the list of all user entries. + * + * @return ArrayList containing all User objects + */ + public ArrayList getUserEntryList() { + return this.userEntryList; + } + + /** + * Returns the most recently added user entry. + * + * @return The last User object in the list + */ + public User getLastEntry() { + return userEntryList.get(userEntryList.size() - 1); + } + + /** + * Checks if the list of user entries is empty. + * @return {@code true} if the user entry list contains no elements, {@code false} otherwise. + */ + public boolean isEmpty() { + return this.userEntryList.isEmpty(); + } + + @Override + public String toString() { + return this.userEntryList.stream() + .map(user -> user.toString()) // Convert each User to its String representation + .reduce((user1, user2) -> user1 + "\n" + user2) // Concatenate with newlines + .orElse(""); // Return empty string if the list is empty + } + + +} diff --git a/src/main/java/seedu/healthmate/core/WeightEntryDisplay.java b/src/main/java/seedu/healthmate/core/WeightEntryDisplay.java new file mode 100644 index 0000000000..78d161af4a --- /dev/null +++ b/src/main/java/seedu/healthmate/core/WeightEntryDisplay.java @@ -0,0 +1,148 @@ +package seedu.healthmate.core; + +import seedu.healthmate.services.UI; +import seedu.healthmate.utils.Pair; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * This class is responsible for displaying weight entries in a timeline graph. + */ +public class WeightEntryDisplay { + private static List> weightEntries = new ArrayList<>(); + + /** + * Displays a timeline graph of weight entries from the provided user entry list. + * The graph is printed in the console, with the most recent weight entries shown. + * If there are no entries or if all weight entries are identical, an appropriate message is displayed. + * + * @param userEntryList An {@link Optional} containing the user entry list. If empty, no weight entries are + * displayed. + */ + public static void display(Optional userEntryList) { + if (userEntryList.isEmpty()) { + UI.printReply("User data is missing.",""); + return; + } + + weightEntries = extractWeightEntries(userEntryList.get()); + if (weightEntries.isEmpty()) { + UI.printReply("Weight Entries is missing.",""); + return; + } + + double minWeight = calculateMinWeight(); + double maxWeight = calculateMaxWeight(); + + if(minWeight == maxWeight) { + UI.printReply("Not enough weight entries of different variance.",""); + return; + } + double scale = calculateScale(minWeight, maxWeight); + printGraph(minWeight, maxWeight, scale); + } + + /** + * Extracts the weight entries from the provided user entry list. + * + * @param userEntryList The user entry list. + * @return A list of pairs containing the weight entries (date-time and weight). + */ + private static List> extractWeightEntries(UserEntryList userEntryList) { + List> entries = new ArrayList<>(); + ArrayList users = userEntryList.getUserEntryList(); + int size = users.size(); + int startIndex = Math.max(0, size - 10); + + for (int i = startIndex; i < size; i++) { + User user = users.get(i); + entries.add(new Pair<>(user.getLocalDateTime(), user.getWeight())); + } + return entries; + } + + /** + * Calculates the minimum weight value from the weight entries. + * + * @return The minimum weight value. + */ + private static double calculateMinWeight() { + return weightEntries.stream() + .mapToDouble(Pair::u) + .min() + .orElse(Double.MAX_VALUE); + } + + /** + * Calculates the maximum weight value from the weight entries. + * + * @return The maximum weight value. + */ + private static double calculateMaxWeight() { + return weightEntries.stream() + .mapToDouble(Pair::u) + .max() + .orElse(Double.MIN_VALUE); + } + + /** + * Calculates the scale factor to fit the weight entries into the graph. + * + * @param minWeight The minimum weight value. + * @param maxWeight The maximum weight value. + * @return The scale factor. + */ + private static double calculateScale(double minWeight, double maxWeight) { + final int graphHeight = 20; + return graphHeight / (maxWeight - minWeight); + } + + /** + * Prints the weight timeline graph to the console. + * + * @param minWeight The minimum weight value. + * @param maxWeight The maximum weight value. + * @param scale The scale factor for the graph. + */ + private static void printGraph(double minWeight, double maxWeight, double scale) { + final int graphHeight = 20; + System.out.println("Weight Timeline"); + + for (int y = graphHeight; y >= 0; y--) { + double weightValue = minWeight + (y * (maxWeight - minWeight) / graphHeight); + System.out.printf("%5.1f | ", weightValue); + + for (Pair entry : weightEntries) { + double weight = entry.u(); + if ((weight - minWeight) * scale >= y) { + System.out.print(" * "); + } else { + System.out.print(" "); + } + } + System.out.println(); + } + + printGraphFooter(); + } + + /** + * Prints the footer of the weight timeline graph (dates). + */ + private static void printGraphFooter() { + System.out.print(" "); + for (int i = 0; i < weightEntries.size(); i++) { + System.out.print("----- "); + } + System.out.println(); + + System.out.print(" "); + for (Pair entry : weightEntries) { + System.out.printf("%-5s ", entry.t().toLocalDate().toString().substring(5)); // Format MM-DD, padded + } + System.out.println(); + } +} diff --git a/src/main/java/seedu/healthmate/exceptions/BadCalorieException.java b/src/main/java/seedu/healthmate/exceptions/BadCalorieException.java new file mode 100644 index 0000000000..2af34e9b87 --- /dev/null +++ b/src/main/java/seedu/healthmate/exceptions/BadCalorieException.java @@ -0,0 +1,8 @@ +package seedu.healthmate.exceptions; + +/** + * Exception thrown when the calorie value in a meal is invalid. + */ +public class BadCalorieException extends Exception { + +} diff --git a/src/main/java/seedu/healthmate/exceptions/BadPortionException.java b/src/main/java/seedu/healthmate/exceptions/BadPortionException.java new file mode 100644 index 0000000000..0f0d3a8ac6 --- /dev/null +++ b/src/main/java/seedu/healthmate/exceptions/BadPortionException.java @@ -0,0 +1,7 @@ +package seedu.healthmate.exceptions; + +/** + * Exception thrown when the portion value in a meal is invalid. + */ +public class BadPortionException extends Exception { +} diff --git a/src/main/java/seedu/healthmate/exceptions/BadTimestampException.java b/src/main/java/seedu/healthmate/exceptions/BadTimestampException.java new file mode 100644 index 0000000000..cfc3b92f91 --- /dev/null +++ b/src/main/java/seedu/healthmate/exceptions/BadTimestampException.java @@ -0,0 +1,7 @@ +package seedu.healthmate.exceptions; + +/** + * Exception thrown when the timestamp in a meal is invalid. + */ +public class BadTimestampException extends Exception { +} diff --git a/src/main/java/seedu/healthmate/exceptions/EmptyCalorieException.java b/src/main/java/seedu/healthmate/exceptions/EmptyCalorieException.java new file mode 100644 index 0000000000..178cadd536 --- /dev/null +++ b/src/main/java/seedu/healthmate/exceptions/EmptyCalorieException.java @@ -0,0 +1,7 @@ +package seedu.healthmate.exceptions; + +/** + * Exception thrown when the calorie value in a meal is empty. + */ +public class EmptyCalorieException extends Exception { +} diff --git a/src/main/java/seedu/healthmate/exceptions/EmptyTimestampException.java b/src/main/java/seedu/healthmate/exceptions/EmptyTimestampException.java new file mode 100644 index 0000000000..1ecd4b4e53 --- /dev/null +++ b/src/main/java/seedu/healthmate/exceptions/EmptyTimestampException.java @@ -0,0 +1,7 @@ +package seedu.healthmate.exceptions; + +/** + * Exception thrown when the timestamp in a meal is empty. + */ +public class EmptyTimestampException extends Exception { +} diff --git a/src/main/java/seedu/healthmate/exceptions/MealNotFoundException.java b/src/main/java/seedu/healthmate/exceptions/MealNotFoundException.java new file mode 100644 index 0000000000..d0502d88ae --- /dev/null +++ b/src/main/java/seedu/healthmate/exceptions/MealNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.healthmate.exceptions; + +/** + * Exception thrown when a meal is not found in the list. + */ +public class MealNotFoundException extends Exception { +} diff --git a/src/main/java/seedu/healthmate/recommender/Goals.java b/src/main/java/seedu/healthmate/recommender/Goals.java new file mode 100644 index 0000000000..2e2b4db159 --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/Goals.java @@ -0,0 +1,7 @@ +package seedu.healthmate.recommender; + +public enum Goals { + WEIGHT_LOSS, + STEADY_STATE, + BULKING; +} diff --git a/src/main/java/seedu/healthmate/recommender/Recipe.java b/src/main/java/seedu/healthmate/recommender/Recipe.java new file mode 100644 index 0000000000..171eeaa09f --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/Recipe.java @@ -0,0 +1,85 @@ +package seedu.healthmate.recommender; + +/** + * Represents a recipe with nutritional information and a set of ingredients. + * A Recipe includes details about the recipe name, calories, macronutrients, + * fiber content, a list of ingredients, and an associated goal. + */ +public abstract class Recipe { + private static final String INDENTATION = " "; + + public final String recipeName; + + private final int calories; + private final int protein; + private final int carbs; + private final int fat; + private final int fiber; + private final String recipe; + private final Goals goal; + + /** + * Constructs a Recipe instance with the specified details. + * + * @param name Name of the recipe. + * @param calories Caloric value of the recipe. + * @param protein Protein content of the recipe. + * @param carbs Carbohydrate content of the recipe. + * @param fat Fat content of the recipe. + * @param fiber Fiber content of the recipe. + * @param recipe List of ingredients and instructions for the recipe. + * @param goal Health or fitness goal associated with the recipe. + */ + public Recipe(String name, int calories, int protein, int carbs, int fat, int fiber, String recipe, Goals goal) { + this.recipeName = name; + this.calories = calories; + this.protein = protein; + this.carbs = carbs; + this.fat = fat; + this.fiber = fiber; + this.recipe = recipe; + this.goal = goal; + } + + /** + * Returns a formatted string representation of the recipe, including the + * recipe name, calories, macronutrients, and list of ingredients. + * + * @return A formatted string representing the recipe details. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(recipeName).append(": ").append(calories).append(" calories\n") + .append(INDENTATION).append("Protein: ").append(protein).append("g\n") + .append(INDENTATION).append("Carbs: ").append(carbs).append("g\n") + .append(INDENTATION).append("Fat: ").append(fat).append("g\n") + .append(INDENTATION).append("Fiber: ").append(fiber).append("g\n"); + + String[] ingredients = recipe.split("\n"); + for (String ingredient : ingredients) { + sb.append(INDENTATION).append(ingredient).append("\n"); + } + + return sb.toString(); + } + + + /** + * Returns the caloric value of the recipe. + * + * @return The calories of the recipe. + */ + public int getCalories() { + return calories; + } + + /** + * Returns the goal associated with this recipe. + * + * @return The health or fitness goal for this recipe. + */ + public Goals getGoal() { + return goal; + } +} diff --git a/src/main/java/seedu/healthmate/recommender/RecipeMap.java b/src/main/java/seedu/healthmate/recommender/RecipeMap.java new file mode 100644 index 0000000000..1585c260c6 --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/RecipeMap.java @@ -0,0 +1,42 @@ +package seedu.healthmate.recommender; + +import seedu.healthmate.recommender.recipes.BulkingOatmeal; +import seedu.healthmate.recommender.recipes.FruitSmoothie; +import seedu.healthmate.recommender.recipes.HealthySandwich; +import seedu.healthmate.recommender.recipes.VeggieWrap; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A utility class that manages a collection of recipes and provides methods to access them. + * Contains a static map of recipe names to Recipe objects and methods to retrieve recipes + * based on different criteria. + */ +public class RecipeMap { + private static final Map RECIPEMAP = new HashMap<>(); + static { + RECIPEMAP.put(FruitSmoothie.RECIPE_NAME, new FruitSmoothie()); + RECIPEMAP.put(BulkingOatmeal.RECIPE_NAME, new BulkingOatmeal()); + RECIPEMAP.put(HealthySandwich.RECIPE_NAME, new HealthySandwich()); + RECIPEMAP.put(VeggieWrap.RECIPE_NAME, new VeggieWrap()); + } + + /** + * Retrieves all recipes that match a specific fitness goal. + * + * @param userGoal The fitness goal to filter recipes by + * @return A list of recipes that match the specified goal + */ + public static List getRecipesByGoal(Goals userGoal) { + List filteredRecipes = new ArrayList<>(); + for (Recipe recipe : RECIPEMAP.values()) { + if (recipe.getGoal() == userGoal) { + filteredRecipes.add(recipe); + } + } + return filteredRecipes; + } +} diff --git a/src/main/java/seedu/healthmate/recommender/recipes/BulkingOatmeal.java b/src/main/java/seedu/healthmate/recommender/recipes/BulkingOatmeal.java new file mode 100644 index 0000000000..8cd74df8bb --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/recipes/BulkingOatmeal.java @@ -0,0 +1,31 @@ +package seedu.healthmate.recommender.recipes; + +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.recommender.Recipe; + +public class BulkingOatmeal extends Recipe { + //https://bonytobeastly.com/bulking-meals/#6-high-protein-oatmeal-second-breakfast + public static final String RECIPE_NAME = "High-Protein Oatmeal (Second Breakfast)"; + private static final int CALORIES = 850; + private static final int PROTEIN = 55; + private static final int CARBS = 115; + private static final int FAT = 22; + private static final int FIBER = 10; + private static final Goals GOAL = Goals.BULKING; + + private static final String RECIPE = """ + 1 cup oats (such as quick oats) + 2 cups whole milk (or soy milk, low-fat milk, etc) + 1 diced peach (or apple, mangoes, berries, etc) + 1 tbsp honey + 1 scoop protein powder (e.g., whey isolate) + A pinch of salt + 1 tsp cinnamon + 1/4 tsp vanilla + 1 tsp ashwagandha powder (optional) + """; + + public BulkingOatmeal() { + super(RECIPE_NAME, CALORIES, PROTEIN, CARBS, FAT, FIBER, RECIPE, GOAL); + } +} diff --git a/src/main/java/seedu/healthmate/recommender/recipes/FruitSmoothie.java b/src/main/java/seedu/healthmate/recommender/recipes/FruitSmoothie.java new file mode 100644 index 0000000000..30e159b8fd --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/recipes/FruitSmoothie.java @@ -0,0 +1,29 @@ +package seedu.healthmate.recommender.recipes; + +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.recommender.Recipe; + +public class FruitSmoothie extends Recipe { + //https://bonytobeastly.com/bulking-meals/#2-generic-bulking-smoothie-snackbreakfast + public static final String RECIPE_NAME = "Green Bulking Smoothie"; + private static final int CALORIES = 500; + private static final int PROTEIN = 40; + private static final int CARBS = 55; + private static final int FAT = 20; + private static final int FIBER = 20; + private static final Goals GOAL = Goals.BULKING; + + private static final String RECIPE = """ + 1 banana (frozen or fresh) + 1 handful fresh spinach + 1 tablespoon of almond butter + 4 frozen strawberries + 1 tablespoon flax or chia seeds + 225ml of cold milk, soy milk, or oat milk + 1 scoop of unflavoured protein powder + """; + + public FruitSmoothie() { + super(RECIPE_NAME, CALORIES, PROTEIN, CARBS, FAT, FIBER, RECIPE, GOAL); + } +} diff --git a/src/main/java/seedu/healthmate/recommender/recipes/HealthySandwich.java b/src/main/java/seedu/healthmate/recommender/recipes/HealthySandwich.java new file mode 100644 index 0000000000..d4e0fce704 --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/recipes/HealthySandwich.java @@ -0,0 +1,28 @@ +package seedu.healthmate.recommender.recipes; + +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.recommender.Recipe; + +public class HealthySandwich extends Recipe { + //https://www.eatingwell.com/high-protein-veggie-sandwich-formula-8714142 + public static final String RECIPE_NAME = "Healthy Turkey Avocado Sandwich"; + private static final int CALORIES = 400; + private static final int PROTEIN = 30; // grams + private static final int CARBS = 45; // grams + private static final int FAT = 15; // grams + private static final int FIBER = 10; // grams + private static final Goals GOAL = Goals.STEADY_STATE; + + private static final String RECIPE = """ + 2 slices of whole grain bread + 100g sliced turkey breast + 1/2 avocado, sliced + 1 handful of mixed greens (lettuce, spinach) + 2 slices of tomato + 1 tablespoon mustard or hummus + """; + + public HealthySandwich() { + super(RECIPE_NAME, CALORIES, PROTEIN, CARBS, FAT, FIBER, RECIPE, GOAL); + } +} diff --git a/src/main/java/seedu/healthmate/recommender/recipes/VeggieWrap.java b/src/main/java/seedu/healthmate/recommender/recipes/VeggieWrap.java new file mode 100644 index 0000000000..8c4321e1a7 --- /dev/null +++ b/src/main/java/seedu/healthmate/recommender/recipes/VeggieWrap.java @@ -0,0 +1,33 @@ +package seedu.healthmate.recommender.recipes; + +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.recommender.Recipe; + +public class VeggieWrap extends Recipe { + //recipe from https://www.eatingwell.com/veggie-wraps-8690591 + public static final String RECIPE_NAME = "Veggie Wrap with Hummus"; + private static final int CALORIES = 361; // per serving + private static final int PROTEIN = 12; // grams + private static final int CARBS = 50; // grams + private static final int FAT = 14; // grams + private static final int FIBER = 8; // grams + private static final Goals GOAL = Goals.WEIGHT_LOSS; + + private static final String RECIPE = """ + 1 teaspoon extra-virgin olive oil + 1/2 small zucchini, sliced + 1/2 medium red bell pepper, sliced + 1/4 small red onion, sliced + 1/2 teaspoon dried oregano + Pinch of salt + 2 whole-grain wraps + 1/4 cup hummus + 1/2 cup baby spinach + 2 tablespoons crumbled feta cheese + 4 black olives, sliced + """; + + public VeggieWrap() { + super(RECIPE_NAME, CALORIES, PROTEIN, CARBS, FAT, FIBER, RECIPE, GOAL); + } +} diff --git a/src/main/java/seedu/healthmate/services/ChatParser.java b/src/main/java/seedu/healthmate/services/ChatParser.java new file mode 100644 index 0000000000..adec181c02 --- /dev/null +++ b/src/main/java/seedu/healthmate/services/ChatParser.java @@ -0,0 +1,246 @@ +package seedu.healthmate.services; + +import seedu.healthmate.command.CommandPair; +import seedu.healthmate.command.commands.MealLogCommand; +import seedu.healthmate.command.commands.SaveMealCommand; +import seedu.healthmate.command.commands.ListCommandsCommand; +import seedu.healthmate.command.commands.AddMealEntryCommand; +import seedu.healthmate.command.commands.DeleteMealCommand; +import seedu.healthmate.command.commands.DeleteMealEntryCommand; +import seedu.healthmate.command.commands.MealMenuCommand; +import seedu.healthmate.command.commands.UpdateUserDataCommand; +import seedu.healthmate.command.commands.ClearUserDataCommand; +import seedu.healthmate.command.commands.TodayCalorieProgressCommand; +import seedu.healthmate.command.commands.HistoricCalorieProgressCommand; +import seedu.healthmate.command.commands.CurrentUserDataCommand; +import seedu.healthmate.command.commands.MealRecommendationsCommand; +import seedu.healthmate.command.commands.WeightTimelineCommand; +import seedu.healthmate.command.commands.ByeCommand; + +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.MealList; +import seedu.healthmate.core.User; +import seedu.healthmate.utils.Logging; + +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.IntStream; + +/** + * Encapsulates the main logic of the application by parsing user input into objects + * and storing them respectively. + */ +public class ChatParser { + + private static Logger logger = Logger.getLogger(ChatParser.class.getName()); + private MealEntriesList mealEntries; + private MealList mealOptions; + private final HistoryTracker historyTracker; + private final UserHistoryTracker userHistoryTracker; + + public ChatParser(){ + Logging.setupLogger(logger, ChatParser.class.getName()); + + UI.printSeparator(); + + this.historyTracker = new HistoryTracker(); + logger.log(Level.INFO, "Initialized HistoryTracker"); + + this.mealEntries = historyTracker.loadMealEntries(false); + logger.log(Level.INFO, "Loaded MealEntries"); + + this.mealOptions = historyTracker.loadMealOptions(false); + logger.log(Level.INFO, "Loaded MealOptions"); + + this.userHistoryTracker = new UserHistoryTracker(); + logger.log(Level.INFO, "Initializing UserHistoryTracker"); + + this.assertCheckParserInit(); + logger.log(Level.INFO, "ChatParser correctly initialized."); + + UI.printSeparator(); + } + + /** + * Reads in user input from the command line + * and initiates the parsing process steered by one-token and two-token-based user prompts. + */ + public void run() { + + logger.log(Level.INFO, "Checking if user data exists"); + User user = this.userHistoryTracker.checkForUserData(); + UI.printHelpReminder(); + assert user != null : "User entry should not be null"; + logger.log(Level.INFO, "User is: " + user); + + parseUserInput(user); + } + + /** + * Function simulating the above run() method with a User stub for testing. + */ + public void simulateRunWithStub(User userStub) { + assert userStub != null : "User stub should not be null"; + parseUserInput(userStub); + } + + /** + * Reads in user input via a scanner and maintains the main loop until the user enters "bye" + * @param user The user profile connected with the current application run. + */ + public void parseUserInput(User user) { + Scanner scanner = new Scanner(System.in); + String userInput = ""; + + while (!userInput.equals(ByeCommand.COMMAND)) { + logger.log(Level.INFO, "Getting next user input line"); + userInput = scanner.nextLine().strip(); + logger.log(Level.INFO, "User input is: " + userInput); + if (userInput.equals(ByeCommand.COMMAND)) { + logger.log(Level.INFO, "User closes application"); + UI.printFarewell(); + } else { + try { + logger.log(Level.INFO, "Start multicCommandParsing"); + this.multiCommandParsing(userInput, user); + } catch (ArrayIndexOutOfBoundsException a) { + logger.log(Level.WARNING, "Invalid command", a); + UI.printReply("Invalid command", "Retry: "); + } + } + } + } + + /** + * Steers the execution of features activated by the user via multi-token commands. + * @param userInput String the user's input from the command line. + * @param user The user profile connected with the current application run. + */ + public void multiCommandParsing(String userInput, User user) { + assert userInput != null && !userInput.isEmpty() : "User input should not be null or empty"; + assert user != null : "User should not be null in multiCommandParsing"; + user = this.userHistoryTracker.checkForUserData(); + + this.updateMealLists(); + CommandPair commandPair = getCommandFromInput(userInput); + assert commandPair != null : "CommandPair should not be null"; + logger.log(Level.INFO, "User commands are: " + commandPair); + + String command = commandPair.getMainCommand(); + + switch (command.toLowerCase()) { + case MealMenuCommand.COMMAND_LOWER: + MealMenuCommand.executeCommand(mealOptions, logger); + break; + case SaveMealCommand.COMMAND_LOWER: + SaveMealCommand.executeCommand(historyTracker, mealOptions, userInput, logger); + break; + case DeleteMealCommand.COMMAND_LOWER: + DeleteMealCommand.executeCommand( + historyTracker, mealOptions, userInput, command, logger); + break; + case DeleteMealEntryCommand.COMMAND_LOWER: + DeleteMealEntryCommand.executeCommand( + historyTracker, mealEntries, user, userInput, command, logger); + break; + case AddMealEntryCommand.COMMAND_LOWER: + AddMealEntryCommand.executeCommand( + historyTracker, mealOptions, mealEntries, user, userInput, command, logger); + break; + case MealLogCommand.COMMAND_LOWER: + MealLogCommand.executeCommand(mealEntries, logger); + break; + case ListCommandsCommand.COMMAND_LOWER: + ListCommandsCommand.executeCommand(userInput, command, logger); + break; + case UpdateUserDataCommand.COMMAND_LOWER: + UpdateUserDataCommand.executeCommand(userHistoryTracker, logger); + break; + case ClearUserDataCommand.COMMAND_LOWER: + ClearUserDataCommand.executeCommand(userHistoryTracker, logger); + break; + case CurrentUserDataCommand.COMMAND_LOWER: + CurrentUserDataCommand.executeCommand(userHistoryTracker, logger); + break; + case TodayCalorieProgressCommand.COMMAND_LOWER: + TodayCalorieProgressCommand.executeCommands(mealEntries, user, logger); + break; + case HistoricCalorieProgressCommand.COMMAND_LOWER: + HistoricCalorieProgressCommand.executeCommand(mealEntries, commandPair, user, logger); + break; + case MealRecommendationsCommand.COMMAND_LOWER: + MealRecommendationsCommand.executeCommand(user, logger); + break; + case WeightTimelineCommand.COMMAND_LOWER: + WeightTimelineCommand.executeCommand(userHistoryTracker, logger); + break; + default: + logger.log(Level.WARNING, "Invalid command received"); + UI.printReply("Use a valid command", "Retry: "); + break; + } + } + + + public UserHistoryTracker getUserHistoryTracker() { + return this.userHistoryTracker; + } + + public String getMealOptionsStringWithNewMeal(String newMealString) { + return UI.toMealOptionsString(this.mealOptions, newMealString); + } + + public void cleanMealLists() { + this.mealEntries = this.historyTracker.loadEmptyMealEntries(); + this.mealOptions = this.historyTracker.loadEmptyMealOptions(); + historyTracker.saveMealOptions(mealOptions); + historyTracker.saveMealEntries(mealEntries); + } + + //@@author kennethSty + /** + * Takes in user input and structures it into a preprocessed pair of a main command and additional commands. + * @param userInput The user input from the command line. + * @return CommandPair containing the main command and any additional commands. + */ + private CommandPair getCommandFromInput(String userInput) { + assert userInput != null && !userInput.isEmpty() : "User input should not be null or empty"; + + String[] inputTokens = userInput.split(" "); + assert inputTokens.length > 0 : "Input tokens should not be empty"; + + String commandToken1 = inputTokens[0].strip(); + String commandToken2 = (inputTokens.length > 1) ? inputTokens[1].strip() : ""; + String twoTokenCommand = commandToken1 + (commandToken2.isEmpty() ? "" : " " + commandToken2); + + String[] additionalCommands = IntStream.range(2, inputTokens.length) + .boxed() + .map(i -> inputTokens[i].strip()) + .toArray(String[]::new); + + return new CommandPair(twoTokenCommand, additionalCommands); + } + //@@author + + + /** + * Loads the most recent mealList from the file, to ensure that data is + * synchronized between multiple running instances of the app + */ + private void updateMealLists() { + this.mealOptions = historyTracker.loadMealOptions(true); + this.mealEntries = historyTracker.loadMealEntries(true); + } + + private void assertCheckParserInit() { + assert this.historyTracker != null: "History Tracker should not be null"; + assert this.userHistoryTracker != null: "User History Tracker should not be null"; + assert this.mealEntries != null : "Meal entries list should not be null"; + assert this.mealOptions != null : "Meal options list should not be null"; + } + + + + +} diff --git a/src/main/java/seedu/healthmate/services/ConsumptionStatistics.java b/src/main/java/seedu/healthmate/services/ConsumptionStatistics.java new file mode 100644 index 0000000000..73dd800b20 --- /dev/null +++ b/src/main/java/seedu/healthmate/services/ConsumptionStatistics.java @@ -0,0 +1,52 @@ +package seedu.healthmate.services; + +import java.time.LocalDateTime; +import java.util.Optional; + +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.MealEntry; +import seedu.healthmate.core.User; +import seedu.healthmate.utils.DateTimeUtils; + +public class ConsumptionStatistics { + + private final int idealCalories; + private final int totalIdealCalories; + private final int totalCaloriesConsumed; + private final Optional maxMeal; + + public ConsumptionStatistics(int idealCalories, int totalIdealCalories, + int totalCaloriesConsumed, Optional maxMeal) { + this.idealCalories = idealCalories; + this.totalIdealCalories = totalIdealCalories; + this.totalCaloriesConsumed = totalCaloriesConsumed; + this.maxMeal = maxMeal; + } + //@@author DarkDragoon2002 + /** + * Computes consumption statistics + * @param user the user for which the ideal consumption mark is computed + * @param days the number of days going in the past for which the total statistics are computed + * @param mealEntries the mealEntries based on which the consumption is computed + * @return A new consumption instance containing the statistics + */ + public static ConsumptionStatistics computeStats(User user, int days, MealEntriesList mealEntries) { + + LocalDateTime today = DateTimeUtils.currentDate().atTime(23, 59); + LocalDateTime lastDate = today.minusDays(days); + MealEntriesList mealsConsumed = mealEntries.getMealEntriesByDate(lastDate, today); + int totalCaloriesConsumed = mealsConsumed.getTotalCaloriesConsumed(); + int idealCalories = user.getTargetCalories(); + int totalIdealCalories = days * idealCalories; + Optional maxMeal = mealsConsumed.getMaxCaloriesConsumed(); + + return new ConsumptionStatistics(idealCalories, totalIdealCalories, totalCaloriesConsumed, maxMeal); + } + //@@author + + public void printStats(int days) { + UI.printHistoricConsumptionStats(days, this.idealCalories, this.totalCaloriesConsumed, + this.totalIdealCalories, this.maxMeal); + } + +} diff --git a/src/main/java/seedu/healthmate/services/HistoryTracker.java b/src/main/java/seedu/healthmate/services/HistoryTracker.java new file mode 100644 index 0000000000..57808531d6 --- /dev/null +++ b/src/main/java/seedu/healthmate/services/HistoryTracker.java @@ -0,0 +1,203 @@ +package seedu.healthmate.services; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import seedu.healthmate.core.Meal; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.MealEntry; +import seedu.healthmate.core.MealList; +import seedu.healthmate.utils.Pair; + +/** + * Handles saving and loading of meal entries and meal options to/from persistent storage. + * Uses CSV files to store the data in a data directory. + */ +public class HistoryTracker { + protected static final String DATA_DIRECTORY = "data"; + private static final String MEAL_ENTRIES_FILE = "meal_entries.csv"; + private static final String MEAL_OPTIONS_FILE = "meal_options.csv"; + + /** + * Creates a new HistoryTracker and ensures the data directory exists. + */ + public HistoryTracker() { + createDirectoryIfNotExists(DATA_DIRECTORY); + } + + /** + * Creates a directory if it does not already exist. + * @param folderName The name of the directory to create + */ + public static void createDirectoryIfNotExists(String folderName) { + File directory = new File(folderName); + if (!directory.exists()) { + directory.mkdir(); + } + assert directory.exists() : "Data directory should exist after creation"; + } + + /** + * Saves the list of meal entries to a CSV file. + * @param mealEntries The list of meal entries to save + */ + public void saveMealEntries(MealEntriesList mealEntries) { + saveMealToFile(mealEntries.getMealEntries(), MEAL_ENTRIES_FILE); + } + + /** + * Saves new or modified meal options to a CSV file. + * Only saves meals that don't already exist in the file. + * @param mealOptions The list of meal options to save + */ + public void saveMealOptions(MealList mealOptions) { + //only saves meals which are new/details change + List mealList = mealOptions.getMealList(); + List existingMeals = loadMealFromFile(MEAL_OPTIONS_FILE, false, true); + List newMeals = new ArrayList<>(); + + for (Meal meal : mealList) { + if (!existingMeals.contains(meal)) { + newMeals.add(meal); + } + } + + saveMealToFile(newMeals, MEAL_OPTIONS_FILE); + } + + + /** + * Loads meal entries from the CSV file. + * @return A MealEntriesList containing all saved meal entries + */ + public MealEntriesList loadMealEntries(boolean loadSilent) { + List meals = loadMealFromFile(MEAL_ENTRIES_FILE, true, loadSilent); + MealEntriesList mealEntriesList = new MealEntriesList(); + for (Meal meal : meals) { + mealEntriesList.addMealWithoutCLIMessage(meal); + } + return mealEntriesList; + } + + /** + * Loads meal options from the CSV file. + * @return A MealList containing all saved meal options + */ + public MealList loadMealOptions(boolean loadSilent) { + List meals = loadMealFromFile(MEAL_OPTIONS_FILE, false, loadSilent); + MealList mealList = new MealList(); + for (Meal meal : meals) { + mealList.addMealWithoutCLIMessage(meal); + } + return mealList; + } + + /** + * Creates and returns an empty MealEntriesList. + * @return An empty MealEntriesList + */ + public MealEntriesList loadEmptyMealEntries() { + return new MealEntriesList(); + } + + /** + * Creates and returns an empty MealList. + * @return An empty MealList + */ + public MealList loadEmptyMealOptions() { + return new MealList(); + } + + /** + * Saves a list of meals to a specified CSV file. + * @param meals The list of meals to save + * @param fileName The name of the file to save to + */ + private void saveMealToFile(List meals, String fileName) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(DATA_DIRECTORY + File.separator + fileName))) { + for (Meal meal : meals) { + writer.write(meal.toSaveString()); + writer.newLine(); + } + } catch (IOException e) { + UI.printString("Error saving to file: " + fileName + ". " + e.getMessage()); + } + } + + /** + * Loads meals from a specified CSV file. + * @param fileName The name of the file to load from + * @param isEntry Whether the meals being loaded are meal entries (true) or meal options (false) + * @return A list of meals loaded from the file + */ + private List loadMealFromFile(String fileName, boolean isEntry, boolean loadSilent) { + List meals = new ArrayList<>(); + int totalCorruptedMeals = 0; + File file = new File(DATA_DIRECTORY + File.separator + fileName); + + if (!file.exists()) { + if (!loadSilent) { + String mealTypeString = isEntry ? "Meal Entries" : "Meal Options"; + UI.printString("No locally saved " + mealTypeString + " found."); + } + return meals; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split(","); + Pair, Integer> parseResult = parseAndAddMeal(meals, parts, isEntry); + meals = parseResult.t(); + totalCorruptedMeals += parseResult.u(); + } + } catch (IOException e) { + UI.printString("Error loading from file: " + fileName + ". " + e.getMessage()); + } + if (totalCorruptedMeals > 0 && !loadSilent) { + String mealTypeString = isEntry ? "Meal Entries" : "Meal Options"; + UI.printString("We found " + totalCorruptedMeals + " manually corrupted line(s) in: " + mealTypeString); + UI.printString("If you want to keep your data close the app now and manually undo your modifications."); + UI.printString("Otherwise, if you proceed using the app a new clean data file will overwrite this one."); + UI.printString("To not loose your data in the future, please do not modify your files."); + } else if (totalCorruptedMeals == 0 && !loadSilent) { + String mealTypeString = isEntry ? "Meal Entries" : "Meal Options"; + UI.printString(mealTypeString + " Loaded Successfully!"); + } + return meals; + } + + /** + * Parses meal data from CSV format and adds it to the meals list. + * @param meals The list to add the parsed meal to + * @param parts The array of strings containing the meal data + * @param isEntry Whether the meal being parsed is a meal entry (true) or meal option (false) + * @return The updated list of meals + */ + private Pair, Integer> parseAndAddMeal(List meals, String[] parts, boolean isEntry) { + boolean isCorrectMealEntry = isEntry && (parts.length == 3); + boolean isCorrectMeal = !isEntry && (parts.length == 2); + int corruptedMealsDetected = 0; + if (isCorrectMealEntry) { + String name = parts[0].isEmpty() ? null : parts[0]; + int calories = Integer.parseInt(parts[1]); + LocalDateTime timestamp = LocalDateTime.parse(parts[2].strip()); + meals.add(new MealEntry(Optional.ofNullable(name), calories, timestamp)); + } else if (isCorrectMeal) { + String name = parts[0].isEmpty() ? null : parts[0]; + int calories = Integer.parseInt(parts[1]); + meals.add(new Meal(Optional.ofNullable(name), calories)); + } else { + corruptedMealsDetected++; + } + return new Pair, Integer>(meals, corruptedMealsDetected); + } +} diff --git a/src/main/java/seedu/healthmate/services/MealSaver.java b/src/main/java/seedu/healthmate/services/MealSaver.java new file mode 100644 index 0000000000..174259b0ac --- /dev/null +++ b/src/main/java/seedu/healthmate/services/MealSaver.java @@ -0,0 +1,98 @@ +package seedu.healthmate.services; + + +import java.util.List; +import java.util.Optional; + +import seedu.healthmate.core.Meal; +import seedu.healthmate.core.MealList; +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.EmptyCalorieException; +import seedu.healthmate.utils.DuplicateEntryChecker; + +/** + * Service class responsible for saving and managing meal data. + * Handles extraction of meal information from user input and persistence of meals. + */ +public class MealSaver { + private HistoryTracker historyTracker; + + /** + * Constructs a new MealSaver with the specified history tracker. + * + * @param historyTracker The history tracker to use for saving meal data + */ + public MealSaver(HistoryTracker historyTracker) { + this.historyTracker = historyTracker; + } + + /** + * Extracts a meal object from the user's input string. + * + * @param userInput The raw input string from the user + * @return Optional containing the extracted Meal if successful, empty Optional otherwise + */ + public Optional extractMealFromUserInput(String userInput) { + try { + if (userInput.contains(",")) { + UI.printReply("No Commas Allowed", "Retry: "); + return Optional.empty(); + } + String command = "save meal"; + Meal meal = Meal.extractMealFromString(userInput, command); + if (meal.descriptionIsEmpty()) { + UI.printReply("Meal options require a name", "Retry: "); + return Optional.empty(); + } + return Optional.of(meal); + } catch (EmptyCalorieException | BadCalorieException e) { + UI.printReply("Every meal needs a calorie integer. (e.g. /c120)", ""); + } catch (StringIndexOutOfBoundsException s) { + UI.printReply("Do not forget to use /c to mark the following integer as calories", "Retry: "); + } catch (Exception n) { + UI.printReply("A calorie entry needs to be an integer", "Error: "); + } + return Optional.empty(); + } + + /** + * Saves a meal to the meal list, handling duplicate entries. + * + * @param meal The meal to save + * @param mealList The list to save the meal to + */ + public void saveMeal(Meal meal, MealList mealList) { + if (DuplicateEntryChecker.isDuplicate(meal.getName(), mealList.getMealList())) { + List messages = List.of("Duplicate meal found: " + meal.getName().orElse(""), + "Updated existing meal with new meal specifics!"); + UI.printMultiLineReply(messages); + if (shouldOverwrite(meal)) { + overwriteMeal(meal, mealList); + } + } else { + mealList.addMeal(meal); + } + historyTracker.saveMealOptions(mealList); + } + + /** + * Determines if a duplicate meal should be overwritten. + * + * @param meal The meal to check + * @return true if the meal should be overwritten, false otherwise + */ + private boolean shouldOverwrite(Meal meal) { + // Logic to determine if the meal should be overwritten + return true; + } + + /** + * Overwrites an existing meal in the meal list with a new meal. + * + * @param newMeal The new meal to replace the existing one + * @param mealList The list containing the meal to overwrite + */ + private void overwriteMeal(Meal newMeal, MealList mealList) { + mealList.updateMeal(newMeal); + } +} diff --git a/src/main/java/seedu/healthmate/services/UI.java b/src/main/java/seedu/healthmate/services/UI.java new file mode 100644 index 0000000000..e9c1e8d9b3 --- /dev/null +++ b/src/main/java/seedu/healthmate/services/UI.java @@ -0,0 +1,418 @@ +package seedu.healthmate.services; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +import seedu.healthmate.command.Command; +import seedu.healthmate.core.MealEntriesList; +import seedu.healthmate.core.MealEntry; +import seedu.healthmate.core.MealList; +import seedu.healthmate.core.User; +import seedu.healthmate.recommender.Recipe; +import seedu.healthmate.utils.DateTimeUtils; + +/** + * Handles UI interactions in a structured format. + * Provides methods for printing messages to the user, format output and create UI elements such as consumption bars. + */ +public class UI { + + private static final String NEW_LINE = System.lineSeparator(); + private static final String SEPARATOR = + "_____________________________________________________________________________"; + private static final String INDENTATION = " "; + private static final String LINE = INDENTATION + SEPARATOR; + private static final String FRAME_LINE = LINE + NEW_LINE; + private static final String LOGO = + INDENTATION + " |\n" + + INDENTATION + " \\\\|//\n" + + INDENTATION + " \\\\|//\n" + + INDENTATION + " \\\\\\|///\n" + + INDENTATION + " \\\\\\|///\n" + + INDENTATION + " \\\\|//\n" + + INDENTATION + " \\|/\n" + + INDENTATION + " |\n"; + + + /** + * Prints a formatted reply with a specified action and message. + * + * @param message The message to be printed to the user. + * @param signaller A signaller, representing the kind of message. + */ + public static void printReply(String message, String signaller) { + System.out.println(LINE); + System.out.println(INDENTATION + signaller + message); + System.out.println(LINE); + } + + /** + * Prints an array of strings in the standard format of the UI + * @param messages the strings to print to the user + */ + public static void printMultiLineReply(List messages) { + System.out.println(LINE); + messages.stream().forEach(message -> System.out.println(INDENTATION + message)); + System.out.println(LINE); + } + + /** Prints a greeting message with a welcome logo. */ + public static void printGreeting() { + System.out.println(INDENTATION + LOGO); + System.out.println(LINE); + System.out.println(INDENTATION + "Welcome to HealthMate"); + System.out.println(INDENTATION + "Let's get healthy!"); + System.out.println(LINE); + } + + public static void printHelpReminder() { + System.out.println(INDENTATION + "Use the `list commands` command to have a look at all commands."); + } + /** Prints a farewell message. */ + public static void printFarewell() { + System.out.println(INDENTATION + "Stay healthy!"); + System.out.println(LINE); + } + + public static void printSeparator() { + System.out.println(LINE); + } + + /** Prints a String with standard Indentation message. */ + public static void printString(String message) { + System.out.println(INDENTATION + message); + } + + /** + * Prints the list of available meal options in a structured list format with indices signalling the position + * of meals within the list. + * @param mealOptions The list of meal options to display. + */ + public static void printMealOptions(MealList mealOptions) { + if (mealOptions.size() > 0) { + printSeparator(); + for (int i = 0; i < mealOptions.size(); i++) { + System.out.println(INDENTATION + (i + 1) + + ": " + mealOptions.toMealStringByIndex(i)); + } + printSeparator(); + } else { + printReply("No meal options added yet", ""); + } + } + + /** + * Prints the list of meal entries in a structured format, with indices indicating the position + * of each entry in the list. + * @param mealEntries The list of meal entries to display. + */ + public static void printMealEntries(MealEntriesList mealEntries) { + + if (mealEntries.size() > 0) { + printSeparator(); + for (int i = 0; i < mealEntries.size(); i++) { + System.out.println(INDENTATION + (i + 1) + + ": " + mealEntries.toMealStringByIndex(i)); + } + printSeparator(); + } else { + printReply("No meal entries added yet", ""); + } + + } + + public static void printMealNotFound() { + System.out.println(INDENTATION + "The meal was not found in the meal menu!"); + } + + /** + * Prints list of possible commands to the command line + * @param commands A list of possible commands the user can choose to interact with the system + */ + public static void printCommands(List commands) { + System.out.println(LINE); + if(commands.isEmpty()) { + System.out.println(INDENTATION + "Command queried does not exist. Please use `command list` to view all " + + "searchable commands"); + System.out.println(LINE); + } else if (commands.size() == 1) { + System.out.println(INDENTATION + commands.get(0).toString()); + System.out.println(LINE); + } else { + System.out.println(INDENTATION + "Use `list commands ` to view a command's syntax"); + System.out.println(LINE); + for (Command command : commands) { + System.out.println(INDENTATION + command.shortDescription()); + System.out.println(LINE); + } + } + } + public static void printRecommendation(List recipes) { + System.out.println(LINE); + System.out.println(INDENTATION + "Recommended recipes for your health goal"); + if (recipes.size() == 1) { + System.out.println(INDENTATION + recipes.get(0).toString()); + System.out.println(LINE); + } else { + System.out.println(LINE); + for (Recipe recipe : recipes) { + System.out.println(INDENTATION + recipe.toString()); + System.out.println(LINE); + } + } + } + + /** + * Prints a progress bar comparing actual versus expected calorie consumption + * + * @param message Message printed if actual is 2x larger than expected with exact value + * @param expectedValue double expected value + * @param actualValue int actual value + * @param timestamp timestamp in which the provided actualValue was consumed + */ + public static void printConsumptionBar(String message, + double expectedValue, + int actualValue, + LocalDate timestamp, + boolean useSpecialChars) { + assert timestamp != null : "Timestamp cannot be null"; + String consumptionBar = buildConsumptionBar(message, expectedValue, actualValue, timestamp, useSpecialChars); + System.out.println(consumptionBar); + } + + /** + * High-level creation function of the progress bar. + * Embedds the progress bar into the visual format of the UI + * + * @param message Message printed if actual is 2x larger than expected with exact value + * @param expectedValue double expected value + * @param actualValue int actual value + * @param timestamp timestamp in which the provided actualValue was consumed + * @return the progressBar in form of a String ready to be printed + */ + public static String buildConsumptionBar(String message, + double expectedValue, + int actualValue, + LocalDate timestamp, + boolean useSpecialChars) { + + String header = INDENTATION + message + NEW_LINE; + + String progressBarBody = INDENTATION + + progressBarStringBuilder(expectedValue, actualValue, useSpecialChars) + + " (" + timestamp + ")" + + NEW_LINE; + + String end = LINE; + + return header + progressBarBody + end; + + } + + /** + * Prints a progress bar for historic consumption data. + * + * @param expectedValue the expected consumption value + * @param actualValue the actual consumption value + * @param timestamp the timestamp of the consumption data + * @throws IllegalArgumentException if the timestamp is null + */ + public static void printHistoricConsumptionBar(double expectedValue, int actualValue, + LocalDate timestamp, boolean useSpecialChars) { + assert timestamp != null : "Timestamp cannot be null"; + System.out.println(INDENTATION + + progressBarStringBuilder(expectedValue, actualValue, useSpecialChars) + + " (" + timestamp + ")"); + } + + /** + * Prints a summary of historic consumption statistics over a specified period. + * + * @param days The number of days over which statistics are calculated. + * @param idealCalories The ideal daily calorie intake for comparison. + * @param totalCaloriesConsumed The total calories consumed over the specified period. + * @param totalIdealCalories The total ideal calorie intake for the specified period. + * @param maxMeal A meal entry with the highest calorie count (possibly empty). + */ + public static void printHistoricConsumptionStats(int days, + int idealCalories, + int totalCaloriesConsumed, + int totalIdealCalories, + Optional maxMeal) { + LocalDateTime today = DateTimeUtils.currentDate().atTime(23, 59); + + LocalDateTime maxConsumptionDate = maxMeal + .map(mealEntry -> mealEntry.getTimestamp()) + .orElse(today); + int maxCaloriesConsumed = maxMeal + .map(mealEntry -> mealEntry.getCalories()) + .orElse(0); + String maxMealString = maxMeal + .map(mealEntry -> mealEntry.toString()) + .orElse("No maximum meal available"); + double percentOfIdealConsumed = Math.round(100.0 * (double)totalCaloriesConsumed + / (double)totalIdealCalories); + double percentMaxOfIdeal = Math.round(100.0 * (double)maxCaloriesConsumed + / (double)idealCalories); + + UI.printString("Stats over past " + days + " days"); + UI.printString("Total Calories Consumed: " + totalCaloriesConsumed); + UI.printString("Total Ideal Calories: " + totalIdealCalories); + UI.printString("Percentage of Total Ideal Calories : " + percentOfIdealConsumed + "%"); + UI.printString( "Day With Heaviest Meal: " + maxConsumptionDate.toLocalDate()); + UI.printString("Heaviest Meal Consumed: " + maxMealString); + UI.printString("Meals Consumption's Percentage of Daily Ideal Calories: " + percentMaxOfIdeal + "%"); + UI.printSeparator(); + } + + /** + * Builds a string representing a progress bar + * that visualizes the percentage of an actual value relative to an target value with filled/unfilled + * and the percentage number in the midpoint of the bar. + * 100 % is reached at the middle to allow to visualize "overshooting". + * The whole bar is filled when the actual value is 200% of the target value. + * If more than 200% is reached, the bar stops "filling" but the percentage in the middle keeps growing accordingly. + * Inspired by: + * https://medium.com/javarevisited/how-to-display-progressbar-on-the-standard-console-using-java-18f01d52b30e + * + * @param targetValue The target value for the progress calculation. + * @param actualValue The actual value achieved, which is used to determine progress percentage. + * @return A string representing the progress bar. + */ + public static String progressBarStringBuilder(double targetValue, int actualValue, boolean useSpecialChars) { + int percentageOfExpected = (int) Math.ceil((actualValue / targetValue) * 100); + + String incomplete = useSpecialChars ? "░" : "-"; // U+2591 Unicode Character + String complete = useSpecialChars ? "█" : "*"; // U+2588 Unicode Character + + + int numberOfBoxes = 60; + double totalPercent = 100.0; + int hundredPercentMark = (numberOfBoxes / 2); + StringBuilder builder = new StringBuilder(); + + IntStream.rangeClosed(1, numberOfBoxes) + .boxed() + .map(i -> { + //maps progress from 100 percent scale to numberOfIcons scale + if (i == hundredPercentMark) { + return "|" + String.format("%6s", percentageOfExpected + "%|"); + } else if (i <= ((percentageOfExpected / totalPercent) * hundredPercentMark)) { + return complete; + } else { + return incomplete; + } + }).forEach(step -> builder.append(step)); + + return builder.toString(); + } + + + // Functions to simulate UI behaviour for testing + + + /** + * Outputs the result of list meals as a String if a newMealString would be added at the end. + * Used for testing. + * @param mealOptions List of meal options to which a meal should be added + * @param newMealString The string representing the meal that should be added to the mealOptions. + * @return String representing the output the user would see if this meal would be correclty added to mealOptions. + */ + public static String toMealOptionsString(MealList mealOptions, String newMealString) { + String mealOptionsString = ""; + for (int i = 0; i < mealOptions.size(); i++) { + mealOptionsString += INDENTATION + (i + 1) + ": " + mealOptions.toMealStringByIndex(i) + NEW_LINE; + } + mealOptionsString += INDENTATION + (mealOptions.size() + 1) + ": " + newMealString + NEW_LINE; + return LINE + NEW_LINE + mealOptionsString + LINE + NEW_LINE; + } + + /** + * Function that simulates the behaviour of {@code UI.printReply()} for testing + * @param message The message to be printed to the user. + * @param signaller A signaller, representing the kind of message. + * @return A formatted String that would be printed to the console if using {@code UI.printReply()} + */ + public static String simulateReply(String message, String signaller) { + String line1 = FRAME_LINE; + String line2 = INDENTATION + signaller + message + NEW_LINE; + return line1 + line2 + FRAME_LINE; + } + + /** + * Simulates the output of {@code printString()} in the form of a string. + * + * @param message The message string to be simulated. + * @return A formatted String representing the simulated message. + */ + public static String simulateString(String message) { + return INDENTATION + message + NEW_LINE; + } + + /** + * Simulates a farewell message output for testing purposes. + * @return A String representing the farewell message to the user. + */ + public static String simulateFareWell() { + String line1 = INDENTATION + "Stay healthy!" + NEW_LINE;; + return line1 + FRAME_LINE; + } + + /** + * Simulates the initial output for successfully loaded meal entries and meal options. + * @return A String representing the formatted initial output message. + */ + public static String simulateInitOutputAddMeal(){ + String line2 = INDENTATION + "Meal Entries Loaded Successfully!" + NEW_LINE; + String line3 = INDENTATION + "Meal Options Loaded Successfully!" + NEW_LINE; + return FRAME_LINE + line2 + line3 + FRAME_LINE; + } + + public static String simulateInitOutput() { + String line2 = INDENTATION + "Meal Entries Loaded Successfully!" + NEW_LINE; + String line3 = INDENTATION + "Meal Options Loaded Successfully!" + NEW_LINE; + String line4 = INDENTATION + "Use the `list commands` command to have a look at all commands." + NEW_LINE; + return FRAME_LINE + line2 + line3 + FRAME_LINE +line4; + } + + + + /** + * Simulates the progress bar for historic consumption with a timestamp for testing. + * + * @param targetValue The target value for comparison. + * @param actualValue The actual value to display. + * @param timestamp The timestamp when the consumption occurred. + * @return A formatted String with the progress bar and timestamp. + * @throws IllegalArgumentException if {@code timestamp} is null. + */ + public static String simulateHistoricConsumptionBar(double targetValue, int actualValue, LocalDate timestamp) { + assert timestamp != null : "Timestamp cannot be null"; + return INDENTATION + progressBarStringBuilder(targetValue, actualValue, true) + " (" + timestamp + ")"; + } + + /** + * Simulates the construction of a user-specific consumption bar for testing. + * + * @param caloriesConsumed The actual calories consumed by the user. + * @param timestamp The date for which the consumption is being simulated. + * @return A string representation of the simulated consumption bar, including + * target calories, current calories consumed, and the percentage of + * expected calorie intake consumed. + */ + public static String simulateUsersConsumptionBar(int caloriesConsumed, LocalDate timestamp, User user) { + Integer targetCalories = user.getTargetCalories(); + return UI.simulateReply("Ideal Daily Caloric Intake: " + targetCalories, "") + + UI.simulateString("Current Calories Consumed: " + caloriesConsumed) + + UI.buildConsumptionBar("% of Expected Calorie Intake Consumed: ", + targetCalories, + caloriesConsumed, + timestamp, + true); + } + + +} diff --git a/src/main/java/seedu/healthmate/services/UserHistoryTracker.java b/src/main/java/seedu/healthmate/services/UserHistoryTracker.java new file mode 100644 index 0000000000..c35dc14816 --- /dev/null +++ b/src/main/java/seedu/healthmate/services/UserHistoryTracker.java @@ -0,0 +1,252 @@ +package seedu.healthmate.services; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Scanner; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import seedu.healthmate.core.User; +import seedu.healthmate.core.UserEntryList; + +/** + * Manages the saving and loading of UserEntry lists, which store user information over time. + * Handles adding new user entries, and retrieving stored data for display or further processing. + */ +public class UserHistoryTracker extends HistoryTracker { + + public static final int USER_ENTRY_PRINTING_COUNT = 5; + public static final int USER_DATA_SAVE_FILE_FIELDS = 8; + private static final String USER_DATA_FILE = "user_data.csv"; + + public UserHistoryTracker() { + super(); + } + + //@@author kennethSty + /** + * Loads a User instance if a file with user data exists. + * Creates a new User instance otherwise + * @return A newly created or "loaded" user object + */ + public User checkForUserData() { + return getLatestUser().orElseGet(() -> User.askForUserData()); + } + + public Optional getLatestUser() { + Optional optionalUserEntryList = this.loadUserEntries(); + return optionalUserEntryList.map(userEntryList -> userEntryList.getLastEntry()); + } + + public Optional loadUserEntries() { + + UserEntryList userEntryList = new UserEntryList(); + + try { + File userDataFile = createFileIfNotExists(); + Scanner s = new Scanner(userDataFile); + while (s.hasNextLine()) { + String line = s.nextLine(); + User user = getUserEntryFromFileLine(line); + userEntryList.addUserEntry(user); + } + } catch (IOException e) { + List messages = List.of("Sorry: There was an error loading your user profile.", + "A new profile needs to be created."); + UI.printMultiLineReply(messages); + } catch (ArrayIndexOutOfBoundsException e) { + clearSaveFile(); + List messages = List.of("It seems your user profile is incomplete.", + "A new profile needs to be created."); + UI.printMultiLineReply(messages); + } catch (NumberFormatException e) { + clearSaveFile(); + List messages = List.of("It seems some numbers in you user profile are corrupted.", + "A new profile needs to be created."); + UI.printMultiLineReply(messages); + } catch (IllegalArgumentException e) { + clearSaveFile(); + List messages = List.of("It seems some booleans / health goals in you user profile are corrupted.", + "A new profile needs to be created."); + UI.printMultiLineReply(messages); + } catch (DateTimeParseException e) { + clearSaveFile(); + List messages = List.of("It seems some datetime records in you user profile are corrupted.", + "A new profile needs to be created."); + UI.printMultiLineReply(messages); + } catch (NoSuchElementException e) { + // silent catch if existing user file contains no content + } + return userEntryList.isEmpty() ? Optional.empty() : Optional.of(userEntryList); + } + + /** + * Saves the provided User entry to a file. If the file does not exist, + * it will be created first, and then the user data will be appended to the save file. + * + * @param userEntry The User object containing data to be saved. + */ + public void saveUserToFile(User userEntry) { + try { + createFileIfNotExists(); + addUserEntry(userEntry); + } catch (IOException e) { + UI.printReply("Saving to the user file was unsuccessful", "Error: "); + } + } + //@@author + + //@@author ryan-txn + /** + * Prints all user entries from the data file to the console. + * Displays an error message if the file is not found. + */ + public void printAllUserEntries() { + File userDataFile = new File(super.DATA_DIRECTORY + File.separator + USER_DATA_FILE); + + try (Scanner scanner = new Scanner(userDataFile)) { + System.out.println("Last few records..."); + Optional userListOpt = loadUserEntries(); + + if (userListOpt.isEmpty()) { + System.out.println("No user entries found."); + return; + } + + UserEntryList userList = userListOpt.get(); + int start = userList.getUserEntryList().size() - 1; // Calculate starting index for last 5 entries + int end = Math.max(start - (USER_ENTRY_PRINTING_COUNT - 1), 0); + + for (int i = start; i >= end; i--) { + User user = userList.getUserEntryList().get(i); + System.out.println(); + user.printUIString(); + System.out.println(); + } + } catch (FileNotFoundException e) { + System.out.println("Error: User data file not found. " + e.getMessage()); + } + } + + /** + * Converts a CSV line from the data file into a User object. + * + * @param line CSV-formatted string representing a user entry. + * @return User object with data from the parsed line. + */ + private static User getUserEntryFromFileLine(String line) throws ArrayIndexOutOfBoundsException, + IllegalArgumentException, DateTimeParseException { + + String[] fields = line.split(","); // Split the CSV line by commas + if (fields.length != USER_DATA_SAVE_FILE_FIELDS) { + throw new ArrayIndexOutOfBoundsException(); + } + double height = Double.parseDouble(fields[0]); + double weight = Double.parseDouble(fields[1]); + boolean isMale = parseGender(fields[2].trim()); + int age = Integer.parseInt(fields[3]); + String healthGoal = parseHealthGoal(fields[4].trim()); + double idealCalories = Double.parseDouble(fields[5]); + String localDateTime = parseLocalDateTime(fields[6].trim()); + boolean isAbleToSeeSpecialChars = Boolean.parseBoolean(fields[7]); + + return new User(height, weight, isMale, age, healthGoal, idealCalories, localDateTime, isAbleToSeeSpecialChars); + } + + /** + * Parses and validates a health goal string. + * + * @param healthGoal the health goal to parse, expected to be "WEIGHT_LOSS", "STEADY_STATE", or "BULKING" + * @return the validated health goal string if it matches one of the expected values + * @throws IllegalArgumentException if the health goal is invalid + */ + private static String parseHealthGoal(String healthGoal) throws IllegalArgumentException{ + if (!healthGoal.equals("WEIGHT_LOSS") && !healthGoal.equals("STEADY_STATE") && !healthGoal.equals("BULKING")) { + throw new IllegalArgumentException(); + } + return healthGoal; + } + + /** + * Parses and validates a local date-time string. + * + * @param localDateTime the date-time string to parse, expected to follow User.DATE_TIME_FORMATTER + * @return the validated date-time string if it matches the expected format + * @throws DateTimeParseException if the date-time format is invalid + */ + private static String parseLocalDateTime(String localDateTime) throws DateTimeParseException{ + LocalDateTime.parse(localDateTime, User.DATE_TIME_FORMATTER); + return localDateTime; + } + + /** + * Parses and validates a gender string. + * + * @param isMaleString the gender string to parse, expected to be "true" or "false" + * @return true if the string is "true", false if the string is "false" + * @throws IllegalArgumentException if the gender string is neither "true" nor "false" + */ + private static boolean parseGender(String isMaleString) { + if ("true".equals(isMaleString)) { + return true; + } else if ("false".equals(isMaleString)) { + return false; + } else { + throw new IllegalArgumentException(); + } + } + + /** + * Appends the given User entry to the data file. Creates a new line with + * the user's information in CSV format. If an error occurs, an error message is displayed. + * + * @param userEntry The User object to add to the data file. + */ + public void addUserEntry(User userEntry) { + File userDataFile = new File(DATA_DIRECTORY + File.separator + USER_DATA_FILE); + + try { + FileWriter fw = new FileWriter(userDataFile, true); + fw.write(userEntry.toString() + System.lineSeparator()); + fw.close(); + } catch (IOException e) { + System.out.println("Error adding userEntry to data file: " + e.getMessage()); + } + } + //@@author + + //@@author kennethSty + /** + * Creates the user data file if it does not already exist. + * If the file is created successfully, a confirmation message is displayed. + */ + private File createFileIfNotExists() throws IOException{ + File userDataFile = new File(DATA_DIRECTORY + File.separator + USER_DATA_FILE); + if (!userDataFile.exists()) { + userDataFile.createNewFile(); + } + return userDataFile; + } + //@@author + + //@@author ryan-txn + /** + * Clears the save file by overwriting it with an empty string. + * If an error occurs during file access, an error message is printed to the console. + */ + public void clearSaveFile() { + try { + FileWriter fw = new FileWriter(DATA_DIRECTORY + File.separator + USER_DATA_FILE, false); + fw.write(""); // Overwrite with an empty string + fw.close(); + } catch (IOException e) { + System.out.println("Error clearing save file: " + e.getMessage()); + } + } + //@@ author +} diff --git a/src/main/java/seedu/healthmate/utils/DateTimeUtils.java b/src/main/java/seedu/healthmate/utils/DateTimeUtils.java new file mode 100644 index 0000000000..5fb12fd522 --- /dev/null +++ b/src/main/java/seedu/healthmate/utils/DateTimeUtils.java @@ -0,0 +1,52 @@ +package seedu.healthmate.utils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * Utility class for handling LocalDate and LocalDateTime operations. + */ +public final class DateTimeUtils { + + // Private constructor to prevent instantiation + private DateTimeUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + /** + * Gets the current date. + * + * @return the current LocalDate + */ + public static LocalDate currentDate() { + return LocalDate.now(); + } + + /** + * Gets the LocalDateTime representing the end of the given day. + * The end of day is defined as 23:59:59.999999999. + * + * @param date the LocalDate for which the end of day is needed + * @return the LocalDateTime at the end of the given day + * @throws IllegalArgumentException if the date is null + */ + public static LocalDateTime endOfDayLocalDateTime(LocalDate date) { + assert date != null : "Date cannot be null"; + return date.atTime(LocalTime.MAX); + } + + /** + * Gets the LocalDateTime representing the start of the given day. + * The start of day is defined as 00:00:00. + * + * @param date the LocalDate for which the start of day is needed + * @return the LocalDateTime at the start of the given day + * @throws IllegalArgumentException if the date is null + */ + public static LocalDateTime startOfDayLocalDateTime(LocalDate date) { + assert date != null : "Date cannot be null"; + return date.atStartOfDay(); + } +} + diff --git a/src/main/java/seedu/healthmate/utils/DuplicateEntryChecker.java b/src/main/java/seedu/healthmate/utils/DuplicateEntryChecker.java new file mode 100644 index 0000000000..5d16d34d8b --- /dev/null +++ b/src/main/java/seedu/healthmate/utils/DuplicateEntryChecker.java @@ -0,0 +1,60 @@ +package seedu.healthmate.utils; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.HashSet; + +import seedu.healthmate.core.Meal; + +public class DuplicateEntryChecker { + + /** + * Checks if the given entry is a duplicate in the list of meal options. + * + * @param optional The entry to check for duplication. + * @param mealList The list of meal options to check against. + * @return true if the entry is a duplicate, false otherwise. + */ + public static boolean isDuplicate(Optional optional, List mealList) { + for (Meal option : mealList) { + if (option.getName().equals(optional)) { + return true; + } + } + return false; + } + + + /** + * Checks if there are any duplicate entries in the given list of meal options. + * + * @param mealOptions The list of meal options to check for duplicates. + * @return true if duplicates are found, false otherwise. + */ + public static boolean hasDuplicates(List mealOptions) { + Set uniqueOptions = new HashSet<>(); + for (String option : mealOptions) { + if (!uniqueOptions.add(option)) { + return true; + } + } + return false; + } + + /** + * Finds and returns the first duplicate entry in the given list of meal options. + * + * @param mealOptions The list of meal options to check for duplicates. + * @return The first duplicate entry found, or null if no duplicates exist. + */ + public static String findFirstDuplicate(List mealOptions) { + Set uniqueOptions = new HashSet<>(); + for (String option : mealOptions) { + if (!uniqueOptions.add(option)) { + return option; + } + } + return null; + } +} diff --git a/src/main/java/seedu/healthmate/utils/Logging.java b/src/main/java/seedu/healthmate/utils/Logging.java new file mode 100644 index 0000000000..dc413f1a4e --- /dev/null +++ b/src/main/java/seedu/healthmate/utils/Logging.java @@ -0,0 +1,46 @@ +package seedu.healthmate.utils; + +import java.io.File; +import java.io.IOException; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +import seedu.healthmate.services.HistoryTracker; + +/** + * Utility class for setting up logging functionality. + */ +public class Logging { + /** + * Sets up a logger with both console and file handlers. + * The console handler is set to only log SEVERE messages, while the file handler logs ALL levels. + * Log files are stored in a 'logs' directory with the class name as the file name. + * + * @param logger The logger instance to be configured + * @param nameClassToBeLogged The name of the class being logged (used for the log file name) + * @return The configured logger instance + */ + public static Logger setupLogger(Logger logger, String nameClassToBeLogged) { + LogManager.getLogManager().reset(); + logger.setLevel(Level.ALL); + + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.SEVERE); + logger.addHandler(ch); + + try { + HistoryTracker.createDirectoryIfNotExists("logs"); + FileHandler fh = new FileHandler("logs" + File.separator + nameClassToBeLogged + ".log"); + fh.setFormatter(new SimpleFormatter()); + fh.setLevel(Level.ALL); + logger.addHandler(fh); + } catch (IOException ex) { + logger.log(Level.SEVERE, "Logger file creation unsuccessful", ex); + } + return logger; + } +} diff --git a/src/main/java/seedu/healthmate/utils/Pair.java b/src/main/java/seedu/healthmate/utils/Pair.java new file mode 100644 index 0000000000..1bbfffd057 --- /dev/null +++ b/src/main/java/seedu/healthmate/utils/Pair.java @@ -0,0 +1,48 @@ +package seedu.healthmate.utils; + +/** + * A generic class representing a pair of two values of potentially different types. + * @param The type of the first element + * @param The type of the second element + */ +public class Pair { + + private final T t; + private final U u; + + /** + * Constructs a new Pair with the given values. + * @param t The first element + * @param u The second element + */ + public Pair(T t, U u) { + this.t = t; + this.u = u; + } + + /** + * Returns the first element of the pair. + * @return The first element + */ + public T t() { + return this.t; + } + + /** + * Returns the second element of the pair. + * @return The second element + */ + public U u() { + return this.u; + } + + /** + * Returns a string representation of the pair. + * @return A string in the format "Pair of [first element], [second element]" + */ + @Override + public String toString() { + return "Pair of " + this.t + ", " + this.u; + } + +} diff --git a/src/main/java/seedu/healthmate/utils/Parameter.java b/src/main/java/seedu/healthmate/utils/Parameter.java new file mode 100644 index 0000000000..33c4e29605 --- /dev/null +++ b/src/main/java/seedu/healthmate/utils/Parameter.java @@ -0,0 +1,114 @@ +package seedu.healthmate.utils; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.BadPortionException; +import seedu.healthmate.exceptions.BadTimestampException; +import seedu.healthmate.exceptions.EmptyCalorieException; +import seedu.healthmate.exceptions.EmptyTimestampException; + +public enum Parameter { + EMPTY_SIGNALLER("/"), + CALORIE_SIGNALLER("/c"), + PORTIONS_SIGNALLER("/p"), + TIMESTAMP_SIGNALLER("/t"); + private static int maxCalories = 100000; + private String prefix; + + // Enum constructor + Parameter(String prefix) { + this.prefix = prefix; + } + + // Getter for the prefix + public String getPrefix() { + return prefix; + } + + // Method to parse the value associated with a parameter + // Method to parse the value associated with a parameter + public static int parseParameter(String input, Parameter param) throws NumberFormatException { + // Create a regex pattern for the parameter prefix (like /c or /p) + String regex = param.getPrefix() + "(\\d+)(\\s|$)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + boolean containsParam = input.contains(param.getPrefix()); + + if (matcher.find()) { + try { + return Integer.parseInt(matcher.group(1)); + } catch (NumberFormatException e) { + return -2; + } + } else { + // Contains param but bad format response is -2 + // If param is missing return 1 for portions for default and -1 for Calories + return containsParam? -2: param == PORTIONS_SIGNALLER?1:-1; + + } + } + /** + * Extracts the number of portions from the input string. + * @param input The input string containing portion information + * @return The number of portions specified, or 1 if not specified + * @throws BadPortionException if the portion format is invalid + */ + public static int getPortions(String input) throws BadPortionException { + int portions = parseParameter(input, Parameter.PORTIONS_SIGNALLER); + if (portions <= 0) { + throw new BadPortionException(); + } + return parseParameter(input, Parameter.PORTIONS_SIGNALLER); + } + + /** + * Extracts the calorie count from the input string. + * @param input The input string containing calorie information + * @return The number of calories specified + * @throws BadCalorieException if the calorie format is invalid + * @throws EmptyCalorieException if no calorie value is specified + */ + public static int getCalories(String input) throws BadCalorieException, EmptyCalorieException { + int calories = parseParameter(input, Parameter.CALORIE_SIGNALLER); + if (calories == -1) { + throw new EmptyCalorieException(); + } else if (calories == -2) { + throw new BadCalorieException(); + } + return calories; + } + + /** + * Extracts and parses the timestamp from the input string. + * @param input The input string containing timestamp information in yyyy-MM-dd format + * @return The parsed LocalDate object + * @throws EmptyTimestampException if no timestamp is specified + * @throws BadTimestampException if the timestamp format is invalid + */ + public static LocalDate getTimestamp(String input) throws EmptyTimestampException, BadTimestampException { + String regex = TIMESTAMP_SIGNALLER.getPrefix() + "\\d{4}-\\d{2}-\\d{2}"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + boolean containsTimestamp = input.contains(TIMESTAMP_SIGNALLER.getPrefix()); + + if (!containsTimestamp) { + throw new EmptyTimestampException(); + } + + if (matcher.find()) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return LocalDate.parse(matcher.group(0).replace(TIMESTAMP_SIGNALLER.getPrefix(), ""), formatter); + } catch (DateTimeParseException e) { + throw new BadTimestampException(); + } + } else { + throw new BadTimestampException(); + } + } +} diff --git a/src/test/java/seedu/healthmate/ChatParserTest.java b/src/test/java/seedu/healthmate/ChatParserTest.java new file mode 100644 index 0000000000..6b4c1e3ffa --- /dev/null +++ b/src/test/java/seedu/healthmate/ChatParserTest.java @@ -0,0 +1,421 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +import seedu.healthmate.services.UserHistoryTracker; +import seedu.healthmate.core.MealEntry; +import seedu.healthmate.core.User; +import seedu.healthmate.services.ChatParser; +import seedu.healthmate.services.UI; + + +public class ChatParserTest { + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private final PrintStream originalOutput = System.out; + + + @BeforeEach + public void setOutputStream() { + System.setOut(new PrintStream(outputStream)); + } + + @AfterEach + public void restoreStream() { + System.setOut(originalOutput); + } + + /** + * Executes the chatParser's run method with simulated Input. + * Tests full integration with no stubs + * @param chatParser + * @param simulatedInput User input for which the behaviour of chatParser.run() is asserted + * @param expectedOutput Expected output printed to the console that is to be compared with the actual output + */ + private void compareChatParserOutput(ChatParser chatParser, String simulatedInput, String expectedOutput) { + System.setIn(new ByteArrayInputStream(simulatedInput.getBytes())); + chatParser.run(); + + // Normalize both actual and expected outputs by trimming and standardizing line breaks + String actualOutput = outputStream.toString().trim().replace("\r\n", "\n").replaceAll("\\s+$", ""); + String normalizedExpectedOutput = expectedOutput.trim().replace("\r\n", "\n").replaceAll("\\s+$", ""); + + assertEquals(normalizedExpectedOutput, actualOutput); + chatParser.cleanMealLists(); + } + + /** + * Mocks a chatParser run with a User stub + * @param chatParser + * @param simulatedInput User input for which the behaviour of chatParser.run() is asserted + * @param expectedOutput Expected output printed to the consule that is to be compared with the actual ouput + */ + private void compareChatParserOutputWithStub(ChatParser chatParser, User userStub, + String simulatedInput, String expectedOutput) { + System.setIn(new ByteArrayInputStream(simulatedInput.getBytes())); + chatParser.simulateRunWithStub(userStub); + assertEquals(expectedOutput,outputStream.toString()); + chatParser.cleanMealLists(); + } + + + /** + * Tests if the ChatParser correctly flags invalid random input + * Inspired by: https://stackoverflow.com/questions/1119385/junit-test-for-system-out-println + */ + @Test + public void randomInput_printsError() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "hi\nbye\n"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("Use a valid command", "Retry: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests the situation of adding a meal (without portions and dates specified) + */ + @Test + void addMealToOptionsWithName_success() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "save meal burger /c300\nmeal menu\nbye\n"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("burger with 300 calories", "Added to options: ") + + chatParser.getMealOptionsStringWithNewMeal("burger with 300 calories") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests if adding a meal to meal options without a name failes correctly + */ + @Test void addMealToOptionsNoName_failure() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "save meal /c300\nbye"; + String expectedOuput = UI.simulateInitOutput() + + UI.simulateReply("Meal options require a name", "Retry: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOuput); + } + + /** + * Tests if tracking a meal entry with correctly specified calories was successful. + */ + @Test void trackMealEntryWithCalories_success() { + ChatParser chatParser = new ChatParser(); + UserHistoryTracker userHistoryTracker = chatParser.getUserHistoryTracker(); + User user = userHistoryTracker.checkForUserData(); + String simulatedInput = "add mealEntry pizza /c300\nbye"; + LocalDate today = LocalDateTime.now().toLocalDate(); + String timeString = "(at: " + today + ")"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("pizza with 300 calories " + timeString, "Tracked: ") + + UI.simulateUsersConsumptionBar(300, today, user) + + System.lineSeparator() + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Negative test of a wrong showHistoricCalories command + */ + @Test void showHistoricCaloriesNoTime_failure() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "show historicCalories \n bye"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("Specify the number of days you want to look into the past", + "Missing input: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests adding 10 "burger /c300" mealEntries over the past 10 days as well as the show historicCalories feature + * Scenario simulated: + * A user adds one burger with 300 calories as a new mealEntrie for each of the past 10 days. + * Afterwards, he wants to see the impact on the consumption bars (over the last 10 days). + * The test is based on a user stub with the specifics: + * height: 180, weight: 80.0, isMale: true, age: 20, HealthGoal: "BULKING" + */ + @Test void addPastMealsAndShowHistory_success() { + ChatParser chatParser = new ChatParser(); + User user = User.createUserStub(); + MealEntry testMeal = new MealEntry(Optional.of("burger"), 300); + + LocalDate today = LocalDateTime.now().toLocalDate(); + List pastTenDays = generatePastDates(today, 10); + String simulatedInput = buildInputHistoricCaloriesTest(pastTenDays, user); + String expectedOutput = buildExpectedOutputHistoricCaloriesTest(testMeal, user, pastTenDays); + + compareChatParserOutputWithStub(chatParser, user, simulatedInput, expectedOutput); + } + + /** + * Helper method generating a List of past days (starting today) + * @param today today's date + * @param numberOfDaysIntoPast the number of past days for which we want to generate the dates + * @return List A list of past days + */ + private List generatePastDates(LocalDate today, int numberOfDaysIntoPast) { + return IntStream.range(0, numberOfDaysIntoPast).boxed() + .map(i -> today.minusDays(i)) + .toList(); + } + + /** + * Helper method to generate the input for the test 'addPastMealsAndShowHistory_success'. + * @param pastDays the days for which past mealEntries are added + * @param user the user for whom the mealEntries are entered as consumption + * @return String the input provided to the system to simulate the usage scenario described above + */ + private String buildInputHistoricCaloriesTest(List pastDays, User user) { + String mealEntryBasePrompt = "add mealEntry burger /c300 /t"; + String addMealEntriesPrompts = pastDays.stream() + .map(date -> mealEntryBasePrompt + date.toString()) + .reduce("", (total, prompt) -> total + prompt + "\n"); + String historicCaloriesPrompt = "show historicCalories 10"; + String closeAppPrompt = "\nbye"; + + return addMealEntriesPrompts + historicCaloriesPrompt + closeAppPrompt; + } + + /** + * Helper method to build the expected output of the test 'addPastMealsAndShowHistory_success' + * @param testMeal the meal for which the behaviour is tested + * @param user the user for whom the ideal calorie intake is computed + * @param pastDays the days for which the historic consumption bars are generated + * @return String the output expected to be returned by the system. + */ + private String buildExpectedOutputHistoricCaloriesTest(MealEntry testMeal, User user, List pastDays) { + String expectedAddPastMealResult = simulateAddingPastMeals(pastDays, user); + String expectedShowHistoricCalories = simulateHistoricCalories(testMeal, pastDays); + + return UI.simulateInitOutputAddMeal() + + expectedAddPastMealResult + + expectedShowHistoricCalories + + System.lineSeparator() + + UI.simulateFareWell(); + } + + /** + * Helper method for the test 'addPastMealsAndShowHistory_success'. + * Creates the output String of the system when adding the past mealEntries. + * @param pastDays the days for which the mealEntries are added + * @param user the user for whom the mealEntries are entered as consumption + * @return String the output caused by adding the testMeal to each of the pastDays. + */ + private String simulateAddingPastMeals(List pastDays, User user) { + return pastDays.stream() + .map(date -> + UI.simulateReply("burger with 300 calories " + + "(at: " + date + ")", "Tracked: ") + + UI.simulateUsersConsumptionBar(300, date, user)) + .reduce("", (total, oneOutput) -> total + oneOutput + System.lineSeparator()); + } + + /** + * Helper method for the test 'addPastMealsAndShowHistory_success' + * @param testMealEntry the test meal that was added to each of the past days specified in the test + * @param pastDays the past days + * @return String the expected output of the system when executing the show historicCalories command + */ + private String simulateHistoricCalories(MealEntry testMealEntry, List pastDays) { + User userStub = User.createUserStub(); + double idealCalories = userStub.getTargetCalories(); + int testMealCalories = testMealEntry.getCalories(); + assert (int)idealCalories == 2674 : "Test assumes ideal daily calories of 2674"; + assert pastDays.size() == 10 : "Test expects an input of 10"; + assert testMealCalories == 300 : "Test expects a calorie intake of 300"; + assert testMealEntry.getName().orElse("") == "burger" : "Tests expects a burger as test meal"; + + String idealIntakeString = UI.simulateReply("2674", "Ideal Daily Caloric Intake: "); + String historyBarsString = simulateHistoryConsumptionBars(pastDays, idealCalories, 300); + String statsString = getStatsString(pastDays.get(9)); + + return idealIntakeString + historyBarsString + statsString; + } + + /** + * Helper method for the test 'addPastMealsAndShowHistory_success' + * Generates the consumptions bars in the format of the show historicCalories command. + * @param pastDays List days for which the consumptions bars are generated + * @param idealCalories Double calories the respective user should consume dayls + * @param testMealCalories Integer calories of the testMeal that is consumed per day + * @return String the historic consumption bars for a daily consumption of testMealCalories + */ + private String simulateHistoryConsumptionBars(List pastDays, double idealCalories, int testMealCalories){ + return pastDays.stream().sorted() + .map(date -> UI.simulateHistoricConsumptionBar(idealCalories, testMealCalories, date)) + .reduce("", (total, oneBar) -> total + oneBar + System.lineSeparator()); + } + + /** + * Helper method for the test 'addPastMealsAndShowHistory_success'. + * @param maxConsumptionDate The day which the system will select as the maximum consumption day + * @return the Consumption statistics expected for the consumption tested in 'addPastMealsAndShowHistory_success' + */ + private String getStatsString(LocalDate maxConsumptionDate) { + return + " Stats over past 10 days" + System.lineSeparator() + + " Total Calories Consumed: 3000" + System.lineSeparator() + + " Total Ideal Calories: 26740" + System.lineSeparator() + + " Percentage of Total Ideal Calories : 11.0%" + System.lineSeparator() + + " Day With Heaviest Meal: " + maxConsumptionDate + System.lineSeparator() + + " Heaviest Meal Consumed: burger with 300 calories (at: "+ maxConsumptionDate + ")" + + System.lineSeparator() + + " Meals Consumption's Percentage of Daily Ideal Calories: 11.0%" + System.lineSeparator() + + " _____________________________________________________________________________"; + } + + /** + * Tests if deleting a meal option by its index is successful. + */ + @Test + void deleteMeal_existingMealByIndex_success() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "save meal burger /c300\n" + + "delete meal 1\n" + + "meal menu\nbye\n"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("burger with 300 calories", "Added to options: ") + + UI.simulateReply("Deleted option: burger with 300 calories", "") + + " _____________________________________________________________________________\n" + + " No meal options added yet\n" + + " _____________________________________________________________________________\n" + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests if attempting to delete a non-existent meal option by index returns an error. + */ + @Test + void deleteMeal_nonExistentIndex_failure() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "delete meal 1\nbye\n"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("No Meal Options", "Error: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests if trying to delete a meal option without specifying an index returns an error. + */ + @Test + void deleteMeal_noIndex_failure() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "save meal potato /c30\ndelete meal\nbye\n"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("potato with 30 calories", "Added to options: ") + + UI.simulateReply("Meal index needs to be an integer", "Error: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + + private static String simulateConsumptionMessageWithBar(int idealCalories, int consumedCalories) { + return UI.simulateReply("Ideal Daily Caloric Intake: " + idealCalories, "") + + UI.simulateString("Current Calories Consumed: " + consumedCalories) + + UI.buildConsumptionBar("% of Expected Calorie Intake Consumed: ", idealCalories, + consumedCalories, LocalDate.now(), true) + + "\n"; + } + + /** + * Tests if deleting a meal entry by index in the meal log is successful. + */ + @Test + void deleteMealEntry_existingEntryByIndex_success() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "add mealEntry pizza /c300\n" + + "delete mealEntry 1\nbye\n"; + int idealCalories = 2674; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("pizza with 300 calories (at: " + LocalDate.now() + ")", "Tracked: ") + + simulateConsumptionMessageWithBar(idealCalories, 300) + + UI.simulateReply("Deleted entry: pizza with 300 calories (at: " + LocalDate.now() + ")", "") + + simulateConsumptionMessageWithBar(idealCalories, 0) + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests if attempting to delete a non-existent meal entry by index returns an error. + */ + @Test + void deleteMealEntry_nonExistentIndex_failure() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "delete mealEntry 1\nbye\n"; + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("No Meal Entries", "Error: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests if trying to delete a meal entry without specifying an index returns an error. + */ + @Test + void deleteMealEntry_noIndex_failure() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "add mealEntry potato /c30\ndelete mealEntry\nbye\n"; + int idealCalories = 2674; + LocalDate today = LocalDate.now(); + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("potato with 30 calories (at: " + today + ")", "Tracked: ") + + simulateConsumptionMessageWithBar(idealCalories, 30) + + UI.simulateReply("Meal Entry index needs to be an integer", "Error: ") + + UI.simulateFareWell(); + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests the scenario where todayCalories command is executed successfully with meals added. + */ + @Test + void todayCalorieProgress_withMeals_success() { + ChatParser chatParser = new ChatParser(); + chatParser.cleanMealLists(); + String simulatedInput = "add mealEntry pizza /c500\nshow todayCalories\nbye\n"; + int idealCalories = 2674; + LocalDate today = LocalDate.now(); + String expectedOutput = UI.simulateInitOutput() + + UI.simulateReply("pizza with 500 calories (at: " + today + ")", "Tracked: ") + + simulateConsumptionMessageWithBar(idealCalories, 500) + + simulateConsumptionMessageWithBar(idealCalories, 500) + + UI.simulateFareWell(); + + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + /** + * Tests the scenario where todayCalories command is executed with no meals added. + */ + @Test + void todayCalorieProgress_noMeals_success() { + ChatParser chatParser = new ChatParser(); + String simulatedInput = "show todayCalories\nbye\n"; + int idealCalories = 2674; + LocalDate today = LocalDate.now(); + String expectedOutput = UI.simulateInitOutput() + + simulateConsumptionMessageWithBar(idealCalories, 0) + + UI.simulateFareWell(); + + compareChatParserOutput(chatParser, simulatedInput, expectedOutput); + } + + +} diff --git a/src/test/java/seedu/healthmate/CommandMapTest.java b/src/test/java/seedu/healthmate/CommandMapTest.java new file mode 100644 index 0000000000..e2b654e0b3 --- /dev/null +++ b/src/test/java/seedu/healthmate/CommandMapTest.java @@ -0,0 +1,61 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.healthmate.command.Command; +import seedu.healthmate.command.CommandMap; +import seedu.healthmate.command.commands.MealLogCommand; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + + +class CommandMapTest { + + @BeforeEach + void setUp() { + // Initialize CommandMap if necessary + // Since the static block initializes the COMMANDSMAP, no setup is needed + } + + @Test + void testGetCommandsRegularUsage() { + // Test when userInput matches the command exactly + String userInput = "list commands"; + String command = "list commands"; + + List commands = CommandMap.getCommands(userInput, command); + + // Expect that all commands are returned + assertEquals(14, commands.size()); + } + + @Test + void testGetCommandsInvalidCommand() { + // Test when userInput is not in the command map + String userInput = "list commands asdas"; + String command = "list commands"; + + List commands = CommandMap.getCommands(userInput, command); + + // Expect that all commands are returned + assertEquals(0, commands.size()); + } + + @Test + void testGetCommandsValidCommand() { + // Test when userInput is not in the command map + String userInput = "list commands meal log"; + String command = "list commands"; + + List commands = CommandMap.getCommands(userInput, command); + + // Expect that all commands are returned + assertEquals(1, commands.size()); + assertInstanceOf(MealLogCommand.class, commands.get(0)); + } +} + + diff --git a/src/test/java/seedu/healthmate/CommandTest.java b/src/test/java/seedu/healthmate/CommandTest.java new file mode 100644 index 0000000000..7d6c06e3d5 --- /dev/null +++ b/src/test/java/seedu/healthmate/CommandTest.java @@ -0,0 +1,42 @@ +package seedu.healthmate; + + + +import org.junit.jupiter.api.Test; +import seedu.healthmate.command.Command; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CommandTest { + + // Concrete subclass of Command for testing + private static class TestCommand extends Command { + + public TestCommand(String command, String format, String description) { + super(command, format, description); + } + } + + @Test + public void testGetCommand() { + Command command = new TestCommand("add", "add ", "Adds a meal to the system"); + assertEquals("add", command.getCommand(), "The command should be 'add'."); + } + + @Test + public void testToString() { + Command command = new TestCommand("add", "add ", "Adds a meal to the system"); + String expectedString = "Command: add\n Format: add \n Description: Adds a meal to the system"; + assertEquals(expectedString, command.toString(), "The toString method should return the expected " + + "formatted string."); + } + + @Test + public void testToStringWithDifferentValues() { + Command command = new TestCommand("remove", "remove ", "Removes a meal from the system"); + String expectedString = "Command: remove\n Format: remove \n Description: Removes a meal " + + "from the system"; + assertEquals(expectedString, command.toString(), "The toString method should return the expected formatted " + + "string for different values."); + } +} diff --git a/src/test/java/seedu/healthmate/HealthGoalTest.java b/src/test/java/seedu/healthmate/HealthGoalTest.java new file mode 100644 index 0000000000..ff84f7e762 --- /dev/null +++ b/src/test/java/seedu/healthmate/HealthGoalTest.java @@ -0,0 +1,95 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import seedu.healthmate.core.HealthGoal; + +/** + * Test class for the HealthGoal class. + */ +public class HealthGoalTest { + + private HealthGoal healthGoal; + + /** + * Sets up the test environment before each test. + */ + @BeforeEach + public void setUp() { + // Initialize with a default health goal for each test + healthGoal = new HealthGoal(2); + } + + /** + * Tests saving a valid health goal. + */ + @Test + public void testSaveHealthGoal_validGoal() { + healthGoal.saveHealthGoal(1); + assertEquals("WEIGHT_LOSS", healthGoal.getCurrentHealthGoal(), + "Expected health goal to be WEIGHT_LOSS"); + + healthGoal.saveHealthGoal(2); + assertEquals("STEADY_STATE", healthGoal.getCurrentHealthGoal(), + "Expected health goal to be STEADY_STATE"); + + healthGoal.saveHealthGoal(3); + assertEquals("BULKING", healthGoal.getCurrentHealthGoal(), + "Expected health goal to be BULKING"); + } + + /** + * Tests saving an invalid health goal. + */ + @Test + public void testSaveHealthGoal_invalidGoal() { + healthGoal.saveHealthGoal(4); + assertEquals("STEADY_STATE", healthGoal.getCurrentHealthGoal(), + "Health goal should remain unchanged for invalid input"); + } + + /** + * Tests target calories calculation for a male with a weight loss goal. + */ + @Test + public void testGetTargetCalories_maleWeightLoss() { + healthGoal.saveHealthGoal(1); + double targetCalories = healthGoal.getTargetCalories(180, 75, true, 25); + assertEquals(1633.0, targetCalories, 0.01, + "Expected target calories for male weight loss to be 1270.52"); + } + + /** + * Tests target calories calculation for a female with a bulking goal. + */ + @Test + public void testGetTargetCalories_femaleBulking() { + healthGoal.saveHealthGoal(3); + double targetCalories = healthGoal.getTargetCalories(160, 55, false, 30); + assertEquals(1850.0, targetCalories, 0.01, + "Expected target calories for female bulking to be 1982.94"); + } + + /** + * Tests target calories calculation for steady state. + */ + @Test + public void testGetTargetCalories_steadyState() { + healthGoal.saveHealthGoal(2); + double targetCalories = healthGoal.getTargetCalories(170, 68, true, 28); + assertEquals(1821.0, targetCalories, 0.01, + "Expected target calories for steady state to be 1821.86"); + } + + /** + * Tests the toString() method. + */ + @Test + public void testToString() { + healthGoal.saveHealthGoal(3); + assertEquals("BULKING", healthGoal.toString(), + "Expected toString() to return BULKING"); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/healthmate/HealthMateTest.java similarity index 77% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/seedu/healthmate/HealthMateTest.java index 2dda5fd651..75ceacf4a0 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/healthmate/HealthMateTest.java @@ -1,10 +1,10 @@ -package seedu.duke; +package seedu.healthmate; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -class DukeTest { +class HealthMateTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/seedu/healthmate/HistoryTrackerTest.java b/src/test/java/seedu/healthmate/HistoryTrackerTest.java new file mode 100644 index 0000000000..5cf1263115 --- /dev/null +++ b/src/test/java/seedu/healthmate/HistoryTrackerTest.java @@ -0,0 +1,18 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.File; + +import seedu.healthmate.services.HistoryTracker; + +public class HistoryTrackerTest { + + @Test + public void testCreateDataDirectory() { + HistoryTracker historyTracker = new HistoryTracker(); + File dataDirectory = new File("data"); + assertEquals(true, dataDirectory.exists(), "Data directory should be created"); + dataDirectory.delete(); + } +} diff --git a/src/test/java/seedu/healthmate/ParametersTest.java b/src/test/java/seedu/healthmate/ParametersTest.java new file mode 100644 index 0000000000..cb3931e30e --- /dev/null +++ b/src/test/java/seedu/healthmate/ParametersTest.java @@ -0,0 +1,160 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; + +import seedu.healthmate.exceptions.BadCalorieException; +import seedu.healthmate.exceptions.BadPortionException; +import seedu.healthmate.exceptions.BadTimestampException; +import seedu.healthmate.exceptions.EmptyCalorieException; +import seedu.healthmate.exceptions.EmptyTimestampException; +import seedu.healthmate.utils.Parameter; + +public class ParametersTest { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + } + + /** + * Test cases for getCalories method + */ + @Test + public void testGetCalories() { + // valid case + try { + int calories = Parameter.getCalories("add mealEntry grapes /c400 /p3"); + assertEquals(400, calories); + } catch (Exception e) { + // Shouldn't throw exception for valid input + } + + // missing calories (throws EmptyCalorieException) + assertThrows(EmptyCalorieException.class, () -> { + Parameter.getCalories("add mealEntry grapes /p3"); + }); + + // bad format (throws BadCalorieException) + assertThrows(EmptyCalorieException.class, () -> { + Parameter.getCalories("add mealEntry grapes /1ch5 /p3"); + }); + + // bad format (throws BadCalorieException) + assertThrows(BadCalorieException.class, () -> { + Parameter.getCalories("add mealEntry grapes /c123123asdasd /p3"); + }); + + // bad format (throws BadCalorieException) + assertThrows(EmptyCalorieException.class, () -> { + Parameter.getCalories("add mealEntry grapes /abc 400 /p3"); + }); + + } + + /** + * Test cases for getPortions method + */ + @Test + public void testGetPortions() { + // valid case + try { + int portions = Parameter.getPortions("add mealEntry grapes /c400 /p3"); + assertEquals(3, portions); + } catch (Exception e) { + // Shouldn't throw exception for valid input + fail("Exception thrown for valid input: " + e.getMessage()); + } + + // missing portions, default to 1 + try { + int portions = Parameter.getPortions("add mealEntry grapes /c400"); + assertEquals(1, portions); // Default to 1 when portions are not specified + } catch (Exception e) { + // Shouldn't throw exception for missing portion, default to 1 + fail("Exception thrown for valid input: " + e.getMessage()); + } + + // bad format (throws BadPortionException) + assertThrows(BadPortionException.class, () -> { + Parameter.getPortions("add mealEntry grapes /c400 /pabc"); + }); + + // bad format (throws BadPortionException) + assertThrows(BadPortionException.class, () -> { + Parameter.getPortions("add mealEntry grapes /c400 /p3abc"); + }); + + // valid with spaces + try { + int portions = Parameter.getPortions("add mealEntry grapes /p2 "); + assertEquals(2, portions); // Spaces should be ignored + } catch (Exception e) { + // Shouldn't throw exception for valid input with spaces + fail("Exception thrown for valid input: " + e.getMessage()); + } + + // no portion signaller, default to 1 + try { + int portions = Parameter.getPortions("add mealEntry grapes /c400"); + assertEquals(1, portions); // Default to 1 when portions are not specified + } catch (Exception e) { + // Shouldn't throw exception for missing portion, default to 1 + fail("Exception thrown for valid input: " + e.getMessage()); + } + } + + /** + * Test cases for getTimestamp method. + */ + @Test + public void testGetTimestamp() { + // valid case + try { + LocalDate date = Parameter.getTimestamp("add mealEntry grapes /c400 /t2024-11-05"); + assertEquals(LocalDate.of(2024, 11, 5), date); + } catch (Exception e) { + // Shouldn't throw an exception for valid input + fail("Exception thrown for valid input: " + e.getMessage()); + } + + // missing timestamp (throws EmptyTimestampException) + assertThrows(EmptyTimestampException.class, () -> { + Parameter.getTimestamp("add mealEntry grapes /c400"); + }); + + // invalid date format (throws BadTimestampException) + assertThrows(BadTimestampException.class, () -> { + Parameter.getTimestamp("add mealEntry grapes /c400 /t05-11-2024"); + }); + + // invalid date format with characters (throws BadTimestampException) + assertThrows(BadTimestampException.class, () -> { + Parameter.getTimestamp("add mealEntry grapes /c400 /t2024-11-aa"); + }); + + // valid case with spaces after timestamp + try { + LocalDate date = Parameter.getTimestamp(" add mealEntry grapes /c400 /t2024-11-05 "); + assertEquals(LocalDate.of(2024, 11, 5), date); // Leading and trailing spaces ignored + } catch (Exception e) { + // Shouldn't throw an exception for valid input with spaces + fail("Exception thrown for valid input with spaces: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/seedu/healthmate/RecipeMapTest.java b/src/test/java/seedu/healthmate/RecipeMapTest.java new file mode 100644 index 0000000000..de01ff4c5b --- /dev/null +++ b/src/test/java/seedu/healthmate/RecipeMapTest.java @@ -0,0 +1,35 @@ +package seedu.healthmate; + +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.recommender.RecipeMap; +import seedu.healthmate.recommender.Recipe; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecipeMapTest { + @Test + public void testGetRecipesByGoalReturnsFilteredRecipes() { + Goals userGoal = Goals.BULKING; + List recipes = RecipeMap.getRecipesByGoal(userGoal); + for (Recipe recipe : recipes) { + assertEquals(userGoal, recipe.getGoal()); + } + + userGoal = Goals.STEADY_STATE; + recipes = RecipeMap.getRecipesByGoal(userGoal); + for (Recipe recipe : recipes) { + assertEquals(userGoal, recipe.getGoal()); + } + + userGoal = Goals.WEIGHT_LOSS; + recipes = RecipeMap.getRecipesByGoal(userGoal); + for (Recipe recipe : recipes) { + assertEquals(userGoal, recipe.getGoal()); + } + } + +} diff --git a/src/test/java/seedu/healthmate/RecipeTest.java b/src/test/java/seedu/healthmate/RecipeTest.java new file mode 100644 index 0000000000..3184d22557 --- /dev/null +++ b/src/test/java/seedu/healthmate/RecipeTest.java @@ -0,0 +1,55 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.Test; +import seedu.healthmate.recommender.Goals; +import seedu.healthmate.recommender.Recipe; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RecipeTest { + + // A concrete subclass of Recipe to test its functionality + private static class TestRecipe extends Recipe { + + public TestRecipe(String name, int calories, int protein, int carbs, int fat, int fiber, String recipe, + Goals goal) { + super(name, calories, protein, carbs, fat, fiber, recipe, goal); + } + } + + @Test + public void testGetCalories() { + Recipe recipe = new TestRecipe("Test Recipe", 300, 20, 40, 10, 5, "Ingredient 1\nIngredient 2", Goals.BULKING); + assertEquals(300, recipe.getCalories(), "The calories should be 300."); + } + + @Test + public void testGetGoal() { + Recipe recipe = new TestRecipe("Test Recipe", 300, 20, 40, 10, 5, "Ingredient 1\nIngredient 2", Goals.BULKING); + assertEquals(Goals.BULKING, recipe.getGoal(), "The goal should be BULKING."); + } + + @Test + public void testToString() { + Recipe recipe = new TestRecipe("Test Recipe", 300, 20, 40, 10, 5, + "Ingredient 1\nIngredient 2", Goals.BULKING); + String expectedString = """ + Test Recipe: 300 calories + Protein: 20g + Carbs: 40g + Fat: 10g + \ + Fiber: 5g + Ingredient 1 + Ingredient 2 + """; + assertEquals(expectedString, recipe.toString(), "The toString method should return the expected " + + "formatted string."); + } + + @Test + public void testGetCaloriesZero() { + Recipe recipe = new TestRecipe("Zero Calorie Recipe", 0, 0, 0, 0, 0, "No ingredients", Goals.STEADY_STATE); + assertEquals(0, recipe.getCalories(), "The calories should be 0."); + } +} diff --git a/src/test/java/seedu/healthmate/UITest.java b/src/test/java/seedu/healthmate/UITest.java new file mode 100644 index 0000000000..6b28060d92 --- /dev/null +++ b/src/test/java/seedu/healthmate/UITest.java @@ -0,0 +1,85 @@ +package seedu.healthmate; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import seedu.healthmate.services.UI; + + +public class UITest { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final String SEPARATOR = + "_____________________________________________________________________________"; + private static final String INDENTATION = " "; + private static final String LINE = INDENTATION + SEPARATOR; + private static final String FRAME_LINE = LINE + LINE_SEPARATOR; + private static final String LOGO = + INDENTATION + " |\n" + + INDENTATION + " \\\\|//\n" + + INDENTATION + " \\\\|//\n" + + INDENTATION + " \\\\\\|///\n" + + INDENTATION + " \\\\\\|///\n" + + INDENTATION + " \\\\|//\n" + + INDENTATION + " \\|/\n" + + INDENTATION + " |\n"; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + } + + /** + * Tests if print greeting methods prints correctly + */ + @Test + public void testPrintGreeting() { + UI.printGreeting(); + String expectedOutput = INDENTATION + LOGO + + LINE_SEPARATOR + + LINE + LINE_SEPARATOR + + INDENTATION + "Welcome to HealthMate" + LINE_SEPARATOR + + INDENTATION + "Let's get healthy!" + LINE_SEPARATOR + + LINE + LINE_SEPARATOR; + + assertEquals(expectedOutput, outContent.toString()); + } + + /** + * Tests if farewell message prints correctly + */ + @Test + public void testPrintFarewell() { + UI.printFarewell(); + String expectedOutput = INDENTATION + "Stay healthy!" + LINE_SEPARATOR + LINE + LINE_SEPARATOR; + assertEquals(expectedOutput, outContent.toString()); + } + + /** + * Tests if reply method works with and without arguments + */ + @Test + public void testPrintReply() { + UI.printReply("Test input", "Action performed: "); + String expectedOutput = LINE + LINE_SEPARATOR + + INDENTATION + "Action performed: Test input" + LINE_SEPARATOR + + LINE + LINE_SEPARATOR; + assertEquals(expectedOutput, outContent.toString()); + + UI.printReply("", ""); + String expectedOutput2 = LINE + LINE_SEPARATOR + + INDENTATION + LINE_SEPARATOR + + LINE + LINE_SEPARATOR; + assertEquals(expectedOutput + expectedOutput2, outContent.toString()); + } + +} diff --git a/src/test/java/seedu/healthmate/UserHistoryTrackerTest.java b/src/test/java/seedu/healthmate/UserHistoryTrackerTest.java new file mode 100644 index 0000000000..707b2f70a3 --- /dev/null +++ b/src/test/java/seedu/healthmate/UserHistoryTrackerTest.java @@ -0,0 +1,69 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; + +import seedu.healthmate.core.HealthGoal; +import seedu.healthmate.core.User; +import seedu.healthmate.core.UserEntryList; +import seedu.healthmate.services.UserHistoryTracker; + +public class UserHistoryTrackerTest { + private UserHistoryTracker userHistoryTracker; + + @BeforeEach + public void setUp() { + userHistoryTracker = new UserHistoryTracker(); + } + + /** + * Verifies that adding a User entry to UserHistoryTracker successfully saves the entry. + * Checks if the last saved user entry matches the expected User data. + */ + @Test + public void shouldSaveUserWhenEntryIsAdded() { + User user = User.createAlternativeUserStub(); + userHistoryTracker.addUserEntry(user); + String expectedUser = user.toString(); + String savedUser = userHistoryTracker.getLatestUser() + .map(x -> x.toString()) + .orElseThrow(() -> new AssertionError("Expected a saved user entry, but none was found")); + assertEquals(expectedUser, savedUser); + } + + /** + * Tests that UserEntryList loads correctly from the save file. Clears the save file, + * adds multiple User entries to UserHistoryTracker, and constructs the expected content. + * Loads the UserEntryList and verifies it matches the expected result. + */ + @Test + public void shouldLoadUserEntryListWhenSaveFileExists() { + userHistoryTracker.clearSaveFile(); + User altUser = User.createAlternativeUserStub(); + User user = User.createUserStub(); + StringBuilder expectedSaveFileContent = new StringBuilder(); + for (int i = 0; i < 5; i++) { + userHistoryTracker.addUserEntry(altUser); + expectedSaveFileContent.append("\n").append(altUser.toString()); + userHistoryTracker.addUserEntry(user); + expectedSaveFileContent.append("\n").append(user.toString()); + } + + UserEntryList userEntryList = userHistoryTracker.loadUserEntries() + .orElseThrow(() -> new AssertionError("Expected a loaded UserEntryList, but none was found")); + + assertEquals(expectedSaveFileContent.toString().trim(), userEntryList.toString()); + } + + @AfterEach + public void userHistoryTracker_cleanup() { + userHistoryTracker.clearSaveFile(); + HealthGoal healthGoal = new HealthGoal(3); + User testUser = new User(180, 80, true, 20, healthGoal, true); + + userHistoryTracker.addUserEntry(testUser); + } + +} diff --git a/src/test/java/seedu/healthmate/WeightEntryDisplayTest.java b/src/test/java/seedu/healthmate/WeightEntryDisplayTest.java new file mode 100644 index 0000000000..cce8f0a81d --- /dev/null +++ b/src/test/java/seedu/healthmate/WeightEntryDisplayTest.java @@ -0,0 +1,62 @@ +package seedu.healthmate; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.healthmate.core.User; +import seedu.healthmate.core.UserEntryList; +import seedu.healthmate.core.WeightEntryDisplay; + + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WeightEntryDisplayTest { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + public void tearDown() { + System.setOut(originalOut); + } + + @Test + public void testShouldPrintGraph() { + // Setup test data + UserEntryList users = new UserEntryList(); + + users.addUserEntry(new User(70.0, 79.0, true, 25,"WEIGHT_LOSS", 2200, "2024-10-24 16:34:15", true)); + users.addUserEntry(new User(70.0, 78.0, true, 25, "WEIGHT_LOSS", 2200, "2024-10-25 16:34:15", true)); + users.addUserEntry(new User(70.0, 77.5, true, 25, "WEIGHT_LOSS", 2200, "2024-10-26 16:34:15", true)); + users.addUserEntry(new User(70.0, 76.0, true, 25, "WEIGHT_LOSS", 2200, "2024-10-27 16:34:15", true)); + users.addUserEntry(new User(70.0, 75.0, true, 25, "WEIGHT_LOSS", 2200, "2024-10-28 16:34:15", true)); + users.addUserEntry(new User(70.0, 74.0, true, 25, "WEIGHT_LOSS", 2200, "2024-10-29 16:34:15", true)); + users.addUserEntry(new User(70.0, 73.5, true, 25, "WEIGHT_LOSS", 2200, "2024-10-30 16:34:15", true)); + users.addUserEntry(new User(70.0, 72.0, true, 25, "WEIGHT_LOSS", 2200, "2024-10-31 16:34:15", true)); + users.addUserEntry(new User(70.0, 71.0, true, 25, "WEIGHT_LOSS", 2200, "2024-11-01 16:34:15", true)); + users.addUserEntry(new User(70.0, 70.0, true, 25, "WEIGHT_LOSS", 2200, "2024-11-02 16:34:15", true)); + + Optional optionalUserEntryList = Optional.of(users); + + + WeightEntryDisplay.display(optionalUserEntryList); + + + String output = outContent.toString(); + + + assertTrue(output.contains("Weight Timeline")); + assertTrue(output.contains("|")); + assertTrue(output.contains("--")); + } +} diff --git a/text-ui-test/ACTUAL.TXT b/text-ui-test/ACTUAL.TXT new file mode 100644 index 0000000000..16ee9a47a1 --- /dev/null +++ b/text-ui-test/ACTUAL.TXT @@ -0,0 +1,40 @@ + _____________________________________________________________________________ + Meal Entries Loaded Successfully! + Meal Options Loaded Successfully! + _____________________________________________________________________________ + | + \\|// + \\|// + \\\|/// + \\\|/// + \\|// + \|/ + | + + _____________________________________________________________________________ + Welcome to HealthMate + Let's get healthy! + _____________________________________________________________________________ + Create your profile: please enter... + Height in cm (e.g. 180): + Weight in kg (e.g. 80): + Gender (male or female): + Age (e.g. 20): + _____________________________________________________________________________ + Enter a health goal: + Choose one of the following: + 1. WEIGHT_LOSS + 2. STEADY_STATE + 3. BULKING + Enter the necessary number (1,2,3) to select + _____________________________________________________________________________ + _____________________________________________________________________________ + Does progressbar below look well formatted as a questionmark? + Enter: {y} if it looks good. Enter: {n} if it contains weird characters such as '?'. + _____________________________________________________________________________ + ███████░░░░░░░░░░░░░░░░░░░░░░| 25%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + Profile creation Successful! + _____________________________________________________________________________ + Great! You can now begin to use the app! + _____________________________________________________________________________ + Use the `list commands` command to have a look at all commands. diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..16ee9a47a1 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,40 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + _____________________________________________________________________________ + Meal Entries Loaded Successfully! + Meal Options Loaded Successfully! + _____________________________________________________________________________ + | + \\|// + \\|// + \\\|/// + \\\|/// + \\|// + \|/ + | -What is your name? -Hello James Gosling + _____________________________________________________________________________ + Welcome to HealthMate + Let's get healthy! + _____________________________________________________________________________ + Create your profile: please enter... + Height in cm (e.g. 180): + Weight in kg (e.g. 80): + Gender (male or female): + Age (e.g. 20): + _____________________________________________________________________________ + Enter a health goal: + Choose one of the following: + 1. WEIGHT_LOSS + 2. STEADY_STATE + 3. BULKING + Enter the necessary number (1,2,3) to select + _____________________________________________________________________________ + _____________________________________________________________________________ + Does progressbar below look well formatted as a questionmark? + Enter: {y} if it looks good. Enter: {n} if it contains weird characters such as '?'. + _____________________________________________________________________________ + ███████░░░░░░░░░░░░░░░░░░░░░░| 25%|░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + Profile creation Successful! + _____________________________________________________________________________ + Great! You can now begin to use the app! + _____________________________________________________________________________ + Use the `list commands` command to have a look at all commands. diff --git a/text-ui-test/data/meal_entries.csv b/text-ui-test/data/meal_entries.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/text-ui-test/data/meal_options.csv b/text-ui-test/data/meal_options.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/text-ui-test/data/user_data.csv b/text-ui-test/data/user_data.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..827fdafa4a 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,7 @@ -James Gosling \ No newline at end of file +180 +80 +male +20 +1 +y +bye \ No newline at end of file diff --git a/text-ui-test/logs/seedu.healthmate.ChatParser.log b/text-ui-test/logs/seedu.healthmate.ChatParser.log new file mode 100644 index 0000000000..4d2a4f1232 --- /dev/null +++ b/text-ui-test/logs/seedu.healthmate.ChatParser.log @@ -0,0 +1,12 @@ +Okt. 17, 2024 2:31:13 PM seedu.healthmate.services.ChatParser +INFORMATION: Initializing HistoryTracker +Okt. 17, 2024 2:31:13 PM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealEntries +Okt. 17, 2024 2:31:13 PM seedu.healthmate.services.ChatParser +INFORMATION: Loaded MealOptions +Okt. 17, 2024 2:31:13 PM seedu.healthmate.services.ChatParser run +INFORMATION: Checking if user data exists +Okt. 17, 2024 2:31:13 PM seedu.healthmate.services.ChatParser run +INFORMATION: Getting next user input line +Okt. 17, 2024 2:31:13 PM seedu.healthmate.services.ChatParser run +INFORMATION: User closes application diff --git a/text-ui-test/logs/seedu.healthmate.services.ChatParser.log b/text-ui-test/logs/seedu.healthmate.services.ChatParser.log new file mode 100644 index 0000000000..4c751c46e5 --- /dev/null +++ b/text-ui-test/logs/seedu.healthmate.services.ChatParser.log @@ -0,0 +1,16 @@ +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser +INFO: Initialized HistoryTracker +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser +INFO: Loaded MealEntries +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser +INFO: Loaded MealOptions +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser +INFO: Initializing UserHistoryTracker +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser +INFO: ChatParser correctly initialized. +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser run +INFO: Checking if user data exists +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser run +INFO: User is: 180.0,80.0,true,20,WEIGHT_LOSS,1719.0,2024-11-11 21:40:38,true +Nov 11, 2024 9:40:38 PM seedu.healthmate.services.ChatParser parseUserInput +INFO: Getting next user input line diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 25ac7a2989..b6f2488ce3 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -12,8 +12,7 @@ for /f "tokens=*" %%a in ( set jarloc=%%a ) -java -jar %jarloc% < ..\..\text-ui-test\input.txt > ..\..\text-ui-test\ACTUAL.TXT - cd ..\..\text-ui-test +java -jar ../build/libs/%jarloc% < input.txt > ACTUAL.TXT FC ACTUAL.TXT EXPECTED.TXT >NUL && ECHO Test passed! || Echo Test failed! diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh index 1dcbd12021..b6e8516ff8 100755 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -11,7 +11,6 @@ cd text-ui-test java -jar $(find ../build/libs/ -mindepth 1 -print -quit) < input.txt > ACTUAL.TXT cp EXPECTED.TXT EXPECTED-UNIX.TXT -dos2unix EXPECTED-UNIX.TXT ACTUAL.TXT diff EXPECTED-UNIX.TXT ACTUAL.TXT if [ $? -eq 0 ] then diff --git a/tp/.idea/misc.xml b/tp/.idea/misc.xml new file mode 100644 index 0000000000..80d3a4e299 --- /dev/null +++ b/tp/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tp/.idea/workspace.xml b/tp/.idea/workspace.xml new file mode 100644 index 0000000000..72377a0ec9 --- /dev/null +++ b/tp/.idea/workspace.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + 1727883657946 + + + + + + \ No newline at end of file