diff --git a/.gitignore b/.gitignore index 2873e189e1..3a354b1ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,13 @@ # Gradle build files /.gradle/ /build/ -src/main/resources/docs/ # MacOS custom attributes files created by Finder .DS_Store *.iml bin/ -/text-ui-test/ACTUAL.TXT +/text-ui-test/EXPECTED.TXT text-ui-test/EXPECTED-UNIX.TXT + +data \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..c5f3f6b9c7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..19e86fe56e --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: seedu.duke.Duke + diff --git a/build.gradle b/build.gradle index ea82051fab..85211a852e 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation 'com.google.code.gson:gson:2.10.1' } test { @@ -43,4 +44,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..56ea56a041 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,8 @@ # 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 | PPP +--------|:-----------:|:--------------------------------------:|:---------:|:----:| +![](https://picsum.photos/98) | Kevin Zhang | [Github](https://github.com/kevinz420) | [Portfolio](docs/team/johndoe.md) +![](https://picsum.photos/99) | Ethan Huang | [Github](https://github.com/remy9926) | [Portfolio](docs/team/johndoe.md) | [PPP](./team/remy9926.md) +![](https://picsum.photos/100) | Jiayan Tian | [Github](https://github.com/j-y-yan) | [Portfolio](docs/team/johndoe.md) +![](https://picsum.photos/102) | Feng Guangyao | [Github](https://github.com/mroppa1) | [Portfolio](docs/team/fengguangyao.md) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..a368695ae8 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,107 @@ # Developer Guide +## **Content** +- [Handing User Inputs & Displaying Output](#item-one) +- [Updates Exercise from Log feature](#item-two) +- [Adding a New Exercise Goal](#item-three) + ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +[Remy9926 iP for File I/O](https://github.com/Remy9926/ip) ## Design & implementation -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} + +### Handling User Inputs & Displaying Output + +A lot of the initial project setup that deals with things like parsing, displaying outputs back to the user, exceptions, etc. are strongly inspired by the address book example project demoed in class. Specifically, we have classes that are responsible for various aspects of our project in order to follow the single-responsibility principle and limit the amount of coupling. + +One specific design implementation we chose was having a Command superclass that all commands in our application will inherit from. This allows every command we write in the future to be guaranteed to possess all methods specified in the superclass. Therefore, we can call these methods in our main function without needing to worry about the differences between various commands because they all inherit from the same parent. Another alternative we considered was simply making each separate command its own function and calling them from our parser class. However, we want to keep the commands as high level as possible and avoid calling more lower-level functions if possible, so we can provide more encapsulation to each command. Therefore, we decided to go with this class-based command hierarchy that was also found in the example address book repo. + +Below is a class diagram that describes how our various components interact with one another: + +![image](https://github.com/AY2324S1-CS2113-F11-1/tp/assets/54857388/21bd2aa9-8e50-49d2-b888-50446115f922) + +The parser class is responsible for reading the user input and creating specific command objects. All of these subcommands inherit from a general Command parent class which has an `execute` method that can be called regardless of child class and will return a CommandResult object. The CommandResult object dictates what info (string or list of items) needs to be returned to the user for viewing. Both CommandResult and TextUi (responsible for actually displaying data from CommandResult to user) will only print objects from classes that inherit from the Printable interface. This ensures that objects in the `relevantItems` list are all serializable as a human-readable string to be displayed to the end user. + + +### Update Exercises from Log feature + +The proposed feature is to allow users to update exercises from the ExerciseLog class to allow for full CRUD functionality of the class. Currently, users are able to create exercises and log them as well as view all the exercises that they have created, but in the case of a typo, they are unable to update the log to accommodate for the user error. To improve the user experience, this update feature has been proposed to allow users to have a better experience using the app. + +The update feature will be implemented by allowing users to select the task that they want to edit by specifying the month and day that they logged this exercise at, as well as the corresponding exercise name and the number of calories that the exercise has burned. This is because each exercise is stored within Day objects, and in order to check if two exercises are equivalent, we check both the exerciseName and caloriesBurned fields. Thus, these parameters will need to be specified by the user in order to update the exercise log. + +Below is an example of how this feature will work: + +Step 1: Upon starting the app, the ExerciseLog class is initialized and a log is created for the user to log their exercises. + +Screenshot 2023-10-22 at 22 21 04 + +Step 2: The user calls the log command with the specified parameters to add a new exercise to the list. However, the user notices that they made a typo and want to change the details of the exercise that they just logged. + +Screenshot 2023-10-22 at 22 23 59 + +Step 3: The user calls the update command with the information of the old exercise as well as the new information that the user wants to update with. With this, the update is done. + +Screenshot 2023-10-22 at 22 25 09 + + +### Adding a New Exercise Goal +Setting up exercise goals is one of the major components in our FitNus app. It could guide users to do exercise in a more systematic way, while ensuring the possibility of keeping track of the record. + +Inheritance Level: + +As one of the commands input by user, the goal class should be inherited from the command class, with the general abstract method such as constructor, execution of command, etc. + +Implementation: +The goalist class has some helper functions, including checkGoalInput to ensure the validity of user input, addGoal to add a new goal record into the goal list. The execution of goal command simply create a new goal record by first validating the user input, and create a goal record and finally return the result of creating the goal. + +1. View Goal function and Delete Goal function + +The view function is vital in visualization of the goal list while delete function helps to remove some redundant, incorrect, or achieved goals. + +- Implementation 1: + + Note that since other command classes may also need a list function or delete function, these two functions can be created for a general use and applicable for all commands. + +- Implementation 2 (Current Implementation): + + Since the user need different input to differentiate type of objects to delete, it is acceptable to implement different delete classes, with some common implementation of interface(if any). + +2. Achieve Goal function + +This aims to keep the record of achieved goals. Once the user marks a goal as finished, the record will be automatically backed up into the history list, with supported view function. Also, it is designed not to allow user to modify any achivement. If user made any mistakes, either he starts a new achievement files, or he keeps the false record. + +Potential Improvements: + +All records in the history list(and general goal list) should be sorted according to their finish time and in alphabetical order. + +To provent user from making mistakes, we could design an algorithm to detect whether an achievement is accomplished. ## Product scope ### Target user profile -{Describe the target user profile} +Our target users are NUS students who are looking to get in shape and live a healthier lifestyle. We want to provide fitness and eating recommendations that are near the NUS campus which will be easily accessible to our end users. ### Value proposition -{Describe the value proposition: what problem does it solve?} +* track calories consumption vs usage +* set fitness goals and monitor their progress towards these goals + ## User Stories |Version| As a ... | I want to ... | So that I can ...| |--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +|v1.0|New User|see usage instructions|refer to them when I forget how to use the application| +|v2.0|Obesity Victim|set up my work out plan|lose weight| +|v2.0|Health Enthusiasts|set up fitness goals|follow and keep myself healthy in a systematic way| +|v2.1|Dieter|record everyday diet|monitor the amount of calories taking in and check total amount| -## Non-Functional Requirements - -{Give non-functional requirements} -## Glossary - -* *glossary item* - Definition +## Non-Functional Requirements -## Instructions for manual testing +* Java 17 is the most recommended version to use. +* Make sure the login user of your pc keeps the same, otherwise the storage address might differ, resulting in the loss of precious data. -{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..4d176dc589 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ # Duke -{Give product intro here} +FITNUS helps NUS students set and maintain fitness goals for themselves. Students can log their meals that they've eaten as well as any exercise they have done and manage it all in our intuitive CLI application! -Useful links: +Here are some useful links: * [User Guide](UserGuide.md) * [Developer Guide](DeveloperGuide.md) -* [About Us](AboutUs.md) +* [About Us](AboutUs.md) \ No newline at end of file diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..270e29e08d 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,41 +2,291 @@ ## Introduction -{Give a product intro} +Welcome to the FITNUS UserGuide! We're glad you're here and if you have any questions about how to operate the application, all the information will be here for you. ## Quick Start -{Give steps to get started quickly} +Please follows the steps below to start your Fitnus journey. 1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Down the latest version of `FITNUS` from [here](https://github.com/AY2324S1-CS2113-F11-1/tp/releases). +3. Put the downloaded jar file into any directory you prefer. This directory should allow read and write of any files inside the directory. +4. Open the command terminal on your device. Run the jar file using command `java -jar ABSOLUTE_PATH_OF_JAR_FILE`. **If it failed, please try steps 5 and 6.** +5. Make sure that you start the terminal from the same position/directory where the jar file is stored. Below is an example on Windows. + + + +6. Run the FITNUS app using command `java -jar JAR_FILE_NAME.jar` where `JAR_FILE_NAME` is the name of the jar file as stored on your local computer + + A simple example as below, with downloaded file name "Fitnus": + + ![image](https://github.com/AY2324S1-CS2113-F11-1/tp/assets/142566176/a7d7f0af-1d52-42fa-b4ac-72b599b545d5) + + + + + ## Features -{Give detailed description of each feature} +### Notes about the input command format + +- For each type command, a specific `COMMAND FORMAT` is required. Please go through the following guidelines to look for the details of each command. + +- Both `CAPITAL LETTER` and `small letter` are acceptable. But consecutive white space between words will be eliminated automatically. + + e.g. `VIEWG`, `viewG`, `viewg` will be consider the same. This is to favour the typing habits of different users. + +- In some cases, you may need to input a date information. FitNus app provides the following supported formats. + + 1. Format: "yyyy/MM/dd" (e.g., "2023/08/22") + 2. Format: "dd/MM/yyyy" (e.g., "22/08/2023") + + Command example: `set 1000 on 22/08/2023` (For setting a new goal command) + + Please note that some **unexisting date with correct format** will be treated as last day of the month. If you find the input date is wrong, please use a delete command to help you. (e.g. 31/02/2021 will be treated as 28/02/2021, depends on the year.) + +## Content of Command -### Adding a todo: `todo` -Adds a new item to the list of todo items. +1. Quick link to particular command. -Format: `todo n/TODO_NAME d/DEADLINE` +| Exercise | Goal | Meal | Others | +| :------------ |:---------------:| -------------:| --------------:| +| [Add an Exercise](#log---adding-an-exercise) | [Add a Goal](#set-on---setting-up-a-calorie-goal) | [Add a Meal](#meal_add---adding-a-meal) | [Help](#help---viewing-help) | +| [Update an Exercise](#update---updating-an-exercise) | [Delete a Goal](#deleteg---delete-a-goal-from-current-goal-list) | [Delete a Meal](#meal_delete---deleting-a-meal) | [Exit](#exiting-the-program-exit) | +| [View Exercise](#view---viewing-exercises) | [Make Achievement](#achieve---turn-one-goal-as-achieved) | [List a Meal](#meal_list---listing-meals) |[View Achievement](#achievement---view-your-achieved-goals) | +| [Delete Exercise](https://github.com/AY2324S1-CS2113-F11-1/tp/blob/master/docs/UserGuide.md#deletelog---deleting-exercises) | [View Goals](#viewg---view-your-current-goal-list) | | | -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +2. Frequently-asked-quesions([**FAQs**](#faqs)) + +## Command Type + +### `help` - Viewing help +Shows a message explaining how to access the help page. + +Format: `help` + +### `meal_add` - Adding a meal +Adds a new item to the meal list. + +Format: `meal_add MEAL_NAME CALORIES CATEGORY [DATE]` + +* The `CALORIES` should be a standard number. +* The `MEAL_NAME` cannot contain space, use "_" as replacement. +* The `CATEGORY` should be a number of {0, 1, 2} or a string of {"staple_food", "snack", "beverage"}. +* The `DATE` should be a standard string that indicates a valid date, conforming with any format of {"yyyy/M/d", "d/M/yyyy", "yyyy-M-d", "d-M-yyyy"}. Example of usage: -`todo n/Write the rest of the User Guide d/next week` +`meal_add potatoes 100` + +`meal_add baked_bread 66` + +### `meal_delete` - Deleting a meal +Delete a meal from the existing list. + +Format: `meal_delete INDEX` + +* The `INDEX` should be a standard and positive number. + +Example of usage: + +`meal_delete 1` + +`meal_delete 100` + +### `meal_list` - Listing meals +List all the meals that have been recorded. + +Format: `meal_list [CATEGORY]` + +* The `CATEGORY` should be a number of {0, 1, 2} or a string of {"staple_food", "snack", "beverage"} and can be omitted. + +Example of usage: + +`meal_list` + +`meal_list 1` + +`meal_list snack` + +### `log` - Adding an exercise +Adds a new exercise to the Exercise Log. + +Format: `log MONTH DAY EXERCISE_NAME CALORIES_BURNED` + +* The `MONTH` is an integer specifying the month in which the exercise was performed (1-12 inclusive) +* The `DAY` is an integer specifying the day of the month in which the exercise was performed +* The `EXERCISE_NAME` is a string specifying the type of exercise performed +* The `CALORIES_BURNED` is an integer specifying the number of calories burned by the exercise + +Example of usage: + +`log 1 26 Basketball 179` + +`log 12 24 Volleyball 5` + +### `updatelog` - Updating an exercise +Updates the specified exercise within the Exercise Log if the old exercise can be found. If the old exercise is not +found, then the user will not be prompted to provide new details. + +Format: `updatelog MONTH DAY OLD_EXERCISE_NAME OLD_CALORIES_BURNED` `NEW_EXERCISE_NAME NEW_CALORIES_BURNED` + +* The `MONTH` is an integer specifying the month in which the exercise was performed (1-12 inclusive) +* The `DAY` is an integer specifying the day of the month in which the exercise was performed +* The `OLD_EXERCISE_NAME` is a string specifying the name of the exercise to be updated +* The `OLD_CALORIES_BURNED` is an integer specifying the number of calories burned by the exercise to be updated +* The `NEW_EXERCISE_NAME` is a string specifying the new exercise name +* The `NEW_CALORIES_BURNED` is an integer specifying the new number of calories burned + +Example of usage: -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +`updatelog 1 26 Basketball 179` +`Rugby 55` -## FAQ +`updatelog 12 24 Volleyball 5` +`Hockey 98` -**Q**: How do I transfer my data to another computer? +### `viewlog` - Viewing Exercises +View the number of, or each exercise in a day, month, or the entire log. + +Format `viewlog VIEW_TYPE VIEW_SCOPE` + +* The `VIEW_TYPE` is either `total` or `exercises` depending on whether you want to view the total number of exercises +or each exercise and its details +* The `VIEW_SCOPE` can take on either `all` to see all exercises in the log, `month MONTH` where `MONTH` is the month +whose exercises you want to view, or `month MONTH day DAY` where MONTH is the same as above, but `DAY` is the specific +day of the month whose exercises you want to view. + +Example of usage: + +`viewlog exercises all` + +`viewlog total month 1 day 24` + +### `deletelog` - Deleting Exercises +Delete the exericse on the specified month and day + +Format `deletelog MONTH DAY EXERCISE_NAME CALORIES_BURNED` + +* The `MONTH` is an integer specifying the month in which the exercise was performed (1-12 inclusive) +* The `DAY` is an integer specifying the day of the month in which the exercise was performed +* The `OLD_EXERCISE_NAME` is a string specifying the name of the exercise to be deleted +* The `OLD_CALORIES_BURNED` is an integer specifying the number of calories burned by the exercise to be deleted + +Example of usage: + +`deletelog 6 16 Running 179` + +`deletelog 2 27 Hockey 5` + +### `set on` - Setting up a calorie goal +Set up a calorie goal to achieve. The goal are expected to help user accomplish a particular amount of calorie consumption before a **deadline**. + +Format: `set AMOUNT on Date` + +The AMOUNT is in terms of **kcal**. Please follows the provided formats at the beginning of this guide. + +Example of outcome: +``` +[Command entered:set 1000 on 11/11/2023] +Nice! I have added the following goal to your goals list: +Consume 1000 kcal on Nov 11, 2023 +``` + + +### `viewG` - View your current goal list +Look for the content of current goal list. + +Format: `viewG` + +Example of outcome: +``` +[Command entered:viewg] +Here you go! Remember to stick to your exercise and meal plans. +1. Consume 1000 kcal on Nov 11, 2023 +2. Consume 1500 kcal on Nov 25, 2023 +``` + +### `deleteG` - Delete a goal from current goal list +Remove a undersired goal from the goal list. Note that it is not encouraged to remove a goal from the goal list, unless the inserted goal contains errors or is near impossible to be accomplished. + +Format: `deleteG Index` + +You can retrieve the index by showing the goal list + +Example of outcome: + +``` +[Command entered:deleteg 2] +Good. I have removed this goal: Consume 1500 kcal on Nov 25, 2023 +Remember not to give up unaccomplished target! +You now have 1 goals to accomplish. +``` + +### `achieve` - Turn one goal as achieved +Be careful that this funnction will mark a goal as achieved by **removing** a goal from the current goal list and **permanantly** add a new achieved goal into your achievement list. (You can only destroy the whole list if you made any errors or want to give up an achievement.) + +Format: `achieve Index` + +Example of outcome: + +``` +[Command entered:achieve 1] +Congratulation! You have achieved one goal! +[Finished]Consume 1000 kcal on Nov 11, 2023 (: +``` + +### `achievement` - View your achieved goals +The achievement can only be shown. Any modification except adding a new record is not allowed. + +Format: `achievement` + +Expected outcome: + +``` +[Command entered:achievement] +Congratulation! See your achievements below: +1. [A]Consume 1000 kcal on Nov 11, 2023 +``` + +### Exiting the program: `exit` +Exits the program. + +Format: `exit` + +## FAQs + +**Q1**: How do I transfer my data to another computer? + +**A**: It is not suggested to transfer data between machines. The purpose of the app is developed solely for personal use. Nonetheless, you can copy the folder named `data` that is created in the same directory (location) as where you are running the application into your new computer to restore your data. + +**Q2**: Can I edit the data inside my list? e.g. change the calories consumption targets in the goal list + +**A**: Sorry but we do not support the function to edit a list directly. But you can remove a unwanted data from a list and insert a new data again. + +**Q3**: I have accidentally mark an unaccomplished goal as achieved. Could I change the achievement? + +**A**: Sorry but we do not allow user to modify achievement list. If you really want to destory a false record, you must remove all the achievements and start a new save file. One way of doing this is to delete the file "achievement.txt" under the folder "data" inside your jar file repository. -**A**: {your answer here} ## Command Summary -{Give a 'cheat sheet' of commands here} +* Log an exercise `log MONTH DAY EXERCISE_NAME CALORIES_BURNED` +* Update an existing exercise `update MONTH DAY OLD_EXERCISE_NAME OLD_CALORIES_BURNED` +`NEW_EXERCISE_NAME NEW_CALORIES_BURNED` +* View existing exercises `view VIEW_TYPE VIEW_SCOPE` +* Insert a new calorie consumption goal `set AMOUNT on DD/MM/YYYY` +* Delete a calorie goal `deleteG Index` +* View the input calories goal `viewG` +* Achieve a calories goal `achieve Index` +* View achievement `achievement` +* Exit program `exit` + + +## Future Development -* Add todo `todo n/TODO_NAME d/DEADLINE` +- We will update the achievement functionality such that we will have an algorithm to check whether you have achieved a particular goal by taking your exercise record and/or meal record into account. You don't need to worry about making false achievement. +- The goal list record will be updated at the start of the application to check if some current goals are not finished. It will mark such unfinished goal as failed automatically. User will also be allowed to delete such records by command. +- New feature to evaluate and seperate level of achievement +- New feature to differentiate proper level of goal for different people in different demand diff --git a/docs/team/j-y-yan.md b/docs/team/j-y-yan.md new file mode 100644 index 0000000000..865f061990 --- /dev/null +++ b/docs/team/j-y-yan.md @@ -0,0 +1,26 @@ +# Jia Yan's Project Portfolio Page + +FITNUS is a desktop fitness tracking application that allows NUS students to set and maintain fitness goals for themselves. The user interacts with it using a command line interface(CMI). It is believed that if user can type fast, the efficiency of using this app will be much higher than an usual application that supports graphic interface. + + +[My tP Code Contributed](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=J-Y-Yan&tabRepo=AY2324S1-CS2113-F11-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +## Contributions + +### The Calorie Goal Class +The way that I contributed to the project is by creating and implementing the Goal class and its associated classes including the various goal command classes, goal and goalList data type classes, and goal storage class. The app now allows user to input a new calories consumption target on a particular date. It is able to spot errors if the user input a wrong date that is passed or not in the designated format. It also allows user to make achievement from goals, with supported view function and delete function. + +### Documentation +I contributed to the UG in the parts that are related to goal related classes. In the DG, I contributed to the `Adding a new exercise goal` section. + +### Team-Based Tasks +* Release various issues about improvement and bug reports of the app +* Enable assertions in gradle +* Correct coding style problems for all classes before v2.1 +* Update UG for calorie goal related functionality +* Add table of content with markdowns for all parts in UG +* Add questions and answers for FAQs part in UG +* Add features and future developments in UG to enhance completeness +* Add more detailed method documentations to improve readibility +* Change the overall user input processing to allow more flexible inputs for all functionalities +* Review code contributed by teammates and give advice on what can improve diff --git a/docs/team/kevinz420.md b/docs/team/kevinz420.md new file mode 100644 index 0000000000..933c03423c --- /dev/null +++ b/docs/team/kevinz420.md @@ -0,0 +1,30 @@ +# kevinz420's Project Portfolio Page + +FITNUS is a fitness tracking app that allows NUS students to set and maintain fitness goals for themselves. +We support features that allowe students to track their eating, exercise, and fitness achievements +as they progress throughout their own personalized fitness journey. + +**Code contributed:** [RepoSense Link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=kevinz420&tabRepo=AY2324S1-CS2113-F11-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +## Contributions + +### New Feature: Initial Project Setup +I was responsible for setting up the file structure of the project, and creating components that would be utilized +across all features. This included classes such as `Parser` to read in user inputs and `TextUi` to display data in a human-readable way as well as the main function that serves +as the entrypoint to our application. I also wrote custom Exceptions that will be used throughout our program to provide +more descriptive feedback when errors occur and are caught. + +### New Feature: Command Class + Essential Commands +The `Command` class serves as the parent class for all commands supported by our program. As part of the initial setup, I also +implemented some general purpose commands such as `ExitCommand` and `HelpCommand` so that the rest of the team can successfully run +a barebone implementation of our application. + +## Documentation +* User Guide: + * Added documentation for the features help and exit +* Developer Guide: + * Added implementation details about handling user input and displaying output. +## Team-Based Tasks: + * Making bug fixes from bugs found by other students from the PE-D + * Communicating with other members of the team to clarify how my features can be integrated with theirs + * Helping other members of the team with the debugging process and suggesting methods to fix the problems they encounter diff --git a/docs/team/mroppa1.md b/docs/team/mroppa1.md new file mode 100644 index 0000000000..7391895d71 --- /dev/null +++ b/docs/team/mroppa1.md @@ -0,0 +1,30 @@ +# MrOPPA1 Project Portfolio Page + +FITNUS is a fitness tracking app that allows NUS students to set and maintain fitness goals for themselves. + +We support features that allowe students to track their eating, exercise, and fitness achievements as they progress throughout their own personalized fitness journey. + +[My tP Code Dashboard](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=MrOPPA1&tabRepo=AY2324S1-CS2113-F11-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +## Contributions + +### The Meal Class +I take responsibility to implement the Meal class and its associated classes.Meal-related functionailty enables user to record every meal they consume at a specific time, with unique categories and the amount of calories. Also, user can search recorded meals with the specific category and get the total amount of calories of all meals. + +### Documentation +1. UG + + * Meal class and related funtion usage. +2. DG + + * `Product Scope` section. + * `User Story` section. + * `Non-functional Requirements` section. + +### Team-Based Tasks +* Fixing version bugs and renew V2.0 jar files on Github +* Adding dependencies into the gradle +* Set up milestone v2.0 +* Update UG for Meal-related functionality +* Contributing to the date class +* Managing exception and standarize the error output diff --git a/docs/team/remy9926.md b/docs/team/remy9926.md new file mode 100644 index 0000000000..bbaaf605cd --- /dev/null +++ b/docs/team/remy9926.md @@ -0,0 +1,23 @@ +# Remy9926 PPP + +FITNUS is a fitness tracking app that allows NUS students to set and maintain fitness goals for themselves. + +[My tP Code Dashboard](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22&tabOpen=true&tabType=authorship&tabAuthor=Remy9926&tabRepo=AY2324S1-CS2113-F11-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +## Contributions + +### The ExerciseLog Class +The way that I contributed to the project is by creating and implementing the ExerciseLog class and its associated classes. For example, +there are specific commands that users who want to interact with the exercise log can use, which are the log, delete, update, and view commands. +This gives the user full CRUD functionality of the exercise log. + +### Documentation +I contributed to the UG in the parts that are related to the ExerciseLog class and the introduction. In the DG, I contributed to the `Update Exercises from Log feature` section and all the diagrams in that section. + +### Team-Based Tasks +* Set up milestone v1.0 +* Upload v1.0 and v2.0 jar files to Github +* Enable assertions in gradle +* Update UG for ExerciseLog-related functionality +* Fix errors whenever CI/CD checks were not passed +* Correct grammar errors in documentation \ No newline at end of file diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 5c74e68d59..6567840b1f 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,21 +1,139 @@ package seedu.duke; -import java.util.Scanner; +import java.util.ArrayList; +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.commands.ExitCommand; +import seedu.duke.commands.meal.MealCommand; +import seedu.duke.goal.GoalList; +import seedu.duke.meal.Meal; +import seedu.duke.parser.Parser; +import seedu.duke.exerciselog.Log; +import seedu.duke.storagefile.AchmStorage; +import seedu.duke.storagefile.DataManager; +import seedu.duke.storagefile.GoalStorage; +import seedu.duke.ui.TextUi; +import seedu.duke.storagefile.ExerciseLogStorage; + +/** + * Entry point of the Address Book application. + * Initializes the application and starts the interaction with the user. + */ public class Duke { + + /** + * Version info of the program. + */ + public static final String VERSION = "Version-2.1"; + public static Log exerciseLog = new Log(); + public static GoalList goalList = new GoalList(); + public static GoalList achievedGoals = new GoalList(); + public static ExerciseLogStorage exerciseLogStorage; + public static TextUi ui; + public static GoalStorage goalStorage; + public static AchmStorage achmStorage; + private final String dirPath = "data"; + private final String exerciseLogFilePath = "./data/ExerciseLog.txt"; + private final String goalFilePath = "./data/GoalRecord.txt"; + private final String achmFilePath = "./data/Achievement.txt"; + private final String mealSavePath = "Meal.json"; + private static ArrayList meals = new ArrayList(); + + public static void main(String... launchArgs) { + new Duke().run(launchArgs); + } + + /** + * @param launchArgs + * Runs the program until termination. + */ + public void run(String[] launchArgs) { + start(launchArgs); + runCommandLoopUntilExitCommand(); + exit(); + } + + /** + * Sets up the required objects, loads up the data from the storage file, and + * prints the welcome message. + * + * @param launchArgs arguments supplied by the user at program launch + */ + private void start(String[] launchArgs) { + try { + ui = new TextUi(); + exerciseLogStorage = ExerciseLogStorage.initializeStorage(dirPath, exerciseLogFilePath); + exerciseLogStorage.checkForLogTextFile(exerciseLog); + goalStorage = GoalStorage.initializeGoalStorage(dirPath, goalFilePath); + goalStorage.restoreGoalRecord(); + achmStorage = AchmStorage.initializeGoalStorage(dirPath, achmFilePath); + achmStorage.restoreGoalRecord(); + ui.showWelcomeMessage(VERSION, "storage.getPath()"); + DataManager.setRelativePath(mealSavePath); + String dataJson = DataManager.readData(); + ArrayList data = DataManager.convertFromJsonToMealList(dataJson); + if (data != null) { + meals = data; + } + MealCommand.setMeals(meals); + } catch (Exception e) { // TODO: change to specific storage exceptions later + ui.showInitFailedMessage(); + throw new RuntimeException(e); + } + } + + /** + * Prints the Goodbye message and exits. + */ + private void exit() { + ui.showGoodbyeMessage(); + try { + DataManager.saveData(DataManager.convertToJson(meals)); + } catch (Exception exception) { + ui.showToUser(exception.toString()); + } + System.exit(0); + } + + /** + * Reads the user command and executes it, until the user issues the exit + * command. + * The command will be formatted before execution by following: + * 1. change to lower case + * 2. remove leading and ending whitespace + * 3. remove consecutive white space between words + */ + private void runCommandLoopUntilExitCommand() { + Command command; + do { + try { + String userCommandText = ui.getUserCommand(); + command = new Parser().parseCommand(userCommandText.toLowerCase().trim().replaceAll("\\s+", " ")); + CommandResult result = executeCommand(command); + ui.showResultToUser(result); + if (ExitCommand.isExit(command)) { + break; + } + } catch (Exception e) { + ui.showToUser(e.getMessage()); + } + } while (true); + } + /** - * Main entry-point for the java.duke.Duke application. + * Executes the command and returns the result. + * + * @param command user command + * @return result of the command */ - 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()); + private CommandResult executeCommand(Command command) { + try { + CommandResult result = command.execute(); + return result; + } catch (Exception e) { + throw new RuntimeException(e); + } } + } diff --git a/src/main/java/seedu/duke/TipsException.java b/src/main/java/seedu/duke/TipsException.java new file mode 100644 index 0000000000..47b9ac1e06 --- /dev/null +++ b/src/main/java/seedu/duke/TipsException.java @@ -0,0 +1,26 @@ +package seedu.duke; + +/** + * Any excption will be throw in this type, which contains information about + * this exception and the possible solution. + * It's suggested to use toString() to show the content of this exception so + * that the tester can have a clear view of the error reason and the possible + * solution to solve this error. + */ +public class TipsException extends Exception { + public String error, tips; + + /** + * Creating a new Tips Exception. + */ + public TipsException(String error, String tips) { + this.error = error; + this.tips = tips; + } + + @Override + public String toString() { + return "Error: " + error + + "\nTips: " + tips; + } +} diff --git a/src/main/java/seedu/duke/commands/ByeCommand.java b/src/main/java/seedu/duke/commands/ByeCommand.java new file mode 100644 index 0000000000..a4ac9f949f --- /dev/null +++ b/src/main/java/seedu/duke/commands/ByeCommand.java @@ -0,0 +1,39 @@ +package seedu.duke.commands; + +/** + * Commands for Terminating the Program. + * + * @see #COMMAND_WORD + */ +public class ByeCommand extends Command { + + public static final String COMMAND_WORD = "bye"; + protected static final int validArgumentAmount = 0; + + /** + * Create a new Command. + * + * @param arguments The specified arguments will be used for creating command, + * it will automatically check whethere the arguments are + * valid. + * @see #validArgumentAmount + * @throws TipsException Any excption will be throw in this type, which contains + * information about this exception and the possible + * solution. + */ + public ByeCommand(String[] arguments) throws Exception { + // super(COMMAND_WORD, validArgumentAmount); + // checkArguments(arguments); + } + + // @Override + public String customFunction() { + // runningState.exit(); + return ""; + } + + // @Override + public CommandResult getCommandResult(String content) { + return new CommandResult(" Bye. Hope to see you again soon!"); + } +} diff --git a/src/main/java/seedu/duke/commands/Command.java b/src/main/java/seedu/duke/commands/Command.java new file mode 100644 index 0000000000..ba325237d3 --- /dev/null +++ b/src/main/java/seedu/duke/commands/Command.java @@ -0,0 +1,19 @@ +package seedu.duke.commands; + +/** + * Represents an executable command. + */ +public class Command { + protected String userCommand; + + public Command() { + } + + public Command(String cmd) { + this.userCommand = cmd; + } + + public CommandResult execute() throws Exception { + throw new UnsupportedOperationException("This method is to be implemented by child classes"); + } +} diff --git a/src/main/java/seedu/duke/commands/CommandResult.java b/src/main/java/seedu/duke/commands/CommandResult.java new file mode 100644 index 0000000000..d7dd07ca73 --- /dev/null +++ b/src/main/java/seedu/duke/commands/CommandResult.java @@ -0,0 +1,41 @@ +package seedu.duke.commands; + +import java.util.List; +import java.util.Optional; + +import seedu.duke.data.Printable; + +/** + * Represents the result of a command execution. + */ +public class CommandResult { + + /** + * The feedback message to be shown to the user. Contains a description of the execution result + */ + public final String feedbackToUser; + + /** + * The list of objects that was produced by the command + */ + private final List relevantItems; + + public CommandResult(String feedbackToUser) { + this.feedbackToUser = feedbackToUser; + relevantItems = null; + } + + public CommandResult(String feedbackToUser, List relevantItems) { + this.feedbackToUser = feedbackToUser; + this.relevantItems = relevantItems; + } + + /** + * Returns a list of objects relevant to the command result, if any. + * @return relevant items + */ + public Optional> getRelevantItems() { + return Optional.ofNullable(relevantItems); + } + +} diff --git a/src/main/java/seedu/duke/commands/ExitCommand.java b/src/main/java/seedu/duke/commands/ExitCommand.java new file mode 100644 index 0000000000..3d23ab7bee --- /dev/null +++ b/src/main/java/seedu/duke/commands/ExitCommand.java @@ -0,0 +1,22 @@ +package seedu.duke.commands; + +/** + * Terminates the program. + */ +public class ExitCommand extends Command { + + public static final String COMMAND_WORD = "exit"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Exits the program.\n" + + "Example: " + COMMAND_WORD; + public static final String MESSAGE_EXIT_ACKNOWEDGEMENT = "Exiting FITNUS as requested ..."; + + @Override + public CommandResult execute() { + return new CommandResult(MESSAGE_EXIT_ACKNOWEDGEMENT); + } + + public static boolean isExit(Command command) { + return command instanceof ExitCommand; // instanceof returns false if it is null + } +} diff --git a/src/main/java/seedu/duke/commands/HelpCommand.java b/src/main/java/seedu/duke/commands/HelpCommand.java new file mode 100644 index 0000000000..203c32adbd --- /dev/null +++ b/src/main/java/seedu/duke/commands/HelpCommand.java @@ -0,0 +1,39 @@ +package seedu.duke.commands; + +import seedu.duke.commands.goal.*; +import seedu.duke.commands.logcommands.LogCommand; +import seedu.duke.commands.logcommands.ViewLogCommand; +import seedu.duke.commands.logcommands.UpdateLogCommand; +import seedu.duke.commands.logcommands.DeleteLogCommand; +import seedu.duke.commands.meal.*; + +/** + * Shows help instructions. + */ +public class HelpCommand extends Command { + + public static final String COMMAND_WORD = "help"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + + "\tExample: " + COMMAND_WORD; + + @Override + public CommandResult execute() { + String helpMsg = HelpCommand.MESSAGE_USAGE + "\n"; + + return new CommandResult( + helpMsg + + "\n" + LogCommand.MESSAGE_USAGE + + "\n" + DeleteLogCommand.MESSAGE_USAGE + + "\n" + UpdateLogCommand.MESSAGE_USAGE + + "\n" + ViewLogCommand.MESSAGE_USAGE + + "\n" + DeleteLogCommand.MESSAGE_USAGE + + "\n" + ViewGoalCommand.MESSAGE_USAGE + + "\n" + AchieveGoalCommand.MESSAGE_USAGE + + "\n" + AchievementCommand.MESSAGE_USAGE + + "\n" + AddCommand.MESSAGE_USAGE + + "\n" + DeleteCommand.MESSAGE_USAGE + + "\n" + ListCommand.MESSAGE_USAGE + + "\n" + ExitCommand.MESSAGE_USAGE); + } +} diff --git a/src/main/java/seedu/duke/commands/IncorrectCommand.java b/src/main/java/seedu/duke/commands/IncorrectCommand.java new file mode 100644 index 0000000000..2a182d1a94 --- /dev/null +++ b/src/main/java/seedu/duke/commands/IncorrectCommand.java @@ -0,0 +1,20 @@ +package seedu.duke.commands; + + +/** + * Represents an incorrect command. Upon execution, produces some feedback to the user. + */ +public class IncorrectCommand extends Command { + + public final String feedbackToUser; + + public IncorrectCommand(String feedbackToUser) { + this.feedbackToUser = feedbackToUser; + } + + @Override + public CommandResult execute() { + return new CommandResult(feedbackToUser); + } + +} diff --git a/src/main/java/seedu/duke/commands/goal/AchieveGoalCommand.java b/src/main/java/seedu/duke/commands/goal/AchieveGoalCommand.java new file mode 100644 index 0000000000..8b24ee7c3d --- /dev/null +++ b/src/main/java/seedu/duke/commands/goal/AchieveGoalCommand.java @@ -0,0 +1,33 @@ +package seedu.duke.commands.goal; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; + +import seedu.duke.goal.GoalList; +import seedu.duke.data.exception.IncorrectFormatException; + +public class AchieveGoalCommand extends Command { + public static final String COMMAND_WORD = "achieve"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Achieve a goal from the current goal list.\n" + + "\tExample: " + COMMAND_WORD + " 2"; + public String feedbackToUser; + + public AchieveGoalCommand(String cmd) { + super(cmd); + } + + @Override + public CommandResult execute() { + try { + feedbackToUser = GoalList.achieveGoal(this.userCommand); + + } catch (NumberFormatException nfe) { + feedbackToUser = "Please use a valid arabic number as index."; + } catch (IncorrectFormatException ife) { + feedbackToUser = ife.getMessage(); + } catch (Exception e) { + feedbackToUser = "Something went wrong, please try again."; + } + return new CommandResult(feedbackToUser); + } +} diff --git a/src/main/java/seedu/duke/commands/goal/AchievementCommand.java b/src/main/java/seedu/duke/commands/goal/AchievementCommand.java new file mode 100644 index 0000000000..5d4a45b629 --- /dev/null +++ b/src/main/java/seedu/duke/commands/goal/AchievementCommand.java @@ -0,0 +1,33 @@ +package seedu.duke.commands.goal; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.goal.GoalList; +import seedu.duke.data.exception.IncorrectFormatException; +import seedu.duke.ui.TextUi; + +public class AchievementCommand extends Command { + public static final String COMMAND_WORD = "achievement"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Show all your achievements\n" + + "\tExample: " + COMMAND_WORD; + public String feedbackToUser; + + public AchievementCommand(String cmd) { + super(cmd); + } + + @Override + public CommandResult execute() { + try { + GoalList.verifyViewAchievementInput(this.userCommand); + feedbackToUser = TextUi.showAchievement(); + + } catch (IncorrectFormatException ife) { + feedbackToUser = ife.getMessage(); + } catch (Exception e) { + feedbackToUser = "Something went wrong, please try again."; + } + + return new CommandResult(feedbackToUser); + } +} diff --git a/src/main/java/seedu/duke/commands/goal/DeleteGoalCommand.java b/src/main/java/seedu/duke/commands/goal/DeleteGoalCommand.java new file mode 100644 index 0000000000..02e2f08f60 --- /dev/null +++ b/src/main/java/seedu/duke/commands/goal/DeleteGoalCommand.java @@ -0,0 +1,43 @@ +package seedu.duke.commands.goal; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.goal.GoalList; +import seedu.duke.data.exception.IncorrectFormatException; + +import java.io.IOException; + +public class DeleteGoalCommand extends Command { + public static final String COMMAND_WORD = "deleteg"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Delete an goal from the current goal list.\n" + + "\tExample: " + COMMAND_WORD + " 1"; + public String feedbackToUser; + + public DeleteGoalCommand(String cmd) { + super(cmd); + } + + /** + * execute to remove a goal object from global goal list, by indexing + * If failed to delete a goal, tells user the specific problem. + * @return feedback to user of either success or fail + */ + @Override + public CommandResult execute() { + try { + feedbackToUser = GoalList.deleteGoal(this.userCommand); + + } catch (NumberFormatException nfe) { + feedbackToUser = "Please input a valid number for delete index."; + } catch (IncorrectFormatException ife) { + feedbackToUser = ife.getMessage(); + } catch (IOException io) { + feedbackToUser = "Failed to save data. Please check the output file and restart the app."; + } catch (Exception e) { + feedbackToUser = "Something went wrong, please try again."; + } + + + return new CommandResult(feedbackToUser); + } +} diff --git a/src/main/java/seedu/duke/commands/goal/GoalCommand.java b/src/main/java/seedu/duke/commands/goal/GoalCommand.java new file mode 100644 index 0000000000..34105c2350 --- /dev/null +++ b/src/main/java/seedu/duke/commands/goal/GoalCommand.java @@ -0,0 +1,46 @@ +package seedu.duke.commands.goal; + +import seedu.duke.Duke; +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.goal.GoalList; +import seedu.duke.data.exception.IncorrectFormatException; + +import java.io.IOException; + + +public class GoalCommand extends Command { + + public static final String COMMAND_WORD = "set"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Add a new goal into the goal list\n" + + "\tExample: " + COMMAND_WORD + " 123 on 18/12/2023"; + public String feedbackToUser; + + + public GoalCommand(String cmd) { + super(cmd); + } + + /** + * The execution of goalCommand new a goal record into the goal list. + * If error occurs in creating goal record, possibly includes + * incorrect format of command or invalid number is input. + * @return result of adding goal successfully message + */ + @Override + public CommandResult execute() { + try { + feedbackToUser = GoalList.addGoal(this.userCommand, Duke.goalList, Duke.goalStorage); + } catch (IncorrectFormatException ife) { + feedbackToUser = ife.getMessage(); + } catch (NumberFormatException nfe) { + feedbackToUser = "Please input a valid number for calories."; + } catch (IOException io) { + feedbackToUser = "Failed to save data. Please check the output file and restart the app."; + } catch (Exception e) { + feedbackToUser = "Something went wrong, please try again."; + } + + return new CommandResult(feedbackToUser); + } +} diff --git a/src/main/java/seedu/duke/commands/goal/ViewGoalCommand.java b/src/main/java/seedu/duke/commands/goal/ViewGoalCommand.java new file mode 100644 index 0000000000..cc3038f6da --- /dev/null +++ b/src/main/java/seedu/duke/commands/goal/ViewGoalCommand.java @@ -0,0 +1,33 @@ +package seedu.duke.commands.goal; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.goal.GoalList; +import seedu.duke.data.exception.IncorrectFormatException; +import seedu.duke.ui.TextUi; + +public class ViewGoalCommand extends Command { + + public static final String COMMAND_WORD = "viewg"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": List out the current goal list to see.\n" + + "\tExample: " + COMMAND_WORD; + public String feedbackToUser; + + public ViewGoalCommand(String cmd) { + super(cmd); + } + + @Override + public CommandResult execute() { + try { + GoalList.verifyViewGoalInput(this.userCommand); + feedbackToUser = TextUi.showGoalList(); + } catch (IncorrectFormatException ife) { + feedbackToUser = ife.getMessage(); + } catch (Exception e) { + feedbackToUser = "Something went wrong, please try again."; + } + + return new CommandResult(feedbackToUser); + } +} diff --git a/src/main/java/seedu/duke/commands/logcommands/DeleteLogCommand.java b/src/main/java/seedu/duke/commands/logcommands/DeleteLogCommand.java new file mode 100644 index 0000000000..f98cb2e4e9 --- /dev/null +++ b/src/main/java/seedu/duke/commands/logcommands/DeleteLogCommand.java @@ -0,0 +1,68 @@ +package seedu.duke.commands.logcommands; + +import java.io.IOException; +import java.util.List; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.Duke; +import seedu.duke.data.exception.IncorrectFormatException; + +public class DeleteLogCommand extends Command { + public static final String COMMAND_WORD = "deletelog"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Delete an exercise from the exercise log.\n" + + "\tExample: " + COMMAND_WORD + " 1 2 Running 68"; + public String feedbackToUser; + private List exerciseDetails; + + public DeleteLogCommand(List exerciseDetails) { + super(); + this.exerciseDetails = exerciseDetails; + } + + /** + * Extracts the details of the delete command and deletes the specified exercise. + * + * @return CommandResult that tells the user whether an exercise was successfully removed. + * @throws IncorrectFormatException when the command is not entered with the right type of parameters. + */ + public CommandResult execute() throws IncorrectFormatException, IOException { + if (exerciseDetails.size() < 4) { + throw new IncorrectFormatException("The delete log command needs to take at least 4 parameters!"); + } + + try { + int month = Integer.parseInt(exerciseDetails.get(0)); + if (month <= 0 || month > 12) { + throw new IncorrectFormatException("The month you specified does not exist."); + } + + int day = Integer.parseInt(exerciseDetails.get(1)); + if (day <= 0 || day > Duke.exerciseLog.getNumberOfDays(month)) { + throw new IncorrectFormatException("The day you specified does not exist for the month."); + } + + String exerciseName = ""; + for (int i = 2; i < exerciseDetails.size() - 1; i++) { + exerciseName += exerciseDetails.get(i) + " "; + } + + int caloriesBurned = Integer.parseInt(exerciseDetails.get(exerciseDetails.size() - 1)); + if (caloriesBurned < 0) { + throw new IncorrectFormatException("You cannot burn a negative number of calories."); + } + + feedbackToUser = Duke.exerciseLog.removeExercise(month, day, exerciseName.trim(), caloriesBurned) ? + "Successfully removed exercise!" : + "Could not find the specified exercise!"; + Duke.exerciseLogStorage.deleteFromStorage(month, day, exerciseName.trim().split(" "), caloriesBurned); + + return new CommandResult(feedbackToUser); + } catch (NumberFormatException e) { + throw new IncorrectFormatException("Please specify reasonable positive numbers in the month, day, and " + + "calories burned fields"); + } catch (IOException e) { + throw new IOException("The ExerciseLog.txt file could not be found."); + } + } +} diff --git a/src/main/java/seedu/duke/commands/logcommands/LogCommand.java b/src/main/java/seedu/duke/commands/logcommands/LogCommand.java new file mode 100644 index 0000000000..4efceefd48 --- /dev/null +++ b/src/main/java/seedu/duke/commands/logcommands/LogCommand.java @@ -0,0 +1,72 @@ +package seedu.duke.commands.logcommands; + +import java.io.IOException; +import java.util.List; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.Duke; +import seedu.duke.data.exception.IncorrectFormatException; + +public class LogCommand extends Command { + public static final String COMMAND_WORD = "log"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Logs an exercise into the exercise log.\n" + + "\tExample: " + COMMAND_WORD + " 1 2 Football 89"; + public String feedbackToUser; + private List exerciseDetails; + + public LogCommand() { + super(); + feedbackToUser = "Successfully added exercise to log:\n\t"; + } + + public LogCommand(List exerciseDetails) { + super(); + feedbackToUser = "Successfully added exercise to log:\n\t"; + this.exerciseDetails = exerciseDetails; + } + + /** + * Extracts the details of the log command and logs the exercise with its specific details. + * + * @return CommandResult telling the user that the exercise was successfully added along with the exercise details. + * @throws IncorrectFormatException when the command is not entered with the right type of parameters. + */ + public CommandResult execute() throws IncorrectFormatException, IOException { + if (exerciseDetails.size() < 4) { + throw new IncorrectFormatException("The log command needs to take at least 4 parameters!"); + } + + try { + int month = Integer.parseInt(exerciseDetails.get(0)); + if (month <= 0 || month > 12) { + throw new IncorrectFormatException("The month you specified does not exist."); + } + + int day = Integer.parseInt(exerciseDetails.get(1)); + if (day <= 0 || day > Duke.exerciseLog.getNumberOfDays(month)) { + throw new IncorrectFormatException("The day you specified does not exist for the month."); + } + + String exerciseName = ""; + for (int i = 2; i < exerciseDetails.size() - 1; i++) { + exerciseName += exerciseDetails.get(i) + " "; + } + + int caloriesBurned = Integer.parseInt(exerciseDetails.get(exerciseDetails.size() - 1)); + if (caloriesBurned < 0) { + throw new IncorrectFormatException("You cannot burn a negative number of calories."); + } + + String exerciseDescription = Duke.exerciseLog.addExercise(month, day, exerciseName.trim(), caloriesBurned); + Duke.exerciseLogStorage.writeToStorage(month, day, exerciseName.trim().split(" "), caloriesBurned); + + return new CommandResult((feedbackToUser + exerciseDescription).trim()); + } catch (NumberFormatException e) { + throw new IncorrectFormatException("Please specify reasonable positive numbers in the month, day, and " + + "calories burned fields"); + } catch (IOException e) { + throw new IOException("The ExerciseLog.txt file could not be found."); + } + } +} diff --git a/src/main/java/seedu/duke/commands/logcommands/UpdateLogCommand.java b/src/main/java/seedu/duke/commands/logcommands/UpdateLogCommand.java new file mode 100644 index 0000000000..eba8a8f164 --- /dev/null +++ b/src/main/java/seedu/duke/commands/logcommands/UpdateLogCommand.java @@ -0,0 +1,98 @@ +package seedu.duke.commands.logcommands; + +import java.io.IOException; +import java.util.List; +import java.util.Scanner; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.Duke; +import seedu.duke.data.exception.IncorrectFormatException; + +public class UpdateLogCommand extends Command { + public static final String COMMAND_WORD = "updatelog"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Updates an exercise from the exercise log.\n" + + "\tExample: " + COMMAND_WORD + " 1 2 Football 89" + "\n" + "\tPlease specify the new exercise details:\n" + + "\tSquash 44"; + public String feedbackToUser; + private List exerciseDetails; + + public UpdateLogCommand() { + super(); + } + + public UpdateLogCommand(List exerciseDetails) { + super(); + this.exerciseDetails = exerciseDetails; + } + + /** + * Extracts the details of the update command and updates the specified exercise. + * + * @return CommandResult that tells the user whether an exercise was successfully updated. + * @throws IncorrectFormatException when the command is not entered with the right type of parameters. + */ + public CommandResult execute() throws IncorrectFormatException, IOException { + if (exerciseDetails.size() < 4) { + throw new IncorrectFormatException("The update log command needs to take at least 4 parameters!"); + } + + try { + int month = Integer.parseInt(exerciseDetails.get(0)); + if (month <= 0 || month > 12) { + throw new IncorrectFormatException("The month you specified does not exist."); + } + + int day = Integer.parseInt(exerciseDetails.get(1)); + if (day <= 0 || day > Duke.exerciseLog.getNumberOfDays(month)) { + throw new IncorrectFormatException("The day you specified does not exist for the month."); + } + + String oldExerciseName = ""; + for (int i = 2; i < exerciseDetails.size() - 1; i++) { + oldExerciseName += exerciseDetails.get(i) + " "; + } + + int oldCaloriesBurned = Integer.parseInt(exerciseDetails.get(exerciseDetails.size() - 1)); + if (oldCaloriesBurned < 0) { + throw new IncorrectFormatException("You cannot burn a negative number of calories."); + } + + if (!Duke.exerciseLog.hasExercise(month, day, oldExerciseName.trim(), oldCaloriesBurned)) { + feedbackToUser = "Could not find exercise to update."; + return new CommandResult(feedbackToUser); + } + + Scanner scanner = new Scanner(System.in); + System.out.println("Please specify the new exercise details:"); + String newExerciseString = scanner.nextLine(); + String[] newExerciseDetails = newExerciseString.split(" "); + + if (newExerciseDetails.length < 2) { + throw new IncorrectFormatException("The new exercise needs to have a name and calories burned!"); + } + + String newExerciseName = ""; + for (int i = 0; i < newExerciseDetails.length - 1; i++) { + newExerciseName += newExerciseDetails[i] + " "; + } + + int newCaloriesBurned = Integer.parseInt(newExerciseDetails[newExerciseDetails.length - 1]); + if (newCaloriesBurned < 0) { + throw new IncorrectFormatException("You cannot burn a negative number of calories."); + } + + feedbackToUser = Duke.exerciseLog.updateExercise(month, day, oldExerciseName.trim(), oldCaloriesBurned, + newExerciseName.trim(), newCaloriesBurned) ? "Exercise successfully updated!" : + "Could not find exercise to update."; + Duke.exerciseLogStorage.updateInStorage(month, day, + oldExerciseName.trim().split(" "), oldCaloriesBurned, + newExerciseName.trim().split(" "), newCaloriesBurned); + + return new CommandResult(feedbackToUser); + } catch (NumberFormatException e) { + throw new IncorrectFormatException("Please specify reasonable positive numbers in the month, day, and " + + "calories burned fields"); + } + } +} diff --git a/src/main/java/seedu/duke/commands/logcommands/ViewLogCommand.java b/src/main/java/seedu/duke/commands/logcommands/ViewLogCommand.java new file mode 100644 index 0000000000..7545b88a7d --- /dev/null +++ b/src/main/java/seedu/duke/commands/logcommands/ViewLogCommand.java @@ -0,0 +1,138 @@ +package seedu.duke.commands.logcommands; + +import java.util.List; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.Duke; +import seedu.duke.data.exception.IncorrectFormatException; + + +enum ViewType { + TOTAL, + EXERCISES; +} + +enum ViewScope { + ALL, + MONTH, + DAY; +} + +public class ViewLogCommand extends Command { + public static final String COMMAND_WORD = "viewlog"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays exercises from the exercise log.\n" + + "\tExample: " + COMMAND_WORD + " exercises all" + " | " + COMMAND_WORD + "total all"; + private ViewType viewType; + private ViewScope viewScope; + private int month; + private int day; + + public ViewLogCommand() { + super(); + } + + /** + * Assigns the view attribute a specific enum based on the scope at which the user wants to view the exercise log. + * + * @param viewArgs the details of the scope at which the user wants to view the exercise log. + * @throws IncorrectFormatException when the command is not entered with the right type of parameters. + */ + public ViewLogCommand(List viewArgs) throws IncorrectFormatException { + super(); + if (viewArgs.size() < 2) { + throw new IncorrectFormatException("Please specify both a ViewType and ViewScope"); + } + switch (viewArgs.get(0)) { + case "total": + viewType = ViewType.TOTAL; + break; + case "exercises": + viewType = ViewType.EXERCISES; + break; + default: + throw new IncorrectFormatException("Incorrect or no ViewType specified."); + } + + if (viewArgs.get(1).equals("all") || viewArgs.get(1).equals("month")) { + switch (viewArgs.size()) { + case 2: + viewScope = ViewScope.ALL; + break; + case 3: + try { + viewScope = ViewScope.MONTH; + month = Integer.parseInt(viewArgs.get(2)); + if (month <= 0 || month > 12) { + throw new IncorrectFormatException("The month you specified does not exist."); + } + break; + } catch (NumberFormatException e) { + throw new IncorrectFormatException("Please specify reasonable positive numbers in the " + + "month, day, and calories burned fields"); + } + case 5: + try { + viewScope = ViewScope.DAY; + month = Integer.parseInt(viewArgs.get(2)); + if (month <= 0 || month > 12) { + throw new IncorrectFormatException("The month you specified does not exist."); + } + day = Integer.parseInt(viewArgs.get(4)); + if (day <= 0 || day > Duke.exerciseLog.getNumberOfDays(month)) { + throw new IncorrectFormatException("The day you specified does not exist for the month."); + } + break; + } catch (NumberFormatException e) { + throw new IncorrectFormatException("Please specify reasonable positive numbers in the " + + "month, day, and calories burned fields"); + } + default: + throw new IncorrectFormatException("Incorrect view command format."); + } + } else { + throw new IncorrectFormatException("Please specify a proper ViewScope."); + } + } + + /** + * A different message is returned depending on the scope at which a user wants to view their exercises. + * + * @return CommandResult telling the user their total number of exercises in total, for a month, or for a day. + */ + public CommandResult execute() { + String result; + if (viewType == ViewType.TOTAL) { + switch (viewScope) { + case ALL: + result = Integer.toString(Duke.exerciseLog.getNumberOfExercises()); + return new CommandResult("Here are the total number of exercises you have logged: " + + result); + case MONTH: + result = Integer.toString(Duke.exerciseLog.getNumberOfExercisesForMonth(month)); + return new CommandResult("Here are the total number of exercises for that month: " + + result); + case DAY: + result = Integer.toString(Duke.exerciseLog.getNumberOfExercisesForDay(month, day)); + return new CommandResult("Here are the total number of exercises for that day: " + + result); + default: + return new CommandResult("Invalid search type"); + } + } else { + switch (viewScope) { + case ALL: + result = Duke.exerciseLog.toString().stripTrailing(); + return new CommandResult("Here are all the exercises:\n" + result); + case MONTH: + result = Duke.exerciseLog.getMonth(month).toString().stripTrailing(); + return new CommandResult("Here are the exercises for the month:\n" + result); + case DAY: + result = Duke.exerciseLog.getDay(month, day).toString().stripTrailing(); + return new CommandResult("Here are the exercises for the day:\n" + result); + default: + return new CommandResult("Invalid search type"); + } + } + } +} diff --git a/src/main/java/seedu/duke/commands/meal/AddCommand.java b/src/main/java/seedu/duke/commands/meal/AddCommand.java new file mode 100644 index 0000000000..7e045ccffb --- /dev/null +++ b/src/main/java/seedu/duke/commands/meal/AddCommand.java @@ -0,0 +1,37 @@ +package seedu.duke.commands.meal; + +import seedu.duke.commands.CommandResult; +import seedu.duke.meal.*; +import seedu.duke.data.Date; + +import java.util.List; +import java.util.Locale.Category; + +public class AddCommand extends MealCommand { + public static final String COMMAND_WORD = "meal_add"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Add a meal and record the amount of calories.\n" + + "\tExample: " + COMMAND_WORD + " potato 15"; + private static final int[] validArgumentAmounts = new int[] { 3, 4 }; + private final String name; + private final int calories; + private final String category; + private final Date time; + + public AddCommand(List arguments) throws Exception { + checkArgument(arguments, validArgumentAmounts); + name = arguments.get(0); + calories = Integer.parseInt(arguments.get(1)); + category = arguments.get(2); + if (arguments.size() >= 4) { + time = new Date(arguments.get(3), false); + } else { + time = Date.now(); + } + } + + @Override + public CommandResult execute() throws Exception { + meals.add(new Meal(name, calories, category, time)); + return new CommandResult("Successfully add meal " + meals.get(meals.size() - 1) + "!"); + } +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/commands/meal/DeleteCommand.java b/src/main/java/seedu/duke/commands/meal/DeleteCommand.java new file mode 100644 index 0000000000..32c6082a41 --- /dev/null +++ b/src/main/java/seedu/duke/commands/meal/DeleteCommand.java @@ -0,0 +1,33 @@ +package seedu.duke.commands.meal; + +import seedu.duke.commands.CommandResult; +import seedu.duke.meal.*; + +import java.util.List; + +public class DeleteCommand extends MealCommand { + public static final String COMMAND_WORD = "meal_delete"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Delete a meal from the existing list.\n" + + "\tExample: " + COMMAND_WORD + " 2"; + private static final int[] validArgumentAmounts = new int[] { 1 }; + private final int index; + + public DeleteCommand(List arguments) throws Exception { + checkArgument(arguments, validArgumentAmounts); + index = Integer.parseInt(arguments.get(0)) - 1; + if (index <= -1) { + throw new Exception("Invalid index!"); + } + } + + @Override + public CommandResult execute() throws Exception { + if (meals.size() <= index) { + return new CommandResult("Exceeded index!"); + } + CommandResult result = new CommandResult( + "Successfully delete meal at index " + (index + 1) + "!\n" + meals.get(index)); + meals.remove(index); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/commands/meal/ListCommand.java b/src/main/java/seedu/duke/commands/meal/ListCommand.java new file mode 100644 index 0000000000..55d238e9a4 --- /dev/null +++ b/src/main/java/seedu/duke/commands/meal/ListCommand.java @@ -0,0 +1,55 @@ +package seedu.duke.commands.meal; + +import seedu.duke.commands.CommandResult; +import seedu.duke.meal.Category; +import seedu.duke.meal.CategoryParser; +import seedu.duke.meal.Meal; + +import java.util.ArrayList; +import java.util.List; + +public class ListCommand extends MealCommand { + public static final String COMMAND_WORD = "meal_list"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": List all the meals that have been recorded.\n" + + "\tExample: " + COMMAND_WORD; + private static final int[] validArgumentAmounts = new int[] { 0, 1 }; + private final Category category; + + public ListCommand(List arguments) throws Exception { + checkArgument(arguments, validArgumentAmounts); + if (arguments.size() == 1 && arguments.get(0) != "") { + category = CategoryParser.Parse(arguments.get(0)); + } else { + category = null; + } + } + + @Override + public CommandResult execute() throws Exception { + ArrayList selectedMeals = new ArrayList(); + for (int i = 0; i < meals.size(); i++) { + Meal meal = meals.get(i); + if (category == null || meal.category == category) { + selectedMeals.add(meal); + } + } + + if (selectedMeals.size() == 0) { + if (category == null) { + return new CommandResult("The meal list is empty!"); + } + return new CommandResult("The selected meal list with category '" + category.toString() + "' is empty!"); + } + String listString = ""; + int total = 0; + for (int i = 0; i < selectedMeals.size(); i++) { + Meal meal = selectedMeals.get(i); + listString += "\n" + (i + 1) + "." + meal.toString(); + total += meal.calories; + } + listString += "\nTotal calories: " + total; + return new CommandResult( + (category == null ? "Here's the meal list:" + : "Here's the selected meal list with category '" + category.toString() + "':") + listString); + } +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/commands/meal/MealCommand.java b/src/main/java/seedu/duke/commands/meal/MealCommand.java new file mode 100644 index 0000000000..74a4a1c276 --- /dev/null +++ b/src/main/java/seedu/duke/commands/meal/MealCommand.java @@ -0,0 +1,49 @@ +package seedu.duke.commands.meal; + +import java.util.List; +import java.util.ArrayList; + +import seedu.duke.commands.Command; +import seedu.duke.commands.CommandResult; +import seedu.duke.meal.*; + +public class MealCommand extends Command { + protected static ArrayList meals; + + public MealCommand() { + super(); + } + + public MealCommand(List meals) { + + } + + public static void setMeals(ArrayList meals) { + MealCommand.meals = meals; + } + + protected void checkArgument(List arguments, int[] validArgumentAmounts) throws Exception { + Boolean isValid = false; + if (arguments != null) { + int size = arguments.size(); + if (arguments.get(0) == "") { + size = 0; + } + for (int i = 0; i < validArgumentAmounts.length; i++) { + if (size == validArgumentAmounts[i]) { + isValid = true; + break; + } + } + } + + if (!isValid) { + throw new Exception("Incorrect amount of the arguments."); + } + } + + @Override + public CommandResult execute() throws Exception { + throw new UnsupportedOperationException("Meal command that hasn't been implemented."); + } +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/common/Messages.java b/src/main/java/seedu/duke/common/Messages.java new file mode 100644 index 0000000000..c2228cc56a --- /dev/null +++ b/src/main/java/seedu/duke/common/Messages.java @@ -0,0 +1,14 @@ +package seedu.duke.common; + +/** + * Container for user visible messages. + */ +public class Messages { + public static final String MESSAGE_GOODBYE = "Good bye!"; + public static final String MESSAGE_INIT_FAILED = "Failed to initialise address book application. Exiting..."; + public static final String MESSAGE_PROGRAM_LAUNCH_ARGS_USAGE = "Launch command format: " + + "java seedu.duke.Duke [STORAGE_FILE_PATH]"; + public static final String MESSAGE_WELCOME = "Welcome to your FITNUS journey!"; + public static final String MESSAGE_USING_STORAGE_FILE = "Using storage file : %1$s"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; +} diff --git a/src/main/java/seedu/duke/data/Date.java b/src/main/java/seedu/duke/data/Date.java new file mode 100644 index 0000000000..7b3dc7bf6a --- /dev/null +++ b/src/main/java/seedu/duke/data/Date.java @@ -0,0 +1,71 @@ +package seedu.duke.data; + +import seedu.duke.data.exception.IncorrectFormatException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * Customized class for showing date and parsing supported string to date. + */ +public class Date { + private static DateTimeFormatter[] formatters = new DateTimeFormatter[] { + DateTimeFormatter.ofPattern("yyyy/M/d"), + DateTimeFormatter.ofPattern("yyyy/MM/dd"), + DateTimeFormatter.ofPattern("dd/MM/yyyy"), + DateTimeFormatter.ofPattern("d/M/yyyy"), + DateTimeFormatter.ofPattern("yyyy-M-d"), + DateTimeFormatter.ofPattern("d-M-yyyy"), + DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH) }; + private static DateTimeFormatter toStringFormatter = formatters[formatters.length - 1]; + public String standardString; + transient LocalDate date; + + /** + * @param rawData refers to the date String + * @param NotAllowPast true for all goal related commands only + * @throws IncorrectFormatException if failed to parse date string input + */ + public Date(String rawData, Boolean notAllowPast) throws IncorrectFormatException { + setRawData(rawData, notAllowPast); + } + + /** + * The method is used to set up the date field of a Date object + * It contains the actual implementation to parse date information from a string + * + * @param rawData refers to a date string + * @param notAllowPast if past is not allowed, for example for goal functions + * @throws IncorrectFormatException if failed to parse date string input + */ + public void setRawData(String rawData, boolean notAllowPast) throws IncorrectFormatException { + for (DateTimeFormatter formatter : formatters) { + try { + date = LocalDate.parse(rawData, formatter); + if (notAllowPast) { + if (date.isBefore(LocalDate.now())) { + throw new IncorrectFormatException("Target Deadline has passed! "); + } + } + standardString = this.toString(); + return; + } catch (IncorrectFormatException ide) { + throw new IncorrectFormatException("Target Deadline has passed! "); + } catch (Exception exception) { + continue; + } + } + throw new IncorrectFormatException("Please use a valid date with correct format!"); + } + + @Override + public String toString() { + return date.format(toStringFormatter); + } + + public static Date now() throws Exception { + Date now = new Date(LocalDate.now().format(toStringFormatter), false); + return now; + } +} diff --git a/src/main/java/seedu/duke/data/DateTime.java b/src/main/java/seedu/duke/data/DateTime.java new file mode 100644 index 0000000000..2848099e86 --- /dev/null +++ b/src/main/java/seedu/duke/data/DateTime.java @@ -0,0 +1,92 @@ +package seedu.duke.data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +// import com.google.gson.annotations.SerializedName; + +/** + * Customized class for showing date time and parsing supported string to date + * time. + */ +public class DateTime { + private static DateTimeFormatter[] formatters = new DateTimeFormatter[] { + DateTimeFormatter.ofPattern("yyyy-M-d HHmm"), + DateTimeFormatter.ofPattern("yyyy-M-d H:m"), + DateTimeFormatter.ofPattern("M-d-yyyy HHmm"), + DateTimeFormatter.ofPattern("M-d-yyyy H:m"), + DateTimeFormatter.ofPattern("MMM d, yyyy HH:mm", Locale.ENGLISH), }; + private static DateTimeFormatter toStringFormatter = formatters[formatters.length - 1]; + public String standardString; + transient LocalDateTime dateTime; + + public DateTime(String rawData) throws Exception { + setRawData(rawData); + } + + public void setRawData(String rawData) throws Exception { + System.out.println(rawData); + for (DateTimeFormatter formatter : formatters) { + try { + dateTime = LocalDateTime.parse(rawData, formatter); + standardString = this.toString(); + return; + } catch (Exception exception) { + continue; + } + } + throw new Exception("Unable to parse date time!"); + } + + @Override + public String toString() { + return dateTime.format(toStringFormatter); + } + + /** + * Compare the dates of this DateTime and another DateTime. + * + * @param dateTime The other DateTime that is used to compare. + * @return If the date of current instance is earlier,it returns -1. + * If the date of current instance is later,it returns 1. + * Otherwise(when they're at exactly the same date) it returns 0. + */ + public int compareDate(DateTime dateTime) { + LocalDate date1 = this.dateTime.toLocalDate(); + LocalDate date2 = dateTime.dateTime.toLocalDate(); + if (date1.isBefore(date2)) { + return 1; + } + if (date1.isAfter(date2)) { + return -1; + } + return 0; + } + + /** + * Compare the dates of this DateTime and another Date. + * + * @param date The other Date that is used to compare. + * @return If the date of current instance is earlier,it returns -1. + * If the date of current instance is later,it returns 1. + * Otherwise(when they're at exactly the same date) it returns 0. + */ + public int compareDate(Date date) { + LocalDate date1 = this.dateTime.toLocalDate(); + LocalDate date2 = date.date; + if (date1.isBefore(date2)) { + return -1; + } + if (date1.isAfter(date2)) { + return 1; + } + return 0; + } + + public static DateTime now() throws Exception { + DateTime now = new DateTime(LocalDateTime.now().format(toStringFormatter)); + return now; + } +} diff --git a/src/main/java/seedu/duke/data/Printable.java b/src/main/java/seedu/duke/data/Printable.java new file mode 100644 index 0000000000..446da683e3 --- /dev/null +++ b/src/main/java/seedu/duke/data/Printable.java @@ -0,0 +1,5 @@ +package seedu.duke.data; + +public interface Printable { + String getAsText(); +} diff --git a/src/main/java/seedu/duke/data/exception/IllegalValueException.java b/src/main/java/seedu/duke/data/exception/IllegalValueException.java new file mode 100644 index 0000000000..315698dcff --- /dev/null +++ b/src/main/java/seedu/duke/data/exception/IllegalValueException.java @@ -0,0 +1,14 @@ +package seedu.duke.data.exception; + +/** + * Signals that some given data does not fulfill some constraints. + */ +public class IllegalValueException extends IncorrectFormatException { + + /** + * @param message should contain relevant information on the failed constraint(s) + */ + public IllegalValueException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/data/exception/IncorrectFormatException.java b/src/main/java/seedu/duke/data/exception/IncorrectFormatException.java new file mode 100644 index 0000000000..660ddf8522 --- /dev/null +++ b/src/main/java/seedu/duke/data/exception/IncorrectFormatException.java @@ -0,0 +1,9 @@ +package seedu.duke.data.exception; + +public class IncorrectFormatException extends Exception { + + public IncorrectFormatException(String message) { + super(message); + } + +} diff --git a/src/main/java/seedu/duke/exerciselog/Day.java b/src/main/java/seedu/duke/exerciselog/Day.java new file mode 100644 index 0000000000..91d76a0033 --- /dev/null +++ b/src/main/java/seedu/duke/exerciselog/Day.java @@ -0,0 +1,106 @@ +package seedu.duke.exerciselog; + +import java.util.ArrayList; + +public class Day { + private final ArrayList exercises; + + public Day() { + exercises = new ArrayList<>(); + } + + /** + * Instantiates and adds a new Exercise Object into the Day. + * + * @param exerciseName the name of the exercise to be added to the day + * @param caloriesBurned the number of calories burned during the exercise + * @return a string representing the new exercise + */ + public String addExercise(String exerciseName, int caloriesBurned) { + Exercise newExercise = new Exercise(exerciseName, caloriesBurned); + exercises.add(newExercise); + return newExercise.toString(); + } + + /** + * Attempts to remove an exercise with the specified details and returns whether or not removal was successful. + * + * @param exerciseName the name of the exercise to be removed + * @param caloriesBurned the number of calories burned during the exercise + * @return true if an exercise was successfully removed, false otherwise + */ + public boolean removeExercise(String exerciseName, int caloriesBurned) { + return exercises.remove(new Exercise(exerciseName, caloriesBurned)); + } + + /** + * Returns the exercise object at the specified index for the Day. + * + * @param index the number of the exercise the user wants to check on + * @return the exercise object at the specified index + */ + public Exercise getExercise(int index) { + return exercises.get(index - 1); + } + + /** + * Returns the total number of exercises logged for the day. + * + * @return the total number of exercises logged for the day. + */ + public Integer getNumberOfExercises() { + return exercises.size(); + } + + /** + * Returns whether the Day has exercises or not + * + * @return true if there are exercises, false if not + */ + public boolean containsExercises() { + return !exercises.isEmpty(); + } + + /** + * Given the specific details of an Exercise, finds the exercise in the log and updates it to the new specified + * details. + * + * @param oldExerciseName original name of the exercise to be updated + * @param oldCaloriesBurned original number of calories burned by the exercise to be updated + * @param newExerciseName the new name to update the exercise to + * @param newCaloriesBurned the new number of calories burned to update the exercise to + * @return true when an exercise is found and updated, false otherwise + */ + public boolean updateExercise(String oldExerciseName, int oldCaloriesBurned, String newExerciseName, + int newCaloriesBurned) { + int index = exercises.indexOf(new Exercise(oldExerciseName, oldCaloriesBurned)); + if (index == -1) { + return false; + } + + Exercise targetExercise = exercises.get(index); + targetExercise.setExerciseName(newExerciseName); + targetExercise.setCaloriesBurned(newCaloriesBurned); + return true; + } + + public boolean hasExercise(String exerciseName, int caloriesBurned) { + return exercises.contains(new Exercise(exerciseName, caloriesBurned)); + } + + /** + * Returns a string representing all the exercises contained for the Day. + * + * @return a string representing all the exercises for the day + */ + public String toString() { + if (exercises.isEmpty()) { + return "\tNO EXCERCISES FOR THIS DAY!\n"; + } + String newString = ""; + for (Exercise e: exercises) { + newString += "\t" + e.toString(); + } + return newString; + } +} diff --git a/src/main/java/seedu/duke/exerciselog/Exercise.java b/src/main/java/seedu/duke/exerciselog/Exercise.java new file mode 100644 index 0000000000..7d4af09b91 --- /dev/null +++ b/src/main/java/seedu/duke/exerciselog/Exercise.java @@ -0,0 +1,40 @@ +package seedu.duke.exerciselog; + +public class Exercise { + private String exerciseName; + private int caloriesBurned; + + public Exercise(String exerciseName, int caloriesBurned) { + this.exerciseName = exerciseName; + this.caloriesBurned = caloriesBurned; + } + + public String getExerciseName() { + return exerciseName; + } + + public int getCaloriesBurned() { + return caloriesBurned; + } + + public void setExerciseName(String newName) { + this.exerciseName = newName; + } + + public void setCaloriesBurned(int newCalories) { + this.caloriesBurned = newCalories; + } + + public String toString() { + return "Exercise: " + exerciseName + ", Calories Burned: " + Integer.toString(caloriesBurned) + " Calories\n"; + } + + public boolean equals(Object other) { + if (!(other instanceof Exercise)) { + return false; + } + Exercise otherExercise = (Exercise) other; + return this.exerciseName.equals(otherExercise.getExerciseName()) && + this.caloriesBurned == otherExercise.getCaloriesBurned(); + } +} diff --git a/src/main/java/seedu/duke/exerciselog/Log.java b/src/main/java/seedu/duke/exerciselog/Log.java new file mode 100644 index 0000000000..31f705872d --- /dev/null +++ b/src/main/java/seedu/duke/exerciselog/Log.java @@ -0,0 +1,143 @@ +package seedu.duke.exerciselog; + +import java.util.ArrayList; + +public class Log { + public static ArrayList exerciseLog; + + public Log() { + exerciseLog = new ArrayList<>(); + exerciseLog.add(new Month(31, "jan")); // Jan 31 + exerciseLog.add(new Month(29, "feb")); // Feb 29 + exerciseLog.add(new Month(31, "mar")); // Mar 31 + exerciseLog.add(new Month(30, "apr")); // Apr 30 + exerciseLog.add(new Month(31, "may")); // May 31 + exerciseLog.add(new Month(30, "jun")); // Jun 30 + exerciseLog.add(new Month(31, "jul")); // Jul 31 + exerciseLog.add(new Month(31, "aug")); // Aug 31 + exerciseLog.add(new Month(30, "sep")); // Sep 30 + exerciseLog.add(new Month(31, "oct")); // Oct 31 + exerciseLog.add(new Month(30, "nov")); // Nov 30 + exerciseLog.add(new Month(31, "dec")); // Dec 31 + } + + /** + * Adds an exercise to the specified month and day with the specified name and number of calories burned. + * + * @param month the month of the exercise to be added to the log + * @param day the day of the month of the exercise to be added to the log + * @param exerciseName the name of the exercise to be added to the log + * @param caloriesBurned the number of calories burned by the exercise + * @return the string representation of the exercise that is added to the log + */ + public String addExercise(int month, int day, String exerciseName, int caloriesBurned) { + return exerciseLog.get(month - 1).addExercise(day - 1, exerciseName, caloriesBurned); + } + + /** + * Attempts to remove the specified exercise from the log and returns true only if removal is successful. + * + * @param month the month of the exercise to be deleted from the log + * @param day the day of the month of the exercise to be deleted from the log + * @param exerciseName the name of the exercise to be deleted from the log + * @param caloriesBurned the number of calories burned by the exercise + * @return true if the exercise was removed, false otherwise + */ + public boolean removeExercise(int month, int day, String exerciseName, int caloriesBurned) { + return exerciseLog.get(month - 1).removeExercise(day - 1, exerciseName, caloriesBurned); + } + + /** + * Returns the total number of exercises logged + * + * @return the total number of exercises logged + */ + public int getNumberOfExercises() { + int totalNumber = 0; + for (Month m: exerciseLog) { + totalNumber += m.getTotalNumberOfExercises(); + } + return totalNumber; + } + + /** + * Returns the total number of exercises logged for the specific month + * + * @param month the month to be checked + * @return the total number of exercises logged for the specific month + */ + public int getNumberOfExercisesForMonth(int month) { + return exerciseLog.get(month - 1).getTotalNumberOfExercises(); + } + + /** + * Returns the total number of exercises logged for the specific day of the specified month + * + * @param month the month to be checked + * @param day the day of the month to be checked + * @return the total number of exercises logged for a specific day of the month + */ + public int getNumberOfExercisesForDay(int month, int day) { + return exerciseLog.get(month - 1).getNumberOfExercisesForDay(day - 1); + } + + /** + * Given the specific details of an Exercise, finds the exercise in the log on the given month and day, and updates + * it to the new specified details. + * + * @param month the month of the exercise to be updated + * @param day the day of the month of the exercise to be updated + * @param oldExerciseName original name of the exercise to be updated + * @param oldCaloriesBurned original number of calories burned by the exercise to be updated + * @param newExerciseName the new name to update the exercise to + * @param newCaloriesBurned the new number of calories burned to update the exercise to + * @return true if an exercise is successfully updated, false otherwise + */ + public boolean updateExercise(int month, int day, String oldExerciseName, int oldCaloriesBurned, + String newExerciseName, int newCaloriesBurned) { + return exerciseLog.get(month - 1).updateExercise(day - 1, oldExerciseName, oldCaloriesBurned, newExerciseName, + newCaloriesBurned); + } + + public boolean hasExercise(int month, int day, String exerciseName, int caloriesBurned) { + return exerciseLog.get(month - 1).hasExercise(day - 1, exerciseName, caloriesBurned); + } + + public int getNumberOfDays(int month) { + return exerciseLog.get(month - 1).getNumberOfDays(); + } + + /** + * Returns the Month object of the specified month. + * + * @param month the month to be checked + * @return the Month object of the specified month + */ + public Month getMonth(int month) { + return exerciseLog.get(month - 1); + } + + /** + * Returns the Day object of the specified day. + * + * @param month the month to be checked + * @param day the day of the month to be checked + * @return the Day object of the specified month and day + */ + public Day getDay(int month, int day) { + return exerciseLog.get(month - 1).getDay(day - 1); + } + + /** + * The string representation of the exercise log, showing each month with their days and exercises + * + * @return the string representation of the exercise log + */ + public String toString() { + String newString = ""; + for (Month m: exerciseLog) { + newString += m.toString() + "\n"; + } + return newString; + } +} diff --git a/src/main/java/seedu/duke/exerciselog/Month.java b/src/main/java/seedu/duke/exerciselog/Month.java new file mode 100644 index 0000000000..78e7939836 --- /dev/null +++ b/src/main/java/seedu/duke/exerciselog/Month.java @@ -0,0 +1,128 @@ +package seedu.duke.exerciselog; + +import java.util.ArrayList; + +public class Month { + private final ArrayList dates; + private String name; + + public Month(int days, String name) { + this.name = name; + dates = new ArrayList(); + for (int i = 0; i < days; i++) { + dates.add(new Day()); + } + } + + /** + * Adds an exercise with the specified name and calories burned to the specified day. + * + * @param day the day to add the exercise to + * @param exerciseName the name of the exercise + * @param caloriesBurned the number of calories burned by the exercise + * @return the string representation of the new exercise created + */ + public String addExercise(int day, String exerciseName, int caloriesBurned) { + return dates.get(day).addExercise(exerciseName, caloriesBurned); + } + + /** + * Attempts to remove the specified exercise at the specified day of the month + * + * @param day the day to remove the exercise from + * @param exerciseName the name of the exercise to be removed + * @param caloriesBurned the number of calories burned by the exercise + * @return true if the exercise was removed, false otherwise + */ + public boolean removeExercise(int day, String exerciseName, int caloriesBurned) { + return dates.get(day).removeExercise(exerciseName, caloriesBurned); + } + + /** + * Returns the total number of exercises for the month + * + * @return the total number of exercises for the month + */ + public int getTotalNumberOfExercises() { + int totalExercises = 0; + for (Day d: dates) { + totalExercises += d.getNumberOfExercises(); + } + return totalExercises; + } + + /** + * Returns a Day object of the day specified + * + * @param day the number of the day to be returned + * @return a Day object of the specified day + */ + public Day getDay(int day) { + return dates.get(day); + } + + /** + * Returns the total number of exercises for the specified day + * + * @param day the day to be checked + * @return the total number of exercises for the specified day + */ + public int getNumberOfExercisesForDay(int day) { + return dates.get(day).getNumberOfExercises(); + } + + /** + * Given the specific details of an Exercise, finds the exercise in the log on the given day, and updates it to the + * new specified details. + * + * @param day the day of the month of the exercise to be updated + * @param oldExerciseName original name of the exercise to be updated + * @param oldCaloriesBurned original number of calories burned by the exercise to be updated + * @param newExerciseName the new name to update the exercise to + * @param newCaloriesBurned the new number of calories burned to update the exercise to + * @return true if an exercise is successfully updated, false otherwise + */ + public boolean updateExercise(int day, String oldExerciseName, int oldCaloriesBurned, String newExerciseName, + int newCaloriesBurned) { + return dates.get(day).updateExercise(oldExerciseName, oldCaloriesBurned, newExerciseName, + newCaloriesBurned); + } + + public boolean hasExercise(int day, String exerciseName, int caloriesBurned) { + return dates.get(day).hasExercise(exerciseName, caloriesBurned); + } + + /** + * Returns the name of the month + * + * @return the name of the month + */ + public String getName() { + return name; + } + + /** + * Returns the number of days in the month + * + * @return the number of days in the month + */ + public int getNumberOfDays() { + return dates.size(); + } + + /** + * Returns the string representation of the month by showing the name of the month and each of its days and + * exercises + * + * @return the string representation of the month + */ + public String toString() { + String newString = "Month: " + name.toUpperCase() + "\n"; + for (int i = 0; i < dates.size(); i++) { + String day = Integer.toString(i + 1); + newString += "Day " + day + ":\n"; + newString += dates.get(i).toString(); + } + return newString; + } +} diff --git a/src/main/java/seedu/duke/goal/GoalList.java b/src/main/java/seedu/duke/goal/GoalList.java new file mode 100644 index 0000000000..da441cf996 --- /dev/null +++ b/src/main/java/seedu/duke/goal/GoalList.java @@ -0,0 +1,210 @@ +package seedu.duke.goal; + +import seedu.duke.Duke; +import seedu.duke.data.Date; +import seedu.duke.data.exception.IllegalValueException; +import seedu.duke.data.exception.IncorrectFormatException; +import seedu.duke.storagefile.GoalStorage; +import seedu.duke.ui.TextUi; + +import java.io.IOException; +import java.util.ArrayList; + +public class GoalList extends ArrayList { + private static final String DATEKEYWORD = "on"; + + private ArrayList goals; + private int goalCount; + + public GoalList() { + goals = new ArrayList<>(); + this.goalCount = 0; + } + + public Goal getGoal(int index) { + return this.goals.get(index); + } + + public int getGoalCount() { + return this.goalCount; + } + + /** + * This method removes a goal object from the global field goals list by indexing + * It also decrements goalCount by 1 + * @param cmd Raw user command + * @return message of succeeding to delete goal and tell user the updated total number of goals + * @throws IOException if failed to access output file + * @throws NumberFormatException if index is invalid number + * @throws IncorrectFormatException is user command is in incorrect format + */ + public static String deleteGoal(String cmd) throws IncorrectFormatException, + NumberFormatException, IOException { + verifyDeleteGoalInput(cmd); + String[] cmdSplit = cmd.toLowerCase().trim().split(" "); + int index = Integer.parseInt(cmdSplit[1]); + Goal targetGoal = Duke.goalList.goals.remove(index - 1); + Duke.goalList.goalCount--; + Duke.goalStorage.overwriteGoalToFile(Duke.goalList); + + return TextUi.deleteGoalMsg(targetGoal) + TextUi.noOfGoalMsg(Duke.goalList.goalCount); + } + + public static String achieveGoal(String cmd) throws IncorrectFormatException, + NumberFormatException, IOException { + verifyAchieveGoalInput(cmd); + String[] cmdSplit = cmd.split(" "); + int index = Integer.parseInt(cmdSplit[1]); + Goal achievedGoal = Duke.goalList.goals.remove(index - 1); + Duke.goalList.goalCount--; + Duke.goalStorage.overwriteGoalToFile(Duke.goalList); //update goal file + Duke.achievedGoals.goals.add(achievedGoal); + Duke.achievedGoals.goalCount++; + Duke.achmStorage.overwriteGoalToFile(Duke.achievedGoals); // update achievement file + return "Congratulation! You have achieved one goal!\n" + + "[Finished]" + achievedGoal + " (:"; + } + + /** + * Begins to format user input by change to small letter, remove leading and + * checks if the user Input is valid by: + * 1. check if the length of the command equals 4 + * 2. detect keywords "on" + * 3. check if user inputs a calories number at valid range + * The userCmd should be like: set 1234 on Date + * @param userCmd represents the raw userInput + * @throws IncorrectFormatException + */ + private static void verifyGoalInput(String userCmd) throws IncorrectFormatException, NumberFormatException { + + String[] cmdSplit = userCmd.split(" "); + if (cmdSplit.length != 4) { + throw new IncorrectFormatException("Oops! The goal instruction is in wrong format."); + } + + if (!cmdSplit[2].equals(DATEKEYWORD)) { + throw new IncorrectFormatException("Sorry. I cannot detect the [" + DATEKEYWORD + "] keyword." ); + } + + int calories = Integer.parseInt(cmdSplit[1]); //throws number exception if not a number string + if (calories <= 0 ){ + throw new IllegalValueException("Please input a positive value."); + } + + } + + /** + * Possible exceptions: + * 1. Missing target index or index is invalid, includes wrong range or even not a number + * 2. Command length not equals to 2 + * @param cmd Raw User Command + * @throws IncorrectFormatException if the input command is in incorrect format, + */ + private static void verifyDeleteGoalInput(String cmd) throws IncorrectFormatException, NumberFormatException { + String[] cmdSplit = cmd.split(" "); + if (cmdSplit.length == 1) { + throw new IncorrectFormatException("Oops! Please provide the target index."); + } + + if (cmdSplit.length != 2) { + throw new IncorrectFormatException("Oops! Wrong format of delete goal instruction."); + } + + int index = Integer.parseInt(cmdSplit[1]); //throws number format exception if not a number string + if (index <= 0 || index > Duke.goalList.goalCount){ + throw new IllegalValueException("Please input a valid index by referring to your goals list."); + } + } + + /** + * This method is similar to verifyDeleteGoalInput method as the input of each command is similar + * @param cmd Raw User Command + * @throws IncorrectFormatException Either missing the target index or command contains more than 2 words + * @throws NumberFormatException if an invalid number string is input, e.g. achieve one + */ + private static void verifyAchieveGoalInput(String cmd) throws IncorrectFormatException, NumberFormatException { + String[] cmdSplit = cmd.split(" "); + if (cmdSplit.length == 1) { + throw new IncorrectFormatException("Oops! Please tell me the target index."); + } + + if (cmdSplit.length != 2) { + throw new IncorrectFormatException("Oops! Wrong format of achieve goal instruction."); + } + + int index = Integer.parseInt(cmdSplit[1]); //throws number format exception if not a number string + if (index <= 0 || index > Duke.goalList.goalCount){ + throw new IllegalValueException("Please input a valid index by referring to your goals list."); + } + } + + /** + * This method will first check if the raw user input is in the correct format. + * If not, terminate the method and throws error message. + * If yes, continue to add a new goal object into the goals list. + * @param userCmd represents raw user input + * @param target represents to target list to add new goal + * @param storage represents the target storage to update goal data + * @return String about succeeding to create goal object + * @throws IncorrectFormatException if user input is in wrong format + * @throws NumberFormatException if the user does not input a valid number + * @throws IOException if failed to access and update output file + */ + public static String addGoal(String userCmd, GoalList target, GoalStorage storage) throws IncorrectFormatException, + NumberFormatException, IOException { + verifyGoalInput(userCmd); //if invalid, exceptions is thrown + + String[] cmdSplit = userCmd.split(" "); + int calories = Integer.parseInt(cmdSplit[1]); + String date = cmdSplit[3]; + + target.goals.add(new Goal(calories, date)); + target.goalCount++; + storage.overwriteGoalToFile(target); + + return TextUi.addGoalSuccessMessage(); + } + + /** + * Exception appears if the length of view goal command does not equal to 1 + * i.e. containing extra information + * @param cmd Raw user command + * @throws IncorrectFormatException if user command format is incorrect. + */ + public static void verifyViewGoalInput(String cmd) throws IncorrectFormatException { + String[] cmdSplit = cmd.split(" "); + if (cmdSplit.length != 1){ + throw new IncorrectFormatException("Use single word [viewG] to view your goal list."); + } + } + + /** + * similar implementation to verify view goal list command + * @param cmd Raw user command + * @throws IncorrectFormatException if more than one word is input + */ + public static void verifyViewAchievementInput(String cmd) throws IncorrectFormatException { + String[] cmdSplit = cmd.split(" "); + if (cmdSplit.length != 1){ + throw new IncorrectFormatException("Use single word [achievement] to view your achievement."); + } + } + + public static class Goal { + private int calories; + private Date date; + + public Goal(int calories, String date) throws IncorrectFormatException { + this.calories = calories; + this.date = setGoalDateTime(date); + } + + private Date setGoalDateTime(String date) throws IncorrectFormatException { + return new Date(date, true); + } + + public String toString() { + return "Consume " + this.calories + " kcal on " + this.date; + } + } +} diff --git a/src/main/java/seedu/duke/meal/Category.java b/src/main/java/seedu/duke/meal/Category.java new file mode 100644 index 0000000000..39d6962724 --- /dev/null +++ b/src/main/java/seedu/duke/meal/Category.java @@ -0,0 +1,5 @@ +package seedu.duke.meal; + +public enum Category { + staple_food, snack, beverage +} diff --git a/src/main/java/seedu/duke/meal/CategoryParser.java b/src/main/java/seedu/duke/meal/CategoryParser.java new file mode 100644 index 0000000000..1a54fa6c9e --- /dev/null +++ b/src/main/java/seedu/duke/meal/CategoryParser.java @@ -0,0 +1,19 @@ +package seedu.duke.meal; + +public class CategoryParser { + private static Category[] categories = Category.values(); + + public static Category Parse(String categoryString) throws Exception { + try { + int index = Integer.parseInt(categoryString); + return categories[index]; + } catch (Exception exception1) { + try { + return Category.valueOf(categoryString); + } catch (Exception exception2) { + throw new Exception( + "Unable to parse Category String into Category enum.\nPlease input a number ranging in 0~2 or a valid category type."); + } + } + } +} diff --git a/src/main/java/seedu/duke/meal/Meal.java b/src/main/java/seedu/duke/meal/Meal.java new file mode 100644 index 0000000000..e6fc60426b --- /dev/null +++ b/src/main/java/seedu/duke/meal/Meal.java @@ -0,0 +1,22 @@ +package seedu.duke.meal; + +import seedu.duke.data.Date; + +public class Meal { + public String name; + public int calories; + public Date time; + public Category category; + + public Meal(String name, int calories, String category, Date time) throws Exception { + this.name = name; + this.calories = calories; + this.time = time; + this.category = CategoryParser.Parse(category); + } + + @Override + public String toString() { + return name + "(" + calories + " calories, " + category + ", on " + time.toString() + ")"; + } +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/parser/Parser.java b/src/main/java/seedu/duke/parser/Parser.java new file mode 100644 index 0000000000..a49454d1f4 --- /dev/null +++ b/src/main/java/seedu/duke/parser/Parser.java @@ -0,0 +1,153 @@ +package seedu.duke.parser; + +import static seedu.duke.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.duke.commands.Command; +import seedu.duke.commands.HelpCommand; +import seedu.duke.commands.goal.GoalCommand; +import seedu.duke.commands.goal.DeleteGoalCommand; +import seedu.duke.commands.goal.ViewGoalCommand; +import seedu.duke.commands.goal.AchievementCommand; +import seedu.duke.commands.goal.AchieveGoalCommand; +import seedu.duke.commands.IncorrectCommand; +import seedu.duke.commands.logcommands.LogCommand; +import seedu.duke.commands.logcommands.DeleteLogCommand; +import seedu.duke.commands.logcommands.ViewLogCommand; +import seedu.duke.commands.logcommands.UpdateLogCommand; +import seedu.duke.data.exception.IllegalValueException; +import seedu.duke.commands.ExitCommand; +import seedu.duke.commands.meal.*; + +public class Parser { + + public static final Pattern PERSON_INDEX_ARGS_FORMAT = Pattern.compile("(?.+)"); + + public static final Pattern KEYWORDS_ARGS_FORMAT = Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); + // one or more keywords separated by whitespace + + /** + * Used for initial separation of command word and args. + */ + public static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws Exception if unexpected exception occurs + */ + public Command parseCommand(String userInput) throws Exception { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + + switch (commandWord) { + case LogCommand.COMMAND_WORD: + return new LogCommand(Arrays.asList(arguments.trim().split(" "))); + + case DeleteLogCommand.COMMAND_WORD: + return new DeleteLogCommand(Arrays.asList(arguments.trim().split(" "))); + + case ViewLogCommand.COMMAND_WORD: + return new ViewLogCommand(Arrays.asList(arguments.trim().split(" "))); + + case UpdateLogCommand.COMMAND_WORD: + return new UpdateLogCommand(Arrays.asList(arguments.trim().split(" "))); + + case AddCommand.COMMAND_WORD: + return new AddCommand(Arrays.asList(arguments.trim().split(" "))); + + case DeleteCommand.COMMAND_WORD: + return new DeleteCommand(Arrays.asList(arguments.trim().split(" "))); + + case ListCommand.COMMAND_WORD: + return new ListCommand(Arrays.asList(arguments.trim().split(" "))); + + case GoalCommand.COMMAND_WORD: + return new GoalCommand(userInput); + + case DeleteGoalCommand.COMMAND_WORD: + return new DeleteGoalCommand(userInput); + + case ViewGoalCommand.COMMAND_WORD: + return new ViewGoalCommand(userInput); + + case AchieveGoalCommand.COMMAND_WORD: + return new AchieveGoalCommand(userInput); + + case AchievementCommand.COMMAND_WORD: + return new AchievementCommand(userInput); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + default: + return new IncorrectCommand("The command you inputted does not exist. Run `help` to see a" + + "list of available commands."); + } + } + + /** + * Returns true if the private prefix is present for a contact detail in the add + * command's arguments string. + */ + private static boolean isPrivatePrefixPresent(String matchedPrefix) { + return matchedPrefix.equals("p"); + } + + /** + * Extracts the new person's tags from the add command's tag arguments string. + * Merges duplicate tag strings. + */ + private static Set getTagsFromArgs(String tagArguments) throws IllegalValueException { + // no tags + if (tagArguments.isEmpty()) { + return Collections.emptySet(); + } + // replace first delimiter prefix, then split + final Collection tagStrings = Arrays.asList(tagArguments.replaceFirst(" t/", "").split(" t/")); + return new HashSet<>(tagStrings); + } + + /** + * Parses the given arguments string as a single index number. + * + * @param args arguments string to parse as index number + * @return the parsed index number + * @throws ParseException if no region of the args string could be found + * for the index + * @throws NumberFormatException the args string region is not a valid number + */ + private int parseArgsAsDisplayedIndex(String args) throws ParseException, NumberFormatException { + final Matcher matcher = PERSON_INDEX_ARGS_FORMAT.matcher(args.trim()); + if (!matcher.matches()) { + throw new ParseException("Could not find index number to parse"); + } + return Integer.parseInt(matcher.group("targetIndex")); + } + + /** + * Signals that the user input could not be parsed. + */ + public static class ParseException extends Exception { + ParseException(String message) { + super(message); + } + } +} diff --git a/src/main/java/seedu/duke/storagefile/AchmStorage.java b/src/main/java/seedu/duke/storagefile/AchmStorage.java new file mode 100644 index 0000000000..b08eda76b3 --- /dev/null +++ b/src/main/java/seedu/duke/storagefile/AchmStorage.java @@ -0,0 +1,69 @@ +package seedu.duke.storagefile; + +import seedu.duke.Duke; +import seedu.duke.goal.GoalList; +import seedu.duke.ui.TextUi; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Scanner; + +public class AchmStorage extends GoalStorage { + + public AchmStorage(String dirName, String textFileName) { + super(dirName, textFileName); + } + + public static AchmStorage initializeGoalStorage(String dirName, String textFilePath) { + return new AchmStorage(dirName, textFilePath); + } + + /** + * Note that this function exhibits the same structure as the method in parent class + * except that the nested method textToGoalObject is overridden + * @throws IOException if failed to write into file + */ + @Override + public void restoreGoalRecord() throws IOException { + if (dir.exists() && textFile.exists()) { + try { + Scanner s = new Scanner(textFile); + while (s.hasNextLine()) { + String line = s.nextLine(); + if (!line.trim().isEmpty()) { + textToGoalObject(line); + } + } + s.close(); + } catch (FileNotFoundException fnf) { + System.out.println("Saved file cannot be found! FItNus will start with empty goal records."); + System.out.println(TextUi.buildingFileMsg()); + Duke.goalList = new GoalList(); + } catch (Exception e) { + System.out.println("Saved goal file is corrupted! FitNus will start with empty goal records."); + System.out.println(TextUi.buildingFileMsg()); + Duke.goalList = new GoalList(); //start with an empty goal list + } + } + + if (!dir.exists()) { + dir.mkdir(); + } + if (!textFile.exists()) { + textFile.createNewFile(); + Duke.goalList = new GoalList(); //start with an empty goal list + } + + } + + /** + * This method convert the saved text command into the goal object + * @param goalRecord refers to saved goal record + * @throws Exception if failed to access file + */ + @Override + protected void textToGoalObject(String goalRecord) throws Exception { + String restoredCommand = restoreOrigionalCommand(goalRecord); + GoalList.addGoal(restoredCommand, Duke.achievedGoals, Duke.achmStorage); + } +} diff --git a/src/main/java/seedu/duke/storagefile/DataManager.java b/src/main/java/seedu/duke/storagefile/DataManager.java new file mode 100644 index 0000000000..637988330c --- /dev/null +++ b/src/main/java/seedu/duke/storagefile/DataManager.java @@ -0,0 +1,181 @@ +package seedu.duke.storagefile; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +import seedu.duke.Duke; +import seedu.duke.data.Date; +import seedu.duke.data.DateTime; +import seedu.duke.meal.*; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +/** + * A util used for managing the data of the program, using the io stream to + * save and read the data at a local address. + */ +public class DataManager { + private static String absolutePath; + private static String home = System.getProperty("user.home"); + + /** + * Set the relative path to make clear which exact address the data file is + * going to save and read.Using relative path may make the program easier to + * modify when you need to change the absolute saving path. + * + * @param relativePath The address containing the name of the data file under + * the data folder, ignoring the address of the data folder. + * @throws IOException + */ + public static void setRelativePath(String relativePath) throws IOException { + String dataFolderPath = home + "\\AppData\\LocalLow\\FITNUS\\"; + // Logger.customPrint(dataFolderPath); + Files.createDirectories(Paths.get(dataFolderPath)); + + absolutePath = dataFolderPath + relativePath; + // Logger.customPrint(absolutePath); + } + + /** + * Read the data from the path that has been previously set. + * + * @throws IOException + */ + public static String readData() throws IOException { + File file = new File(absolutePath); + + // No existing file + if (file.createNewFile()) { + return ""; + } + BufferedReader reader = new BufferedReader(new FileReader(file)); + + String result = "", line; + while ((line = reader.readLine()) != null) { + result += line + "\n"; + } + reader.close(); + return result.trim(); + } + + /** + * Save the data to a file at the path that has been previously set. + * + * @param content The serialized data json that is going to be saved. + * @throws IOException + */ + public static void saveData(String content) throws Exception { + File file = new File(absolutePath); + file.createNewFile(); + + FileWriter writer = new FileWriter(file); + writer.write(content); + writer.close(); + Duke.ui.showToUser("Your data has been saved at the path:\n " + absolutePath); + } + + /** + * Convert a json String to a CustomType that is set by the user. + * + * @param content A valid json String indicates a CustomType instance. + */ + public static CustomType convertFromJson(String json) { + Type type = new TypeToken() { + }.getType(); + + Gson gson = new Gson(); + + return gson.fromJson(json, type); + } + + /** + * Convert a json String to an ArrayList. + * + * @param content A valid json String indicates an ArrayList. + */ + public static ArrayList convertFromJsonToMealList(String json) { + Type type = new TypeToken>() { + }.getType(); + + Gson gson = new GsonBuilder() + // .registerTypeAdapter(Meal.class, new MealAdapter()) + // .registerTypeAdapter(DateTime.class, new DateTimeAdapter()) + .registerTypeAdapter(Date.class, new DateAdapter()) + .create(); + + return gson.fromJson(json, type); + } + + /** + * Serialize any object to json. + * + * @param object Any variable that you want to serialize. + */ + public static String convertToJson(Object object) { + Gson gson = new Gson(); + return gson.toJson(object); + } + + /** + * Used for deserializing Meal with a custom rule. + */ + private static class MealAdapter implements JsonDeserializer { + @Override + public Meal deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + return context.deserialize(jsonObject, Meal.class); + } + } + + /** + * Used for deserializing DateTime with a custom rule. + */ + private static class DateTimeAdapter implements JsonDeserializer { + @Override + public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + JsonObject jsonObject = json.getAsJsonObject(); + + String rawData = jsonObject.get("rawData").getAsString(); + try { + return new DateTime(rawData); + } catch (Exception exception) { + Duke.ui.showToUser(exception.toString()); + } + return null; + } + } + + /** + * Used for deserializing Date with a custom rule. + */ + private static class DateAdapter implements JsonDeserializer { + @Override + public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + JsonObject jsonObject = json.getAsJsonObject(); + + String rawData = jsonObject.get("standardString").getAsString(); + try { + return new Date(rawData, false); + } catch (Exception exception) { + Duke.ui.showToUser(exception.toString()); + } + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/seedu/duke/storagefile/ExerciseLogStorage.java b/src/main/java/seedu/duke/storagefile/ExerciseLogStorage.java new file mode 100644 index 0000000000..98d6a80b51 --- /dev/null +++ b/src/main/java/seedu/duke/storagefile/ExerciseLogStorage.java @@ -0,0 +1,116 @@ +package seedu.duke.storagefile; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Scanner; + +import seedu.duke.exerciselog.Log; + +public class ExerciseLogStorage extends Storage { + + + public ExerciseLogStorage(String dirName, String textFileName) { + super(dirName, textFileName); + } + + public static ExerciseLogStorage initializeStorage(String dirName, String textFilePath) { + return new ExerciseLogStorage(dirName, textFilePath); + } + + public void checkForLogTextFile(Log exerciseLog) throws IOException { + if (dir.exists() && textFile.exists()) { + Scanner s = new Scanner(textFile); + while (s.hasNextLine()) { + textToExercise(s.nextLine().split(","), exerciseLog); + } + s.close(); + } + if (!dir.exists()) { + dir.mkdir(); + } + if (!textFile.exists()) { + textFile.createNewFile(); + } + writeFile = new FileWriter(textFile.toPath().toString(), true); + } + + public static void textToExercise(String[] exerciseDetails, Log exerciseLog) { + int month = Integer.parseInt(exerciseDetails[0]); + int day = Integer.parseInt(exerciseDetails[1]); + String exerciseName = String.join(" ", exerciseDetails[2].split("_")); + int caloriesBurned = Integer.parseInt(exerciseDetails[3]); + exerciseLog.addExercise(month, day, exerciseName, caloriesBurned); + } + + public void writeToStorage(int month, int day, String[] exerciseName, int caloriesBurned) + throws IOException { + String writeToFile = ""; + writeToFile += month + ","; + writeToFile += day + ","; + writeToFile += String.join("_", exerciseName); + writeToFile += "," + caloriesBurned; + writeFile.write(writeToFile + "\n"); + writeFile.flush(); + } + + public void deleteFromStorage(int month, int day, String[] exerciseName, int caloriesBurned) + throws IOException { + Scanner readFile = new Scanner(textFile); + File tempFile = new File("./data/temp.txt"); + FileWriter tempWriter = new FileWriter(tempFile.toPath().toString()); + + String removeLine = ""; + removeLine += month + ","; + removeLine += day + ","; + removeLine += String.join("_", exerciseName); + removeLine += "," + caloriesBurned; + + while (readFile.hasNextLine()) { + String line = readFile.nextLine(); + if (!line.equals(removeLine)) { + tempWriter.write(line + "\n"); + } + } + readFile.close(); + tempWriter.close(); + textFile.delete(); + tempFile.renameTo(textFile); + textFile = new File("./data/ExerciseLog.txt"); + writeFile = new FileWriter(textFile.toPath().toString(), true); + } + + public void updateInStorage(int month, int day, String[] oldExerciseName, int oldCaloriesBurned, + String[] newExerciseName, int newCaloriesBurned) throws IOException { + Scanner readFile = new Scanner(textFile); + File tempFile = new File("./data/temp.txt"); + FileWriter tempWriter = new FileWriter(tempFile.toPath().toString()); + + String oldLine = ""; + oldLine += month + ","; + oldLine += day + ","; + oldLine += String.join("_", oldExerciseName); + oldLine += "," + oldCaloriesBurned; + + String newLine = ""; + newLine += month + ","; + newLine += day + ","; + newLine += String.join("_", newExerciseName); + newLine += "," + newCaloriesBurned; + + while (readFile.hasNextLine()) { + String line = readFile.nextLine(); + if (!line.equals(oldLine)) { + tempWriter.write(line + "\n"); + } else { + tempWriter.write(newLine + "\n"); + } + } + readFile.close(); + tempWriter.close(); + textFile.delete(); + tempFile.renameTo(textFile); + textFile = new File("./data/ExerciseLog.txt"); + writeFile = new FileWriter(textFile.toPath().toString(), true); + } +} diff --git a/src/main/java/seedu/duke/storagefile/GoalStorage.java b/src/main/java/seedu/duke/storagefile/GoalStorage.java new file mode 100644 index 0000000000..81b69317d9 --- /dev/null +++ b/src/main/java/seedu/duke/storagefile/GoalStorage.java @@ -0,0 +1,113 @@ +package seedu.duke.storagefile; + +import seedu.duke.Duke; +import seedu.duke.goal.GoalList; +import seedu.duke.ui.TextUi; + +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Scanner; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class GoalStorage extends Storage { + + public GoalStorage(String dirName, String textFileName) { + super(dirName, textFileName); + } + + public static GoalStorage initializeGoalStorage(String dirName, String textFilePath) { + return new GoalStorage(dirName, textFilePath); + } + + /** + * This method restore any saved goal into goal list at the beginning to start the app. + * Both directory or save file not found will cause the app to start with an empty goal list + * if error occurs in loading saved date, the app will start with empty list immediately + * without loading part of data + * Note that this method also set up the file writer into overwrite mode + * @throws IOException if failed to access file or create file + */ + public void restoreGoalRecord() throws IOException { + if (dir.exists() && textFile.exists()) { + try { + Scanner s = new Scanner(textFile); + while (s.hasNextLine()) { + String line = s.nextLine(); + if (!line.trim().isEmpty()) { + textToGoalObject(line); + } + } + s.close(); + } catch (FileNotFoundException fnf) { + System.out.println("Saved file cannot be found! FItNus will start with empty goal records."); + System.out.println(TextUi.buildingFileMsg()); + Duke.goalList = new GoalList(); + } catch (Exception e) { + System.out.println("Saved goal file is corrupted! FitNus will start with empty goal records."); + System.out.println(TextUi.buildingFileMsg()); + Duke.goalList = new GoalList(); //start with an empty goal list + } + } + + if (!dir.exists()) { + dir.mkdir(); + } + if (!textFile.exists()) { + textFile.createNewFile(); + Duke.goalList = new GoalList(); //start with an empty goal list + } + + } + + /** + * This method update the content of this.textFile by using content from + * source goalList goals + * Note that in the following implementation, the field writeFile in not used + * Instead, everytime a new file writer is created to update content of file + * @param goals represents source of goalList to retrieve content + * @throws IOException if failed to access file + */ + public void overwriteGoalToFile(GoalList goals) throws IOException { + String content = TextUi.contentOfGoalList(goals); + if (content == null) { + return; + } + FileWriter fw = new FileWriter(textFile.toPath().toString(), false); + fw.write(content); + fw.close(); + } + + protected void textToGoalObject(String goalRecord) throws Exception { + String restoredCommand = restoreOrigionalCommand(goalRecord); + GoalList.addGoal(restoredCommand, Duke.goalList, Duke.goalStorage); + } + + protected static String restoreOrigionalCommand(String goalRecord) { + String[] goalRecordParts = goalRecord.split(" ", 5); + //example of saved record: Consume 1230 kcal on Nov 11, 2023 + int amount =Integer.parseInt(goalRecordParts[1]); + String savedDateString = goalRecordParts[4]; + String date = convertDateFormat(savedDateString); + return "set " + amount + " on " + date; + } + + + /** + * This method is used to convert a date String with format MMM d, yyyy into a date String with format dd/MM/yyyy + * @param originalDateString date String with format MMM d, yyyy, e.g. 11 Nov, 2023 + * @return date String with format dd/MM/yyyy, e.g. 11/11/2023 + */ + private static String convertDateFormat(String originalDateString) { + DateTimeFormatter originalFormatter = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.ENGLISH); + LocalDate originalDate = LocalDate.parse(originalDateString, originalFormatter); + DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("dd/M/yyyy"); + return originalDate.format(newFormatter); + } + + +} + + diff --git a/src/main/java/seedu/duke/storagefile/Storage.java b/src/main/java/seedu/duke/storagefile/Storage.java new file mode 100644 index 0000000000..00b3dce119 --- /dev/null +++ b/src/main/java/seedu/duke/storagefile/Storage.java @@ -0,0 +1,17 @@ +package seedu.duke.storagefile; + +import java.io.File; +import java.io.FileWriter; + +class Storage { + + protected File dir; + protected File textFile; + protected FileWriter writeFile; + + public Storage(String dirName, String textFileName) { + dir = new File(dirName); + textFile = new File(textFileName); + } + +} diff --git a/src/main/java/seedu/duke/ui/TextUi.java b/src/main/java/seedu/duke/ui/TextUi.java new file mode 100644 index 0000000000..a7a037a94b --- /dev/null +++ b/src/main/java/seedu/duke/ui/TextUi.java @@ -0,0 +1,281 @@ +package seedu.duke.ui; + +import static seedu.duke.common.Messages.MESSAGE_GOODBYE; +import static seedu.duke.common.Messages.MESSAGE_INIT_FAILED; +import static seedu.duke.common.Messages.MESSAGE_USING_STORAGE_FILE; +import static seedu.duke.common.Messages.MESSAGE_WELCOME; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Scanner; + +import seedu.duke.commands.CommandResult; +import seedu.duke.goal.GoalList; +import seedu.duke.data.Printable; +import seedu.duke.Duke; + +/** + * Text UI of the application. + */ +public class TextUi { + + /** + * Offset required to convert between 1-indexing and 0-indexing. + */ + public static final int DISPLAYED_INDEX_OFFSET = 1; + + /** + * A decorative prefix added to the beginning of lines printed by FitNUS + */ + private static final String LINE_PREFIX = "|| "; + + /** + * A platform independent line separator. + */ + private static final String LS = System.lineSeparator(); + + private static final String DIVIDER = "==================================================="; + + /** + * Format of indexed list item + */ + private static final String MESSAGE_INDEXED_LIST_ITEM = "\t%1$d. %2$s"; + + /** + * Format of a comment input line. Comment lines are silently consumed when + * reading user input. + */ + private static final String COMMENT_LINE_FORMAT_REGEX = "#.*"; + + private final Scanner in; + private final PrintStream out; + + public TextUi() { + this(System.in, System.out); + } + + public TextUi(InputStream in, PrintStream out) { + this.in = new Scanner(in); + this.out = out; + } + + /** + * Returns true if the user input line should be ignored. + * Input should be ignored if it is parsed as a comment, is only whitespace, or + * is empty. + * + * @param rawInputLine full raw user input line. + * @return true if the entire user input line should be ignored. + */ + private boolean shouldIgnore(String rawInputLine) { + return rawInputLine.trim().isEmpty() || isCommentLine(rawInputLine); + } + + /** + * Returns true if the user input line is a comment line. + * + * @param rawInputLine full raw user input line. + * @return true if input line is a comment. + */ + private boolean isCommentLine(String rawInputLine) { + return rawInputLine.trim().matches(COMMENT_LINE_FORMAT_REGEX); + } + + /** + * Prompts for the command and reads the text entered by the user. + * Ignores empty, pure whitespace, and comment lines. + * Echos the command back to the user. + * + * @return command (full line) entered by the user + */ + public String getUserCommand() { + out.print(LINE_PREFIX + "Enter command: "); + String fullInputLine = in.nextLine(); + + // silently consume all ignored lines + while (shouldIgnore(fullInputLine)) { + fullInputLine = in.nextLine(); + } + + showToUser("[Command entered:" + fullInputLine + "]"); + return fullInputLine; + } + + /** + * Generates and prints the welcome message upon the start of the application. + * + * @param version current version of the application. + * @param storageFilePath path to the storage file being used. + */ + public void showWelcomeMessage(String version, String storageFilePath) { + String storageFileInfo = String.format(MESSAGE_USING_STORAGE_FILE, storageFilePath); + showToUser( + DIVIDER, + DIVIDER, + MESSAGE_WELCOME, + version, + DIVIDER); + } + + public void showGoodbyeMessage() { + showToUser(MESSAGE_GOODBYE, DIVIDER, DIVIDER); + } + + public void showInitFailedMessage() { + showToUser(MESSAGE_INIT_FAILED, DIVIDER, DIVIDER); + } + + /** + * Shows message(s) to the user + * + * @param message the message to show to the user + */ + public void showToUser(String... message) { + for (String m : message) { + if (m == null) { + continue; + } + out.println(LINE_PREFIX + m.replace("\n", LS + LINE_PREFIX)); + } + } + + /** + * Shows the result of a command execution to the user. Includes additional + * formatting to demarcate different + * command execution segments. + * + * @param result the command result to be shown to the user + */ + public void showResultToUser(CommandResult result) { + final Optional> resultItems = result.getRelevantItems(); + if (resultItems.isPresent()) { + showItemsListView(resultItems.get()); + } + showToUser(result.feedbackToUser, DIVIDER); + } + + /** + * Shows a list of persons to the user, formatted as an indexed list. + * Private contact details are hidden. + */ + private void showItemsListView(List items) { + final List formattedItems = new ArrayList<>(); + for (Printable person : items) { + formattedItems.add(person.getAsText()); + } + showToUserAsIndexedList(formattedItems); + } + + /** + * Shows a list of strings to the user, formatted as an indexed list. + */ + private void showToUserAsIndexedList(List list) { + showToUser(getIndexedListForViewing(list)); + } + + /** + * Formats a list of strings as a viewable indexed list. + */ + private static String getIndexedListForViewing(List listItems) { + final StringBuilder formatted = new StringBuilder(); + int displayIndex = 0 + DISPLAYED_INDEX_OFFSET; + for (String listItem : listItems) { + formatted.append(getIndexedListItem(displayIndex, listItem)).append("\n"); + displayIndex++; + } + return formatted.toString(); + } + + /** + * Formats a string as a viewable indexed list item. + * + * @param visibleIndex visible index for this listing + */ + private static String getIndexedListItem(int visibleIndex, String listItem) { + return String.format(MESSAGE_INDEXED_LIST_ITEM, visibleIndex, listItem); + } + + public static String noOfGoalMsg(int goalCount) { + return "You now have " + goalCount + " goals to accomplish."; + } + + public static String deleteGoalMsg(GoalList.Goal deletedGoal) { + + return "Good. I have removed this goal: " + deletedGoal + "\n" + + "Remember not to give up unaccomplished target!" + "\n"; + + } + + /** + * This method is used to implement Goal commend execution, when adding a new goal + * @return string contains information of generating a new goal successfully + */ + public static String addGoalSuccessMessage() { + int currentNoOfGoal = Duke.goalList.getGoalCount(); + GoalList.Goal newlyAddedGoal = Duke.goalList.getGoal(currentNoOfGoal - 1); + return "Nice! I have added the following goal to your goals list: \n\t" + newlyAddedGoal; + } + + /** + * This is used to show the content inside the goal list. + * It first checks if the list contains at least one goal, + * then print the goal by using string builder. + * @return String containing all the inserted goal in the global field goal list + */ + public static String showGoalList() { + int numberOfGoal = Duke.goalList.getGoalCount(); + if (numberOfGoal == 0) { + return "Oh not! You don't have any goal to achieve currently."; + } + StringBuilder sb = new StringBuilder(); + sb.append("Here you go! Remember to stick to your exercise and meal plans.\n"); + for (int i = 0; i < numberOfGoal; i++){ + sb.append(i + 1).append(". ").append(Duke.goalList.getGoal(i)).append("\n"); + } + + return sb.toString(); + } + + /** + * Similar to show Goal List. This method is used to list out all achieved goal in record. + * @return String containing all achieved goal + */ + public static String showAchievement() { + int numberOfGoal = Duke.achievedGoals.getGoalCount(); + if (numberOfGoal == 0) { + return "Add oil! There is no achievement found."; + } + StringBuilder sb = new StringBuilder(); + sb.append("Congratulation! See your achievements below: \n"); + for (int i = 0; i < numberOfGoal; i++){ + sb.append(i + 1).append(". [A]").append(Duke.achievedGoals.getGoal(i)).append("\n"); + } + + return sb.toString(); + } + + /** + * This method return content of goal list in any goalList object + * It is typically used to overwrite save file whenever change in goal records + * @param goals a GoalList object + * @return String containing goal information of the goal object + */ + public static String contentOfGoalList(GoalList goals) { + if (goals.getGoalCount() == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < goals.getGoalCount(); i++){ + sb.append(goals.getGoal(i)).append("\n"); + } + return sb.toString(); + } + + public static String buildingFileMsg() { + return "Building new save file...\n" + "Building new file succeed!"; + } + +} diff --git a/src/main/java/seedu/duke/util/Logger.java b/src/main/java/seedu/duke/util/Logger.java new file mode 100644 index 0000000000..4157b8ab3e --- /dev/null +++ b/src/main/java/seedu/duke/util/Logger.java @@ -0,0 +1,84 @@ +package seedu.duke.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * An util used for showing log with a more standard way. + */ +public class Logger { + public static Boolean debugMode = true; + + /** + * Simply show greeting. + */ + public static void showGreeting() { + showLog(" Hello! I'm EggyByte!\n What can I do for you?", true); + } + + public enum LogLevel { + DEBUG, WARNING, INFO, ERROR, FATAL, IMPORTANT + } + + /** + * Simply showing an object and choose whether to show 2 lines to clamp it. + * + * @param object The object you want to show. + * @param showLine Indicate whether to show lines. + */ + public static void showLog(Object object, Boolean showLine) { + LogLevel logLevel = debugMode ? LogLevel.DEBUG : LogLevel.INFO; + String content = object.toString(); + if (object instanceof Exception) { + logLevel = LogLevel.ERROR; + content = ((Exception) object).getMessage(); + } + showLog(content, logLevel, showLine); + } + + /** + * Showing a message and choose whether to show 2 lines to clamp it, and also + * need to choose the LogLevel of this information. + * + * @param content The message you want to show. + * @param logLevel The essence level of your message. + * @param showLine Indicate whether to show lines. + */ + public static void showLog(String content, LogLevel logLevel, Boolean showLine) { + if (showLine) { + customPrint("____________________________________________________________"); + } + customPrint( + "[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + + "][" + logLevel + "]"); + String[] lines = content.split("\n"); + for (String line : lines) { + customPrint(" " + line); + } + if (showLine) { + customPrint("____________________________________________________________\n"); + } + } + + /** + * Printing with a tab in front of the content to make it more easy to + * distinguish the different lines and meaning. + */ + public static void customPrint(String content) { + System.out.println(" " + content); + } + + /** + * Printing all elements in an array. + * It's usually used in debuging. + */ + public static void printArray(Type[] array) { + int length = array.length; + String result = "Array Length : " + length + "\n["; + for (int i = 0; i < length; i++) { + result += array[i].toString() + (i < length - 1 ? ", " : ""); + } + result += "]"; + showLog(result, false); + } +} diff --git a/src/test/java/seedu/duke/exerciselog/DayTest.java b/src/test/java/seedu/duke/exerciselog/DayTest.java new file mode 100644 index 0000000000..856dedc2ac --- /dev/null +++ b/src/test/java/seedu/duke/exerciselog/DayTest.java @@ -0,0 +1,33 @@ +package seedu.duke.exerciselog; + +public class DayTest { + public static void main(String[] args) { + DayTest test = new DayTest(); + test.containsExercisesTest(); + test.getExerciseTest(); + test.toStringTest(); + } + + public void containsExercisesTest() { + Day day = new Day(); + assert !day.containsExercises(); + day.addExercise("Swimming", 0); + assert day.containsExercises(); + day.removeExercise("Swimming", 0); + assert !day.containsExercises(); + } + + public void getExerciseTest() { + Day day = new Day(); + day.addExercise("Basketball", 197); + assert day.getExercise(1).equals(new Exercise("Basketball", 197)); + assert day.getNumberOfExercises() == 1; + } + + public void toStringTest() { + Day day = new Day(); + assert day.toString().equals("\tNO EXCERCISES FOR THIS DAY!\n"); + day.addExercise("chicken", 28); + assert day.toString().equals("\tExercise: chicken, Calories Burned: 28 Calories\n"); + } +} diff --git a/src/test/java/seedu/duke/exerciselog/ExerciseTest.java b/src/test/java/seedu/duke/exerciselog/ExerciseTest.java new file mode 100644 index 0000000000..3e62f2e694 --- /dev/null +++ b/src/test/java/seedu/duke/exerciselog/ExerciseTest.java @@ -0,0 +1,49 @@ +package seedu.duke.exerciselog; + +public class ExerciseTest { + public static void main(String[] args) { + ExerciseTest test = new ExerciseTest(); + test.initializeExerciseTest(); + test.initializeExerciseTest2(); + test.testEqualsMethod(); + test.testToStringMethod(); + test.setNewExerciseNameTest(); + test.setNewCaloriesBurnedTest(); + assert true; + } + + public void initializeExerciseTest() { + Exercise testExercise = new Exercise("Running", 280); + assert testExercise.getExerciseName().equals("Running"); + assert testExercise.getCaloriesBurned() == 280; + } + + public void initializeExerciseTest2() { + Exercise testExercise = new Exercise("Running", 280); + assert !testExercise.getExerciseName().equals("running"); + assert !(testExercise.getCaloriesBurned() == 2800); + } + + public void testEqualsMethod() { + Exercise testExercise = new Exercise("Walking", 100); + assert testExercise.equals(new Exercise("Walking", 100)); + assert !(testExercise.equals(new Exercise("Swimming", 100))); + } + + public void testToStringMethod() { + Exercise testExercise = new Exercise("Golfing", 76); + assert testExercise.toString().equals("Exercise: Golfing, Calories Burned: 76 Calories\n"); + } + + public void setNewExerciseNameTest() { + Exercise testExercise = new Exercise("Floorball", 143); + testExercise.setExerciseName("hockey"); + assert testExercise.getExerciseName().equals("hockey"); + } + + public void setNewCaloriesBurnedTest() { + Exercise testExercise = new Exercise("Football", 309); + testExercise.setCaloriesBurned(229); + assert testExercise.getCaloriesBurned() == 229; + } +} diff --git a/src/test/java/seedu/duke/exerciselog/LogTest.java b/src/test/java/seedu/duke/exerciselog/LogTest.java new file mode 100644 index 0000000000..df9ceb096c --- /dev/null +++ b/src/test/java/seedu/duke/exerciselog/LogTest.java @@ -0,0 +1,27 @@ +package seedu.duke.exerciselog; + +public class LogTest { + public static void main(String[] args) { + LogTest test = new LogTest(); + test.getNumberOfExercisesTest(); + test.addAndRemoveExerciseTest(); + } + + public void getNumberOfExercisesTest() { + Log log = new Log(); + assert log.getNumberOfExercises() == 0; + assert log.getNumberOfExercisesForMonth(1) == 0; + assert log.getNumberOfExercisesForDay(1, 21) == 0; + } + + public void addAndRemoveExerciseTest() { + Log log = new Log(); + log.addExercise(6, 7, "Exercise", 8999999); + log.addExercise(8, 24, "Exercise", 112); + assert log.getNumberOfExercises() == 2; + log.removeExercise(6, 7, "Exercise", 8999999); + assert log.getNumberOfExercises() == 1; + log.removeExercise(8, 24, "Exercise", 112); + assert log.getNumberOfExercises() == 0; + } +} diff --git a/src/test/java/seedu/duke/exerciselog/MonthTest.java b/src/test/java/seedu/duke/exerciselog/MonthTest.java new file mode 100644 index 0000000000..d583f15f71 --- /dev/null +++ b/src/test/java/seedu/duke/exerciselog/MonthTest.java @@ -0,0 +1,34 @@ +package seedu.duke.exerciselog; + +public class MonthTest { + public static void main(String[] args) { + MonthTest test = new MonthTest(); + test.createMonthTest(); + test.addExerciseTest(); + test.removeExerciseTest(); + } + + public void createMonthTest() { + Month month = new Month(31, "JAN"); + assert month.getName().equals("JAN"); + assert month.getNumberOfDays() == 31; + } + + public void addExerciseTest() { + Month month = new Month(28, "FEB"); + month.addExercise(3, "Volleyball", 79); + assert month.getNumberOfExercisesForDay(3) == 1; + month.addExercise(14, "Running", 121); + assert month.getNumberOfExercisesForDay(14) == 1; + assert month.getTotalNumberOfExercises() == 2; + } + + public void removeExerciseTest() { + Month month = new Month(28, "FEB"); + month.addExercise(3, "Volleyball", 79); + month.addExercise(14, "Running", 121); + month.removeExercise(3, "Volleyball", 79); + assert month.getNumberOfExercisesForDay(3) == 0; + assert month.getTotalNumberOfExercises() == 1; + } +} diff --git a/src/test/java/seedu/duke/parser/ParserTest.java b/src/test/java/seedu/duke/parser/ParserTest.java new file mode 100644 index 0000000000..f75885ed8c --- /dev/null +++ b/src/test/java/seedu/duke/parser/ParserTest.java @@ -0,0 +1,73 @@ +package seedu.duke.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.duke.common.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import seedu.duke.commands.Command; +import seedu.duke.commands.ExitCommand; +import seedu.duke.commands.HelpCommand; +import seedu.duke.commands.IncorrectCommand; + +public class ParserTest { + private Parser parser; + + @BeforeEach + public void setUp() { + parser = new Parser(); + } + + @Test + public void parse_emptyInput_returnsIncorrect() throws Exception { + final String[] emptyInputs = { "", " ", "\n \n" }; + final String resultMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE); + parseAndAssertIncorrectWithMessage(resultMessage, emptyInputs); + } + + /* + * Tests for 0-argument commands ======================================================================= + */ + + @Test + public void parse_helpCommand_parsedCorrectly() throws Exception { + final String input = "help"; + parseAndAssertCommandType(input, HelpCommand.class); + } + + @Test + public void parse_exitCommand_parsedCorrectly() throws Exception { + final String input = "exit"; + parseAndAssertCommandType(input, ExitCommand.class); + } + + /* + * Utility methods ==================================================================================== + */ + + /** + * Asserts that parsing the given inputs will return IncorrectCommand with the given feedback message. + */ + private void parseAndAssertIncorrectWithMessage(String feedbackMessage, String... inputs) throws Exception { + for (String input : inputs) { + final IncorrectCommand result = parseAndAssertCommandType(input, IncorrectCommand.class); + assertEquals(result.feedbackToUser, feedbackMessage); + } + } + + /** + * Parses input and asserts the class/type of the returned command object. + * + * @param input to be parsed + * @param expectedCommandClass expected class of returned command + * @return the parsed command object + */ + private T parseAndAssertCommandType(String input, Class expectedCommandClass) + throws Exception { + final Command result = parser.parseCommand(input); + assertTrue(result.getClass().isAssignableFrom(expectedCommandClass)); + return (T) result; + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT deleted file mode 100644 index 892cb6cae7..0000000000 --- a/text-ui-test/EXPECTED.TXT +++ /dev/null @@ -1,9 +0,0 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling diff --git a/text-ui-test/UploadCommand.cmd b/text-ui-test/UploadCommand.cmd new file mode 100644 index 0000000000..9ce5e9b9a9 --- /dev/null +++ b/text-ui-test/UploadCommand.cmd @@ -0,0 +1,11 @@ +rem customize the tag name and message here +set TAG_NAME=fengguangyao +set MESSAGE="Personal Job" + +rem create a branch +git branch %TAG_NAME% +git switch %TAG_NAME% +rem commit all changes +git add . +git commit -m %MESSAGE% +git push origin %TAG_NAME% \ No newline at end of file diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..ae3bc0a936 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1 @@ -James Gosling \ No newline at end of file +exit \ No newline at end of file diff --git a/tp/.idea/checkstyle-idea.xml b/tp/.idea/checkstyle-idea.xml new file mode 100644 index 0000000000..9ce56c5a1b --- /dev/null +++ b/tp/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + 10.12.3 + JavaOnly + true + + + \ No newline at end of file diff --git a/tp/.idea/vcs.xml b/tp/.idea/vcs.xml new file mode 100644 index 0000000000..6c0b863585 --- /dev/null +++ b/tp/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tp/.idea/workspace.xml b/tp/.idea/workspace.xml new file mode 100644 index 0000000000..ade91fc5a4 --- /dev/null +++ b/tp/.idea/workspace.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + +