diff --git a/.gitignore b/.gitignore index 2873e189e1..1c2255e49c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT + +data/* \ No newline at end of file diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..1d2b1b8f1b --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: seedu.lifetrack.LifeTrack + diff --git a/build.gradle b/build.gradle index ea82051fab..9fec70c1ae 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.lifetrack.LifeTrack") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("lifetrack") archiveClassifier.set("") } @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..4b36fcfdb1 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://avatars.githubusercontent.com/u/64789669?v=4) | Paturi Karthik | [Github](https://github.com/paturikarthik) | [Portfolio](team/paturikarthik.md) | +| ![](https://avatars.githubusercontent.com/u/110764881?s=400&u=f41e3f40315f394bd71538063882c06bcfa2b624&v=4) | Shawn Pong | [Github](https://github.com/shawnpong) | [Portfolio](team/shawnpong.md) | +| ![](https://avatars.githubusercontent.com/u/76645229?s=400&u=235aba3bc4b5de57d0a76e24506f094e28734637&v=4) | Rex Yong Jin Xiang | [Github](https://github.com/rexyyong) | [Portfolio](team/rexyyong.md) | +| ![](https://via.placeholder.com/100.png?text=Photo) | Yanyu | [Github](https://github.com/a-wild-chocolate/) |[Portfolio](team/a-wild-chocolate.md)| +| ![](https://via.placeholder.com/100.png?text=Photo) | Ong Wei Xiang | [GitHub](https://github.com/owx0130) | [Portfolio](team/owx0130.md) | | diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..305ed54040 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,657 @@ # Developer Guide +## Quick links +* [Acknowledgements](#acknowledgements) +* [Design](#design) + * [Calories Component](#calories-component) + * [Hydration Component](#hydration-component) + * [Sleep Component](#sleep-component) + * [User Component](#user-component) +* [Implementation](#implementation) + * [Adding calorie entries feature](#adding-calorie-entries-feature) + * [Calculating calorie requirements based on user's goals](#calculating-calorie-requirements-based-on-a-users-goals) + * [Calories list feature](#calories-list-feature) + * [Calories delete feature](#calories-delete-feature) + * [Parsing user input for hydration entries](#parsing-user-input-for-hydration-entries) + * [Calculating hydration requirements for each user](#calculating-hydration-requirements-for-each-user) + * [Hydration list feature](#hydration-list-feature) + * [Hydration delete feature](#hydration-delete-feature) + * [Adding sleep entries feature](#adding-sleep-entries-feature) + * [User details feature](#user-details-feature) + * [Parsing user input for sleep entries](#parsing-user-input-for-sleep-entries) + * [Sleep list feature](#sleep-list-feature) + * [Sleep delete feature](#sleep-delete-feature) + * [Calculating sleep requirements for each user (Planning)](#calculating-sleep-requirements-for-each-user-planning) +* [Appendix A: Product scope](#appendix-a-product-scope) +* [Appendix B: User Stories](#appendix-b-user-stories) +* [Appendix C: Non-Functional Requirements](#appendix-c-non-functional-requirements) +* [Appendix D: Glossary](#appendix-d-glossary) +* [Appendix E: Instructions for manual testing](#appendix-e-instructions-for-manual-testing) + ## Acknowledgements +To calculate the number of calories needed for a user to hit their goal, we used the following references: +1. [BMR Formula based on gender](https://www.inchcalculator.com/bmr-calculator/) +2. [BMR Formula based on exercise levels and goal](https://www.musclehacking.com/calorie-calculator/) +## Design + +### Calories Component +Here's a (partial) class diagram of the `calories` component. + +![calories.png](assets%2Fcalories.png) + +The calories component consists of the following Classes +1. `Ui` : Handles user and program interaction. +2. `CalorieList` : Handles the list of calories entries. +3. `CaloriesFileHandler` : Handles the saving and reading of data from file. +4. `ParserCalories` : Handles the parsing of user input to determine type of command. +5. `Entry` : Handles calories entries data. +6. `OutputEntry` : Handles calories output entries data. +7. `InputEntry` : Handles calories input entries data. +8. `Food` : Handles macronutrients data for calories input. + +The sequence diagram bellow illustrates the interactions within the `calories` component, taking +`calories in donut c/1000 d/2024-04-10` call as an example. +![calories_component.png](assets%2Fcalories_component.png) + +How the `calories` component works: +1. When the user keys in the `calories in donut c/1000 d/2024-04-10` command, +the input is sent to `Ui#handleCaloriesInput(String, CalorieList)`, which calls +`CalorieList#addEntry(String)`. + +2. Inside `CalorieList#addEntry(String)`, the function `ParserCalories#parseCaloriesInput(String, int)` +is then called to extract information such as the description, number of calories, and date of entry. + +3. With the extracted information, the function `ParserCalories#makeNewInputEntry(int, String, int, String)` +is called, which creates a new entry of `InputEntry` that extends `Entry`. The `InputEntry` object is then returned +to the caller, `CalorieList#addEntry(String)` which was called in step 2. + +4. The returned `InputEntry` object is added into the `calorieArrayList` member of type +`ArrayList` in the `CalorieList`, via the `ArrayList.add()` method. + +5. `CalorieList#UpdateFile()` is then called, which calls `CaloriesFileHandler#writeEntries(ArrayList)`. +Within that function, `CaloriesFileHandler#writeToFile(String)` function is called, which writes the new data +into the data file. + +6. If the dates of entries are not sorted in ascending order, `CalorieList#sortEntriesByDate()` +function is called, which sorts the entries in ascending order. + + +### Hydration Component +Here's a (partial) class diagram of the `hydration` component. + +![hydration.png](assets%2Fhydration.png) + +The hydration component consists of the following Classes +1. `Ui` : Handles user and program interaction. +2. `HydrationList` : Handles the list of hydration entries. +3. `HydrationFileHandler` : Handles the saving and reading of data from file. +4. `ParserHydration` : Handles the parsing of user input to determine type of command. +6. `HydrationEntry` : Handles hydration entries data. + +The sequence diagram bellow illustrates the interactions within the `hydration` component, taking +`hydration in milo v/100 d/2024-04-10` call as an example. +![hydration_component.png](assets%2Fhydration_component.png) + +1. When the user inputs the hydration in `water v/500 d/2024-04-15` command, the input is sent to +`Ui#handleHydrationInput(String, HydrationList)`, which then calls `HydrationList#addEntry(String)`. + +2. Inside `HydrationList#addEntry(String)`, the function `ParserHydration#parseHydrationInput(String, int)` is called to +extract information such as the description, volume, and date of entry. + +3. With the extracted information, the function `ParserHydration#makeNewInputEntry(int, String, int, String)` is called, +which creates a new entry of `HydrationEntry` that extends `Entry`. The `HydrationEntry` object is then returned to the +caller, `HydrationList#addEntry(String)` which was called in step 2. + +4. The returned `HydrationEntry` object is added into the `hydrationArrayList` member of type `ArrayList` in the +HydrationList, via the ArrayList.add() method. + +5. `HydrationList#UpdateFile()` is then called, which calls `HydrationFileHandler#writeEntries(ArrayList)`. +Within that function, `HydrationFileHandler#writeToFile(String)` function is called, which writes the new data into the +data file. + +6. If the dates of entries are not sorted in ascending order, `HydrationList#sortEntriesByDate()` function is called, +which sorts the entries in ascending order. + +### Sleep Component +Here's a (partial) class diagram of the `sleep` component. + +![sleep.png](assets%2Fsleep.png) + +The sleep component consists of the following classes: +1. `Ui`: Handles user and program interaction. +2. `SleepList`: Handles the list of sleep records. +3. `SleepFileHandler`: Handles the saving and reading of data from the file. +4. `ParserSleep`: Handles the parsing of user input to determine type of command. +5. `Entry`: Handles all entries data. +6. `SleepEntry`: Handles sleep entries data. + +The sequence diagram bellow illustrates the interactions within the `sleep` component, taking `sleep add 7 d/2024-04-13` +call as an example. +![SleepAddSeqDiagram.png](assets%2FSleepAddSeqDiagram.png) + +How the `sleep` component works: +1. When the user keys in the`sleep add 7 d/2024-04-13` command, + the input is sent to `Ui#handleSleepInput(String, SleepList)`, which calls + `SleepList#addSleep(String)`. + +2. Inside `SleepList#addSleep(String)`, the function `ParserSleep#parseSleepInput(String)` + is then called to extract information such as the duration and date by calling `parseDate(String)` and `parseDuration(String)`. + +3. With the extracted information,a new entry of `SleepEntry` that extends `Entry` will be created. The `SleepEntry` object is then returned + to the caller, `SleepList#addSleep(String)` which was called in step 2. + +4. The returned `SleepEntry` object is added into the `sleepList` member of type + `ArrayList` in the `SleepList`, via the `ArrayList.add()` method. + +5. `SleepList#UpdateFile()` is then called, which calls `SleepFileHandler#writeEntries(ArrayList)`. + Within that function, `SleepFileHandler#writeToFile(String)` function is called, which writes the new data + into the data file. + + +### User Component +Here's a (partial) class diagram of the `user` component. +![user.png](assets/user.png) + +How does `user` component work using `user setup` command as an example: +1. When the user keys in the `user setup` command, the input is sent to `Ui#handleUserCommands(String, User)`, which calls `User#setUp(String)`. +2. Inside `User#setUp(String)`, the function `ParserUser#parseSetUp(String, User)` is then called to extract information such as name, height, weight, age, sex, exerciseLevels, goals. The information is then stored int he `User` class. +3. The function `#UserFileHandler#writeUserData(User)` is then called, which calls `#UserFileHandler#writeToFile(String)` which updates user data to the file. + +## Implementation +### Adding calorie entries feature + +#### Implementation + +This functionality is facilitated by `UI`, `CalorieList`, and `ParserCalories`. It implements one operation, namely: +- `UI#handleCaloriesInput(String, CalorieList)` +- `CalorieList#addEntry(String)` +- `CalorieList#updateFile()` +- `ParserCalories#parseCaloriesInput(String)` + +This feature is activated when the user inputs a `calories in` or `calories out` command in the terminal. + +Given below is an example usage scenario and how this mechanism behaves at every step: + +- Step 1: When the user inputs the command `calories in burger c/200 d/2024-02-02` in the terminal, +the string is sent to `UI#handleCaloriesInput(String, CalorieList)`, which calls `CalorieList#addEntry(String)`. + +- Step 2: Inside `CalorieList#addEntry(String)`, the function `ParserCalories#parseCaloriesInput(String)` is then called to extract information such as the description, number of calories, and date of entry. + +- Step 3: The obtained information is sent to the private method `ParserCalories#makeNewInputEntry(String, int, String)` to create a new entry of class `InputEntry` that extends `Entry`. An `Entry` object is then returned to the caller, `CalorieList#addEntry(String)`. + +- Step 4: The returned `Entry` object is added into the `calorieArrayList` member of type `ArrayList` in the `CalorieList`, via the `ArrayList.add()` method. + +- Step 5: `CalorieList#updateFile()` is then called to update the data file with the new entry in the `CalorieList`. + +The sequence diagram for this feature is shown below: + +![CaloriesAddEntrySeqDiagram.png](assets%2FCaloriesAddEntrySeqDiagram.png) + +### User Details Feature + +#### Implementation + +This functionality is facilitated by `UI`, `User` and `UserUI` Classes. It implements the following +operation, namely: +- `UI#handleUserInput(String, User)` +- `UI#handleUserCommands(String, User)` +- `User#getUserDetails()` +- `UserUI#printUserDetails(User)` + +This feature is activated when the user inputs `user details` command in the terminal. + +Given below is an example usage scenario and how this mechanism behaves at every step: + +- Step 1: When the user inputs the command `user details` in the terminal, + the string is sent to `UI#handleUserInput(String, User)` and + `UI#handleUserCommands(String, User)`, which calls `User#getUserDetails()`. + +- Step 2: Inside `User#getUserDetails()`, the function `UserUI#printUserDetails(User)` + is then called. The user's details are printed out. + +- Step 3: After printing out the details, the program returns and awaits + the next command typed in by user. + +The class and sequence diagram for this feature is shown below: +Unrelated attributes and Classes were excluded. + +![UserClassDiagram.png](assets/UserClassDiagram.png) +![UserDetailsSequenceDiagram.png](assets/UserDetailsSequenceDiagram.png) + +### Calculating calorie requirements based on a user`s goals + +#### Implementation + +This functionality is facilitated by `UserGoals`. It implements one operation, namely: +- `UserGoals#getHealthInfo(User)` + +This operation is exposed in the `User` class as `User#getHealhInfo()`. + +Given below is an example usage scenario and how this mechanism behaves at every step: +- Step 1: When the user inputs the command `user setup` in the terminal, + the input string is sent to `Ui#handleUserCommands(User,String)`, which calls `User#setup(String)`. + +- Step 2: `User#setup(String)` calls `ParserUser#parseSetUp(String, User)` which parses the user's information such as + his height, weight, age, gender, exercise levels and intended goal and sets these information to the `User`. + +- Step 3: `User#setup(String)` then calls `User#getHealthInfo()` which calls `UserGoals#getHealthInfo(User)`. + +- Step 4: This method calculates the basal metabolism rate of the `user` using the user's height, weight, age and gender. + +- Step 5: Using the `user`'s exercise levels, the method calculates his active metabolism rate, and finally calculates the calories required by the user to achieve their goals based on their input. + +- Step 6: This value is set to `User.caloriesRequired` by `User#setCaloriesRequired(int)`. + +The Sequence Diagram for the above-mentioned process is as follows: + +![Sequence Diagram](assets/UserCalculateCaloriesSeqDiagram.png) + + +#### Design considerations + +- **Alternative 1 (current choice):** Uses an algorithm to find the number of calories needed + - Pros: Not dependent on external APIs + - Cons: Need to come up with an algorithm to use + +- **Alternative 2 :** Uses an API to get the calories needed + - Pros: No need to figure out the optimal algorithm + - Cons: Need to parse response to sieve out necessary information + +### Calories list feature + +#### Implementation + +This functionality is facilitated by `UI` and `CalorieList` Classes. It implements the following +operation, namely: +- `UI#handleUserInput(String, CalorieList)` +- `UI#handleCaloriesInput(String, CalorieList)` +- `CalorieList#printCalorieList()` +- `CalorieList#printCalorieInflow()` +- `CalorieList#printCalorieOutflow()` + +This feature is activated when the user inputs `calories list` command in the terminal. + +Given below is an example usage scenario and how this mechanism behaves at every step: + +- Step 1: When the user inputs the command `calories list` in the terminal, + the string is sent to `UI#handleUserInput(String, CalorieList)` and +`UI#handleCaloriesInput(String, CalorieList)`, which calls `CalorieList#printCalorieList()`. + +- Step 2: Inside `CalorieList#printCalorieList()`, the function `CalorieList#printCalorieInflow()` +is then called. Entries that are classified under `InputEntry` will be printed out. + +- Step 3: Inside `CalorieList#printCalorieList()`, the function `CalorieList#printCalorieInflow()` + is then called. Entries that are classified under `OuputEntry` will be printed out. + +- Step 4: After printing out both the Input Entries and Output Entries, the program returns and awaits +the next command typed in by user. + +The class and sequence diagram for this feature is shown below: +Unrelated attributes and Classes were excluded. + +![CaloriesListClassDiagram.png](assets%2FCaloriesListClassDiagram.png) +![CaloriesListSequenceDiagram.png](assets%2FCaloriesListSequenceDiagram.png) + +#### Design considerations +- **Alternative 1 (current choice):** Use one arrayList and use instanceof to print out +the Input Entries and Output Entries. + - Pros: Only 1 arrayList is required. + - Cons: Need to loop the same arrayList twice, which is less efficient. Furthermore, + when deleting entries from arrayList, we need to use a unique entryID instead of using the + index of entry in the arrayList. + +- **Alternative 2 :** Use 2 arraylists. One for Input Entries and one for Output Entries. + - Pros: More efficient, as each arrayList only needs to be looped through one time. + Furthermore, we can directly delete entries based on index, and no unique entryID field will be required. + - Cons: Troublesome to implement. Requires a lot of changes to current Class structure and code structure. + +### Calories delete feature + +The `calories delete` feature can delete the calories record at specific index of calorie list. This functionality is facilitated by `CaloriesList`. It implements one operation, namely: +- `deleteEntry(String line)` + +Given below is an example usage scenario and how this mechanism behaves at every step: +- Step 1: When the user inputs the command `calories delete INDEX` in the terminal, the string is sent to `Ui#handleUserInput()`, which will call `Ui#handleCaloriesInput()`. + +- Step 2: After the `Ui#handleCaloriesInput()` matching `delete calories` key word, the string will be passed into deleteEntry(String line) to execute delete process. + +- Step 3: The string will be divided to two substrings according to the command syntax. Index will be tried to get from the second substring by `Integer.parseInt()`. + +- Step 4: The calories record (`Entry`) stored in the `ArrayList caloriesList` will be deleted by calling `calorieArrayList.remove((index-1));` and a successful deleting message will be shown in terminal by calling `CalorieListUi#successfulDeletedMessage(toDelete)` + +- Step 5: The latest calories list will be updated to saving file by calling `CalorieList#updateFile()`. + +The Class diagram for Calories delete feature is shown below: + +![CaloriesDeleteClassDiagram](assets%2FcaloriesDeleteUML.jpg) + +### Parsing user input for hydration entries + +This functionality is facilitated by `ParserHydration`. It implements one operation, namely: +- `ParserHydration#parseHydrationInput(String input)` + +This operation is exposed in the `HydrationList` class as `HydrationList#addEntry(String)`. + +Given below is an example usage scenario and how this mechanism behaves at every step: +- Step 1: When the user inputs the command `hydration in Milo v/100 d/2022-01-02` in the terminal, + the string is sent to `HydrationList#addEntry(String)`, which calls `ParserHydration#parseHydrationInput(String)`. + +- Step 2: Using `String.split()`, the method extracts information such as the description, volume of beverage, and date of entry. The obtained information is sent to the private method `ParserHydration#makeNewInputEntry(String, int, String)` to create a new entry of class `HydrationEntry` that extends `Entry`. + +- Step 3: The created `HydrationEntry` instance is added into the `ArrayList` attribute of the `HydrationList`. + +### Calculating hydration requirements for each user -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +#### Design Considerations -## Design & implementation +**General Health Guidelines:** The recommended daily intake of water for an average adult is around 8 glasses or approximately 2000 milliliters. This guideline is commonly recommended by health authorities and organizations such as HealthHub. -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +**Ease of Implementation:** Setting a standard hydration requirement simplifies the tracking process for users. It provides a clear goal to strive for, making it easier for individuals to monitor and maintain their hydration levels consistently. +### Hydration list feature -## Product scope +The `hydration list` feature lists out the record of all the Hydration data that the user has keyed in. The Hydration data are all stored into a `ArrayList hydrationArrayList` attribute of the `HydrationList` Class. Hydration data are printed when the `printHydrationList()` function is called. + +The `printHydrationList()` function iterates through the `hydrationArrayList` and prints out the Entries according to its order in the Array List. + +The Class diagram for Hydration list feature is shown below. Unrelated attributes and Classes were excluded. + +![HydrationListClassDiagram.png](assets/HydrationListClassDiagram.png) + +### Hydration delete feature + +The `hydration delete` feature can delete the hydration record at specific index of hydration list. This functionality is facilitated by `HydrationList`. It implements one operation, namely: +- `deleteEntry(String line)` + +Given below is an example usage scenario and how this mechanism behaves at every step: +- Step 1: When the user inputs the command `hydration delete INDEX` in the terminal, the string is sent to `Ui#handleUserInput()`, which will call `Ui#handleHydrationInput()`. + +- Step 2: After the `Ui#handleHydrationInput()` matching `delete hydration` key word, the string will be passed into deleteEntry(String line) to execute delete process. + +- Step 3: The string will be divided to two substrings according to the command syntax. Index will be tried to get from the second substring by `Integer.parseInt()`. + +- Step 4: The hydration record (`Entry`) stored in the `ArrayList hydrationList` will be deleted by calling `hydrationArrayList.remove((index-1));` and a successful deleting message will be shown in terminal by calling `HydrationListUi#successfulDeletedMessage(toDelete)` + +- Step 5: The latest hydration list will be updated to saving file by calling `HydrationList#updateFile()`. + +The Sequence diagram for Hydration delete feature is shown below: +![HydrationDeleteEntrySeqDiagram.png](assets%2FHydrationDeleteEntrySeqDiagram.png) + +### Adding sleep entries feature + +#### Implementation + +This functionality is facilitated by `UI`, `SleepList`, `FileHandler` and `ParserSleep`. It implements one operation, namely: +- `UI#handleSleepInput(String,SleepList)` +- `SleepList#addEntry(String)` +- `ParserSleep#parseSleepInput(String)` +- `FileHandler#updateFile()` + +This feature is activated when the user inputs a `sleep add` command in the terminal. + +Given below is an example usage scenario and how this mechanism behaves at every step: + +- Step 1: When the user inputs the command `sleep add 7.5 d/2022-01-02` in the terminal, + the string is sent to `UI#handleSleepInput(String, SleepList)`, which calls `SleepList#addSleep(String)`. + +- Step 2: Inside `SleepList#addSleep(String)`, the function `ParserSleep#parseSleepInput(String)` is then called to extract information such as the duration and date of entry. + +- Step 3: It will create a new entry of class `SleepEntry` that extends `Entry`based on the information. + +- Step 4: The created `SleepEntry` instance is added into the `ArrayList sleepList` attribute of the `SleepList`. + +- Step 5: `FileHandler#updateFile()` is then called to update the data file with the new entry in the `SleepList`. + +The sequence diagram for this feature is shown below: +![SleepAddSeqDiagram.jpg](assets%2FSleepAddSeqDiagram.png) + +### Parsing user input for sleep entries + +This functionality is facilitated by `ParserSleep`. It implements one operation, namely: +- `ParserSleep#parseSleepInput(String input)` + +This operation is exposed in the `SleepList` class as `SleepList#addSleep(String)`. + +Given below is an example usage scenario and how this mechanism behaves at every step: +- Step 1: When the user inputs the command `sleep add 7.5 d/2022-01-02` in the terminal, + the string is sent to `SleepList#addEntry(String)`, which calls `ParserSleep#parseSleepInput(String)`. + +- Step 2: Using `String.split()`, the method extracts information such as the duration and date of entry. + +- Step 3: It will create a new entry of class `SleepEntry` that extends `Entry`based on the information. + +- Step 4: The created `SleepEntry` instance is added into the `ArrayList sleepList` attribute of the `SleepList`. + + +### Sleep list feature + +The `sleep list` feature lists out the record of all the sleep data that the user has keyed in. The sleep data are all stored into a `ArrayList sleepList` attribute of the `SleepList` Class. Sleep data are printed when the `printSleepList()` function is called. + +The `printSleepList()` function iterates through the `sleepList` and each entry will call `SleepEntry#toString()` to return its information string to be printed. + +The Sequence diagram for Sleep list feature is shown below. Unrelated attributes and Classes were excluded. +![SleepListSeqDiagram.jpg](assets%2FSleepListSeqDiagram.jpg) + +### Sleep delete feature + +The `sleep delete` feature can delete the sleep record at specific index of sleep list. This functionality is facilitated by `SleepList`. It implements one operation, namely: +- `deleteSleep(String line)` + +Given below is an example usage scenario and how this mechanism behaves at every step: +- Step 1: When the user inputs the command `sleep delete INDEX` in the terminal, the string is sent to `Ui#handleUserInput()`, which will call `Ui#handleSleepInput()`. + +- Step 2: After the `Ui#handleSleepInput()` matching `sleep delete` key word, the string will be passed into deleteSleep(String line) to execute delete process. + +- Step 3: The string will be divided to two substrings according to the command syntax. Index will be tried to get from the second substring by `Integer.parseInt()`. + +- Step 4: The sleep record (`Entry`) stored in the `ArrayList sleepList` will be deleted by calling `sleepArrayList.remove((index-1));` and a successful deleting message will be shown in terminal by calling `SleepListUi#successfulDeletedMessage(toDelete)` + +- Step 5: The latest sleep list will be updated to saving file by calling `SleepList#updateFile()`. + +The Sequence diagram for Sleep delete feature is shown below: +![sleepDeleteSeqDiagram.png](assets%2FsleepDeleteSeqDiagram.png) + +### Calculating sleep requirements for each user (Planning) + +#### Design Considerations + +**General Health Guidelines:** The recommended daily sleep duration for an average adult is around 7.5 hours. However, sleep standard is different for different individual with different healthy status. This feature will calculate recommend sleep duration by formula based on user health information. + +**Ease of Implementation:** Setting a standard sleep requirement simplifies the tracking process for users. It provides a clear goal to strive for, making it easier for individuals to monitor and maintain their sleep time levels consistently. + +## Appendix A: Product scope ### Target user profile +* Year 2 NUS Computer Engineering (CEG students) +* can type fast +* is reasonably comfortable using CLI apps -{Describe the target user profile} ### Value proposition -{Describe the value proposition: what problem does it solve?} +It is no secret that Year 2 is the busiest/most difficult period that CEG students will experience in university. +As such, it may be easy for students to neglect their health in the midst of the hustle and bustle. We hope that through +this application, tracking one's health can be made easy and straightforward, so that students can get their health info +quickly on the go, and thus know whether they need to eat/drink/sleep more/less. + +## Appendix B: User Stories + +| Version | As a ... | I want to ... | So that I can ... | +|---------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------| +| v1.1 | busy student | register my calorie gained by inputting the food consumed, amount of calories and date | track my calorie gained history | +| v1.1 | busy student | register my calories burnt by simply inputting the name of the activity, calories burnt and date | track my burned outflow status | +| v1.1 | busy student | add my macronutrient details into my calorie entries, such as protein, carbohydrates and fats | better track my nutrition | +| v1.1 | busy student | see my daily calories burned and consumed at a glance | get a summary of my calorie status | +| v1.1 | busy student | delete my calories burned data | remove data that was keyed in wrongly | +| v1.1 | busy student | delete my calories gained data | remove data that was keyed in wrongly | +| v1.1 | busy student | add my daily hydration intake | track how hydrated I am daily | +| v1.1 | busy student | delete my daily hydration intake data | remove wrong hydration data | +| v1.1 | busy student | see my hydration record at a glance | have an idea of how much water I have been drinking so far | +| v2.0 | busy student | add my daily sleep hours | track my daily sleeping hours | +| v2.0 | busy student | delete my daily sleep hours | remove wrong sleeping data | +| v2.0 | busy student | see my sleep hours data at a glance. | track my sleep status | +| v2.0 | busy student | see my calorie input and output sorted according to days | see what my calorie intake and outflow for each day is | +| v2.0 | busy student | delete my calories entries based on the entry id | delete entries based on entry id, instead of based on array list index | +| v2.0 | busy student | see my daily calorie goals on a progress bar | better visualise my daily progress | +| v2.0 | busy student | see my daily sleep goals on a progress bar | better visualise my daily progress | +| v2.0 | busy student | see my daily hydration goals on a progress bar | better visualise my daily progress | +| v2.0 | busy student | store my personal details such has height, weight, age, gender, exercise levels and body goals for the program calculator to calculate my recommended daily caloric intake | hit my body goals and be healthier | + +## Appendix C: Non-Functional Requirements +1. Should work on mainstream OS as long as it has Java `11` installed. +2. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able +to accomplish most of the tasks fast and efficiently. +3. Calories, sleep and hydration data should be accurately recorded and stored without loss if users follow instructions on app usage. +4. Data retrieval and display operations such as listing calories, sleep or hydration entries should be completed within 5 seconds. +5. Error messages should be descriptive and provide actionable or intuitive guidance to users on how to resolve issues. +6. The app codebase should be well-organised and documented to facilitate future updates and maintenance by development teams. + +## Appendix D: Glossary +* Mainstream OS: Windows, Linux, Unix, MacOS + +## Appendix E: Instructions for manual testing + +### Adding calories intake entries +1. Test case: `calories in burger c/100 d/2024-04-15` + + Expected: Calories entry is added to the calories list. + +2. Test case: `calories in burger c/100 d/2024-04-15 m/19,10,10` + + Expected: Calories entry with macronutrients is added to the calories list. + +3. Test case: `calories in burger c/-1 d/2024-04-15` + + Expected: Calories entry is not added into calories list. Error details shown in message. + +4. Test case: `calories in burger c/100 c/100 d/2024-04-15` + + Expected: Calories entry is not added into calories list. Error details shown in message. + +5. Test case: `calories in burger d/2024-04-15` + + Expected: Calories entry is not added into calories list. Error details shown in message. + +### Adding calories outflow entries + +1. Test case: `calories out run c/100 d/2024-04-15` + + Expected: Calories entry is added to the calories list. + +2. Test case: `calories out go gym c/-1 d/2024-04-15` + + Expected: Calories entry is not added into calories list. Error details shown in message. + +3. Test case: `calories out run c/100 c/100 d/2024-04-15` + + Expected: Calories entry is not added into calories list. Error details shown in message. + +4. Test case: `calories out play softball d/2024-04-15` + + Expected: Calories entry is not added into calories list. Error details shown in message. + +### Listing calories entries +1. Prerequisites: Calories data has already been added into calories list. +2. Test case: `calories list` + + Expected: List of calories data is displayed. + +### Deleting calories entries +1. Prerequisites: Calories data has already been added into the Calories list. +2. Test case: `calories list`, followed by `calories delete 1` + + Expected: Calories entry with `SLEEPID` 1 is deleted from list. + +3. Test case: `sleep list`, followed by `sleep delete -1` + + Expected: No sleep entry deleted. Error details shown in message. + +### Searching for calories item or items +1. Prerequisites: Calories data with `cream` has already been added into the Calories list. +2. Test case: `calories find cream` + Expected: Calories entry with keyword cream in description is shown. + +### Adding hydration entries +1. Test case: `hydration in water v/500 d/2024-04-15` + + Expected: Hydration entry is added to the hydration list. + +2. Test case: `hydration in water v/500 d/24-04-15` + + Expected: No hydration entry is added to the hydration list. Error details shown in message. + +3. Test case: `hydration in water d/2024-04-15 v/500` + + Expected: No hydration entry is added to the hydration list. Error details shown in message. + +### Listing hydration entries +1. Prerequisites: Hydration data has already been added into hydration list. +2. Test case: `hydration list` + + Expected: List of hydration data is displayed. + +### Deleting a hydration entry +1. Prerequisites: hydration data has already been added into hydration list. +2. Test case: `hydration list, followed by hydration delete 1` + + Expected: Hydration entry with HYDRATIONID 1 is deleted from list. + +3. Test case: hydration list, followed by hydration delete -1 + + Expected: No hydration entry deleted. Error details shown in message. + +### Searching for hydration item or items +1. Prerequisites: Hydration data with `milo` has already been added into the Hydration list. +2. Test case: `hydration find milo` + Expected: Hydration entry with keyword milo in description is shown. + +### Adding sleep entries +1. Test case: `sleep add 7 d/2024-04-15` + + Expected: Sleep entry is added to the sleep list. + +2. Test case: `sleep add 7 d/20-04-15` + + Expected: No sleep entry is added to the sleep list. Error details shown in message. + +3. Test case: `sleep add 25 d/20-04-15` + + Expected: No sleep entry is added to the sleep list. Error details shown in message. + +### Listing sleep entries +1. Prerequisites: Sleep data has already been added into sleep list. +2. Test case: `sleep list` + + Expected: List of sleep data is displayed. + +### Deleting a sleep entry +1. Prerequisites: Sleep data has already been added into sleep list. +2. Test case: `sleep list`, followed by `sleep delete 1` + Expected: Sleep entry with `SLEEPID` 1 is deleted from list. +3. Test case: `sleep list`, followed by `sleep delete -1` + Expected: No sleep entry deleted. Error details shown in message. + +### Setup user details +1. Prerequisites: User details already populated with User Setup. +2. Test case: `user details` -## User Stories + Expected: List of user details is displayed. -|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| +### Listing user details +1. Prerequisites: User details already populated with User Setup. +2. Test case: `user details` -## Non-Functional Requirements + Expected: List of user details is displayed. -{Give non-functional requirements} +### Updating user details +1. Prerequisites: User details already populated with User Setup. +2. Test case: `user update height 170` -## Glossary + Expected: User details height updated. -* *glossary item* - Definition +### Viewing user progress +1. Prerequisites: User details already populated with User Setup. Calories and hydration and sleep lists have been +populated with some entries. +2. Test case: `user progress` -## Instructions for manual testing + Expected: User progress is shown for calories, hydration and sleep. -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..2ee8ccb7aa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# Duke +# LifeTrack -{Give product intro here} +LifeTrack is a desktop app for students to track their health data, optimized for use via a Command Line Interface (CLI). It tracks calories, hydration and sleep data for the user, while also providing daily recommendations for calorie and hydration intake, based on the user's build and gender, as well as their body goals and activity levels. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..fb183506ad 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,41 +2,470 @@ ## Introduction -{Give a product intro} +LifeTrack is a desktop app for students to track their health data, optimized for use via a Command Line Interface (CLI). It tracks calories, hydration and sleep data for the user, while also providing daily recommendations for calorie and hydration intake, based on the user's build and gender, as well as their body goals and activity levels. -## Quick Start +## Quick links +- [Quick Start](#quick-start) +- [General](#general) + - [help](#viewing-help-help) + - [bye](#exiting-the-program-bye) +- [Calories Tracker](#calories-tracker) + - [Input calorie Intake](#input-calorie-intake-calories-in) + - [Input calorie loss](#input-calorie-loss-calories-out) + - [Listing calorie items](#listing-calorie-items-calories-list) + - [Deleting a calorie item](#deleting-a-calorie-item-calories-delete) + - [Searching for a calorie item](#searching-for-a-calorie-item-calories-find) +- [Hydration Tracker](#hydration-tracker) + - [Input hydration intake](#input-hydration-intake-hydration-in) + - [Listing hydration items](#listing-hydration-items-hydration-list) + - [Deleting a hydration item](#deleting-a-hydration-item-hydration-delete) + - [Searching for a hydration item from hydration list](#searching-for-a-hydration-item-hydration-find) +- [Sleep Tracker](#sleep-tracker) + - [Input sleeping hours](#input-sleeping-hours-sleep-add) + - [Listing sleep records](#listing-sleep-records-sleep-list) + - [Deleting a sleep record](#deleting-a-sleep-record-sleep-delete) +- [User Profile](#user-profile) + - [Set Up User Profile](#set-up-user-profile-user-setup) + - [Check User Details](#check-your-users-details-user-details) + - [Update User Details](#update-your-users-details-user-update) + - [Check User's daily calories and hydration consumption and your sleep statistics](#check-your-daily-calories-and-hydration-consumption-and-your-sleep-statistics-user-progress) +- [Saving the data](#saving-the-data) +- [Editing the data](#editing-the-data) +- [FAQ](#faq) +- [Command Summary](#command-summary) -{Give steps to get started quickly} +## Quick Start 1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `LifeTrack` from [here](https://github.com/AY2324S2-CS2113-F15-2/tp/releases). You may move the JAR file to anywhere in your computer if you wish. +3. Open a terminal window and change directories to where the JAR file is located. +4. Run the command `java -jar LifeTrack.jar` and the application will start running. + +[//]: # (## Features ) + +## General + +### Viewing help: `help` +Shows a help message listing the commands available in the application. + +**Format:** +`help` + +### Exiting the program: `bye` + +Exits the program. + +**Format:** +`bye` + +#### Expected output + + ----------------------------------------------------------------------------- + Bye! See you again soon ^^ + +## Calories Tracker + +### Input calorie intake: `calories in` +Adds a calorie gaining activity into the calories tracker. +Macronutrients such as Carbohydrates, Proteins and Fats can be included if needed. +The `caloriesID` of each entry increments based on the previous `caloriesID`. For example, if highest +`caloriesID` is now 10, and user deletes the highest entry, the `caloriesID` of the next addition +would be 11. + +**Format:** +`calories in DESCRIPTION c/CALORIES d/DATE [m/CARBOHYDRATES,PROTEIN,FATS]` + +* The `DESCRIPTION` refers to the food that the person consumed. +* The `CALORIES` must be a positive **integer** 1, 2, 3, …, measured in kcal. The limit for `CALORIES` for each entry + is 5000 (inclusive). +* The `DATE` provided should be of the form YYYY-MM-DD, such as 2024-03-04. +* Macronutrients field including `CARBOHYDRATES`, `PROTEINS` and `FATS` is optional. The macronutrients must be a positive **integer** 1, 2, 3, measured in grams. +The limit for each macronutrient per entry is 800g (inclusive). + +**Examples:** +* `calories in chicken rice c/678 d/2022-02-24` +* `calories in cai png c/543 d/2024-04-13 m/200, 150, 100` +* `calories in drink liho milk tea c/200 d/2024-04-14 m/50, 20, 10` + +### Input calorie loss: `calories out` +Adds a calorie burning activity into the calories tracker. + +**Format:** +`calories out DESCRIPTION c/CALORIES d/DATE` + +* The `DESCRIPTION` refers to any activity that resulted in loss of calories. +* The `CALORIES` must be a positive **integer** 1, 2, 3, …, measured in kcal. The limit for `CALORIES` for each entry + is 5000 (inclusive). +* The `DATE` provided should be of the form YYYY-MM-DD such as 2024-04-03. + +**Examples:** + +* `calories out Run around NUS c/678 d/2022-02-24` +* `calories out go gym c/300 d/2024-04-03` + +#### Expected output for `calories out go gym c/300 d/2024-04-03` + + ----------------------------------------------------------------------------- + The following entry has been added to your caloric list! + caloriesID: 4, Date: 2024-04-03, Description: go gym, Calories: 300 + ----------------------------------------------------------------------------- + +### Listing calorie items: `calories list` +Shows a list of all activities in the calories tacker. Calories inflow and outflow are displayed separately. +All entries are sorted by date, in ascending order, from earlier dates to present dates. + +**Format:** +`calories list` + +### Deleting a calorie item: `calories delete` +Deletes the specified calories ID entry from the calories tracker according to the `CALORIESID`. + +**Format:** +`calories delete CALORIESID` +* The `CALORIESID` must be a positive **integer** 1, 2, 3 and so on. + +**Examples:** + +* `calories list` followed by `calories delete 2` deletes the entry with `CALORIESID` 2 in the calories tracker. + +#### Expected output for `calories delete 3` based on calories list shown in example above. + +### Searching for a calorie item: `calories find` +Finds and retrieves all calories entries from the caloric list containing the keyword to search for. + +**Format:** +`calories find KEYWORD` + +**Examples:** + +* `calories find cream` retrieves all the calories entries with `cream` in their description. +* +#### Expected output of `calories find gym` based on calories list shown in example above. + + + ----------------------------------------------------------------------------- + Caloric List based on your search: + + Your Caloric Inflow List: + + Your Caloric Outflow List: + 1. caloriesID: 4, Date: 2024-04-03, Description: go gym, Calories: 300 + ----------------------------------------------------------------------------- + +## Hydration Tracker + +### Input hydration intake: `hydration in` +Adds a hydration record into the hydration tracker. + +**Format:** +`hydration in DESCRIPTION v/VOLUME d/DATE` + +* The `DESCRIPTION` refers to the food that the person consumed. +* The `VOLUME` must be a positive integer 1, 2, 3, …, measured in milliliters. The limit for `volume` for each entry is +10000(inclusive). +* The `DATE` provided should be of the form YYYY-MM-DD, such as 2024-03-04. + +**Examples:** +* `hydration in Milo v/1000 d/2022-03-25` +* `hydration in Tea v/200 d/2022-02-05` + +### Listing hydration items: `hydration list` +Show the list of all hydration records in the hydration tracker. + +**Format:** +`hydration list` +#### Expected output + + ----------------------------------------------------------------------------- + Your Hydration List: + 1. hydrationID: 1, Date: 2024-04-10, Description: milo, Volume: 100 + 2. hydrationID: 2, Date: 2024-04-10, Description: coke, Volume: 1000 + ----------------------------------------------------------------------------- + + +### Deleting a hydration item: `hydration delete` +Deletes the specified hydration entry according to the `HYDRATIONID`. + +**Format:** +`hydration delete HYDRATIONID` +* The `HYDRATIONID` must be a positive **integer** 1, 2, 3 and so on. + +**Examples:** +* `hydration list` followed by `hydration delete 2` deletes the entry with `HYDRATIONID` 2 in the hydration tracker. + +#### Expected output for `hydration delete 1` + + ----------------------------------------------------------------------------- + The following hydration record has been successfully deleted! + hydrationID: 1, Date: 2024-04-15, Description: water, Volume: 1000 + ----------------------------------------------------------------------------- + +### Searching for a hydration item: `hydration find` +Finds and retrieves all hydration entries from the hydration list containing the keyword to search for. + +**Format:** +`hydration find KEYWORD` + +**Examples:** + +* `hydration find water` retrieves all the hydration entries with `water` in their description. -## Features +#### Expected output for `hydration find water` -{Give detailed description of each feature} + ----------------------------------------------------------------------------- + Hydration List based on your search: + 1. hydrationID: 1, Date: 2024-04-15, Description: water, Volume: 1000 + ----------------------------------------------------------------------------- -### Adding a todo: `todo` -Adds a new item to the list of todo items. +## Sleep Tracker -Format: `todo n/TODO_NAME d/DEADLINE` +### Input sleeping hours: `sleep add` +Adds a sleep record into the sleep tracker. -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +**Format:** +`sleep add DURATION d/DATE` +* The duration provided must be a positive real number. +* The duration should not exceed 24 hours. +* The date provided should be of the form YYYY-MM-DD. -Example of usage: -`todo n/Write the rest of the User Guide d/next week` +**Examples:** +* `sleep add 7.5 d/2024-03-11` -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +#### Expected output for `sleep add 7.5 d/2024-03-11`: + + ----------------------------------------------------------------------------- + The following entry has been added to your sleep list! + Sleep ID: 5, Date: 2024-03-11, Duration: 7.50 hours + ----------------------------------------------------------------------------- + +### Listing sleep records: `sleep list` +Show the list of all sleep records in the sleep tracker. + +**Format:** +`sleep list` + +#### Expected output: + + ----------------------------------------------------------------------------- + Your Sleep List: + 1. Sleep ID: 5, Date: 2024-03-11, Duration: 7.50 hours + ----------------------------------------------------------------------------- + +### Deleting a sleep record: `sleep delete` +Deletes the specified sleep entry according to the `SLEEPID`. + +**Format:** +`sleep delete SLEEPID` +* Delete the sleep record with specified `SLEEPID`. +* The `SLEEPID` refers to the id number shown in the displayed sleeping records list. +* The `SLEEPID` must be a positive integer 1, 2, 3, …​ + +**Examples:** +* `sleep list` followed by `sleep delete 2` deletes the sleep record with `SLEEPID` 2 from the sleep tracker. + +#### Expected output for `sleep delete 5`: + + ----------------------------------------------------------------------------- + The following sleep record has been successfully deleted! + Sleep ID: 5, Date: 2024-03-11, Duration: 7.50 hours + ----------------------------------------------------------------------------- + +## User Profile + +### Set up user profile: `user setup` +Creates/edits an existing user profile. + +**Format:** +`user setup NAME h/HEIGHT w/WEIGHT a/AGE s/GENDER e/EXERCISE_LEVELS g/BODY_GOAL` +* The height provided must be an integer between 90 and 225 cms. +* The weight provided must be an integer between 30 and 200 kgs. +* The age provided must be an integer between 13 and 30 years old. +* The gender provided must be either `male`/`m` or `female`/`f`. It is not case-sensitive. +* The exercise levels provided must be an integer between 1 and 5. +* The body goal provided must be an integer between 1 and 5. + +**Notes about the command format:** + +| Exercise Level Input | Corresponding Exercise Levels | +|:--------------------:|:---------------------------------------------------:| +| 1 | Sedentary (little or no exercise) | +| 2 | Lightly Active (exercise 1 to 3 days a week) | +| 3 | Moderately Active (exercise 3 to 5 days a week) | +| 4 | Very Active (exercise 6 to 7 days a week) | +| 5 | Extremely Active (hard exercise 6 to 7 days a week) | + +| Body Goal Input | Corresponding Goal | +|:---------------:|:------------------------------------------:| +| 1 | Quick Weight Loss (20% calorie deficit) | +| 2 | Moderate Weight Loss (10% calorie deficit) | +| 3 | Maintain Weight | +| 4 | Moderate Weight Gain (10% calorie surplus) | +| 5 | Quick Weight Gain (20% calorie surplus) | + + +**Examples:** +* `user setup Tom h/180 w/80 a/25 s/male e/3 g/2` +* `user setup Jane h/163 w/54 a/23 s/female e/2 g/3` + +#### Expected Output: +When the command `user setup Jane h/163 w/54 a/23 s/female e/2 g/3` is entered in the terminal, the following output is expected: + + ----------------------------------------------------------------------------- + Hello, Jane! Thank you for completing the setup :) + You need to consume 1763 calories per day to hit your goals! + User details: + Name: Jane + Height: 163 + Weight: 54 + Age: 23 + Sex: female + Exercise Levels: 2 out of 5 (Lightly Active) + Goal: 3 out of 5 (Maintain Weight) + ----------------------------------------------------------------------------- + +### Check your user's details: `user details` +Displays the details of the user who is using _LifeTrack_. + +**Format:** +`user details` + +**Notes about the command:** +If you have not set your user up beforehand, this command will prompt you to do so instead. + +#### Expected output + + ----------------------------------------------------------------------------- + User details: + Name: John + Height: 170 + Weight: 80 + Age: 23 + Sex: male + Exercise Levels: 2 out of 5 (Lightly Active) + Goal: 4 out of 5 (Moderate Weight Gain) + ----------------------------------------------------------------------------- + +### Update your user's details: `user update` +Updates the details of the user depending on their input. + +**Format:** +`user update ` + +**List of possible fields to update:** +1. name +2. height +3. weight +4. age +5. sex +6. exercise levels +7. goal + +#### Examples: +- `user update weight 70` +- `user update height 170` +- `user update exercise levels 2` + +#### Expected Output: +When the command `user update name Karthik` is entered, the following output is expected: + + ----------------------------------------------------------------------------- + The following change has been made: + Name: Karthik + ----------------------------------------------------------------------------- + +### Check your daily calories and hydration consumption and your sleep statistics: `user progress` +Displays progress bars to show the percentage of calories and hydration you have consumed as well as sleep you have gotten over the past 3 days. + +**Format:** +`user progress` + +**Notes about the command:** +If you have not set your user up beforehand, this command will prompt you to do so instead. + +## Saving the data + +Any data input by the user while the application is running will be stored in the `[JAR file location]/data` directory, which is automatically created by the application. Existing data found in the `/data` directory is automatically retrieved from this directory when the application is relaunched. There is no need to save manually. + +## Editing the data + +All data is saved as `.txt` files in the `[JAR file location]/data` directory. The data files that users should be aware of are: +- Calories List data: `/data/caloriesData.txt` +- Hydration List data: `/data/hydrationData.txt` +- Sleep List data: `/data/sleepData.txt` +- User data: `/data/userData.txt` + +Advanced users are welcome to update data directly by editing the data files. However, do take note of the following: + +> **Caution**: LifeTrack reads the data files line-by-line upon launching. If any line is found to be of invalid format, it will automatically be ignored, and the application will continue reading from subsequent lines (if any). Do take note that performing the below functions will update the data file, which means that any corrupt data existing prior will be wiped. +> - Calories List: `calories in`, `calories out`, `calories delete` +> - Hydration List: `hydration in`, `hydration delete` +> - Sleep List: `sleep add`, `sleep delete` +> - User: `user setup`, `user update` + +As such, do exercise sufficient caution while manually editing the data files. Refer to the relevant FAQ below where the correct format for all data files is described. ## FAQ -**Q**: How do I transfer my data to another computer? +**Q**: How do I transfer my data to another computer? + +**A**: In the same directory as where the JAR file is located, the application will automatically create a `/data` directory which stores all the data files required for the application. Simply copy the entire directory and its contents to your new computer and ensure that it is in the same directory as your JAR file, then run the application as per normal. + +**Q**: How do I manually edit the data files in the correct format? + +**A**: +- For the calories data file, the format is as such: `ENTRY_ID;DATE;DESCRIPTION;ENTRY_TYPE;CALORIES;CARBOHYDRATES;PROTEINS;FATS`. `ENTRY_ID` and `ENTRY_TYPE` is not mentioned in [Calories Tracker](#calories-tracker), as they are automatically input by the application. `ENTRY_ID` should be a unique positive integer, while `ENTRY_TYPE` must be either `C_IN` or `C_OUT`. There is no exception thrown for inputting the same `ENTRY_ID` for different entries, but the application may behave undesirably, such as when performing `calories delete`. In this case, the first entry to be added will be deleted. Take note that `ENTRY_TYPE` must be `C_IN` for the application to accept additional macronutrient fields after the `CALORIES` field. +- For the hydration data file, the format is as such: `ENTRY_ID;DATE;DESCRIPTION;VOLUME`. See above for notes on `ENTRY_ID`. +- For the sleep data file, the format is as such: `ENTRY_ID;DATE;DURATION`. See above for notes on `ENTRY_ID`. +- For the user data file, the format is as such: `NAME;HEIGHT;WEIGHT;AGE;SEX;EXERCISE_LEVELS;BODY_GOAL;REQUIRED_CALORIES`. The `REQUIRED_CALORIES` field is not mentioned in the [User Profile](#user-profile) section as it is automatically calculated by the application. The delimiter used in the data files must be a semicolon (;). + +**Q**: Why must I input integers for my calories when it is a continuous variable? + +**A**: Although calories is technically a continuous variable, we chose to only take in integer inputs in our application as the difference is just not that significant, i.e. users can just round up values that have decimal values of 0.5 and above, and round down any values below that. An average human will have calorie intake in the thousands daily, thus such a small inaccuracy is insignificant in comparison. An `int` is also much easier to work with than `float`, which is why we chose to only use the former. + +**Q**: Why is the limit for `CALORIES` 5000? + +**A**: Our team decided that a rational amount of calories per entry/meal would be 5000 calories. We decided on 5000 calories +because it is not too big, nor is it too small an amount. Thus, it would account for extreme cases of high calorie intake. + +## Coming soon + +### Undo/Redo feature + +An undo/redo feature will be a nice quality of life boost for the app, as it provides users with the freedom to navigate their actions with confidence. Mistakes are inevitable, whether it's unintentional deletions, accidental changes, or simply exploring different options. This feature will help to enhance user experience by instilling a sense of control and reducing anxiety about irreversible actions. + +### Automatic calculation of calories burnt + +Calories burnt from any exercise can be calculated from online calculators depending on the user's body proportions. If such calculators were implemented within the application itself, the calories burnt can be made even more accurate, and also improve user experience as they do not have to manually calculate and key in the values now. + +### Implement daily requirements for macronutrients intake + +Daily requirements for macronutrients intake can also be implemented to enhance the user's diet further. These requirements also change depending on the user's exercise levels and body goals. + +### Provide recommendations to meet daily requirements -**A**: {your answer here} +The `user progress` command displays the user's current progress towards the daily caloric and hydration requirements. This can be enhanced if the application was able to provide recommendations on how to meet these requirements. For example, healthy meals with carefully calculated macronutrients can be recommended to users to meet their daily caloric and macronutrient requirements. ## Command Summary -{Give a 'cheat sheet' of commands here} +| Action | Format, Examples | +|------------------------------------|----------------------------------------------------------------------------------| +| Help | `help` | +| Exit program | `bye` | +| Add calories intake | `calories in DESCRIPTION c/CALORIES d/DATE [m/CARBOHYDRATES,PROTEIN,FATS]` | +| Add calories outflow | `calories out DESCRIPTION c/CALORIES d/DATE` | +| List calories | `calories list` | +| Delete calories entry | `calories delete CALORIESID` | +| Search for calorie entry/entries | `calories find KEYWORD` | +| Add hydration intake | `hydration in DESCRIPTION v/VOLUME d/DATE` | +| List hydration | `hydration list` | +| Delete hydration entry | `hydration delete HYDRATIONID` | +| Search for hydration entry/entries | `hydration find KEYWORD` | +| Add sleep | `sleep add DURATION d/DATE` | +| List sleep | `sleep list` | +| Delete sleep entry | `sleep delete SLEEPID` | +| Set Up User Profile | `user setup NAME h/HEIGHT w/WEIGHT a/AGE s/GENDER e/EXERCISE LEVELS g/BODY GOAL` | +| Check User Profile | `user details` | +| Update User Details | `user update ` | +| Check User Progress | `user progress` | -* Add todo `todo n/TODO_NAME d/DEADLINE` diff --git a/docs/assets/ArchitectureDiagram.png b/docs/assets/ArchitectureDiagram.png new file mode 100644 index 0000000000..f54d12e1e4 Binary files /dev/null and b/docs/assets/ArchitectureDiagram.png differ diff --git a/docs/assets/CaloriesAddEntrySeqDiagram.png b/docs/assets/CaloriesAddEntrySeqDiagram.png new file mode 100644 index 0000000000..c0921c3618 Binary files /dev/null and b/docs/assets/CaloriesAddEntrySeqDiagram.png differ diff --git a/docs/assets/CaloriesListClassDiagram.png b/docs/assets/CaloriesListClassDiagram.png new file mode 100644 index 0000000000..14be055a91 Binary files /dev/null and b/docs/assets/CaloriesListClassDiagram.png differ diff --git a/docs/assets/CaloriesListSequenceDiagram.png b/docs/assets/CaloriesListSequenceDiagram.png new file mode 100644 index 0000000000..2386758f13 Binary files /dev/null and b/docs/assets/CaloriesListSequenceDiagram.png differ diff --git a/docs/assets/HydrationDeleteEntrySeqDiagram.png b/docs/assets/HydrationDeleteEntrySeqDiagram.png new file mode 100644 index 0000000000..3d43b8e0af Binary files /dev/null and b/docs/assets/HydrationDeleteEntrySeqDiagram.png differ diff --git a/docs/assets/HydrationListClassDiagram.png b/docs/assets/HydrationListClassDiagram.png new file mode 100644 index 0000000000..972cdaa195 Binary files /dev/null and b/docs/assets/HydrationListClassDiagram.png differ diff --git a/docs/assets/SleepAddSeqDiagram.jpg b/docs/assets/SleepAddSeqDiagram.jpg new file mode 100644 index 0000000000..2ca47c719e Binary files /dev/null and b/docs/assets/SleepAddSeqDiagram.jpg differ diff --git a/docs/assets/SleepAddSeqDiagram.png b/docs/assets/SleepAddSeqDiagram.png new file mode 100644 index 0000000000..72a4bf015c Binary files /dev/null and b/docs/assets/SleepAddSeqDiagram.png differ diff --git a/docs/assets/SleepDeleteSeqDiagram.jpg b/docs/assets/SleepDeleteSeqDiagram.jpg new file mode 100644 index 0000000000..bf521746cb Binary files /dev/null and b/docs/assets/SleepDeleteSeqDiagram.jpg differ diff --git a/docs/assets/SleepListSeqDiagram.jpg b/docs/assets/SleepListSeqDiagram.jpg new file mode 100644 index 0000000000..1aa44c1696 Binary files /dev/null and b/docs/assets/SleepListSeqDiagram.jpg differ diff --git a/docs/assets/UserCalculateCaloriesSeqDiagram.png b/docs/assets/UserCalculateCaloriesSeqDiagram.png new file mode 100644 index 0000000000..20c67051bf Binary files /dev/null and b/docs/assets/UserCalculateCaloriesSeqDiagram.png differ diff --git a/docs/assets/UserClassDiagram.png b/docs/assets/UserClassDiagram.png new file mode 100644 index 0000000000..c94b28df68 Binary files /dev/null and b/docs/assets/UserClassDiagram.png differ diff --git a/docs/assets/UserDetailsSequenceDiagram.png b/docs/assets/UserDetailsSequenceDiagram.png new file mode 100644 index 0000000000..c076b2be05 Binary files /dev/null and b/docs/assets/UserDetailsSequenceDiagram.png differ diff --git a/docs/assets/calories.png b/docs/assets/calories.png new file mode 100644 index 0000000000..dbaa747dc3 Binary files /dev/null and b/docs/assets/calories.png differ diff --git a/docs/assets/caloriesDeleteUML.jpg b/docs/assets/caloriesDeleteUML.jpg new file mode 100644 index 0000000000..c36887bc85 Binary files /dev/null and b/docs/assets/caloriesDeleteUML.jpg differ diff --git a/docs/assets/calories_component.png b/docs/assets/calories_component.png new file mode 100644 index 0000000000..c4f7310aeb Binary files /dev/null and b/docs/assets/calories_component.png differ diff --git a/docs/assets/hydration.png b/docs/assets/hydration.png new file mode 100644 index 0000000000..94915fea61 Binary files /dev/null and b/docs/assets/hydration.png differ diff --git a/docs/assets/hydration_component.png b/docs/assets/hydration_component.png new file mode 100644 index 0000000000..2164633809 Binary files /dev/null and b/docs/assets/hydration_component.png differ diff --git a/docs/assets/sleep.png b/docs/assets/sleep.png new file mode 100644 index 0000000000..bc184ef271 Binary files /dev/null and b/docs/assets/sleep.png differ diff --git a/docs/assets/sleepDeleteSeqDiagram.png b/docs/assets/sleepDeleteSeqDiagram.png new file mode 100644 index 0000000000..3252947ca5 Binary files /dev/null and b/docs/assets/sleepDeleteSeqDiagram.png differ diff --git a/docs/assets/user.png b/docs/assets/user.png new file mode 100644 index 0000000000..b21c19ff86 Binary files /dev/null and b/docs/assets/user.png differ diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml new file mode 100644 index 0000000000..51c2f8adfa --- /dev/null +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -0,0 +1,42 @@ +@startuml + + +actor bob +rectangle { + rectangle Ui + rectangle Main + rectangle calories + rectangle system.parser + rectangle hydration.hydrationlist + rectangle sleep.sleeplist + rectangle system.storage + rectangle Entry +} + +bob --[dotted]> Ui +Main -[dotted]> Ui +Main ----> sleep.sleeplist +Main ----> hydration.hydrationlist +Main ----> calories + +Ui -[dotted]> calories +Ui -[dotted]> hydration.hydrationlist +Ui -[dotted]> sleep.sleeplist + +calories -[dotted]---> system.parser +calories ----> Entry +calories --[dotted]> system.storage + +hydration.hydrationlist ---[dotted]-> system.parser +hydration.hydrationlist -> Entry +hydration.hydrationlist --[dotted]> system.storage + +sleep.sleeplist --[dotted]--> system.parser +sleep.sleeplist -> Entry +sleep.sleeplist --[dotted]> system.storage + +system.storage -[dotted]-> Entry + +hide circle + +@enduml diff --git a/docs/diagrams/CaloriesAddEntrySeqDiagram.puml b/docs/diagrams/CaloriesAddEntrySeqDiagram.puml new file mode 100644 index 0000000000..a2aee2cfaa --- /dev/null +++ b/docs/diagrams/CaloriesAddEntrySeqDiagram.puml @@ -0,0 +1,32 @@ +@startuml +actor Bob +Bob -> UI: Input "calories in" or "calories out" command +activate UI + +UI -> UI: handleUserInput() +activate UI + +UI -> UI: handleCaloriesInput() +activate UI + +UI -> CalorieList: addEntry() +activate CalorieList + +CalorieList -> ParserCalories: parseCaloriesInput() +activate ParserCalories +return entry + +CalorieList -> CalorieList: calorieArrayList.add(entry) +activate CalorieList +return + +CalorieList -> CalorieList: updateFile() +activate CalorieList +return + +return +return +return +return + +@enduml \ No newline at end of file diff --git a/docs/diagrams/CaloriesListClassDiagram.puml b/docs/diagrams/CaloriesListClassDiagram.puml new file mode 100644 index 0000000000..11033051ca --- /dev/null +++ b/docs/diagrams/CaloriesListClassDiagram.puml @@ -0,0 +1,43 @@ +@startuml + +skinparam classAttributeIconSize 0 + +class LifeTrack { + +main(args: String[]): void +} + +package ui { + class Ui { + + readUserInput(calorieList: calories.CalorieList, ...... ) : void + + handleUserInput(line: String, calorieLIst: CalorieList, ......): void + + handleCaloriesInput(line: String, calorieList: calories.CalorieList): void + } + class CalorieListUi { + + calorieListHeader(): void + + emptyListMessage(): void + + inputCalorieListHeader(): void + + outputCalorieListHeader(): void + } +} + +package calories { + package calorielist { + class CalorieList { + - ArrayList calorieArrayList + + printCalorieList(): void + + printCalorieInflow(): void + + printCalorieOutflow(): void + } + + } +} + +hide circle +LifeTrack --> calories.calorielist.CalorieList +LifeTrack -[dotted]-> ui.Ui +ui.Ui -[dotted]-> calories.calorielist.CalorieList +CalorieList -[dotted]-> CalorieListUi + +@enduml + + diff --git a/docs/diagrams/CaloriesListSequenceDiagram.puml b/docs/diagrams/CaloriesListSequenceDiagram.puml new file mode 100644 index 0000000000..fc1eddc5d3 --- /dev/null +++ b/docs/diagrams/CaloriesListSequenceDiagram.puml @@ -0,0 +1,37 @@ +@startuml +actor Bob + +Bob -> UI: Input "calories list" command +activate UI + +UI -> UI: handleUserInput(String input, CalorieList calorieList, .....) +activate UI + +UI -> UI: handleCaloriesInput(String input, CalorieList calorieList, .....) +activate UI + +UI -> CalorieList: printCalorieList() +activate CalorieList + +CalorieList -> CalorieList: printCalorieInflow() +activate CalorieList + +loop calorieArrayList.size() +CalorieList -> CalorieList : print Calorie Inflow entries +end +return + +CalorieList -> CalorieList: printCalorieOutflow() +activate CalorieList + +loop calorieArrayList.size() +CalorieList -> CalorieList : print Calorie Outflow entries +end +return + +return +return +return +return + +@enduml \ No newline at end of file diff --git a/docs/diagrams/HydrationDeleteEntrySeqDiagram.puml b/docs/diagrams/HydrationDeleteEntrySeqDiagram.puml new file mode 100644 index 0000000000..7a7d40afcf --- /dev/null +++ b/docs/diagrams/HydrationDeleteEntrySeqDiagram.puml @@ -0,0 +1,32 @@ +@startuml +actor Bob +Bob -> UI: Input "hydration delete" command +activate UI + +UI -> UI: handleUserInput() +activate UI + +UI -> UI: handleHydrationInput() +activate UI + +UI -> HydrationList: deleteEntry() +activate HydrationList + +HydrationList -> HydrationList: getIndexFromEntryID() +activate HydrationList +return + +HydrationList -> HydrationList: hydrationArrayList.remove(entry) +activate HydrationList +return + +HydrationList -> HydrationList: updateFile() +activate HydrationList +return + +return +return +return +return + +@enduml diff --git a/docs/diagrams/HydrationListClassDiagram.puml b/docs/diagrams/HydrationListClassDiagram.puml new file mode 100644 index 0000000000..4eee670443 --- /dev/null +++ b/docs/diagrams/HydrationListClassDiagram.puml @@ -0,0 +1,37 @@ +@startuml + +skinparam classAttributeIconSize 0 + +hide circle + +class LifeTrack { + + main(args: String[]): void +} + +package ui { + class Ui { + + readUserInput(hydrationList: hydration.HydrationList, ...... ) : void + + handleUserInput(line: String, hydrationList: HydrationList, ......): void + + handleHydrationInput(line: String, hydrationList: hydration.HydrationList): void + } + class HydrationListUi { + + hydrationListHeader(): void + + emptyListMessage(): void + + inputHydrationListHeader(): void + } +} + +package hydration { + package hydrationlist { + class HydrationList { + - ArrayList hydrationArrayList + + printHydrationList(): void + } +} + +LifeTrack --> hydration.hydrationlist.HydrationList +LifeTrack -[dotted]-> ui.Ui +ui.Ui -[dotted]-> hydration.hydrationlist.HydrationList +HydrationList -[dotted]-> HydrationListUi + +@enduml diff --git a/docs/diagrams/SleepAddSeqDiagram.puml b/docs/diagrams/SleepAddSeqDiagram.puml new file mode 100644 index 0000000000..882737361c --- /dev/null +++ b/docs/diagrams/SleepAddSeqDiagram.puml @@ -0,0 +1,39 @@ +@startuml +actor Bob +Bob -> UI: Input "sleep add" command +activate UI + +UI -> UI: handleUserInput() +activate UI + +UI -> UI: handleSleepInput() +activate UI + +UI -> SleepList: addSleep() +activate SleepList + +SleepList -> ParserSleep: parseSleepInput() +activate ParserSleep + +ParserSleep -> ParserSleep: parseDuration() +activate ParserSleep +return Double + +ParserSleep -> ParserSleep: parseDate() +activate ParserSleep +return LocalDate + +return SleepEntry + +SleepList -> SleepList: sleepList.add(SleepEntry) + +SleepList->SleepListUi: printNewSleepEntry(SleepEntry) +activate SleepListUi +return + +return +return +return +UI-> Bob:Display successful message + +@enduml diff --git a/docs/diagrams/UserCalculateCaloriesSeqDiagram.puml b/docs/diagrams/UserCalculateCaloriesSeqDiagram.puml new file mode 100644 index 0000000000..92943b25ff --- /dev/null +++ b/docs/diagrams/UserCalculateCaloriesSeqDiagram.puml @@ -0,0 +1,36 @@ +'https://plantuml.com/sequence-diagram + +@startuml +actor Tom +Tom -> UI: Input "user setup" command +activate UI + +UI -> UI: handleUserInput() +activate UI + +UI -> UI: handleUserCommands() +activate UI + +UI -> User : setUp() +activate User + +User -> ParserUser: parseSetUp() +activate ParserUser + +ParserUser -> User: setName(),setHeight(),setWeight(),setAge(),setSex(),setExerciseLevels(),setGoal() +ParserUser -> User: getHealthInfo() +activate User + +User -> UserGoals : getHealthInfo(user) +activate UserGoals +return caloriesRequired + +return +return +return +return +return +return + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/UserClassDiagram.puml b/docs/diagrams/UserClassDiagram.puml new file mode 100644 index 0000000000..7e98a0f82a --- /dev/null +++ b/docs/diagrams/UserClassDiagram.puml @@ -0,0 +1,56 @@ +@startuml + +skinparam classAttributeIconSize 0 + +class LifeTrack { + +main(args: String[]): void +} + +package ui { + class Ui { + + readUserInput(user: user.User, ...... ) : void + + handleUserInput(line: String, user: User, ......): void + + handleUserCommands(line: String, user: user.User): void + } + class UserUi { + + printUserCaloriesRequired(caloriesRequired: int): void + + printUserCalorieProgress(caloriesConsumed: int, caloriesRequired: int,......): void + + printUserHydrationProgress(hydrationConsumed: int, hydrationRequired: int,......): void + + printUserSleepProgress(sleepConsumed: double, sleepRequired: int, ......): void + + printUserSetUpComplete(user: user.User): void + + printNoUserYetMessage(): void + + printNewUserName(name: String): void + + printNewUserAge(age: int): void + + printNewUserHeight(height: int): void + + printNewUserWeight(weight: int): void + + printNewUserSex(sex: String): void + + printNewUserExerciseLevels(user: user.User, level: int): void + + printNewUserGoal(user: user.User, goal: int): void + + printUserDetails(user: user.User): void + } +} + +package user { + class User { + - name: String + - height: int + - weight: int + - age: int + - sex: String + - exerciseLevels: int + - goal: int + - caloriesRequired: int + + setUp(line: String): void + + update(line: String): void + + getUserDetails(): void + } +} + +hide circle +LifeTrack --> user.User +LifeTrack -[dotted]-> ui.Ui +ui.Ui -[dotted]-> user.User +User -[dotted]-> UserUi + +@enduml + diff --git a/docs/diagrams/UserDetailsSequenceDiagram.puml b/docs/diagrams/UserDetailsSequenceDiagram.puml new file mode 100644 index 0000000000..4798955bbf --- /dev/null +++ b/docs/diagrams/UserDetailsSequenceDiagram.puml @@ -0,0 +1,32 @@ +'https://plantuml.com/sequence-diagram + +@startuml +actor Tom +Tom -> UI: Input "user details" command +activate UI + +UI -> UI: handleUserInput() +activate UI + +UI -> UI: handleUserCommands() +activate UI + +UI -> User : getUserDetails() +activate User + +User -> UserUI: printUserDetails() +activate UserUI + +UserUI -> User: getName(), getHeight(), getWeight(), getAge(), getSex() +UserUI -> User: getExerciseLevels(), getExerciseLevelsAsString(), getGoal(), getGoalAsString() +activate User +return name, height, sex, age, exerciseLevels, exerciseLevelsAsString, goal, goalAsString + + +return +return +return +return +return + +@enduml \ No newline at end of file diff --git a/docs/diagrams/calories.puml b/docs/diagrams/calories.puml new file mode 100644 index 0000000000..fd9f4fd9cd --- /dev/null +++ b/docs/diagrams/calories.puml @@ -0,0 +1,26 @@ +@startuml + +rectangle CalorieList +rectangle InputEntry +rectangle OutputEntry +rectangle Food + +rectangle Entry +rectangle CaloriesFileHandler +rectangle Ui +rectangle ParserCalories + +CalorieList -> "*" Entry +CalorieList --> "1" CaloriesFileHandler +CalorieList --[dotted]> ParserCalories + +Ui -[dotted]> CalorieList + +InputEntry --|> Entry +OutputEntry --|> Entry +InputEntry --> "0..1" Food + +ParserCalories -[dotted]-> InputEntry +ParserCalories -[dotted]-> OutputEntry + +@enduml \ No newline at end of file diff --git a/docs/diagrams/calories_component.puml b/docs/diagrams/calories_component.puml new file mode 100644 index 0000000000..fb9331e9c8 --- /dev/null +++ b/docs/diagrams/calories_component.puml @@ -0,0 +1,48 @@ +@startuml +actor Bob +Bob -> UI: Input "calories in" command +activate UI + +UI -> UI: handleCaloriesInput(String, CalorieList) +activate UI + +UI -> CalorieList: addEntry(String) +activate CalorieList + +CalorieList -> ParserCalories: parseCaloriesInput(String, int) +activate ParserCalories + +ParserCalories -> ParserCalories : makeNewInputEntry(int, String, int, String) +activate ParserCalories + +return InputEntry +return InputEntry + +CalorieList -> CalorieList: calorieArrayList.add(InputEntry) +activate CalorieList +return + + + +alt calorieArrayList not sorted by date in ascending order + CalorieList -> CalorieList : sortEntriesByDate() + activate CalorieList + return +end + +CalorieList -> CalorieList: updateFile() +activate CalorieList + +CalorieList -> CaloriesFileHandler : writeEntries(ArrayList) +activate CaloriesFileHandler +CaloriesFileHandler -> CaloriesFileHandler : writeToFile(String) +activate CaloriesFileHandler +return +return + +return +return +return +return New InputEntry added + +@enduml \ No newline at end of file diff --git a/docs/diagrams/hydration.puml b/docs/diagrams/hydration.puml new file mode 100644 index 0000000000..4832ec0395 --- /dev/null +++ b/docs/diagrams/hydration.puml @@ -0,0 +1,21 @@ +@startuml + +rectangle HydrationList +rectangle HydrationEntry + +rectangle Entry +rectangle HydrationFileHandler +rectangle Ui +rectangle ParserHydration + +HydrationList --> "*" Entry +HydrationList ---> "1" HydrationFileHandler +HydrationList --[dotted]> ParserHydration + +Ui -[dotted]> HydrationList + +HydrationEntry --|> Entry + +ParserHydration -[dotted]-> HydrationEntry + +@enduml \ No newline at end of file diff --git a/docs/diagrams/hydration_component.puml b/docs/diagrams/hydration_component.puml new file mode 100644 index 0000000000..00b20066af --- /dev/null +++ b/docs/diagrams/hydration_component.puml @@ -0,0 +1,47 @@ +@startuml +actor Bob +Bob -> UI: Input "hydration in" command +activate UI + +UI -> UI: handleHydrationInput(String, HydrationList) +activate UI + +UI -> HydrationList: addEntry(String) +activate HydrationList + +HydrationList -> ParserHydration: parseHydrationInput(String, int) +activate ParserHydration + +ParserHydration -> ParserHydration : makeNewHydrationEntry(int, String, int, String) +activate ParserHydration + +return HydrationEntry +return HydrationEntry + +HydrationList -> HydrationList: hydrationArrayList.add(HydrationEntry) +activate HydrationList +return + +HydrationList -> HydrationList: updateFile() +activate HydrationList + +HydrationList -> HydrationFileHandler : writeEntries(ArrayList) +activate HydrationFileHandler +HydrationFileHandler -> HydrationFileHandler : writeToFile(String) +activate HydrationFileHandler +return +return + +alt hydrationArrayList not sorted by date in ascending order + HydrationList -> HydrationList : sortEntriesByDate() + activate HydrationList + return +end + + +return +return +return +return New HydrationEntry added + +@enduml diff --git a/docs/diagrams/sleep.puml b/docs/diagrams/sleep.puml new file mode 100644 index 0000000000..5f84d4331a --- /dev/null +++ b/docs/diagrams/sleep.puml @@ -0,0 +1,21 @@ +@startuml + +rectangle SleepList +rectangle SleepEntry + +rectangle Entry +rectangle SleepFileHandler +rectangle Ui +rectangle ParserSleep + +SleepList --> "*" Entry +SleepList ---> "1" SleepFileHandler +SleepList --[dotted]> ParserSleep + +Ui -[dotted]> SleepList + +SleepEntry --|> Entry + +ParserSleep -[dotted]-> SleepEntry + +@enduml \ No newline at end of file diff --git a/docs/diagrams/sleepDeleteSeqDiagram.puml b/docs/diagrams/sleepDeleteSeqDiagram.puml new file mode 100644 index 0000000000..c2dac344d6 --- /dev/null +++ b/docs/diagrams/sleepDeleteSeqDiagram.puml @@ -0,0 +1,29 @@ +@startuml +actor User +participant "Ui#handleUserInput()" as UI +participant "Ui#handleSleepInput()" as SleepInput +participant "SleepList#deleteEntry(String line)" as DeleteEntry +participant "SleepList#updateFile()" as UpdateFile +participant "SleepFileHandler#writeEntries(ArrayList entries)" as WriteEntries + +User -> UI : Enters command +activate UI +UI -> SleepInput : Sends command for sleep input handling +activate SleepInput +SleepInput -> DeleteEntry : Calls delete entry method +activate DeleteEntry +DeleteEntry -> UpdateFile : Deletes entry +activate UpdateFile +UpdateFile -> WriteEntries : Updates file with new entries +activate WriteEntries +WriteEntries --> UpdateFile : Updates file +deactivate WriteEntries +UpdateFile --> DeleteEntry : File updated +deactivate UpdateFile +DeleteEntry --> SleepInput : Entry deleted +deactivate DeleteEntry +SleepInput --> UI : Sends success message +deactivate SleepInput +UI -> User : Displays success message +deactivate UI +@enduml \ No newline at end of file diff --git a/docs/diagrams/user.puml b/docs/diagrams/user.puml new file mode 100644 index 0000000000..2e724f9ef9 --- /dev/null +++ b/docs/diagrams/user.puml @@ -0,0 +1,24 @@ +@startuml + +rectangle User + +rectangle UserFileHandler + +rectangle ParserUser + +rectangle UserGoals + +rectangle HydrationList +rectangle CaloriesList +rectangle SleepList + +User ---> "1" UserFileHandler +User --[dotted]> ParserUser + +User <-[dotted]> UserGoals + +UserGoals --[dotted]> HydrationList +UserGoals --[dotted]> CaloriesList +UserGoals --[dotted]> SleepList + +@enduml \ No newline at end of file diff --git a/docs/team/a-wild-chocolate.md b/docs/team/a-wild-chocolate.md new file mode 100644 index 0000000000..b682ba16b5 --- /dev/null +++ b/docs/team/a-wild-chocolate.md @@ -0,0 +1,59 @@ +# Mao Yanyu's Project Portfolio Page +## Project: LifeTrack +LifeTrack is a desktop app for students to track their health data, +optimized for use via a Command Line Interface (CLI). +It tracks calories, hydration and sleep data for the user, +while also providing daily recommendations for calorie and hydration intake, +based on the user's build and gender, as well as their body goals and activity levels. + +The program was created using Java. Version control was done using Git. + +## My contributions to the project + +### New features added and enhancements to existing features + +1. **Added the ability to delete entries for calories intake. [PR #8](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/8)** + * What it does: Allows users to delete records relating to their calories intake by index + * Testing: Added JUnit tests for the feature as well. + +2. **Added logger for debugging.** [PR #45](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/45) + * What it does: Allow coder to better handling the bugs. + +3. **Added the ability to add entries for sleep.** [PR #60](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/60) + * What it does: Allows users to add records relating to their sleep, which includes `Duration` and `Date` + * Testing: Added JUnit tests for the feature as well. + +4. **Added the ability to delete entries for sleep added.** [PR #60](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/60) + * What it does: Allows users to delete sleep records by id. + * Testing: Added JUnit tests for the feature as well. + +5. **Added the ability to list entries in the sleep list.** [PR #60](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/60) + * What it does: Allows users to see sleep records by date in the sleep list. + * Testing: Added JUnit tests for the feature as well. + +6. **Added the sleep id for sleep.** [PR #157](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/157) + * What it does: Give sleep records unique sleep id and can save it into file + * Testing: Added JUnit tests for the feature as well. + + +### Contributions to exception handling +* Added robust exception handling for sleep class and lists, in order to ensure that program does not crash + when users type the wrong command to add, delete or list sleep entries. [PR #16](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/16) +* Added robust exception handling for ParserSleep, in order to ensure that program does not crash + when users enter invalid commands [PR #60](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/60) + +### Contributions to documentation +* **Developer guide** + * Added implementation details for `Sleep` class. [PR #201](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/201) + * Ensure consistent formatting throughout developer guide. + * Generated sequence diagram and class diagram for above feature +* **User guide** + * Converted User guide from word document to Markdown format for all existing features. + * Added documentation for features `calories delete`,`sleep add`, `sleep delete`, `sleep list`. [PR #91](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/91) + * Ensure consistent formatting throughout user guide. + +### Contributions to project management + + +### Code contributed +* [Reposense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=functional-code&since=2024-02-23&tabOpen=true&tabType=zoom&zA=a-wild-chocolate&zR=AY2324S2-CS2113-F15-2%2Ftp%5Bmaster%5D&zACS=106.912839737582&zS=2024-02-23&zFS=&zU=2024-04-15&zMG=false&zFTF=commit&zFGS=groupByRepos&zFR=false) diff --git a/docs/team/owx0130.md b/docs/team/owx0130.md new file mode 100644 index 0000000000..c05866fb26 --- /dev/null +++ b/docs/team/owx0130.md @@ -0,0 +1,49 @@ +# Ong Wei Xiang's Project Portfolio Page + +## Project: LifeTrack + +LifeTrack is a desktop app for students to track their health data, +optimized for use via a Command Line Interface (CLI). +It tracks calories, hydration and sleep data for the user, +while also providing daily recommendations for calorie and hydration intake, +based on the user's build and gender, as well as their body goals and activity levels. + +Given below are my contributions to the project. + +- **New Feature**: Added the ability to read and write from a data file. + - What it does: allows the application to store caloric/hydration/sleep entries that were input previously. Data is retrieved from a data file whenever the application is relaunched. + - Justification: the user should be able to compare against all their previous calorie/hydration/sleep entries to monitor their health which is the very purpose of the application. Additionally, it is exceedingly inconvenient to have the application open all the time if there was no form of storage available, thus this capability is also a huge boost to the user's quality of life while using the application. + - Highlights: This enhancement required an in-depth analysis of design alternatives, such as choosing a suitable delimiter when storing data. In this case, the delimiter needed to be a seldom used character to ensure that it does not show up too often in user input. The implementation was also challenging as there were many alternatives, including when and where in the code to update the data file. + +- **Code contributed**: [Reposense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=owx0130&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +- **Project management**: + - Managed releases `v1.0`, `v1.1` and `v2.0` on GitHub + - Maintaining the issue tracker i.e. closing completed issues, labelling and assigning issues appropriately + +- **Enhancements to existing features**: + - Added capability for `calories in` inputs to take in an optional argument for macronutrients ([PR #39](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/39)) + - What it does: more health-conscious users can further break down their calories consumed into macronutrients which can be passed as an optional argument into the `calories in` command. + - Justification: This feature serves to enhance the experience of the more health-conscious people among our target audience, and by making this argument optional, the user experience of people who do not wish to track their macronutrients will not be affected. + - Reorganized all exception messages into a new class for ease of viewing and editing ([PR #56](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/56), [PR #160](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/160)) + - Created `InputEntry` and `OutputEntry` classes to inherit the original `Entry` class to differentiate between a calorie input and output entry ([PR #39](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/39)) + - Reorganized the `ParserCalories` class by extracting methods to make it more OOP-like ([PR #48](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/48)) + - Reorganized JUnit test cases into individual classes for ease of access ([PR #48](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/48)) + - Added JUnit tests for existing features ([PR #26](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/26), [PR #56](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/56)) + - Reorganized the `FileHandler` class, extracting methods into more classes for more OOP and neatness ([PR #218](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/218)) + - Exception handling for `CaloriesFileHandler`, `HydrationFileHandler`, `SleepFileHandler`, and `UserFileHandler` ([PR #218](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/218)) + +- **Documentation**: + - User Guide: + - Added to `Introduction` ([PR #97](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/97)) + - Added to `Quick Start` section ([PR #160](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/160), [PR #170](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/170)) + - Added relevant FAQs ([PR #170](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/170), [PR #218](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/218)) + - Added `Coming Soon` section ([PR #218](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/218)) + - Added `Saving the data` and `Editing the data` sections ([PR #264](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/264)) + - Developer Guide: + - Added implementation details of the `Adding calorie entries feature` ([PR #70](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/70), [PR #96](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/96)) + +- **Community**: + - PRs reviewed: [#38](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/38), [#43](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/43), [#50](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/50), [#60](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/60), [#67](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/67), [#83](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/83), [#98](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/98) + - Helped review another team's project (T11-2) + - Issues raised: [#233](https://github.com/AY2324S2-CS2113-T11-2/tp/issues/233), [#235](https://github.com/AY2324S2-CS2113-T11-2/tp/issues/235), [#236](https://github.com/AY2324S2-CS2113-T11-2/tp/issues/236), [#239](https://github.com/AY2324S2-CS2113-T11-2/tp/issues/239) \ No newline at end of file diff --git a/docs/team/paturikarthik.md b/docs/team/paturikarthik.md new file mode 100644 index 0000000000..12e1af13af --- /dev/null +++ b/docs/team/paturikarthik.md @@ -0,0 +1,55 @@ +# Paturi Karthik's Project Portfolio Page + +## Project: LifeTrack +LifeTrack is a desktop app for students to track their health data, +optimized for use via a Command Line Interface (CLI). +It tracks calories, hydration and sleep data for the user, +while also providing daily recommendations for calorie and hydration intake, +based on the user's build and gender, as well as their body goals and activity levels. + +The program was created using Java. Version control was done using Git. + +## My contributions to the project + +### New features added and enhancements to existing features +1. **Added a `User` Class to store the details of the user of the app and calculate their daily calorie goal. [PR #73](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/73)** + * What it does: Allows users to set up their details such as their `NAME`, `HEIGHT`, `WEIGHT`, `AGE`, `SEX`, `EXERCISE LEVELS` and `GOALS`. + Using, these information, the user's calorie goal is also calculated and stored in `caloriesRequired`. Using the `user progress` command, + users can track the percentage of calories and hydration they have consumed and sleep they have gotten for the last 3 days. **[PR #184](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/184)** + * Testing: Add JUnit tests for the feature. **[PR #219](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/219)** + +2. **Added `user update` and `user details` functions for users to quickly glance at and update their details. [PR #171](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/171)** + * What it does: `user update` allows users to update their details one field at a time, while `user details` allows user to view all their details at once. + * Testing: Add JUnit tests for the feature. **[PR #219](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/219)** + +3. **Added `UI` class for overall handling of user commands. [PR #52](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/52), [PR #58](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/58)** + * What it does: Redirects commands from user to the relevant methods. + * Justification: Having a separate class for `UI` ensures that the code is well factored out and that changes to the UI do not need to be made in multiple files. . + +4. **Added `UserUI` class to handle all System messages regarding the `User` class. [PR #108](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/108)** + * What it does: Ensures that all messages from the System are factored out and consolidated into a single class for code clarity. + +5. **Added a `find` feature to sieve through `calorieList` and `hydrationList` to find entries based on a given keyword. [PR #220](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/220)** + * What it does: Prints the calories list/ hydration list based on the keyword that was searched. + * Justification: So that users have an easier time finding certain entries from the lists, instead of looking through the full list. + +### Contributions to exception handling +* Added robust exception handling for `ParserUser`, in order to ensure that program does not crash + when users give incomplete commands, invalid commands, missing fields, impractical entries or entries which are out of the range of LifeTrack. **[PR #89](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/89)** +* Contributed to the robust exception handling for calorie features, in order to ensure that program does not crash + when users type the wrong command to add calorie entries. **[PR #33](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/33)** + +### Contributions to documentation +* **Developer guide** + * Added implementation details and diagrams for `User` class and `user progress` feature. **[PR #109](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/109)** + * Added implementation details and diagrams for `user details` feature. **[PR #247](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/247)** +* **User guide** + * Added documentation for features `user setup`, `user progress` and `help`. **[PR #109](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/109)** + * Added documentation for features `user details` and `user update`. **[PR #224](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/224)** + +### Contributions to project management +* In charge of organising and factoring out code written to ensure high levels of OOP, as well as one of 2 designation PR reviewers to maintain the GitHub repository. +* Conducted Peer Testing for Team T11-2 so that we could both learn from each other. **[Peer Testing Contributions](https://github.com/AY2324S2-CS2113-T11-2/tp/issues?q=is%3Aissue+author%3Apaturikarthik)** + +### Code contributed +* [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=paturikarthik&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=paturikarthik&tabRepo=AY2324S2-CS2113-F15-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) \ No newline at end of file diff --git a/docs/team/rexyyong.md b/docs/team/rexyyong.md new file mode 100644 index 0000000000..87deb96ef0 --- /dev/null +++ b/docs/team/rexyyong.md @@ -0,0 +1,62 @@ +# Rex Yong's Project Portfolio Page +## Project: LifeTrack +LifeTrack is a desktop app for students to track their health data, +optimized for use via a Command Line Interface (CLI). +It tracks calories, hydration and sleep data for the user, +while also providing daily recommendations for calorie and hydration intake, +based on the user's build and gender, as well as their body goals and activity levels. + +The program was created using Java. Version control was done using Git. + +## My contributions to the project + +### New features added and enhancements to existing features +1. **Added the ability to add entries for calories intake. [PR #12](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/12)** + * What it does: Allows users to add records relating to their calories intake, which includes + `DESCRIPTION` of food, amount of `CALORIES`, as well as `DATE`. + * Testing: Added JUnit tests for the feature as well. + +2. **Added date format using Class LocalDate for Class Calories, Hydration and Sleep. [PR #83](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/83)** + * What it does: Allows users to key in dates into their entries. + * Justification: By using the Java LocalDate Class, it allows methods to check if dates keyed + in by users is valid. It also allows easier comparison of dates among entries. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required + the editing of many other classes to change the `DATE` from type String to type LocalDate. + * Testing: Edited JUnit tests to follow LocalDate format. + +3. **Added `entryID` for calories entries in calories list. [PR #153](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/153)** + * What it does: Ensures that all entries added by users will be tagged to a unique `entryID`. + * Justification: This allows users to remove calories entries based on the `entryID`, instead of + removing entries based on the index of entry in the array list. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required + the editing of many other classes to ensure that all entries will be given an `entryID`. + +4. **Added feature to sort entries by `DATE`. [PR #161](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/161)** + * What it does: Ensures that dates of entries are sorted in ascending order. + * Justification: So that user can see the breakdown of calories entries grouped by `DATE`. + +5. **Added feature to print calories list grouped by calories in and calories out. [PR #154](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/154)** + * What it does: Prints the calories list grouped by calories in and calories out data. + * Justification: So that user can distinguish which entries are for calories in and calories out. + +### Contributions to exception handling +* Added robust exception handling for calories Parser, in order to ensure that program does not crash + when users type the wrong command to add, delete or list calories entries. [PR #43](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/43) +* Added robust exception handling for hydration features, in order to ensure that program does not crash + when users type the wrong command to add, delete or list hydration entries. [PR #50](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/50) + +### Contributions to documentation +* **Developer guide** + * Added implementation details for `calories list` feature. [PR #74](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/74) +* **User guide** + * Added documentation for features `calories in`, `calories out`, `calories list`. + Also added quick links as well as command summary. [PR #100](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/100) + +### Contributions to project management +* In charge of organising and documenting project meeting minutes and scheduling project meetings. + * Click [here](https://docs.google.com/document/d/1hQchbh4mrso-WWNApsfkhvX7QF_kqfvNnIorwwQwjzU/edit) + to view the meeting minutes document. + +### Code contributed +* (Clink RepoSense link and search for rexyyong to see my code contributed: +[RepoSense link](https://nus-cs2113-ay2324s2.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-02-23&tabOpen=true&tabType=authorship&tabAuthor=rexyyong&tabRepo=AY2324S2-CS2113-F15-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) \ No newline at end of file diff --git a/docs/team/shawnpong.md b/docs/team/shawnpong.md new file mode 100644 index 0000000000..61ea44c532 --- /dev/null +++ b/docs/team/shawnpong.md @@ -0,0 +1,53 @@ +# Shawn Pong's Project Portfolio Page +## Project: LifeTrack +LifeTrack is a desktop app for students to track their health data, +optimized for use via a Command Line Interface (CLI). +It tracks calories, hydration and sleep data for the user, +while also providing daily recommendations for calorie and hydration intake, +based on the user's build and gender, as well as their body goals and activity levels. + +The program was created using Java. Version control was done using Git. + +## My contributions to the project + +### New features added and enhancements to existing features + +1. **Added the ability to add entries for hydration intake.** [PR #71](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/71) + * What it does: Allows users to add records relating to their hydration intake, which includes `DESCRIPTION` of + drink, `VOLUME` of drink, as well as `DATE`. + * Testing: Added JUnit tests for the feature as well. + +2. **Added functionality for users to view their caloric and hydration goals** [PR #90](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/90) + * What it does: Allows users to view their progress with a progress bar with `user progress` function. + * Justification: This lets users easily view their progress with an easy to comprehend progress bar, so users can + know how much more calories or hydration to consume. + +3. **Added feature for users to view caloric and hydration goals for current day** [PR #180](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/181) + * What it does: Users can now view their current daily caloric and hydration intake with `user progress` function, + instead of the cumulative intake. + * Justification: As this is a lifestyle tracker, the intent is for users to use it on a day-to-day basis, tracking + their current daily instake against their goals. They are still able to view their previous history of consumption + with `calories list` and `hydration list`. + +4. **Added feature to sort hydration entries by DATE.** [PR #181](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/181) + * What it does: Ensures that dates of hydration entries are sorted in ascending order. + * Justification: So that user can see the breakdown of hydration entries grouped by DATE. + +### Contributions to exception handling +* Added robust exception handling for hydration class and lists, in order to ensure that program does not crash + when users type the wrong command to add, delete or list hydration entries. [PR #38](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/38) +* Added robust exception handling for ParserHydration, in order to ensure that program does not crash + when users enter invalid commands [PR #71](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/71) + +### Contributions to documentation +* **Developer guide** + * Added implementation details for `Hydration` class. [PR #275](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/275) + * Added implementation details for `HydrationList` class. [PR #93](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/93) + * Ensure consistent formatting throughout developer guide. +* **User guide** + * Converted User guide from word document to Markdown format for all existing features. + * Added documentation for features `hydration in`, `hydration delete`, `hydration list`. [PR #91](https://github.com/AY2324S2-CS2113-F15-2/tp/pull/91) + * Ensure consistent formatting throughout user guide. + +### Code contributed +* [Reposense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=shawnpong&sort=groupTitle&sortWithin=totalCommits%20dsc&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=false) diff --git a/sample-data/caloriesTestData.txt b/sample-data/caloriesTestData.txt new file mode 100644 index 0000000000..116a4e103f --- /dev/null +++ b/sample-data/caloriesTestData.txt @@ -0,0 +1,3 @@ +1;2024-02-02;burger;C_IN;200 +2;2024-02-01;rice;C_IN;190 +3;2024-02-03;noodle;C_IN;180 diff --git a/sample-data/hydrationTestData.txt b/sample-data/hydrationTestData.txt new file mode 100644 index 0000000000..4172ffc823 --- /dev/null +++ b/sample-data/hydrationTestData.txt @@ -0,0 +1,3 @@ +1;2024-02-02;milo;200 +2;2024-02-01;coffee;190 +3;2024-02-03;tea;180 diff --git a/sample-data/sleepTestData.txt b/sample-data/sleepTestData.txt new file mode 100644 index 0000000000..479b9d1879 --- /dev/null +++ b/sample-data/sleepTestData.txt @@ -0,0 +1,3 @@ +1;2024-02-02;20.3 +2;2024-02-01;19.2 +3;2024-02-03;18.1 diff --git a/sample-data/userTestData.txt b/sample-data/userTestData.txt new file mode 100644 index 0000000000..8c00e4adb4 --- /dev/null +++ b/sample-data/userTestData.txt @@ -0,0 +1 @@ +john;170;90;23;male;5;5;1900 \ No newline at end of file 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/lifetrack/Entry.java b/src/main/java/seedu/lifetrack/Entry.java new file mode 100644 index 0000000000..d6235b1e16 --- /dev/null +++ b/src/main/java/seedu/lifetrack/Entry.java @@ -0,0 +1,49 @@ +//@@author owx0130 +package seedu.lifetrack; + +import seedu.lifetrack.calories.calorielist.InputEntry; +import seedu.lifetrack.calories.calorielist.OutputEntry; + +import java.time.LocalDate; + +public abstract class Entry { + + private String description; + private LocalDate date; + private int entryID; + + public Entry(int lastEntryID, String description, LocalDate date){ + this.entryID = lastEntryID; + this.description = description; + this.date = date; + } + + public String getDescription() { + return description; + } + + public LocalDate getDate() { + return date; + } + + public int getEntryID() { + return entryID; + } + + public String toString() { + if(this instanceof InputEntry) { + return ("\t caloriesID: " + entryID + ", Date: " + date + + ", Description: " + description); + } else if (this instanceof OutputEntry) { + return ("\t caloriesID: " + entryID + ", Date: " + date + ", " + + "Description: " + description); + } else { + return ("\t hydrationID: " + entryID + ", Date: " + date + ", " + + "Description: " + description); + } + } + + public String toFileFriendlyString() { + return (entryID + ";" + date + ";" + description); + } +} diff --git a/src/main/java/seedu/lifetrack/LifeTrack.java b/src/main/java/seedu/lifetrack/LifeTrack.java new file mode 100644 index 0000000000..5bb315ccde --- /dev/null +++ b/src/main/java/seedu/lifetrack/LifeTrack.java @@ -0,0 +1,26 @@ +package seedu.lifetrack; + +import seedu.lifetrack.calories.calorielist.CalorieList; +import seedu.lifetrack.hydration.hydrationlist.HydrationList; +import seedu.lifetrack.sleep.sleeplist.SleepList; +import seedu.lifetrack.ui.Ui; +import seedu.lifetrack.user.User; + +import java.io.File; + +public class LifeTrack { + + public static CalorieList calorieList = new CalorieList("data/caloriesData.txt"); + public static HydrationList hydrationList = new HydrationList("data/hydrationData.txt"); + public static SleepList sleepList = new SleepList("data/sleepData.txt"); + public static User user = new User("data/userData.txt"); + /** + * Main entry-point for the java.lifetrack.LifeTrack application. + */ + public static void main(String[] args) { + new File("data/").mkdir(); + Ui.sayHello(); + Ui.readUserInput(calorieList,hydrationList,user,sleepList); + Ui.byeMessage(); + } +} diff --git a/src/main/java/seedu/lifetrack/calories/Food.java b/src/main/java/seedu/lifetrack/calories/Food.java new file mode 100644 index 0000000000..fcd01bb02f --- /dev/null +++ b/src/main/java/seedu/lifetrack/calories/Food.java @@ -0,0 +1,27 @@ +//@@author owx0130 +package seedu.lifetrack.calories; + +public class Food { + + private int carbohydrates; + private int proteins; + private int fats; + + public Food(int carbohydrates, int proteins, int fats) { + this.carbohydrates = carbohydrates; + this.proteins = proteins; + this.fats = fats; + } + + public int getCarbohydrates() { + return carbohydrates; + } + + public int getProteins() { + return proteins; + } + + public int getFats() { + return fats; + } +} diff --git a/src/main/java/seedu/lifetrack/calories/calorielist/CalorieList.java b/src/main/java/seedu/lifetrack/calories/calorielist/CalorieList.java new file mode 100644 index 0000000000..b4b221eef8 --- /dev/null +++ b/src/main/java/seedu/lifetrack/calories/calorielist/CalorieList.java @@ -0,0 +1,284 @@ +//@@author rexyyong +package seedu.lifetrack.calories.calorielist; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.system.exceptions.InvalidInputException; +import seedu.lifetrack.system.parser.ParserCalories; +import seedu.lifetrack.system.storage.CaloriesFileHandler; +import seedu.lifetrack.ui.CalorieListUi; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.io.FileNotFoundException; +import java.util.ArrayList; + +public class CalorieList { + + private static Logger logr = Logger.getLogger(CalorieList.class.getName()); + + private final int SIZE_OF_DELETE = 16; + + //constant for finding entry index from entryID + private final int NO_INDEX_FOUND = -1; + private ArrayList calorieArrayList; + private CaloriesFileHandler fileHandler; + private int lastEntryID; + + /** + * Constructs a new CalorieList object. + * This constructor is used for JUnit tests and initializes an empty list of calorie entries. + */ + public CalorieList() { + calorieArrayList = new ArrayList<>(); + } + + /** + * Constructs a new CalorieList object using a file path. + * This constructor is intended for usage in a terminal environment. + * + * @param filePath the path to the file containing calorie entries + */ + public CalorieList(String filePath) { + try { + fileHandler = new CaloriesFileHandler(filePath); + calorieArrayList = fileHandler.getCalorieEntriesFromFile(); + // Initialize lastEntryID from stored data or default to 0 if not available + this.lastEntryID = loadLastEntryID(); + } catch (FileNotFoundException e) { + calorieArrayList = new ArrayList<>(); + } + } + + /** + * Updates the file with the current list of calorie entries. + * If the file handler is not initialized, no action is taken. + */ + private void updateFile() { + if (fileHandler != null) { + fileHandler.writeEntries(calorieArrayList); + } + } + + public Entry getEntry(int index) { + return calorieArrayList.get(index); + } + + + /** + * Deletes a calorie entry from the list based on the provided entryID. + * entryID should be in an integer from 1 to size of the list, and be present in the calorieArrayList. + * + * @param line the string containing the entryID of calorie record user want to delete + */ + //@@author a-wild-chocolate + public void deleteEntry(String line) { + assert (line.startsWith("calories delete") ) : "ensures that input is correct"; + + try { + int entryID = Integer.parseInt(line.substring(SIZE_OF_DELETE).trim()); + int index = getIndexFromEntryID(entryID); + if (calorieArrayList.isEmpty()) { + CalorieListUi.emptyCalorieList();; + } else if (index == NO_INDEX_FOUND) { + CalorieListUi.unsuccessfulDeletedMessage(entryID); + } else { + Entry toDelete = calorieArrayList.get(index); + calorieArrayList.remove((index)); + CalorieListUi.successfulDeletedMessage(toDelete); + updateFile(); + } + } catch (IndexOutOfBoundsException | NumberFormatException e) { + System.out.println(CalorieListUi.deleteLogNumberMessage()); + } + } + //@@author + + /** + * Returns the index of the entry in the calorie ArrayList based on the given entry ID. + * + * @param entryID the entry ID to search for in the list + * @return the index of the entry with the specified entry ID, or NO_INDEX_FOUND if not found + */ + public int getIndexFromEntryID(int entryID) { + for (int i = 0; i < calorieArrayList.size(); i++) { + if (calorieArrayList.get(i).getEntryID() == entryID) { + return i; + } + } + return NO_INDEX_FOUND; + } + + /** + * Parses a string input representing calorie intake and adds it to the calorie list. + * + * This method takes a string input representing calorie intake information and + * attempts to parse it using the parseCaloriesInput method from the ParserCalories class. + * If the input format is incorrect or contains missing components, it catches + * the InvalidInputException and prints an error message. Otherwise, it adds + * the parsed Entry object to the calorieArrayList. + * Additionally, if the date of the newly added entry is earlier than the date of the final + * entry before adding the new entry, the list is sorted by date. + * + * @param input the input string containing date, time, activity, and calorie count + */ + public void addEntry(String input) { + assert (input.startsWith("calories in") || input.startsWith("calories out")) : "ensures that input is correct"; + logr.setLevel(Level.SEVERE); + try { + Entry newEntry = ParserCalories.parseCaloriesInput(input, lastEntryID); + calorieArrayList.add(newEntry); + CalorieListUi.printNewCalorieEntry(newEntry); + lastEntryID ++; + //only sort if newly added date is earlier than date in final entry before adding entry + if (calorieArrayList.size() > 1 && + calorieArrayList.get(calorieArrayList.size() - 2).getDate().compareTo(newEntry.getDate()) > 0 ) { + sortEntriesByDate(); + } + updateFile(); + } catch (InvalidInputException e) { + logr.log(Level.WARNING, e.getMessage(), e); + System.out.println(e.getMessage()); + } + } + + /** + * Prints the list of calorie entries along with its activity description. + * If the list is empty, it prints a message indicating that the list is empty. + * Otherwise, it prints each entry's activity description and calorie count. + * It prints calorieInflow entries, followed by calorieOutflow entries. + */ + public void printCalorieList() { + if (calorieArrayList.isEmpty()) { + CalorieListUi.emptyListMessage(); + } else { + CalorieListUi.calorieListHeader(); + printCalorieInflow(); + printCalorieOutflow(); + } + } + + /** + * Prints the list of calorie entries representing calorie intake along with their activity descriptions. + * This method prints each entry's activity description and calorie count if it represents calorie intake. + */ + public void printCalorieInflow() { + CalorieListUi.inputCalorieListHeader(); + int serialNumber = 1; + for (Entry value : calorieArrayList) { + if (value instanceof InputEntry) { + Entry entry = value; + System.out.println("\t " + serialNumber + ". " + entry); + serialNumber++; + } + } + } + + /** + * Prints the list of calorie entries representing calorie expenditure along with their activity descriptions. + * This method prints each entry's activity description and calorie count if it represents calorie expenditure. + */ + public void printCalorieOutflow() { + CalorieListUi.outputCalorieListHeader(); + int serialNumber = 1; + for (Entry value : calorieArrayList) { + if (value instanceof OutputEntry) { + Entry entry = value; + System.out.println("\t " + serialNumber + ". " + entry); + serialNumber++; + } + } + } + + + /** + * Returns the size of the list of calorie entries. + * + * @return the number of calorie entries in the list + */ + public int getSize() { + return calorieArrayList.size(); + } + + /** + * Calculates and returns the total number of calories consumed from all entries in the list. + * + * @return the total number of calories consumed + */ + public int getCaloriesConsumed(LocalDate date) { + int totalCalories = 0; + for (Entry entry : calorieArrayList) { + if (entry instanceof InputEntry && entry.getDate().isEqual(date)) { + InputEntry tempEntry = (InputEntry) entry; + totalCalories += tempEntry.getCalories(); + } else if (entry instanceof OutputEntry && entry.getDate().isEqual(date)) { + OutputEntry tempEntry = (OutputEntry) entry; + totalCalories -= tempEntry.getCalories(); + } + } + return totalCalories; + } + + public int getCaloriesConsumedCurrentDay() { + int totalCalories = 0; + for (Entry entry : calorieArrayList) { + if (entry.getDate().isEqual(LocalDate.now())) { + if (entry instanceof InputEntry) { + InputEntry tempEntry = (InputEntry) entry; + totalCalories += tempEntry.getCalories(); + } else if (entry instanceof OutputEntry) { + OutputEntry tempEntry = (OutputEntry) entry; + totalCalories -= tempEntry.getCalories(); + } + } + } + return totalCalories; + } + + /** + * Loads the last entry ID from a text file. + * This method retrieves the last entry ID from the file using the FileHandler.getMaxCaloriesID method. + * If the file doesn't exist or an error occurs during the process, it returns a default value. + * + * @return the last entry ID loaded from the file, or a default value if the file doesn't exist or an error occurs + */ + private int loadLastEntryID() { + return CaloriesFileHandler.maxCaloriesID; // Default value if file doesn't exist or error occurs + } + + /** + * Sorts the list of calorie entries by date. + * This method uses the Collections.sort(List, Comparator) method to sort the list of + * calorie entries in ascending order based on their dates. It provides a custom comparator that + * compares the dates of two entries and returns the result of the comparison. + */ + public void sortEntriesByDate() { + Collections.sort(calorieArrayList, new Comparator() { + @Override + public int compare(Entry entry1, Entry entry2) { + return entry1.getDate().compareTo(entry2.getDate()); + } + }); + } + + //@@author paturikarthik + public void findEntries(String input){ + ParserCalories.findCalorieListEntries(input,this); + } + + public void addCalorieEntry(Entry entry){ + this.calorieArrayList.add(entry); + } + + public void printFoundCalorieList() { + if (calorieArrayList.isEmpty()) { + CalorieListUi.emptyFoundListMessage(); + } else { + CalorieListUi.calorieListFoundHeader(); + printCalorieInflow(); + printCalorieOutflow(); + } + } +} diff --git a/src/main/java/seedu/lifetrack/calories/calorielist/InputEntry.java b/src/main/java/seedu/lifetrack/calories/calorielist/InputEntry.java new file mode 100644 index 0000000000..77e0de8fab --- /dev/null +++ b/src/main/java/seedu/lifetrack/calories/calorielist/InputEntry.java @@ -0,0 +1,68 @@ +//@@author rexyyong +package seedu.lifetrack.calories.calorielist; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.calories.Food; + +import java.time.LocalDate; + +/** + * Represents an entry for calories intake. + * Extends the Entry class and includes additional fields and methods specific to input entries. + */ +public class InputEntry extends Entry { + + private Food food; + private int calories; + private boolean doesFoodExist = false; + + /** + * Constructs a new InputEntry object with the given description, calories, and date. + * + * @param description the description of the entry + * @param calories the number of calories consumed + * @param date the date of the entry + */ + public InputEntry(int lastEntryID, String description, int calories, LocalDate date) { + super(lastEntryID, description, date); + this.calories = calories; + } + + /** + * Constructs a new InputEntry object with the given description, calories, date, + * and food details with macronutrients. + * + * @param description the description of the entry + * @param calories the number of calories consumed + * @param date the date of the entry + * @param food the food details with macronutrients associated with the entry + */ + public InputEntry(int lastEntryID, String description, int calories, LocalDate date, Food food) { + super(lastEntryID, description, date); + this.food = food; + this.calories = calories; + this.doesFoodExist = true; + } + + public Food getFood() { + return food; + } + + public int getCalories() { + return this.calories; + } + + public String toString() { + return (super.toString() + ", Calories: " + calories + (doesFoodExist ? + " (C: " + food.getCarbohydrates() + + ", P: " + food.getProteins() + + ", F: " + food.getFats() + ")" + : "")); + } + + public String toFileFriendlyString() { + return (super.toFileFriendlyString() + ";C_IN;" + calories + + (doesFoodExist ? ";" + food.getCarbohydrates() + ";" + food.getProteins() + ";" + food.getFats() + : "")); + } +} diff --git a/src/main/java/seedu/lifetrack/calories/calorielist/OutputEntry.java b/src/main/java/seedu/lifetrack/calories/calorielist/OutputEntry.java new file mode 100644 index 0000000000..8faa1eaf76 --- /dev/null +++ b/src/main/java/seedu/lifetrack/calories/calorielist/OutputEntry.java @@ -0,0 +1,39 @@ +//@@author rexyyong +package seedu.lifetrack.calories.calorielist; + +import seedu.lifetrack.Entry; + +import java.time.LocalDate; + +/** + * Represents an entry for calories output. + * Extends the Entry class and includes additional fields and methods specific to output entries. + */ +public class OutputEntry extends Entry { + + private int calories; + + /** + * Constructs a new OutputEntry object with the given description, calories, and date. + * + * @param description the description of the entry + * @param calories the number of calories burnt + * @param date the date of the entry + */ + public OutputEntry(int lastEntryID, String description, int calories, LocalDate date) { + super(lastEntryID, description, date); + this.calories = calories; + } + + public int getCalories() { + return calories; + } + + public String toString() { + return (super.toString() + ", Calories: " + calories); + } + + public String toFileFriendlyString() { + return (super.toFileFriendlyString() + ";C_OUT;" + calories); + } +} diff --git a/src/main/java/seedu/lifetrack/hydration/hydrationlist/HydrationEntry.java b/src/main/java/seedu/lifetrack/hydration/hydrationlist/HydrationEntry.java new file mode 100644 index 0000000000..83a8f958bc --- /dev/null +++ b/src/main/java/seedu/lifetrack/hydration/hydrationlist/HydrationEntry.java @@ -0,0 +1,32 @@ +//@@author shawnpong +package seedu.lifetrack.hydration.hydrationlist; + +import seedu.lifetrack.Entry; + +import java.time.LocalDate; + +public class HydrationEntry extends Entry { + + private int volume; + + public HydrationEntry(int lastHydrationEntryID, String description, int volume, LocalDate date){ + super(lastHydrationEntryID, description, date); + this.volume= volume; + } + + public int getHydration() { + return volume; + } + + public int getVolume() { + return volume; + } + + public String toString() { + return (super.toString() + ", Volume: " + volume); + } + + public String toFileFriendlyString() { + return (super.toFileFriendlyString() + ";" + volume); + } +} diff --git a/src/main/java/seedu/lifetrack/hydration/hydrationlist/HydrationList.java b/src/main/java/seedu/lifetrack/hydration/hydrationlist/HydrationList.java new file mode 100644 index 0000000000..98a1726dc9 --- /dev/null +++ b/src/main/java/seedu/lifetrack/hydration/hydrationlist/HydrationList.java @@ -0,0 +1,222 @@ +//@@author shawnpong +package seedu.lifetrack.hydration.hydrationlist; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.calories.calorielist.CalorieList; +import seedu.lifetrack.system.exceptions.InvalidInputException; +import seedu.lifetrack.system.parser.ParserHydration; +import seedu.lifetrack.system.storage.HydrationFileHandler; +import seedu.lifetrack.ui.HydrationListUI; + +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a list of liquid entries. + * Provides methods to add, delete, and print liquid entries. + */ +public class HydrationList { + + private static Logger logr = Logger.getLogger(CalorieList.class.getName()); + + private final int SIZE_OF_DELETE = 16; + + private final int NO_INDEX_FOUND = -1; + private ArrayList hydrationArrayList; + private HydrationFileHandler fileHandler; + private int lastHydrationEntryID; + + //constructor for JUnit tests + public HydrationList() { + hydrationArrayList = new ArrayList<>(); + } + + //constructor for usage in terminal + public HydrationList(String filePath) { + try { + fileHandler = new HydrationFileHandler(filePath); + hydrationArrayList = fileHandler.getHydrationEntriesFromFile(); + this.lastHydrationEntryID = loadLastEntryID(); + } catch (FileNotFoundException e) { + hydrationArrayList = new ArrayList<>(); + } + } + + /** + * Updates the file with the current list of hydration entries. + */ + private void updateFile() { + if (fileHandler != null) { + fileHandler.writeEntries(hydrationArrayList); + } + } + + /** + * Retrieves the liquid entry at the specified index. + * + * @param index the index of the liquid entry to retrieve + * @return the liquid entry at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ + public Entry getEntry(int index) { + return hydrationArrayList.get(index); + } + + /** + * Deletes the liquid entry indicated by the provided line. + * + * @param line the string containing the index of the liquid record to delete + */ + public void deleteEntry(String line) { + assert (line.startsWith("hydration delete")) : "ensures that input is correct"; + try { + int entryID = Integer.parseInt(line.substring(SIZE_OF_DELETE).trim()); + int index = getIndexFromEntryID(entryID); + if (hydrationArrayList.isEmpty()) { + HydrationListUI.emptyHydrationList(); + } else if (index == NO_INDEX_FOUND) { + HydrationListUI.unsuccessfulDeletedMessage(entryID); + } else { + Entry toDelete = hydrationArrayList.get(index); + hydrationArrayList.remove((index)); + HydrationListUI.successfulDeletedMessage(toDelete); + updateFile(); + } + } catch (IndexOutOfBoundsException e) { + System.out.println(HydrationListUI.deleteLogIndexMessage()); + } catch (NumberFormatException e) { + System.out.println(HydrationListUI.deleteLogNumberMessage()); + } + } + + public int getIndexFromEntryID(int lastEntryID) { + for (int i = 0; i < hydrationArrayList.size(); i++) { + if (hydrationArrayList.get(i).getEntryID() == lastEntryID) { + return i; + } + } + return NO_INDEX_FOUND; + } + + /** + * Adds a new liquid entry based on the provided input. + * + * @param input the input string containing liquid entry information + */ + public void addEntry(String input) { + assert (input.startsWith("hydration in")) : "ensures that input is correct"; + logr.setLevel(Level.SEVERE); + try { + Entry newEntry = ParserHydration.parseHydrationInput(input, lastHydrationEntryID); + hydrationArrayList.add(newEntry); + HydrationListUI.printNewHydrationEntry(newEntry); + lastHydrationEntryID++; + if (hydrationArrayList.size() > 1 && + hydrationArrayList.get(hydrationArrayList.size() - 2).getDate().compareTo(newEntry.getDate()) > 0) { + sortEntriesByDate(); + } + updateFile(); + } catch (InvalidInputException e) { + logr.log(Level.WARNING, e.getMessage(), e); + System.out.println(e.getMessage()); + } + } + + public void sortEntriesByDate() { + Collections.sort(hydrationArrayList, new Comparator() { + @Override + public int compare(Entry entry1, Entry entry2) { + return entry1.getDate().compareTo(entry2.getDate()); + } + }); + } + + /** + * Prints the list of liquid entries. + * If the list is empty, prints a message indicating that the list is empty. + */ + public void printHydrationList() { + if (hydrationArrayList.isEmpty()) { + HydrationListUI.emptyListMessage(); + } else { + HydrationListUI.hydrationListHeader(); + printHydrationInflow(); + } + } + + public void printHydrationInflow() { + int serialNumber = 1; + for (Entry value : hydrationArrayList) { + if (value instanceof HydrationEntry) { + Entry entry = value; + System.out.println("\t " + serialNumber + ". " + entry); + serialNumber++; + } + } + } + + /** + * Retrieves the total amount of hydration consumed. + * + * @return the total amount of hydration consumed + */ + public int getHydrationConsumed(LocalDate date) { + int totalHydration = 0; + for (Entry entry : hydrationArrayList) { + if (entry.getDate().isEqual(date)) { + HydrationEntry tempEntry = (HydrationEntry) entry; + totalHydration += tempEntry.getHydration(); + } + } + return totalHydration; + } + + public int getHydrationConsumedCurrentDay() { + int totalHydration = 0; + for (Entry entry : hydrationArrayList) { + if (entry.getDate().isEqual(LocalDate.now())) { + HydrationEntry tempEntry = (HydrationEntry) entry; + totalHydration += tempEntry.getHydration(); + } + } + return totalHydration; + } + + + /** + * Retrieves the size of the liquid list. + * + * @return the number of liquid entries in the list + */ + public int getSize() { + return hydrationArrayList.size(); + } + + private int loadLastEntryID() { + return HydrationFileHandler.maxHydrationID; + } + + //@@author paturikarthik + public void findEntries(String input){ + ParserHydration.findHydrationListEntries(input,this); + } + + public void addHydrationEntry(Entry entry){ + this.hydrationArrayList.add(entry); + } + + public void printFoundHydrationList() { + if (hydrationArrayList.isEmpty()) { + HydrationListUI.emptyFoundListMessage(); + } else { + HydrationListUI.hydrationListFoundHeader(); + printHydrationInflow(); + } + } + +} diff --git a/src/main/java/seedu/lifetrack/sleep/sleeplist/SleepEntry.java b/src/main/java/seedu/lifetrack/sleep/sleeplist/SleepEntry.java new file mode 100644 index 0000000000..d06e1db096 --- /dev/null +++ b/src/main/java/seedu/lifetrack/sleep/sleeplist/SleepEntry.java @@ -0,0 +1,60 @@ +//@@author a-wild-chocolate +package seedu.lifetrack.sleep.sleeplist; + +import seedu.lifetrack.Entry; + +import java.time.LocalDate; + +public class SleepEntry extends Entry { + + private static int sleepEntryNum=1; + private LocalDate date; + private double duration; + private int sleepEntryID; + + + /*** + * Sleep constructor: date can be empty. If date input is empty, automatically fill with N/A; + * date should be in format DDMMYY, duration should be a positive real number in hour unit. + * @param date + * @param duration + */ + public SleepEntry (double duration, LocalDate date){ + super(sleepEntryNum++, "SLEEP", date); + this.date = date; + this.duration = duration; + this.sleepEntryID=sleepEntryNum-1; + } + + public SleepEntry (int sleepEntryID,double duration, LocalDate date){ + super(sleepEntryID, "SLEEP", date); + if(sleepEntryNum<=sleepEntryID){ + sleepEntryNum=sleepEntryID+1; + } + this.date = date; + this.duration = duration; + this.sleepEntryID=sleepEntryID; + } + + public LocalDate getDate() { + return date; + } + + public double getDuration() { + return duration; + } + + public int getSleepEntryID() { + return sleepEntryID; + } + + public String toString() { + return "\t Sleep ID: " +this.sleepEntryID+", Date: " + date + + ", Duration: " + String.format("%.2f", duration) + " hours"; + } + + public String toFileFriendlyString() { + return (sleepEntryID + ";" + date + ";" + duration); + } +} +//@@author diff --git a/src/main/java/seedu/lifetrack/sleep/sleeplist/SleepList.java b/src/main/java/seedu/lifetrack/sleep/sleeplist/SleepList.java new file mode 100644 index 0000000000..8bcd83706c --- /dev/null +++ b/src/main/java/seedu/lifetrack/sleep/sleeplist/SleepList.java @@ -0,0 +1,149 @@ +//@@author a-wild-chocolate +package seedu.lifetrack.sleep.sleeplist; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.system.exceptions.InvalidInputException; +import seedu.lifetrack.system.parser.ParserSleep; +import seedu.lifetrack.system.storage.SleepFileHandler; +import seedu.lifetrack.ui.SleepListUi; + +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncorrectSleepDeleteMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getSleepDurationSumTooLongMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getEmptySleepListMessage; + + +public class SleepList { + + private static int DELETE_IDX = 2; + private static int DELETE_LEN = 3; + private ArrayList sleepList; + private SleepFileHandler fileHandler; + private int lastSleepEntryID; + + //constructor for JUnit tests + public SleepList() { + sleepList = new ArrayList<>(); + } + + //constructor for usage in terminal + public SleepList(String filePath) { + try { + fileHandler = new SleepFileHandler(filePath); + sleepList = fileHandler.getSleepEntriesFromFile(); + this.lastSleepEntryID = loadLastEntryID(); + } catch (FileNotFoundException e) { + sleepList = new ArrayList<>(); + } + } + + private void updateFile() { + if (fileHandler != null) { + fileHandler.writeEntries(sleepList); + } + } + + public Entry getSleep(int index) { + assert index >= 0 && index < sleepList.size() : "Index out of bounds"; + return sleepList.get(index); + } + + public void addSleep(String input) { + try { + SleepEntry newSleep = ParserSleep.parseSleepInput(input); + double sleepRecord = newSleep.getDuration(); + for(int i=0;i24){ + System.out.println(getSleepDurationSumTooLongMessage()); + return; + } + sleepList.add(newSleep); + SleepListUi.printNewSleepEntry(newSleep); + //only sort if newly added date is earlier than date in final entry before adding entry + if (sleepList.size() > 1 && + sleepList.get(sleepList.size() - 2).getDate().compareTo(newSleep.getDate()) > 0 ) { + sortEntriesByDate(); + } + updateFile(); + } catch (InvalidInputException e) { + System.out.println(e.getMessage()); + } + } + + public void sortEntriesByDate() { + Collections.sort(sleepList, new Comparator() { + @Override + public int compare(Entry entry1, Entry entry2) { + return entry1.getDate().compareTo(entry2.getDate()); + } + }); + } + + public void deleteSleep(String line) { + try { + if(line.split(" ").length!=DELETE_LEN) { + System.out.println(getIncorrectSleepDeleteMessage()); + return; + } + int index = Integer.parseInt(line.split(" ")[DELETE_IDX]) ; //User input format: sleep delete ID + if(sleepList.isEmpty()){ + System.out.println(getEmptySleepListMessage()); + return; + } + for(int i=0; i\n" + + "\t 3) user update weight \n" + + "\t 4) user update age \n" + + "\t 5) user update sex \n" + + "\t 6) user update exercise levels \n" + + "\t 7) user update goal "; + + //general error messages + public static String getInvalidDateMessage() { + String message = "\t Invalid date! Please enter a valid date in format YYYY-MM-DD."; + return message; + } + + public static String getDateLaterThanPresentDateMessage() { + String message = "\t Invalid date! Please enter a date that is not later than today's date: " + LocalDate.now(); + return message; + } + + //calories list related messages + public static String getCaloriesIncorrectOrderMessage() { + String message = "\t Please ensure that you have keyed the input in the correct order!\n"; + return HEADER + message + CALORIES_IN_INPUT; + } + + public static String getCaloriesDuplicateKeywordMessage() { + String message = "\t Please ensure there are no duplicate c/ or d/ or m/ keywords!\n"; + return HEADER + message + CALORIES_IN_INPUT; + } + + public static String getIncorrectCaloriesInputMessage() { + return "\t Please input only positive integers into the calories field!"; + } + + public static String getCaloriesOverLimitMessage() { + return "\t Please ensure that calories is within the limit of 5000 calories per entry!"; + } + + public static String getCaloriesMissingKeywordsMessage() { + String message = "\t Please ensure that you have entered all keywords!\n"; + return HEADER + message + CALORIES_IN_INPUT; + } + + public static String getWhitespaceInInputMessage() { + String message = "\t Please ensure that there is no whitespace in your input!\n"; + return HEADER + message + CALORIES_IN_INPUT; + } + + public static String getHydrationWhitespaceInInputMessage() { + String message = "\t Please ensure that there is no whitespace in your input!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getEmptyMacrosMessage() { + String message = "\t Your macronutrients field is empty!\n"; + return HEADER + message + MACROS_INPUT; + } + + public static String getIncorrectMacrosInputMessage() { + return "\t Please input only positive integers into the macronutrients field!"; + } + + public static String getMacrosOverLimitMessage() { + return "\t Please ensure that all macros entered are within the limit of 800g per macro per entry!"; + } + + public static String getIncompleteMacrosMessage() { + String message = "\t Please ensure that all macronutrients fields are filled up!\n"; + return HEADER + message + MACROS_INPUT; + } + + public static String getWhitespaceInMacrosInputMessage() { + String message = "\t Please ensure that there is no whitespace in your macros input!\n"; + return HEADER + message + MACROS_INPUT; + } + + public static String getMacrosInCaloriesOutMessage() { + String message = "\t Calorie output entry cannot have macros!\n"; + return HEADER + message + CALORIES_OUT_INPUT; + } + + //hydration list related messages + public static String getHydrationMissingKeywordMessage() { + String message = "\t Please ensure that you have entered all keywords!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getHydrationIncorrectOrderMessage() { + String message = "\t Please ensure that you have keyed the input in the correct order!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getHydrationEmptyDescriptionMessage() { + String message = "\t Please ensure that beverage and volume is not empty!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getHydrationNegativeIntegerVolumeMessage() { + String message = "\t Please ensure that positive integer value is keyed in for volume!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getHydrationOverVolumeLimitMessage() { + String message = "\t Please ensure that volume is not more than 10000!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getHydrationDuplicateInputsMessage() { + String message = "\t Please ensure that there are no duplicate v/ or d/ inputs!\n"; + return HEADER + message + HYDRATION_IN_INPUT; + } + + public static String getIncorrectVolumeInputMessage() { + return "\t Please input only positive integers into the volume field!"; + } + + //sleep list related messages + //@@author a-wild-chocolate + public static String getSleepMissingKeywordMessage() { + String message = "\t Please ensure that you have entered all keywords!\n"; + return HEADER + message + SLEEP_IN_INPUT; + } + + public static String getIncorrectSleepInputMessage() { + return "\t Please input one positive real number into the sleep duration field!"; + } + + public static String getIncorrectSleepDeleteMessage() { + return "\t Please input delete command in correct format: sleep delete SLEEPID "; + } + + public static String getEmptySleepListMessage() { + return "\t Sorry, there is no sleep record in the sleep list." + + "You cannot delete sleep entry."; + } + + public static String getTooLongSleepDurationMessage() { + return "\t Please enter a sleep duration less than 24 hours."; + } + + public static String getSleepDurationSumTooLongMessage() { + return "\t Sorry, your sum of sleep duration exceeds 24 hours in this day. This record failed to add " + + "into the list. Please ensure your sum of duration of a day do not exceed 24 hours."; + } + //@@author + + //user related messages + public static String getOutOfGoalRangeMessage() { + return "\t Invalid Goal input!\n " + + "Please key in a number between 1 and 5!\n " + + "1 being quick fat loss " + + "and 5 being quick bulking"; + } + + public static String getOutOfExerciseLevelsRangeMessage() { + return "\t Invalid Exercise Level input!\n" + + "\t Please key in a number between 1 and 5!\n" + + " 1 being little exercise done per week and 5 being" + + " very heavy levels of exercise done per week."; + } + + public static String getEmptyUserSetupInputMessage() { + return HEADER + "\t Please key in the relevant user fields!\n" + USER_SETUP_INPUT; + } + + public static String getInvalidNumberOfSetUpInputs() { + return "\t Sorry, this command is invalid. Please enter the setup command in the following format " + + "user setup {NAME} h/{HEIGHT} w/{WEIGHT} a/{AGE} s/{SEX} e/{EXERCISE LEVELS} g/{GOAL}"; + } + + public static String getInvalidGoalNumberMessage() { + return "\t Invalid input for goal number. Please enter a number between 1 and 5."; + } + + public static String getInvalidExerciseLevelsNumberMessage() { + return "\t Invalid input for exercise level. Please enter a number between 1 and 5."; + } + + public static String getHeightOutOfRangeMessage() { + return "\t Please enter a valid height!"; + } + + public static String getInvalidHeightNumberMessage(){ + return "\t Please enter your height(in cm) as an integer!"; + } + + public static String getWeightOutOfRangeMessage(){ + return "\t Please enter a valid weight!"; + } + + public static String getInvalidWeightNumberMessage(){ + return "\t Please enter your weight(in kg) as an integer!"; + } + + public static String getUnderAgeMessage(){ + return "\t You are too young to use this app :("; + } + + public static String getAgeOutOfRangeMessage(){ + return "\t Please enter a valid age!"; + } + + public static String getInvalidAgeNumberMessage(){ + return "\t Please enter your age as an integer!"; + } + public static String getEmptyUserUpdateInputMessage() { + return "\t Please enter your name!"; + } + public static String getUnknownUpdateMessage() { + return "\t Oops, I've not seen this command before!\n" + + "\t Here are a list of possible update commands:\n" + USER_UPDATE_FIELDS; + } + + public static String getEmptyGenderInputMessage() { + return "\t Please enter your gender!"; + } + + public static String getInvalidGenderInputMessage() { + return "\t Please enter either male or female as your gender!"; + } + + public static String getEmptyNameInputMessage() { + return "\t Please enter a non-empty name!"; + } + + public static String getEmptyUserUpdateFieldMessage(){ + return "\t Please include the value you would like to update the field to as such:\n" + USER_UPDATE_FIELDS; + } + + public static String getReachedMaximumSleepMessage(){ + return "\t Invalid input! The total duration you have slept on this day exceeds 24 hours!"; + } +} diff --git a/src/main/java/seedu/lifetrack/system/parser/ParserCalories.java b/src/main/java/seedu/lifetrack/system/parser/ParserCalories.java new file mode 100644 index 0000000000..0212e399a8 --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/parser/ParserCalories.java @@ -0,0 +1,371 @@ +//@@author owx0130 +package seedu.lifetrack.system.parser; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.calories.Food; +import seedu.lifetrack.calories.calorielist.CalorieList; +import seedu.lifetrack.calories.calorielist.InputEntry; +import seedu.lifetrack.calories.calorielist.OutputEntry; +import seedu.lifetrack.system.exceptions.InvalidInputException; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getCaloriesIncorrectOrderMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getCaloriesMissingKeywordsMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getDateLaterThanPresentDateMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getEmptyMacrosMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncompleteMacrosMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncorrectCaloriesInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncorrectMacrosInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidDateMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getMacrosInCaloriesOutMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getWhitespaceInInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getWhitespaceInMacrosInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getCaloriesOverLimitMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getMacrosOverLimitMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getCaloriesDuplicateKeywordMessage; + +public class ParserCalories { + + private static final int CARBS_IDX = 0; + private static final int PROTEINS_IDX = 1; + private static final int FATS_IDX = 2; + private static final int CALORIES_OUT_PADDING = 12; + private static final int CALORIES_FIND_LENGTH = "calories find".length(); + private static final int CALORIES_LIMIT_5000 = 5000; + private static final int MACROS_LIMIT_800 = 800; + + /** + * Parses a string input to create an Entry object representing calorie intake. + *

+ * This method expects the input string to follow a specific format, where the + * description, calorie count, date and macronutrients are separated by the + * delimiters 'desc/', 'c/', 'date/', and 'm/'. The method extracts these components + * and creates either an InputEntry or OutputEntry object depending on the user command. + * If required inputs are missing or empty, an InvalidInputException is thrown. + * + * @param input the input string to be parsed, containing date, time, activity, + * and calorie count information + * @return an Entry object representing calorie intake + * @throws InvalidInputException if the input string is missing components or contains empty fields + */ + public static Entry parseCaloriesInput(String input, int lastEntryID) throws InvalidInputException { + int caloriesIndex = input.indexOf("c/"); + int dateIndex = input.indexOf("d/"); + int macrosIndex = input.indexOf("m/"); + + checkKeywordsExist(caloriesIndex, dateIndex); + assert caloriesIndex != -1 : "The c/ keyword should exist!"; + assert dateIndex != -1 : "The d/ keyword should exist!"; + + checkKeywordsCorrectlyOrdered(caloriesIndex, dateIndex, macrosIndex); + assert caloriesIndex < dateIndex : "The c/ keyword must appear before strDate/ in the input!"; + + //extract command, description, calories, strDate from input + String[] parts = input.split("c/|d/|m/"); + String command = parts[0].substring(0, CALORIES_OUT_PADDING).trim(); + String description = getDescriptionFromInput(input, command, caloriesIndex); + String strCalories = parts[1].trim(); + + // Used to detect whether there are duplicate parameters entered + int caloriesCount = input.split("c/").length - 1; + int dateCount = input.split("d/").length - 1; + int macrosCount = input.split("m/").length - 1; + + // catch duplicate input for c/ and d/ + try { + checkNoDuplicateKeywords(caloriesCount); + checkNoDuplicateKeywords(dateCount); + } catch (InvalidInputException e) { + throw new InvalidInputException(getCaloriesDuplicateKeywordMessage()); + } + + // Try catch here is needed because if user inputs "calories in chicken c/1000 d/ "", + // code fails because index out of bounds occurs due to parts[2].trim() + String strDate = null; + try { + strDate = parts[2].trim(); + } catch (ArrayIndexOutOfBoundsException e) { + throw new InvalidInputException(getWhitespaceInInputMessage()); + } + + checkInputsAreNonEmpty(description, strCalories, strDate); + assert description != "" : "The description field should be a non-empty string!"; + assert strCalories != "" : "The calories field should be a non-empty string!"; + assert strDate != "" : "The strDate field should be a non-empty string!"; + + //extract macronutrients if user provided it in their input, otherwise initialise it as null + int[] macros = null; + if (macrosIndex != -1) { + if (command.equals("calories out")) { + throw new InvalidInputException(getMacrosInCaloriesOutMessage()); + } + try { + String macroString = parts[3].trim(); + macros = getMacrosFromInput(macroString); + } catch (InvalidInputException e) { + throw new InvalidInputException(e.getMessage()); + } catch (ArrayIndexOutOfBoundsException e) { + throw new InvalidInputException(getEmptyMacrosMessage()); + } + // try catch for duplicate macros + try { + checkNoDuplicateKeywords(macrosCount); + } catch (InvalidInputException e) { + throw new InvalidInputException(getCaloriesDuplicateKeywordMessage()); + } + } + + int calories; + //convert calories from string to integer + try { + calories = getIntegerCaloriesFromInput(strCalories); + checkCaloriesIsPositiveInteger(calories); + } catch (InvalidInputException e) { + throw new InvalidInputException(getIncorrectCaloriesInputMessage()); + } + + if (calories > CALORIES_LIMIT_5000) { + throw new InvalidInputException(getCaloriesOverLimitMessage()); + } + + assert calories > 0 : "Calories value must be a positive integer!"; + + //@@author rexyyong + // Convert strDate from type String to date of type LocalDate + LocalDate date = null; + try { + date = getLocalDateFromInput(strDate); + + if (date.isAfter(LocalDate.now())) { + throw new InvalidInputException(getDateLaterThanPresentDateMessage()); + } + } catch (DateTimeParseException e) { + throw new InvalidInputException(getInvalidDateMessage()); + } + //@@author owx0130 + + lastEntryID++; + if (command.equals("calories out")) { + return makeNewOutputEntry(lastEntryID, description, calories, date); + } else if (macros == null) { + return makeNewInputEntry(lastEntryID, description, calories, date); + } else { + return makeNewInputEntry(lastEntryID, description, calories, date, macros); + } + } + + //@@author rexyyong + public static void checkNoDuplicateKeywords(int keywordCount) throws InvalidInputException { + if (keywordCount != 1) { + throw new InvalidInputException(); + } + } + + /** + * Parses a string representation of a date and returns a LocalDate object. + * + * @param strDate the string representation of the date + * @return the LocalDate object parsed from the input string + * @throws DateTimeParseException if the input string cannot be parsed as a valid date + */ + public static LocalDate getLocalDateFromInput(String strDate) throws DateTimeParseException { + LocalDate date = LocalDate.parse(strDate); + return date; + } + //@@author owx0130 + + /** + * Parses a string representation of calories and returns an integer value. + * + * @param strCalories the string representation of calories + * @return the integer value of calories parsed from the input string + */ + private static int getIntegerCaloriesFromInput(String strCalories) throws InvalidInputException { + int calories = 0; + try { + calories = Integer.parseInt(strCalories); + } catch (NumberFormatException e) { + throw new InvalidInputException(); + } + return calories; + } + + /** + * Extracts the description from the input string based on the command and the index of calories. + * + * @param inputString the input string containing the description + * @param command the command string indicating the type of entry ("calories in" or "calories out") + * @param caloriesIndex the index of the "calories" keyword in the input string + * @return the description extracted from the input string + */ + private static String getDescriptionFromInput(String inputString, String command, int caloriesIndex) { + String description; + if (command.equals("calories out")) { + description = inputString.substring(CALORIES_OUT_PADDING, caloriesIndex).trim(); + } else { + command = inputString.substring(0, CALORIES_OUT_PADDING - 1).trim(); + description = inputString.substring(CALORIES_OUT_PADDING - 1, caloriesIndex).trim(); + } + return description; + } + + /** + * Parses a string containing macros (carbs, proteins, fats) and returns an array of integers. + * The string should be in the format "carbs,proteins,fats". + * + * @param macroString the string containing macros separated by commas + * @return an array of integers representing macros [carbs, proteins, fats] + * @throws InvalidInputException if the input string is not in the correct format or contains invalid values + */ + private static int[] getMacrosFromInput(String macroString) throws InvalidInputException { + int[] macros = new int[3]; + try { + String[] macroParts = macroString.split(","); + int idx = 0; + for (String macro : macroParts) { + //throw exception if user inputs whitespace in the macros field i.e. m/123, ,123 + if (macro.trim().isEmpty()) { + throw new InvalidInputException(getWhitespaceInMacrosInputMessage()); + } + int macrosInt = Integer.parseInt(macro.trim()); + //Exception to handle negative macros + if (macrosInt <= 0) { + throw new InvalidInputException(getIncorrectMacrosInputMessage()); + } + //exception to handle macros over limit + if (macrosInt > MACROS_LIMIT_800) { + throw new InvalidInputException(getMacrosOverLimitMessage()); + } + macros[idx] = macrosInt; + idx++; + } + //throw exception if there are missing values in the macros field + if (idx != 3) { + throw new InvalidInputException(getIncompleteMacrosMessage()); + } + } catch (NumberFormatException e) { + throw new InvalidInputException(getIncorrectMacrosInputMessage()); + } + return macros; + } + + /** + * Checks if the calorie value is a positive integer. + * + * @param calories the calorie value to check + * @throws InvalidInputException if the calorie value is not a positive integer + */ + private static void checkCaloriesIsPositiveInteger(int calories) throws InvalidInputException { + if (calories <= 0) { + throw new InvalidInputException(); + } + } + + /** + * Checks if the inputs (description, calories, and date) are non-empty strings. + * + * @param description the description of the entry + * @param strCalories the string representation of calories + * @param date the date of the entry + * @throws InvalidInputException if any of the inputs are empty strings + */ + private static void checkInputsAreNonEmpty(String description, String strCalories, String date) + throws InvalidInputException { + //check if the description, calories or date fields are empty + if (description.isEmpty() || strCalories.isEmpty() || date.isEmpty()) { + throw new InvalidInputException(getWhitespaceInInputMessage()); + } + } + + /** + * Checks if the keywords for calories and date exist in the input string. + * + * @param caloriesIndex the index of the "c/" keyword in the input + * @param dateIndex the index of the "date/" keyword in the input + * @throws InvalidInputException if the keywords are missing + */ + private static void checkKeywordsExist(int caloriesIndex, int dateIndex) throws InvalidInputException { + //check that c/ and date/ keywords exist in the input, else throw exception + if (caloriesIndex == -1 || dateIndex == -1) { + throw new InvalidInputException(getCaloriesMissingKeywordsMessage()); + } + } + + /** + * Checks if the keywords for calories and date are correctly ordered in the input string. + * + * @param caloriesIndex the index of the "c/" keyword in the input + * @param dateIndex the index of the "date/" keyword in the input + * @param macrosIndex the index of the "macros/" keyword in the input + * @throws InvalidInputException if the keywords are not correctly ordered + */ + private static void checkKeywordsCorrectlyOrdered(int caloriesIndex, int dateIndex, int macrosIndex) + throws InvalidInputException { + if ((macrosIndex != -1 && !(caloriesIndex < dateIndex && dateIndex < macrosIndex)) || + (macrosIndex == -1 && !(caloriesIndex < dateIndex))) { + throw new InvalidInputException(getCaloriesIncorrectOrderMessage()); + } + } + + /** + * Creates a new output entry with the given description, calories, and date. + * + * @param description the description of the entry + * @param calories the number of calories + * @param date the date of the entry + * @return a new OutputEntry object + */ + private static Entry makeNewOutputEntry(int lastEntryID, String description, int calories, LocalDate date) { + return new OutputEntry(lastEntryID, description, calories, date); + } + + /** + * Creates a new input entry with the given description, calories, and date. + * + * @param description the description of the entry + * @param calories the number of calories + * @param date the date of the entry + * @return a new InputEntry object + */ + private static Entry makeNewInputEntry(int lastEntryID, String description, int calories, LocalDate date) { + return new InputEntry(lastEntryID, description, calories, date); + } + + /** + * Creates a new input entry with the given description, calories, date, and food macros. + * + * @param description the description of the entry + * @param calories the number of calories + * @param date the date of the entry + * @param foodMacros an array containing food macros (carbs, proteins, fats) + * @return a new InputEntry object with food macros + */ + private static Entry makeNewInputEntry(int lastEntryID, String description, int calories, LocalDate date, + int[] foodMacros) { + Food newFood = new Food(foodMacros[CARBS_IDX], foodMacros[PROTEINS_IDX], foodMacros[FATS_IDX]); + return new InputEntry(lastEntryID, description, calories, date, newFood); + } + + //@@author paturikarthik + public static void findCalorieListEntries(String input, CalorieList originalList) { + String keyword = input.substring(CALORIES_FIND_LENGTH).trim(); + CalorieList searchList = new CalorieList(); + for (int i = 0; i < originalList.getSize(); i++) { + Entry entry = originalList.getEntry(i); + if (entry.getDescription().contains(keyword)) { + if (entry instanceof InputEntry) { + Entry entryToAdd = makeNewInputEntry(entry.getEntryID(), entry.getDescription() + , ((InputEntry) entry).getCalories(), entry.getDate()); + searchList.addCalorieEntry(entryToAdd); + } else { + Entry entryToAdd = makeNewOutputEntry(entry.getEntryID(), entry.getDescription(), + ((OutputEntry) entry).getCalories(), entry.getDate()); + searchList.addCalorieEntry(entryToAdd); + } + } + } + searchList.printFoundCalorieList(); + } +} diff --git a/src/main/java/seedu/lifetrack/system/parser/ParserHydration.java b/src/main/java/seedu/lifetrack/system/parser/ParserHydration.java new file mode 100644 index 0000000000..7f139bbc0d --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/parser/ParserHydration.java @@ -0,0 +1,238 @@ +//@@author shawnpong +package seedu.lifetrack.system.parser; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.hydration.hydrationlist.HydrationEntry; +import seedu.lifetrack.hydration.hydrationlist.HydrationList; +import seedu.lifetrack.system.exceptions.InvalidInputException; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationWhitespaceInInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getDateLaterThanPresentDateMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidDateMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationOverVolumeLimitMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncorrectVolumeInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationNegativeIntegerVolumeMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationEmptyDescriptionMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationMissingKeywordMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationIncorrectOrderMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHydrationDuplicateInputsMessage; + + + + + +public class ParserHydration { + private static final int VOLUME_IDX = 1; + private static final int DATE_IDX = 2; + private static final int HYDRATION_ADD_PADDING = 13; + private static final int HYDRATION_LIMIT_10000 = 10000; + private static final int HYDRATION_FIND_LENGTH = "hydration find".length(); + + /** + * Parses a string input to create a Liquid object representing liquid intake. + * + * This method expects the input string to follow a specific format, where the + * beverage name and quantity are separated by the delimiters 'b/' and 'v/'. + * The method extracts these components and creates a Liquid object. + * If any part of the input is missing or empty, an InvalidInputException is thrown. + * + * @param input the input string to be parsed, containing beverage name and quantity + * @return a Liquid object representing liquid intake + * @throws InvalidInputException if the input string is missing components or + * contains empty fields + */ + public static Entry parseHydrationInput(String input, int lastHydrationEntryID) throws InvalidInputException { + + int volumeIndex = input.indexOf("v/"); + int dateIndex = input.indexOf("d/"); + + checkKeywordsExist(dateIndex, volumeIndex); + assert volumeIndex != -1 : "The v/ keyword should exist!"; + assert dateIndex != -1 : "The d/ keyword should exist!"; + + checkKeywordsCorrectlyOrdered(dateIndex, volumeIndex); + assert volumeIndex < dateIndex : "The v/ keyword must appear before date/ in the input!"; + + int volumeCount = input.split("v/").length - 1; + int dateCount = input.split("d/").length - 1; + + checkNoDuplicateKeywords(volumeCount, dateCount); + assert volumeCount == 1 : "The v/ keyword must appear before date/ in the input!"; + assert dateCount == 1 : "The v/ keyword must appear before date/ in the input!"; + + String[] parts = input.split("v/|d/"); + String description = getDescriptionFromInput(input, volumeIndex); + String strVolume = parts[VOLUME_IDX].trim(); + + // Try catch here is needed because if user inputs "hydration in milo v/1000 d/ "", + // code fails because index out of bounds occurs due to parts[DATE_IDX].trim() + String strDate = null; + try { + strDate = parts[DATE_IDX].trim(); + } catch (ArrayIndexOutOfBoundsException e) { + throw new InvalidInputException(getHydrationWhitespaceInInputMessage()); + } + + checkInputsAreNonEmpty(description, strVolume, strDate); + assert description != "" : "The description field should be a non-empty string!"; + assert strVolume != "" : "The volume field should be a non-empty string!"; + assert strDate != "" : "The date field should be a non-empty string!"; + + int volume = getIntegerVolumeFromInput(strVolume); + checkVolumeIsPositiveInteger(volume); + checkVolumeMaxAllowable(volume); + assert volume > 0 : "Volume value must be a positive integer!"; + + //@@author rexyyong + // Convert strDate from type String to date of type LocalDate + LocalDate date = null; + try { + date = getLocalDateFromInput(strDate); + if (date.isAfter(LocalDate.now())) { + throw new InvalidInputException(getDateLaterThanPresentDateMessage()); + } + } catch (DateTimeParseException e) { + throw new InvalidInputException(getInvalidDateMessage()); + } + //@@author + lastHydrationEntryID++; + return makeNewInputEntry(lastHydrationEntryID, description, volume, date); + } + + private static void checkNoDuplicateKeywords(int volumeCount, int dateCount) throws InvalidInputException{ + if (volumeCount != 1 || dateCount != 1) { + throw new InvalidInputException(getHydrationDuplicateInputsMessage()); + } + } + + private static void checkVolumeMaxAllowable(int volume) throws InvalidInputException { + if (volume > HYDRATION_LIMIT_10000) { + throw new InvalidInputException(getHydrationOverVolumeLimitMessage()); + } + } + + /** + * Creates a new HydrationEntry object with the specified description, volume, and date. + * + * @param description the description of the hydration entry + * @param volume the volume of liquid intake + * @param date the date of the hydration entry + * @return a new HydrationEntry object with the specified attributes + */ + private static HydrationEntry makeNewInputEntry(int lastHydrationEntryID, String description, int volume, + LocalDate date) { + return new HydrationEntry(lastHydrationEntryID, description, volume, date); + } + + /** + * Parses a string representation of a date and returns a LocalDate object. + * + * @param strDate the string representation of the date + * @return a LocalDate object representing the parsed date + * @throws DateTimeParseException if the input string cannot be parsed into a valid date + */ + //@@author rexyyong + public static LocalDate getLocalDateFromInput(String strDate) throws DateTimeParseException { + LocalDate date = LocalDate.parse(strDate); + return date; + } + //@@author + /** + * Parses the volume from a string and returns the integer value. + * + * @param strVolume the string representation of the volume + * @return the integer value of the volume + */ + private static int getIntegerVolumeFromInput(String strVolume) { + int volume = 0; + try { + volume = Integer.parseInt(strVolume); + } catch (NumberFormatException e) { + System.out.println(getIncorrectVolumeInputMessage()); + } + return volume; + } + + /** + * Checks if the given volume is a positive integer. + * + * @param volume the volume value to be checked + * @throws InvalidInputException if the volume is not a positive integer + */ + private static void checkVolumeIsPositiveInteger(int volume) throws InvalidInputException { + if (volume <= 0) { + throw new InvalidInputException(getHydrationNegativeIntegerVolumeMessage()); + } + } + + /** + * Checks if the description, volume, and date fields are non-empty. + * + * @param description the description of the hydration entry + * @param strVolume the string representation of the volume + * @param date the string representation of the date + * @throws InvalidInputException if any of the fields are empty + */ + private static void checkInputsAreNonEmpty(String description, String strVolume, String date) + throws InvalidInputException { + if (description.isEmpty() || strVolume.isEmpty() || date.isEmpty()) { + throw new InvalidInputException(getHydrationEmptyDescriptionMessage()); + } + } + + /** + * Extracts the description from the input string. + * + * @param inputString the input string containing description, volume, and date + * @param volumeIndex the index of the 'v/' delimiter + * @return the description extracted from the input string + */ + private static String getDescriptionFromInput(String inputString, int volumeIndex) { + String description; + description = inputString.substring(HYDRATION_ADD_PADDING, volumeIndex).trim(); + return description; + } + + /** + * Checks if the 'v/' and 'd/' keywords are in the correct order in the input string. + * + * @param dateIndex the index of the 'd/' keyword + * @param volumeIndex the index of the 'v/' keyword + * @throws InvalidInputException if the keywords are in the incorrect order + */ + private static void checkKeywordsCorrectlyOrdered(int dateIndex, int volumeIndex) throws InvalidInputException { + if (volumeIndex >= dateIndex) { + throw new InvalidInputException(getHydrationIncorrectOrderMessage()); + } + } + + /** + * Checks if the 'v/' and 'd/' keywords exist in the input string. + * + * @param dateIndex the index of the 'd/' keyword + * @param volumeIndex the index of the 'v/' keyword + * @throws InvalidInputException if any of the keywords are missing + */ + private static void checkKeywordsExist(int dateIndex, int volumeIndex) throws InvalidInputException { + if (dateIndex == -1 || volumeIndex == -1) { + throw new InvalidInputException(getHydrationMissingKeywordMessage()); + } + } + + //@@author paturikarthik + public static void findHydrationListEntries(String input, HydrationList originalList){ + String keyword = input.substring(HYDRATION_FIND_LENGTH).trim(); + HydrationList searchList = new HydrationList(); + for (int i = 0 ; i MAX_SLEEP_PER_DAY){ + throw new InvalidInputException(getReachedMaximumSleepMessage()); + } + + SleepEntry newSleep = new SleepEntry(duration, date); + + + return newSleep; + } + + private static double parseDuration(String durationStr) throws InvalidInputException { + double duration=0; + + try { + duration = Double.parseDouble(durationStr); + } catch (NumberFormatException e) { + throw new InvalidInputException(getIncorrectSleepInputMessage()); + } + + if (duration <= 0) { + throw new InvalidInputException(getIncorrectSleepInputMessage()); + } else if (duration >= 24) { + throw new InvalidInputException(getTooLongSleepDurationMessage()); + } + return duration; + } + + + private static LocalDate parseDate(String strDate) throws InvalidInputException { + try { + return LocalDate.parse(strDate, DateTimeFormatter.ISO_LOCAL_DATE); + } catch (DateTimeParseException e) { + throw new InvalidInputException(getInvalidDateMessage()); + } + + } + + private static void checkKeywordsExist(int dateIndex) throws InvalidInputException { + if (dateIndex == -1 ) { + throw new InvalidInputException(getSleepMissingKeywordMessage()); + } + } + + private static void checkValidFormat(String[] parts) throws InvalidInputException { + if (parts.length != STRING_PARTS_LEN || parts[DURATION_IDX].isEmpty()) { + throw new InvalidInputException("Please ensure that you have keyed in the correct format: " + + "sleep add d/"); + } + + } +} +//@@author diff --git a/src/main/java/seedu/lifetrack/system/parser/ParserUser.java b/src/main/java/seedu/lifetrack/system/parser/ParserUser.java new file mode 100644 index 0000000000..cf3c1985bd --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/parser/ParserUser.java @@ -0,0 +1,364 @@ +//@@author paturikarthik +package seedu.lifetrack.system.parser; + +import seedu.lifetrack.system.exceptions.InvalidInputException; +import seedu.lifetrack.user.User; + +import java.util.Objects; + +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getAgeOutOfRangeMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getEmptyGenderInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getEmptyNameInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getEmptyUserSetupInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getEmptyUserUpdateFieldMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getHeightOutOfRangeMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidAgeNumberMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidExerciseLevelsNumberMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidGenderInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidGoalNumberMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidHeightNumberMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidNumberOfSetUpInputs; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getInvalidWeightNumberMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getOutOfExerciseLevelsRangeMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getOutOfGoalRangeMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getUnderAgeMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getUnknownUpdateMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getWeightOutOfRangeMessage; +import static seedu.lifetrack.ui.UserUi.printNewUserAge; +import static seedu.lifetrack.ui.UserUi.printNewUserExerciseLevels; +import static seedu.lifetrack.ui.UserUi.printNewUserGoal; +import static seedu.lifetrack.ui.UserUi.printNewUserHeight; +import static seedu.lifetrack.ui.UserUi.printNewUserName; +import static seedu.lifetrack.ui.UserUi.printNewUserSex; +import static seedu.lifetrack.ui.UserUi.printNewUserWeight; +import static seedu.lifetrack.ui.UserUi.printUserCaloriesRequired; +import static seedu.lifetrack.ui.UserUi.printUserSetUpComplete; + +/** + * Utility Class to parse the commands made with regard to the User class. + */ +public class ParserUser { + private static final int LENGTH_OF_SETUP_COMMAND = "user setup".length(); + private static final int USER_INPUT_NAME_INDEX = 0; + private static final int USER_INPUT_HEIGHT_INDEX = 1; + private static final int USER_INPUT_WEIGHT_INDEX = 2; + private static final int USER_INPUT_AGE_INDEX = 3; + private static final int USER_INPUT_GENDER_INDEX = 4; + private static final int USER_INPUT_EXERCISE_LEVELS_INDEX = 5; + private static final int USER_INPUT_GOAL_INDEX = 6; + private static final int LENGTH_OF_UPDATE_COMMAND = "user update".length(); + private static final int LENGTH_OF_NAME = "name".length(); + private static final int LENGTH_OF_HEIGHT = "height".length(); + private static final int LENGTH_OF_WEIGHT = "weight".length(); + private static final int LENGTH_OF_AGE = "age".length(); + private static final int LENGTH_OF_SEX = "sex".length(); + private static final int LENGTH_OF_EXERCISE_LEVELS = "exercise levels".length(); + private static final int LENGTH_OF_GOAL = "goal".length(); + + /** + * Parses the input from user to sieve out name, height, weight, age, gender, exercise levels and goals of the user + * and sets them accordingly in the User class. + * + * @param input input String from the user + * @param user current user of the app + * @throws InvalidInputException if user does not input all the information in the correct order and format + * @throws NumberFormatException if user does not input a number for exercise levels and goals + */ + public static void parseSetUp(String input, User user) throws InvalidInputException, NumberFormatException { + checkEmptyInput(input); + + int heightIndex = input.indexOf("h/"); + String name = null; + if (heightIndex != -1) { + name = parseName(input.substring(LENGTH_OF_SETUP_COMMAND, heightIndex).trim()); + input = input.substring(heightIndex).toLowerCase(); + } + int weightIndex = input.indexOf("w/"); + int ageIndex = input.indexOf("a/"); + int sexIndex = input.indexOf("s/"); + int exerciseLevelsIndex = input.indexOf("e/"); + int goalIndex = input.indexOf("g/"); + + if (heightIndex == -1 || weightIndex == -1 || ageIndex == -1 || sexIndex == -1 + || exerciseLevelsIndex == -1 || goalIndex == -1) { + throw new InvalidInputException(getInvalidNumberOfSetUpInputs()); + } + checkSetUpInputsCorrectOrder(weightIndex, ageIndex, sexIndex, exerciseLevelsIndex, goalIndex); + + String[] parts = input.split("h/|w/|a/|s/|e/|g/"); + if (parts.length != 7) { + throw new InvalidInputException(getInvalidNumberOfSetUpInputs()); + } + int height = parseHeightIndex(parts[USER_INPUT_HEIGHT_INDEX].trim()); + int weight = parseWeightIndex(parts[USER_INPUT_WEIGHT_INDEX].trim()); + int age = parseAgeIndex(parts[USER_INPUT_AGE_INDEX].trim()); + String sex = parseGenderIndex(parts[USER_INPUT_GENDER_INDEX].trim().toLowerCase()); + int exerciseLevels = parseExerciseLevels(parts[USER_INPUT_EXERCISE_LEVELS_INDEX].trim()); + int goal = parseGoalIndex(parts[USER_INPUT_GOAL_INDEX].trim()); + + user.setName(name); + user.setHeight(height); + user.setWeight(weight); + user.setAge(age); + user.setSex(sex); + user.setExerciseLevels(exerciseLevels); + user.setGoal(goal); + user.getHealthInfo(); + printUserSetUpComplete(user); + } + + private static String parseName(String input) throws InvalidInputException { + if (input.isEmpty()) { + throw new InvalidInputException(getEmptyNameInputMessage()); + } + return input; + } + + /** + * Parses the user's height input for an Integer + * + * @param input user's height input + * @return user's height as an integer + * @throws InvalidInputException if the height input is not an integer or if the user's height is not between + * 90 and 225 cm + */ + private static int parseHeightIndex(String input) throws InvalidInputException { + try { + int height = Integer.parseInt(input); + if (height < 90 || height > 225) { + throw new InvalidInputException(getHeightOutOfRangeMessage()); + } + return height; + } catch (NumberFormatException e) { + throw new InvalidInputException(getInvalidHeightNumberMessage()); + } + } + + /** + * Parses the user's gender input for a String + * + * @param input user's gender input + * @return user's gender as a string + * @throws InvalidInputException if the gender input is empty or if it is not male/m or female/f + */ + private static String parseGenderIndex(String input) throws InvalidInputException { + try { + if (input.isEmpty()) { + throw new InvalidInputException(getEmptyGenderInputMessage()); + } + if (input.equals("male") || input.equals("m")) { + return "male"; + } + if (input.equals("female") || input.equals("f")) { + return "female"; + } + throw new InvalidInputException(getInvalidGenderInputMessage()); + } catch (NullPointerException e) { + throw new InvalidInputException(getEmptyUserSetupInputMessage()); + } + } + + /** + * Parses the user's weight input for an Integer + * + * @param input user's weight input + * @return user's weight as an integer + * @throws InvalidInputException if the weight input is not an integer or if the user's weight is not between + * 30 and 200 kg + */ + private static int parseWeightIndex(String input) throws InvalidInputException { + try { + int weight = Integer.parseInt(input); + if (weight < 30 || weight > 200) { + throw new InvalidInputException(getWeightOutOfRangeMessage()); + } + return weight; + } catch (NumberFormatException e) { + throw new InvalidInputException(getInvalidWeightNumberMessage()); + } + } + + /** + * Parses the user's age input for an Integer + * + * @param input user's age input + * @return user's age as an integer + * @throws InvalidInputException if the age input is not an integer or if the user's age is not between + * 13 and 30 years old + */ + private static int parseAgeIndex(String input) throws InvalidInputException { + try { + int age = Integer.parseInt(input); + if (age < 13 && age > 0) { + throw new InvalidInputException(getUnderAgeMessage()); + } + if (age < 0 || age > 30) { + throw new InvalidInputException(getAgeOutOfRangeMessage()); + } + return age; + } catch (NumberFormatException e) { + throw new InvalidInputException(getInvalidAgeNumberMessage()); + } + } + + /** + * Parses the user's goal input for an Integer and assigns the String equivalent of it + * + * @param input user's goal input + * @return String equivalent of User's goals + * @throws InvalidInputException if the goal input is not an integer between 1 and 7 + */ + private static int parseGoalIndex(String input) throws InvalidInputException { + try { + int goalNumber = Integer.parseInt(input); + if (goalNumber > 5 || goalNumber < 1) { + + throw new InvalidInputException(getOutOfGoalRangeMessage()); + } + return goalNumber; + } catch (NumberFormatException e) { + throw new InvalidInputException(getInvalidGoalNumberMessage()); + } + } + + /** + * Parses the user's exercise levels input for an Integer and assigns the String equivalent of it + * + * @param input user's exercise levels input + * @return String equivalent of User's exercise levels + * @throws InvalidInputException if the goal input is not an integer between 1 and 5 + */ + private static int parseExerciseLevels(String input) throws InvalidInputException { + try { + int levelInNumber = Integer.parseInt(input); + if (levelInNumber < 1 || levelInNumber > 5) { + throw new InvalidInputException(getOutOfExerciseLevelsRangeMessage()); + } + return levelInNumber; + } catch (NumberFormatException e) { + throw new InvalidInputException(getInvalidExerciseLevelsNumberMessage()); + } + } + + /** + * Ensures that the input given by the user is in the correct order + * + * @param weightIndex Index of the input where user's weight is specified + * @param ageIndex Index of the input where user's age is specified + * @param sexIndex Index of the input where user's gender is specified + * @param exerciseLevelsIndex Index of the input where user's exercise levels is specified in Integer form + * @param goalIndex Index of the input where user's goal is specified in Integer form + * @throws InvalidInputException if the order of the inputs is not correct. The input should be in this order: + * height, weight, age, gender, exercise levels and goal. + */ + private static void checkSetUpInputsCorrectOrder(int weightIndex, int ageIndex, int sexIndex, + int exerciseLevelsIndex, + int goalIndex) throws InvalidInputException { + if (!(weightIndex < ageIndex && sexIndex < exerciseLevelsIndex + && exerciseLevelsIndex < goalIndex)) { + throw new InvalidInputException(getInvalidNumberOfSetUpInputs()); + } + } + + /** + * Checks if User Setup command is empty + * + * @param input input from user + * @throws InvalidInputException if the command is empty + */ + private static void checkEmptyInput(String input) throws InvalidInputException { + if (input.substring(LENGTH_OF_SETUP_COMMAND).trim().isEmpty()) { + throw new InvalidInputException(getEmptyUserSetupInputMessage()); + } + } + + /** + * Parses "user update" command to update the relevant field of the user. Recalculates the user's calorific goal as + * well. + * + * @param input input from the user + * @param user current User + * @throws InvalidInputException if the command is empty, the update field is empty, if the field given to update + * is unknown or if the value to update is not correct. + */ + public static void parseUpdate(String input, User user) throws InvalidInputException { + checkEmptyUpdateInput(input); + String fieldToUpdate = input.substring(LENGTH_OF_UPDATE_COMMAND).trim(); + checkEmptyUpdateField(fieldToUpdate); + if (fieldToUpdate.startsWith("name ")) { + String name = parseName(fieldToUpdate.substring(LENGTH_OF_NAME).trim()); + user.setName(name); + assert Objects.equals(user.getName(), name); + printNewUserName(name); + } else if (fieldToUpdate.startsWith("height ")) { + int height = parseHeightIndex(fieldToUpdate.toLowerCase().substring(LENGTH_OF_HEIGHT).trim()); + user.setHeight(height); + assert Objects.equals(user.getHeight(), height); + printNewUserHeight(height); + user.getHealthInfo(); + printUserCaloriesRequired(user.getCaloriesRequired()); + } else if (fieldToUpdate.startsWith("weight ")) { + int weight = parseWeightIndex(fieldToUpdate.toLowerCase().substring(LENGTH_OF_WEIGHT).trim()); + user.setWeight(weight); + assert Objects.equals(user.getWeight(), weight); + printNewUserWeight(weight); + user.getHealthInfo(); + printUserCaloriesRequired(user.getCaloriesRequired()); + } else if (fieldToUpdate.startsWith("age ")) { + int age = parseAgeIndex(fieldToUpdate.toLowerCase().substring(LENGTH_OF_AGE).trim()); + user.setAge(age); + assert Objects.equals(user.getAge(), age); + printNewUserAge(age); + user.getHealthInfo(); + printUserCaloriesRequired(user.getCaloriesRequired()); + } else if (fieldToUpdate.startsWith("exercise levels ")) { + int level = parseExerciseLevels(fieldToUpdate.substring(LENGTH_OF_EXERCISE_LEVELS).trim()); + user.setExerciseLevels(level); + assert Objects.equals(user.getExerciseLevels(), level); + printNewUserExerciseLevels(user, level); + user.getHealthInfo(); + printUserCaloriesRequired(user.getCaloriesRequired()); + } else if (fieldToUpdate.startsWith("goal ")) { + int goal = parseGoalIndex(fieldToUpdate.toLowerCase().substring(LENGTH_OF_GOAL).trim()); + user.setGoal(goal); + assert Objects.equals(user.getGoal(), goal); + printNewUserGoal(user, goal); + user.getHealthInfo(); + printUserCaloriesRequired(user.getCaloriesRequired()); + } else if (fieldToUpdate.startsWith("sex ")) { + String sex = parseGenderIndex(fieldToUpdate.toLowerCase().substring(LENGTH_OF_SEX).trim()); + user.setSex(sex); + assert Objects.equals(user.getSex(), sex); + printNewUserSex(sex); + user.getHealthInfo(); + printUserCaloriesRequired(user.getCaloriesRequired()); + } else { + throw new InvalidInputException(getUnknownUpdateMessage()); + } + } + + /** + * Checks if the "user update" command is empty + * + * @param input input from the user + * @throws InvalidInputException if the command is empty + */ + private static void checkEmptyUpdateInput(String input) throws InvalidInputException { + if (input.substring(LENGTH_OF_UPDATE_COMMAND).trim().isBlank()) { + throw new InvalidInputException(getEmptyUserUpdateFieldMessage()); + } + } + + /** + * Checks if the field to update is empty + * + * @param input input from the user + * @throws InvalidInputException if the field is empty + */ + private static void checkEmptyUpdateField(String input) throws InvalidInputException { + if (input.trim().isEmpty()) { + throw new InvalidInputException(getEmptyUserUpdateFieldMessage()); + } + } + +} diff --git a/src/main/java/seedu/lifetrack/system/storage/CaloriesFileHandler.java b/src/main/java/seedu/lifetrack/system/storage/CaloriesFileHandler.java new file mode 100644 index 0000000000..ae9196479f --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/storage/CaloriesFileHandler.java @@ -0,0 +1,150 @@ +//@@author owx0130 +package seedu.lifetrack.system.storage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Scanner; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.calories.Food; +import seedu.lifetrack.calories.calorielist.InputEntry; +import seedu.lifetrack.calories.calorielist.OutputEntry; +import seedu.lifetrack.system.exceptions.FileHandlerException; + +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileCaloriesTooFewFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileTooFewMacrosMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileCaloriesTooManyFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidCaloriesMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidCarbsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidEntryIDMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidEntryTypeMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidFatsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidProteinsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileMacrosInOutputMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidDateMessage; + +public class CaloriesFileHandler extends FileHandler { + + //class-level member for lastEntryID calories + public static int maxCaloriesID = 0; + + //calorie list constants + private final int ENTRY_TYPE_INDEX = 3; + private final int CALORIES_INDEX = 4; + private final int CARBOHYDRATES_INDEX = 5; + private final int PROTEINS_INDEX = 6; + private final int FATS_INDEX = 7; + + public CaloriesFileHandler(String filePath) { + super(filePath); + } + + protected void checkCorrectNumberOfFields(int lineNumber, int dataLength) throws FileHandlerException { + if (dataLength < 5) { + throw new FileHandlerException(getFileCaloriesTooFewFieldsMessage(lineNumber, filePath)); + } else if (dataLength > 8) { + throw new FileHandlerException(getFileCaloriesTooManyFieldsMessage(lineNumber, filePath)); + } + } + + private void calculateMaxCaloriesEntry(int entryID) { + if (entryID > maxCaloriesID) { + maxCaloriesID = entryID; + } + } + + private void checkCaloriesIsPositive(int lineNumber, int calories) throws FileHandlerException { + if (calories <= 0) { + throw new FileHandlerException(getFileInvalidCaloriesMessage(lineNumber, filePath)); + } + } + + private void checkMacrosArePositive(int lineNumber, int carbs, int proteins, int fats) + throws FileHandlerException { + if (carbs <= 0) { + throw new FileHandlerException(getFileInvalidCarbsMessage(lineNumber, filePath)); + } else if (proteins <= 0) { + throw new FileHandlerException(getFileInvalidProteinsMessage(lineNumber, filePath)); + } else if (fats <= 0) { + throw new FileHandlerException(getFileInvalidFatsMessage(lineNumber, filePath)); + } + } + + private void checkValidEntryType(int lineNumber, String entryType, int dataLength) throws FileHandlerException { + if (!(entryType.equals("C_IN") || entryType.equals("C_OUT"))) { + throw new FileHandlerException(getFileInvalidEntryTypeMessage(lineNumber, filePath)); + } + + if (entryType.equals("C_IN") && dataLength != 8 && dataLength != 5) { + throw new FileHandlerException(getFileTooFewMacrosMessage(lineNumber, filePath)); + } else if (entryType.equals("C_OUT") && dataLength != 5) { + throw new FileHandlerException(getFileMacrosInOutputMessage(lineNumber, filePath)); + } + } + + private void getSingleCalorieEntry(ArrayList entries, String[] words, int lineNumber) + throws FileHandlerException { + checkCorrectNumberOfFields(lineNumber, words.length); + int entryID = Integer.parseInt(words[ENTRYID_INDEX].trim()); + checkPositiveEntryID(lineNumber, entryID); + calculateMaxCaloriesEntry(entryID); + LocalDate date = LocalDate.parse(words[DATE_INDEX].trim()); + checkDateNotLaterThanCurrent(lineNumber, date); + String description = words[DESCRIPTION_INDEX].trim(); + checkNonEmptyDescription(lineNumber, description); + int calories = Integer.parseInt(words[CALORIES_INDEX].trim()); + checkCaloriesIsPositive(lineNumber, calories); + String entryType = words[ENTRY_TYPE_INDEX].trim(); + checkValidEntryType(lineNumber, entryType, words.length); + if (entryType.equals("C_IN") && words.length == 8) { + int carbohydrates = Integer.parseInt(words[CARBOHYDRATES_INDEX].trim()); + int proteins = Integer.parseInt(words[PROTEINS_INDEX].trim()); + int fats = Integer.parseInt(words[FATS_INDEX].trim()); + Food food = new Food(carbohydrates, proteins, fats); + checkMacrosArePositive(lineNumber, carbohydrates, proteins, fats); + entries.add(new InputEntry(entryID, description, calories, date, food)); + } else if (entryType.equals("C_IN")) { + entries.add(new InputEntry(entryID, description, calories, date)); + } else { + entries.add(new OutputEntry(entryID, description, calories, date)); + } + } + + public ArrayList getCalorieEntriesFromFile() throws FileNotFoundException { + File f = new File(filePath); + Scanner s = new Scanner(f); + ArrayList entries = new ArrayList<>(); + String line = ""; + int i = 1; + while (s.hasNext()) { + line = s.nextLine(); + String[] words = line.split(";"); + try { + getSingleCalorieEntry(entries, words, i); + } catch (NumberFormatException e) { + if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[ENTRYID_INDEX] + "\"")) { + System.out.println(getFileInvalidEntryIDMessage(i, filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[CALORIES_INDEX] + "\"")) { + System.out.println(getFileInvalidCaloriesMessage(i, filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[CARBOHYDRATES_INDEX] + "\"")) { + System.out.println(getFileInvalidCarbsMessage(i, filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[PROTEINS_INDEX] + "\"")) { + System.out.println(getFileInvalidProteinsMessage(i, filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[FATS_INDEX] + "\"")) { + System.out.println(getFileInvalidFatsMessage(i, filePath)); + } + } catch (DateTimeParseException e) { + System.out.println(getFileInvalidDateMessage(i, filePath)); + } catch (FileHandlerException e) { + System.out.println(e.getMessage()); + } finally { + i++; + } + } + s.close(); + return entries; + } +} diff --git a/src/main/java/seedu/lifetrack/system/storage/FileHandler.java b/src/main/java/seedu/lifetrack/system/storage/FileHandler.java new file mode 100644 index 0000000000..935a7d2181 --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/storage/FileHandler.java @@ -0,0 +1,70 @@ +//@@author owx0130 +package seedu.lifetrack.system.storage; + +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.system.exceptions.FileHandlerException; + +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileDateLaterThanCurrentMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileEmptyDescriptionMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidEntryIDMessage; + +public class FileHandler { + + //general list constants + protected final int ENTRYID_INDEX = 0; + protected final int DATE_INDEX = 1; + protected final int DESCRIPTION_INDEX = 2; + + //NumberFormatException exception message prefix + protected final String NF_EXCEPTION_PREFIX = "For input string: \""; + + //error message for IO exception + protected final String message = "\t Unable to write to file!"; + + protected String filePath; + + public FileHandler(String filePath) { + this.filePath = filePath; + } + + protected void checkDateNotLaterThanCurrent(int lineNumber, LocalDate date) throws FileHandlerException { + if (date.isAfter(LocalDate.now())) { + throw new FileHandlerException(getFileDateLaterThanCurrentMessage(lineNumber, filePath)); + } + } + + protected void checkNonEmptyDescription(int lineNumber, String description) throws FileHandlerException { + if (description.equals("")) { + throw new FileHandlerException(getFileEmptyDescriptionMessage(lineNumber, filePath)); + } + } + + protected void checkPositiveEntryID(int lineNumber, int entryID) throws FileHandlerException { + if (entryID <= 0) { + throw new FileHandlerException(getFileInvalidEntryIDMessage(lineNumber, filePath)); + } + } + + public void writeToFile(String textToAdd) throws IOException { + FileWriter fw = new FileWriter(filePath); + fw.write(textToAdd); + fw.close(); + } + + public void writeEntries(ArrayList entries) { + try { + String newData = ""; + for (Entry entry : entries) { + newData += entry.toFileFriendlyString() + System.lineSeparator(); + } + writeToFile(newData); + } catch (IOException e) { + System.out.println(message); + } + } +} diff --git a/src/main/java/seedu/lifetrack/system/storage/HydrationFileHandler.java b/src/main/java/seedu/lifetrack/system/storage/HydrationFileHandler.java new file mode 100644 index 0000000000..c09f14b1a7 --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/storage/HydrationFileHandler.java @@ -0,0 +1,96 @@ +//@@author owx0130 +package seedu.lifetrack.system.storage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Scanner; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.hydration.hydrationlist.HydrationEntry; +import seedu.lifetrack.system.exceptions.FileHandlerException; + +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidEntryIDMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidVolumeMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileHydrationTooFewFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileHydrationTooManyFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidDateMessage; + +public class HydrationFileHandler extends FileHandler { + + //class-level member for lastEntryID calories + public static int maxHydrationID = 0; + + //hydration list constants + private final int VOLUME_INDEX = 3; + + public HydrationFileHandler(String filePath) { + super(filePath); + } + + private void calculateMaxHydrationEntry(int entryID) { + if (entryID > maxHydrationID) { + maxHydrationID = entryID; + } + } + + private void checkCorrectNumberOfFields(int lineNumber, int dataLength) throws FileHandlerException { + if (dataLength < 4) { + throw new FileHandlerException(getFileHydrationTooFewFieldsMessage(lineNumber, filePath)); + } else if (dataLength > 4) { + throw new FileHandlerException(getFileHydrationTooManyFieldsMessage(lineNumber, filePath)); + } + } + + private void checkVolumeIsPositive(int lineNumber, int volume) throws FileHandlerException { + if (volume <= 0) { + throw new FileHandlerException(getFileInvalidVolumeMessage(lineNumber, filePath)); + } + } + + private void getSingleHydrationEntry(ArrayList entries, String[] words, int lineNumber) + throws FileHandlerException { + checkCorrectNumberOfFields(lineNumber, words.length); + int entryID = Integer.parseInt(words[ENTRYID_INDEX]); + checkPositiveEntryID(lineNumber, entryID); + calculateMaxHydrationEntry(entryID); + LocalDate date = LocalDate.parse(words[DATE_INDEX]); + checkDateNotLaterThanCurrent(lineNumber, date); + String description = words[DESCRIPTION_INDEX]; + checkNonEmptyDescription(lineNumber, description); + int volume = Integer.parseInt(words[VOLUME_INDEX]); + checkVolumeIsPositive(lineNumber, volume); + entries.add(new HydrationEntry(entryID, description, volume, date)); + } + + public ArrayList getHydrationEntriesFromFile() throws FileNotFoundException { + File f = new File(filePath); + Scanner s = new Scanner(f); + ArrayList entries = new ArrayList<>(); + String line = ""; + int i = 1; + while (s.hasNext()) { + line = s.nextLine(); + String[] words = line.split(";"); + try { + getSingleHydrationEntry(entries, words, i); + } catch (NumberFormatException e) { + if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[ENTRYID_INDEX] + "\"")) { + System.out.println(getFileInvalidEntryIDMessage(i, filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[VOLUME_INDEX] + "\"")) { + System.out.println(getFileInvalidVolumeMessage(i, filePath)); + } + } catch (DateTimeParseException e) { + System.out.println(getFileInvalidDateMessage(i, filePath)); + } catch (FileHandlerException e) { + System.out.println(e.getMessage()); + } finally { + i++; + } + } + s.close(); + return entries; + } +} diff --git a/src/main/java/seedu/lifetrack/system/storage/SleepFileHandler.java b/src/main/java/seedu/lifetrack/system/storage/SleepFileHandler.java new file mode 100644 index 0000000000..320535225a --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/storage/SleepFileHandler.java @@ -0,0 +1,84 @@ +//@@author owx0130 +package seedu.lifetrack.system.storage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Scanner; + +import seedu.lifetrack.Entry; +import seedu.lifetrack.sleep.sleeplist.SleepEntry; +import seedu.lifetrack.system.exceptions.FileHandlerException; + +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidEntryIDMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileSleepTooFewFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileSleepTooManyFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidDateMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidDurationMessage; + +public class SleepFileHandler extends FileHandler { + + //sleep list constants + private final int DURATION_INDEX = 2; + + public SleepFileHandler(String filePath) { + super(filePath); + } + + private void checkCorrectNumberOfFields(int lineNumber, int dataLength) throws FileHandlerException { + if (dataLength < 3) { + throw new FileHandlerException(getFileSleepTooFewFieldsMessage(lineNumber, filePath)); + } else if (dataLength > 3) { + throw new FileHandlerException(getFileSleepTooManyFieldsMessage(lineNumber, filePath)); + } + } + + private void checkDurationIsPositive(int lineNumber, double duration) throws FileHandlerException { + if (duration <= 0) { + throw new FileHandlerException(getFileInvalidDurationMessage(lineNumber, filePath)); + } + } + + private void getSingleSleepEntry(ArrayList entries, String[] words, int lineNumber) + throws FileHandlerException { + checkCorrectNumberOfFields(lineNumber, words.length); + int entryID = Integer.parseInt(words[ENTRYID_INDEX]); + checkPositiveEntryID(lineNumber, entryID); + LocalDate date = LocalDate.parse(words[DATE_INDEX]); + checkDateNotLaterThanCurrent(lineNumber, date); + double duration = Double.parseDouble(words[DURATION_INDEX]); + checkDurationIsPositive(lineNumber, duration); + entries.add(new SleepEntry(entryID, duration, date)); + } + + public ArrayList getSleepEntriesFromFile() throws FileNotFoundException { + File f = new File(filePath); + Scanner s = new Scanner(f); + ArrayList entries = new ArrayList<>(); + String line = ""; + int i = 1; + while (s.hasNext()) { + line = s.nextLine(); + String[] words = line.split(";"); + try { + getSingleSleepEntry(entries, words, i); + } catch (NumberFormatException e) { + if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[ENTRYID_INDEX] + "\"")) { + System.out.println(getFileInvalidEntryIDMessage(i, filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[DURATION_INDEX] + "\"")) { + System.out.println(getFileInvalidDurationMessage(i, filePath)); + } + } catch (DateTimeParseException e) { + System.out.println(getFileInvalidDateMessage(i, filePath)); + } catch (FileHandlerException e) { + System.out.println(e.getMessage()); + } finally { + i++; + } + } + s.close(); + return entries; + } +} diff --git a/src/main/java/seedu/lifetrack/system/storage/UserFileHandler.java b/src/main/java/seedu/lifetrack/system/storage/UserFileHandler.java new file mode 100644 index 0000000000..d38bc54a91 --- /dev/null +++ b/src/main/java/seedu/lifetrack/system/storage/UserFileHandler.java @@ -0,0 +1,160 @@ +//@@author owx0130 +package seedu.lifetrack.system.storage; + +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidAgeMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidExerciseLevelMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidGoalMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidHeightMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidReqCalMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidSexMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileInvalidWeightMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileUserEmptyNameMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileUserTooFewFieldsMessage; +import static seedu.lifetrack.system.exceptions.FileHandlerExceptionMessage.getFileUserTooManyFieldsMessage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +import seedu.lifetrack.system.exceptions.FileHandlerException; +import seedu.lifetrack.user.User; + +public class UserFileHandler extends FileHandler { + + //user data constants + private final int NAME_INDEX = 0; + private final int HEIGHT_INDEX = 1; + private final int WEIGHT_INDEX = 2; + private final int AGE_INDEX = 3; + private final int SEX_INDEX = 4; + private final int EXERCISE_LEVEL_INDEX = 5; + private final int GOAL_INDEX = 6; + private final int REQ_CAL_INDEX = 7; + + public UserFileHandler(String filePath) { + super(filePath); + } + + private void checkCorrectNumberOfFields(int dataLength) throws FileHandlerException { + if (dataLength < 8) { + throw new FileHandlerException(getFileUserTooFewFieldsMessage(filePath)); + } else if (dataLength > 8) { + throw new FileHandlerException(getFileUserTooManyFieldsMessage(filePath)); + } + } + + private void checkNonEmptyName(String name) throws FileHandlerException { + if (name.equals("")) { + throw new FileHandlerException(getFileUserEmptyNameMessage(filePath)); + } + } + + private void checkValidHeight(int height) throws FileHandlerException { + if (!(height >= 90 && height <= 225)) { + throw new FileHandlerException(getFileInvalidHeightMessage(filePath)); + } + } + + private void checkValidWeight(int weight) throws FileHandlerException { + if (!(weight >= 30 && weight <= 200)) { + throw new FileHandlerException(getFileInvalidWeightMessage(filePath)); + } + } + + private void checkValidAge(int age) throws FileHandlerException { + if (!(age >= 13 && age <= 30)) { + throw new FileHandlerException(getFileInvalidAgeMessage(filePath)); + } + } + + private void checkValidSex(String sex) throws FileHandlerException { + if (!(sex.equals("male") || sex.equals("female"))) { + throw new FileHandlerException(getFileInvalidSexMessage(filePath)); + } + } + + private void checkValidExerciseLevel(int exerciseLevel) throws FileHandlerException { + if (!(exerciseLevel >= 1 && exerciseLevel <= 5)) { + throw new FileHandlerException(getFileInvalidExerciseLevelMessage(filePath)); + } + } + + private void checkValidGoal(int goal) throws FileHandlerException { + if (!(goal >= 1 && goal <= 5)) { + throw new FileHandlerException(getFileInvalidGoalMessage(filePath)); + } + } + + private void checkReqCalIsPositive(int requiredCals) throws FileHandlerException { + if (requiredCals <= 0) { + throw new FileHandlerException(getFileInvalidReqCalMessage(filePath)); + } + } + + private void extractUserData(ArrayList data, String[] words) throws FileHandlerException { + checkCorrectNumberOfFields(words.length); + String name = words[NAME_INDEX]; + checkNonEmptyName(name); + int height = Integer.parseInt(words[HEIGHT_INDEX]); + checkValidHeight(height); + int weight = Integer.parseInt(words[WEIGHT_INDEX]); + checkValidWeight(weight); + int age = Integer.parseInt(words[AGE_INDEX]); + checkValidAge(age); + String sex = words[SEX_INDEX]; + checkValidSex(sex); + int exerciseLevel = Integer.parseInt(words[EXERCISE_LEVEL_INDEX]); + checkValidExerciseLevel(exerciseLevel); + int goal = Integer.parseInt(words[GOAL_INDEX]); + checkValidGoal(goal); + int requiredCals = Integer.parseInt(words[REQ_CAL_INDEX]); + checkReqCalIsPositive(requiredCals); + data.add(words[NAME_INDEX]); + data.add(words[HEIGHT_INDEX]); + data.add(words[WEIGHT_INDEX]); + data.add(words[AGE_INDEX]); + data.add(words[SEX_INDEX]); + data.add(words[EXERCISE_LEVEL_INDEX]); + data.add(words[GOAL_INDEX]); + data.add(words[REQ_CAL_INDEX]); + } + + public ArrayList getUserDataFromFile() throws FileNotFoundException { + File f = new File(filePath); + Scanner s = new Scanner(f); + ArrayList data = new ArrayList<>(); + String line = s.nextLine(); + String[] words = line.split(";"); + try { + extractUserData(data, words); + } catch (NumberFormatException e) { + if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[HEIGHT_INDEX] + "\"")) { + System.out.println(getFileInvalidHeightMessage(filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[WEIGHT_INDEX] + "\"")) { + System.out.println(getFileInvalidWeightMessage(filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[AGE_INDEX] + "\"")) { + System.out.println(getFileInvalidAgeMessage(filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[EXERCISE_LEVEL_INDEX] + "\"")) { + System.out.println(getFileInvalidExerciseLevelMessage(filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[GOAL_INDEX] + "\"")) { + System.out.println(getFileInvalidGoalMessage(filePath)); + } else if (e.getMessage().equals(NF_EXCEPTION_PREFIX + words[REQ_CAL_INDEX] + "\"")) { + System.out.println(getFileInvalidReqCalMessage(filePath)); + } + } catch (FileHandlerException e) { + System.out.println(e.getMessage()); + } + s.close(); + return data; + } + + public void writeUserData(User user) { + try { + writeToFile(user.toFileFriendlyString()); + } catch (IOException e) { + System.out.println(message); + } + } +} diff --git a/src/main/java/seedu/lifetrack/ui/CalorieListUi.java b/src/main/java/seedu/lifetrack/ui/CalorieListUi.java new file mode 100644 index 0000000000..dcaed20b8b --- /dev/null +++ b/src/main/java/seedu/lifetrack/ui/CalorieListUi.java @@ -0,0 +1,61 @@ +//@@author rexyyong +package seedu.lifetrack.ui; + +import seedu.lifetrack.Entry; + +public class CalorieListUi { + private static final String CALORIES_DELETE_SAMPLE_INPUT = "\t Example input: calories delete CALORIES_ID"; + + + public static void successfulDeletedMessage(Entry toDelete) { + System.out.println("\t The following calorie record has been successfully deleted!"); + System.out.println("\t " + toDelete.toString()); + } + public static void unsuccessfulDeletedMessage(int entryID) { + System.out.println("\t The following calorie record corresponding to caloriesID " + entryID + " could " + + "not be found\n" + "\t Refer to calories list to see valid caloriesIDs."); + } + + public static void emptyCalorieList() { + System.out.println("\t There is nothing to delete as the calorie list is empty!"); + } + + public static void emptyListMessage() { + System.out.println("\t Your caloric list is empty. Add new entries to populate your list :)"); + } + public static void emptyFoundListMessage() { + System.out.println("\t There were no matches to your search!"); + } + //@@author a-wild-chocolate + public static String deleteLogIndexMessage() { + return "\t Sorry, this index is invalid. Please enter a positive integer within the size of the list."; + } + + public static String deleteLogNumberMessage() { + return "\t Please enter a valid positive integer within the range of all caloriesID " + + "shown in the calories list.\n" + + CALORIES_DELETE_SAMPLE_INPUT; + } + //@@author + public static void calorieListHeader() { + System.out.println("\t Your Caloric List:"); + } + + public static void calorieListFoundHeader() { + System.out.println("\t Caloric List based on your search:"); + } + public static void outputCalorieListHeader() { + System.out.println(""); + System.out.println("\t Your Caloric Outflow List:"); + } + + public static void inputCalorieListHeader() { + System.out.println(""); + System.out.println("\t Your Caloric Inflow List:"); + } + + public static void printNewCalorieEntry(Entry newEntry) { + System.out.println("\t The following entry has been added to your caloric list!"); + System.out.println("\t " + newEntry.toString()); + } +} diff --git a/src/main/java/seedu/lifetrack/ui/HydrationListUI.java b/src/main/java/seedu/lifetrack/ui/HydrationListUI.java new file mode 100644 index 0000000000..8f2708d934 --- /dev/null +++ b/src/main/java/seedu/lifetrack/ui/HydrationListUI.java @@ -0,0 +1,47 @@ +//@@author shawnpong +package seedu.lifetrack.ui; + +import seedu.lifetrack.Entry; + +public class HydrationListUI { + + public static void successfulDeletedMessage(Entry toDelete) { + System.out.println("\t The following hydration record has been successfully deleted!"); + System.out.println("\t " + toDelete.toString()); + } + public static void unsuccessfulDeletedMessage(int entryID) { + System.out.println("\t The following hydration record corresponding to hydrationID " + entryID + " could " + + "not be found"); + } + + public static void emptyListMessage() { + System.out.println("\t Your hydration list is empty. Add new entries to populate your list :)"); + } + public static void emptyFoundListMessage() { + System.out.println("\t There were no matches to your search!"); + } + public static void hydrationListFoundHeader() { + System.out.println("\t Hydration List based on your search:"); + } + + public static String deleteLogIndexMessage() { + return "\t Sorry, this index is invalid. Please enter a positive integer within the size of the list."; + } + + public static String deleteLogNumberMessage() { + return "\t Please enter a valid hydrationID!"; + } + + public static void hydrationListHeader() { + System.out.println("\t Your Hydration List:"); + } + + public static void printNewHydrationEntry(Entry newEntry) { + System.out.println("\t The following entry has been added to your hydration list!"); + System.out.println("\t " + newEntry.toString()); + } + + public static void emptyHydrationList() { + System.out.println("\t There is nothing to delete as the hydration list is empty!"); + } +} diff --git a/src/main/java/seedu/lifetrack/ui/SleepListUi.java b/src/main/java/seedu/lifetrack/ui/SleepListUi.java new file mode 100644 index 0000000000..dd19701b35 --- /dev/null +++ b/src/main/java/seedu/lifetrack/ui/SleepListUi.java @@ -0,0 +1,34 @@ +//@@author a-wild-chocolate +package seedu.lifetrack.ui; + +import seedu.lifetrack.Entry; + +public class SleepListUi { + + public static void successfulDeletedMessage(Entry toDelete) { + System.out.println("\t The following sleep record has been successfully deleted!"); + System.out.println("\t " + toDelete.toString()); + } + + public static void emptyListMessage() { + System.out.println("\t Your sleep list is empty. Add new entries to populate your list :)"); + } + + public static String deleteLogIndexMessage() { + return "\t Sorry, this index is invalid. Please enter a positive integer within the sleep id of the list."; + } + + public static String deleteLogNumberMessage() { + return "\t Please enter a valid index!"; + } + + public static void sleepListHeader() { + System.out.println("\t Your Sleep List:"); + } + + public static void printNewSleepEntry(Entry newEntry) { + System.out.println("\t The following entry has been added to your sleep list!"); + System.out.println("\t " + newEntry.toString()); + } +} +//@@author diff --git a/src/main/java/seedu/lifetrack/ui/Ui.java b/src/main/java/seedu/lifetrack/ui/Ui.java new file mode 100644 index 0000000000..4c9ff44ee0 --- /dev/null +++ b/src/main/java/seedu/lifetrack/ui/Ui.java @@ -0,0 +1,235 @@ +//@@author paturikarthik +package seedu.lifetrack.ui; + +import seedu.lifetrack.calories.calorielist.CalorieList; +import seedu.lifetrack.hydration.hydrationlist.HydrationList; +import seedu.lifetrack.sleep.sleeplist.SleepList; +import seedu.lifetrack.user.User; + +import java.util.Scanner; + +import static seedu.lifetrack.ui.UserUi.printNoUserYetMessage; + +/** + * Reads user input from the console and processes it. + *

+ * This method continuously reads input from the console until the user + * inputs "bye". For each input line, it checks if it's empty and prompts + * the user to enter a non-empty input if it is. If the input line starts + * with "calories in", it attempts to parse the input as calorie intake + * information using the calorieIn method from the CalorieList class. + */ +public class Ui { + + private static final String WHITESPACE = " "; + + private static final String logo = + "\n" + + ".____ .__ _____ ___________ __\n" + + "| | |__|/ ____\\____ \\__ ___/___________ ____ | | __\n" + + "| | | \\ __\\/ __ \\ | | \\_ __ \\__ \\ _/ ___\\| |/ /\n" + + "| |___| || | \\ ___/ | | | | \\// __ \\\\ \\___| <\n" + + "|_______ \\__||__| \\___ > |____| |__| (____ /\\___ >__|_ \\\n" + + " \\/ \\/ \\/ \\/ \\/\n"; + + /** + * Reads in the input from the user + * + * @param calorieList list containing all entries pertinent to calories + * @param hydrationList list containing all entries pertinent to liquids + */ + public static void readUserInput(CalorieList calorieList, HydrationList hydrationList, + User user, SleepList sleepList) { + String line; + do { + line = new Scanner(System.in).nextLine(); + handleUserInput(line, calorieList, hydrationList, user, sleepList); + } while (!line.equalsIgnoreCase("bye")); + } + + /** + * handles input from the user + * + * @param line input from the user + * @param calorieList list containing all entries pertinent to calories + */ + public static void handleCaloriesInput(String line, CalorieList calorieList) { + assert !line.startsWith("bye") : "exit the app"; + if (line.startsWith("calories in ") || line.startsWith("calories out ")) { + calorieList.addEntry(line); + } else if (line.equals("calories list")) { + calorieList.printCalorieList(); + } else if (line.startsWith("calories delete ")) { + calorieList.deleteEntry(line); + } else if (line.startsWith("calories find ")) { + calorieList.findEntries(line); + } else { + handleUnknownInput(line); + } + } + + public static void handleHydrationInput(String line, HydrationList hydrationList) { + assert !line.startsWith("bye") : "exit the app"; + if (line.startsWith("hydration in ")) { + hydrationList.addEntry(line); + } else if (line.equals("hydration list")) { + hydrationList.printHydrationList(); + } else if (line.startsWith("hydration delete ")) { + hydrationList.deleteEntry(line); + } else if (line.startsWith("hydration find ")){ + hydrationList.findEntries(line); + } else { + handleUnknownInput(line); + } + } + //@@author + + //@@author a-wild-chocolate + public static void handleSleepInput(String line, SleepList sleepList) { + assert !line.startsWith("bye") : "exit the app"; + if (line.startsWith("sleep add ")) { + sleepList.addSleep(line); + } else if (line.equals("sleep list")) { + sleepList.printSleepList(); + } else if (line.startsWith("sleep delete ")) { + sleepList.deleteSleep(line); + } else { + handleUnknownInput(line); + } + } + //@@author + + //@@author paturikarthik + public static void handleUserInput(String line, CalorieList calorieList, HydrationList hydrationList, + User user, SleepList sleepList) { + if (!line.trim().equalsIgnoreCase("bye")) { + printLine(); + line = line.trim(); + if (line.isEmpty()) { + printEmptyInputMessage(); + } else if (line.startsWith("calories")) { + handleCaloriesInput(line.toLowerCase(), calorieList); + } else if (line.equalsIgnoreCase("help")) { + showHelp(); + } else if (line.startsWith("hydration")) { + handleHydrationInput(line.toLowerCase(), hydrationList); + } else if (line.startsWith("sleep")) { + handleSleepInput(line.toLowerCase(), sleepList); + } else if (line.startsWith("user")) { + handleUserCommands(line, user); + } else { + handleUnknownInput(line); + } + printLine(); + } + } + + public static void handleUserCommands(String line, User user) { + if (line.startsWith("user setup ")) { + user.setUp(line); + } else if (line.equals("user progress")) { + handleUserProgress(user); + } else if (line.startsWith("user update")) { + if (user.getName() == null) { + printNoUserYetMessage(); + } else { + user.update(line); + } + } else if (line.equals("user details")) { + if (user.getName() == null) { + printNoUserYetMessage(); + } else { + user.getUserDetails(); + } + } else { + handleUnknownInput(line); + } + } + + private static void handleUserProgress(User user) { + if (user.getName() == null) { + printNoUserYetMessage(); + } else { + user.getCaloriesProgressBar(); + user.getHydrationProgressBar(); + user.getSleepProgressBar(); + } + } + + public static void sayHello() { + printLine(); + System.out.println(WHITESPACE + "Hello from\n\n" + logo); + System.out.println(WHITESPACE + "How can I help you today?"); + printLine(); + } + + public static void byeMessage() { + printLine(); + System.out.println(WHITESPACE + "Bye! See you again soon ^^"); + } + + public static void printEmptyInputMessage() { + System.out.println("\t Please enter a non-empty input!"); + } + + public static void printLine() { + System.out.println(WHITESPACE + "-------------------------------------" + + "----------------------------------------"); + } + + /** + * Sorts unhandled inputs into incomplete inputs and unknown inputs + * + * @param line string input from user + */ + public static void handleUnknownInput(String line) { + if (line.startsWith("calories in") || line.startsWith("calories out") || line.startsWith("calories delete") || + line.startsWith("hydration in") || line.startsWith("hydration delete") || line.startsWith("sleep add") + || line.startsWith("sleep delete") || line.startsWith("user setup") + || line.startsWith("calories find") || line.startsWith("hydration find")) { + System.out.println("\t Oops! Incomplete command!"); + } else { + System.out.println("\t Oops! I've never seen this input before..."); + } + System.out.println("\t If you are unsure of the commands, use the help command for a quick recap :)"); + + } + + public static void showHelp() { + System.out.println("\t LifeTrack Command List:"); + System.out.println("\t - help: Displays a list of available commands and their descriptions."); + printLine(); + System.out.println("\t - calories in c/ d/ " + + "m/[carbohydrates, proteins, fats]:\n" + "\t Adds a calorie gaining entry into the calories tracker."); + System.out.println("\t - calories out c/ d/:\n" + + "\t Adds a calorie burning entry into the calories tracker."); + System.out.println("\t - calories list: Displays all entries currently stored in the calorie list."); + System.out.println("\t - calories delete : Deletes the entry at the specified ID" + + " from the calorie list."); + System.out.println("\t - calories find : finds and lists all calorie entries " + + "containing the keyword " + "in their description"); + printLine(); + System.out.println("\t - hydration in v/ d/:\n" + + "\t Adds a hydration entry into the hydration tracker."); + System.out.println("\t - hydration list: Displays all entries currently stored in the hydration list."); + System.out.println("\t - hydration delete : Deletes the hydration entry at the specified ID " + + "from the hydration list."); + System.out.println("\t - hydration find : finds and lists all hydration entries containing " + + "the keyword " + "in their description"); + printLine(); + System.out.println("\t - sleep add d/: " + + "Adds a sleep entry into the sleep tracker."); + System.out.println("\t - sleep list: Displays all entries currently stored in the sleep list."); + System.out.println("\t - sleep delete : Deletes the entry at the specified index " + + "from the sleep list."); + printLine(); + System.out.println("\t - user setup h/ w/ a/ s/ e/ " + + "g/:\n" + "\t Create a new user, or edit an existing one."); + System.out.println("\t - user details: prints the details of the user."); + System.out.println("\t - user update name/height/weight/age/sex/exercise levels/goal : " + + "updates the corresponding field of the user."); + System.out.println("\t - user progress: Display calories and hydration progress towards the daily " + + "requirement."); + } +} +//@@author diff --git a/src/main/java/seedu/lifetrack/ui/UserUi.java b/src/main/java/seedu/lifetrack/ui/UserUi.java new file mode 100644 index 0000000000..1c39092786 --- /dev/null +++ b/src/main/java/seedu/lifetrack/ui/UserUi.java @@ -0,0 +1,119 @@ +package seedu.lifetrack.ui; + +import seedu.lifetrack.user.User; + +public class UserUi { + private static final String CHANGE_MADE_MESSAGE = "\t The following change has been made:\n"; + private static final int INDEX_OF_TODAY = 0; + private static final int INDEX_OF_YESTERDAY = 1; + private static final int INDEX_OF_DAY_BEFORE = 2; + private static final String UNDERLINE = "\t ----------"; + + public static void printUserCaloriesRequired(int caloriesRequired) { + System.out.println("\t You need to consume " + caloriesRequired + " calories per day to hit your goals!"); + } + + public static void printUserCalorieProgress(int caloriesConsumed, int caloriesRequired, String progressBar, + int percentage, int date) { + String dateInString; + if (date == INDEX_OF_TODAY) { + System.out.print("\t Calories:\n"); + System.out.println(UNDERLINE); + dateInString = "today"; + } else if (date == INDEX_OF_YESTERDAY) { + dateInString = "yesterday"; + } else { + dateInString = "on the day before yesterday"; + } + System.out.printf("\t You have consumed " + caloriesConsumed + " calories out of your goal of " + + caloriesRequired + " calories " + dateInString + ".\n"); + System.out.printf("\t %s %d%%\n\n", progressBar, percentage); + } + + public static void printUserHydrationProgress(int hydrationConsumed, int hydrationRequired, String progressBar, + int percentage, int date) { + String dateInString; + if (date == INDEX_OF_TODAY) { + System.out.print("\t Hydration:\n"); + System.out.println(UNDERLINE); + dateInString = "today"; + } else if (date == INDEX_OF_YESTERDAY) { + dateInString = "yesterday"; + } else { + dateInString = "on the day before yesterday"; + } + System.out.printf("\t You have consumed " + hydrationConsumed + "ml out of your goal of " + + hydrationRequired + "ml " + dateInString + ".\n"); + System.out.printf("\t %s %d%%\n\n", progressBar, percentage); + } + + public static void printUserSleepProgress(double sleepConsumed, int sleepRequired, String progressBar, + int percentage, int date) { + String dateInString; + if (date == INDEX_OF_TODAY) { + System.out.print("\t Sleep:\n"); + System.out.println(UNDERLINE); + dateInString = "today"; + } else if (date == INDEX_OF_YESTERDAY) { + dateInString = "yesterday"; + } else { + dateInString = "on the day before yesterday"; + } + System.out.print("\t You have slept for " + sleepConsumed + " hrs out of your goal of " + + sleepRequired + " hrs " + dateInString + ".\n"); + System.out.printf("\t %s %d%%\n\n", progressBar, percentage); + } + + public static void printUserSetUpComplete(User user) { + System.out.println("\t Hello, " + user.getName() + "! Thank you for completing the setup :)"); + printUserCaloriesRequired(user.getCaloriesRequired()); + printUserDetails(user); + } + + public static void printNoUserYetMessage() { + System.out.println("\t Please set up your profile first!"); + } + + public static void printNewUserName(String name) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Name: " + name); + } + + public static void printNewUserAge(int age) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Age: " + age); + } + + public static void printNewUserHeight(int height) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Height: " + height); + } + + public static void printNewUserWeight(int weight) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Weight: " + weight); + } + + public static void printNewUserSex(String sex) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Sex: " + sex); + } + + public static void printNewUserExerciseLevels(User user, int level) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Exercise Levels: " + level + " out of 5 (" + + user.getExerciseLevelAsString() + ")"); + } + + public static void printNewUserGoal(User user, int goal) { + System.out.println(CHANGE_MADE_MESSAGE + "\t Goal: " + goal + " out of 5 (" + + user.getGoalAsString() + ")"); + } + + public static void printUserDetails(User user) { + System.out.println("\t User details:\n" + + "\t Name: " + (user.getName().substring(0, 1).toUpperCase() + user.getName().substring(1)) + + "\n" + + "\t Height: " + user.getHeight() + "\n" + + "\t Weight: " + user.getWeight() + "\n" + + "\t Age: " + user.getAge() + "\n" + + "\t Sex: " + user.getSex() + "\n" + + "\t Exercise Levels: " + user.getExerciseLevels() + " out of 5 (" + + user.getExerciseLevelAsString() + ")" + "\n" + + "\t Goal: " + user.getGoal() + " out of 5 (" + user.getGoalAsString() + ")"); + } +} diff --git a/src/main/java/seedu/lifetrack/user/User.java b/src/main/java/seedu/lifetrack/user/User.java new file mode 100644 index 0000000000..64ca138343 --- /dev/null +++ b/src/main/java/seedu/lifetrack/user/User.java @@ -0,0 +1,205 @@ +//@@author paturikarthik +package seedu.lifetrack.user; + +import seedu.lifetrack.system.exceptions.InvalidInputException; +import seedu.lifetrack.system.storage.UserFileHandler; +import seedu.lifetrack.user.usergoals.UserGoals; + +import java.io.FileNotFoundException; +import java.util.ArrayList; + +import static seedu.lifetrack.system.parser.ParserUser.parseSetUp; +import static seedu.lifetrack.system.parser.ParserUser.parseUpdate; +import static seedu.lifetrack.ui.UserUi.printUserDetails; + +public class User { + + private UserFileHandler fileHandler; + private String name; + private int height; + private int weight; + private int age; + private String sex; + private int exerciseLevels; + private int goal; + + private int caloriesRequired; + private int hydrationRequired = 2000; + private int sleepRequired = 7; + + //user data constants + private final int NAME_INDEX = 0; + private final int HEIGHT_INDEX = 1; + private final int WEIGHT_INDEX = 2; + private final int AGE_INDEX = 3; + private final int SEX_INDEX = 4; + private final int EXERCISE_INDEX = 5; + private final int GOAL_INDEX = 6; + private final int REQ_CAL_INDEX = 7; + + //constructor for JUnit tests + public User() { + + } + + //constructor for usage in terminal + public User(String filePath) { + try { + fileHandler = new UserFileHandler(filePath); + ArrayList data = fileHandler.getUserDataFromFile(); + if (data.size() == 8) { + name = data.get(NAME_INDEX); + height = Integer.parseInt(data.get(HEIGHT_INDEX)); + weight = Integer.parseInt(data.get(WEIGHT_INDEX)); + age = Integer.parseInt(data.get(AGE_INDEX)); + sex = data.get(SEX_INDEX); + exerciseLevels = Integer.parseInt(data.get(EXERCISE_INDEX)); + goal = Integer.parseInt(data.get(GOAL_INDEX)); + caloriesRequired = Integer.parseInt(data.get(REQ_CAL_INDEX)); + } + } catch (FileNotFoundException e) { + return; + } + } + + public void setUp(String line) { + try { + parseSetUp(line, this); + fileHandler.writeUserData(this); + } catch (InvalidInputException e) { + System.out.println(e.getMessage()); + } + } + + public void update(String line) { + try { + parseUpdate(line, this); + fileHandler.writeUserData(this); + } catch (InvalidInputException e) { + System.out.println(e.getMessage()); + } + } + + public void setName(String name) { + this.name = name; + } + + public void setHeight(int height) { + this.height = height; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public void setAge(int age) { + this.age = age; + } + + public void setSex(String sex) { + this.sex = sex; + } + + public void setExerciseLevels(int exerciseLevels) { + this.exerciseLevels = exerciseLevels; + } + + public void setGoal(int goal) { + this.goal = goal; + } + + public String getName() { + return name; + } + + public int getHeight() { + return height; + } + + public int getWeight() { + return weight; + } + + public int getAge() { + return age; + } + + public String getSex() { + return sex; + } + + public int getExerciseLevels() { + return exerciseLevels; + } + + public int getGoal() { + return goal; + } + + public void getHealthInfo() { + UserGoals.getHealthInfo(this); + } + + public void setCaloriesRequired(int caloriesRequired) { + this.caloriesRequired = caloriesRequired; + } + + public int getCaloriesRequired() { + return caloriesRequired; + } + + public int getHydrationRequired() { + return hydrationRequired; + } + public int getSleepRequired() { + return sleepRequired; + } + + public String toFileFriendlyString() { + return (name + ";" + height + ";" + weight + ";" + age + ";" + sex + ";" + + exerciseLevels + ";" + goal + ";" + caloriesRequired); + } + + public void getCaloriesProgressBar() { + UserGoals.getCaloriesProgressBar(this); + } + + public void getHydrationProgressBar() { + UserGoals.getHydrationProgressBar(this); + } + public void getSleepProgressBar() { + UserGoals.getSleepProgressBar(this); + } + + public String getExerciseLevelAsString() { + if (exerciseLevels == 1) { + return "Sedentary"; + } else if (exerciseLevels == 2) { + return "Lightly Active"; + } else if (exerciseLevels == 5) { + return "Extremely Active"; + } else if (exerciseLevels == 4) { + return "Very Active"; + } else { + return "Moderately Active"; + } + } + + public String getGoalAsString() { + if (goal == 1) { + return "Quick Weight Loss"; + } else if (goal == 2) { + return "Moderate Weight Loss"; + } else if (goal == 5) { + return "Quick Weight Gain"; + } else if (goal == 4) { + return "Moderate Weight Gain"; + } else { + return "Maintain Weight"; + } + } + + public void getUserDetails(){ + printUserDetails(this); + } +} diff --git a/src/main/java/seedu/lifetrack/user/usergoals/UserGoals.java b/src/main/java/seedu/lifetrack/user/usergoals/UserGoals.java new file mode 100644 index 0000000000..3c90ffc0b2 --- /dev/null +++ b/src/main/java/seedu/lifetrack/user/usergoals/UserGoals.java @@ -0,0 +1,219 @@ +//@@author paturikarthik +package seedu.lifetrack.user.usergoals; + +import seedu.lifetrack.user.User; + +import java.time.LocalDate; + +import static seedu.lifetrack.LifeTrack.calorieList; +import static seedu.lifetrack.LifeTrack.hydrationList; +import static seedu.lifetrack.LifeTrack.sleepList; +import static seedu.lifetrack.ui.UserUi.printUserCalorieProgress; +import static seedu.lifetrack.ui.UserUi.printUserHydrationProgress; +import static seedu.lifetrack.ui.UserUi.printUserSleepProgress; + +public class UserGoals { + private static final int BMR_WEIGHT_MULTIPLIER = 10; + private static final double BMR_HEIGHT_MULTIPLIER = 6.25; + private static final int BMR_AGE_MULTIPLIER = 5; + private static final int BMR_MALE_MODIFIER = 5; + private static final int BMR_FEMALE_MODIFIER = -161; + private static final int PROGRESS_BAR_WIDTH = 50; + private static final int INDEX_OF_TODAY = 0; + private static final int INDEX_OF_YESTERDAY = 1; + private static final int INDEX_OF_DAY_BEFORE = 2; + private static final int NUMBER_OF_DAYS_TO_TRACK = 3; + private static final int NUMBER_OF_DAYS_TO_YESTERDAY = -1; + private static final int NUMBER_OF_DAYS_TO_DAY_BEFORE = -2; + + public static void getHealthInfo(User user) { + double rawBMR = BMR_WEIGHT_MULTIPLIER * user.getWeight() + BMR_HEIGHT_MULTIPLIER * user.getHeight() + - BMR_AGE_MULTIPLIER * user.getAge(); + String gender = user.getSex(); + int genderBMRModifier = gender.equals("male") ? BMR_MALE_MODIFIER : BMR_FEMALE_MODIFIER; + int exerciseLevel = user.getExerciseLevels(); + double rawAMR = getAMR(rawBMR + genderBMRModifier, exerciseLevel); + int goal = user.getGoal(); + int caloriesRequired = adjustAMRWithGoal(rawAMR, goal); + user.setCaloriesRequired(caloriesRequired); + } + + private static int adjustAMRWithGoal(double rawAMR, int goal) { + if (goal == 1) { + rawAMR *= 0.8; + } else if (goal == 2) { + rawAMR *= 0.9; + } else if (goal == 4) { + rawAMR *= 1.1; + } else if (goal == 5) { + rawAMR *= 1.2; + } + return (int) rawAMR; + } + + private static double getAMR(double calories, int exerciseLevel) { + if (exerciseLevel == 1) { + calories *= 1.2; + } else if (exerciseLevel == 2) { + calories *= 1.375; + } else if (exerciseLevel == 3) { + calories *= 1.55; + } else if (exerciseLevel == 4) { + calories *= 1.725; + } else { + calories *= 1.9; + } + return calories; + } + + public static void getCaloriesProgressBar(User user) { + int caloriesRequired = user.getCaloriesRequired(); + int caloriesConsumedToday = calorieList.getCaloriesConsumed(LocalDate.now()); + if (caloriesConsumedToday < 0) { + caloriesConsumedToday = 0; + } + double progressToday = (double) caloriesConsumedToday / caloriesRequired; + int caloriesConsumedYesterday = calorieList.getCaloriesConsumed + (LocalDate.now().plusDays(NUMBER_OF_DAYS_TO_YESTERDAY)); + if (caloriesConsumedYesterday < 0) { + caloriesConsumedYesterday = 0; + } + double progressYesterday = (double) caloriesConsumedYesterday / caloriesRequired; + int width = PROGRESS_BAR_WIDTH; + + int caloriesConsumedDayBefore = calorieList.getCaloriesConsumed + (LocalDate.now().plusDays(NUMBER_OF_DAYS_TO_DAY_BEFORE)); + if (caloriesConsumedDayBefore < 0) { + caloriesConsumedDayBefore = 0; + } + double progressDayBefore = (double) caloriesConsumedDayBefore / caloriesRequired; + + double[] progress = new double[NUMBER_OF_DAYS_TO_TRACK]; + progress[INDEX_OF_TODAY] = progressToday; + progress[INDEX_OF_YESTERDAY] = progressYesterday; + progress[INDEX_OF_DAY_BEFORE] = progressDayBefore; + + int[] caloriesConsumed = new int[NUMBER_OF_DAYS_TO_TRACK]; + caloriesConsumed[INDEX_OF_TODAY] = caloriesConsumedToday; + caloriesConsumed[INDEX_OF_YESTERDAY] = caloriesConsumedYesterday; + caloriesConsumed[INDEX_OF_DAY_BEFORE] = caloriesConsumedDayBefore; + + for (int date = 0; date < NUMBER_OF_DAYS_TO_TRACK; date++) { + int progressWidth = (int) (width * progress[date]); + StringBuilder progressBar = new StringBuilder("["); + for (int i = 0; i < width; i++) { + if (i < progressWidth) { + progressBar.append("="); + } else { + progressBar.append(" "); + } + } + progressBar.append("] "); + + int percentage = (int) (progress[date] * 100); + printUserCalorieProgress(caloriesConsumed[date], caloriesRequired, progressBar.toString(), + percentage, date); + } + } + + public static void getHydrationProgressBar(User user) { + int hydrationRequired = user.getHydrationRequired(); + int hydrationConsumedToday = hydrationList.getHydrationConsumed(LocalDate.now()); + if (hydrationConsumedToday < 0) { + hydrationConsumedToday = 0; + } + double progressToday = (double) hydrationConsumedToday / hydrationRequired; + int hydrationConsumedYesterday = hydrationList.getHydrationConsumed + (LocalDate.now().plusDays(NUMBER_OF_DAYS_TO_YESTERDAY)); + if (hydrationConsumedYesterday < 0) { + hydrationConsumedYesterday = 0; + } + double progressYesterday = (double) hydrationConsumedYesterday / hydrationRequired; + int width = PROGRESS_BAR_WIDTH; + + int hydrationConsumedDayBefore = hydrationList.getHydrationConsumed + (LocalDate.now().plusDays(NUMBER_OF_DAYS_TO_DAY_BEFORE)); + if (hydrationConsumedDayBefore < 0) { + hydrationConsumedDayBefore = 0; + } + double progressDayBefore = (double) hydrationConsumedDayBefore / hydrationRequired; + + double[] progress = new double[NUMBER_OF_DAYS_TO_TRACK]; + progress[INDEX_OF_TODAY] = progressToday; + progress[INDEX_OF_YESTERDAY] = progressYesterday; + progress[INDEX_OF_DAY_BEFORE] = progressDayBefore; + + int[] hydrationConsumed = new int[NUMBER_OF_DAYS_TO_TRACK]; + hydrationConsumed[INDEX_OF_TODAY] = hydrationConsumedToday; + hydrationConsumed[INDEX_OF_YESTERDAY] = hydrationConsumedYesterday; + hydrationConsumed[INDEX_OF_DAY_BEFORE] = hydrationConsumedDayBefore; + + for (int date = 0; date < NUMBER_OF_DAYS_TO_TRACK; date++) { + + int progressWidth = (int) (width * progress[date]); + StringBuilder progressBar = new StringBuilder("["); + for (int i = 0; i < width; i++) { + if (i < progressWidth) { + progressBar.append("="); + } else { + progressBar.append(" "); + } + } + progressBar.append("] "); + + int percentage = (int) (progress[date] * 100); + printUserHydrationProgress(hydrationConsumed[date], hydrationRequired, progressBar.toString(), + percentage, date); + } + } + + public static void getSleepProgressBar(User user) { + int sleepRequired = user.getSleepRequired(); + double sleepConsumedToday = sleepList.getSleepConsumed(LocalDate.now()); + if (sleepConsumedToday < 0) { + sleepConsumedToday = 0; + } + double progressToday = sleepConsumedToday / sleepRequired; + double sleepConsumedYesterday = sleepList.getSleepConsumed + (LocalDate.now().plusDays(NUMBER_OF_DAYS_TO_YESTERDAY)); + if (sleepConsumedYesterday < 0) { + sleepConsumedYesterday = 0; + } + double progressYesterday = sleepConsumedYesterday / sleepRequired; + int width = PROGRESS_BAR_WIDTH; + + double sleepConsumedDayBefore = sleepList.getSleepConsumed + (LocalDate.now().plusDays(NUMBER_OF_DAYS_TO_DAY_BEFORE)); + if (sleepConsumedDayBefore < 0) { + sleepConsumedDayBefore = 0; + } + double progressDayBefore = sleepConsumedDayBefore / sleepRequired; + + double[] progress = new double[NUMBER_OF_DAYS_TO_TRACK]; + progress[INDEX_OF_TODAY] = progressToday; + progress[INDEX_OF_YESTERDAY] = progressYesterday; + progress[INDEX_OF_DAY_BEFORE] = progressDayBefore; + + double[] sleepConsumed = new double[NUMBER_OF_DAYS_TO_TRACK]; + sleepConsumed[INDEX_OF_TODAY] = sleepConsumedToday; + sleepConsumed[INDEX_OF_YESTERDAY] = sleepConsumedYesterday; + sleepConsumed[INDEX_OF_DAY_BEFORE] = sleepConsumedDayBefore; + + for (int date = 0; date < NUMBER_OF_DAYS_TO_TRACK; date++) { + int progressWidth = (int) (width * progress[date]); + StringBuilder progressBar = new StringBuilder("["); + for (int i = 0; i < width; i++) { + if (i < progressWidth) { + progressBar.append("="); + } else { + progressBar.append(" "); + } + } + progressBar.append("] "); + + int percentage = (int) (progress[date] * 100); + printUserSleepProgress(sleepConsumed[date], sleepRequired, progressBar.toString(), + percentage, date); + } + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} diff --git a/src/test/java/seedu/lifetrack/CalorieListTest.java b/src/test/java/seedu/lifetrack/CalorieListTest.java new file mode 100644 index 0000000000..4597efed95 --- /dev/null +++ b/src/test/java/seedu/lifetrack/CalorieListTest.java @@ -0,0 +1,343 @@ +package seedu.lifetrack; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; + +import seedu.lifetrack.calories.calorielist.CalorieList; +import seedu.lifetrack.calories.calorielist.InputEntry; +import seedu.lifetrack.calories.calorielist.OutputEntry; + +public class CalorieListTest { + + private final String ADDED_ENTRY_HEADER = "\t The following entry has been added to your caloric list!"; + private final String DELETE_ENTRY_HEADER = "\t The following calorie record has been successfully deleted!"; + + private final String DELETE_ENTRY_INVALID_INPUT = "\t Please enter a valid positive integer " + + "within the range of all caloriesID shown in the calories list.\n" + + "\t Example input: calories delete CALORIES_ID"; + + @Test + public void addEntry_validInput_entryAdded() { + // Test setup + CalorieList calorieList = new CalorieList(); + String validInputCalorieIn = "calories in Eat burger c/369 d/2024-03-14"; + String validInputCalorieOut = "calories out run c/679 d/2024-03-15"; + + // Call method to test + calorieList.addEntry(validInputCalorieIn); + calorieList.addEntry(validInputCalorieOut); + + // Verify that the entry has been added to the list + assertEquals(2, calorieList.getSize()); + InputEntry firstEntry = (InputEntry)calorieList.getEntry(0); + OutputEntry secondEntry = (OutputEntry)calorieList.getEntry(1); + + // Check calories intake entry + LocalDate dateIntake = LocalDate.parse("2024-03-14"); + assertTrue(firstEntry.getDate().isEqual(dateIntake)); + assertEquals("Eat burger", firstEntry.getDescription()); + assertEquals(369, firstEntry.getCalories()); + + // Check calories outflow entry + LocalDate dateOutflow = LocalDate.parse("2024-03-15"); + assertTrue(secondEntry.getDate().isEqual(dateOutflow)); + assertEquals("run", secondEntry.getDescription()); + assertEquals(679, secondEntry.getCalories()); + } + + //@@author a-wild-chocolate + @Test + public void testDeleteCalorieValidIndex() { + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories out Run c/200 d/2024-03-14"); + int initialSize = calorieList.getSize(); + calorieList.deleteEntry("calories delete 1"); + assertEquals(initialSize - 1, calorieList.getSize()); + calorieList.addEntry("calories out Run c/200 d/2024-03-14"); + calorieList.addEntry("calories in Eat c/200 d/2024-03-14"); + initialSize = calorieList.getSize(); + calorieList.deleteEntry("calories delete 2"); + assertEquals(initialSize - 1, calorieList.getSize()); + } + + @Test + public void testDeleteCalorieInvalidIndex() { + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories out Run c/200 date/2024-03-14"); + int initialSize = calorieList.getSize(); + calorieList.deleteEntry("calories delete 2"); // Index out of bounds + calorieList.deleteEntry("calories delete -1"); + assertEquals(initialSize, calorieList.getSize()); + } + + //@@author shawnpong + @Test + public void testPrintCalorieListEmpty() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + CalorieList calorieList = new CalorieList(); + calorieList.printCalorieList(); + System.setOut(System.out); + String expectedOutput = "\t Your caloric list is empty. " + + "Add new entries to populate your list :)" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + } + + @Test + public void testPrintCalorieListNonEmpty() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories in burger king c/200 d/2024-03-14"); + calorieList.printCalorieList(); + System.setOut(System.out); + String expectedOutput = ADDED_ENTRY_HEADER + lineSeparator + + "\t " + calorieList.getEntry(0).toString() + lineSeparator + + "\t Your Caloric List:" + lineSeparator + lineSeparator + + "\t Your Caloric Inflow List:" + lineSeparator + + "\t 1. \t caloriesID: 1, Date: 2024-03-14, Description: burger king, Calories: 200" + lineSeparator + + lineSeparator + "\t Your Caloric Outflow List:" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + } + + @Test + public void testPrintCalorieListMultipleEntries() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories in burger king c/200 d/2024-03-14"); + calorieList.addEntry("calories out Walk c/150 d/2024-03-14"); + calorieList.addEntry("calories in acai c/500 d/2024-03-14"); + calorieList.addEntry("calories out Run c/250 d/2024-03-14"); + calorieList.addEntry("calories in commhall dinner c/300 d/2024-03-14"); + calorieList.printCalorieList(); + System.setOut(System.out); + StringBuilder expectedOutput = new StringBuilder(); + for (int i = 0; i < 5; i++) { + expectedOutput.append(ADDED_ENTRY_HEADER) + .append(lineSeparator).append("\t ").append(calorieList.getEntry(i).toString()) + .append(lineSeparator); + } + expectedOutput.append("\t Your Caloric List:") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Inflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 1, Date: 2024-03-14, Description: burger king, Calories: 200") + .append(lineSeparator) + .append("\t 2. \t caloriesID: 3, Date: 2024-03-14, Description: acai, Calories: 500") + .append(lineSeparator) + .append("\t 3. \t caloriesID: 5, Date: 2024-03-14, Description: commhall dinner, Calories: 300") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Outflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 2, Date: 2024-03-14, Description: Walk, Calories: 150") + .append(lineSeparator) + .append("\t 2. \t caloriesID: 4, Date: 2024-03-14, Description: Run, Calories: 250") + .append(lineSeparator); + assertEquals(expectedOutput.toString(), outputStream.toString()); + assertEquals(5, calorieList.getSize()); + } + + @Test + public void testAddEntry_addDifferentDates_datesSortedCorrectly() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories in burger king c/200 d/2024-03-14"); + calorieList.addEntry("calories out Walk c/150 d/2022-02-22"); + calorieList.addEntry("calories in acai c/500 d/2022-02-22"); + calorieList.addEntry("calories out Run c/250 d/2024-03-14"); + calorieList.addEntry("calories in commhall dinner c/300 d/2021-01-11"); + calorieList.addEntry("calories out play pool c/69 d/2021-01-11"); + calorieList.printCalorieList(); + + System.setOut(System.out); + StringBuilder expectedOutput = new StringBuilder(); + + // expected output string for adding entries + for (int i = 1; i < 7; i++) { + expectedOutput.append(ADDED_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(i)).toString()) + .append(lineSeparator); + } + + // expected output string for printing calorie list + expectedOutput.append("\t Your Caloric List:") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Inflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 5, Date: 2021-01-11, Description: commhall dinner, Calories: 300") + .append(lineSeparator) + .append("\t 2. \t caloriesID: 3, Date: 2022-02-22, Description: acai, Calories: 500") + .append(lineSeparator) + .append("\t 3. \t caloriesID: 1, Date: 2024-03-14, Description: burger king, Calories: 200") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Outflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 6, Date: 2021-01-11, Description: play pool, Calories: 69") + .append(lineSeparator) + .append("\t 2. \t caloriesID: 2, Date: 2022-02-22, Description: Walk, Calories: 150") + .append(lineSeparator) + .append("\t 3. \t caloriesID: 4, Date: 2024-03-14, Description: Run, Calories: 250") + .append(lineSeparator); + assertEquals(expectedOutput.toString(), outputStream.toString()); + assertEquals(6, calorieList.getSize()); + } + + @Test + public void testDeleteEntry_deleteUsingEntryID_correctlyDeletesBasedOnEntryID() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories in burger king c/200 d/2022-02-22"); + calorieList.addEntry("calories out Walk c/150 d/2022-02-22"); + calorieList.addEntry("calories in acai c/500 d/2022-02-22"); + + System.setOut(System.out); + StringBuilder expectedOutput = new StringBuilder(); + // expected output string for adding entries + for (int i = 1; i < 4; i++) { + expectedOutput.append(ADDED_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(i)).toString()) + .append(lineSeparator); + } + // expected output string for deleting entry + expectedOutput.append(DELETE_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(3)).toString()) + .append(lineSeparator); + + calorieList.deleteEntry("calories delete 3"); + calorieList.printCalorieList(); + + // expected output string for printing calories list + expectedOutput.append("\t Your Caloric List:") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Inflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 1, Date: 2022-02-22, Description: burger king, Calories: 200") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Outflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 2, Date: 2022-02-22, Description: Walk, Calories: 150") + .append(lineSeparator); + assertEquals(expectedOutput.toString(), outputStream.toString()); + assertEquals(2, calorieList.getSize()); + } + + @Test + public void testAddEntry_addAndDeleteEntries_entryIDIncrementsProperly() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories in burger king c/200 d/2022-02-22"); + calorieList.addEntry("calories out Walk c/150 d/2022-02-22"); + calorieList.addEntry("calories in acai c/500 d/2022-02-22"); + + System.setOut(System.out); + StringBuilder expectedOutput = new StringBuilder(); + // expected output string for first 3 added entries + for (int i = 1; i < 4; i++) { + expectedOutput.append(ADDED_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(i)).toString()) + .append(lineSeparator); + } + + // expected output string for first deleted entry + expectedOutput.append(DELETE_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(3)).toString()) + .append(lineSeparator); + + calorieList.deleteEntry("calories delete 3"); + calorieList.addEntry("calories in yong tau foo c/688 d/2022-02-22 m/10,10,10"); + + // expected output string for fourth added entry + expectedOutput.append(ADDED_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(4)).toString()) + .append(lineSeparator); + + calorieList.printCalorieList(); + + // expected output string for printing calorie list + expectedOutput.append("\t Your Caloric List:") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Inflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 1, Date: 2022-02-22, Description: burger king, Calories: 200") + .append(lineSeparator) + .append("\t 2. \t caloriesID: 4, Date: 2022-02-22, Description: yong tau foo, Calories: 688 " + + "(C: 10, P: 10, F: 10)") + .append(lineSeparator) + .append(lineSeparator) + .append("\t Your Caloric Outflow List:") + .append(lineSeparator) + .append("\t 1. \t caloriesID: 2, Date: 2022-02-22, Description: Walk, Calories: 150") + .append(lineSeparator); + assertEquals(expectedOutput.toString(), outputStream.toString()); + assertEquals(3, calorieList.getSize()); + } + + + @Test + public void testDeleteEntry_invalidInputs_exceptionThrown() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + + CalorieList calorieList = new CalorieList(); + calorieList.addEntry("calories in burger king c/200 d/2022-02-22"); + calorieList.addEntry("calories out Walk c/150 d/2022-02-22"); + calorieList.addEntry("calories in acai c/500 d/2022-02-22"); + + System.setOut(System.out); + StringBuilder expectedOutput = new StringBuilder(); + // expected output string for adding entries + for (int i = 1; i < 4; i++) { + expectedOutput.append(ADDED_ENTRY_HEADER) + .append(lineSeparator).append("\t ") + .append(calorieList.getEntry(calorieList.getIndexFromEntryID(i)).toString()) + .append(lineSeparator); + } + + // Invalid Inputs for calories delete + calorieList.deleteEntry("calories delete "); + calorieList.deleteEntry("calories delete"); + calorieList.deleteEntry("calories delete a b"); + calorieList.deleteEntry("calories delete 1 2"); + calorieList.deleteEntry("calories delete $12"); + + // expected output string for invalid entry inputs + for (int i = 0; i < 5; i++) { + expectedOutput.append(DELETE_ENTRY_INVALID_INPUT).append(lineSeparator); + } + + assertEquals(expectedOutput.toString(), outputStream.toString()); + assertEquals(3, calorieList.getSize()); + } +} diff --git a/src/test/java/seedu/lifetrack/CaloriesFileHandlerTest.java b/src/test/java/seedu/lifetrack/CaloriesFileHandlerTest.java new file mode 100644 index 0000000000..7d279dada3 --- /dev/null +++ b/src/test/java/seedu/lifetrack/CaloriesFileHandlerTest.java @@ -0,0 +1,104 @@ +package seedu.lifetrack; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import seedu.lifetrack.calories.Food; +import seedu.lifetrack.calories.calorielist.InputEntry; +import seedu.lifetrack.calories.calorielist.OutputEntry; +import seedu.lifetrack.system.storage.CaloriesFileHandler; + +public class CaloriesFileHandlerTest { + + private String filePath = "sample-data/caloriesTestData.txt"; + private CaloriesFileHandler fileHandler = new CaloriesFileHandler(filePath); + private ArrayList expectedEntries = new ArrayList<>(); + + @Test + public void getCalorieEntriesFromFile_correctCaloriesInInput_allEntriesRetrievedFromFile() { + try { + InputEntry expectedEntry_1 = new InputEntry(1, "burger", 200, LocalDate.parse("2024-02-02")); + InputEntry expectedEntry_2 = new InputEntry(2, "rice", 190, LocalDate.parse("2024-02-01")); + InputEntry expectedEntry_3 = new InputEntry(3, "noodle", 180, LocalDate.parse("2024-02-03")); + expectedEntries.add(expectedEntry_1); + expectedEntries.add(expectedEntry_2); + expectedEntries.add(expectedEntry_3); + fileHandler.writeEntries(expectedEntries); + ArrayList actualEntries = fileHandler.getCalorieEntriesFromFile(); + for (int i = 0; i < expectedEntries.size(); i++) { + InputEntry expectedEntryOut = (InputEntry)expectedEntries.get(i); + InputEntry actualEntryOut = (InputEntry)actualEntries.get(i); + assertEquals(expectedEntryOut.getEntryID(), actualEntryOut.getEntryID()); + assertEquals(expectedEntryOut.getDescription(), actualEntryOut.getDescription()); + assertEquals(expectedEntryOut.getDate(), actualEntryOut.getDate()); + assertEquals(expectedEntryOut.getCalories(), actualEntryOut.getCalories()); + } + expectedEntries.clear(); + } catch (FileNotFoundException e) { + return; + } + } + + @Test + public void getCalorieEntriesFromFile_correctCaloriesInInputWithMacros_allEntriesRetrievedFromFile() { + try { + Food food_1 = new Food(100, 200, 300); + InputEntry expectedEntry_1 = new InputEntry(1, "burger", 200, LocalDate.parse("2024-02-02"), food_1); + Food food_2 = new Food(200, 400, 600); + InputEntry expectedEntry_2 = new InputEntry(2, "rice", 190, LocalDate.parse("2024-02-01"), food_2); + Food food_3 = new Food(300, 600, 900); + InputEntry expectedEntry_3 = new InputEntry(3, "noodle", 180, LocalDate.parse("2024-02-03"), food_3); + expectedEntries.add(expectedEntry_1); + expectedEntries.add(expectedEntry_2); + expectedEntries.add(expectedEntry_3); + fileHandler.writeEntries(expectedEntries); + ArrayList actualEntries = fileHandler.getCalorieEntriesFromFile(); + for (int i = 0; i < expectedEntries.size(); i++) { + InputEntry expectedEntryOut = (InputEntry)expectedEntries.get(i); + InputEntry actualEntryOut = (InputEntry)actualEntries.get(i); + Food expectedFoodOut = expectedEntryOut.getFood(); + Food actualFoodOut = actualEntryOut.getFood(); + assertEquals(expectedEntryOut.getEntryID(), actualEntryOut.getEntryID()); + assertEquals(expectedEntryOut.getDescription(), actualEntryOut.getDescription()); + assertEquals(expectedEntryOut.getDate(), actualEntryOut.getDate()); + assertEquals(expectedEntryOut.getCalories(), actualEntryOut.getCalories()); + assertEquals(expectedFoodOut.getCarbohydrates(), actualFoodOut.getCarbohydrates()); + assertEquals(expectedFoodOut.getProteins(), actualFoodOut.getProteins()); + assertEquals(expectedFoodOut.getFats(), actualFoodOut.getFats()); + } + expectedEntries.clear(); + } catch (FileNotFoundException e) { + return; + } + } + + @Test + public void getCalorieEntriesFromFile_correctCaloriesOutInput_allEntriesRetrievedFromFile() { + try { + OutputEntry expectedEntry_1 = new OutputEntry(1, "run", 200, LocalDate.parse("2024-02-02")); + OutputEntry expectedEntry_2 = new OutputEntry(2, "swim", 190, LocalDate.parse("2024-02-01")); + OutputEntry expectedEntry_3 = new OutputEntry(3, "football", 180, LocalDate.parse("2024-02-03")); + expectedEntries.add(expectedEntry_1); + expectedEntries.add(expectedEntry_2); + expectedEntries.add(expectedEntry_3); + fileHandler.writeEntries(expectedEntries); + ArrayList actualEntries = fileHandler.getCalorieEntriesFromFile(); + for (int i = 0; i < expectedEntries.size(); i++) { + OutputEntry expectedEntryOut = (OutputEntry)expectedEntries.get(i); + OutputEntry actualEntryOut = (OutputEntry)actualEntries.get(i); + assertEquals(expectedEntryOut.getEntryID(), actualEntryOut.getEntryID()); + assertEquals(expectedEntryOut.getDescription(), actualEntryOut.getDescription()); + assertEquals(expectedEntryOut.getDate(), actualEntryOut.getDate()); + assertEquals(expectedEntryOut.getCalories(), actualEntryOut.getCalories()); + } + expectedEntries.clear(); + } catch (FileNotFoundException e) { + return; + } + } +} diff --git a/src/test/java/seedu/lifetrack/HydrationFileHandlerTest.java b/src/test/java/seedu/lifetrack/HydrationFileHandlerTest.java new file mode 100644 index 0000000000..ca116b873d --- /dev/null +++ b/src/test/java/seedu/lifetrack/HydrationFileHandlerTest.java @@ -0,0 +1,44 @@ +package seedu.lifetrack; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import seedu.lifetrack.hydration.hydrationlist.HydrationEntry; +import seedu.lifetrack.system.storage.HydrationFileHandler; + +public class HydrationFileHandlerTest { + + private String filePath = "sample-data/hydrationTestData.txt"; + private HydrationFileHandler fileHandler = new HydrationFileHandler(filePath); + private ArrayList expectedEntries = new ArrayList<>(); + + @Test + public void getHydrationEntriesFromFile_correctHydrationInInput_allEntriesRetrievedFromFile() { + try { + HydrationEntry expectedEntry_1 = new HydrationEntry(1, "milo", 200, LocalDate.parse("2024-02-02")); + HydrationEntry expectedEntry_2 = new HydrationEntry(2, "coffee", 190, LocalDate.parse("2024-02-01")); + HydrationEntry expectedEntry_3 = new HydrationEntry(3, "tea", 180, LocalDate.parse("2024-02-03")); + expectedEntries.add(expectedEntry_1); + expectedEntries.add(expectedEntry_2); + expectedEntries.add(expectedEntry_3); + fileHandler.writeEntries(expectedEntries); + ArrayList actualEntries = fileHandler.getHydrationEntriesFromFile(); + for (int i = 0; i < expectedEntries.size(); i++) { + HydrationEntry expectedEntryOut = (HydrationEntry)expectedEntries.get(i); + HydrationEntry actualEntryOut = (HydrationEntry)actualEntries.get(i); + assertEquals(expectedEntryOut.getEntryID(), actualEntryOut.getEntryID()); + assertEquals(expectedEntryOut.getDescription(), actualEntryOut.getDescription()); + assertEquals(expectedEntryOut.getDate(), actualEntryOut.getDate()); + assertEquals(expectedEntryOut.getVolume(), actualEntryOut.getVolume()); + } + expectedEntries.clear(); + } catch (FileNotFoundException e) { + return; + } + } +} diff --git a/src/test/java/seedu/lifetrack/HydrationListTest.java b/src/test/java/seedu/lifetrack/HydrationListTest.java new file mode 100644 index 0000000000..6661d77adf --- /dev/null +++ b/src/test/java/seedu/lifetrack/HydrationListTest.java @@ -0,0 +1,123 @@ +//@@author shawnpong +package seedu.lifetrack; + +import org.junit.jupiter.api.Test; + +import seedu.lifetrack.hydration.hydrationlist.HydrationList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +public class HydrationListTest { + + @Test + public void testDeleteHydrationValidIndex() { + HydrationList hydrationList = new HydrationList(); + hydrationList.addEntry("hydration in Milo v/200 d/2024-02-22"); + int initialSize = hydrationList.getSize(); + hydrationList.deleteEntry("hydration delete 1"); + assertEquals(initialSize - 1, hydrationList.getSize()); + } + + @Test + public void testDeleteHydrationInvalidIndex() { + HydrationList hydrationList = new HydrationList(); + hydrationList.addEntry("hydration in Milo v/200 d/2024-02-22"); + int initialSize = hydrationList.getSize(); + hydrationList.deleteEntry("hydration delete 2"); // Index out of bounds + hydrationList.deleteEntry("hydration delete -1"); + assertEquals(initialSize, hydrationList.getSize()); + } + + @Test + public void testPrintHydrationListEmpty() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + HydrationList hydrationList = new HydrationList(); + hydrationList.printHydrationList(); + System.setOut(System.out); + String expectedOutput = "\t Your hydration list is empty. Add new entries to populate your list :)" + + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + } + + @Test + public void testPrintHydrationListNonEmpty() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + HydrationList hydrationList = new HydrationList(); + hydrationList.addEntry("hydration in Milo v/200 d/2024-02-22"); + hydrationList.printHydrationList(); + System.setOut(System.out); + String expectedOutput = "\t The following entry has been added to your hydration list!" + lineSeparator + + "\t \t hydrationID: 1, Date: 2024-02-22, Description: Milo, Volume: 200" + lineSeparator + + "\t Your Hydration List:" + lineSeparator + + "\t 1. \t hydrationID: 1, Date: 2024-02-22, Description: Milo, Volume: 200" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + } + + @Test + public void testPrintHydrationListMultipleEntries() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + HydrationList hydrationList = new HydrationList(); + hydrationList.addEntry("hydration in Milo v/200 d/2024-02-22"); + hydrationList.addEntry("hydration in Water v/300 d/2024-02-22"); + hydrationList.addEntry("hydration in Juice v/150 d/2024-02-22"); + hydrationList.printHydrationList(); + System.setOut(System.out); + String expectedOutput = "\t The following entry has been added to your hydration list!" + lineSeparator + + "\t \t hydrationID: 1, Date: 2024-02-22, Description: Milo, Volume: 200" + lineSeparator + + "\t The following entry has been added to your hydration list!" + lineSeparator + + "\t \t hydrationID: 2, Date: 2024-02-22, Description: Water, Volume: 300" + lineSeparator + + "\t The following entry has been added to your hydration list!" + lineSeparator + + "\t \t hydrationID: 3, Date: 2024-02-22, Description: Juice, Volume: 150" + lineSeparator + + "\t Your Hydration List:" + lineSeparator + + "\t 1. \t hydrationID: 1, Date: 2024-02-22, Description: Milo, Volume: 200" + lineSeparator + + "\t 2. \t hydrationID: 2, Date: 2024-02-22, Description: Water, Volume: 300" + lineSeparator + + "\t 3. \t hydrationID: 3, Date: 2024-02-22, Description: Juice, Volume: 150" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + assertEquals(3, hydrationList.getSize()); + } + @Test + public void testAddEntry() { + HydrationList hydrationList = new HydrationList(); + int initialSize = hydrationList.getSize(); + hydrationList.addEntry("hydration in Water v/250 d/2024-02-23"); + assertEquals(initialSize + 1, hydrationList.getSize()); + } + + @Test + public void testDeleteEntryFromEmptyList() { + HydrationList hydrationList = new HydrationList(); + int initialSize = hydrationList.getSize(); + hydrationList.deleteEntry("hydration delete 1"); + assertEquals(initialSize, hydrationList.getSize()); + } + + @Test + public void testPrintHydrationListWithMultipleEntries() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + HydrationList hydrationList = new HydrationList(); + hydrationList.addEntry("hydration in Coffee v/150 d/2024-02-22"); + hydrationList.addEntry("hydration in Tea v/200 d/2024-02-22"); + hydrationList.printHydrationList(); + System.setOut(System.out); + String expectedOutput = "\t The following entry has been added to your hydration list!" + lineSeparator + + "\t \t hydrationID: 1, Date: 2024-02-22, Description: Coffee, Volume: 150" + lineSeparator + + "\t The following entry has been added to your hydration list!" + lineSeparator + + "\t \t hydrationID: 2, Date: 2024-02-22, Description: Tea, Volume: 200" + lineSeparator + + "\t Your Hydration List:" + lineSeparator + + "\t 1. \t hydrationID: 1, Date: 2024-02-22, Description: Coffee, Volume: 150" + lineSeparator + + "\t 2. \t hydrationID: 2, Date: 2024-02-22, Description: Tea, Volume: 200" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + assertEquals(2, hydrationList.getSize()); + } +} diff --git a/src/test/java/seedu/lifetrack/ParserCaloriesTest.java b/src/test/java/seedu/lifetrack/ParserCaloriesTest.java new file mode 100644 index 0000000000..f902a307e1 --- /dev/null +++ b/src/test/java/seedu/lifetrack/ParserCaloriesTest.java @@ -0,0 +1,163 @@ +//@@author owx0130 +package seedu.lifetrack; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import seedu.lifetrack.calories.Food; +import seedu.lifetrack.calories.calorielist.InputEntry; +import seedu.lifetrack.calories.calorielist.OutputEntry; +import seedu.lifetrack.system.exceptions.InvalidInputException; +import static seedu.lifetrack.system.parser.ParserCalories.parseCaloriesInput; + +import java.time.LocalDate; + +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncorrectCaloriesInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncorrectMacrosInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getWhitespaceInInputMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getIncompleteMacrosMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getMacrosInCaloriesOutMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getCaloriesIncorrectOrderMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getCaloriesMissingKeywordsMessage; +import static seedu.lifetrack.system.exceptions.InvalidInputExceptionMessage.getWhitespaceInMacrosInputMessage; + +class ParserCaloriesTest { + + @Test + public void parseCaloriesInput_missingKeywords_exceptionThrown() { + try { + parseCaloriesInput("calories in burger", 0); + } catch (InvalidInputException e) { + assertEquals(getCaloriesMissingKeywordsMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_incompleteInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger c/ d/2024-02-02", 0); + } catch (InvalidInputException e) { + assertEquals(getWhitespaceInInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_incorrectlyOrderedInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger d/2024-02-02 c/123", 0); + } catch (InvalidInputException e) { + assertEquals(getCaloriesIncorrectOrderMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_incorrectMacrosInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger c/123 d/2024-03-22 m/abc", 0); + } catch (InvalidInputException e) { + assertEquals(getIncorrectMacrosInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_incorrectCaloriesInput_exceptionThrown() { + try { + parseCaloriesInput("calories out Running c/abc d/2024-02-02", 0); + } catch (InvalidInputException e) { + assertEquals(getIncorrectCaloriesInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_incompleteMacrosInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger c/123 d/2024-02-02 m/123,132", 0); + } catch (InvalidInputException e) { + assertEquals(getIncompleteMacrosMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_macrosInCaloriesOut_exceptionThrown() { + try { + parseCaloriesInput("calories out running c/123 d/2024-02-02 m/123,123,132", 0); + } catch (InvalidInputException e) { + assertEquals(getMacrosInCaloriesOutMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_whitespaceInMacrosInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger c/123 d/2024-02-02 m/123, ,132", 0); + } catch (InvalidInputException e) { + assertEquals(getWhitespaceInMacrosInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_negativeMacrosInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger c/123 d/2024-02-02 m/123,-3,132", 0); + } catch (InvalidInputException e) { + assertEquals(getIncorrectMacrosInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_negativeCaloriesInput_exceptionThrown() { + try { + parseCaloriesInput("calories in burger c/-123 d/2024-02-02 m/123,123,123", 0); + } catch (InvalidInputException e) { + assertEquals(getIncorrectCaloriesInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_correctCaloriesInInput_entryReturned() { + try { + InputEntry expectedEntry = new InputEntry(1, "burger", 123, LocalDate.parse("2024-02-02")); + InputEntry entry = (InputEntry)parseCaloriesInput("calories in burger c/123 d/2024-02-02", 0); + assertEquals(expectedEntry.getEntryID(), entry.getEntryID()); + assertEquals(expectedEntry.getDescription(), entry.getDescription()); + assertEquals(expectedEntry.getDate(), entry.getDate()); + assertEquals(expectedEntry.getCalories(), entry.getCalories()); + } catch (InvalidInputException e) { + assertEquals(getIncorrectCaloriesInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_correctCaloriesInInputWithMacros_entryReturned() { + try { + Food expectedFood = new Food(10, 10, 10); + InputEntry expectedEntry = new InputEntry(1, "burger", 123, LocalDate.parse("2024-02-02"), expectedFood); + InputEntry actualEntry = + (InputEntry)parseCaloriesInput("calories in burger c/123 d/2024-02-02 m/10,10,10", 0); + assertEquals(expectedEntry.getEntryID(), actualEntry.getEntryID()); + assertEquals(expectedEntry.getDescription(), actualEntry.getDescription()); + assertEquals(expectedEntry.getDate(), actualEntry.getDate()); + assertEquals(expectedEntry.getCalories(), actualEntry.getCalories()); + Food actualFood = actualEntry.getFood(); + assertEquals(expectedFood.getCarbohydrates(), actualFood.getCarbohydrates()); + assertEquals(expectedFood.getProteins(), actualFood.getProteins()); + assertEquals(expectedFood.getFats(), actualFood.getFats()); + } catch (InvalidInputException e) { + assertEquals(getIncorrectCaloriesInputMessage(), e.getMessage()); + } + } + + @Test + public void parseCaloriesInput_correctCaloriesOutInput_entryReturned() { + try { + OutputEntry expectedEntry = new OutputEntry(1, "run", 123, LocalDate.parse("2024-02-02")); + OutputEntry entry = (OutputEntry)parseCaloriesInput("calories out run c/123 d/2024-02-02", 0); + assertEquals(expectedEntry.getEntryID(), entry.getEntryID()); + assertEquals(expectedEntry.getDescription(), entry.getDescription()); + assertEquals(expectedEntry.getDate(), entry.getDate()); + assertEquals(expectedEntry.getCalories(), entry.getCalories()); + } catch (InvalidInputException e) { + assertEquals(getIncorrectCaloriesInputMessage(), e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/lifetrack/ParserHydrationTest.java b/src/test/java/seedu/lifetrack/ParserHydrationTest.java new file mode 100644 index 0000000000..9672a9007c --- /dev/null +++ b/src/test/java/seedu/lifetrack/ParserHydrationTest.java @@ -0,0 +1,151 @@ +//@@author rexyyong +package seedu.lifetrack; + +import org.junit.jupiter.api.Test; +import seedu.lifetrack.system.exceptions.InvalidInputException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.lifetrack.system.parser.ParserHydration.parseHydrationInput; + +public class ParserHydrationTest { + + @Test + public void parseHydrationInput_inputContains2Beverages_invalidInputExceptionThrown() { + // setup test + String invalidInput = "liquids in Milo b/1000 b/1000"; + + // Call methods to test + try { + parseHydrationInput(invalidInput, 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + + } + } + + @Test + public void parseHydrationInput_inputContains2Volumes_invalidInputExceptionThrown() { + // setup test + String invalidInput = "liquids in Milo v/1000 v/1000"; + + // Call methods to test + try { + parseHydrationInput(invalidInput, 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + + } + } + + @Test + public void parseHydrationInput_inputMissingVolume_invalidInputExceptionThrown() { + // setup test + String invalidInput = "liquids in Milo date/221024"; + + // Call methods to test + try { + parseHydrationInput(invalidInput, 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + + } + } + + @Test + public void parseHydrationInput_inputWrongOrderDateBeforeVolume_invalidInputExceptionThrown() { + // setup test + String invalidInput = "hydration add Milo d/221024 v/1000"; + + // Call methods to test + try { + parseHydrationInput(invalidInput, 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have keyed the input in the correct order!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + } + } + + @Test + public void parseHydrationInput_inputNonIntegerValueForVolume_invalidInputExceptionThrown() { + // setup test + String invalidInput = "hydration in Milo v/##s100 d/221024"; + + // Call methods to test + try { + parseHydrationInput(invalidInput, 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that positive integer value is keyed in for volume!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + } + } + + @Test + public void parseHydrationInput_inputNegativeValueForVolume_invalidInputExceptionThrown() { + // setup test + String invalidInput = "hydration add Milo v/-1000 d/221024"; + + // Call methods to test + try { + parseHydrationInput(invalidInput, 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that positive integer value is keyed in for volume!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + } + } + + //@@author shawnpong + @Test + public void parseHydrationInput_missingKeywords_exceptionThrown() { + try { + parseHydrationInput("liquids in", 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + } + } + + @Test + public void parseHydrationInput_incompleteInput_exceptionThrown() { + try { + parseHydrationInput("liquids in b/Milo", 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + + } + } + + @Test + public void parseHydrationInput_emptyBeverageName_exceptionThrown() { + try { + parseHydrationInput("liquids in v/1000", 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + + } + } + @Test + public void parseHydrationInput_emptyVolumeDescription_exceptionThrown() { + try { + parseHydrationInput("liquids in Milo v/ ", 0); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n" + + "\t Please ensure that you have entered all keywords!\n" + + "\t Example input: hydration in Milo v/1000 d/2024-04-19", e.getMessage()); + + } + } +} diff --git a/src/test/java/seedu/lifetrack/ParserSleepTest.java b/src/test/java/seedu/lifetrack/ParserSleepTest.java new file mode 100644 index 0000000000..b210a6cc53 --- /dev/null +++ b/src/test/java/seedu/lifetrack/ParserSleepTest.java @@ -0,0 +1,70 @@ +package seedu.lifetrack; + +import org.junit.jupiter.api.Test; +import seedu.lifetrack.system.exceptions.InvalidInputException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.lifetrack.system.parser.ParserSleep.parseSleepInput; + +public class ParserSleepTest { + @Test + public void parseSleepInput_inputContains2Duration_invalidInputExceptionThrown() { + // setup test + String invalidInput = "sleep add 8.0 9.2"; + // Call methods to test + try { + parseSleepInput(invalidInput); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n"+"\t Please ensure that you have entered all keywords!\n"+ + "\t Example input: sleep add 7.5 d/2024-03-11", e.getMessage()); + } + } + @Test + public void parseSleepInput_inputContains2Date_invalidInputExceptionThrown() { + // setup test + String invalidInput = "sleep add d/2024-12-12 d/2024-11-11"; + // Call methods to test + try { + parseSleepInput(invalidInput); + } catch (InvalidInputException e) { + assertEquals("Please ensure that you have keyed in the correct format: " + + "sleep add d/", e.getMessage()); + } + } + @Test + public void parseSleepInput_inputMissingDuration_invalidInputExceptionThrown() { + // setup test + String invalidInput = "sleep add d/2024-02-11"; + // Call methods to test + try { + parseSleepInput(invalidInput); + } catch (InvalidInputException e) { + assertEquals("Please ensure that you have keyed in the correct format: " + + "sleep add d/", e.getMessage()); + } + } + + @Test + public void parseSleepInput_inputNonPositiveValueForDuration_invalidInputExceptionThrown() { + // setup test + String invalidInput = "sleep add -2 d/2024-03-11"; + + // Call methods to test + try { + parseSleepInput(invalidInput); + } catch (InvalidInputException e) { + assertEquals("\t Please input one positive real number into the sleep duration field!" + , e.getMessage()); + } + } + @Test + public void parseSleepInput_missingKeywords_exceptionThrown() { + + try { + parseSleepInput("sleep add"); + } catch (InvalidInputException e) { + assertEquals("\t Invalid input!\n"+"\t Please ensure that you have entered all keywords!\n"+ + "\t Example input: sleep add 7.5 d/2024-03-11", e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/lifetrack/ParserUserTest.java b/src/test/java/seedu/lifetrack/ParserUserTest.java new file mode 100644 index 0000000000..ed69e180fb --- /dev/null +++ b/src/test/java/seedu/lifetrack/ParserUserTest.java @@ -0,0 +1,192 @@ +package seedu.lifetrack; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import seedu.lifetrack.system.exceptions.InvalidInputException; +import seedu.lifetrack.system.parser.ParserUser; +import seedu.lifetrack.user.User; + +public class ParserUserTest { + + @Test + public void parseUserSetUp_validInput_success() throws InvalidInputException { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + ParserUser.parseSetUp(input, user); + Assertions.assertEquals("karthik", user.getName()); + Assertions.assertEquals(175, user.getHeight()); + Assertions.assertEquals(70, user.getWeight()); + Assertions.assertEquals(25, user.getAge()); + Assertions.assertEquals("male", user.getSex()); + Assertions.assertEquals(3, user.getExerciseLevels()); + Assertions.assertEquals(2, user.getGoal()); + } + + @Test + public void parseUserSetUp_emptyInput_throwsException() { + String input = "user setup"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_inCorrectOrder_throwsException() { + String input = "user setup karthik w/175 h/70 a/25 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidNumberOfInputs_throwException() { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/2 h/140"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingName_throwsException() { + String input = "user setup h/175 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingHeight_throwsException() { + String input = "user setup karthik h/ w/70 a/25 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingWeight_throwsException() { + String input = "user setup karthik h/175 w/ a/25 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingAge_throwsException() { + String input = "user setup karthik h/175 w/70 a/ s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingSex_throwsException() { + String input = "user setup karthik h/175 w/70 a/25 s/ e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingExerciseLevels_throwsException() { + String input = "user setup karthik h/175 w/70 a/25 s/male e/ g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_missingGoal_throwsException() { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidHeight_throwsInvalidInputException() { + String input = "user setup karthik h/80 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidWeight_throwsInvalidInputException() { + String input = "user setup karthik h/175 w/20 a/25 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidAge_throwsInvalidInputException() { + String input = "user setup karthik h/175 w/70 a/10 s/male e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidGender_throwsInvalidInputException() { + String input = "user setup karthik h/175 w/70 a/25 s/non-binary e/3 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidExerciseLevels_throwsInvalidInputException() { + String input = "user setup karthik h/175 w/70 a/25 s/male e/6 g/2"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserSetUp_invalidGoal_throwsInvalidInputException() { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/7"; + User user = new User(); + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseSetUp(input, user)); + } + + @Test + public void parseUserUpdate_validNameSetUp() throws InvalidInputException { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + ParserUser.parseSetUp(input, user); + String name = "user update name John"; + String height = "user update height 174"; + String weight = "user update weight 90"; + String age = "user update age 18"; + String sex = "user update sex female"; + String exerciseLevel = "user update exercise levels 2"; + String goal = "user update goal 1"; + ParserUser.parseUpdate(name, user); + ParserUser.parseUpdate(height, user); + ParserUser.parseUpdate(weight, user); + ParserUser.parseUpdate(age, user); + ParserUser.parseUpdate(sex, user); + ParserUser.parseUpdate(exerciseLevel, user); + ParserUser.parseUpdate(goal, user); + Assertions.assertEquals("John", user.getName()); + Assertions.assertEquals(174, user.getHeight()); + Assertions.assertEquals(90, user.getWeight()); + Assertions.assertEquals(18, user.getAge()); + Assertions.assertEquals("female", user.getSex()); + Assertions.assertEquals(2, user.getExerciseLevels()); + Assertions.assertEquals(1, user.getGoal()); + } + + @Test + public void parseUserUpdate_emptyCommand_throwsException() throws InvalidInputException { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + ParserUser.parseSetUp(input, user); + String test = "user update"; + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseUpdate(test, user)); + } + + @Test + public void parseUserUpdate_emptyField_throwsException() throws InvalidInputException { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + ParserUser.parseSetUp(input, user); + String test = "user update name"; + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseUpdate(test, user)); + } + + @Test + public void parseUserUpdate_unknownInput_throwsException() throws InvalidInputException { + String input = "user setup karthik h/175 w/70 a/25 s/male e/3 g/2"; + User user = new User(); + ParserUser.parseSetUp(input, user); + String test = "user update abcd abcd"; + Assertions.assertThrows(InvalidInputException.class, () -> ParserUser.parseUpdate(test, user)); + } + +} diff --git a/src/test/java/seedu/lifetrack/SleepFileHandlerTest.java b/src/test/java/seedu/lifetrack/SleepFileHandlerTest.java new file mode 100644 index 0000000000..caf32f9828 --- /dev/null +++ b/src/test/java/seedu/lifetrack/SleepFileHandlerTest.java @@ -0,0 +1,44 @@ +package seedu.lifetrack; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import seedu.lifetrack.sleep.sleeplist.SleepEntry; +import seedu.lifetrack.system.storage.SleepFileHandler; + +public class SleepFileHandlerTest { + + private String filePath = "sample-data/sleepTestData.txt"; + private SleepFileHandler fileHandler = new SleepFileHandler(filePath); + private ArrayList expectedEntries = new ArrayList<>(); + + @Test + public void getSleepEntriesFromFile_correctSleepAddInput_allEntriesRetrievedFromFile() { + try { + SleepEntry expectedEntry_1 = new SleepEntry(1, 20.3, LocalDate.parse("2024-02-02")); + SleepEntry expectedEntry_2 = new SleepEntry(2, 19.2, LocalDate.parse("2024-02-01")); + SleepEntry expectedEntry_3 = new SleepEntry(3, 18.1, LocalDate.parse("2024-02-03")); + expectedEntries.add(expectedEntry_1); + expectedEntries.add(expectedEntry_2); + expectedEntries.add(expectedEntry_3); + fileHandler.writeEntries(expectedEntries); + ArrayList actualEntries = fileHandler.getSleepEntriesFromFile(); + for (int i = 0; i < expectedEntries.size(); i++) { + SleepEntry expectedEntryOut = (SleepEntry)expectedEntries.get(i); + SleepEntry actualEntryOut = (SleepEntry)actualEntries.get(i); + assertEquals(expectedEntryOut.getEntryID(), actualEntryOut.getEntryID()); + assertEquals(expectedEntryOut.getDescription(), actualEntryOut.getDescription()); + assertEquals(expectedEntryOut.getDate(), actualEntryOut.getDate()); + assertEquals(expectedEntryOut.getDuration(), actualEntryOut.getDuration()); + } + expectedEntries.clear(); + } catch (FileNotFoundException e) { + return; + } + } +} diff --git a/src/test/java/seedu/lifetrack/SleepListTest.java b/src/test/java/seedu/lifetrack/SleepListTest.java new file mode 100644 index 0000000000..5d38366d24 --- /dev/null +++ b/src/test/java/seedu/lifetrack/SleepListTest.java @@ -0,0 +1,84 @@ +package seedu.lifetrack; + +import org.junit.jupiter.api.Test; +import seedu.lifetrack.sleep.sleeplist.SleepList; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SleepListTest { + @Test + public void testDeleteSleepValidIndex(){ + SleepList sleepList = new SleepList(); + sleepList.addSleep("sleep add 7.5 d/2024-03-03"); + sleepList.addSleep("sleep add 8 d/2024-12-10"); + sleepList.printSleepList(); + int initialSize = sleepList.getSize(); + sleepList.deleteSleep("sleep delete "+sleepList.getSleep(0).getEntryID()); + assertEquals(initialSize - 1, sleepList.getSize()); + } + @Test + public void testDeleteSleepInvalidIndex() { + SleepList sleepList = new SleepList(); + sleepList.addSleep("sleep add 7.5 d/2024-13-10"); + sleepList.addSleep("sleep add 8"); + int initialSize = sleepList.getSize(); + sleepList.deleteSleep("sleep delete 20"); // Index out of bounds + sleepList.deleteSleep("sleep delete -2"); + assertEquals(initialSize, sleepList.getSize()); + } + @Test + public void testPrintSleepListEmpty() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + SleepList sleepList = new SleepList(); + sleepList.printSleepList(); + System.setOut(System.out); + String expectedOutput = "\t Your sleep list is empty. Add new entries to populate your list :)" + + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + } + @Test + public void testPrintSleepListNonEmpty() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + SleepList sleepList = new SleepList(); + sleepList.addSleep("sleep add 7.5 d/2024-03-11"); + sleepList.printSleepList(); + System.setOut(System.out); + String expectedOutput = "\t The following entry has been added to your sleep list!" + lineSeparator + + "\t \t Sleep ID: 4, Date: 2024-03-11, Duration: 7.50 hours" + lineSeparator + + "\t Your Sleep List:" + lineSeparator + + "\t 1. \t Sleep ID: 4, Date: 2024-03-11, Duration: 7.50 hours" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + } + + @Test + public void testPrintSleepListMultipleEntries() { + String lineSeparator = System.lineSeparator(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + SleepList sleepList = new SleepList(); + sleepList.addSleep("sleep add 7.5 d/2024-03-11"); + sleepList.addSleep("sleep add 8.0 d/2024-04-09"); + sleepList.addSleep("sleep add 4.2 d/2024-02-21"); + sleepList.printSleepList(); + System.setOut(System.out); + String expectedOutput = "\t The following entry has been added to your sleep list!" + lineSeparator + + "\t \t Sleep ID: 5, Date: 2024-03-11, Duration: 7.50 hours" + lineSeparator + + "\t The following entry has been added to your sleep list!" + lineSeparator + + "\t \t Sleep ID: 6, Date: 2024-04-09, Duration: 8.00 hours" + lineSeparator + + "\t The following entry has been added to your sleep list!" + lineSeparator + + "\t \t Sleep ID: 7, Date: 2024-02-21, Duration: 4.20 hours" + lineSeparator + + "\t Your Sleep List:" + lineSeparator + + "\t 1. \t Sleep ID: 7, Date: 2024-02-21, Duration: 4.20 hours" + lineSeparator + + "\t 2. \t Sleep ID: 5, Date: 2024-03-11, Duration: 7.50 hours" + lineSeparator + + "\t 3. \t Sleep ID: 6, Date: 2024-04-09, Duration: 8.00 hours" + lineSeparator; + assertEquals(expectedOutput, outputStream.toString()); + assertEquals(3, sleepList.getSize()); + } +} diff --git a/src/test/java/seedu/lifetrack/UITest.java b/src/test/java/seedu/lifetrack/UITest.java new file mode 100644 index 0000000000..9da72cf7d2 --- /dev/null +++ b/src/test/java/seedu/lifetrack/UITest.java @@ -0,0 +1,42 @@ +package seedu.lifetrack; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import seedu.lifetrack.calories.calorielist.CalorieList; +import seedu.lifetrack.hydration.hydrationlist.HydrationList; +import seedu.lifetrack.sleep.sleeplist.SleepList; +import seedu.lifetrack.ui.Ui; +import seedu.lifetrack.user.User; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +public class UITest { + 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 + // Expect Empty String as function is exited + public void handleUserInput_inputBye_printByeMessage() { + CalorieList calorieList = new CalorieList(); + HydrationList hydrationList = new HydrationList(); + SleepList sleepList = new SleepList(); + User user = new User(); + String input = "bye"; + Ui.handleUserInput(input, calorieList, hydrationList,user,sleepList); + assertEquals("", outContent.toString()); + } +} diff --git a/src/test/java/seedu/lifetrack/UserFileHandlerTest.java b/src/test/java/seedu/lifetrack/UserFileHandlerTest.java new file mode 100644 index 0000000000..6749e98433 --- /dev/null +++ b/src/test/java/seedu/lifetrack/UserFileHandlerTest.java @@ -0,0 +1,54 @@ +package seedu.lifetrack; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.FileNotFoundException; +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; + +import seedu.lifetrack.system.storage.UserFileHandler; +import seedu.lifetrack.user.User; + +public class UserFileHandlerTest { + + //user data constants + private final int NAME_INDEX = 0; + private final int HEIGHT_INDEX = 1; + private final int WEIGHT_INDEX = 2; + private final int AGE_INDEX = 3; + private final int SEX_INDEX = 4; + private final int EXERCISE_INDEX = 5; + private final int GOAL_INDEX = 6; + private final int REQ_CAL_INDEX = 7; + + private String filePath = "sample-data/userTestData.txt"; + private UserFileHandler fileHandler = new UserFileHandler(filePath); + + @Test + public void getUserDataFromFile_correctUserInput_allUserDataRetrievedFromFile() { + try { + User expectedUser = new User(); + expectedUser.setName("john"); + expectedUser.setHeight(170); + expectedUser.setWeight(90); + expectedUser.setAge(23); + expectedUser.setSex("male"); + expectedUser.setExerciseLevels(5); + expectedUser.setGoal(5); + expectedUser.setCaloriesRequired(1900); + fileHandler.writeUserData(expectedUser); + ArrayList data = fileHandler.getUserDataFromFile(); + assertEquals(expectedUser.getName(), data.get(NAME_INDEX)); + assertEquals(expectedUser.getHeight(), Integer.parseInt(data.get(HEIGHT_INDEX))); + assertEquals(expectedUser.getWeight(), Integer.parseInt(data.get(WEIGHT_INDEX))); + assertEquals(expectedUser.getAge(), Integer.parseInt(data.get(AGE_INDEX))); + assertEquals(expectedUser.getSex(), data.get(SEX_INDEX)); + assertEquals(expectedUser.getExerciseLevels(), Integer.parseInt(data.get(EXERCISE_INDEX))); + assertEquals(expectedUser.getGoal(), Integer.parseInt(data.get(GOAL_INDEX))); + assertEquals(expectedUser.getCaloriesRequired(), Integer.parseInt(data.get(REQ_CAL_INDEX))); + } catch (FileNotFoundException e) { + return; + } + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..7bdc6d7158 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,15 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + ----------------------------------------------------------------------------- + Hello from -What is your name? -Hello James Gosling + +.____ .__ _____ ___________ __ +| | |__|/ ____\____ \__ ___/___________ ____ | | __ +| | | \ __\/ __ \ | | \_ __ \__ \ _/ ___\| |/ / +| |___| || | \ ___/ | | | | \// __ \\ \___| < +|_______ \__||__| \___ > |____| |__| (____ /\___ >__|_ \ + \/ \/ \/ \/ \/ + + How can I help you today? + ----------------------------------------------------------------------------- + ----------------------------------------------------------------------------- + Bye! See you again soon ^^ diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..0abaeaa993 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1 @@ -James Gosling \ No newline at end of file +bye \ No newline at end of file