diff --git a/.gitignore b/.gitignore index 2873e189e1..4846138ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +/data/ + +.vscode/ +text-ui-test/data/ \ No newline at end of file diff --git a/README.md b/README.md index f82e2494b7..5e261abbce 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,37 @@ -# Duke project template - -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. - -## Setting up in Intellij - -Prerequisites: JDK 11 (use the exact version), update Intellij to the most recent version. - -1. **Ensure Intellij JDK 11 is defined as an SDK**, as described [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk) -- this step is not needed if you have used JDK 11 in a previous Intellij project. -1. **Import the project _as a Gradle project_**, as described [here](https://se-education.org/guides/tutorials/intellijImportGradleProject.html). -1. **Verify the set up**: After the importing is complete, locate the `src/main/java/seedu/duke/Duke.java` file, right-click it, and choose `Run Duke.main()`. If the setup is correct, you should see something like the below: - ``` - > Task :compileJava - > Task :processResources NO-SOURCE - > Task :classes - - > Task :Duke.main() - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - - What is your name? - ``` - Type some word and press enter to let the execution proceed to the end. - -## Build automation using Gradle - -* This project uses Gradle for build automation and dependency management. It includes a basic build script as well (i.e. the `build.gradle` file). -* If you are new to Gradle, refer to the [Gradle Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/gradle.html). - -## Testing - -### I/O redirection tests - -* To run _I/O redirection_ tests (aka _Text UI tests_), navigate to the `text-ui-test` and run the `runtest(.bat/.sh)` script. - -### JUnit tests - -* A skeleton JUnit test (`src/test/java/seedu/duke/DukeTest.java`) is provided with this project template. -* If you are new to JUnit, refer to the [JUnit Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/junit.html). - -## Checkstyle - -* A sample CheckStyle rule configuration is provided in this project. -* If you are new to Checkstyle, refer to the [Checkstyle Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/checkstyle.html). - -## CI using GitHub Actions - -The project uses [GitHub actions](https://github.com/features/actions) for CI. When you push a commit to this repo or PR against it, GitHub actions will run automatically to build and verify the code as updated by the commit/PR. - -## Documentation - -`/docs` folder contains a skeleton version of the project documentation. - -Steps for publishing documentation to the public: -1. If you are using this project template for an individual project, go your fork on GitHub.
- If you are using this project template for a team project, go to the team fork on GitHub. -1. Click on the `settings` tab. -1. Scroll down to the `GitHub Pages` section. -1. Set the `source` as `master branch /docs folder`. -1. Optionally, use the `choose a theme` button to choose a theme for your documentation. +
+

AthletiCLI

+

+ Your all-in-one solution to track, analyse, and optimize your athletic performance. +

+ +

+ GitHub contributors + GitHub commit activity (branch) + + GitHub issues + GitHub pull requests + GitHub all releases +

+

+ :green_book: User Guide + | + :blue_book: Developer Guide + | + :orange_book: About Us +

+
+ +**AthletiCLI** is your all-in-one solution to track, analyse, and optimize your athletic performance. Designed for the committed athlete, this command-line interface (CLI) tool not only keeps tabs on your physical activities but also covers dietary habits, sleep metrics, and more. + +* If you are interested in using AthletiCLI, head over to the [:green_book: User Guide](UserGuide.html). +* If you are interested about developing AthletiCLI, the [:blue_book: Developer Guide](DeveloperGuide.html) is a good place to start. +* If you would like to learn more about our development team, please visit the [:orange_book: About Us](AboutUs.html) page. + + + +## :rocket: Quick Start + +* :white_check_mark: Ensure you have the required runtime environment (JRE 11 or above) installed on your computer. +* :white_check_mark: Download the latest [release](https://github.com/AY2324S1-CS2113-T17-1/tp/releases) of AthletiCLI. +* :white_check_mark: Copy the downloaded file to a folder you want to designate as the home for AthletiCLI. +* :white_check_mark: Open a command terminal, `cd` into the folder where you copied the file, and run `java -jar AthletiCLI.jar` . \ No newline at end of file diff --git a/build.gradle b/build.gradle index ea82051fab..20ce57b54d 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("athleticli.AthletiCLI") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("athleticli") archiveClassifier.set("") } @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..818ff30bce --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +Gemfile.lock +_site/ \ No newline at end of file diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..76199580b4 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,18 @@ -# About us +--- +layout: page +title: About Us +--- -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + + +| Display | Name | GitHub Profile | Portfolio | +|:----------------------------------------------:|:-----------------:|:----------------------------------------:|:----------------------------------:| +| ![](https://github.com/AlWo223.png) | Alexander Wolters | [GitHub](https://github.com/AlWo223) | [Portfolio](team/alwo223.html) | +| ![](https://github.com/nihalzp.png) | Nihal Parash | [GitHub](https://github.com/nihalzp) | [Portfolio](team/nihalzp.html) | +| ![](https://github.com/DaDevChia.png) | Dylan Chia | [GitHub](https://github.com/DaDevChia) | [Portfolio](team/dadevchia.html) | +| ![](./team/photo/yicheng-toh.png) | Toh Yi Cheng | [GitHub](https://github.com/yicheng-toh) | [Portfolio](team/yicheng-toh.html) | +| ![](https://github.com/skylee03.png) | Yang Ming-Tian | [GitHub](https://github.com/skylee03) | [Portfolio](team/skylee03.html) | diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..902e2fba25 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,784 @@ -# Developer Guide +--- +layout: page +title: Developer Guide +--- + + + +- Table of Contents +{:toc} +--- ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +[//]: # ({list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well}) + +1. [AB-3 Developer Guide](https://se-education.org/addressbook-level3/DeveloperGuide.html) +2. [PlantUML for sequence diagrams](https://plantuml.com/) + +--- +## Setting Up and Getting Started + +First, fork [this repo](https://github.com/AY2324S1-CS2113-T17-1/tp), and clone the fork into your computer. + +If you plan to use IntelliJ IDEA (highly recommended): + +1. **Configure the JDK**: Follow the guide [se-edu/guides IDEA: Configuring the JDK](https://se-education.org/guides/tutorials/intellijJdk.html) to ensure IntelliJ is configured to use JDK 11. +2. **Import the project as a Gradle project**: Follow the guide +[se-edu/guides IDEA: Importing a Gradle project](https://se-education.org/guides/tutorials/intellijImportGradleProject.html) +to import the project into IDEA. + + :exclamation: **Note:** Importing a Gradle project is slightly different from importing a normal Java project. +3. **Verify the setup**: + * Run `athlethicli.AthletiCLI` and try a few commands. + * Run the tests using `./gradlew check` and ensure they all pass. + + + + +--- +## Design + +This section provides a high-level explanation of the design and implementation of AthletiCLI, +supported by UML diagrams and short code snippets to illustrate the flow of data and interactions between the +components. + +--- +### Architecture + +Given below is a quick overview of main components and how they interact with each other. + +'set-diet-goal' Sequence Diagram + +**Main components of the architecture** + +[`AthletiCLI`](https://github.com/AY2324S1-CS2113-T17-1/tp/blob/master/src/main/java/athleticli/AthletiCLI.java) is in charge of the app launch and shut down. + +The bulk of the AthletiCLI’s work is done by the following components, with each of them corresponds to a package: + +* [`Ui`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/ui): Interacts with the user via the command line. +* [`Parser`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/parser): Parses the commands input by the users. +* [`Storage`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/storage): Reads data from, and writes data to, the hard disk. +* [`Data`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/data): Holds the data of AthletiCLI in memory. +* [`Commands`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/commands): Contains multiple command executors. + +Other components: + +* [`Exceptions`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/exceptions): Represents exceptions used by multiple other components. +* [`Common`](https://github.com/AY2324S1-CS2113-T17-1/tp/tree/master/src/main/java/athleticli/common): Contains configurations that shared by other components. + +### Overview + +The class diagram shows the relationship between `AthletiCLI`, `Ui`, `Parser`, and `Data`. + +![](images/MainClassDiagram.svg) + +### Data Component + +The class diagram shows how the `Data` component is constructed with multiple classes. + +![](images/DataClassDiagram.svg) + +### Parser Component + +The class diagram shows how the `Parser` component is constructed with multiple classes. + +![](images/ParserClassDiagram.png) + +**How the architecture components interact with each other** + +The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `help add-diet`. + +![](images/HelpAddDiet.svg) + +This diagram involves the interaction between `AthletiCLI`, `Ui`, `Parser`, `Commands` components and the user. + +The `Storage` component only interacts with the `Data` component. The _Sequence Diagram_ below shows how they interact with each other for the scenario where a `save` command is executed. + +![](images/Save.svg) + +For simplicity, only 1 `StorableList` is drawn instead of the actual 6. + +--- + +## Implementation + +### Diet Management in AthletiCLI + +#### [Implemented] Adding, Editing, Deleting, Listing, and Finding Diets + +Regardless of the operation you are performing on diets (adding, editing, deleting, listing, or finding), the process +follows a general five-step pattern in AthletiCLI: + +**Step 1 - Input Processing:** The user's input is passed through AthletiCLI to the `Parser` class. Examples of user +inputs include: + +- `add-diet calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00` for adding a diet. +- `edit-diet 1 calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00` for editing a diet at index 1. +- `delete-diet 1` for deleting a diet at index 1. +- `list-diet` for listing all diets. +- `find-diet 2021-09-01` for finding all diets on 1st September 2021. + +**Step 2 - Command Identification:** The `Parser` class identifies the type of diet operation and calls the +appropriate `DietParser` method to parse the necessary parameters (if any). For example, the `add-diet` command will +call the `DietParser#parseDiet()` method, which will return a `Diet` object. + +**Step 3 - Command Creation**: An instance of the corresponding command class is created (e.g., `AddDietCommand`, +`EditDietCommand`, etc.) using the returned object (if any) from the `DietParser` and returned to AthletiCLI. + +**Step 4 - Command Execution**: AthletiCLI executes the command, interacting with the data instance of DietList to +perform the required operation. For example, the `AddDietCommand` will add the `Diet` object to the `DietList` object, +while the `EditDietCommand` will edit the `Diet` object at the specified index in the `DietList` object. + +**Step 5 - Result Display**: A message is returned post-execution and passed through AthletiCLI to the UI for +display to the user. This is useful for informing the user of the success or failure of the operation. + +By following these general steps, AthletiCLI ensures a streamlined process for managing diet-related tasks. + +Here is the sequence diagram for the `edit-diet` command to illustrate the five-step process: + +![](images/editDietSequenceDiagram.png) + +> The diagram shows the interaction between the `AthletiCLI`, `Parser`, `Command`, and `Data` components. +> The use of HashMaps in the `DietParser` class allows for a more flexible and extensible design, as it facilitates +> the modification of necessary parameters without requiring the user to specify all parameters in the command. For +> example, the user can choose to edit only the calories and protein of a diet, without specifying the carb and fat values. + + +#### [Implemented] Setting Up of Diet Goals + +This following sequence diagram show how the 'set-diet-goal' command works: + +![](images/DietGoalsSequenceDiagram.svg) + +**Step 1:** The input from the user ("set-diet-goal WEEKLY fat/1") runs through AthletiCLI to the Parser Class. + +**Step 2:** The Parser Class will identify the request as setting up a diet goal and pass in the parameters +"WEEKLY fat/1". + +**Step 3:** A temporary dietGoalList is created to store newly created diet goals. In this case, a weekly healthy goal +for fat with a target value of 1mg. + +**Step 4:** The inputs are validated against our lists of approved diet goals. + +**Step 5:** For each of the diet goals that are valid, if it is a healthy goal, a HealthyDietGoal object will be created and stored in the +temporary dietGoalList, else an UnhealthyDietGoal will be created instead. + +**Step 6:** The Parser then creates for an instance of SetDietGoalCommand and returns the instance to +AthletiCLI. + +**Step 7:** AthletiCLI will execute the SetDietGoalCommand. This adds the dietGoals that are present in the +temporary list into the data instance of DietGoalList which will be kept for records. + +**Step 8:** After executing the SetDietGoalCommand, SetDietGoalCommand returns a message that is passed to +AthletiCLI to be passed to UI(not shown) for display. + +#### [Proposed] Future Implementation of DietGoalList Class + +The current implementation of DietGoalList is an ArrayList. This is because the number of nutrients currently is 4. O(n^2) +operations can be treated as O(1). Furthermore, DietGoalListClass gets to inherits from superclass like its other goals' counterpart. +However, it is not efficient in searching for a particular dietGoal especially when the number of goals and time span for goals increases. +At any instance of time, there could only be the existence of one dietGoal. +Verifying if there is an existence of a diet goal using an ArrayList takes O(n) time, where n is the number of dietGoals. +The proposed change will be to change the underlying data structure to a hashmap in the future for amortised O(1) time complexity +for checking the presence of a dietGoal. + +### Activity Management in AthletiCLI + +#### [Implemented] Adding activities + +The `add-activity` feature is a core functionality which allows users to record new activities in the application. +The feature is designed in a modular and extendable way, ensuring seamless integration of future enhancements and +especially new activity types. + +The architecture of the `add-activity` feature is composed of the following main components. +1. `AthletiCLI`: Facilitates the mechanism. It captures the user input and initiates the parsing and execution. +2. `Parser` (`Activity Parser`): Interprets the user input, generating both the appropriate command object and + the activity instance. +3. `AddActivityCommand`: Encapsulates the execution of the `add-activity` command, adding the activity to the data. +4. `ActivityChanges`: Contains the arguments of the activity to be added. It is used to transfer the data from the + parser to the activity in a modular way. +5. `Activity`: Represents the activity to be added. It is a superclass for specific activity types like Run, Swim and + Cycle. +6. `Data`: Manages the current state of the activity list. +7. `ActivityList`: Maintains the list of all activities added to the application. + + +Class Relationships: + +Below is a class diagram illustrating the relationships between the data components `Activity`,`Data` and +`ActivityList`: + +Activity Data Components + +> The diagram shows the inheritance relationship between the `Activity` class and the specific activity types `Run`, +> `Swim` and `Cycle`, each with unique attributes and methods. This design becomes especially crucial in future +> development cycles with added parameters and activity types. The `ActivityList` aggregates these instances. + +Usage Scenario and Process Flow: + +The process of adding an activity involves several steps, each handled by different components. +Given below is an example usage scenario on how the add mechanism behaves. + +**Step 1 - Input Capture:** The user issues an `add-activity ...` (or `add-run`, etc.) command which is +captured and forwarded to the Parser by the running AthletiCLI instance. + +**Step 2 - Activity Parsing:** The ActivityParser interprets the raw input to obtain the arguments of the activity. +Given that all parameters are provided correctly and no exception is thrown, a new activity object is created. + +This diagram illustrates the activity parsing process in more detail: +The `ActivityChanges` object plays a key role in the parsing process. It is used for storing the +different attributes of the activity that are to be added. Later, the `ActivityParser` +will use the `ActivityChanges` to create the `Activity` object. +> This way of transferring data between the parser and the activity is more flexible which is suitable for future +> extensions of the activity types and allows for a more modular design. This design and most of the methods can be reused +> for the `edit-activity` mechanism, which works in the same way with slight modifications due to optional parameters. + +

+ Activity Parsing Process +

+ +**Step 3 - Command Parsing:** Afterwards the parser constructs an `AddActivityCommand` embedding the newly created +activity within it. The `AddActivityCommand` implements the `AddActivityCommand#execute()` operation and is passed to +the AthletiCLI instance. + +**Step 4 - Activity Addition:** The AthletiCLI instance executes the `AddActivityCommand` object. The command +accesses the data and retrieves the currently stored list of activities stored inside it. The new `Activity` object is +then added to the `ActivityList`. + +**Step 5 - User Interaction:** Upon successful addition of the activity, a confirmation message is displayed to the +user. + +The following sequence diagram visually represents the flow and interactions of components during the `add-activity` +operation: + +![](images/AddActivity.svg) + +#### [Implemented] Tracking activity goals + +The `set-activity-goal` feature allows users to set and track periodic goals for their activities. +The goal fulfillment is automatically monitored and can be reviewed by the user at any time. -## Design & implementation +These are the key components and their roles in the architecture of the goal tracking: +* `SetActivityGoalCommand`: Encapsulates the execution of the `set-activity-goal` command. It adds + the activity goal to the data. +* `ActivityGoal`: Represents the activity goal that is to be added and contains functionality to + track the fulfillment of the goal. +* `ActivityList`: Contains key functionality to retrieve and filter the activity list according to the specified + criteria of the goal. -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +Given below is an example usage scenario and how the goal setting and tracking mechanism behaves at +each step. +**Step 1 - Input Capture:** The user issues a `set-activity-goal ...` command which is captured and passed to the + Parser by the running AthletiCLI instance. + +**Step 2 - Goal Parsing:** The `ActivityParser` parses the raw input to obtain the sports, target and timespan of the + goal. + Given that all these parameters are provided correctly and no exception is thrown, a new activity goal object is + created. + +**Step 3 - Command Parsing:** In addition the parser will create a `SetActivityGoalCommand` object with the newly + added activity goal attached to it. The command implements the `SetActivityGoalCommand#execute()` operation and is + passed to the AthletiCLI instance. + +**Step 4 - Goal Addition:** The AthletiCLI instance executes the `SetActivityGoalCommand` object. The command will + access the data and retrieve the currently stored list of activity goals stored inside it. The new `ActivityGoal` + object is added to the list. + +The following sequence diagram shows how the `set-activity-goal` operation works: + +![](images/AddActivityGoal.svg) + +Assume that the user has set a goal to run 10km per week and has already tracked two running activities of 5km each +within the last 7 days as well as three older sport activities. The object diagram below shows the state of the +scenario with the eligible activities for the goal highlighted in green. + +![](images/ActivityObjectDiagram.svg) + +The following describes how the goal evaluation works after being invoked by the user, e.g., with a `list-activity-goal` command: + +**Step 5 - Goal Assessment:** The evaluation of the goal is operated by the `ActivityGoal` object. It retrieves the +activity list with the five tracked activities from the data and calls the total distance calculation function. It + filters the activity list according to the specified timespan and sports of the goal. The current value obtained by this, + 10km in the example, is returned to the `ActivityGoal` object. This output is compared to the target value of the + goal. This mechanism is visualized in the following sequence diagram: + +![](images/ActivityGoalEvaluation.svg) + +The `edit-activity-goal` and `delete-activity-goal` operations function similarly. They use the arguments `sport`, +`type`, and `period` to identify the specific goal to be edited or deleted. If there is no existing goal that +matches the specified criteria, an error message is displayed to the user. + +Similar to `set-activity-goal`, the operations `edit-activity-goal` and `delete-activity-goal` utilize +`ActivityGoal` objects to represent the goals being edited or deleted. During the execution of these commands, the +system quickly verifies whether the goal exists in the `ActivityGoalList`. If the goal is found, it is then edited +or deleted as requested. + +Finally, the `list-activity-goal` operation is designed similarly to the `list-activity` operation. It involves +retrieving the `ActivityGoalList` from the database and displaying the goals to the user. + +### Sleep Management in AthletiCLI + +#### [Implemented] Finding, Adding, Editing, Deleting, Listing Sleep + +1. **Input Processing**: The user's input is passed through AthletiCLI to the Parser Class. Examples of user inputs include: + - "add-sleep hours/8 datetime/2021-09-01 06:00" for adding sleep. + - "edit-sleep 1 hours/8 datetime/2021-09-01 06:00" for editing sleep. + - "delete-sleep 1" for deleting sleep. + - "list-sleep" for listing all sleep. + +2. **Command Identification**: The Parser Class identifies the type of sleep operation and passes the necessary parameters. + +3. **Command Creation**: An instance of the corresponding command class is created (e.g., AddSleepCommand, EditSleepCommand, etc.) and returned to AthletiCLI. + +4. **Command Execution**: AthletiCLI executes the command, interacting with the data instance of SleepList to perform the required operation. + +5. **Result Display**: A message is returned post-execution and passed through AthletiCLI to the UI for display to the user. + +In particular to demonstrate all parser classes, the following sequence diagram shows how the `edit-sleep` command works: + +![](images/EditSleepObjectSequenceDiagram.svg) + +The sleep parser was originally designed with little modularity, with each sleep instruction having to be parsed individually. This resulted in a lot of code duplication and was not very modular. This also resulted in having to reimplement much of the input checking logic for each sleep instruction, and many different error messages that was difficult to maintain. + +Therefore a refactoring was done such that we only have a sleep object parser, sleep index parser and sleep goal parser that interacts with the sleep parser. This allows us to reuse the input checking logic and error messages. This also allows us to have a more modular design and reduce code duplication. + + +#### [Implemented] Sleep Structure + +The following class diagram demonstrates the relationship between the data components Sleep, SleepList, as well as the Findable interface and the StorableList abstract class. + +![](images/SleepAndSleepListClassDiagram.svg) + +The design decision for why we have decided to implement a findable interface and a storable list abstract class is to have a more modular design. Therefore allowing for easier extension of the code in the future when implementing other data classes as well, such as extending to hydration and other possible data classes. + +#### [Implemented] Sleep Duration and Date calculation + +Initially sleep entries do not have an associated date, this makes it much more difficult to find the sleep entries for a specific date. Therefore we have decided to add a date field to the sleep entries. + +However, there are complications surrounding calculation of sleep date. Many people often sleep past midnight, and this results in the sleep date being the next day instead of the current day. Therefore we have decided that for sleeps starting before 06:00 on the next day, the sleep date will be the previous day. This allows us to have a more accurate representation of the sleep date. + +**[Challenge]** + +Initially, the design of the sleep duration used integer to store the seconds of the sleep duration. However, this design results in much difficulty when it comes to the calculation of the sleep duration. + +For instance, when printing the sleep duration string, we have to convert the seconds into hours, minutes and seconds. This results in a lot of code duplication and is not very modular. + +**[Solution]** + +Therefore we have decided to change the design of the sleep duration to use the Duration class from the Java library. This allows us to use the built-in functions to calculate the sleep duration and convert the sleep duration into a string. This results in a more modular design and reduces code duplication. + +#### [Implemented] Sleep Goals + +The sleep goals feature allows users to set and track periodic goals for their sleep duration. + +The implementation of sleep goals is similar to the implementation of activity goals. Therefore the implementation of sleep goals is not described in detail here. + +**[Future Implementation]** +For Sleep Goals originally there were plans to incorporate a sleep quality goal where an optimum sleep start time and end time would be set. However, due to issues surrounding modular design, and how we will have to extend our common Goal interface and abstract classes to include more methods, we have decided to not implement this feature. It will be implemented in a future version of AthletiCLI. + +--- ## Product scope + ### Target user profile -{Describe the target user profile} +AthletiCLI is designed for athletic individuals who are committed to optimizing their performance. + +These users are highly disciplined and engaged not only in regular, intense physical training but also in nutrition, mental conditioning, and recovery. + +They are looking for a holistic tool that integrates all facets of an athletic lifestyle. AthletiCLI serves as a daily or weekly companion, designed to monitor, track, and analyze various elements crucial for high-level athletic performance. ### Value proposition -{Describe the value proposition: what problem does it solve?} +AthletiCLI provides a streamlined, integrated solution for athletic individuals focused on achieving peak performance. + +While the app includes robust capabilities for tracking physical training metrics, it also offers features for monitoring dietary habits and sleep metrics. + +By providing a comprehensive view of various performance-related factors over time, AthletiCLI enables athletes to identify trends, refine their training and lifestyle habits, and optimize outcomes. The app is more than a tracking tool—it's a performance optimization platform that takes into account the full spectrum of an athlete's life. + +--- ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| + +| Version | As a ... | I want to ... | So that I can ... | +|---------|---------------------------------|-------------------------------------------------------------------|----------------------------------------------------------------------------------------| +| v1.0 | fitness enthusiastic user | add different activities including running, swimming and cycling) | keep track of my fitness activities and athletic performance. | +| v1.0 | analytical user | view my activity details at any point in time | track my progress and make informed decisions about my fitness routine. | +| v1.0 | clumsy user | delete any tracked activity | I can correct any mistakes or remove accidentally added activities. | +| v1.0 | detail-oriented user | modify any of my tracked activities | ensure accuracy in my fitness records. | +| v1.0 | health-conscious user | add my dietary information | keep track of my daily calorie and nutrient intake | +| v1.0 | organized user | delete a dietary entry | remove outdated or incorrect data from my diet records | +| v1.0 | fitness enthusiast | view all my diet records | have a clear overview of my dietary habits and make informed decisions on my diet | +| v1.0 | new user | see usage instructions | refer to them when I forget how to use the application | +| v1.0 | motivated weight-conscious user | set diet goals | have the motivation to work towards keeping weight in check. | +| v1.0 | forgetful user | see all my diet goals | remind myself of all the diet goals I have set. | +| v1.0 | regretful user | remove my diet goals | I can rescind the strict goals I set previously when I find the goals too far fetched. | +| v1.0 | motivated user | update my diet goals | I can work towards better version of myself by setting stricter goals. | +| v1.0 | sleep deprived user | add my sleep information | keep track of my sleep habits and identify areas for improvement | +| v1.0 | sleep deprived user | delete a sleep entry | remove outdated or incorrect data from my sleep records | +| v1.0 | sleep deprived user | view all my sleep records | have a clear overview of my sleep habits and make informed decisions on my sleep | +| v1.0 | sleep deprived user | edit my sleep entries | correct any mistakes or update my sleep information as needed | +| v2.0 | user | find a to-do item by name | locate a to-do without having to go through the entire list | +| v2.0 | meticulous user | edit my dietary entries | correct any mistakes or update my diet information as needed | +| v2.0 | active user | set activity goals | work towards a specific fitness target for different sports activities. | +| v2.0 | adaptable athlete | edit my activity goals | modify my fitness targets to align with my current fitness level and schedule. | +| v2.0 | organized athlete | list all my activity goals | have a clear overview of my set targets and track my progress easily. | +| v2.0 | meticulous user | find my diets by date | easily retrieve my dietary records for a specific day and monitor my eating habits. | +| v2.0 | motivated user | keep track of my diet goals for a period of time | I can monitor my diet progress on a weekly basis and increase or reduce if needed. | | +| v2.0 | goal-oriented user | delete a specific activity goal | remove goals that are no longer relevant or achievable for me. | | +| v2.0 | sleep deprived user | calculate sleep duration | keep track of my sleep habits and identify areas for improvement | +| v2.0 | sleep deprived user | find how much I slept on a specific date | easily retrieve my sleep records for a specific day and monitor my sleep habits. | +| v2.1 | user with bad sleep habits | set sleep goals | work towards a specific sleep target. | +| v2.1 | user with bad sleep habits | edit my sleep goals | modify my sleep targets to align with my current sleep habits. | +| v2.1 | user with bad sleep habits | list all my sleep goals | have a clear overview of my set targets and track my progress easily. | + +--- ## Non-Functional Requirements -{Give non-functional requirements} +1. AthletiCLI should work on Windows, macOS and Linux that has Java 11 installed. +2. AthletiCLI should be able to store data locally. +3. AthletiCLI should be able to work offline. +4. AthletiCLI should be easy to use. + +--- ## Glossary -* *glossary item* - Definition +[//]: # (* *glossary item* - Definition) +* **UI** - A short form for User Interface. A UI class refers to the class that is responsible for handling user input +and provide feedback to the users. + + +--- ## Instructions for manual testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +**Note**: This section serves to provide a quick start for manual testing on AthletiCLI. This list is not exhaustive. +Developers are expected to conduct more extensive tests. + +### Initial Launch + +* ✅ Download the latest AthletiCLI from the official repository. +* ✅ Copy the downloaded file to a folder you want to designate as the home for AthletiCLI. +* ✅ Open a command terminal, cd into the folder where you copied the file, and run `java -jar AthletiCLI.jar`. + +### Activity Management + +#### Activity Records + +1. Adding different activities: + - Test case 1: + * Add a general activity. + * Command: `add-activity Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01 06:00` + * Expected Outcome: A general activity with duration of 1 hour, distance of 10km, and datetime of 2021-09-01 is + added successfully and a short summary of the activity is displayed to the user. + - Test case 2: + * Add a run. + * Command: `add-run Berlin Marathon duration/03:33:17 distance/42125 datetime/2023-08-10 07:00 elevation/10` + * Expected Outcome: A run with duration of 3 hours 33 minutes 17 seconds, distance of 42.125km, datetime of + 2023-08-10, and elevation of 10m is added successfully and a short summary of the activity is displayed to + the user. + - Test case 3: + * Try to add a swim without specifying swimming style. + * Command: `add-swim Evening Swim duration/00:30:00 distance/1000 datetime/2021-09-01 06:00` + * Expected Outcome: Error message indicating the swimming style is not specified is displayed. +2. Deleting an activity: + - Test case 1: + * Delete the first activity in a non-empty activity list. + * Command: `delete-activity 1` + * Expected Outcome: The first activity is deleted successfully. The activity is displayed to the user and + the activity list is updated. + - Test case 2: + * Delete an activity at an invalid index. + * Command: `delete-activity 0` + * Expected Outcome: Error message indicating the index is invalid is displayed. +3. List all activities: + - Test case 1: + * List all activities in a non-empty activity list. + * Command: `list-activity` + * Expected Outcome: All activities in the activity list are displayed to the user sorted by datetime. + - Test case 2: + * List all activities in a non-empty activity list with the detailed flag. + * Command: `list-activity -d` + * Expected Outcome: All activities in the activity list are displayed to the user with detailed information + like elevation for runs and cycles. + - Test case 2: + * List all activities in an empty activity list. + * Command: `list-activity` + * Expected Outcome: Message indicating the activity list is empty is displayed. +4. Find activities of a specific date: + - Test case 1: + * Find activities of a specific date with multiple entries on that date. + * Command: `find-activity 2021-09-01` + * Expected Outcome: All activities on 1st September 2021 are displayed to the user. + - Test case 2: + * Find activities of a specific date with no entries on that date. + * Command: `find-activity 2021-09-02` + * Expected Outcome: No activities are displayed +5. Edit an activity: + - Test case 1: + * Edit the caption of the first activity in the activity list, which is of type run. + * Command: `edit-run 1 caption/Sunday=Runday` + * Expected Outcome: The caption of the first activity is updated to "Sunday=Runday". + - Test case 2: + * Try to use the edit-swim command to edit a run. + * Command: `edit-swim 1 caption/Sunday=Runday` + * Expected Outcome: Error message indicating the activity type is not a swim is displayed. + +#### Activity Goals + +1. Setting Activity Goals + - Test case 1: + * Set a weekly running distance goal. + * Command: `set-activity-goal sport/running type/distance period/weekly target/15000` + * Expected Outcome: Weekly running goal of 15km is set successfully. + - Test case 2: + * Set a monthly swimming duration goal. + * Command: `set-activity-goal sport/swimming type/duration period/monthly target/300` + * Expected Outcome: Monthly swimming duration goal of 300 minutes is set successfully. + +2. Editing Activity Goals + - Test case 1: + * Edit an existing weekly cycling distance goal. + * Command: `edit-activity-goal sport/cycling type/distance period/weekly target/20000` + * Expected Outcome: Weekly cycling distance goal is updated to 20km. + - Test case 2: + * Edit a non-existent yearly running duration goal. + * Command: `edit-activity-goal sport/running type/duration period/yearly target/1000` + * Expected Outcome: Error indicating no existing yearly running duration goal. + +3. Listing Activity Goals + - Test case 1: + * List all set activity goals. + * Command: `list-activity-goal` + * Expected Outcome: All set activity goals along with their details are listed. + +4. Deleting Activity Goals + - Test case 1: + * Delete an existing monthly swimming duration goal. + * Command: `delete-activity-goal sport/swimming type/duration period/monthly` + * Expected Outcome: Monthly swimming duration goal is deleted successfully. + - Test case 2: + * Attempt to delete a non-existent daily general activity goal. + * Command: `delete-activity-goal sport/general type/distance period/daily` + * Expected Outcome: Error indicating no such daily general activity goal exists. + +### Diet Management + +#### Diet Records + +1. Adding Diets + - Test case 1: + * Add a complete diet entry. + * Command: `add-diet calories/700 protein/25 carb/55 fat/15 datetime/2023-10-12 07:30` + * Expected Outcome: Diet entry is successfully added with 700 calories, 25mg protein, 55mg carb, and 15mg fat. + - Test case 2: + * Attempt to add a diet entry with a future datetime. + * Command: `add-diet calories/800 protein/30 carb/60 fat/20 datetime/3024-01-01 08:00` + * Expected Outcome: Error indicating the datetime cannot be in the future. + +2. Editing Diets + - Test case 1: + * Edit a specific diet entry. + * Command: `edit-diet 2 calories/900 protein/40 carb/70 fat/25 datetime/2023-10-13 09:00` + * Expected Outcome: The 2nd diet entry is updated with the new values. + - Test case 2: + * Edit a diet entry with only one parameter. + * Command: `edit-diet 3 fat/30` + * Expected Outcome: Only the fat value of the 3rd diet entry is updated. + +3. Deleting Diets + - Test case 1: + * Delete a specific diet entry. + * Command: `delete-diet 2` + * Expected Outcome: The 2nd diet entry is successfully deleted. + - Test case 2: + * Attempt to delete a non-existent diet entry. + * Command: `delete-diet 5` + * Expected Outcome: Error indicating the diet entry does not exist. + +4. Listing Diets + - Test case 1: + * List all diet entries. + * Command: `list-diet` + * Expected Outcome: All existing diet entries are displayed. + +5. Finding Diets + - Test case 1: + * Find diets recorded on a specific date. + * Command: `find-diet 2023-10-12` + * Expected Outcome: Diets recorded on 12th October 2023 are displayed. + - Test case 2: + * Find diets on a date with no entries. + * Command: `find-diet 2023-11-01` + * Expected Outcome: No diets are displayed. + +#### Diet Goals + +1. Setting diet goals + * Prerequisite: There are no similar goals present + * Test case 1: + * There are no diet goals constructed. + * `set-diet-goal DAILY calories/500` creates a daily healthy calories goal with a target value of 500 + * Test case 2: + * There are no diet goals constructed. + * `set-diet-goal WEEKLY calories/500 fat/600` Creates 2 weekly healthy nutrient goals: calories and fat. + * Test case 3: + * There is a daily healthy calories goal present. + * `set-diet-goal DAILY calories/500` will result in an error since the goal is already present. + * Test case 4: + * There is a daily healthy calories goal present. + * `set-diet-goal DAILY unhealthy calories/500` will result in an error as a nutrient goal cannot be healthy + and unhealthy at the same time. + * Test case 5: + * There is a daily healthy calories goal present with a target value of 1000 + * `set-diet-goal WEEKLY healthy calories/500` will result in an error since the value of the daily diet goal + is greater than the value of weekly diet goal. +2. Listing diet goals + * Test case 1: + * `list-diet-goal` lists all the diet goals that are created and present in the diet goal records. +3. Deleting diet goals + * Test case 1: + * There is one diet goal present in the diet goal records. + * `delete-diet-goal 1` removes the goal from the diet goal records. + * Test case 2: + * `delete-diet-goal` without any index to delete the goal or non-positive integers provided + or the value is greater than the number of diet goals present in the diet goal records, error will be thrown. +4. Editing diet goals + * This is similar to setting diet goal, but the goal is required to be in the diet goals record first. + * Users are only allowed to edit the target value of the goal. There is no edit supported to edit diet goal + types or diet goal time span. + * Test case 1: + * No goals present in the records. + * `edit-diet-goal WEEKLY calories/5000` will return an error since there are no associated goals to + make an edit to the goal's target value. + * Test case 2: + * Weekly healthy calories goal is present with a target value of 20. + * `edit-diet-goal WEEKLY calories/5000` will update the target value of weekly healthy calories goal to 5000. + * Similar to setting diet goals, the weekly goal values should always be greater than the daily goal values. + + +### Sleep Management + +#### Sleep Records +1. Adding Sleep + - Test case 1: + * Add a sleep record. + * Command: `add-sleep start/2021-09-01 06:00 end/2021-09-01 07:00` + * Expected Outcome: Sleep record is successfully added with start time of 2021-09-01 06:00 and end time of + 2021-09-01 07:00. + - Test case 2: + * Attempt to add a sleep record with a future start time. + * Command: `add-sleep start/3024-01-01 08:00 end/3024-01-01 09:00` + * Expected Outcome: Error indicating the start time cannot be in the future. + - Test case 3: + * Attempt to add a sleep record with a start time later than the end time. + * Command: `add-sleep start/2021-09-01 08:00 end/2021-09-01 07:00` + * Expected Outcome: Error indicating the start time cannot be later than the end time. + +2. Editing Sleep + - Test case 1: + * Edit a specific sleep record. + * Command: `edit-sleep 2 start/2021-09-01 09:00 end/2021-09-01 10:00` + * Expected Outcome: The 2nd sleep record is updated with the new values. + - Test case 2: + * Edit a sleep record with only one parameter. + * Command: `edit-sleep 3 end/2021-09-01 11:00` + * Expected Outcome: Error indicating the start time is not specified. + - Test case 3: + * Edit a sleep record with a invalid index. + * Command: `edit-sleep -1011 start/2021-09-01 09:00 end/2021-09-01 10:00` + * Expected Outcome: Error indicating the index is invalid. + +3. Deleting Sleep + + **Assuming there are 4 sleep records in the sleep list** + - Test case 1: + * Delete a specific sleep record. + * Command: `delete-sleep 2` + * Expected Outcome: The 2nd sleep record is successfully deleted. + - Test case 2: + * Attempt to delete a non-existent sleep record. + * Command: `delete-sleep 5` + * Expected Outcome: Error indicating the sleep record does not exist. +4. Listing Sleep + - Test case 1: + * List all sleep records. + * Command: `list-sleep` + * Expected Outcome: All existing sleep records are displayed. + + +#### Sleep Goals +1. Setting sleep goals + + - Test case 1: + * Set a daily sleep duration goal. + * Command: `set-sleep-goal type/duration period/daily target/90` + * Expected Outcome: Daily sleep duration goal of 90 minutes is set successfully. + - Test case 2: + * Set a weekly sleep duration goal. + * Command: `set-sleep-goal type/duration period/weekly target/600` + * Expected Outcome: Weekly sleep duration goal of 600 minutes is set successfully. + - Test case 3: + * Attempt to set a duplicate daily sleep duration goal. + + **Assuming there is a daily sleep duration goal** + + * Command: `set-sleep-goal type/duration period/daily target/90` + * Expected Outcome: Error indicating the daily sleep duration goal already exists. + +2. Editing sleep goals + - Test case 1: + * Edit an existing daily sleep duration goal. + * Command: `edit-sleep-goal type/duration period/daily target/120` + * Expected Outcome: Daily sleep duration goal is updated to 120 minutes. + - Test case 2: + * Edit a non-existent weekly sleep duration goal. + * Command: `edit-sleep-goal type/duration period/weekly target/1000` + * Expected Outcome: Error indicating no existing weekly sleep duration goal. + +3. Listing sleep goals + - Test case 1: + * List all set sleep goals. + * Command: `list-sleep-goal` + * Expected Outcome: All set sleep goals along with their details are listed. + +### Miscellaneous + +1. Finding Records + * Test case: + * Command: `find-diet 2023-12-31` + * Expected Outcome: All records on 31st December 2023 are displayed. + +1. Saving Files + * Test case: + * Command: `save` + * Expected Outcome: Data are safely saved into the files. + +1. Exiting AthletiCLI: + * Test case 1: + * Immediately after detecting a format error in the saved files. + * Command: `bye` + * Expected Outcome: AthletiCLI is exited without rewriting the files. + * Test case 2: + * During normal execution. + * Command: `bye` + * Expected Outcome: AthletiCLI is exited and the files are safely saved. + +1. Viewing Help Messages: + * Test case 1: + * Command: `help` + * Expected Outcome: A list containing the syntax of all commands is shown. + * Test case 2: + * Command: `help add-diet` + * Expected Outcome: The syntax of the `add-diet` command is shown. diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000000..4aef0833b3 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } + +gem 'jekyll' +gem 'github-pages', group: :jekyll_plugins +gem 'wdm', '~> 0.1.0' if Gem.win_platform? +gem 'webrick' \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..cbca12285d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,19 @@ -# Duke +--- +permalink: / +layout: page +title: About AthletiCLI +feature_text: | + # AthletiCLI + Your all-in-one solution to track, analyse, and optimize your athletic performance. +feature_image: "https://picsum.photos/1300/400?image=989" +--- -{Give product intro here} +[![](https://github.com/AY2324S1-CS2113-T17-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2113-T17-1/tp/actions) -Useful links: -* [User Guide](UserGuide.md) -* [Developer Guide](DeveloperGuide.md) -* [About Us](AboutUs.md) +**AthletiCLI** is your all-in-one solution to track, analyse, and optimize your athletic performance. Designed for the +committed athlete, this command-line interface (CLI) tool not only keeps tabs on your physical activities but also +covers dietary habits, sleep metrics, and more. + +* If you are interested in using AthletiCLI, head over to the [User Guide](UserGuide.html). +* If you are interested about developing AthletiCLI, the [Developer Guide](DeveloperGuide.html) is a good place to start. +* If you would like to learn more about our development team, please visit the [About Us](AboutUs.html) page. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..47dc904f75 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,916 @@ -# User Guide +--- +layout: page +title: User Guide +--- +*Your all-in-one solution to track, analyse, and optimize your athletic performance.* +*Designed for the committed athlete, this command-line interface (CLI) tool not only keeps track of your physical +activities but also covers dietary habits, sleep metrics, and more.* -## Introduction +* Table of Contents +{:toc} -{Give a product intro} +## 🚀 Quick Start -## Quick Start +* ✅ Ensure you have the required runtime environment installed on your computer. +* ✅ Download the latest AthletiCLI from the official repository. +* ✅ Copy the downloaded file to a folder you want to designate as the home for AthletiCLI. +* ✅ Open a command terminal, cd into the folder where you copied the file, and run `java -jar AthletiCLI.jar` . -{Give steps to get started quickly} +## Features -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +**Notes about Command Format** -## Features +* Words in UPPER_CASE are parameters provided by the user. +* Parameters need to be specified in the given order unless specified otherwise. +* Parameters enclosed in square brackets [] are optional. -{Give detailed description of each feature} +**Notes about lack of Goal Delete for Sleep** -### Adding a todo: `todo` -Adds a new item to the list of todo items. +The absence of a "Goal Delete" feature for Sleep in the current version of AthletiCLI, while present for Diet and Activity, can be concisely justified as follows: -Format: `todo n/TODO_NAME d/DEADLINE` +1. **Diversity of Diet and Acitivity Goals:** The Diet and Activity features encompasses a wider range of goals compared to Sleep. With such variability, users might frequently need to delete diet goals, making a delete function more essential. -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +2. **Stability of Sleep Goals:** There are only 4 settable goals for sleep. This stability reduces the immediate need for a delete feature, as users are less likely to remove these goals frequently. -Example of usage: +3. **Planned for Future Implementation:** The absence of this feature in the current version for Sleep does not indicate it will never be implemented. It is planned for a future update, aligning with a phased development approach. -`todo n/Write the rest of the User Guide d/next week` +## 🏃 Activity Management -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +- [Adding Activities](#-adding-activities) +- [Deleting Activities](#-deleting-activities) +- [Listing Activities](#-listing-activities) +- [Editing Activities](#-editing-activities) +- [Setting Activity Goals](#-setting-activity-goals) +- [Editing Activity Goals](#-editing-activity-goals) +- [Listing Activity Goals](#-listing-activity-goals) +- [Deleting Activity Goals](#-deleting-activity-goals) + +### ➕ Adding Activities: + +`add-activity` `add-run` `add-swim` `add-cycle` + +You can record your activities in AtheltiCLI by adding different activities including running, cycling, and swimming. +A brief summary of the activity will be shown after adding the activity. Use the detailed list command to access the +full activity insights. + +**Syntax:** + +* `add-activity CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME` +* `add-run CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION` +* `add-swim CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME style/STYLE` +* `add-cycle CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION` + +**Parameters:** + +* CAPTION: A short description of the activity. +* DURATION: The duration of the activity in ISO Time Format: HH:mm:ss. +* DISTANCE: The distance of the activity in meters. It must be a non-negative number smaller than 1000001. +* DATETIME: The date and time of the start of the activity. It must follow the ISO Date Time Format yyyy-MM-dd HH:mm, + must be valid, and cannot be in the future. +* ELEVATION: The elevation gain of a run or cycle in meters. It must be a number with an absolute value smaller than +10001. +* STYLE: The style of the swim. It must be one of the following: freestyle, backstroke, breaststroke, butterfly. + +**Examples:** + +* `add-activity Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01 06:00` +* `add-cycle Evening Ride duration/02:00:00 distance/20000 datetime/2021-09-01 18:00 elevation/1000` +* `add-swim Evening Swim duration/01:00:00 distance/1000 datetime/2023-10-16 20:00 style/freestyle` + +--- + +### ➖ Deleting Activities: + +`delete-activity` + +Accidentally added an activity? You can quickly delete any type of activity including run, swims and cycles by using +the following command. + +**Syntax:** + +* `delete-activity INDEX` + +**Parameters:** + +* INDEX: The index of the activity as shown in the displayed activity list. Note, that the list is sorted by + date and that the index must be a positive number which is not larger than the number of activities recorded. + +**Examples:** + +* `delete-activity 2` Deletes the second activity in the activity list. +* `delete-activity 1` Deletes the most recent activity in the activity list. + +--- + +### 📅 Listing Activities: + +`list-activity` + +By using this command, you can see all your tracked activities in a list sorted by date. For more +detailed information about your activities including evaluations like pace (running), speed (cycling) or lap time +(swimming), you can use the `-d` flag. + +**Syntax:** + +* `list-activity [-d]` + +**Flags:** + +* `-d`: Shows a detailed list of the activities. + +**Metrics:** +* Pace: the average time taken to run 1km. Common performance metric for runners. +* Speed: the average speed of the cycle in km/h. Common performance metric for cyclists. +* Lap Time: the time taken to swim 1 lap (50m). Common performance metric for swimmers. + +**Examples:** + +* `list-activity` Shows a brief overview of all activities. +

+ List returned by `list-activity` +

+ +* `list-activity -d` Shows a detailed summary of all activities. +

+ Detailed list returned by `list-activity -d` +

+ +--- + +### ⚙️ Editing Activities: + +`edit-activity` `edit-run` `edit-swim` `edit-cycle` + +You can edit your activities in AthletiCLI by editing the activity at the specified index. +Specify the parameters you want to edit with the corresponding flags. At least one parameter must be specified. + +**Syntax:** + +* `edit-activity INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME]` +* `edit-run INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [elevation/ELEVATION]` +* `edit-swim INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [style/STYLE]` +* `edit-cycle INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [elevation/ELEVATION]` + +**Parameters:** + +* INDEX: The index of the activity to be edited as shown in the displayed activity list - must be a positive number + which is not larger than the number of activities recorded. Note, that the indices are allocated based on the date of the activity. +* See [adding activities](#-adding-activities) for the other parameters. + +**Examples:** + +* `edit-activity 1 caption/Morning Run distance/10000` +* `edit-cycle 2 datetime/2021-09-01 18:00 elevation/1000` + +--- + +### 🔍 Finding Activities: + +`find-activity` + +You can find all your activities on a specific date in AtheltiCLI. + +**Syntax:** + +* `find-activity DATE` + +**Parameters:** + +* DATE: The date of the activity. It must follow the ISO Date Format: yyyy-MM-dd, must be valid and cannot be in + the future. + +**Example:** + +* `find-activity 2021-09-01` + +--- + +### 🎯 Setting Activity Goals: + +`set-activity-goal` + +You can set goals for specific sports by defining target distance or duration over various periods. The goals can track +your daily, weekly, monthly, or yearly progress. + +**Syntax** + +* `set-activity-goal sport/SPORT type/TYPE period/PERIOD target/TARGET` + +**Parameters** + +* SPORT: The sport for which to set a goal. Options: running, cycling, swimming, general. +* TYPE: The metric for the goal. Options: distance, duration. +* PERIOD: The period for the goal. Options: daily, weekly, monthly, yearly. Only activities that are recorded within + the period will be counted towards the goal. +* TARGET: The target value. It must be a non-negative number smaller than 2^31-1. For distance, in meters. For + duration, in minutes. + +**Examples** + +* `set-activity-goal sport/running type/distance period/weekly target/10000` Sets a goal of running 10km per week. +* `set-activity-goal sport/swimming type/duration period/monthly target/120` Sets a goal of swimming for 2 hours + per month. + +--- + +### ⚙️ Editing Activity Goals: + +`edit-activity-goal` + +You can edit your set goals by specifying the sport, target, and period. + +**Syntax** + +* `edit-activity-goal sport/SPORT type/TYPE period/PERIOD target/TARGET` + +**Parameters** + +* TARGET: The new target value. For distance (in meters), for duration (in minutes). +* See [setting activity goals](#-setting-activity-goals) for the other parameters. + +**Examples** + +* `edit-activity-goal sport/running type/distance period/weekly target/20000` Adjusts the goal of running distance + to 20km per week. +* `edit-activity-goal sport/swimming type/duration period/monthly target/60` Adjusts the goal of swimming duration + to 1 hour per month. + +--- + +### 📅 Listing Activity Goals: + +`list-activity-goal` + +You can list all your set goals and view your progress towards them. + +**Syntax** + +* `list-activity-goal` + +**Examples** + +* `list-activity-goal` Lists all activity goals. +

+ List returned by `list-activity-goal` +

+ +--- + +### ➖ Deleting Activity Goals: + +`delete-activity-goal` + +You can delete your set goals by specifying the sport, target, and period. + +**Syntax** + +* `delete-activity-goal sport/SPORT type/TYPE period/PERIOD` + +**Parameters** + +* See [setting activity goals](#-setting-activity-goals) for the parameters. + +**Examples** + +* `delete-activity-goal sport/running type/distance period/weekly` Deletes the weekly running distance goal. +* `delete-activity-goal sport/swimming type/duration period/monthly` Deletes the monthly swimming duration goal. + +--- + +## 🍏 Diet Management + +- [Adding Diets](#-adding-diets) +- [Editing Diets](#-editing-diets) +- [Deleting Diets](#-deleting-diets) +- [Listing Diets](#-listing-diets) +- [Finding Diets](#-finding-diets) +- [Adding Diet Goals](#-adding-diet-goals) +- [Deleting Diet Goals](#-deleting-diet-goals) +- [Listing Diet Goals](#-listing-diet-goals) +- [Editing Diet Goals](#-editing-diet-goals) + +### ➕ Adding Diets: + +`add-diet` + +You can record your diet by specifying calories, protein, carbohydrate, and fat intake. + +**Syntax:** + +* `add-diet calories/CALORIES protein/PROTEIN carb/CARB fat/FAT datetime/DATETIME` + +**Parameters:** + +* CALORIES: Total calories (in cal) of the meal. +* PROTEIN: Total protein (in milligrams) of the meal. +* CARB: Total carbohydrates (in milligrams) of the meal. +* FAT: Total fat (in milligrams) of the meal. +* DATETIME: Date and time of the meal in ISO Date Time Format (yyyy-MM-dd HH:mm). It must be valid and cannot be in the + future. + +**Examples:** + +* `add-diet calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00` Adds a diet entry with 500 calories, + 20mg of protein, 50mg of carbohydrates, and 10mg of fat on 1st September 2021 at 6am. +* `add-diet calories/2000 datetime/2023-09-01 16:00 fat/10 carb/100 protein/200` Adds a diet entry with 2000 calories, + 200mg of protein, 100mg of carbohydrates, and 10mg of fat on 1st September 2023 at 4pm. + +--- + +### ⚙️ Editing Diets: + +`edit-diet` + +You can modify existing diet entries by specifying the index of the diet you wish to edit. + +**Syntax:** + +* `edit-diet INDEX [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT] [datetime/DATETIME]` + +**Parameters:** + +* INDEX: Index of the diet entry (positive integer). +* See [adding diets](#-adding-diets) for the other parameters. + +**Examples:** + +* `edit-diet 1 calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00` Edits the first diet entry. +* `edit-diet 1 protein/215` Edits the first diet entry to have 215mg of protein. + +*Note: Find the index of your diet entry in the listing section.* + +--- + +### ➖ Deleting Diets: + +`delete-diet` + +You can remove a diet entry from your records. + +**Syntax:** + +* `delete-diet INDEX` + +**Parameters:** + +* INDEX: Index of the diet to be deleted (positive integer). + +**Examples:** + +* `delete-diet 1` Deletes the first diet entry. + +--- + +### 📅 Listing Diets: + +`list-diet` + +You can view a list of all your recorded diets. + +**Syntax:** + +* `list-diet` + +**Examples:** + +* `list-diet` Lists all diets. +

+ List returned by `list-diet` +

+ +--- + +### 🔍 Finding Diets: + +`find-diet DATE` + +You can locate diets recorded on a specific date. + +**Syntax:** + +* `find-diet DATE` + +**Parameters:** + +* DATE: Date of the diet in ISO Date Format (yyyy-MM-dd). It must be valid and cannot be in the future. + +**Examples:** + +* `find-diet 2021-09-01` Finds diets recorded on 1st September 2021. + +--- + +### 🎯 Adding Diet Goals: + +`set-diet-goal` + +You can create a new daily or weekly diet goal to track your nutrients intake with AtheltiCLI by adding the nutrients you wish to track and the target value for your nutrient goals. + +You can set multiple nutrients goals at once with the `set-diet-goal` command. + +Do note that you can only set up to the value 999999 and the maximum accumulated value from diets is 1000000. + + +**Syntax:** + +* `set-diet-goal [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT]` + +**Parameters:** + +* DAILY/WEEKLY: Determines if the goal is set for a day or set for the week. It accepts 2 values. + DAILY goals account for what you eat for the day. + WEEKLY goals account for what you eat for the week. +* unhealthy: This determines if you are trying to get more of this nutrient or less of it. + If this flag is placed, it means that you are trying to reduce the intake. Hence, exceeding the target value means + that you have not achieved your goal. If this flag is absent, it means that you are trying to increase the intake. + It is considered achieved if you exceed the target value indicated. +* CALORIES: Your target value for calories intake, in terms of calories. The target value must be a positive integer up to the value 999999. +* PROTEIN: Your target for protein intake, in terms of milligrams. The target value must be a positive integer up to the value 999999. +* CARB: Your target value for carbohydrate intake, in terms of milligrams. The target value must be a positive integer up to the value 999999. +* FAT: Your target value for fat intake, in terms of milligrams. The target value must be a positive integer up to the value 999999. + +You can create one or multiple nutrient goals at once with this command. + +**Note: At least one of the nutrients (CALORIES,PROTEIN,CARB,FAT) must be present!** + +**Note: A diet goal of the same nutrient cannot be healthy and unhealthy at the same time!** + +**Note: No repetitions are allowed for the diet goal of the same nutrient and the same time span.** + +**Note: The target value for a weekly goal must be greater than the target value of a daily goal of the same nutrient!** + + +**Examples:** + +* `set-diet-goal WEEKLY calories/500 fat/600` Creates 2 weekly nutrient goals if they have not been created: +calories with a target value of 500 calories and fat of 600 mg. + +* `set-diet-goal DAILY calories/500` Creates a daily calories goal of target value of 500 calories if goal is not created. + +** `set-diet-goal DAILY unhealthy calories/500` Creates an unhealthy daily calories goal of target value of +500 calories if goal is not created. + +**Example of Usage:** + +``` + > set-diet-goal WEEKLY calories/500 fat/600 + _____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] WEEKLY calories intake progress: (0/500) + + 2. [HEALTHY] WEEKLY fat intake progress: (0/600) + + Now you have 2 diet goal(s). + _____________________________________________________________ +``` + +--- + +### ➖ Deleting Diet Goals: + +`delete-diet-goal` + +You can delete your diet goals in AtheltiCLI by deleting the goal at the specified index. +This index will be referenced via `list-diet-goal` command. + +**Syntax:** + +* `delete-diet-goal INDEX` + +**Parameters:** + +* INDEX: The index of the diet goal to be deleted. It must be a positive integer and +it is bounded by the number of diet goals available. + +**Examples:** + +* `delete-diet-goal 1` Deletes a diet goal that is located on the first index of the list if it exists. + +**Example of Usage:** + +``` +____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] WEEKLY calories intake progress: (0/500) + + 2. [HEALTHY] WEEKLY fat intake progress: (0/600) + + Now you have 2 diet goal(s). +____________________________________________________________ + +> delete-diet-goal 1 +____________________________________________________________ + The following goal has been deleted: + + [HEALTHY] WEEKLY calories intake progress: (0/500) + +____________________________________________________________ +``` + +--- + +### 📅 Listing Diet Goals: + +`list-diet-goal` + +You can list all your diet goals in AtheltiCLI. + +**Syntax:** + +* `list-diet-goal` + +**Examples:** + +* `list-diet-goal` + +**Example of Usage:** + +``` +> list-diet-goal +____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] WEEKLY fat intake progress: (0/600) + + Now you have 1 diet goal(s). +____________________________________________________________ +``` + +--- + +### ⚙️️ Editing Diet Goals: + +`edit-diet-goal` + +You can edit the target value of your diet goals in AtheltiCLI, redefining the target value for the specified nutrient. + +This command takes in at least 2 arguments. You are able to edit multiple diet goals target value of the same time frame at once. +No repetition is allowed. The diet goal needs to be present before any edits is allowed. + +Do note that you can only set up to the value 999999 and the maximum accumulated value from diets is 1000000. + +**Syntax:** + +* `edit-diet-goal [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT]` + +**Parameters:** + +* DAILY/WEEKLY: This determines if the goal you want to edit is a daily goal or a weekly goal. It accepts 2 values. + DAILY goals account for what you eat for the day. + WEEKLY goals account for what you eat for the week. +* unhealthy: This determines if you are trying to get more of this nutrient or less of it. +This flag is used to change target values of goals that are set as unhealthy previously. + +* CALORIES: Your target value for calories intake, in terms of cal. The target value must be a positive integer up to the value 999999. +* PROTEIN: The target for protein intake, in terms of milligrams. The target value must be a positive integer up to the value 999999. +* CARB: Your target value for carbohydrate intake, in terms of milligrams. The target value must be a positive integer up to the value 999999. +* FAT: Your target value for fat intake, in terms of milligrams. The target value must be a positive integer up to the value 999999. + + +**Note: At least one of the nutrients (CALORIES,PROTEIN,CARB,FAT) must be present!** + +**Note: The target value for a weekly goal must be greater than the target value of a daily goal of the same nutrient!** + +You can edit one or multiple nutrient goals with this command. + +**Examples:** + +* `edit-diet-goal DAILY calories/5000 protein/200 carb/500 fat/100` +Edits multiple nutrients goals if all of them exists and the corresponding new target value is valid. +* `edit-diet-goal WEEKLY calories/5000` +Edits a single calories goal target value to 5000 calories if the goal exists and new target value is valid. + +**Example of Usage:** + +``` +____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] WEEKLY fat intake progress: (0/600) + + Now you have 1 diet goal(s). +____________________________________________________________ + +> edit-diet-goal WEEKLY fat/50 +____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] WEEKLY fat intake progress: (0/50) + + Now you have 1 diet goal(s). +____________________________________________________________ +``` + +--- + +## 🛌 Sleep Management + +- [Adding Sleep](#-adding-sleep) +- [Listing Sleep](#-listing-sleep) +- [Deleting Sleep](#-deleting-sleep) +- [Editing Sleep](#-editing-sleep) +- [Finding Sleep](#-finding-sleep) +- [Adding Sleep Goals](#-adding-sleep-goals) +- [Listing Sleep Goals](#-listing-sleep-goals) +- [Editing Sleep Goals](#-editing-sleep-goals) + +### ➕ Adding Sleep: + +`add-sleep` + +You can record your sleep timings in AtheltiCLI by adding your sleep start and end time. It also automagically calculates for you the duration of your sleep, as well as the sleep date. + +**Syntax:** + +* `add-sleep start/START end/END` + +**Parameters:** + +* START: The start time of the sleep. It must follow the ISO Date Time Format: yyyy-MM-dd HH:mm, must be valid + and cannot be in the future. + +* END: The end time of the sleep. It must follow the ISO Date Time Format: yyyy-MM-dd HH:mm, must be valid and + cannot be in the future. + +**Examples:** + +Take note that all sleep entries have an associated date. + +All sleep entries with a start time before 06:00 will be taken to represent the previous days sleep. + +* `add-sleep start/2023-01-20 02:00 end/2023-01-20 08:00` will be taken to represent the sleep record on `2022-01-19`, which is the day before, since the start time is before 06:00 on `2022-01-20`. + +* `add-sleep start/2022-01-20 22:00 end/2022-01-21 06:00` will be taken to represent the sleep record on `2022-01-20`, since the start time is after 06:00 on `2022-01-20`. + +--- + +### 📅 Listing Sleep: + +`list-sleep` + +You can see all your tracked sleep records in a list by using this command. + +**Syntax:** `list-sleep` + +**Example:** `list-sleep` + +--- + +### ➖ Deleting Sleep: + +`delete-sleep` + +Accidentally added a sleep record? You can quickly delete sleep records by using the following command. +The index must be a positive number and is not larger than the number of sleep records recorded. + +**Syntax:** + +* `delete-sleep INDEX` + +**Parameters:** + +* INDEX: The index of the sleep record you wish to delete. It must be a positive number and is not larger than the number of sleep records recorded. +Refer to the list-sleep command for the index of the sleep record you wish to delete. + +**Examples:** + +Assuming that there are 5 sleep records in the list: + +* `delete-sleep 5` will delete the 5th sleep record in the sleep records list. +* `delete-sleep 1` will delete the 1st sleep record in the sleep records list. + +--- + +### ⚙️️ Editing Sleep: + +`edit-sleep` + +You can modify existing sleep records in AtheltiCLI by specifying the sleep's index and then providing the new start and end times. + +**Syntax:** + +* `edit-sleep INDEX start/START end/END` + +**Parameters:** + +* INDEX: The index of the sleep record you wish to edit. It must be a positive number and is not larger than the number of sleep records recorded. +* START: The new start time of the sleep. It must follow the ISO Date Time Format: yyyy-MM-dd HH:mm, must be + valid and cannot be in the future. +* END: The new end time of the sleep. It must follow the ISO Date Time Format: yyyy-MM-dd HH:mm, must be valid and + cannot be in the future. + +**Examples:** + +Assuming that there are 5 sleep records in the list: + +* `edit-sleep 5 start/2023-01-20 02:00 end/2023-01-20 08:00` will edit the 5th sleep record in the sleep records list to have a start time of `2023-01-20 02:00` and an end time of `2023-01-20 08:00`. + +* `edit-sleep 1 start/2022-01-20 22:00 end/2022-01-21 06:00` will edit the 1st sleep record in the sleep records list to have a start time of `2022-01-20 22:00` and an end time of `2022-01-21 06:00`. + +--- + +### 🔍 Finding Sleep: + +`find-sleep DATE` + +You can find your sleep record on a specific date in AtheltiCLI. + +**Syntax:** + +* `find-sleep DATE` + +**Parameters:** + +* DATE: The date of the sleep. It must follow the ISO Date Format: yyyy-MM-dd, must be valid and cannot be in the + future. + +**Examples:** + +* `find-sleep 2021-09-01` + +--- + +### 🎯 Setting Sleep Goals: + +`set-sleep-goal` + +You can set goals for your sleep AthletiCLI by setting the target duration specified in minutes. Tracking can be done for the past day, week, month or year. + +**NOTE: Only one sleep goal can be set for each time period.** + +**Syntax:** +* `set-sleep-goal type/TYPE period/PERIOD target/TARGET` + +**Parameters:** +* TYPE: The type of sleep goal. It must be the following: `duration`. +* PERIOD: The period for which you want to set a goal. It must be one of the following: `daily, weekly, monthly, yearly`. Only sleeps that are recorded within the period from the current time will be counted towards the goal. +* TARGET: The target value. It must be a positive number. For duration, it is in minutes. + +**Examples:** +* `set-sleep-goal type/duration period/daily target/420` Sets a goal of sleeping 7 hours per day. +* `set-sleep-goal type/duration period/weekly target/2940` Sets a goal of sleeping 49 hours per week. + +--- + +### 📅 Editing Sleep Goals: + +`edit-sleep-goal` + +You can edit your already set sleep goals by mentioning the type, period, and target of the goal you want to edit. + +**Syntax:** +* `edit-sleep-goal type/TYPE period/PERIOD target/TARGET` + +**Parameters:** +* TYPE: The type of sleep goal. It must be the following: `duration`. +* PERIOD: The period for which you want to set a goal. It must be one of the following: `daily, weekly, monthly, yearly`. Only sleeps that are recorded within the period from the current timewill be counted towards the goal. +* TARGET: The target value. It must be a positive number. For duration, it is in minutes. + +**Examples:** +* `edit-sleep-goal type/duration period/daily target/360` Edits the daily goal to sleeping 6 hours per day. +* `edit-sleep-goal type/duration period/weekly target/2520` Edits the weekly goal to sleeping 42 hours per week. + +--- + +### 📅 Listing Sleep Goals: + +`list-sleep-goal` + +You can list all your sleep goals in AthletiCLI and see your progress towards them. + +--- + +## Miscellaneous + +### 🔍 Finding Records: + +You can find all your records, including activities, sleeps, and diets, on a specific date in AtheltiCLI. + +**Syntax:** + +* `find DATE` + +**Parameters:** + +* `DATE`: The date of the records. It must follow the ISO Date Format `yyyy-MM-dd`, must be valid and cannot be in + the future. + +**Example:** + +* `find 2023-11-01` + +--- + +### 📦 Saving Files: + +You can save files while using AthletiCLI if you want to, rather than waiting until the AthletiCLI exits to automatically save them. + +**Syntax:** + +* `save` + +--- + +### 👋 Exiting AthletiCLI: + +You can use the `bye` command at any time to safely store the file and exit AthletiCLI. + +**Syntax:** + +* `bye` + +--- + +### ℹ️ Viewing Help Messages: + +If you forget a command, you can always use the `help` command to see their syntax. + +**Syntax:** + +* `help [COMMAND]` + +**Parameters:** + +* `COMMAND`: The command you want to view. If it is omitted, a list containing the syntax of all commands will be shown. + +**Examples:** + +* `help` lists the syntax of all commands. +* `help add-diet` shows the syntax of the `add-diet` command. + +--- ## FAQ + **Q: *Am I allowed to update the storage files?*** + + **A**: + While it is generally advisable not to edit the contents of the storage file, you do have the option to make updates. + Please exercise caution when doing so. Incorrect edits to the storage file can result in data loss. If AthleticCLI + encounters incorrect format of the file contents, it will prompt you to exit using the + [bye](#exiting-athleticli) command. + Continuing with the program in such cases will lead to the deletion of all data in the file. + + +--- +## Summary of Commands + +### Activity Management + +| **Command** | **Syntax** | **Parameters** | **Examples** | +|---------------------------|-----------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------------------------------------------------------| +| `add-activity` | `add-activity CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME` | CAPTION, DURATION, DISTANCE, DATETIME | `add-activity Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01 06:00` | +| `add-run` | `add-run CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION` | CAPTION, DURATION, DISTANCE, DATETIME, ELEVATION | `add-run NY Marathon duration/03:33:17 distance/42125 datetime/2023-11-05 07:00` | +| `add-swim` | `add-swim CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME style/STYLE` | CAPTION, DURATION, DISTANCE, DATETIME, STYLE | `add-swim Evening Swim duration/01:00:00 distance/1000 datetime/2023-10-16 20:00 style/freestyle` | +| `add-cycle` | `add-cycle CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION` | CAPTION, DURATION, DISTANCE, DATETIME, ELEVATION | `add-cycle Evening Ride duration/02:00:00 distance/20000 datetime/2021-09-01 18:00 elevation/1000` | +| `delete-activity` | `delete-activity INDEX` | INDEX | `delete-activity 2` | +| `list-activity` | `list-activity [-d]` | -d | `list-activity`, `list-activity -d` | +| `edit-activity` | `edit-activity INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME]` | INDEX, CAPTION, DURATION, DISTANCE, DATETIME | `edit-activity 1 caption/Morning Run distance/10000` | +| `edit-run` | Similar to `edit-activity` but with elevation. | Same as `edit-activity` with ELEVATION | - | +| `edit-swim` | Similar to `edit-activity` but with style.
| Same as `edit-activity` with STYLE | - | +| `edit-cycle` | Similar to `edit-activity` but with elevation. | Same as `edit-activity` with ELEVATION | `edit-cycle 2 datetime/2021-09-01 18:00 elevation/1000` | +| `find-activity` | `find-activity DATE` | DATE | `find-activity 2021-09-01` | +| `set-activity-goal` | `set-activity-goal sport/SPORT type/TYPE period/PERIOD target/TARGET` | SPORT, TYPE, PERIOD, TARGET | `set-activity-goal sport/running type/distance period/weekly target/10000` | +| `edit-activity-goal` | `edit-activity-goal sport/SPORT type/TYPE period/PERIOD target/TARGET` | SPORT, TYPE, PERIOD, TARGET | `edit-activity-goal sport/running type/distance period/weekly target/20000` | +| `list-activity-goal` | `list-activity-goal` | None | `list-activity-goal` | +| `delete-activity-goal` | `delete-activity-goal sport/SPORT type/TYPE period/PERIOD` | SPORT, TYPE, PERIOD | `delete-activity-goal sport/running type/distance period/weekly` | + +### Diet Management + +| **Command** | **Syntax** | **Parameters** | **Examples** | +|---------------------------|----------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------------| +| `add-diet` | `add-diet calories/CALORIES protein/PROTEIN carb/CARB fat/FAT datetime/DATETIME` | CALORIES, PROTEIN, CARB, FAT, DATETIME | `add-diet calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00` | +| `edit-diet` | `edit-diet INDEX [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT] [datetime/DATETIME]` | INDEX, [CALORIES], [PROTEIN], [CARB], [FAT], [DATETIME] | `edit-diet 1 calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00` | +| `delete-diet` | `delete-diet INDEX` | INDEX | `delete-diet 1` | +| `list-diet` | `list-diet` | None | `list-diet` | +| `find-diet` | `find-diet DATE` | DATE | `find-diet 2021-09-01` | +| `set-diet-goal` | `set-diet-goal [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT]` | DAILY/WEEKLY, [unhealthy], [CALORIES], [PROTEIN], [CARB], [FAT] | `set-diet-goal WEEKLY calories/500 fat/600` | +| `edit-diet-goal` | `edit-diet-goal [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT]` | DAILY/WEEKLY, [unhealthy], [CALORIES], [PROTEIN], [CARB], [FAT] | `edit-diet-goal WEEKLY calories/500 fat/600` | +| `delete-diet-goal` | `delete-diet-goal INDEX` | INDEX | `delete-diet-goal 1` | +| `list-diet-goal` | `list-diet-goal` | None | `list-diet-goal` | + + +### Sleep Management -**Q**: How do I transfer my data to another computer? -**A**: {your answer here} +| **Command** | **Syntax** | **Parameters** | **Examples** | +|---------------------------|-------------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------------------------------| +| `add-sleep` | `add-sleep start/START end/END` | START, END | `add-sleep start/2023-01-20 02:00 end/2023-01-20 08:00` | +| `list-sleep` | `list-sleep` | None | `list-sleep` | +| `delete-sleep` | `delete-sleep INDEX` | INDEX | `delete-sleep 1` | +| `edit-sleep` | `edit-sleep INDEX start/START end/END` | INDEX, START, END | `edit-sleep 1 2023-01-20 02:00 2023-01-20 08:00` | +| `find-sleep` | `find-sleep DATE` | DATE | `find-sleep 2021-09-01` | +| `set-sleep-goal` | `set-sleep-goal type/TYPE period/PERIOD target/TARGET` | TYPE, PERIOD, TARGET | `set-sleep-goal type/duration period/daily target/420` | +| `edit-sleep-goal` | `edit-sleep-goal type/TYPE period/PERIOD target/TARGET` | TYPE, PERIOD, TARGET | `edit-sleep-goal type/duration period/daily target/360` | +| `list-sleep-goal` | `list-sleep-goal` | None | `list-sleep-goal` | -## Command Summary -{Give a 'cheat sheet' of commands here} +### Miscellaneous -* Add todo `todo n/TODO_NAME d/DEADLINE` +| **Command** | **Syntax** | **Parameters** | **Examples** | +|---------------------------|-------------------------------------------------------------------------------------|--------------------------------------------------------|----------------------------------------------------------| +| `find` | `find DATE` | DATE | `find 2023-11-01` | +| `save` | `save` | None | `save` | +| `bye` | `bye` | None | `bye` | +| `help` | `help [COMMAND]` | [COMMAND] | `help`, `help add-diet` | diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..250fbcd2af --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,41 @@ +remote_theme: skylee03/alembic@4.1.1 +plugins: + - jekyll-remote-theme + - jekyll-redirect-from + - jemoji +markdown: kramdown +sass: + style: compressed +encoding: utf-8 +lang: en-US +title: "AthletiCLI" +url: "https://ay2324s1-cs2113-t17-1.github.io/tp/" +repo: "https://github.com/AY2324S1-CS2113-T17-1/tp" +css_inline: true +fonts: + preconnect_urls: + - https://fonts.gstatic.com + font_urls: + - https://fonts.googleapis.com/css2?family=Alegreya:wght@400;700&display=swap +navigation_header: + - title: Home + url: / + - title: User Guide + url: /UserGuide.html + - title: Developer Guide + url: /DeveloperGuide.html + - title: About Us + url: /AboutUs.html + - title: GitHub + url: https://github.com/AY2324S1-CS2113-T17-1/tp +navigation_footer: + - title: Home + url: / + - title: User Guide + url: /UserGuide.html + - title: Developer Guide + url: /DeveloperGuide.html + - title: About Us + url: /AboutUs.html + - title: GitHub + url: https://github.com/AY2324S1-CS2113-T17-1/tp \ No newline at end of file diff --git a/docs/images/ActivityGoalEvaluation.svg b/docs/images/ActivityGoalEvaluation.svg new file mode 100644 index 0000000000..a30507f6f3 --- /dev/null +++ b/docs/images/ActivityGoalEvaluation.svg @@ -0,0 +1 @@ +:ActivityGoaldata:Dataactivities:ActivityListisAchieved(data)getCurrentValue(data)getActivityClass()activityClassgetActivities()activitiesgetTotalDistance/Duration(activityClass, timespan)filterByTimespan(timespan)filteredActivitiestotalisAchieved \ No newline at end of file diff --git a/docs/images/ActivityInheritance.svg b/docs/images/ActivityInheritance.svg new file mode 100644 index 0000000000..aec653da54 --- /dev/null +++ b/docs/images/ActivityInheritance.svg @@ -0,0 +1 @@ +Activity-caption-duration-movingTime-distance-startDateTimeRun-elevationGain-averagePaceCycle-elevationGain-averageSpeedSwim-laps-style-averageLapTimeActivityListDataconsists of *1 \ No newline at end of file diff --git a/docs/images/ActivityObjectDiagram.svg b/docs/images/ActivityObjectDiagram.svg new file mode 100644 index 0000000000..ddc5939874 --- /dev/null +++ b/docs/images/ActivityObjectDiagram.svg @@ -0,0 +1 @@ +activities:ActivityListrun1:Rundistance = 5000startDateTime = 2023-11-11run2:Rundistance = 5000startDateTime = 2023-11-10swim:Swimdistance = 1000startDateTime = 2023-10-11cycle:Cycledistance = 40000startDateTime = 2023-10-10activity:Activitydistance = 100startDateTime = 2023-10-09 \ No newline at end of file diff --git a/docs/images/ActivityParsing.svg b/docs/images/ActivityParsing.svg new file mode 100644 index 0000000000..a9a6f70fec --- /dev/null +++ b/docs/images/ActivityParsing.svg @@ -0,0 +1 @@ +«class»Parser«class»ActivityParserac:ActivityChangesa:ActivityparseActivity(arguments)ActivityChanges()acparseActivityArguments(ac, arguments, separators)loop[for each separator]checkMissingActivityArgument(separatorIndex, separator)parseSegment(ac, segment, separator)alt[depending on separator]setCaption(segment)setDuration(parseDuration(segment))other setterssetDistance(parseDistance(segment))getCaption()captionother gettersgetStartDateTime()startDateTimeActivity(caption, duration, ...)aa \ No newline at end of file diff --git a/docs/images/AddActivity.svg b/docs/images/AddActivity.svg new file mode 100644 index 0000000000..325709b8fe --- /dev/null +++ b/docs/images/AddActivity.svg @@ -0,0 +1 @@ +:AthletiCLI«class»Parser«class»ActivityParsera:Activityc:AddActivityCommanddata:Dataactivities:ActivityListparseCommand(userInput)parseActivity(arguments)Activity()aaAddActivityCommand(a)ccexecute(a, data)getActivities()activitiesadd(a)message \ No newline at end of file diff --git a/docs/images/AddActivityGoal.svg b/docs/images/AddActivityGoal.svg new file mode 100644 index 0000000000..fc07a6ebac --- /dev/null +++ b/docs/images/AddActivityGoal.svg @@ -0,0 +1 @@ +:AthletiCLI«class»Parser«class»ActivityParsergoal:ActivityGoalc:SetActivityGoalCommanddata:Datagoals:ActivityGoalListparseCommand(userInput)parseActivityGoal(arguments)ActivityGoal()goalgoalSetActivityGoalCommand(goal)ccexecute(goal, data)getActivityGoals()goalsadd(goal)message \ No newline at end of file diff --git a/docs/images/AthletiCLI-Banner.png b/docs/images/AthletiCLI-Banner.png new file mode 100644 index 0000000000..4cd2dbab65 Binary files /dev/null and b/docs/images/AthletiCLI-Banner.png differ diff --git a/docs/images/DataClassDiagram.svg b/docs/images/DataClassDiagram.svg new file mode 100644 index 0000000000..16b3115b02 --- /dev/null +++ b/docs/images/DataClassDiagram.svg @@ -0,0 +1 @@ +StorableList-path: String+StorableList(path: String): StorableList+load()+save()+parse(s:String)+unparse(t:T)«interface»Findable+find(date: LocalDate): ArrayList<T>Data+getInstance():Data+load(): void+save(): void+clear(): voidActivityList+find(date: LocalDate): ArrayList<T>+parse(s:String)+unparse(t:T)DietList+find(date: LocalDate): ArrayList<T>+parse(s:String)+unparse(t:T)SleepList+find(date: LocalDate): ArrayList<T>+parse(s:String)+unparse(t:T)ActivityGoalList+parse(s:String)+unparse(t:T)DietGoalList+parse(s:String)+unparse(t:T)SleepGoalList+parse(s:String)+unparse(t:T)contains11contains11contains11contains11contains11contains11 \ No newline at end of file diff --git a/docs/images/DietGoalClassDiagram.svg b/docs/images/DietGoalClassDiagram.svg new file mode 100644 index 0000000000..9821ac1812 --- /dev/null +++ b/docs/images/DietGoalClassDiagram.svg @@ -0,0 +1 @@ +Goal-timeSpan:TimeSpan+Goal(timeSpan: TimeSpan): Goal+getTimeSpan():TimeSpan+checkData(date: LocalDate, timeSpan: TimeSpan): boolean+isAcheived(data:Data): booleanDietGoal#nutrient: String#targetValue: int#type: String#achievedSymbol: String#unachievedSymbol: String-dietGoalStringRepresentation: String+DietGoal(timeSpan: TimeSpan, nutrient: String, targetValue:int): DietGoal+getNutrient(): String+getTargetValue(): int+getCurrentValue(data:Data): int+getType(): String+setTargetValue(targetValue: int): void+isSameType(dietGoal: DietGoal): boolean+isSameNutrient(dietGoal: DietGoal): boolean+isSameTimeSpan(dietGoal: DietGoal): boolean+toString(data: Data): String#getSymbol(data: Data): StringHealthyDietGoal+TYPE: String#healthyDietGoalSymbol: String#healthyDietGoalStringRepresentation: String-isHealthy: boolean+HealthyDietGoal(timeSpan: TimeSpan, nutrient: String, targetValue: int): HealthyDietGoal+getType(): String+toString(data: Data): StringUnhealthyDietGoal+TYPE: String#achievedSymbol#unachievedSymbol#unhealthyDietGoalSymbol: String#unhealthyDietGoalStringRepresentation: String-isHealthy: boolean+UnhealthyDietGoal(timeSpan: TimeSpan, nutrient: String, targetValue: int): HealthyDietGoal+isAcheived(data: Data): boolean+getType(): String+toString(data: Data): String#getSymbol(): StringDietGoalList-unparseMessage: String+DietGoalList(): DietGoalList+toString(data: Data): String+isDietgoalUnique(dietGoal: DietGoal): boolean+isDietGoalTypeValid(dietGoal: DietGoal): boolean+isTargetValueConsistentWithTimeSpan(newDietGoal: DietGoal): boolean+parse(s: String): DietGoal-validateParseDietGoal(dietGoal: DietGoal): void-createParseNewDietGoal(goalType: String, timeSpan: String, nutrient: String, targetValue: int): DietGoalcontains*1 \ No newline at end of file diff --git a/docs/images/DietGoalsSequenceDiagram.svg b/docs/images/DietGoalsSequenceDiagram.svg new file mode 100644 index 0000000000..e70da653e4 --- /dev/null +++ b/docs/images/DietGoalsSequenceDiagram.svg @@ -0,0 +1 @@ +:AthletiCLI:Parserdata:Datadata:DietGoalListParseCommand("set-diet-goal WEEKLY fat/1")ParseDietGoalSetEdit("WEEKLY fat/1")dietGoalList()temp:DietGoalListtemp:DietGoalListloop[number of valid new diet goals]alt[is healthy goal]HealthyDietGoal():HealthyDietGoal:HealthyDietGoal[is unhealthy goal]UnhealthyDietGoal():UnhealthyDietGoal:DietGoaltemp:DietGoalListSetDietGoalCommand():SetDietGoalCommand:SetDietGoalCommand:SetDietGoalCommandexecute()getDietGoals()data:DietGoalListloop[number of valid new healthy and unhealthy goals]add()messages:String \ No newline at end of file diff --git a/docs/images/EditSleepObjectSequenceDiagram.svg b/docs/images/EditSleepObjectSequenceDiagram.svg new file mode 100644 index 0000000000..d707cfb8ce --- /dev/null +++ b/docs/images/EditSleepObjectSequenceDiagram.svg @@ -0,0 +1,101 @@ +:AthletiCLI«class»Parser«class»SleepParsers:SleepsleepDuration:Durationc:EditSleepCommanddata:Datasleeps:SleepListparseCommand(input)parseSleep(arguments)parseSleepIndex(arguments)Sleep()calculateSleepDuration()SleepDurationssEditSleepCommand(index, s)ccexecute(s, data)getSleeps()sleepssetSleep(index, s)message \ No newline at end of file diff --git a/docs/images/HelpAddDiet.svg b/docs/images/HelpAddDiet.svg new file mode 100644 index 0000000000..23b8bed9f1 --- /dev/null +++ b/docs/images/HelpAddDiet.svg @@ -0,0 +1 @@ +:AthletiCLI«class»Parser:UiUsergetUserCommand()"help add-diet""help add-diet"parseCommand("help add-diet")new HelpCommand("add-diet"):HelpCommand:HelpCommand:HelpCommandexecute(data)feedback:StringshowMessages(feedback) \ No newline at end of file diff --git a/docs/images/MainClassDiagram.svg b/docs/images/MainClassDiagram.svg new file mode 100644 index 0000000000..a196983e30 --- /dev/null +++ b/docs/images/MainClassDiagram.svg @@ -0,0 +1 @@ +AthletiCLI-logger:Logger-ui:Ui-data:Data-runSaveCommand:Thread-AthletiCLI(): void-run(): void+main(): voidUi-uiInstance: Ui-in: Scanner-out: PrintStream-Ui(): Ui+getInstance(): Ui+getUserCommand(): String+showMessages(messages: String): void+showException(e: Exception): void+showWelcome(): voidParser-INVALID_YEAR: String+splitCommandWordAndArgs(rawUserInput: String): String[]+parseCommand(rawUserInput: String):Command+parseDateTime(datetime: String): LocalDateTime+parseDate(date: String): LocalDate+parseNonNegativeInteger(integer:String, invalidMessage: String, overflowMessage: String): int+getValueForMarker(arguments: String, marker: String): StringData-dataInstance: Data-activities: ActivityList-activityGoals: ActivityGoalList-diets: DietList-dietGoals: DietGoalList-sleeps: SleepList-sleepGoals: SleepGoalList+getInstance():Data+load(): void+save(): void+clear(): void+getActivities(): ActivityList+getActivityGoalList(): ActivityGoalList+getDiets():DietList+getDietGoals(): DietGoalList+getSleeps(): SleepList+getSleepGoals(): SleepGoalList+setActivities(activities: ActivityList): void+setActivityGoalList(activityGoals: ActivityGoalList): void+setDiets(diets: DietList): void+setDietGoals(dietGoals: DietGoalList): void+setSleeps(sleeps: SleepList): void+setSleepGoals(sleepGoals: SleepGoalList): voidsends display message to11sends user command to11saves data to11 \ No newline at end of file diff --git a/docs/images/ParserClassDiagram.png b/docs/images/ParserClassDiagram.png new file mode 100644 index 0000000000..448e34c34b Binary files /dev/null and b/docs/images/ParserClassDiagram.png differ diff --git a/docs/images/Save.svg b/docs/images/Save.svg new file mode 100644 index 0000000000..18d8c70a53 --- /dev/null +++ b/docs/images/Save.svg @@ -0,0 +1 @@ +:SaveCommand:Data:StorableList«class»Storagesave()save()save(path, items)refSave other lists \ No newline at end of file diff --git a/docs/images/SleepAndSleepListClassDiagram.svg b/docs/images/SleepAndSleepListClassDiagram.svg new file mode 100644 index 0000000000..47e172ce23 --- /dev/null +++ b/docs/images/SleepAndSleepListClassDiagram.svg @@ -0,0 +1 @@ +«interface»Findable+find(date: LocalDate): ArrayList<T>StorableListT-path: String+save()+load()+parse(s: String): T+unparse(t: T): StringSleep-startDateTime: LocalDateTime-endDateTime: LocalDateTime-sleepingDuration: Duration-sleepDate: LocalDate+Sleep(startDateTime: LocalDateTime, toDateTime: LocalDateTime)+getStartDateTime(): LocalDateTime+getEndDateTime(): LocalDateTime+getSleepDate(): LocalDate+getSleepingTime(): LocalTime+toString(): String -calculateSleepingDuration(): Duration-calculateSleepDate(): LocalDate-generateSleepingDurationStringOutput(): String-generateStartDateTimeStringOutput(): String-generateEndDateTimeStringOutput(): String-generateSleepDateStringOutput(): StringSleepList+sort()+filterByTimespan(timeSpan: Goal.TimeSpan): ArrayList<Sleep>+getTotalSleepDuration(timeSpan: Goal.TimeSpan): intextendscontains*1implements \ No newline at end of file diff --git a/docs/images/architectureDiagram.svg b/docs/images/architectureDiagram.svg new file mode 100644 index 0000000000..9e4373c141 --- /dev/null +++ b/docs/images/architectureDiagram.svg @@ -0,0 +1 @@ +AthletiCLI AppAthletiCLIUiParserDataStorageCommandsUser \ No newline at end of file diff --git a/docs/images/editDietSequenceDiagram.png b/docs/images/editDietSequenceDiagram.png new file mode 100644 index 0000000000..72a702f909 Binary files /dev/null and b/docs/images/editDietSequenceDiagram.png differ diff --git a/docs/images/listActivityDetailedShowcase.png b/docs/images/listActivityDetailedShowcase.png new file mode 100644 index 0000000000..325a501e3d Binary files /dev/null and b/docs/images/listActivityDetailedShowcase.png differ diff --git a/docs/images/listActivityGoalShowcase.png b/docs/images/listActivityGoalShowcase.png new file mode 100644 index 0000000000..59448fec4f Binary files /dev/null and b/docs/images/listActivityGoalShowcase.png differ diff --git a/docs/images/listActivityShowcase.png b/docs/images/listActivityShowcase.png new file mode 100644 index 0000000000..38c266b16a Binary files /dev/null and b/docs/images/listActivityShowcase.png differ diff --git a/docs/images/listDietShowcase.png b/docs/images/listDietShowcase.png new file mode 100644 index 0000000000..60a624c08b Binary files /dev/null and b/docs/images/listDietShowcase.png differ diff --git a/docs/images/setDietGoalUmlSequenceDiagram.svg b/docs/images/setDietGoalUmlSequenceDiagram.svg new file mode 100644 index 0000000000..b191279923 --- /dev/null +++ b/docs/images/setDietGoalUmlSequenceDiagram.svg @@ -0,0 +1 @@ +:AthletiCLI:Parserdata:Datadata:DietGoalListParseCommand("set-diet-goal fat/1")ParseDietGoalSetEdit("fat/1")dietGoalList()temp:DietGoalListtemp:DietGoalListloop[number of valid new diet goals]DietGoal():DietGoal:DietGoaltemp:DietGoalListSetDietGoalCommand():SetDietGoalCommand:SetDietGoalCommand:SetDietGoalCommandexecute()getDietGoals()data:DietGoalListloop[number of valid new diet goals]add()messages:String \ No newline at end of file diff --git a/docs/puml/Activity/Activity.puml b/docs/puml/Activity/Activity.puml new file mode 100644 index 0000000000..c195276e10 --- /dev/null +++ b/docs/puml/Activity/Activity.puml @@ -0,0 +1,41 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle +class Activity { + -caption + -duration + -movingTime + -distance + -startDateTime +} + +class Run { +-elevationGain +-averagePace +} + +class Cycle { +-elevationGain +-averageSpeed +} + +class Swim { +-laps +-style +-averageLapTime +} + +class ActivityList { +} + +class Data { +} + +Activity <|-- Run +Activity <|-- Cycle +Activity <|-- Swim +ActivityList o-- Activity : consists of * > +Data o-- ActivityList : 1 > + + +@enduml diff --git a/docs/puml/Activity/ActivityGoalEvaluation.puml b/docs/puml/Activity/ActivityGoalEvaluation.puml new file mode 100644 index 0000000000..305b3b0c4d --- /dev/null +++ b/docs/puml/Activity/ActivityGoalEvaluation.puml @@ -0,0 +1,28 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center + +!define LOGIC_COLOR #3333C4 + +participant ":ActivityGoal" as ActivityGoal LOGIC_COLOR +participant "data:Data" as Data #lightgrey +participant "activities:ActivityList" as activities #lightgrey + + +ActivityGoal++ +-> ActivityGoal: isAchieved(data) +ActivityGoal -> ActivityGoal++: getCurrentValue(data) +ActivityGoal -> ActivityGoal++: getActivityClass() +ActivityGoal --> ActivityGoal--: activityClass +ActivityGoal -> Data++: getActivities() +Data --> ActivityGoal--: activities + +ActivityGoal -> activities++: getTotalDistance/Duration(activityClass, timespan) +activities -> activities++: filterByTimespan(timespan) +activities --> activities--: filteredActivities +activities --> ActivityGoal--: total + +ActivityGoal --> ActivityGoal-- +<-- ActivityGoal: isAchieved +@enduml \ No newline at end of file diff --git a/docs/puml/Activity/ActivityObjectDiagram.puml b/docs/puml/Activity/ActivityObjectDiagram.puml new file mode 100644 index 0000000000..1fa15a867b --- /dev/null +++ b/docs/puml/Activity/ActivityObjectDiagram.puml @@ -0,0 +1,37 @@ +@startuml + +object "activities:ActivityList" as activities { +} + +object "run1:Run" as run1 #lightgreen { + distance = 5000 + startDateTime = 2023-11-11 +} + +object "run2:Run" as run2 #lightgreen { + distance = 5000 + startDateTime = 2023-11-10 +} + +object "swim:Swim" as swim { + distance = 1000 + startDateTime = 2023-10-11 +} + +object "cycle:Cycle" as cycle { + distance = 40000 + startDateTime = 2023-10-10 +} + +object "activity:Activity" as activity { + distance = 100 + startDateTime = 2023-10-09 +} + +activities --> run1 +activities --> run2 +activities --> swim +activities --> cycle +activities --> activity + +@enduml \ No newline at end of file diff --git a/docs/puml/Activity/ActivityParsing.puml b/docs/puml/Activity/ActivityParsing.puml new file mode 100644 index 0000000000..fc6ad74004 --- /dev/null +++ b/docs/puml/Activity/ActivityParsing.puml @@ -0,0 +1,44 @@ +@startuml +skinparam Style strictuml +skinparam SequenceMessageAlignment center +participant Parser <> #lightblue +participant ActivityParser <> #lightgreen +participant "ac:ActivityChanges" as ActivityChanges #lightgrey +participant "a:Activity" as Activity #lightgrey + +Parser++ +Parser -> ActivityParser++ : parseActivity(arguments) + +ActivityParser -> ActivityChanges++ : ActivityChanges() +ActivityChanges --> ActivityParser-- : ac +ActivityParser -> ActivityParser++ : parseActivityArguments(ac, arguments, separators) + +loop for each separator + ActivityParser -> ActivityParser : checkMissingActivityArgument(separatorIndex, separator) + ActivityParser -> ActivityParser++ : parseSegment(ac, segment, separator) + alt depending on separator + ActivityParser -> ActivityChanges++: setCaption(segment) + ActivityChanges --> ActivityParser-- + ActivityParser -> ActivityChanges++ : setDuration(parseDuration(segment)) + ActivityChanges --> ActivityParser-- + ... other setters ... + ActivityParser -> ActivityChanges++ : setDistance(parseDistance(segment)) + ActivityChanges --> ActivityParser-- + end + ActivityParser --> ActivityParser -- : +end +ActivityParser --> ActivityParser -- : + +ActivityParser -> ActivityChanges++ : getCaption() +ActivityChanges --> ActivityParser-- : caption +... other getters ... +ActivityParser -> ActivityChanges++ : getStartDateTime() +ActivityChanges --> ActivityParser-- : startDateTime +destroy ActivityChanges + +ActivityParser -> Activity++ : Activity(caption, duration, ...) +Activity --> ActivityParser-- : a + +ActivityParser --> Parser-- : a + +@enduml diff --git a/docs/puml/Activity/AddActivity.puml b/docs/puml/Activity/AddActivity.puml new file mode 100644 index 0000000000..1b01340b8b --- /dev/null +++ b/docs/puml/Activity/AddActivity.puml @@ -0,0 +1,37 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center + +!define LOGIC_COLOR #3333C4 + +participant ":AthletiCLI" as AthletiCLI #orange +participant "Parser" as Parser <> #lightblue +participant "ActivityParser" as ActivityParser <> #lightblue +participant "a:Activity" as Activity #yellow +participant "c:AddActivityCommand" as AddActivityCommand #lightgreen +participant "data:Data" as Data #lightgrey +participant "activities:ActivityList" as activities #lightgrey + +AthletiCLI++ +AthletiCLI -> Parser++: parseCommand(userInput) +Parser -> ActivityParser++: parseActivity(arguments) +ActivityParser -> Activity++: Activity() +Activity --> ActivityParser--: a +ActivityParser --> Parser--: a +Parser -> AddActivityCommand++: AddActivityCommand(a) +AddActivityCommand --> Parser--: c +Parser --> AthletiCLI--: c + +AthletiCLI -> AddActivityCommand++: execute(a, data) +AddActivityCommand -> Data++: getActivities() +'Data --> activities++ +'activities --> Data--: activities + +Data --> AddActivityCommand--: activities +AddActivityCommand -> activities++: add(a) +activities --> AddActivityCommand-- +AddActivityCommand --> AthletiCLI--: message + +destroy AddActivityCommand +@enduml diff --git a/docs/puml/Activity/AddActivityGoal.puml b/docs/puml/Activity/AddActivityGoal.puml new file mode 100644 index 0000000000..386f2705f2 --- /dev/null +++ b/docs/puml/Activity/AddActivityGoal.puml @@ -0,0 +1,35 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center + +!define LOGIC_COLOR #3333C4 + +participant ":AthletiCLI" as AthletiCLI #orange +participant "Parser" as Parser <> #lightblue +participant "ActivityParser" as ActivityParser <> #lightblue +participant "goal:ActivityGoal" as ActivityGoal #yellow +participant "c:SetActivityGoalCommand" as SetActivityGoalCommand #lightgreen +participant "data:Data" as Data #lightgrey +participant "goals:ActivityGoalList" as activityGoals #lightgrey + +AthletiCLI++ +AthletiCLI -> Parser++: parseCommand(userInput) +Parser -> ActivityParser++: parseActivityGoal(arguments) +ActivityParser -> ActivityGoal++: ActivityGoal() +ActivityGoal --> ActivityParser--: goal +ActivityParser --> Parser--: goal +Parser -> SetActivityGoalCommand++: SetActivityGoalCommand(goal) +SetActivityGoalCommand --> Parser--: c +Parser --> AthletiCLI--: c + +AthletiCLI -> SetActivityGoalCommand++: execute(goal, data) +SetActivityGoalCommand -> Data++: getActivityGoals() + +Data --> SetActivityGoalCommand--: goals +SetActivityGoalCommand -> activityGoals++: add(goal) +activityGoals --> SetActivityGoalCommand-- +SetActivityGoalCommand --> AthletiCLI--: message + +destroy SetActivityGoalCommand +@enduml \ No newline at end of file diff --git a/docs/puml/Diet/DietGoalClassDiagram.puml b/docs/puml/Diet/DietGoalClassDiagram.puml new file mode 100644 index 0000000000..b35ced67ab --- /dev/null +++ b/docs/puml/Diet/DietGoalClassDiagram.puml @@ -0,0 +1,81 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +hide circle + +abstract class Goal{ + - timeSpan:TimeSpan + + Goal(timeSpan: TimeSpan): Goal + + getTimeSpan():TimeSpan + + checkData(date: LocalDate, timeSpan: TimeSpan): boolean + + {abstract} isAcheived(data:Data): boolean + +} + +abstract class DietGoal{ + # nutrient: String + # targetValue: int + # type: String + # achievedSymbol: String + # unachievedSymbol: String + - dietGoalStringRepresentation: String + + DietGoal(timeSpan: TimeSpan, nutrient: String, targetValue:int): DietGoal + + getNutrient(): String + + getTargetValue(): int + + getCurrentValue(data:Data): int + + getType(): String + + setTargetValue(targetValue: int): void + + isSameType(dietGoal: DietGoal): boolean + + isSameNutrient(dietGoal: DietGoal): boolean + + isSameTimeSpan(dietGoal: DietGoal): boolean + + toString(data: Data): String + # getSymbol(data: Data): String +} + +class HealthyDietGoal{ + + TYPE: String + # healthyDietGoalSymbol: String + # healthyDietGoalStringRepresentation: String + - isHealthy: boolean + + HealthyDietGoal(timeSpan: TimeSpan, nutrient: String, targetValue: int): HealthyDietGoal + + getType(): String + + toString(data: Data): String +} + +class UnhealthyDietGoal{ + + TYPE: String + # achievedSymbol + # unachievedSymbol + # unhealthyDietGoalSymbol: String + # unhealthyDietGoalStringRepresentation: String + - isHealthy: boolean + + UnhealthyDietGoal(timeSpan: TimeSpan, nutrient: String, targetValue: int): HealthyDietGoal + + isAcheived(data: Data): boolean + + getType(): String + + toString(data: Data): String + # getSymbol(): String + +} + +class DietGoalList{ + - unparseMessage: String + + DietGoalList(): DietGoalList + + toString(data: Data): String + + isDietgoalUnique(dietGoal: DietGoal): boolean + + isDietGoalTypeValid(dietGoal: DietGoal): boolean + + isTargetValueConsistentWithTimeSpan(newDietGoal: DietGoal): boolean + + parse(s: String): DietGoal + - validateParseDietGoal(dietGoal: DietGoal): void + - createParseNewDietGoal(goalType: String, timeSpan: String, nutrient: String, targetValue: int): DietGoal + + +} + + +Goal <|-- DietGoal +DietGoal <|-- HealthyDietGoal +DietGoal <|-- UnhealthyDietGoal +DietGoalList "1" o-l- "*" DietGoal :contains > + + +@enduml \ No newline at end of file diff --git a/docs/puml/Diet/DietGoalsSequenceDiagram.puml b/docs/puml/Diet/DietGoalsSequenceDiagram.puml new file mode 100644 index 0000000000..93b9cc10fe --- /dev/null +++ b/docs/puml/Diet/DietGoalsSequenceDiagram.puml @@ -0,0 +1,57 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center +participant ":AthletiCLI" as AthletiCLI #lightblue +participant ":Parser" as Parser #lightgreen +participant ":HealthyDietGoal" as healthyDietGoal #lightyellow +participant ":UnhealthyDietGoal" as unhealthyDietGoal #lightyellow +participant ":SetDietGoalCommand" as SetDietGoalCommand #lightpink +participant "temp:DietGoalList" as tempDietGoalList #yellow +participant "data:Data" as dataData +participant "data:DietGoalList" as dataDietGoalList #yellow + + +'autonumber +AthletiCLI++ +AthletiCLI -> Parser++ : ParseCommand("set-diet-goal WEEKLY fat/1") +Parser -> Parser++ : ParseDietGoalSetEdit("WEEKLY fat/1") +create tempDietGoalList +Parser -> tempDietGoalList++ : dietGoalList() +tempDietGoalList --> Parser-- : temp:DietGoalList + + loop number of valid new diet goals + alt is healthy goal + create healthyDietGoal + Parser -> healthyDietGoal++ : HealthyDietGoal() + healthyDietGoal --> Parser-- : :HealthyDietGoal + else is unhealthy goal + create unhealthyDietGoal + Parser -> unhealthyDietGoal++ : UnhealthyDietGoal() + unhealthyDietGoal --> Parser-- : :DietGoal + end + end + +Parser --> Parser-- : temp:DietGoalList +create SetDietGoalCommand +Parser -> SetDietGoalCommand++ : SetDietGoalCommand() +SetDietGoalCommand --> Parser-- : :SetDietGoalCommand +Parser --> AthletiCLI-- : :SetDietGoalCommand +AthletiCLI -> SetDietGoalCommand++ : execute() +SetDietGoalCommand -> dataData++ : getDietGoals() +dataData --> SetDietGoalCommand-- : data:DietGoalList + + loop number of valid new healthy and unhealthy goals + SetDietGoalCommand -> dataDietGoalList++ : add() + + dataDietGoalList -- + + + end + +destroy tempDietGoalList +SetDietGoalCommand --> AthletiCLI-- : messages:String + +destroy SetDietGoalCommand + +@enduml \ No newline at end of file diff --git a/docs/puml/Diet/EditDietSequenceDiagram.puml b/docs/puml/Diet/EditDietSequenceDiagram.puml new file mode 100644 index 0000000000..bbffec9aba --- /dev/null +++ b/docs/puml/Diet/EditDietSequenceDiagram.puml @@ -0,0 +1,42 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center + +!define LOGIC_COLOR #3333C4 + +participant ":AthletiCLI" as AthletiCLI LOGIC_COLOR +participant "Parser" as Parser <> #lightblue +participant "DietParser" as DietParser <> #lightblue +participant "c:EditDietCommand" as EditDietCommand #lightgreen +participant "data:Data" as Data #lightgrey +participant "diets:DietList" as DietList #lightgrey +participant "oldDiet:Diet" as oldDiet #yellow + +AthletiCLI++ +AthletiCLI -> Parser++: parseCommand(userInput) +Parser -> DietParser++: parseDietEdit(arguments) +DietParser --> Parser: dietMap +DietParser-- +Parser -> EditDietCommand++: new EditDietCommand(index, dietMap) +EditDietCommand --> Parser--: c +Parser --> AthletiCLI--: c + +AthletiCLI -> EditDietCommand++: execute(data) +EditDietCommand -> Data++: getDiets() +Data --> EditDietCommand--: diets +EditDietCommand -> DietList++: get(index - 1) +DietList --> EditDietCommand--: oldDiet + +loop for each key in dietMap + EditDietCommand -> oldDiet++ : set[key](value) + oldDiet --> EditDietCommand-- +end + + +EditDietCommand -> DietList++: set(index - 1, oldDiet) +DietList --> EditDietCommand-- +EditDietCommand --> AthletiCLI--: message + +destroy EditDietCommand +@enduml diff --git a/docs/puml/General/DataClassDiagram.puml b/docs/puml/General/DataClassDiagram.puml new file mode 100644 index 0000000000..514c4dc4b9 --- /dev/null +++ b/docs/puml/General/DataClassDiagram.puml @@ -0,0 +1,77 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +hide circle + +abstract class "StorableList"{ + - path: String + + StorableList(path: String): StorableList + + load() + + save() + + {abstract} parse(s:String) + + {abstract} unparse(t:T) + +} + +interface "<>\nFindable" { + + find(date: LocalDate): ArrayList +} +class Data{ + + getInstance():Data + + load(): void + + save(): void + + clear(): void +} +class ActivityList{ + + find(date: LocalDate): ArrayList + + parse(s:String) + + unparse(t:T) +} +class DietList{ ++ find(date: LocalDate): ArrayList + + parse(s:String) + + unparse(t:T) +} +class SleepList{ ++ find(date: LocalDate): ArrayList + + parse(s:String) + + unparse(t:T) +} + +class ActivityGoalList{ + + parse(s:String) + + unparse(t:T) +} +class DietGoalList{ + + parse(s:String) + + unparse(t:T) +} +class SleepGoalList{ + + parse(s:String) + + unparse(t:T) +} + + +"<>\nFindable" <|.. ActivityList +"<>\nFindable" <|.. DietList +"<>\nFindable" <|.. SleepList + +"StorableList" <|-- ActivityList +"StorableList" <|-- DietList +"StorableList" <|-- SleepList +"StorableList" <|-- ActivityGoalList +"StorableList" <|-- DietGoalList +"StorableList" <|-- SleepGoalList + +Data "1" --u- "1" ActivityList : contains > +Data "1" --u- "1 " DietList : contains > +Data "1" --u- "1" SleepList : contains > +Data "1" --u- "1" ActivityGoalList : contains > +Data "1 " --u- "1" DietGoalList : contains > +Data "1" --u- "1" SleepGoalList : contains > + + + + + +@enduml \ No newline at end of file diff --git a/docs/puml/General/MainClassDiagram.puml b/docs/puml/General/MainClassDiagram.puml new file mode 100644 index 0000000000..4754586413 --- /dev/null +++ b/docs/puml/General/MainClassDiagram.puml @@ -0,0 +1,70 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +hide circle + +class AthletiCLI{ + -logger:Logger + -ui:Ui + -data:Data + -runSaveCommand:Thread + -AthletiCLI(): void + -run(): void + +main(): void + +} +class Ui{ + - uiInstance: Ui + - in: Scanner + - out: PrintStream + - Ui(): Ui + + getInstance(): Ui + + getUserCommand(): String + + showMessages(messages: String): void + + showException(e: Exception): void + + showWelcome(): void + +} +class Parser{ + - INVALID_YEAR: String + + splitCommandWordAndArgs(rawUserInput: String): String[] + + parseCommand(rawUserInput: String):Command + + parseDateTime(datetime: String): LocalDateTime + + parseDate(date: String): LocalDate + + parseNonNegativeInteger(integer:String, invalidMessage: String, overflowMessage: String): int + + getValueForMarker(arguments: String, marker: String): String + +} +class Data{ + - dataInstance: Data + - activities: ActivityList + - activityGoals: ActivityGoalList + - diets: DietList + - dietGoals: DietGoalList + - sleeps: SleepList + - sleepGoals: SleepGoalList + + getInstance():Data + + load(): void + + save(): void + + clear(): void + + getActivities(): ActivityList + + getActivityGoalList(): ActivityGoalList + + getDiets():DietList + + getDietGoals(): DietGoalList + + getSleeps(): SleepList + + getSleepGoals(): SleepGoalList + + setActivities(activities: ActivityList): void + + setActivityGoalList(activityGoals: ActivityGoalList): void + + setDiets(diets: DietList): void + + setDietGoals(dietGoals: DietGoalList): void + + setSleeps(sleeps: SleepList): void + + setSleepGoals(sleepGoals: SleepGoalList): void + +} + + + +AthletiCLI "1" --> "1" Ui : sends display message to > +AthletiCLI "1" --> "1" Parser : sends user command to > +AthletiCLI "1" --> "1" Data : saves data to > +@enduml \ No newline at end of file diff --git a/docs/puml/General/ParserClassDiagram.puml b/docs/puml/General/ParserClassDiagram.puml new file mode 100644 index 0000000000..8860d97a9f --- /dev/null +++ b/docs/puml/General/ParserClassDiagram.puml @@ -0,0 +1,31 @@ +@startuml +'https://plantuml.com/class-diagram +hide circle + +class Parser { + parseCommand(rawUserInput: String): Command + parseDateTime(datetime: String): LocalDateTime + parseDate(date: String): LocalDate +} + +class SleepParser {} +class ActivityParser {} +class DietParser {} + +abstract class Command { + {abstract} execute(data: Data): String[] +} + +class Parameter {} +class Message {} +class AthletiException {} + +Parser ..> Command : returns +Parser --> AthletiException : throws +Parser ..> SleepParser : uses +Parser ..> ActivityParser : uses +Parser ..> DietParser : uses +Parser ..> Parameter : uses +Parser ..> Message : uses + +@enduml diff --git a/docs/puml/HelpAddDiet.puml b/docs/puml/HelpAddDiet.puml new file mode 100644 index 0000000000..c876f2d0dd --- /dev/null +++ b/docs/puml/HelpAddDiet.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center +participant ":AthletiCLI" as AthletiCLI #lightblue +participant "<>\nParser" as Parser #lightgreen +participant ":HelpCommand" as HelpCommand #lightpink +participant ":Ui" as Ui #lightyellow +actor User as User + + +'autonumber +AthletiCLI++ +AthletiCLI -> Ui++ : getUserCommand() +User -> Ui : "help add-diet" +Ui --> AthletiCLI-- : "help add-diet" +AthletiCLI -> Parser++ : parseCommand("help add-diet") +create HelpCommand +Parser -> HelpCommand++ : new HelpCommand("add-diet") +HelpCommand --> Parser-- : :HelpCommand +Parser --> AthletiCLI-- : :HelpCommand +AthletiCLI -> HelpCommand++ : execute(data) +HelpCommand --> AthletiCLI-- : feedback:String +AthletiCLI -> Ui++ : showMessages(feedback) +Ui --> AthletiCLI-- + +destroy HelpCommand + +@enduml \ No newline at end of file diff --git a/docs/puml/Save.puml b/docs/puml/Save.puml new file mode 100644 index 0000000000..7234ba3147 --- /dev/null +++ b/docs/puml/Save.puml @@ -0,0 +1,20 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center +participant ":SaveCommand" as SaveCommand +participant ":Data" as Data #lightblue +participant ":StorableList" as StorableList #lightgreen +participant "<>\nStorage" as Storage #lightpink + + +'autonumber +SaveCommand -> Data++ : save() +Data -> StorableList++ : save() +StorableList -> Storage++ : save(path, items) +Storage --> StorableList-- +StorableList --> Data-- +ref over Data : Save other lists +Data --> SaveCommand + +@enduml \ No newline at end of file diff --git a/docs/puml/Sleep/SleepAndSleeplistClassDiagram.puml b/docs/puml/Sleep/SleepAndSleeplistClassDiagram.puml new file mode 100644 index 0000000000..50b6df56d9 --- /dev/null +++ b/docs/puml/Sleep/SleepAndSleeplistClassDiagram.puml @@ -0,0 +1,50 @@ +@startuml +'https://plantuml.com/class-diagram +skinparam classAttributeIconSize 0 +hide circle + + +interface Findable<> { + + find(date: LocalDate): ArrayList +} + +abstract class StorableList { + - path: String + + save() + + load() + + {abstract} parse(s: String): T + + {abstract} unparse(t: T): String +} + + +class Sleep { + - startDateTime: LocalDateTime + - endDateTime: LocalDateTime + - sleepingDuration: Duration + - sleepDate: LocalDate + + + Sleep(startDateTime: LocalDateTime, toDateTime: LocalDateTime) + + getStartDateTime(): LocalDateTime + + getEndDateTime(): LocalDateTime + + getSleepDate(): LocalDate + + getSleepingTime(): LocalTime + + toString(): String + + - calculateSleepingDuration(): Duration + - calculateSleepDate(): LocalDate + - generateSleepingDurationStringOutput(): String + - generateStartDateTimeStringOutput(): String + - generateEndDateTimeStringOutput(): String + - generateSleepDateStringOutput(): String +} + +class SleepList { + + sort() + + filterByTimespan(timeSpan: Goal.TimeSpan): ArrayList + + getTotalSleepDuration(timeSpan: Goal.TimeSpan): int +} + +SleepList --|> StorableList: extends +SleepList "1" o-l- "*" Sleep :contains > +SleepList ..|> Findable : implements +@enduml \ No newline at end of file diff --git a/docs/puml/Sleep/SleepObjectSequenceDiagram.puml b/docs/puml/Sleep/SleepObjectSequenceDiagram.puml new file mode 100644 index 0000000000..b2de3c081a --- /dev/null +++ b/docs/puml/Sleep/SleepObjectSequenceDiagram.puml @@ -0,0 +1,46 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam Style strictuml +skinparam SequenceMessageAlignment center + +!define LOGIC_COLOR #3333C4 + +participant ":AthletiCLI" as AthletiCLI #orange +participant "Parser" as Parser <> #lightblue +participant "SleepParser" as SleepParser <> #lightblue +participant "s:Sleep" as Sleep #yellow +participant "sleepDuration:Duration" as duration #gold +participant "c:EditSleepCommand" as EditSleepCommand #lightgreen +participant "data:Data" as Data #lightgrey +participant "sleeps:SleepList" as sleeps #lightgrey + + +AthletiCLI++ +AthletiCLI->Parser++: parseCommand(input) + +Parser -> SleepParser++: parseSleep(arguments) +Parser -> SleepParser++: parseSleepIndex(arguments) +deactivate SleepParser + +SleepParser -> Sleep++: Sleep() + +Sleep --> duration++: calculateSleepDuration() +duration --> Sleep--: SleepDuration +Sleep --> SleepParser--: s +SleepParser --> Parser--: s + +Parser -> EditSleepCommand++: EditSleepCommand(index, s) +EditSleepCommand --> Parser--: c +Parser --> AthletiCLI--: c + +AthletiCLI -> EditSleepCommand++: execute(s, data) +EditSleepCommand -> Data++: getSleeps() + + +Data --> EditSleepCommand--: sleeps +EditSleepCommand -> sleeps++: setSleep(index, s) +sleeps --> EditSleepCommand-- +EditSleepCommand --> AthletiCLI--: message + +destroy EditSleepCommand +@enduml \ No newline at end of file diff --git a/docs/puml/architectureDiagram.puml b/docs/puml/architectureDiagram.puml new file mode 100644 index 0000000000..5280ec691d --- /dev/null +++ b/docs/puml/architectureDiagram.puml @@ -0,0 +1,39 @@ +@startuml +'https://plantuml.com/class-diagram +'!include style.puml +hide footbox + +actor User +'box #white +'frame ""f" +'participant user2 +'participant AthletiCLI +'participant Ui +'participant Parser +'participant Data +'participant Storage +'participant Commands +frame "AthletiCLI App"{ +rectangle AthletiCLI +rectangle Ui +rectangle Parser +rectangle Data +rectangle Storage +rectangle Commands +'end rectangle + +} +'end frame +'end box + +User -d-> Ui +Ui -r-> AthletiCLI +AthletiCLI -d-> Parser +AthletiCLI -d-> Commands +AthletiCLI -d-> Data +Commands -u-> Data +Parser -r-> Data +Data -d-> Storage + + +@enduml \ No newline at end of file diff --git a/docs/team/alwo223.md b/docs/team/alwo223.md new file mode 100644 index 0000000000..ed7ed08bc7 --- /dev/null +++ b/docs/team/alwo223.md @@ -0,0 +1,79 @@ +--- +layout: page +title: Alexander Wolters’ Portfolio +--- + +## Project: AthletiCLI + +**AthletiCLI** is a Java-based application designed for dedicated athletes to track, analyse, and optimize their athletic +performance. This tool not only monitors physical activities but also dietary habits, +sleep metrics, and more. The user interacts through a user-friendly CLI. The project comprises about 11 kLoC. + +View my **code contributions** on [RepoSense](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/? +search=alwo223&breakdown=true). The key enhancements are summarized below. + +#### New Features + * **Added the ability to add, list and delete activities** + * Purpose: This feature is the core of activity management as it enables users to track their athletic + performance and progress. + * Highlights: Implementing an elegant, efficient and unified solution for the different activity types with + unique parameters and commands was a complex challenge. This was achieved by leveraging inheritance, generic + parser functions and extensive refactoring leading to a modular and efficient design. Therefore, this approach involved in-depth analysis and planning. + * **Added command to list all activities** + * Purpose: This feature allows users to view their activities chronologically and in different levels of detail, + aiding in performance analysis and progress tracking. + * Highlights: The implementation included a sorting mechanism by date and time and ensured data consistency + during any data modifying operations. + * **Added command to effortlessly edit activities** + * Purpose: This feature is essential for users to correct or update their activities without + replacing the whole entry, thus enhancing the user experience. + * Highlights: Similar to the add command, this feature required a modular implementation approach to handle the + different activity and to effectively reuse existing parser functions. + * **Implemented parsing and unparsing functionality for activity (goal) storing** + * Purpose: This features allows to store all activities and activity goals in a file and parse them on startup + of the application. This feature improves the product significantly by enabling to close and reopen the + application without any data loss. + * Highlights: The approach was developed in collaboration with @nihalzp. The storing functionality of other + tracking components like sleep were based on this implementation. It involved some analysis of the existing code + to efficiently reuse existing parser functions. + * **Implemented goal tracking mechanism and find feature for activities** + * Purpose: empowers users to set and monitor goals for different periods, sports and metrics. It is essential + for the user to plan their training and to push themselves to improve. + This also comes with the ability to find activities by date. + * Highlights: The implementation was adopted for other goal tracking mechanism like sleep and diet. + +#### Review / Mentoring +* [Reviewed PRs](https://github.com/AY2324S1-CS2113-T17-1/tp/issues?q=reviewed-by%3Aalwo223+) (examples: +[#288](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/288), +[#280](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/280), +[#95](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/95), +[#21](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/21)) +* [Issues reported / discussed](https://github.com/AY2324S1-CS2113-T17-1/tp/issues?q=author%3Aalwo223+type%3Aissue) +* [Discussions in forum](https://github.com/AY2324S1-CS2113-T17-1/tp/discussions/110) + +#### Project Management +* Set up the GitHub repository and team organization for the project. +* Maintained issue tracker, including generating suitable labels. +* Responsible for ensuring proper testing of the implemented features. +* Managed final release v2.1. +* Strictly following deadlines, git conventions and forking workflow. + +#### Documentation +* User Guide: + * Added documentation for the features `add-activity`, `add-run`, `add-swim`, `add-cycle`, `delete-activity`, + `list-activity`, `edit-activity`, `edit-run`, `edit-cycle`, `edit-swim`, `set-activity-goal`: [Activity + Management](../UserGuide.html#-activity-management) + * Improved overall visual appearance of the document: [#253](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/253) +* Developer Guide: + * Explained all implementation details in DG related to Activity Management, including `add-activity` and + `set-activity-goal` features, find by timespan, goal tracking mechanism, detailed parsing process, modular + implementation approach and justification: [Activity Management](../DeveloperGuide.html#activity-management-in-athleticli) + * Created UML diagrams: [#1](../images/ActivityInheritance.svg), + [#2](../images/ActivityGoalEvaluation.svg), + [#3](../images/ActivityObjectDiagram.svg), [#4](../images/ActivityParsing.svg), + [#5](../images/AddActivity.svg), [#6](../images/AddActivityGoal.svg) + * Provided test instructions for activity related features + +#### Community +* PRs reviewed (with non-trivial review comments): [#139](https://github.com/nus-cs2113-AY2324S1/tp/pull/8#pullrequestreview-1709775159), [tp comments dashboard](https://nus-cs2113-ay2324s1.github.io/dashboards/contents/tp-comments.html) +* Reported bugs and suggestions for other teams in the class: [#139](https://github.com/nus-cs2113-AY2324S1/tp/pull/8#pullrequestreview-1709775159), [Issues created](https://github.com/AY2324S1-CS2113-W12-3/tp/issues?q=%22%5BPE-D%5D%5BTester+A%5D%22) diff --git a/docs/team/dadevchia.md b/docs/team/dadevchia.md new file mode 100644 index 0000000000..e2a0c56e7f --- /dev/null +++ b/docs/team/dadevchia.md @@ -0,0 +1,77 @@ +--- +layout: page +title: Dylan Chia's Project Portfolio Page (PPP) +--- + +# Project: AthletiCLI + +## Overview +Contributed to all sleep related features, including the `sleep` commands, `Sleep` class, `SleepParser` class, as well as the `SleepGoal` class. + +Also contributed to the formatting of the `User Guide` including the `Command Summary` and `FAQ`. + +View my **code contributions** on [RepoSense](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/? +search=dadevchia&breakdown=true). + +## Summary of Contributions +Given below are my contributions to the project. + +### New Feature: Added the ability to add, edit, delete and list sleep + +* What it does: Allows the user to track their sleep by adding, editing, deleting and listing sleep. +* Justification: This feature is the core of the sleep management as it allows the user to track their sleep. +* Highlights: It was challenging to find an elegant and efficient implementation which keeps code redundancy to a minimum. I used inheritance to create a parent class for sleep, and child classes for different types of sleep. This allowed for a modular implementation, which was easy to understand and extend. I also used a custom parser to parse the sleep entries, which allowed for a more efficient implementation. + +### New Feature: Added the ability to find sleep by date + +* What it does: Allows the user to find sleep by date. +* Justification: This feature allows the user to find sleep entries by date, which is useful for analysing sleep performance. +* Highlights: The implementation had to be done in a way that was consistent with the other find commands. The difficulty was that sleep records do not have a fixed date, but rather a start and end datetime, which had to be accounted for. I used a custom constructor to analyze the datetime and create a sleep entry with the correct date. + +### New Feature: Added the ability to list all sleep records + +* What it does: Allows the user to list all sleep records, including the sleep start and end datetime, and duration of sleep. +* Justification: This feature allows the user to view their sleep records chronologically and have automated calculations of the duration of sleep. +* Highlights: The sleep duration had to be calculated using the start and end datetime, which was a unique implementation from other classes. I used the Duration class from Java to store the duration of sleep, which can be calculated and tracked easily. + +### New Feature: Added sleep goal tracking mechanism +* What it does: Allows the user to set a sleep goal and track their progress towards the goal of number of minutes slept, in the past day, week, month or year. +* Justification: This feature allows the user to compare their sleep performance and analyse their progress over time. +* Highlights: The implementation had many different parameters and OOP concepts, which had to be applied during any target modifying operations. Duration of sleep was also a unique implementation that had to be accounted for. I used the Duration class from Java to store the duration of sleep, which can be calculated and tracked easily. + + +### New Feature: Implemented storing capabilities for sleep and sleep goals +* What it does: automatically stores all sleep and sleep goals in a file and loads them on startup of the application. It also allows for the user to edit the file manually. +* Justification: This feature improves the product significantly by allowing the user to close the application and reopen it without losing any data. This is especially important as the application is designed to track the progress over a longer period of time. It also allows for the user to edit the file manually, which is useful for manual data entry. +* Highlights: The implementation had issues with the parser and the storage of sleep entries. A custom parser was created to parse the sleep entries, and the storage of sleep entries was done using a custom storage class. The storage class was also used to store sleep goals. + + +#### Review +* [Reviewed PRs](https://github.com/AY2324S1-CS2113-T17-1/tp/issues?q=reviewed-by:dadevchia+) (Examples: [#31](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/31) [#118](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/118) ) + +* [Started Discussions in Github Forum](https://github.com/AY2324S1-CS2113-T17-1/tp/discussions/49) + +* [Issues reported / discussed](https://github.com/AY2324S1-CS2113-T17-1/tp/issues?q=author:dadevchia+type:issue) + +### Code Contributed +[RepoSense Link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=dadevchia&breakdown=true) + +### Project Management +* Maintained issue tracker, including generating suitable labels. +* Responsible for ensuring proper testing of the implemented features. +* Planned and led group meetings, ensuring that a clear direction was set for the project. +* Managed release v1.0 and v2.0. +* Strictly following deadlines, git conventions and forking workflow. + +### Documentation +* User Guide: + * Added documentation for the features `add-sleep`, `delete-sleep`, `edit-sleep` `list-sleep`, `edit-sleep`, `find-sleep`, `set-sleep-goal`, `list-sleep-goal`, `edit-sleep-goal` + * Added the `Command Summary` and `FAQ` sections + +* Developer Guide: + * Explained implementation details of the sleep commands, including `add-sleep`, `delete-sleep`, `edit-sleep` `list-sleep`, `edit-sleep`, `find-sleep`, + * Explained how the parser works for sleep commands with an assosciated UML sequence diagram + * Explained how the sleep object links to assosciated classes with an UML class diagram + * Explained challenges and how they were overcome, particularly with regards to the duration of sleep + * Added Value Proposition and User Stories + * Provided test instructions for sleep commands diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/nihalzp.md b/docs/team/nihalzp.md new file mode 100644 index 0000000000..29e5f03b15 --- /dev/null +++ b/docs/team/nihalzp.md @@ -0,0 +1,57 @@ +--- +layout: page +title: Nihal Parash's Project Portfolio Page +--- + +# Project: AthletiCLI + +## Overview + +**AthletiCLI** is your all-in-one solution to track, analyse, and optimize your athletic performance. Designed for the +committed athlete, this command-line interface (CLI) tool not only keeps tabs on your physical activities but also +covers dietary habits, sleep metrics, and more. + +## Summary of Contributions + +Given below are my contributions to the project. + +### Code Contributed + +[RepoSense Link](https://nus-cs2113-ay2324s1.github.io/tp-dashboard/?search=nihalzp&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=nihalzp&tabRepo=AY2324S1-CS2113-T17-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Features: + +* **Diet Record Management**: Designed and implemented the adding, deleting, editing, listing, and finding of diet + records. +* **Activity Goal Management**: Designed and implemented the deleting, editing, and listing of activity goals. +* **Common Parsing Functions**: Enhanced and refactored widely used `getValueForMarker`, `parseDateTime` and + `parseDate` functions to make them more robust and reusable. +* **Enhancements:** + - Enhanced the `add-diet` feature, enabling the input of various data fields such as `calories`, `fat`, `carb`, + `protein`, `datetime` in any order. + - Refined the `edit-diet` feature, allowing users to modify necessary data fields without altering the remaining data. + +### Project Management + +* Implemented features across versions 1.0, 2.0, and 2.1 incrementally, aligning with development plans. +* Utilized forking and branching for parallel development and seamless feature integration. +* Managed bugs effectively using issue tracking, improving project accuracy and reliability. +* Organized issue tracking by updating labels and assigning tasks, enhancing team efficiency. + +### Documentation +* **User Guide:** + * Added documentation for the features `add-diet`, `edit-diet`, `delete-diet`, `list-diet`, `find-diet`, + `edit-activity-goal`, `delete-activity-goal`, `list-activity-goal`. + * Updated the `Command Summary`, `User Stories` sections regularly. +* **Developer Guide:** + * Explained the implementation structure of adding, editing, deleting, listing and finding diets. + * Outlined the implementation idea for editing, deleting, and listing activity goals. + * Created UML diagrams: + - `Parser` class diagram + - `edit-diet` command sequence diagram + * Provided test instructions for diet record management and activity goal management. + +#### Community +* Reviewed 24 PRs. [PRs I commented on](https://github.com/AY2324S1-CS2113-T17-1/tp/pulls?q=commenter%3Anihalzp+is%3Apr+-author%3Anihalzp) +* Issues reported for other teams: + * [PE Dry Run](https://github.com/AY2324S1-CS2113-W12-4/tp/issues?q=%5BPE-D%5D%5BTester+E%5D+) \ No newline at end of file diff --git a/docs/team/photo/yicheng-toh.png b/docs/team/photo/yicheng-toh.png new file mode 100644 index 0000000000..4d0ef76829 Binary files /dev/null and b/docs/team/photo/yicheng-toh.png differ diff --git a/docs/team/skylee03.md b/docs/team/skylee03.md new file mode 100644 index 0000000000..53e5aaeae1 --- /dev/null +++ b/docs/team/skylee03.md @@ -0,0 +1,45 @@ +--- +layout: page +title: Ming-Tian’s Portfolio +--- + +## Overview + +**AthletiCLI** is your all-in-one solution to track, analyse, and optimize your athletic performance. Designed for the committed athlete, this command-line interface (CLI) tool not only keeps tabs on your physical activities but also covers dietary habits, sleep metrics, and more. + +## Summary of Contributions + +Given below are my contributions to the project. + +* :computer: **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=skylee03&tabRepo=AY2324S1-CS2113-T17-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) +* :house: **Architecture**: Designed the overall software architecture of AthletiCLI. +* :bulb: **Features**: + * **Entry**: Designed and implemented the `AthletiCLI` class as the entry component of the program. + * **UI**: Designed and implemented the `Ui` class which provides the basic interactive functions. + * **Commands**: Designed and implemented the `Command` abstract class as the prototype of any other commands. Designed and implemented the `ByeCommand`, the `FindCommand`, the `HelpCommand`, and the `SaveCommand`. + * **Data**: Designed and implemented the `Data` class which serves as the core component of data processing. + * **Retrieval of Entries**: Designed and implemented the `Findable` interface which is an abstraction to lists whose entries can be retrieved with a keyword. + * **Goals**: Designed and implemented the `Goal` abstract class in which the basic attributes (e.g., time span) and methods of a goal. + * **File Storage**: Designed and implemented the `Storage` class and the `StorableList` abstract class. The former provides underlying file reading and writing functions, and the latter is an abstraction to lists which can be converted into a stream to be used for file reading and writing. + * **Exception Handling**: Designed and implemented the `AthletiCLI` class and some exception wrappers. +* :cop: **Project management**: + * Develops the project iteratively and incrementally. + * Manages our project using GitHub mechanisms: milestones, releases, issues, PRs, etc. + * Strictly adheres to the [schedule](https://nus-cs2113-ay2324s1.github.io/website/schedule/timeline.html). + * Strictly follows our [Git conventions](https://se-education.org/guides/conventions/git.html). + * Strictly follows the [forking workflow](https://nus-cs2113-ay2324s1.github.io/website/se-book-adapted/chapters/gitAndGithub.html#forking-workflow). +* :books: **Documentation**: + * Integrated a Jekyll theme ([Alembic](https://github.com/daviddarnes/alembic)) to the project website. + * Integrated a Jekyll plugin ([Jemoji](https://github.com/jekyll/jemoji)) to the project website. + * :green_book: User Guide: + * Added instructions on [miscellaneous commands](../UserGuide.html#miscellaneous). + * :blue_book: Developer Guide: + * Contributed to the sections of [design](../DeveloperGuide.html#design) and [implementation](../DeveloperGuide.html#implementation). + * Added sequence diagrams for [`help add-diet`](../images/HelpAddDiet.svg) and [`save`](../images/Save.svg). + * Checked and unified the format of the DG. +* :family: **Community**: + * :eyes: Reviewed PRs: [tP comments dashboard](https://nus-cs2113-ay2324s1.github.io/dashboards/contents/tp-comments.html) + * :lips: Contributed to forum discussions: [Forum activities dashboard](https://nus-cs2113-ay2324s1.github.io/dashboards/contents/forum-activities.html) + * :open_hands: Reported bugs and suggestions for other teams in the class: + * [Issues I created](https://github.com/AY2324S1-CS2113-T18-1/tp/issues?q=%5BPE-D%5D%5BTester+E%5D) + * [Issues/PRs I commented on](https://github.com/AY2324S1-CS2113-T18-1/tp/issues?q=involves%3Askylee03) \ No newline at end of file diff --git a/docs/team/yicheng-toh.md b/docs/team/yicheng-toh.md new file mode 100644 index 0000000000..87cdf35339 --- /dev/null +++ b/docs/team/yicheng-toh.md @@ -0,0 +1,109 @@ +--- +layout: page +title: Toh Yi Cheng - Project Portfolio Page +--- + +# Project: AthletiCLI + +## Overview + +**AthletiCLI** is an application used to track, analyse, and optimize the users athletic performance. +It is designed for committed athletes who not only keep track of their physical activities but also dietary habits, +sleep metrics, and more. The user interacts with it using a CLI. It is written in Java, and has more than 10k LoC. + +## Summary of Contributions + +### Code contributed: + +* 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=yicheng-toh&tabRepo=AY2324S1-CS2113-T17-1%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Feature implemented + +#### New feature: Users can add and delete diet goals + +For motivated users on AthletiCLI, they can create diet goals to keep track of their nutrient intake. +The nutrients supported currently are Calories, Fat, Carb and Protein. Consuming more nutrient than the +target value indicates that they have achieved their diet goal for that specific nutrient. + +If they would like to remove their existing goals, they can remvoe it with the delete function + +#### New feature: Users can see all diet goals + +This provides a convenient way for users to see all the diet goals that they have created and their current progress. + +#### New feature: Editing diet goals + +Instead of deleting diet goals and creating a new one, edit diet goal functionality hopes to streamline the process. +Instead of typing 2 commands to edit the target value of users' current diet goals, one command could +do the same trick. + +### Enhancements implemented: + +#### Enhancement: Diet goal tracks diets within a limited time period. Daily and Weekly. + +With the enhancement of diet goals, instead of tracking infinite records of diets in the past, the diet goal is +now optimised with controlled time span of 1 day (daily diet goal) or 7 days (weekly diet goal). This enhancement +provides more meaningful diet goal function for the users to track their nutrients intake from their diets. + +This explains the use of daily/weekly in setting and editing diet goal commands. + +**Example of set diet goal command:** `set-diet-goal DAILY calories/500` + +#### Enhancement: Diet goal not only encourages nutrients intake but also discourages nutrients intake. + +Previously, users can only set diet goals that keep track of their nutrient intake. Once they have exceeded their +target value for the nutrients, the diet goals is marked as achieved. + +However, this may not be the case for all nutrients. +For example, for athletes who want to gain muscles, they would increase their intake of protein. At the same time, +they would need to reduce their weight by cutting on fat consumption. +In this case, the diet goal only encourage them to consume more fat. +Therefore, 'unhealthy' diet goal is created. It marks a diet goal as not achieved if the value consumed is greater +than the target value. + +The creation of such goal can be accomplished by indicating an optional flag `unhealthy`. + +**Example of set diet goal command:** `set-diet-goal DAILY unhealthy calories/500` + + +### Contributions to the UG: + +* Contributed to Activities section of AthlethiCLI for UG draft. +* Contributed to Diet Goals section of AthlethiCLI for UG which includes `set-diet-goal`, +`edit-diet-goal`, `list-diet-goal`, `delete-diet-goal`. +* Contributed to the FAQ section of UG. + +### Contributions to the DG: + +#### Sections contributed: +* Setting up of diet goals + +#### Added UML diagrams: + +Class Diagram for AthletiCLI main class: +![](../images/MainClassDiagram.svg) + +Class Diagram for data class: +![](../images/DataClassDiagram.svg) + +Sequence diagram for creating a diet goal: +![](../images/DietGoalsSequenceDiagram.svg) + +### Contributions to team-based tasks + +* Setup approval system for team's GitHub page so that PR gets reviewed before merging. +* Kept track of deadlines for v1.0 and v2.0. +* Assisted in sorting and assigning of post PE dry run issues. +* Suggested the use of interface for find function and abstract class for goals. +This is only implemented due to [skylee03 (Ming-Tian)](./skylee03.html)'s outstanding effort in convincing the team. +* Examples of PR reviewed: + * [PR for editing activities](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/59#discussion_r1362968136) + * [PR for assertions and logging](https://github.com/AY2324S1-CS2113-T17-1/tp/pull/44#discussion_r1361460286) +* Created issues labels: `type.Optimization`, `UG`, `DG` to facilitate effective classification. + +### Contributions beyond the project team + +* PE dry Run: + * [Screenshot captured during PE dry run](https://github.com/yicheng-toh/ped/tree/main/files) +* DG review for other teams: + * [SysLib CLI](https://github.com/nus-cs2113-AY2324S1/tp/pull/6), [Fit Track](https://github.com/nus-cs2113-AY2324S1/tp/pull/13), [FITNUS](https://github.com/nus-cs2113-AY2324S1/tp/pull/27) diff --git a/src/main/java/athleticli/AthletiCLI.java b/src/main/java/athleticli/AthletiCLI.java new file mode 100644 index 0000000000..ca04acd5fb --- /dev/null +++ b/src/main/java/athleticli/AthletiCLI.java @@ -0,0 +1,91 @@ +package athleticli; + +import java.io.IOException; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import athleticli.commands.Command; +import athleticli.commands.SaveCommand; +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; +import athleticli.parser.Parser; +import athleticli.ui.Ui; + +/** + * Defines the basic structure and the behavior of AthletiCLI. + */ +public class AthletiCLI { + private static Logger logger = Logger.getLogger(AthletiCLI.class.getName()); + private static Ui ui = Ui.getInstance(); + private static Data data = Data.getInstance(); + + private static Thread runSaveCommand = new Thread(() -> { + try { + final String[] feedback = new SaveCommand().execute(data); + ui.showMessages(feedback); + } catch (AthletiException e) { + ui.showException(e); + } + }); + + /** + * Constructs an AthletiCLI object. + */ + private AthletiCLI() { + LogManager.getLogManager().reset(); + try { + logger.addHandler(new FileHandler("%t/athleticli-log.txt")); + } catch(IOException e) { + logger.addHandler(new ConsoleHandler()); + } + } + + /** + * Creates an `AthletiCLI` object and runs it. + * + * @param args Arguments obtained from the command line. + */ + public static void main(String[] args) { + new AthletiCLI().run(); + } + + /** + * Displays the welcome interface, continuously reads user input + * and executes corresponding instructions until exiting. + */ + private void run() { + logger.entering(getClass().getName(), "run"); + ui.showWelcome(); + try { + data.load(); + } catch (AthletiException e) { + ui.showException(e); + data.clear(); + } + boolean isExit = false; + boolean isShutdownHookAdded = false; + while (!isExit) { + final String rawUserInput = ui.getUserCommand(); + try { + logger.info("Command read: " + rawUserInput); + final Command command = Parser.parseCommand(rawUserInput); + final String[] feedback = command.execute(data); + ui.showMessages(feedback); + logger.info("Command executed successfully"); + isExit = command.isExit(); + /* add shutdown hook if the first valid command is not exit */ + if (!isExit && !isShutdownHookAdded) { + /* save data when the JVM begins its shutdown sequence */ + Runtime.getRuntime().addShutdownHook(runSaveCommand); + isShutdownHookAdded = true; + } + } catch (AthletiException e) { + ui.showException(e); + logger.warning("Exception caught: " + e); + } + } + logger.exiting(getClass().getName(), "run"); + } +} diff --git a/src/main/java/athleticli/commands/ByeCommand.java b/src/main/java/athleticli/commands/ByeCommand.java new file mode 100644 index 0000000000..afb77333d5 --- /dev/null +++ b/src/main/java/athleticli/commands/ByeCommand.java @@ -0,0 +1,26 @@ +package athleticli.commands; + +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +public class ByeCommand extends Command { + /** + * Returns true if this is a ByeCommand object, otherwise returns false. + * + * @return true if this is a ByeCommand object, otherwise returns false. + */ + @Override + public boolean isExit() { + return true; + } + + /** + * Returns the bye message to be shown to the user. + * + * @return The messages to be shown to the user. + */ + public String[] execute(Data data) throws AthletiException { + return new String[] {Message.MESSAGE_BYE}; + } +} diff --git a/src/main/java/athleticli/commands/Command.java b/src/main/java/athleticli/commands/Command.java new file mode 100644 index 0000000000..3015f2d6b4 --- /dev/null +++ b/src/main/java/athleticli/commands/Command.java @@ -0,0 +1,27 @@ +package athleticli.commands; + +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; + +/** + * Defines the basic methods of a command. + */ +public abstract class Command { + /** + * Executes the command and returns the messages to be shown to the user. + * + * @param data The current data. + * @return The messages to be shown to the user. + * @throws AthletiException + */ + public abstract String[] execute(Data data) throws AthletiException; + + /** + * Returns true if this is a ByeCommand object, otherwise returns false. + * + * @return true if this is a ByeCommand object, otherwise returns false. + */ + public boolean isExit() { + return false; + } +} diff --git a/src/main/java/athleticli/commands/FindCommand.java b/src/main/java/athleticli/commands/FindCommand.java new file mode 100644 index 0000000000..05291bdc28 --- /dev/null +++ b/src/main/java/athleticli/commands/FindCommand.java @@ -0,0 +1,36 @@ +package athleticli.commands; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import athleticli.commands.activity.FindActivityCommand; +import athleticli.commands.diet.FindDietCommand; +import athleticli.commands.sleep.FindSleepCommand; +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; + +public class FindCommand extends Command { + protected LocalDate date; + + public FindCommand(LocalDate date) { + this.date = date; + } + + /** + * Returns the records to be shown to the user. + * + * @param data The current data. + * @return The records to be shown to the user. + * @throws AthletiException + */ + @Override + public String[] execute(Data data) throws AthletiException { + var activities = Stream.of(new FindActivityCommand(date).execute(data)); + var diets = Stream.of(new FindDietCommand(date).execute(data)); + var sleeps = Stream.of(new FindSleepCommand(date).execute(data)); + return Stream.of(activities, diets, sleeps) + .reduce(Stream::concat) + .orElseGet(Stream::empty) + .toArray(String[]::new); + } +} diff --git a/src/main/java/athleticli/commands/HelpCommand.java b/src/main/java/athleticli/commands/HelpCommand.java new file mode 100644 index 0000000000..556d04be5a --- /dev/null +++ b/src/main/java/athleticli/commands/HelpCommand.java @@ -0,0 +1,125 @@ +package athleticli.commands; + +import static java.util.Map.entry; + +import java.util.Map; + +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; +import athleticli.parser.CommandName; +import athleticli.ui.Message; + +public class HelpCommand extends Command { + private static final String[] HELP_ALL = { + /* Activity Management */ + "\nActivity Management:", + Message.HELP_ADD_ACTIVITY, + Message.HELP_ADD_RUN, + Message.HELP_ADD_SWIM, + Message.HELP_ADD_CYCLE, + Message.HELP_DELETE_ACTIVITY, + Message.HELP_LIST_ACTIVITY, + Message.HELP_EDIT_ACTIVITY, + Message.HELP_EDIT_RUN, + Message.HELP_EDIT_SWIM, + Message.HELP_EDIT_CYCLE, + Message.HELP_FIND_ACTIVITY, + Message.HELP_SET_ACTIVITY_GOAL, + Message.HELP_EDIT_ACTIVITY_GOAL, + Message.HELP_DELETE_ACTIVITY_GOAL, + Message.HELP_LIST_ACTIVITY_GOAL, + /* Diet Management */ + "\nDiet Management:", + Message.HELP_ADD_DIET, + Message.HELP_EDIT_DIET, + Message.HELP_DELETE_DIET, + Message.HELP_LIST_DIET, + Message.HELP_FIND_DIET, + + Message.HELP_SET_DIET_GOAL, + Message.HELP_EDIT_DIET_GOAL, + Message.HELP_DELETE_DIET_GOAL, + Message.HELP_LIST_DIET_GOAL, + /* Sleep Management */ + "\nSleep Management:", + Message.HELP_ADD_SLEEP, + Message.HELP_LIST_SLEEP, + Message.HELP_DELETE_SLEEP, + Message.HELP_EDIT_SLEEP, + Message.HELP_FIND_SLEEP, + Message.HELP_SET_SLEEP_GOAL, + Message.HELP_EDIT_SLEEP_GOAL, + Message.HELP_LIST_SLEEP_GOAL, + /* Misc */ + "\nMisc:", + Message.HELP_FIND, + Message.HELP_SAVE, + Message.HELP_BYE, + Message.HELP_HELP, + "\n" + Message.HELP_DETAILS + }; + private static final Map HELP_MESSAGES = Map.ofEntries( + /* Activity Management */ + entry(CommandName.COMMAND_ACTIVITY, Message.HELP_ADD_ACTIVITY), + entry(CommandName.COMMAND_RUN, Message.HELP_ADD_RUN), + entry(CommandName.COMMAND_SWIM, Message.HELP_ADD_SWIM), + entry(CommandName.COMMAND_CYCLE, Message.HELP_ADD_CYCLE), + entry(CommandName.COMMAND_ACTIVITY_DELETE, Message.HELP_DELETE_ACTIVITY), + entry(CommandName.COMMAND_ACTIVITY_LIST, Message.HELP_LIST_ACTIVITY), + entry(CommandName.COMMAND_ACTIVITY_EDIT, Message.HELP_EDIT_ACTIVITY), + entry(CommandName.COMMAND_RUN_EDIT, Message.HELP_EDIT_RUN), + entry(CommandName.COMMAND_SWIM_EDIT, Message.HELP_EDIT_SWIM), + entry(CommandName.COMMAND_CYCLE_EDIT, Message.HELP_EDIT_CYCLE), + entry(CommandName.COMMAND_ACTIVITY_FIND, Message.HELP_FIND_ACTIVITY), + entry(CommandName.COMMAND_ACTIVITY_GOAL_SET, Message.HELP_SET_ACTIVITY_GOAL), + entry(CommandName.COMMAND_ACTIVITY_GOAL_EDIT, Message.HELP_EDIT_ACTIVITY_GOAL), + entry(CommandName.COMMAND_ACTIVITY_GOAL_DELETE, Message.HELP_DELETE_ACTIVITY_GOAL), + entry(CommandName.COMMAND_ACTIVITY_GOAL_LIST, Message.HELP_LIST_ACTIVITY_GOAL), + /* Diet Management */ + entry(CommandName.COMMAND_DIET_ADD, Message.HELP_ADD_DIET), + entry(CommandName.COMMAND_DIET_EDIT, Message.HELP_EDIT_DIET), + entry(CommandName.COMMAND_DIET_DELETE, Message.HELP_DELETE_DIET), + entry(CommandName.COMMAND_DIET_LIST, Message.HELP_LIST_DIET), + entry(CommandName.COMMAND_DIET_FIND, Message.HELP_FIND_DIET), + + entry(CommandName.COMMAND_DIET_GOAL_SET, Message.HELP_SET_DIET_GOAL), + entry(CommandName.COMMAND_DIET_GOAL_EDIT, Message.HELP_EDIT_DIET_GOAL), + entry(CommandName.COMMAND_DIET_GOAL_DELETE, Message.HELP_DELETE_DIET_GOAL), + entry(CommandName.COMMAND_DIET_GOAL_LIST, Message.HELP_LIST_DIET_GOAL), + /* Sleep Management */ + entry(CommandName.COMMAND_SLEEP_ADD, Message.HELP_ADD_SLEEP), + entry(CommandName.COMMAND_SLEEP_LIST, Message.HELP_LIST_SLEEP), + entry(CommandName.COMMAND_SLEEP_DELETE, Message.HELP_DELETE_SLEEP), + entry(CommandName.COMMAND_SLEEP_EDIT, Message.HELP_EDIT_SLEEP), + entry(CommandName.COMMAND_SLEEP_FIND, Message.HELP_FIND_SLEEP), + entry(CommandName.COMMAND_SLEEP_GOAL_SET, Message.HELP_SET_SLEEP_GOAL), + entry(CommandName.COMMAND_SLEEP_GOAL_EDIT, Message.HELP_EDIT_SLEEP_GOAL), + entry(CommandName.COMMAND_SLEEP_GOAL_LIST, Message.HELP_LIST_SLEEP_GOAL), + /* Misc */ + entry(CommandName.COMMAND_FIND, Message.HELP_FIND), + entry(CommandName.COMMAND_SAVE, Message.HELP_SAVE), + entry(CommandName.COMMAND_BYE, Message.HELP_BYE), + entry(CommandName.COMMAND_HELP, Message.HELP_HELP) + ); + + private String command; + public HelpCommand(String command) { + this.command = command; + } + + /** + * Returns the help messages to be shown to the user. + * + * @param data The current data. + * @return The messages to be shown to the user. + * @throws AthletiException + */ + @Override + public String[] execute(Data data) throws AthletiException { + if (HELP_MESSAGES.containsKey(command)) { + return new String[] {"Usage: " + HELP_MESSAGES.get(command)}; + } else { + return HELP_ALL; + } + } +} diff --git a/src/main/java/athleticli/commands/SaveCommand.java b/src/main/java/athleticli/commands/SaveCommand.java new file mode 100644 index 0000000000..1cdd363615 --- /dev/null +++ b/src/main/java/athleticli/commands/SaveCommand.java @@ -0,0 +1,27 @@ +package athleticli.commands; + +import java.io.IOException; + +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +public class SaveCommand extends Command { + /** + * Saves the data into the file. + * + * @param data The current data. + * @return The messages to be shown to the user. + * @throws AthletiException + */ + @Override + public String[] execute(Data data) throws AthletiException { + assert data != null; + try { + data.save(); + } catch (IOException e) { + throw new AthletiException(Message.MESSAGE_IO_EXCEPTION); + } + return new String[] {Message.MESSAGE_SAVE}; + } +} diff --git a/src/main/java/athleticli/commands/activity/AddActivityCommand.java b/src/main/java/athleticli/commands/activity/AddActivityCommand.java new file mode 100644 index 0000000000..987ea700ed --- /dev/null +++ b/src/main/java/athleticli/commands/activity/AddActivityCommand.java @@ -0,0 +1,46 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityList; +import athleticli.ui.Message; + +/** + * Executes the add activity commands provided by the user. + */ +public class AddActivityCommand extends Command { + private final Activity activity; + + /** + * Constructor for AddActivityCommand. + * + * @param activity Activity to be added. + */ + public AddActivityCommand(Activity activity){ + this.activity = activity; + } + + /** + * Updates the activity list by adding a new activity, sorts the list and returns a message to the user. + * + * @param data Current data containing the activity list. + * @return An array of message which will be shown to the user. + */ + @Override + public String[] execute(Data data) { + ActivityList activities = data.getActivities(); + activities.add(activity); + activities.sort(); + int size = activities.size(); + + String countMessage; + if (size > 1) { + countMessage = String.format(Message.MESSAGE_ACTIVITY_COUNT, size); + } else { + countMessage = Message.MESSAGE_ACTIVITY_FIRST; + } + + return new String[]{Message.MESSAGE_ACTIVITY_ADDED, activity.toString(), countMessage}; + } +} diff --git a/src/main/java/athleticli/commands/activity/DeleteActivityCommand.java b/src/main/java/athleticli/commands/activity/DeleteActivityCommand.java new file mode 100644 index 0000000000..a638cfb1b2 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/DeleteActivityCommand.java @@ -0,0 +1,48 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Executes the delete activity command provided by the user. + */ +public class DeleteActivityCommand extends Command { + private final Integer index; + + /** + * Constructs DeleteActivityCommand. + * + * @param index Index of activity to be deleted. + */ + public DeleteActivityCommand(Integer index) { + this.index = index; + } + + /** + * Deletes the activity at the specified index from the list of activities. + * + * @param data Data object containing the current list of activities. + * @return String array containing the messages to be printed to the user. + * @throws AthletiException If the index provided is out of bounds. + */ + @Override + public String[] execute(Data data) throws AthletiException { + ActivityList activities = data.getActivities(); + try { + // Adjusting index as user input is 1-based and list is 0-based + final Activity activity = activities.get(index-1); + activities.remove(activity); + return new String[]{ + Message.MESSAGE_ACTIVITY_DELETED, + activity.toString(), + String.format(Message.MESSAGE_ACTIVITY_COUNT, activities.size()) + }; + } catch (IndexOutOfBoundsException e) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_INDEX_OUT_OF_BOUNDS); + } + } +} diff --git a/src/main/java/athleticli/commands/activity/DeleteActivityGoalCommand.java b/src/main/java/athleticli/commands/activity/DeleteActivityGoalCommand.java new file mode 100644 index 0000000000..1adbd596a4 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/DeleteActivityGoalCommand.java @@ -0,0 +1,66 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.Goal.TimeSpan; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.ActivityGoal.GoalType; +import athleticli.data.activity.ActivityGoal.Sport; +import athleticli.data.activity.ActivityGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Represents a command which deletes an activity goal. + */ +public class DeleteActivityGoalCommand extends Command { + private static final Logger logger = Logger.getLogger(DeleteActivityGoalCommand.class.getName()); + GoalType goalType; + Sport sport; + TimeSpan timeSpan; + + + /** + * Constructor for DeleteActivityGoalCommand. + * + * @param activityGoal Activity goal to be deleted. + */ + public DeleteActivityGoalCommand(ActivityGoal activityGoal) { + this.goalType = activityGoal.getGoalType(); + this.sport = activityGoal.getSport(); + this.timeSpan = activityGoal.getTimeSpan(); + } + + + /** + * Deletes the activity goal from the activity goal list. + * + * @param data The current data containing the activity goal list. + * @return The message which will be shown to the user. + * @throws AthletiException if no such goal exists + */ + @Override + public String[] execute(Data data) throws AthletiException { + logger.info("Deleting activity goal with goal type " + this.goalType + " and sport " + this.sport + + " and time span " + this.timeSpan); + ActivityGoalList activityGoals = data.getActivityGoals(); + String activityGoalString = ""; + if (!activityGoals.isDuplicate(this.goalType, this.sport, this.timeSpan)) { + logger.warning("No such goal exists"); + throw new AthletiException(Message.MESSAGE_NO_SUCH_GOAL_EXISTS); + } + for (int i = 0; i < activityGoals.size(); i++) { + if (activityGoals.get(i).getGoalType() == this.goalType && + activityGoals.get(i).getSport() == this.sport && + activityGoals.get(i).getTimeSpan() == this.timeSpan) { + activityGoalString = activityGoals.get(i).toString(data); + activityGoals.remove(i); + break; + } + } + logger.info("Activity goal deleted successfully"); + return new String[]{Message.MESSAGE_ACTIVITY_GOAL_DELETED, activityGoalString}; + } +} diff --git a/src/main/java/athleticli/commands/activity/EditActivityCommand.java b/src/main/java/athleticli/commands/activity/EditActivityCommand.java new file mode 100644 index 0000000000..b39ea554f1 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/EditActivityCommand.java @@ -0,0 +1,103 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityChanges; +import athleticli.data.activity.ActivityList; +import athleticli.data.activity.Cycle; +import athleticli.data.activity.Run; +import athleticli.data.activity.Swim; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Executes the edit activity command provided by the user. + */ +public class EditActivityCommand extends Command { + private static final Logger logger = Logger.getLogger("EditActivityCommand"); + private final int index; + private final ActivityChanges activityChanges; + private final Class activityType; + + /** + * Constructs EditActivityCommand. + * + * @param index Index of the activity to be edited. + * @param activityChanges Updated Activity. + */ + public EditActivityCommand(int index, ActivityChanges activityChanges, Class activityType) { + this.index = index; + assert index > 0 : "Index should be greater than 0"; + this.activityChanges = activityChanges; + this.activityType = activityType; + } + + /** + * Edits the activity at the specified index. + * + * @param data Data object containing the current list of activities. + * @return String array containing the messages to be printed to the user. + * @throws AthletiException If the index provided is out of bounds. + */ + @Override + public String[] execute(Data data) throws AthletiException { + logger.log(Level.INFO, "Editing activity at index " + index); + ActivityList activities = data.getActivities(); + try { + // Adjusting index as user input is 1-based and list is 0-based + Activity activity = activities.get(index-1); + + if (!activityType.isInstance(activity)) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_TYPE_MISMATCH); + } + + applyActivityChanges(activity, activityChanges); + + activities.sort(); + logger.log(java.util.logging.Level.INFO, "Activity at index " + index + "successfully edited"); + return new String[]{ + Message.MESSAGE_ACTIVITY_UPDATED, + activity.toString(), + String.format(Message.MESSAGE_ACTIVITY_COUNT, activities.size()) + }; + } catch (IndexOutOfBoundsException e) { + logger.log(Level.WARNING, "Activity index out of bounds"); + throw new AthletiException(Message.MESSAGE_ACTIVITY_INDEX_OUT_OF_BOUNDS); + } + } + + /** + * Applies the changes to the activity object. + * + * @param activity Activity to be edited. + * @param activityChanges ActivityChanges object containing the changes to be applied. + */ + private void applyActivityChanges(Activity activity, ActivityChanges activityChanges) { + if (activityChanges.getCaption() != null) { + activity.setCaption(activityChanges.getCaption()); + } + if (activityChanges.getDistance() != 0) { + activity.setDistance(activityChanges.getDistance()); + } + if (activityChanges.getDuration() != null) { + activity.setMovingTime(activityChanges.getDuration()); + } + if (activityChanges.getStartDateTime() != null) { + activity.setStartDateTime(activityChanges.getStartDateTime()); + } + if (activityChanges.getElevation() != 0) { + if (activity instanceof Run) { + ((Run) activity).setElevationGain(activityChanges.getElevation()); + } else if (activity instanceof Cycle) { + ((Cycle) activity).setElevationGain(activityChanges.getElevation()); + } + } + if (activity instanceof Swim && activityChanges.getSwimmingStyle() != null) { + ((Swim) activity).setStyle(activityChanges.getSwimmingStyle()); + } + } +} diff --git a/src/main/java/athleticli/commands/activity/EditActivityGoalCommand.java b/src/main/java/athleticli/commands/activity/EditActivityGoalCommand.java new file mode 100644 index 0000000000..9b4c9eab59 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/EditActivityGoalCommand.java @@ -0,0 +1,53 @@ +package athleticli.commands.activity; + + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.ActivityGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Represents a command which edits an activity goal. + */ +public class EditActivityGoalCommand extends Command { + private static final Logger logger = Logger.getLogger(EditActivityGoalCommand.class.getName()); + private final ActivityGoal activityGoal; + + /** + * Constructor for EditActivityGoalCommand. + * + * @param activityGoal Activity goal to be edited. + */ + public EditActivityGoalCommand(ActivityGoal activityGoal) { + this.activityGoal = activityGoal; + } + + /** + * Updates the activity goal list. + * + * @param data The current data containing the activity goal list. + * @return The message which will be shown to the user. + * @throws AthletiException if no such goal exists + */ + @Override + public String[] execute(Data data) throws athleticli.exceptions.AthletiException { + logger.info("Editing activity goal with goal type " + this.activityGoal.getGoalType() + " and sport " + + this.activityGoal.getSport() + " and time span " + this.activityGoal.getTimeSpan()); + ActivityGoalList activityGoals = data.getActivityGoals(); + for (ActivityGoal goal : activityGoals) { + if (goal.getSport() == this.activityGoal.getSport() && + goal.getGoalType() == this.activityGoal.getGoalType() && + goal.getTimeSpan() == this.activityGoal.getTimeSpan()) { + goal.setTargetValue(this.activityGoal.getTargetValue()); + logger.info("Activity goal edited successfully"); + return new String[]{Message.MESSAGE_ACTIVITY_GOAL_EDITED, this.activityGoal.toString(data)}; + } + } + logger.warning("No such goal exists"); + throw new AthletiException(Message.MESSAGE_NO_SUCH_GOAL_EXISTS); + } +} diff --git a/src/main/java/athleticli/commands/activity/FindActivityCommand.java b/src/main/java/athleticli/commands/activity/FindActivityCommand.java new file mode 100644 index 0000000000..29a70dcaf3 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/FindActivityCommand.java @@ -0,0 +1,35 @@ +package athleticli.commands.activity; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import athleticli.commands.FindCommand; +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +public class FindActivityCommand extends FindCommand { + public FindActivityCommand(LocalDate date) { + super(date); + } + + /** + * Returns the activities matching the date to be shown to the user. + * + * @param data The current data. + * @return The messages to be shown to the user. + * @throws AthletiException + */ + @Override + public String[] execute(Data data) throws AthletiException { + var resultStream = data.getActivities() + .find(date) + .stream() + .filter(Activity.class::isInstance) + .map(Activity.class::cast) + .map(Activity::toString); + return Stream.concat(Stream.of(Message.MESSAGE_ACTIVITY_FIND), resultStream) + .toArray(String[]::new); + } +} diff --git a/src/main/java/athleticli/commands/activity/ListActivityCommand.java b/src/main/java/athleticli/commands/activity/ListActivityCommand.java new file mode 100644 index 0000000000..b1f94b0512 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/ListActivityCommand.java @@ -0,0 +1,83 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityList; +import athleticli.ui.Message; + +/** + * Executes the list activity command provided by the user. + */ +public class ListActivityCommand extends Command { + private final boolean isDetailed; + + /** + * Constructs instance of ListActivityCommand. + * + * @param isDetailed Whether the list should be detailed. + */ + public ListActivityCommand(boolean isDetailed) { + this.isDetailed = isDetailed; + } + + /** + * Lists the activities in either a detailed or summary format. + * + * @param data Current data containing the activity list. + * @return The message containing listing of activities which will be shown to the user. The format is + * based on the 'isDetailed' flag. + */ + @Override + public String[] execute(Data data) { + ActivityList activities = data.getActivities(); + final int size = activities.size(); + + if (size == 0) { + return new String[]{Message.MESSAGE_EMPTY_ACTIVITY_LIST}; + } + + if (isDetailed) { + return printDetailedList(activities, size); + } else { + return printList(activities, size); + } + } + + /** + * Prints the list of activities. + * + * @param activities The current activity list. + * @param size The size of the activity list. + * @return The message containing listing of activities which will be shown to the user. + */ + public String[] printList(ActivityList activities, int size) { + String[] output = new String[size + 2]; + output[0] = Message.MESSAGE_ACTIVITY_LIST; + + int index = 1; + for (Activity activity : activities) { + output[index++] = index-1 + "." + activity.toString(); + } + + output[index] = Message.MESSAGE_ACTIVITY_LIST_END; + return output; + } + + /** + * Prints the detailed list of activities. + * + * @param activities The current activity list. + * @param size The size of the activity list. + * @return The message containing listing of activities which will be shown to the user. + */ + public String[] printDetailedList(ActivityList activities, int size) { + String[] output = new String[size + 1]; + output[0] = Message.MESSAGE_ACTIVITY_LIST; + int index = 1; + for (Activity activity : activities) { + output[index++] = activity.toDetailedString(); + } + return output; + } +} diff --git a/src/main/java/athleticli/commands/activity/ListActivityGoalCommand.java b/src/main/java/athleticli/commands/activity/ListActivityGoalCommand.java new file mode 100644 index 0000000000..a321e39967 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/ListActivityGoalCommand.java @@ -0,0 +1,40 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.ActivityGoalList; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Lists the activity goals. + */ +public class ListActivityGoalCommand extends Command { + private static final Logger logger = Logger.getLogger(ListActivityGoalCommand.class.getName()); + /** + * Constructor for ListActivityCommand. + */ + public ListActivityGoalCommand() { + } + + /** + * Lists the activities. + * + * @param data The current data containing the activity list. + * @return The message containing listing of activities which will be shown to the user. + */ + @Override + public String[] execute(Data data) { + logger.info("Listing activity goals"); + ActivityGoalList activityGoals = data.getActivityGoals(); + int size = activityGoals.size(); + String[] output = new String[size + 1]; + output[0] = Message.MESSAGE_ACTIVITY_GOAL_LIST; + for (int i = 0; i < activityGoals.size(); i++) { + output[i + 1] = (i + 1) + ". " + activityGoals.get(i).toString(data); + } + logger.info("Found " + size + " activity goals"); + return output; + } +} diff --git a/src/main/java/athleticli/commands/activity/SetActivityGoalCommand.java b/src/main/java/athleticli/commands/activity/SetActivityGoalCommand.java new file mode 100644 index 0000000000..43e0c936f8 --- /dev/null +++ b/src/main/java/athleticli/commands/activity/SetActivityGoalCommand.java @@ -0,0 +1,44 @@ +package athleticli.commands.activity; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.ActivityGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Represents a command which adds an activity goal to the activity goal list. + */ +public class SetActivityGoalCommand extends Command { + private final ActivityGoal activityGoal; + + /** + * Constructs instance of SetActivityGoalCommand. + * + * @param activityGoal Activity goal to be added. + */ + public SetActivityGoalCommand(ActivityGoal activityGoal){ + this.activityGoal = activityGoal; + } + + /** + * Adds the activity goal to the activity goal list. + * + * @param data The current data containing the activity goal list. + * @return The message which will be shown to the user. + * @throws AthletiException If a duplicate activity goal is detected. + */ + @Override + public String[] execute(Data data) throws AthletiException { + ActivityGoalList activityGoals = data.getActivityGoals(); + + if (activityGoals.isDuplicate(activityGoal.getGoalType(), activityGoal.getSport(), + activityGoal.getTimeSpan())) { + throw new AthletiException(Message.MESSAGE_DUPLICATE_ACTIVITY_GOAL); + } + + activityGoals.add(this.activityGoal); + return new String[]{Message.MESSAGE_ACTIVITY_GOAL_ADDED, this.activityGoal.toString(data)}; + } +} diff --git a/src/main/java/athleticli/commands/diet/AddDietCommand.java b/src/main/java/athleticli/commands/diet/AddDietCommand.java new file mode 100644 index 0000000000..a34604e3e8 --- /dev/null +++ b/src/main/java/athleticli/commands/diet/AddDietCommand.java @@ -0,0 +1,49 @@ +package athleticli.commands.diet; + + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.data.diet.DietList; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Executes the add diet commands provided by the user. + */ +public class AddDietCommand extends Command { + private static final Logger logger = Logger.getLogger(AddDietCommand.class.getName()); + private final Diet diet; + + /** + * Constructor for AddDietCommand. + * + * @param diet Diet to be added. + */ + public AddDietCommand(Diet diet) { + this.diet = diet; + } + + /** + * Updates the diet list. + * + * @param data The current data containing the diet list. + * @return The message which will be shown to the user. + */ + @Override + public String[] execute(Data data) { + logger.info("Adding diet" + diet.toString()); + DietList diets = data.getDiets(); + diets.add(this.diet); + int size = diets.size(); + String countMessage; + if (size > 1) { + countMessage = String.format(Message.MESSAGE_DIET_COUNT, size); + } else { + countMessage = Message.MESSAGE_DIET_FIRST; + } + logger.info("Diet added successfully"); + return new String[]{Message.MESSAGE_DIET_ADDED, this.diet.toString(), countMessage}; + } +} diff --git a/src/main/java/athleticli/commands/diet/DeleteDietCommand.java b/src/main/java/athleticli/commands/diet/DeleteDietCommand.java new file mode 100644 index 0000000000..571bb711ae --- /dev/null +++ b/src/main/java/athleticli/commands/diet/DeleteDietCommand.java @@ -0,0 +1,49 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.data.diet.DietList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Executes the add diet commands provided by the user. + */ +public class DeleteDietCommand extends Command { + private static final Logger logger = Logger.getLogger(DeleteDietCommand.class.getName()); + private final int index; + + /** + * Constructor for AddDietCommand. + * + * @param index Diet to be added. + */ + public DeleteDietCommand(int index) { + assert index > 0 : "Index cannot be less than 1"; + this.index = index; + } + + /** + * Updates the diet list. + * + * @param data The current data containing the diet list. + * @return The message which will be shown to the user. + */ + public String[] execute(Data data) throws AthletiException { + logger.info("Deleting diet at index " + index); + DietList dietList = data.getDiets(); + int size = dietList.size(); + if (index > size) { + logger.warning("Index out of bounds"); + throw new AthletiException(Message.MESSAGE_INVALID_DIET_INDEX); + } + Diet oldDiet = dietList.get(index - 1); + dietList.remove(index - 1); + logger.info("Diet deleted successfully"); + return new String[]{Message.MESSAGE_DIET_DELETED, oldDiet.toString(), + String.format(Message.MESSAGE_DIET_COUNT, size - 1)}; + } +} diff --git a/src/main/java/athleticli/commands/diet/DeleteDietGoalCommand.java b/src/main/java/athleticli/commands/diet/DeleteDietGoalCommand.java new file mode 100644 index 0000000000..62391b6309 --- /dev/null +++ b/src/main/java/athleticli/commands/diet/DeleteDietGoalCommand.java @@ -0,0 +1,65 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.DietGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.io.IOException; + +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Executes the delete-diet-goal commands provided by the user. + */ +public class DeleteDietGoalCommand extends Command { + private static final Logger logger = Logger.getLogger(DeleteDietGoalCommand.class.getName()); + private final int deleteIndex; + + /** + * This is a constructor to set up the delete diet goal command. + * + * @param deleteIndex Index of the diet goal to be deleted in the users' perspective. + */ + public DeleteDietGoalCommand(int deleteIndex) { + //deleteIndex that is less than or equal to zero would result in exception + assert deleteIndex >= 1: "'deleteIndex' should have the value of 1 minimally."; + this.deleteIndex = deleteIndex; + LogManager.getLogManager().reset(); + try { + logger.addHandler(new FileHandler("%t/athleticli-log.txt")); + } catch(IOException e) { + logger.addHandler(new ConsoleHandler()); + } + } + + /** + * Deletes a goal from the Diet Goal List. + * + * @param data The current data containing the different nutrient goals. + * @return The message which will be shown to the user. + */ + public String[] execute(Data data) throws AthletiException { + logger.log(Level.FINE, "Executing delete command for diet goals"); + DietGoalList dietGoals = data.getDietGoals(); + if (dietGoals.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_EMPTY_DIET_GOAL_LIST); + } + try { + DietGoal dietGoalRemoved = dietGoals.get(deleteIndex - 1); + dietGoals.remove(deleteIndex - 1); + logger.log(Level.FINE, String.format("Diet goals %s has been successfully removed", + dietGoalRemoved.getNutrient())); + return new String[]{Message.MESSAGE_DIET_GOAL_DELETE_HEADER, + dietGoalRemoved.toString(data)}; + } catch (IndexOutOfBoundsException e) { + throw new AthletiException(String.format(Message.MESSAGE_DIET_GOAL_OUT_OF_BOUND, dietGoals.size())); + } + } +} diff --git a/src/main/java/athleticli/commands/diet/EditDietCommand.java b/src/main/java/athleticli/commands/diet/EditDietCommand.java new file mode 100644 index 0000000000..a7e98d4b1a --- /dev/null +++ b/src/main/java/athleticli/commands/diet/EditDietCommand.java @@ -0,0 +1,81 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.data.diet.DietList; +import athleticli.exceptions.AthletiException; +import athleticli.parser.Parameter; +import athleticli.parser.Parser; +import athleticli.ui.Message; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.logging.Logger; + +/** + * Executes the edit diet command provided by the user. + */ +public class EditDietCommand extends Command { + private static final Logger logger = Logger.getLogger(EditDietCommand.class.getName()); + private final int index; + private final HashMap dietMap; + + /** + * Constructor for EditDietCommand. + * + * @param index Index of the diet to be edited. + * @param dietMap Updated Diet. + */ + public EditDietCommand(int index, HashMap dietMap) { + assert index > 0 : "Index should be greater than 0"; + assert !dietMap.isEmpty() : "Diet map should not be empty"; + this.index = index; + this.dietMap = dietMap; + } + + /** + * Executes the edit diet command. + * + * @param data Data object containing the current list of diets. + * @return String array containing the messages to be printed to the user. + * @throws AthletiException If the index provided is out of bounds. + */ + @Override + public String[] execute(Data data) throws AthletiException { + logger.info("Editing diet at index " + index); + DietList diets = data.getDiets(); + int size = diets.size(); + if (index > size) { + logger.warning("Index out of bounds"); + throw new AthletiException(Message.MESSAGE_INVALID_DIET_INDEX); + } + Diet oldDiet = diets.get(index - 1); + for (String key : dietMap.keySet()) { + assert !java.util.Objects.equals(dietMap.get(key), "") : "Diet parameter should not be empty"; + switch (key) { + case Parameter.CALORIES_SEPARATOR: + oldDiet.setCalories(Integer.parseInt(dietMap.get(key))); + break; + case Parameter.PROTEIN_SEPARATOR: + oldDiet.setProtein(Integer.parseInt(dietMap.get(key))); + break; + case Parameter.CARB_SEPARATOR: + oldDiet.setCarb(Integer.parseInt(dietMap.get(key))); + break; + case Parameter.FAT_SEPARATOR: + oldDiet.setFat(Integer.parseInt(dietMap.get(key))); + break; + case Parameter.DATETIME_SEPARATOR: + LocalDateTime dateTime = Parser.parseDateTime(dietMap.get(key)); + oldDiet.setDateTime(dateTime); + break; + default: + break; + } + } + diets.set(index - 1, oldDiet); + logger.info("Diet edited successfully"); + return new String[]{Message.MESSAGE_DIET_UPDATED, oldDiet.toString()}; + } +} diff --git a/src/main/java/athleticli/commands/diet/EditDietGoalCommand.java b/src/main/java/athleticli/commands/diet/EditDietGoalCommand.java new file mode 100644 index 0000000000..1c4a580cb3 --- /dev/null +++ b/src/main/java/athleticli/commands/diet/EditDietGoalCommand.java @@ -0,0 +1,94 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; + +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.DietGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.ArrayList; + +/** + * Executes the edit-diet-goal commands provided by the user. + */ +public class EditDietGoalCommand extends Command { + private final ArrayList userUpdatedDietGoals; + + /** + * This is a constructor to set up the edit diet goal command. + * + * @param dietGoals This is a list consisting of updated existing diet goals. + * to be added to the current goal list. + */ + public EditDietGoalCommand(ArrayList dietGoals) { + userUpdatedDietGoals = dietGoals; + } + + /** + * Updates the Diet Goal List. + * + * @param data The current data containing the different nutrient goals. + * @return The message which will be shown to the user. + */ + @Override + public String[] execute(Data data) throws AthletiException { + DietGoalList currentDietGoals = data.getDietGoals(); + verifyEditedGoalsValid(currentDietGoals); + updateUserGoals(currentDietGoals); + return generateEditDietGoalSuccessMessage(data, currentDietGoals); + } + + private static String[] generateEditDietGoalSuccessMessage(Data data, DietGoalList currentDietGoals) { + int dietGoalNum = currentDietGoals.size(); + return new String[]{Message.MESSAGE_DIET_GOAL_LIST_HEADER, currentDietGoals.toString(data), + String.format(Message.MESSAGE_DIET_GOAL_COUNT, dietGoalNum)}; + } + + private void updateUserGoals(DietGoalList currentDietGoals) { + int newTargetValue; + for (DietGoal userUpdatedDietGoal : userUpdatedDietGoals) { + for (DietGoal currentDietGoal : currentDietGoals) { + boolean isSameDietGoalNutrient = + userUpdatedDietGoal.getNutrient().equals(currentDietGoal.getNutrient()); + boolean isSameTimeSpan = userUpdatedDietGoal.getTimeSpan().getDays() + == currentDietGoal.getTimeSpan().getDays(); + if (!isSameDietGoalNutrient) { + continue; + } + if (!isSameTimeSpan) { + continue; + } + //update new target value to the current goal + newTargetValue = userUpdatedDietGoal.getTargetValue(); + currentDietGoal.setTargetValue(newTargetValue); + } + } + } + + private void verifyEditedGoalsValid(DietGoalList currentDietGoals) throws AthletiException { + for (DietGoal userDietGoal : userUpdatedDietGoals) { + boolean isDietGoalExisted = false; + currentDietGoals.isDietGoalTypeValid(userDietGoal); + + if (currentDietGoals.isDietGoalUnique(userDietGoal)) { + throw new AthletiException(String.format(Message.MESSAGE_DIET_GOAL_NOT_EXISTED, + userDietGoal.getNutrient(), userDietGoal.getTimeSpan().toString())); + } + if (!currentDietGoals.isDietGoalTypeValid(userDietGoal)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TYPE_CLASH); + } + if (!currentDietGoals.isTargetValueConsistentWithTimeSpan(userDietGoal)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_SCALING_WITH_TIME_SPAN); + } + isDietGoalExisted = true; + + if (!isDietGoalExisted) { + throw new AthletiException(String.format(Message.MESSAGE_DIET_GOAL_NOT_EXISTED, + userDietGoal.getNutrient(), userDietGoal.getTimeSpan().toString())); + } + } + } +} + diff --git a/src/main/java/athleticli/commands/diet/FindDietCommand.java b/src/main/java/athleticli/commands/diet/FindDietCommand.java new file mode 100644 index 0000000000..2bb21b8f41 --- /dev/null +++ b/src/main/java/athleticli/commands/diet/FindDietCommand.java @@ -0,0 +1,43 @@ +package athleticli.commands.diet; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import athleticli.commands.FindCommand; +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.logging.Logger; + + +/** + * Finds diets matching the date. + */ +public class FindDietCommand extends FindCommand { + private static final Logger logger = Logger.getLogger(FindDietCommand.class.getName()); + public FindDietCommand(LocalDate date) { + super(date); + } + + /** + * Returns the diets matching the date to be shown to the user. + * + * @param data The current data. + * @return The messages to be shown to the user. + * @throws AthletiException + */ + @Override + public String[] execute(Data data) throws AthletiException { + logger.info("Finding diets on " + date); + var resultStream = data.getDiets() + .find(date) + .stream() + .filter(Diet.class::isInstance) + .map(Diet.class::cast) + .map(Diet::toString); + return Stream.concat(Stream.of(Message.MESSAGE_DIET_FIND), resultStream) + .toArray(String[]::new); + } +} diff --git a/src/main/java/athleticli/commands/diet/ListDietCommand.java b/src/main/java/athleticli/commands/diet/ListDietCommand.java new file mode 100644 index 0000000000..01fd589119 --- /dev/null +++ b/src/main/java/athleticli/commands/diet/ListDietCommand.java @@ -0,0 +1,36 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.DietList; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Executes the list diet commands provided by the user. + */ +public class ListDietCommand extends Command { + private static final Logger logger = Logger.getLogger(ListDietCommand.class.getName()); + + /** + * Constructor for ListDietCommand. + */ + public ListDietCommand() { + } + + /** + * Updates the diet list. + * + * @param data The current data containing the diet list. + * @return The message which will be shown to the user. + */ + public String[] execute(Data data) { + logger.info("Listing diets"); + DietList dietList = data.getDiets(); + int size = dietList.size(); + logger.info("Found " + size + " diets"); + return new String[]{Message.MESSAGE_DIET_LIST, dietList.toString(), + String.format(Message.MESSAGE_DIET_COUNT, size)}; + } +} diff --git a/src/main/java/athleticli/commands/diet/ListDietGoalCommand.java b/src/main/java/athleticli/commands/diet/ListDietGoalCommand.java new file mode 100644 index 0000000000..877d6062d2 --- /dev/null +++ b/src/main/java/athleticli/commands/diet/ListDietGoalCommand.java @@ -0,0 +1,34 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.DietGoalList; +import athleticli.ui.Message; + +/** + * Executes the list diet goal commands provided by the user. + */ +public class ListDietGoalCommand extends Command { + + /** + * Constructor for ListDietGoalCommand. + */ + public ListDietGoalCommand() { + } + + /** + * Iterate and returns the string representation for each goal. + * + * @param data The current data containing the diet goal list. + * @return The message which will be shown to the user. + */ + public String[] execute(Data data) { + DietGoalList dietGoalList = data.getDietGoals(); + int dietGoalNum = dietGoalList.size(); + if (dietGoalNum == 0) { + return new String[]{Message.MESSAGE_DIET_GOAL_NONE}; + } + return new String[]{Message.MESSAGE_DIET_GOAL_LIST_HEADER, dietGoalList.toString(data), + String.format(Message.MESSAGE_DIET_GOAL_COUNT, dietGoalNum)}; + } +} diff --git a/src/main/java/athleticli/commands/diet/SetDietGoalCommand.java b/src/main/java/athleticli/commands/diet/SetDietGoalCommand.java new file mode 100644 index 0000000000..f383dc435f --- /dev/null +++ b/src/main/java/athleticli/commands/diet/SetDietGoalCommand.java @@ -0,0 +1,66 @@ +package athleticli.commands.diet; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.DietGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.ArrayList; + +/** + * Executes the set-diet-goal commands provided by the user. + */ +public class SetDietGoalCommand extends Command { + + private final ArrayList userNewDietGoals; + + /** + * This is a constructor to set up the set diet goal command + * + * @param dietGoals This is a list consisting of new diet goals + * to be added to the current goal list. + */ + public SetDietGoalCommand(ArrayList dietGoals) { + userNewDietGoals = dietGoals; + } + + /** + * Updates the Diet Goal list. + * + * @param data The current data containing the different nutrients goal value. + * @return The message which will be shown to the user. + */ + @Override + public String[] execute(Data data) throws AthletiException { + DietGoalList currentDietGoals = data.getDietGoals(); + verifyNewGoalsValid(currentDietGoals); + addNewUserDietGoals(currentDietGoals); + return generateSetDietGoalSuccessMessage(data, currentDietGoals); + } + + private static String[] generateSetDietGoalSuccessMessage(Data data, DietGoalList currentDietGoals) { + int dietGoalNum = currentDietGoals.size(); + return new String[]{Message.MESSAGE_DIET_GOAL_LIST_HEADER, currentDietGoals.toString(data), + String.format(Message.MESSAGE_DIET_GOAL_COUNT, dietGoalNum)}; + } + + private void addNewUserDietGoals(DietGoalList currentDietGoals) { + currentDietGoals.addAll(userNewDietGoals); + } + + private void verifyNewGoalsValid(DietGoalList currentDietGoals) throws AthletiException { + for (DietGoal userDietGoal : userNewDietGoals) { + if (!currentDietGoals.isDietGoalUnique(userDietGoal)) { + throw new AthletiException(String.format(Message.MESSAGE_DIET_GOAL_ALREADY_EXISTED, + userDietGoal.getNutrient())); + } else if (!currentDietGoals.isDietGoalTypeValid(userDietGoal)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TYPE_CLASH); + } else if (!currentDietGoals.isTargetValueConsistentWithTimeSpan(userDietGoal)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_SCALING_WITH_TIME_SPAN); + } + } + } + +} diff --git a/src/main/java/athleticli/commands/sleep/AddSleepCommand.java b/src/main/java/athleticli/commands/sleep/AddSleepCommand.java new file mode 100644 index 0000000000..ae8aeab7b9 --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/AddSleepCommand.java @@ -0,0 +1,64 @@ +package athleticli.commands.sleep; + +import java.util.logging.Logger; +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Represents a command which adds a sleep entry. + */ +public class AddSleepCommand extends Command { + private final Logger logger = Logger.getLogger(AddSleepCommand.class.getName()); + private final Sleep sleep; + + /** + * Constructor for AddSleepCommand. + * + * @param sleep Sleep to be added. + */ + public AddSleepCommand(Sleep sleep) { + this.sleep = sleep; + logger.fine("Creating AddSleepCommand with sleep: " + sleep.toString()); + assert sleep.getStartDateTime() != null : "Start time cannot be null"; + assert sleep.getEndDateTime() != null : "End time cannot be null"; + assert sleep.getStartDateTime().isBefore(sleep.getEndDateTime()) : "Start time must be before end time"; + } + + /** + * Adds the sleep record to the sleep list. Sorts the sleep list after adding. + * + * @param data The current data containing the sleep list. + * @return The message which will be shown to the user. + */ + @Override + public String[] execute(Data data) throws AthletiException { + SleepList sleeps = data.getSleeps(); + + for (Sleep s : sleeps) { + if (sleep.getStartDateTime().isBefore(s.getEndDateTime()) + && sleep.getEndDateTime().isAfter(s.getStartDateTime())) { + throw new AthletiException(Message.ERRORMESSAGE_SLEEP_OVERLAP); + } + } + + sleeps.add(this.sleep); + sleeps.sort(); + int size = sleeps.size(); + + logger.info("Added sleep: " + this.sleep.toString()); + logger.info("Sleep count: " + sleeps.size()); + logger.info("Sleep list: " + sleeps.toString()); + + String countMessage; + if (size > 1) { + countMessage = String.format(Message.MESSAGE_SLEEP_COUNT, size); + } else { + countMessage = String.format(Message.MESSAGE_SLEEP_FIRST, size); + } + return new String[] {Message.MESSAGE_SLEEP_ADDED, this.sleep.toString(), countMessage}; + } +} diff --git a/src/main/java/athleticli/commands/sleep/DeleteSleepCommand.java b/src/main/java/athleticli/commands/sleep/DeleteSleepCommand.java new file mode 100644 index 0000000000..d20ba377d7 --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/DeleteSleepCommand.java @@ -0,0 +1,55 @@ +package athleticli.commands.sleep; + +import java.util.logging.Logger; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Represents a command which deletes a sleep entry. + */ +public class DeleteSleepCommand extends Command { + private final Logger logger = Logger.getLogger(DeleteSleepCommand.class.getName()); + private final int index; + + /** + * Constructor for DeleteSleepCommand. + * + * @param index Index of the sleep to be deleted. + */ + public DeleteSleepCommand(int index) { + this.index = index; + logger.fine("Creating DeleteSleepCommand with index: " + index); + } + + /** + * Deletes the sleep record at the specified index. + * + * @param data The current data containing the sleep list. + * @return The message which will be shown to the user. + * @throws AthletiException If the index is out of bounds. + */ + public String[] execute(Data data) throws AthletiException { + SleepList sleeps = data.getSleeps(); + try { + final Sleep sleep = sleeps.get(index-1); + sleeps.remove(sleep); + + logger.info("Deleting sleep: " + sleep.toString()); + logger.info("Sleep count: " + sleeps.size()); + logger.info("Sleep list: " + sleeps.toString()); + + return new String[]{ + Message.MESSAGE_SLEEP_DELETED, + sleep.toString(), + String.format(Message.MESSAGE_SLEEP_COUNT, sleeps.size()) + }; + } catch (IndexOutOfBoundsException e) { + throw new AthletiException(Message.ERRORMESSAGE_SLEEP_DELETE_INDEX_OOBE); + } + } +} diff --git a/src/main/java/athleticli/commands/sleep/EditSleepCommand.java b/src/main/java/athleticli/commands/sleep/EditSleepCommand.java new file mode 100644 index 0000000000..ec6f80bb8b --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/EditSleepCommand.java @@ -0,0 +1,61 @@ +package athleticli.commands.sleep; + +import java.util.logging.Logger; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Represents a command which edits a sleep entry. + */ +public class EditSleepCommand extends Command { + private static final Logger logger = Logger.getLogger(EditSleepCommand.class.getName()); + private final int index; + private final Sleep newSleep; + + /** + * Constructor for EditSleepCommand. + * + * @param index Index of the sleep to be edited. + * @param newSleep New sleep record to update the old one. + */ + public EditSleepCommand(int index, Sleep newSleep) { + this.index = index; + this.newSleep = newSleep; + logger.fine("Creating EditSleepCommand with index: " + index); + } + + /** + * Edits the sleep record at the specified index. + * + * @param data The current data containing the sleep list. + * @return The message which will be shown to the user. + * @throws AthletiException If the index is out of bounds. + */ + public String[] execute(Data data) throws AthletiException { + SleepList sleeps = data.getSleeps(); + for (Sleep s : sleeps) { + if (newSleep.getStartDateTime().isBefore(s.getEndDateTime()) + && newSleep.getEndDateTime().isAfter(s.getStartDateTime())) { + throw new AthletiException(Message.ERRORMESSAGE_SLEEP_OVERLAP); + } + } + try { + final Sleep oldSleep = sleeps.get(index-1); + sleeps.set(index-1, newSleep); + + logger.info("Activity at index " + index + " successfully edited"); + + return new String[]{Message.MESSAGE_SLEEP_EDITED, + "original: " + oldSleep, + "new: " + newSleep + }; + } catch (IndexOutOfBoundsException e) { + throw new AthletiException(Message.ERRORMESSAGE_SLEEP_EDIT_INDEX_OOBE); + } + } +} diff --git a/src/main/java/athleticli/commands/sleep/EditSleepGoalCommand.java b/src/main/java/athleticli/commands/sleep/EditSleepGoalCommand.java new file mode 100644 index 0000000000..5dcb046ef1 --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/EditSleepGoalCommand.java @@ -0,0 +1,54 @@ +package athleticli.commands.sleep; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.SleepGoal; +import athleticli.data.sleep.SleepGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Represents a command which edits an activity goal. + */ +public class EditSleepGoalCommand extends Command { + private static final Logger logger = Logger.getLogger(EditSleepGoalCommand.class.getName()); + private final SleepGoal sleepGoal; + + /** + * Constructor for EditActivityGoalCommand. + * + * @param sleepGoal Activity goal to be edited. + */ + public EditSleepGoalCommand(SleepGoal sleepGoal) { + this.sleepGoal = sleepGoal; + } + + /** + * Updates the sleep goal list. + * + * @param data The current data containing the sleep goal list. + * @return The message which will be shown to the user. + * @throws AthletiException if no such goal exists + */ + + public String[] execute(Data data) throws athleticli.exceptions.AthletiException { + logger.info("Editing sleep goal with goal type " + this.sleepGoal.getGoalType() + " and time span " + + this.sleepGoal.getTimeSpan()); + + SleepGoalList sleepGoals = data.getSleepGoals(); + for (SleepGoal goal : sleepGoals) { + if (goal.getGoalType() == this.sleepGoal.getGoalType() && + goal.getTimeSpan() == this.sleepGoal.getTimeSpan()) { + goal.setTargetValue(this.sleepGoal.getTargetValue()); + logger.info("Sleep goal edited successfully"); + return new String[]{Message.MESSAGE_SLEEP_GOAL_EDITED, this.sleepGoal.toString(data)}; + } + } + + logger.warning("No such goal exists"); + + throw new AthletiException(Message.MESSAGE_NO_SUCH_GOAL_EXISTS); + } +} diff --git a/src/main/java/athleticli/commands/sleep/FindSleepCommand.java b/src/main/java/athleticli/commands/sleep/FindSleepCommand.java new file mode 100644 index 0000000000..d8b30c869a --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/FindSleepCommand.java @@ -0,0 +1,47 @@ +package athleticli.commands.sleep; + +import java.time.LocalDate; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import athleticli.commands.FindCommand; +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Represents a command which finds a sleep entry. + */ +public class FindSleepCommand extends FindCommand { + private final Logger logger = Logger.getLogger(FindSleepCommand.class.getName()); + + /** + * Constructor for FindSleepCommand. + * + * @param date Date of the sleep to be found. + */ + public FindSleepCommand(LocalDate date) { + super(date); + } + + /** + * Returns the sleeps matching the date to be shown to the user. + * + * @param data The current data. + * @return The messages to be shown to the user. + * @throws AthletiException If any errors occur in finding the sleep entry. + */ + @Override + public String[] execute(Data data) throws AthletiException { + logger.info("Finding sleeps on " + date); + var resultStream = data.getSleeps() + .find(date) + .stream() + .filter(Sleep.class::isInstance) + .map(Sleep.class::cast) + .map(Sleep::toString); + return Stream.concat(Stream.of(Message.MESSAGE_SLEEP_FIND), resultStream) + .toArray(String[]::new); + } +} diff --git a/src/main/java/athleticli/commands/sleep/ListSleepCommand.java b/src/main/java/athleticli/commands/sleep/ListSleepCommand.java new file mode 100644 index 0000000000..1471016d52 --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/ListSleepCommand.java @@ -0,0 +1,58 @@ +package athleticli.commands.sleep; + +import java.util.logging.Logger; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.SleepList; +import athleticli.ui.Message; + +/** + * Represents a command which lists all the sleep records. + */ +public class ListSleepCommand extends Command { + private static final Logger logger = Logger.getLogger(ListSleepCommand.class.getName()); + + /** + * Lists all the sleep records in the sleep list. + * + * @param data The current data containing the sleep list. + * @return The message array which will be shown to the user. + */ + @Override + public String[] execute(Data data) { + logger.info("Executing ListSleepCommand"); + SleepList sleeps = data.getSleeps(); + final int size = sleeps.size(); + + if (size == 0) { + logger.fine("Sleep list is empty"); + return new String[] { + Message.MESSAGE_SLEEP_LIST_EMPTY + }; + } + + return printList(sleeps, size); + } + + /** + * Prints the list of sleep records. + * + * @param sleeps The current sleep list. + * @param size The size of the sleep list. + * @return The message containing list of sleep records which will be shown to the user. + */ + public String[] printList(SleepList sleeps, int size) { + logger.fine("Printing sleep list"); + logger.info("Sleep count: " + sleeps.size()); + logger.info("Sleep list: " + sleeps.toString()); + + String[] output = new String[size+1]; + output[0] = Message.MESSAGE_SLEEP_LIST; + for (int i = 0; i < size; i++) { + assert sleeps.get(i) != null : "Sleep record cannot be null"; + output[i+1] = (i + 1) + ". " + sleeps.get(i).toString(); + } + return output; + } +} diff --git a/src/main/java/athleticli/commands/sleep/ListSleepGoalCommand.java b/src/main/java/athleticli/commands/sleep/ListSleepGoalCommand.java new file mode 100644 index 0000000000..11689f301f --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/ListSleepGoalCommand.java @@ -0,0 +1,41 @@ +package athleticli.commands.sleep; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.SleepGoalList; +import athleticli.ui.Message; + +import java.util.logging.Logger; + +/** + * Represents a command which lists all the sleep goals. + */ +public class ListSleepGoalCommand extends Command { + private static final Logger logger = Logger.getLogger(ListSleepGoalCommand.class.getName()); + + /** + * Constructor for ListSleepCommand. + */ + public ListSleepGoalCommand() { + } + + /** + * Lists the sleep goals. + * + * @param data The current data containing the sleep goal list. + * @return The message containing listing of sleep goals which will be shown to the user. + */ + @Override + public String[] execute(Data data) { + logger.info("Listing sleep goals"); + SleepGoalList sleepGoals = data.getSleepGoals(); + int size = sleepGoals.size(); + String[] output = new String[size + 1]; + output[0] = Message.MESSAGE_SLEEP_GOAL_LIST; + for (int i = 0; i < sleepGoals.size(); i++) { + output[i + 1] = (i + 1) + ". " + sleepGoals.get(i).toString(data); + } + logger.info("Found " + size + " sleep goals"); + return output; + } +} diff --git a/src/main/java/athleticli/commands/sleep/SetSleepGoalCommand.java b/src/main/java/athleticli/commands/sleep/SetSleepGoalCommand.java new file mode 100644 index 0000000000..867b0e1308 --- /dev/null +++ b/src/main/java/athleticli/commands/sleep/SetSleepGoalCommand.java @@ -0,0 +1,42 @@ +package athleticli.commands.sleep; + +import athleticli.commands.Command; +import athleticli.data.Data; +import athleticli.data.sleep.SleepGoal; +import athleticli.data.sleep.SleepGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +/** + * Represents a command which sets a sleep goal. + */ +public class SetSleepGoalCommand extends Command { + private final SleepGoal sleepGoal; + + /** + * Constructor for SetSleepGoalCommand. + * + * @param sleepGoal Sleep goal to be added. + */ + public SetSleepGoalCommand(SleepGoal sleepGoal) { + this.sleepGoal = sleepGoal; + } + + /** + * Updates the sleep goal list. + * + * @param data The current data containing the sleep goal list. + * @return The message which will be shown to the user. + */ + @Override + public String[] execute(Data data) throws AthletiException { + SleepGoalList sleepGoals = data.getSleepGoals(); + + if (sleepGoals.isDuplicate(sleepGoal.getGoalType(), sleepGoal.getTimeSpan())) { + throw new AthletiException(Message.ERRORMESSAGE_DUPLICATE_SLEEP_GOAL); + } + + sleepGoals.add(this.sleepGoal); + return new String[]{Message.MESSAGE_SLEEP_GOAL_ADDED, this.sleepGoal.toString(data)}; + } +} diff --git a/src/main/java/athleticli/common/Config.java b/src/main/java/athleticli/common/Config.java new file mode 100644 index 0000000000..9a43bfa87a --- /dev/null +++ b/src/main/java/athleticli/common/Config.java @@ -0,0 +1,27 @@ +package athleticli.common; + +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; + +import static java.util.Locale.ENGLISH; + +/** + * Defines string literals or configurations used for file storage. + */ +public class Config { + public static final DateTimeFormatter DATE_TIME_PRETTY_FORMATTER = + DateTimeFormatter.ofPattern("MMMM d, " + "yyyy 'at' h:mm a", ENGLISH); + public static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm").withResolverStyle(ResolverStyle.STRICT) + .withLocale(ENGLISH); + public static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT) + .withLocale(ENGLISH); + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss", ENGLISH); + public static final String PATH_ACTIVITY = "./data/activity.txt"; + public static final String PATH_ACTIVITY_GOAL = "./data/activity_goal.txt"; + public static final String PATH_SLEEP = "./data/sleep.txt"; + public static final String PATH_SLEEP_GOAL = "./data/sleep_goal.txt"; + public static final String PATH_DIET = "./data/diet.txt"; + public static final String PATH_DIET_GOAL = "./data/diet_goal.txt"; +} diff --git a/src/main/java/athleticli/data/Data.java b/src/main/java/athleticli/data/Data.java new file mode 100644 index 0000000000..35fb58ace2 --- /dev/null +++ b/src/main/java/athleticli/data/Data.java @@ -0,0 +1,128 @@ +package athleticli.data; + +import java.io.IOException; + +import athleticli.data.activity.ActivityGoalList; +import athleticli.data.activity.ActivityList; +import athleticli.data.diet.DietGoalList; +import athleticli.data.diet.DietList; +import athleticli.data.sleep.SleepGoalList; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; + +/** + * Defines the basic fields and methods of data. + */ +public class Data { + private static Data dataInstance; + private ActivityList activities = new ActivityList(); + private ActivityGoalList activityGoals = new ActivityGoalList(); + private DietList diets = new DietList(); + private DietGoalList dietGoals = new DietGoalList(); + private SleepList sleeps = new SleepList(); + private SleepGoalList sleepGoals = new SleepGoalList(); + + /** + * Returns the singleton instance of `Data`. + * + * @return The singleton instance of `Data`. + */ + public static Data getInstance() { + if (dataInstance == null) { + dataInstance = new Data(); + } + return dataInstance; + } + + /** + * Loads data from files. + */ + public void load() throws AthletiException { + activities.load(); + activityGoals.load(); + diets.load(); + dietGoals.load(); + sleeps.load(); + sleepGoals.load(); + } + + /** + * Saves data to files. + */ + public void save() throws IOException { + activities.save(); + activityGoals.save(); + diets.save(); + dietGoals.save(); + sleeps.save(); + sleepGoals.save(); + } + + /** + * Clears all lists. + */ + public void clear() { + activities.clear(); + activityGoals.clear(); + diets.clear(); + dietGoals.clear(); + sleeps.clear(); + sleepGoals.clear(); + } + + /** + * Get all the objects + */ + + public ActivityList getActivities() { + return activities; + } + + public ActivityGoalList getActivityGoals() { + return activityGoals; + } + + public DietList getDiets() { + return diets; + } + + public DietGoalList getDietGoals() { + return dietGoals; + } + + public SleepList getSleeps() { + return sleeps; + } + + public SleepGoalList getSleepGoals() { + return sleepGoals; + } + + + /** + * Set all the objects + */ + public void setActivities(ActivityList activities) { + this.activities = activities; + } + + public void setActivityGoals(ActivityGoalList activityGoals) { + this.activityGoals = activityGoals; + } + + public void setDiets(DietList diets) { + this.diets = diets; + } + + public void setDietGoals(DietGoalList dietGoals) { + this.dietGoals = dietGoals; + } + + public void setSleeps(SleepList sleeps) { + this.sleeps = sleeps; + } + + public void setSleepGoals(SleepGoalList sleepGoals) { + this.sleepGoals = sleepGoals; + } +} diff --git a/src/main/java/athleticli/data/Findable.java b/src/main/java/athleticli/data/Findable.java new file mode 100644 index 0000000000..ed0b96af94 --- /dev/null +++ b/src/main/java/athleticli/data/Findable.java @@ -0,0 +1,14 @@ +package athleticli.data; + +import java.time.LocalDate; +import java.util.ArrayList; + +public interface Findable { + /** + * Returns a list of objects matching the date. + * + * @param date The date to be matched. + * @return A list of objects matching the date. + */ + public ArrayList find(LocalDate date); +} diff --git a/src/main/java/athleticli/data/Goal.java b/src/main/java/athleticli/data/Goal.java new file mode 100644 index 0000000000..7a29b0af78 --- /dev/null +++ b/src/main/java/athleticli/data/Goal.java @@ -0,0 +1,70 @@ +package athleticli.data; + +import java.time.LocalDate; + +/** + * Defines the basic fields and methods for a goal. + */ +public abstract class Goal { + /** + * Defines different types of time spans. + */ + public enum TimeSpan { + DAILY(1), + WEEKLY(7), + MONTHLY(30), + YEARLY(365); + + private final int days; + + TimeSpan(int days) { + this.days = days; + } + + /** + * Returns the number of days in the time span. + * + * @return The number of days in the time span. + */ + public int getDays() { + return days; + } + } + + private TimeSpan timeSpan; + + public Goal(TimeSpan timeSpan) { + this.timeSpan = timeSpan; + } + + /** + * Returns the time span of this goal. + * + * @return The time span of this goal. + */ + public TimeSpan getTimeSpan() { + return timeSpan; + } + + /** + * Checks whether the date is between the time span. + * + * @param date The date to be matched. + * @param timeSpan The time span of the goal. + * @return Whether the date is between the time span. + */ + public static boolean checkDate(LocalDate date, TimeSpan timeSpan) { + final LocalDate endDate = LocalDate.now(); + final LocalDate startDate = endDate.minusDays(timeSpan.getDays() - 1); + return !(date.isBefore(startDate) || date.isAfter(endDate)); + } + + /** + * Returns whether the goal is achieved. + * + * @param data The current data containing all records. + * @return Whether the goal is achieved. + */ + public abstract boolean isAchieved(Data data); + +} diff --git a/src/main/java/athleticli/data/StorableList.java b/src/main/java/athleticli/data/StorableList.java new file mode 100644 index 0000000000..3de1e985eb --- /dev/null +++ b/src/main/java/athleticli/data/StorableList.java @@ -0,0 +1,61 @@ +package athleticli.data; + +import static athleticli.ui.Message.MESSAGE_LOAD_EXCEPTION; + +import java.io.IOException; +import java.util.ArrayList; + +import athleticli.exceptions.AthletiException; +import athleticli.exceptions.WrappedAthletiException; +import athleticli.storage.Storage; + +public abstract class StorableList extends ArrayList { + private String path; + + /** + * Constructs an empty list with its storage path. + */ + public StorableList(String path) { + this.path = path; + } + + /** + * Saves to a file. + */ + public void save() throws IOException { + Storage.save(path, this.stream().map(item -> unparse(item) + "\n")); + } + + /** + * Loads from a file. + */ + public void load() throws AthletiException { + try { + Storage.load(path).map(s -> { + try { + return parse(s); + } catch (AthletiException e) { + throw new WrappedAthletiException(e); + } + }).forEachOrdered(this::add); + } catch (IOException | WrappedAthletiException e) { + throw new AthletiException(String.format(MESSAGE_LOAD_EXCEPTION, path)); + } + } + + /** + * Parses a T object from a string. + * + * @param s The string to be parsed. + * @return The T object parsed from the string. + */ + public abstract T parse(String s) throws AthletiException; + + /** + * Unparses a T object to a string. + * + * @param t The T object to be parsed. + * @return The string unparsed from the T object. + */ + public abstract String unparse(T t); +} diff --git a/src/main/java/athleticli/data/activity/Activity.java b/src/main/java/athleticli/data/activity/Activity.java new file mode 100644 index 0000000000..9e4f45ba15 --- /dev/null +++ b/src/main/java/athleticli/data/activity/Activity.java @@ -0,0 +1,193 @@ +package athleticli.data.activity; + +import athleticli.parser.Parameter; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Locale; + +import static athleticli.common.Config.DATE_TIME_PRETTY_FORMATTER; +import static athleticli.common.Config.TIME_FORMATTER; +import static athleticli.parser.Parameter.ACTIVITY_INDICATOR; +import static athleticli.parser.Parameter.ACTIVITY_OVERVIEW_SEPARATOR; +import static athleticli.parser.Parameter.DISTANCE_PREFIX; +import static athleticli.parser.Parameter.DISTANCE_UNIT_KILOMETERS; +import static athleticli.parser.Parameter.DISTANCE_UNIT_METERS; +import static athleticli.parser.Parameter.KILOMETER_IN_METERS; +import static athleticli.parser.Parameter.SPACE; +import static athleticli.parser.Parameter.TIME_PREFIX; +import static athleticli.parser.Parameter.TIME_UNIT_HOURS; +import static athleticli.parser.Parameter.TIME_UNIT_MINUTES; +import static athleticli.parser.Parameter.TIME_UNIT_SECONDS; + +/** + * Represents a physical activity consisting of basic sports data. + */ +public class Activity { + public static final int COLUMN_WIDTH = 40; + private static final String DISTANCE_PRINT_FORMAT = "%.2f"; + private String caption; + private LocalTime movingTime; + private int distance; + private LocalDateTime startDateTime; + + /** + * Generates a new general sports activity with some basic stats. + * + * @param movingTime Duration of the activity in minutes. + * @param distance Distance covered in meters. + * @param startDateTime Start date and time of the activity. + * @param caption Caption of the activity chosen by the user (e.g., "Morning Run"). + */ + public Activity(String caption, LocalTime movingTime, int distance, LocalDateTime startDateTime) { + this.movingTime = movingTime; + this.distance = distance; + this.startDateTime = startDateTime; + this.caption = caption; + } + + public LocalTime getMovingTime() { + return movingTime; + } + + public int getDistance() { + return distance; + } + + public String getCaption() { + return caption; + } + + public LocalDateTime getStartDateTime() { + return startDateTime; + } + + /** + * Returns a single line summary of the activity. + * + * @return a string representation of the activity. + */ + @Override + public String toString() { + String movingTimeOutput = generateShortMovingTimeStringOutput(); + String distanceOutput = generateDistanceStringOutput(); + String startDateTimeOutput = generateStartDateTimeStringOutput(); + + String output = String.join(ACTIVITY_OVERVIEW_SEPARATOR, caption, distanceOutput, movingTimeOutput, + startDateTimeOutput); + return ACTIVITY_INDICATOR + SPACE + output; + } + + /** + * Returns distance in user-friendly output format. + * Assumes distance is given in meters. + * If the distance is less than 1 km, the distance is displayed in meters. + * + * @return a string representation of the distance. + */ + public String generateDistanceStringOutput() { + StringBuilder output = new StringBuilder(DISTANCE_PREFIX); + if (distance < KILOMETER_IN_METERS) { + output.append(distance); + output.append(DISTANCE_UNIT_METERS); + } else { + double distanceInKm = distance / 1000.0; + output.append(String.format(Locale.ENGLISH, DISTANCE_PRINT_FORMAT, distanceInKm)); + output.append(DISTANCE_UNIT_KILOMETERS); + } + return output.toString(); + } + + /** + * Returns moving time in user-friendly output format. + * + * @return a string representation of the moving time. + */ + public String generateMovingTimeStringOutput() { + return TIME_PREFIX + movingTime.format(TIME_FORMATTER); + } + + /** + * Returns a short representation of the moving time with the format depending on the duration. + * Format is "Xh Ym" if hours are present, otherwise "Ym Zs". + * + * @return a string representation of the moving time. + */ + public String generateShortMovingTimeStringOutput() { + StringBuilder output = new StringBuilder(TIME_PREFIX); + if (movingTime.getHour() > 0) { + output.append(movingTime.getHour()).append(TIME_UNIT_HOURS + SPACE); + output.append(movingTime.getMinute()).append(TIME_UNIT_MINUTES); + } else { + output.append(movingTime.getMinute()).append(TIME_UNIT_MINUTES + SPACE); + output.append(movingTime.getSecond()).append(TIME_UNIT_SECONDS); + } + return output.toString(); + } + + /** + * Returns start date and time in user-friendly output format. + * + * @return a string representation of the start date and time. + */ + public String generateStartDateTimeStringOutput() { + return startDateTime.format(DATE_TIME_PRETTY_FORMATTER); + } + + /** + * Returns a detailed summary of the activity. + * + * @return a multiline string representation of the activity. + */ + public String toDetailedString() { + String startDateTimeOutput = generateStartDateTimeStringOutput(); + String movingTimeOutput = generateMovingTimeStringOutput(); + String distanceOutput = generateDistanceStringOutput(); + + String header = "[Activity - " + getCaption() + " - " + startDateTimeOutput + "]"; + String firstRow = formatTwoColumns("\t" + distanceOutput, movingTimeOutput, COLUMN_WIDTH); + + return String.join(System.lineSeparator(), header, firstRow); + } + + /** + * Formats two strings into two columns of equal width. + * If a string is longer than the specified columnWidth, it will exceed the column. + * + * @param left String to be placed in the left column. + * @param right String to be placed in the right column. + * @param columnWidth Width of each column, should be a positive Integer. + * @return a formatted string with two columns of equal width. + */ + public String formatTwoColumns(String left, String right, int columnWidth) { + return String.format("%-" + columnWidth + "s%s", left, right); + } + + /** + * Returns a string representation of the activity used for storing the data. + * + * @return a string representation of the activity. + */ + public String unparse() { + return Parameter.ACTIVITY_STORAGE_INDICATOR + SPACE + getCaption() + + SPACE + Parameter.DURATION_SEPARATOR + getMovingTime().format(TIME_FORMATTER) + + SPACE + Parameter.DISTANCE_SEPARATOR + getDistance() + + SPACE + Parameter.DATETIME_SEPARATOR + getStartDateTime(); + } + + public void setCaption(String caption) { + this.caption = caption; + } + + public void setMovingTime(LocalTime movingTime) { + this.movingTime = movingTime; + } + + public void setDistance(int distance) { + this.distance = distance; + } + + public void setStartDateTime(LocalDateTime startDateTime) { + this.startDateTime = startDateTime; + } +} diff --git a/src/main/java/athleticli/data/activity/ActivityChanges.java b/src/main/java/athleticli/data/activity/ActivityChanges.java new file mode 100644 index 0000000000..319358e140 --- /dev/null +++ b/src/main/java/athleticli/data/activity/ActivityChanges.java @@ -0,0 +1,72 @@ +package athleticli.data.activity; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import athleticli.data.activity.Swim.SwimmingStyle; + +/** + * Represents an object that tracks changes to an activity. + */ +public class ActivityChanges { + private String caption; + private int distance; + private LocalTime duration; + private LocalDateTime startDateTime; + private int elevation; + private SwimmingStyle swimmingStyle; + + /** + * Constructs an empty ActivityChanges object. + */ + public ActivityChanges() { + + } + + public String getCaption() { + return caption; + } + + public void setCaption(String caption) { + this.caption = caption; + } + + public int getDistance() { + return distance; + } + + public void setDistance(int distance) { + this.distance = distance; + } + + public LocalTime getDuration() { + return duration; + } + + public void setDuration(LocalTime duration) { + this.duration = duration; + } + + public LocalDateTime getStartDateTime() { + return startDateTime; + } + + public void setStartDateTime(LocalDateTime startDateTime) { + this.startDateTime = startDateTime; + } + + public int getElevation() { + return elevation; + } + + public void setElevation(int elevation) { + this.elevation = elevation; + } + + public SwimmingStyle getSwimmingStyle() { + return swimmingStyle; + } + + public void setSwimmingStyle(SwimmingStyle swimmingStyle) { + this.swimmingStyle = swimmingStyle; + } +} diff --git a/src/main/java/athleticli/data/activity/ActivityGoal.java b/src/main/java/athleticli/data/activity/ActivityGoal.java new file mode 100644 index 0000000000..87474c69e6 --- /dev/null +++ b/src/main/java/athleticli/data/activity/ActivityGoal.java @@ -0,0 +1,131 @@ +package athleticli.data.activity; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.parser.Parameter; + +/** + * Represents an activity goal. + */ +public class ActivityGoal extends Goal { + public enum GoalType { + DISTANCE, DURATION + } + public enum Sport { + RUNNING, CYCLING, SWIMMING, GENERAL + } + + private int targetValue; + private final GoalType goalType; + private final Sport sport; + + /** + * Constructs an activity goal. + * + * @param timeSpan Time span of the activity goal. + * @param goalType Goal type of the activity goal. + * @param sport Sport related to the activity goal. + * @param targetValue Target value of the activity goal. + */ + public ActivityGoal(TimeSpan timeSpan, GoalType goalType, Sport sport, int targetValue) { + super(timeSpan); + this.targetValue = targetValue; + this.goalType = goalType; + this.sport = sport; + } + + /** + * Examines whether the activity goal is achieved. + * + * @param data Data containing the activity list. + * @return Whether the activity goal is achieved. + */ + @Override + public boolean isAchieved(Data data) throws IllegalStateException { + return getCurrentValue(data) >= targetValue; + } + + /** + * Returns the current value of the activity goal metric. + * + * @param data Data containing the activity list. + * @return Current value of the activity goal metric. + * @throws IllegalStateException If the goal type is not supported. + */ + public int getCurrentValue(Data data) { + ActivityList activities = data.getActivities(); + Class activityClass = getActivityClass(); + + switch (goalType) { + case DISTANCE: + return activities.getTotalDistance(activityClass, this.getTimeSpan()); + case DURATION: + int totalDuration = activities.getTotalDuration(activityClass, this.getTimeSpan()); + return convertToMinutes(totalDuration); + default: + throw new IllegalStateException("Unexpected value: " + goalType); + } + } + + /** + * Converts the given seconds to minutes. + * + * @param seconds Seconds to be converted. + * @return Minutes converted from the given seconds. + */ + private int convertToMinutes(int seconds) { + return seconds / Parameter.MINUTE_IN_SECONDS; + } + + public void setTargetValue(int targetValue) { + this.targetValue = targetValue; + } + + /** + * Returns the class of the activity associated with the activity goal. + * + * @return The class of the activity. + */ + public Class getActivityClass() { + switch (this.sport) { + case RUNNING: + return Run.class; + case CYCLING: + return Cycle.class; + case SWIMMING: + return Swim.class; + case GENERAL: + return Activity.class; + default: + throw new IllegalStateException("Unexpected value: " + this.sport); + } + } + + /** + * Returns the string representation of the activity goal including progress information. + * + * @param data Data containing the activity list. + * @return The string representation of the activity goal. + */ + public String toString(Data data) { + String goalTypeString = goalType.name().toLowerCase(); + String sportString = sport.name().toLowerCase(); + String timeSpanString = getTimeSpan().name().toLowerCase(); + + return String.format("%s %s %s: %d / %d", timeSpanString, sportString, goalTypeString, + getCurrentValue(data), targetValue); + } + + public GoalType getGoalType() { + return goalType; + } + + public Sport getSport() { + return sport; + } + + public int getTargetValue() { + return targetValue; + } + +} diff --git a/src/main/java/athleticli/data/activity/ActivityGoalList.java b/src/main/java/athleticli/data/activity/ActivityGoalList.java new file mode 100644 index 0000000000..e5e813b9a5 --- /dev/null +++ b/src/main/java/athleticli/data/activity/ActivityGoalList.java @@ -0,0 +1,61 @@ +package athleticli.data.activity; + +import athleticli.data.Goal; +import athleticli.data.StorableList; +import athleticli.exceptions.AthletiException; +import athleticli.parser.ActivityParser; +import athleticli.parser.Parameter; + +import static athleticli.common.Config.PATH_ACTIVITY_GOAL; + +/** + * Represents a list of activity goals. + */ +public class ActivityGoalList extends StorableList { + /** + * Constructs an activity goal list. + */ + public ActivityGoalList() { + super(PATH_ACTIVITY_GOAL); + } + + /** + * Parses an activity goal from a string. + * + * @param arguments The string to be parsed. + * @return The activity goal parsed from the string. + */ + @Override + public ActivityGoal parse(String arguments) throws AthletiException { + return ActivityParser.parseActivityGoal(arguments); + } + + /** + * Unparses an activity goal to a string. + * Example output: "sport/running type/distance period/weekly target/8000". + * + * @param activityGoal Activity goal to be parsed. + * @return The string unparsed from the activity goal. + */ + @Override + public String unparse(ActivityGoal activityGoal) { + return Parameter.SPORT_SEPARATOR + activityGoal.getSport() + + Parameter.SPACE + Parameter.TYPE_SEPARATOR + activityGoal.getGoalType() + + Parameter.SPACE + Parameter.PERIOD_SEPARATOR + activityGoal.getTimeSpan() + + Parameter.SPACE + Parameter.TARGET_SEPARATOR + activityGoal.getTargetValue(); + } + + /** + * Checks if there is a duplicate activity goal with the same goal type, sport and timespan. + * + * @param goalType Goal type of the activity goal. + * @param sport Sport associated with the activity goal. + * @param timeSpan Time span of the activity goal. + * @return Whether the activity goal is a duplicate. + */ + public boolean isDuplicate(ActivityGoal.GoalType goalType, ActivityGoal.Sport sport, Goal.TimeSpan timeSpan) { + return this.stream().anyMatch(activityGoal -> activityGoal.getGoalType() == goalType + && activityGoal.getSport() == sport + && activityGoal.getTimeSpan() == timeSpan); + } +} diff --git a/src/main/java/athleticli/data/activity/ActivityList.java b/src/main/java/athleticli/data/activity/ActivityList.java new file mode 100644 index 0000000000..8a22f99bbb --- /dev/null +++ b/src/main/java/athleticli/data/activity/ActivityList.java @@ -0,0 +1,147 @@ +package athleticli.data.activity; + +import static athleticli.common.Config.PATH_ACTIVITY; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; + +import athleticli.data.Findable; +import athleticli.data.StorableList; +import athleticli.data.Goal; +import athleticli.exceptions.AthletiException; +import athleticli.parser.ActivityParser; +import athleticli.parser.Parameter; +import athleticli.ui.Message; + +/** + * Represents a list of activities. + */ +public class ActivityList extends StorableList implements Findable { + /** + * Constructs an empty activity list. + */ + public ActivityList() { + super(PATH_ACTIVITY); + } + + /** + * Returns a list of activities matching the date. + * + * @param date The date to be matched. + * @return A list of activities matching the date. + */ + @Override + public ArrayList find(LocalDate date) { + ArrayList result = new ArrayList<>(); + for (Activity activity : this) { + if (activity.getStartDateTime().toLocalDate().equals(date)) { + result.add(activity); + } + } + return result; + } + + /** + * Sorts the activities in the list by date. + */ + public void sort() { + this.sort(Comparator.comparing(Activity::getStartDateTime).reversed()); + } + + /** + * Returns a list of activities within the time span. + * + * @param timeSpan Time span to be matched. + * @return A list of activities within the time span. + */ + public ArrayList filterByTimespan(Goal.TimeSpan timeSpan) { + ArrayList result = new ArrayList<>(); + for (Activity activity : this) { + LocalDate activityDate = activity.getStartDateTime().toLocalDate(); + if (Goal.checkDate(activityDate, timeSpan)) { + result.add(activity); + } + } + return result; + } + + /** + * Returns the total distance of all activities in the list matching the specified activity class. + * + * @param activityClass The activity class to be matched. + * @param timeSpan Timespan to be matched. + * @return The total distance of all activities in the list matching the specified activity class and timespan. + */ + public int getTotalDistance(Class activityClass, Goal.TimeSpan timeSpan) { + ArrayList filteredActivities = filterByTimespan(timeSpan); + int totalDistance = 0; + for (Activity activity : filteredActivities) { + if (activityClass.isInstance(activity)) { + totalDistance += activity.getDistance(); + } + } + return totalDistance; + } + + /** + * Returns the total moving time in seconds of all activities in the list matching the specified activity class. + * + * @param activityClass Activity class to be matched. + * @param timeSpan Timespan to be matched. + * @return The total moving time of all activities in the list matching the specified activity class. + */ + public int getTotalDuration(Class activityClass, Goal.TimeSpan timeSpan) { + ArrayList filteredActivities = filterByTimespan(timeSpan); + int movingTime = 0; + for (Activity activity : filteredActivities) { + if (activityClass.isInstance(activity)) { + movingTime += activity.getMovingTime().toSecondOfDay(); + } + } + return movingTime; + } + + /** + * Parses an activity from a string. + * + * @param s The string to be parsed. + * @return The activity parsed from the string. + * @throws AthletiException If the string is invalid or an unknown indicator is found. + */ + @Override + public Activity parse(String s) throws AthletiException { + String[] parts = s.split(" ", 2); + + try { + String indicator = parts[0]; + String arguments = parts[1]; + + switch (indicator) { + case Parameter.ACTIVITY_STORAGE_INDICATOR: + return ActivityParser.parseActivity(arguments); + case Parameter.RUN_STORAGE_INDICATOR: + return ActivityParser.parseRunCycle(arguments, true); + case Parameter.CYCLE_STORAGE_INDICATOR: + return ActivityParser.parseRunCycle(arguments, false); + case Parameter.SWIM_STORAGE_INDICATOR: + return ActivityParser.parseSwim(arguments); + default: + throw new AthletiException(Message.ACTIVITY_STORAGE_INVALID_INDICATOR); + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new AthletiException(Message.ACTIVITY_STORAGE_INVALID_FORMAT); + } + } + + /** + * Unparses an activity to a string. + * + * @param activity The activity to be parsed. + * @return The string unparsed from the activity. + */ + @Override + public String unparse(Activity activity) { + return activity.unparse(); + } +} diff --git a/src/main/java/athleticli/data/activity/Cycle.java b/src/main/java/athleticli/data/activity/Cycle.java new file mode 100644 index 0000000000..d31dfea2ad --- /dev/null +++ b/src/main/java/athleticli/data/activity/Cycle.java @@ -0,0 +1,152 @@ +package athleticli.data.activity; + +import athleticli.parser.Parameter; + +import java.time.LocalDateTime; +import java.util.Locale; +import java.time.LocalTime; + +/** + * Represents a cycling activity consisting of relevant evaluation data. + */ +public class Cycle extends Activity { + private static final String SPEED_PRINT_FORMAT = "%.2f"; + private int elevationGain; + private double averageSpeed; + + /** + * Generates a new cycling activity with cycling specific stats. + * averageSpeed is calculated automatically based on the distance and movingTime. + * + * @param movingTime duration of the activity in minutes. + * @param distance distance covered in meters. + * @param startDateTime start date and time of the activity. + * @param caption a caption of the activity chosen by the user (e.g., "Morning Run"). + * @param elevationGain elevation gain in meters. + */ + public Cycle(String caption, LocalTime movingTime, int distance, LocalDateTime startDateTime, int elevationGain) { + super(caption, movingTime, distance, startDateTime); + this.elevationGain = elevationGain; + this.averageSpeed = this.calculateAverageSpeed(); + } + + /** + * Calculates the average speed of the cycle in km/h. + * + * @return average speed of the cycle in km/h. Return 0 if the movingTime is zero. + */ + public double calculateAverageSpeed() { + double distanceInKm = this.getDistance() / (double) Parameter.KILOMETER_IN_METERS; + double timeInHours = this.getMovingTime().toSecondOfDay() / (double) Parameter.HOUR_IN_SECONDS; + + if (timeInHours == 0) { + return 0; + } + + return distanceInKm / timeInHours; + } + + /** + * Returns a single line summary of the cycling activity. + * + * @return a string representation of the cycle. + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(super.toString()); + result.replace(0, Parameter.ACTIVITY_INDICATOR.length(), Parameter.CYCLE_INDICATOR); + + String speedOutput = generateSpeedStringOutput(); + int timePrefixIndex = result.indexOf(Parameter.TIME_PREFIX); + if (timePrefixIndex != -1) { + result.insert(timePrefixIndex, + Parameter.SPEED_PREFIX + speedOutput + Parameter.ACTIVITY_OVERVIEW_SEPARATOR); + } else { + throw new AssertionError("Time prefix not found in cycle string representation"); + } + + return result.toString(); + } + + /** + * Returns a string representation of the average speed of the cycle. + * + * @return a string representation of the average speed of the cycle. + */ + public String generateSpeedStringOutput() { + return String.format(Locale.ENGLISH, SPEED_PRINT_FORMAT, this.averageSpeed) + + Parameter.SPEED_UNIT_KILOMETERS_PER_HOUR; + } + + /** + * Returns a string representation of the elevation gain of the cycle. + * + * @return a string representation of the elevation gain of the cycle. + */ + public String generateElevationGainStringOutput() { + return Parameter.ELEVATION_PREFIX + elevationGain + Parameter.DISTANCE_UNIT_METERS; + } + + /** + * Returns a detailed summary of the cycle. + * + * @return a multiline string representation of the cycle. + */ + public String toDetailedString() { + String startDateTimeOutput = generateStartDateTimeStringOutput(); + String movingTimeOutput = generateMovingTimeStringOutput(); + String distanceOutput = generateDistanceStringOutput(); + String speedOutput = generateSpeedStringOutput(); + String elevationOutput = generateElevationGainStringOutput(); + + String header = "[Cycle - " + getCaption() + " - " + startDateTimeOutput + "]"; + String firstRow = formatTwoColumns("\t" + distanceOutput, elevationOutput, COLUMN_WIDTH); + String secondRow = formatTwoColumns("\t" + movingTimeOutput, Parameter.AVG_SPEED_PREFIX + speedOutput, + COLUMN_WIDTH); + + return String.join(System.lineSeparator(), header, firstRow, secondRow); + } + + /** + * Returns a string representation of the cycle used for storing the data. + * + * @return a string representation of the cycle. + */ + @Override + public String unparse() { + StringBuilder commandArgs = new StringBuilder(super.unparse()); + commandArgs.replace(0, Parameter.ACTIVITY_STORAGE_INDICATOR.length(), Parameter.CYCLE_STORAGE_INDICATOR); + commandArgs.append(Parameter.SPACE).append(Parameter.ELEVATION_SEPARATOR).append(elevationGain); + return commandArgs.toString(); + } + + public int getElevationGain() { + return this.elevationGain; + } + + public void setElevationGain(int elevationGain) { + this.elevationGain = elevationGain; + } + + /** + * Sets the distance of the cycle and recalculates the average speed. + * + * @param distance Distance in meters. + */ + @Override + public void setDistance(int distance) { + super.setDistance(distance); + this.averageSpeed = this.calculateAverageSpeed(); + } + + /** + * Sets the moving time of the cycle and recalculates the average speed. + * + * @param movingTime Moving time in LocalTime format. + */ + @Override + public void setMovingTime(LocalTime movingTime) { + super.setMovingTime(movingTime); + this.averageSpeed = this.calculateAverageSpeed(); + } +} diff --git a/src/main/java/athleticli/data/activity/Run.java b/src/main/java/athleticli/data/activity/Run.java new file mode 100644 index 0000000000..aa94a9a922 --- /dev/null +++ b/src/main/java/athleticli/data/activity/Run.java @@ -0,0 +1,144 @@ +package athleticli.data.activity; + +import athleticli.parser.Parameter; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * Represents a running activity consisting of relevant evaluation data. + */ +public class Run extends Activity { + private static final String PACE_PRINT_FORMAT = "%d:%02d"; + private int elevationGain; + private double averagePace; + + /** + * Generates a new running activity with running specific stats. + * averageSpeed is calculated automatically based on the distance and movingTime. + * + * @param movingTime duration of the activity in minutes. + * @param distance distance covered in meters. + * @param startDateTime start date and time of the activity. + * @param caption a caption of the activity chosen by the user (e.g., "Morning Run"). + * @param elevationGain elevation gain in meters. + */ + public Run(String caption, LocalTime movingTime, int distance, LocalDateTime startDateTime, int elevationGain) { + super(caption, movingTime, distance, startDateTime); + this.elevationGain = elevationGain; + this.averagePace = this.calculateAveragePace(); + } + + /** + * Calculates the average pace of the run in minutes per km. + * + * @return average pace of the run in minutes per km. Return 0 if the distance is zero. + */ + public double calculateAveragePace() { + double time = getMovingTime().toSecondOfDay() / (double) Parameter.MINUTE_IN_SECONDS; + double distance = getDistance() / (double) Parameter.KILOMETER_IN_METERS; + + if (distance == 0) { + return 0; + } + + return time / distance; + } + + /** + * Converts the average pace of the run to the user-friendly format mm:ss. + * + * @return average pace of run in mm:ss format. + */ + public String convertAveragePaceToString() { + int totalSeconds = (int) Math.round(averagePace * Parameter.MINUTE_IN_SECONDS); + int minutes = totalSeconds / Parameter.MINUTE_IN_SECONDS; + int seconds = totalSeconds % Parameter.MINUTE_IN_SECONDS; + return String.format(PACE_PRINT_FORMAT, minutes, seconds); + } + + /** + * Returns a single line summary of the running activity. + * + * @return a string representation of the run. + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(super.toString()); + result.replace(0, Parameter.ACTIVITY_INDICATOR.length(), Parameter.RUN_INDICATOR); + + String paceOutput = convertAveragePaceToString() + Parameter.PACE_UNIT_TIME_PER_KILOMETER; + int timePrefixIndex = result.indexOf(Parameter.TIME_PREFIX); + if (timePrefixIndex != -1) { + result.insert(timePrefixIndex, + Parameter.PACE_PREFIX + paceOutput + Parameter.ACTIVITY_OVERVIEW_SEPARATOR); + } else { + throw new AssertionError("Time prefix not found in run string representation"); + } + + return result.toString(); + } + + /** + * Returns a string representation of the run used for storing the data. + * + * @return a string representation of the run. + */ + @Override + public String unparse() { + StringBuilder commandArgs = new StringBuilder(super.unparse()); + commandArgs.replace(0, Parameter.ACTIVITY_STORAGE_INDICATOR.length(), Parameter.RUN_STORAGE_INDICATOR); + commandArgs.append(Parameter.SPACE).append(Parameter.ELEVATION_SEPARATOR).append(elevationGain); + return commandArgs.toString(); + } + + /** + * Returns a detailed summary of the run. + * + * @return a multiline string representation of the run. + */ + public String toDetailedString() { + String startDateTimeOutput = generateStartDateTimeStringOutput(); + String movingTimeOutput = generateMovingTimeStringOutput(); + String distanceOutput = generateDistanceStringOutput(); + String paceOutput = convertAveragePaceToString() + Parameter.PACE_UNIT_TIME_PER_KILOMETER; + + String header = "[Run - " + getCaption() + " - " + startDateTimeOutput + "]"; + String firstRow = formatTwoColumns("\t" + distanceOutput, "Avg Pace: " + paceOutput, + COLUMN_WIDTH); + String secondRow = formatTwoColumns("\t" + movingTimeOutput, "Elevation Gain: " + + elevationGain + Parameter.DISTANCE_UNIT_METERS, COLUMN_WIDTH); + + return String.join(System.lineSeparator(), header, firstRow, secondRow); + } + + public int getElevationGain() { + return elevationGain; + } + + public void setElevationGain(int elevationGain) { + this.elevationGain = elevationGain; + } + + /** + * Sets the distance of the run and recalculates the average pace. + * + * @param distance Distance in meters. + */ + @Override + public void setDistance(int distance) { + super.setDistance(distance); + this.averagePace = this.calculateAveragePace(); + } + + /** + * Sets the moving time of the run and recalculates the average pace. + * + * @param movingTime Moving time. + */ + @Override + public void setMovingTime(LocalTime movingTime) { + super.setMovingTime(movingTime); + this.averagePace = this.calculateAveragePace(); + } +} diff --git a/src/main/java/athleticli/data/activity/Swim.java b/src/main/java/athleticli/data/activity/Swim.java new file mode 100644 index 0000000000..da6f918569 --- /dev/null +++ b/src/main/java/athleticli/data/activity/Swim.java @@ -0,0 +1,178 @@ +package athleticli.data.activity; + +import athleticli.parser.Parameter; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * Represents a swimming activity consisting of relevant evaluation data. + */ +public class Swim extends Activity { + private static final int METERS_PER_LAP = 50; + private int laps; + private SwimmingStyle style; + private int averageLapTime; + + public enum SwimmingStyle { + BUTTERFLY, + BACKSTROKE, + BREASTSTROKE, + FREESTYLE + } + + /** + * Generates a new swimming activity with swimming specific stats. + * By default, calories is 0, i.e., not tracked. + * averageLapTime is calculated automatically based on the movingTime and laps. + * + * @param movingTime duration of the activity in HH:mm:ss format. + * @param distance distance covered in meters. + * @param startDateTime start date and time of the activity. + * @param caption a caption of the activity chosen by the user (e.g., "Morning Run"). + * @param style swimming style. + */ + public Swim(String caption, LocalTime movingTime, int distance, LocalDateTime startDateTime, SwimmingStyle style) { + super(caption, movingTime, distance, startDateTime); + this.laps = this.calculateLaps(); + this.style = style; + this.averageLapTime = this.calculateAverageLapTime(); + } + + /** + * Calculates the average lap time in seconds. + * + * @return average lap time in seconds. Return 0 if the movingTime is zero. + */ + public int calculateAverageLapTime() { + if (laps == 0) { + return 0; + } + + return this.getMovingTime().toSecondOfDay() / laps; + } + + /** + * Calculates the number of laps. + * + * @return number of laps. + */ + public int calculateLaps() { + return this.getDistance() / METERS_PER_LAP; + } + + /** + * Returns a short string representation of the swim. + * + * @return a string representation of the swim. + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(super.toString()); + result.replace(0, Parameter.ACTIVITY_INDICATOR.length(), Parameter.SWIM_INDICATOR); + + String averageLapTimeOutput = averageLapTime + Parameter.TIME_UNIT_SECONDS; + int timePrefixIndex = result.indexOf(Parameter.TIME_PREFIX); + if (timePrefixIndex != -1) { + result.insert(timePrefixIndex, + Parameter.LAP_TIME_PREFIX + averageLapTimeOutput + Parameter.ACTIVITY_OVERVIEW_SEPARATOR); + } else { + throw new AssertionError("Time prefix not found in swim string representation"); + } + + return result.toString(); + } + + /** + * Returns a detailed summary of the swim. + * + * @return a multiline string representation of the swim. + */ + public String toDetailedString() { + String startDateTimeOutput = generateStartDateTimeStringOutput(); + String movingTimeOutput = generateMovingTimeStringOutput(); + String distanceOutput = generateDistanceStringOutput(); + String lapsOutput = generateLapsStringOutput(); + String styleOutput = generateStyleStringOutput(); + String averageLapTimeOutput = generateAverageLapTimeStringOutput(); + + String header = "[Swim - " + getCaption() + " - " + startDateTimeOutput + "]"; + String firstRow = formatTwoColumns("\t" + distanceOutput, movingTimeOutput, COLUMN_WIDTH); + String secondRow = formatTwoColumns("\t" + lapsOutput, styleOutput, COLUMN_WIDTH); + String thirdRow = formatTwoColumns("\t" + Parameter.AVG_LAP_TIME_PREFIX + averageLapTimeOutput, "", + COLUMN_WIDTH); + + return String.join(System.lineSeparator(), header, firstRow, secondRow, thirdRow); + } + + /** + * Returns a string representation of the average lap time of a swim. + * + * @return a string representation of the average lap time. + */ + public String generateAverageLapTimeStringOutput() { + return averageLapTime + Parameter.TIME_UNIT_SECONDS; + } + + /** + * Returns a string representation of the number of laps of a swim. + * + * @return a string representation of the number of laps of a swim. + */ + public String generateLapsStringOutput() { + return Parameter.LAPS_PREFIX + laps; + } + + /** + * Returns a string representation of the swimming style. + * + * @return a string representation of the swimming style. + */ + public String generateStyleStringOutput() { + return Parameter.STYLE_PREFIX + getStyle(); + } + + /** + * Returns a string representation of the swim used for storing the data. + * @return a string representation of the swim. + */ + @Override + public String unparse() { + StringBuilder commandArgs = new StringBuilder(super.unparse()); + commandArgs.replace(0, Parameter.ACTIVITY_STORAGE_INDICATOR.length(), Parameter.SWIM_STORAGE_INDICATOR); + commandArgs.append(Parameter.SPACE).append((Parameter.SWIMMING_STYLE_SEPARATOR)).append(style); + return commandArgs.toString(); + } + + public SwimmingStyle getStyle() { + return style; + } + + public void setStyle(SwimmingStyle style) { + this.style = style; + } + + /** + * Sets the distance of the swim and recalculates the total laps and average lap time. + * + * @param distance Distance in meters. + */ + @Override + public void setDistance(int distance) { + super.setDistance(distance); + laps = calculateLaps(); + this.averageLapTime = this.calculateAverageLapTime(); + } + + /** + * Sets the moving time of the swim and recalculates the average lap time. + * + * @param movingTime Moving time in LocalTime format. + */ + @Override + public void setMovingTime(LocalTime movingTime) { + super.setMovingTime(movingTime); + this.averageLapTime = this.calculateAverageLapTime(); + } + +} diff --git a/src/main/java/athleticli/data/diet/Diet.java b/src/main/java/athleticli/data/diet/Diet.java new file mode 100644 index 0000000000..bc78df3c06 --- /dev/null +++ b/src/main/java/athleticli/data/diet/Diet.java @@ -0,0 +1,143 @@ +package athleticli.data.diet; + +import java.time.LocalDateTime; + +import static athleticli.common.Config.DATE_TIME_PRETTY_FORMATTER; + +/** + * Defines the basic fields and methods of a diet. + */ +public class Diet { + private int calories; + private int protein; + private int carb; + private int fat; + private LocalDateTime dateTime; + + /** + * Constructs a Diet object. + * + * @param calories The caloric value of the diet in cal. + * @param protein Protein intake in grams. + * @param carb Carbohydrate intake in grams. + * @param fat Fat intake in grams. + * @param dateTime The date and time of the diet. + */ + public Diet(int calories, int protein, int carb, int fat, LocalDateTime dateTime) { + assert calories >= 0 : "Calories cannot be negative"; + this.calories = calories; + assert protein >= 0 : "Protein cannot be negative"; + this.protein = protein; + assert carb >= 0 : "Carb cannot be negative"; + this.carb = carb; + assert fat >= 0 : "Fat cannot be negative"; + this.fat = fat; + this.dateTime = dateTime; + } + + /** + * Returns the caloric value of the diet in cal. + * + * @return The caloric value of the diet in cal. + */ + public int getCalories() { + return calories; + } + + /** + * Sets the caloric value of the diet in cal. + * + * @param calories The caloric value of the diet in cal. + */ + public void setCalories(int calories) { + assert calories >= 0 : "Calories cannot be negative"; + this.calories = calories; + } + + /** + * Returns the protein intake in grams. + * + * @return Protein intake in grams. + */ + public int getProtein() { + return protein; + } + + /** + * Sets the protein intake in grams. + * + * @param protein Protein intake in grams. + */ + public void setProtein(int protein) { + assert protein >= 0 : "Protein cannot be negative"; + this.protein = protein; + } + + /** + * Returns the carbohydrate intake in grams. + * + * @return Carbohydrate intake in grams. + */ + public int getCarb() { + return carb; + } + + /** + * Sets the carbohydrate intake in grams. + * + * @param carb Carbohydrate intake in grams. + */ + public void setCarb(int carb) { + assert carb >= 0 : "Carb cannot be negative"; + this.carb = carb; + } + + /** + * Returns the fat intake in grams. + * + * @return Fat intake in grams. + */ + public int getFat() { + return fat; + } + + /** + * Sets the fat intake in grams. + * + * @param fat Fat intake in grams. + */ + public void setFat(int fat) { + assert fat >= 0 : "Fat cannot be negative"; + this.fat = fat; + } + + /** + * Returns the date and time of the diet. + * + * @return The date and time of the diet. + */ + public LocalDateTime getDateTime() { + return dateTime; + } + + /** + * Sets the date and time of the diet. + * + * @param dateTime The date and time of the diet. + */ + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + /** + * Returns a string representation of the diet. + * + * @return A string representation of the diet. + */ + + @Override + public String toString() { + return "Calories: " + calories + " cal | Protein: " + protein + " mg | Carb: " + carb + " mg | Fat:" + + " " + fat + " mg | " + dateTime.format(DATE_TIME_PRETTY_FORMATTER); + } +} diff --git a/src/main/java/athleticli/data/diet/DietGoal.java b/src/main/java/athleticli/data/diet/DietGoal.java new file mode 100644 index 0000000000..a884b23add --- /dev/null +++ b/src/main/java/athleticli/data/diet/DietGoal.java @@ -0,0 +1,206 @@ +package athleticli.data.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; + +import java.time.LocalDate; +import java.util.ArrayList; + +import athleticli.parser.Parameter; + +import static java.lang.Math.min; + +/** + * Represents a diet goal. + */ +public abstract class DietGoal extends Goal { + protected String nutrient; + protected int targetValue; + protected final String type; + protected final String achievedSymbol; + protected final String unachievedSymbol; + private final String dietGoalStringRepresentation; + private final int currentValueLimit; + + /** + * Constructs a diet goal with no current value. + * + * @param timespan The timespan of the diet goal. + * @param nutrient The nutrients of the diet goal. + * @param targetValue The target value of the diet goal. + */ + public DietGoal(TimeSpan timespan, String nutrient, int targetValue) { + super(timespan); + this.nutrient = nutrient; + this.targetValue = targetValue; + type = ""; + achievedSymbol = "[Achieved]"; + unachievedSymbol = ""; + dietGoalStringRepresentation = "%s %s %s intake progress: (%d/%d)\n"; + currentValueLimit = 1000000; + } + + /** + * Returns the nutrients of the diet goal. + * + * @return The nutrients of the diet goal. + */ + public String getNutrient() { + return nutrient; + } + + /** + * Sets the nutrients of the diet goal. + * + * @param nutrient The nutrient of the diet goal. + */ + public void setNutrient(String nutrient) { + this.nutrient = nutrient; + } + + /** + * Returns the target value of the diet goal. + * + * @return The target value of the diet goal. + */ + public int getTargetValue() { + return targetValue; + } + + /** + * Sets the target value of the diet goal. + * + * @param targetValue The target value of the diet goal. + */ + public void setTargetValue(int targetValue) { + this.targetValue = targetValue; + } + + /** + * Returns the current value of the diet goal from dietList. + * + * @param data A storage class to retrieve diet information. + * @return The current value of the diet goal. + */ + public int getCurrentValue(Data data) { + return updateCurrentValue(data); + } + + /** + * Returns the type of diet goal. + * + * @return the type of diet goal. + */ + public String getType() { + return type; + } + + private int updateCurrentValue(Data data) { + int currentValue = 0; + DietList diets = data.getDiets(); + int numDays = getTimeSpan().getDays(); + ArrayList dates = getPastDates(numDays); + ArrayList dietRecords; + for (LocalDate date : dates) { + dietRecords = diets.find(date); + for (Diet diet : dietRecords) { + switch (nutrient) { + case Parameter.NUTRIENTS_FAT: + currentValue += diet.getFat(); + break; + case Parameter.NUTRIENTS_CALORIES: + currentValue += diet.getCalories(); + break; + case Parameter.NUTRIENTS_PROTEIN: + currentValue += diet.getProtein(); + break; + case Parameter.NUTRIENTS_CARB: + currentValue += diet.getCarb(); + break; + default: + currentValue += 0; + + } + } + } + return min(currentValue, currentValueLimit); + } + + private ArrayList getPastDates(int numDays) { + ArrayList pastDates = new ArrayList<>(); + LocalDate currentDate = LocalDate.now(); + + for (int i = 0; i < numDays; i++) { + LocalDate pastDate = currentDate.minusDays(i); + pastDates.add(pastDate); + } + return pastDates; + } + + /** + * Returns whether the diet goal is achieved. + * + * @param data A storage class to retrieve diet information. + * @return Whether the diet goal is achieved. + */ + public boolean isAchieved(Data data) { + int currentValue = getCurrentValue(data); + return currentValue >= targetValue; + } + + /** + * Returns the symbol to indicate if a diet goal is achieved. + * + * @param data A storage class to retrieve diet information. + * @return A string symbol indicating that the goal is achieved. + */ + protected String getSymbol(Data data) { + if (isAchieved(data)) { + return achievedSymbol; + } + return unachievedSymbol; + } + + /** + * Checks if the other diet goals are of the same type. + * + * @param dietGoal + * @return + */ + public boolean isSameType(DietGoal dietGoal) { + return dietGoal.getType().equals(getType()); + } + + /** + * Checks if the other diet goals are of the same nutrient. + * + * @param dietGoal + * @return + */ + public boolean isSameNutrient(DietGoal dietGoal) { + return dietGoal.getNutrient().equals(getNutrient()); + } + + /** + * Checks if the other diet goals are of the same time span. + * + * @param dietGoal + * @return + */ + public boolean isSameTimeSpan(DietGoal dietGoal) { + return dietGoal.getTimeSpan().getDays() == getTimeSpan().getDays(); + } + + /** + * Returns the string representation of the diet goal. + * + * @param data A storage class to retrieve diet information. + * @return The string representation of the diet goal. + */ + public String toString(Data data) { + return String.format(dietGoalStringRepresentation, getSymbol(data), getTimeSpan().name(), nutrient, + getCurrentValue(data), targetValue); + + + } +} diff --git a/src/main/java/athleticli/data/diet/DietGoalList.java b/src/main/java/athleticli/data/diet/DietGoalList.java new file mode 100644 index 0000000000..9e13b67221 --- /dev/null +++ b/src/main/java/athleticli/data/diet/DietGoalList.java @@ -0,0 +1,196 @@ +package athleticli.data.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.data.StorableList; +import athleticli.exceptions.AthletiException; +import athleticli.parser.NutrientVerifier; +import athleticli.parser.Parameter; +import athleticli.ui.Message; + +import static athleticli.common.Config.PATH_DIET_GOAL; + +/** + * Represents a list of diet goals. + */ +public class DietGoalList extends StorableList { + + private final String unparseMessage; + + /** + * Constructs a diet goal list. + */ + public DietGoalList() { + super(PATH_DIET_GOAL); + unparseMessage = "dietGoal %s %s %s %s"; + } + + /** + * Returns a string representation of the diet goal list. + * + * @param data A storage class to retrieve diet information. + * @return A string representation of the diet goal list. + */ + public String toString(Data data) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size(); i++) { + result.append("\t").append(i + 1).append(". ").append(get(i).toString(data)); + if (i != size() - 1) { + result.append("\n"); + } + } + return result.toString(); + } + + /** + * Checks if diet goal of the same nutrients and time span existed in the list. + * + * @param dietGoal + * @return boolean value to indicate if it is not in the list. + */ + public boolean isDietGoalUnique(DietGoal dietGoal) { + for (int i = 0; i < size(); i++) { + if (get(i).isSameNutrient(dietGoal) && get(i).isSameTimeSpan(dietGoal)) { + return false; + } + } + return true; + } + + /** + * Checks if a diet goal has clashing type as those existed in the list. + * The type of diet goals are 'healthy' and 'unhealthy'. + * + * @param dietGoal + * @return boolean value to indicate if the type is valid. + */ + public boolean isDietGoalTypeValid(DietGoal dietGoal) { + for (int i = 0; i < size(); i++) { + if (get(i).isSameNutrient(dietGoal) && !get(i).isSameType(dietGoal)) { + return false; + } + } + return true; + } + + /** + * Checks if the diet goals in the list follow the order where the longer the time span, + * the greater the target value. + * + * @return boolean value indicating that the target value is larger for similar diet goals with longer time span. + */ + public boolean isTargetValueConsistentWithTimeSpan(DietGoal newDietGoal) { + DietGoal storedDietGoal; + for (int i = 0; i < size(); i++) { + storedDietGoal = get(i); + if (!storedDietGoal.isSameNutrient(newDietGoal)) { + continue; + } + boolean isTimeSpanGreater = + storedDietGoal.getTimeSpan().getDays() > newDietGoal.getTimeSpan().getDays(); + boolean isTimeSpanEqual = storedDietGoal.getTimeSpan().getDays() == newDietGoal.getTimeSpan().getDays(); + boolean isTimeSpanLess = storedDietGoal.getTimeSpan().getDays() < newDietGoal.getTimeSpan().getDays(); + boolean isTargetValueGreater = storedDietGoal.getTargetValue() > newDietGoal.getTargetValue(); + boolean isTargetValueLess = storedDietGoal.getTargetValue() < newDietGoal.getTargetValue(); + //Goals with the same time span can take on different values due to goal editing. + if (isTimeSpanEqual) { + continue; + } + if (isTimeSpanGreater && isTargetValueGreater) { + continue; + } + if (isTimeSpanLess && isTargetValueLess) { + continue; + } + return false; + } + return true; + } + + /** + * Parses a diet goal from a string. + * + * @param s The string to be parsed. + * @return The diet goal parsed from the string. + */ + @Override + public DietGoal parse(String s) throws AthletiException { + try { + DietGoal dietGoal = null; + String[] dietGoalDetails = s.split("\\s+"); + String dietGoalTimeSpanString = dietGoalDetails[1]; + String dietGoalNutrientString = dietGoalDetails[2]; + String dietGoalTargetValueString = dietGoalDetails[3]; + String dietGoalType = dietGoalDetails[4]; + + if (dietGoalTargetValueString.trim().length() > Parameter.DIET_GOAL_INTEGER_LENGTH_LIMIT){ + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INCORRECT_INTEGER_FORMAT); + } + int dietGoalTargetValue = Integer.parseInt(dietGoalTargetValueString); + int dietGoalTimeSpanValue = Goal.TimeSpan.valueOf(dietGoalTimeSpanString.toUpperCase()).getDays(); + + verifyParseParameters(dietGoalNutrientString, dietGoalTimeSpanValue); + dietGoal = createParseNewDietGoal(dietGoalType, dietGoalTimeSpanString, + dietGoalNutrientString, dietGoalTargetValue); + validateParseDietGoal(dietGoal); + return dietGoal; + + } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_LOAD_ERROR); + } + } + + private void validateParseDietGoal(DietGoal dietGoal) throws AthletiException { + if (!isDietGoalUnique(dietGoal)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_REPEATED_NUTRIENT); + } + if (!isDietGoalTypeValid(dietGoal)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TYPE_CLASH); + } + if (!isTargetValueConsistentWithTimeSpan(dietGoal)){ + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_SCALING_WITH_TIME_SPAN); + } + } + + private static DietGoal createParseNewDietGoal(String dietGoalType, String dietGoalTimeSpanString, + String dietGoalNutrientString, int dietGoalTargetValue) throws AthletiException { + DietGoal dietGoal; + if (dietGoalType.toLowerCase().equals(HealthyDietGoal.TYPE)) { + dietGoal = new HealthyDietGoal(Goal.TimeSpan.valueOf(dietGoalTimeSpanString.toUpperCase()), + dietGoalNutrientString, dietGoalTargetValue); + } else if (dietGoalType.toLowerCase().equals(UnhealthyDietGoal.TYPE)) { + dietGoal = new UnhealthyDietGoal(Goal.TimeSpan.valueOf(dietGoalTimeSpanString.toUpperCase()), + dietGoalNutrientString, dietGoalTargetValue); + } else { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_LOAD_ERROR); + } + return dietGoal; + } + + private static void verifyParseParameters(String dietGoalNutrientString, int dietGoalTimeSpanValue) + throws AthletiException { + if (!NutrientVerifier.verify(dietGoalNutrientString)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INVALID_NUTRIENT); + } + //Diet goal only support up to period that is less than or equal to DIET_GOAL_TIME_SPAN_LIMIT + if (dietGoalTimeSpanValue > Parameter.DIET_GOAL_TIME_SPAN_LIMIT) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_PERIOD_INVALID); + } + } + + /** + * Unparses a diet goal to a string. + * + * @param dietGoal The diet goal to be parsed. + * @return The string unparsed from the diet goal. + */ + @Override + public String unparse(DietGoal dietGoal) { + /* + * diet goal has nutrient, target value, date. there rest are calculated on the spot. + * */ + return String.format(unparseMessage, dietGoal.getTimeSpan(), dietGoal.getNutrient(), + dietGoal.getTargetValue(), dietGoal.getType()); + + } +} diff --git a/src/main/java/athleticli/data/diet/DietList.java b/src/main/java/athleticli/data/diet/DietList.java new file mode 100644 index 0000000000..501bb9942a --- /dev/null +++ b/src/main/java/athleticli/data/diet/DietList.java @@ -0,0 +1,87 @@ +package athleticli.data.diet; + +import athleticli.data.Findable; +import athleticli.data.StorableList; +import athleticli.exceptions.AthletiException; +import athleticli.parser.Parameter; +import athleticli.parser.DietParser; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static athleticli.common.Config.PATH_DIET; + + +/** + * Represents a list of diets. + */ +public class DietList extends StorableList implements Findable { + /** + * Constructs a diet list. + */ + public DietList() { + super(PATH_DIET); + } + + /** + * Returns a string representation of the diet list. + * + * @return A string representation of the diet list. + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size(); i++) { + result.append("\t").append(i + 1).append(". ").append(get(i).toString()); + if (i != size() - 1) { + result.append("\n"); + } + } + return result.toString(); + } + + /** + * Returns a list of diets matching the date. + * + * @param date The date to be matched. + * @return A list of diets matching the date. + */ + @Override + public ArrayList find(LocalDate date) { + ArrayList result = new ArrayList<>(); + for (Diet diet : this) { + if (diet.getDateTime().toLocalDate().equals(date)) { + result.add(diet); + } + } + return result; + } + + /** + * Parses a diet from a string. + * + * @param s The string to be parsed. + * @return The diet parsed from the string. + */ + @Override + public Diet parse(String s) throws AthletiException { + return DietParser.parseDiet(s); + } + + /** + * Unparses a diet to a string. + * + * @param diet The diet to be parsed. + * @return The string unparsed from the diet. + */ + @Override + public String unparse(Diet diet) { + String commandArgs = ""; + commandArgs += Parameter.CALORIES_SEPARATOR + diet.getCalories(); + commandArgs += " " + Parameter.PROTEIN_SEPARATOR + diet.getProtein(); + commandArgs += " " + Parameter.CARB_SEPARATOR + diet.getCarb(); + commandArgs += " " + Parameter.FAT_SEPARATOR + diet.getFat(); + commandArgs += " " + Parameter.DATETIME_SEPARATOR + diet.getDateTime(); + return commandArgs; + } +} diff --git a/src/main/java/athleticli/data/diet/HealthyDietGoal.java b/src/main/java/athleticli/data/diet/HealthyDietGoal.java new file mode 100644 index 0000000000..e79dce8266 --- /dev/null +++ b/src/main/java/athleticli/data/diet/HealthyDietGoal.java @@ -0,0 +1,51 @@ +package athleticli.data.diet; + +import athleticli.data.Data; + +/** + * HealthyDietGoal tracks nutrients goal that the user wants to increase his/her intake on. + */ +public class HealthyDietGoal extends DietGoal { + + public static final String TYPE = "healthy"; + protected final String healthyDietGoalSymbol; + protected final String healthyDietGoalStringRepresentation; + private final boolean isHealthy; + + /** + * Constructs a diet goal with no current value. + * + * @param timeSpan The timespan of the diet goal. + * @param nutrient The nutrients of the diet goal. + * @param targetValue The target value of the diet goal. + */ + public HealthyDietGoal(TimeSpan timeSpan, String nutrient, int targetValue) { + super(timeSpan, nutrient, targetValue); + isHealthy = true; + healthyDietGoalSymbol = "[HEALTHY]"; + healthyDietGoalStringRepresentation = "%s %s"; + } + + /** + * Returns the type of diet goal of this class. + * + * @return the type of diet goal. + */ + @Override + public String getType() { + return TYPE; + } + + + /** + * Returns the string representation of healthy diet goal. + * + * @param data A storage class to retrieve diet information. + * @return The string representation of the healthy diet goal. + */ + @Override + public String toString(Data data) { + return String.format(healthyDietGoalStringRepresentation, healthyDietGoalSymbol, + super.toString(data)); + } +} diff --git a/src/main/java/athleticli/data/diet/UnhealthyDietGoal.java b/src/main/java/athleticli/data/diet/UnhealthyDietGoal.java new file mode 100644 index 0000000000..c8033f8bd2 --- /dev/null +++ b/src/main/java/athleticli/data/diet/UnhealthyDietGoal.java @@ -0,0 +1,73 @@ +package athleticli.data.diet; + +import athleticli.data.Data; + +/** + * UnhealthyDietGoal tracks nutrients goal that the user wants to reduce his/her intake on. + */ +public class UnhealthyDietGoal extends DietGoal { + + public static final String TYPE = "unhealthy"; + protected final String achievedSymbol; + protected final String unachievedSymbol; + protected final String unhealthyDietGoalSymbol; + protected final String unhealthyDietGoalStringRepresentation; + private final boolean isHealthy; + + /** + * Constructs a diet goal with no current value. + * + * @param timeSpan The timespan of the diet goal. + * @param nutrient The nutrients of the diet goal. + * @param targetValue The target value of the diet goal. + */ + public UnhealthyDietGoal(TimeSpan timeSpan, String nutrient, int targetValue) { + super(timeSpan, nutrient, targetValue); + isHealthy = false; + achievedSymbol = ""; + unachievedSymbol = "[Not Achieved]"; + unhealthyDietGoalSymbol = "[UNHEALTHY]"; + unhealthyDietGoalStringRepresentation = "%s %s"; + } + + @Override + public boolean isAchieved(Data data) { + int currentValue = getCurrentValue(data); + return currentValue <= targetValue; + } + + /** + * Returns the type of diet goal of this class. + * + * @return the type of diet goal. + */ + @Override + public String getType() { + return TYPE; + } + + /** + * Returns string indicator to indicate if unhealthy goal has its limit reached. + * + * @param data A storage class to retrieve diet information. + * @return String indicator if the goal is not achieved. + */ + protected String getSymbol(Data data) { + if (isAchieved(data)) { + return achievedSymbol; + } + return unachievedSymbol; + } + + /** + * Returns the string representation of the unhealthy diet goal. + * + * @param data A storage class to retrieve diet information. + * @return The string representation of the unhealthy diet goal. + */ + @Override + public String toString(Data data) { + return String.format(unhealthyDietGoalStringRepresentation, unhealthyDietGoalSymbol, + super.toString(data)); + } +} diff --git a/src/main/java/athleticli/data/sleep/Sleep.java b/src/main/java/athleticli/data/sleep/Sleep.java new file mode 100644 index 0000000000..031bfdde82 --- /dev/null +++ b/src/main/java/athleticli/data/sleep/Sleep.java @@ -0,0 +1,167 @@ +package athleticli.data.sleep; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; + +import athleticli.exceptions.AthletiException; + +import static athleticli.common.Config.DATE_TIME_PRETTY_FORMATTER; +import static athleticli.common.Config.DATE_FORMATTER; + +/** + * Represents a sleep record. + */ +public class Sleep { + private final LocalDateTime startDateTime; + private final LocalDateTime endDateTime; + + private final Duration sleepingDuration; + + private final LocalDate sleepDate; + + /** + * Generates a new sleep record with some basic stats. + * + * @param startDateTime Start time of the sleep. + * @param toDateTime End time of the sleep. + * @throws AthletiException If any invalid input is provided. + */ + public Sleep(LocalDateTime startDateTime, LocalDateTime toDateTime) throws AthletiException { + this.startDateTime = startDateTime; + this.endDateTime = toDateTime; + this.sleepingDuration = calculateSleepingDuration(); + this.sleepDate = calculateSleepDate(); + } + + public LocalDateTime getStartDateTime() { + return startDateTime; + } + + public LocalDateTime getEndDateTime() { + return endDateTime; + } + + public LocalDate getSleepDate() { + return sleepDate; + } + + public Duration getSleepingDuration() { + return sleepingDuration; + } + + /** + * Calculate the sleeping duration based on start and end time. + * + * @return sleeping duration. + * @throws AthletiException If any invalid input is provided. + */ + private Duration calculateSleepingDuration() throws AthletiException { + if (startDateTime == null || endDateTime == null) { + throw new AthletiException("Cannot calculate duration with null start/end time"); + } + Duration duration = Duration.between(startDateTime, endDateTime); + if (duration.toMinutes() < 1 || duration.toDays() >= 7) { + throw new AthletiException("Invalid sleep duration: less than 1 minute or more than 7 days"); + } + return duration; + } + + /** + * Calculate the sleep date based on start time. + * Factor in the possibility of sleeping past midnight. + * We are assuming that user sleeps before 6am are counted as the previous day. + * + * @return sleep date. + */ + private LocalDate calculateSleepDate() { + if (startDateTime.getHour() < 6) { + return startDateTime.toLocalDate().minusDays(1); + } else { + return startDateTime.toLocalDate(); + } + } + + /** + * Returns a single line summary of the sleep record. + * + * @return String representation of the sleep record. + */ + @Override + public String toString() { + String sleepingDurationOutput = generateSleepingDurationStringOutput(); + String startDateTimeOutput = generateStartDateTimeStringOutput(); + String toDateTimeOutput = generateEndDateTimeStringOutput(); + String sleepDateOutput = generateSleepDateStringOutput(); + return "[Sleep]" + " | " + sleepDateOutput + " | " + startDateTimeOutput + + " | " + toDateTimeOutput + " | " + sleepingDurationOutput; + } + + + /** + * Returns a string representation of the sleeping duration. + * + * @return String representation of the sleeping duration. + */ + public String generateSleepingDurationStringOutput() { + Duration tempDuration = sleepingDuration; + String sleepingDurationOutput = ""; + if (tempDuration.toDays() != 0) { + + if (tempDuration.toDays() == 1) { + sleepingDurationOutput += tempDuration.toDays() + " Day "; + } else { + sleepingDurationOutput += tempDuration.toDays() + " Days "; + } + + tempDuration = tempDuration.minusDays(tempDuration.toDays()); + } + if (tempDuration.toHours() != 0) { + + if (tempDuration.toHours() == 1) { + sleepingDurationOutput += tempDuration.toHours() + " Hour "; + } else { + sleepingDurationOutput += tempDuration.toHours() + " Hours "; + } + + tempDuration = tempDuration.minusHours(tempDuration.toHours()); + } + if (tempDuration.toMinutes() != 0) { + + if (tempDuration.toMinutes() == 1) { + sleepingDurationOutput += tempDuration.toMinutes() + " Minute "; + } else { + sleepingDurationOutput += tempDuration.toMinutes() + " Minutes "; + } + + tempDuration = tempDuration.minusMinutes(tempDuration.toMinutes()); + } + return "Sleeping Duration: " + sleepingDurationOutput; + } + + /** + * Returns a string representation of the start date time. + * + * @return String representation of the start date time. + */ + public String generateStartDateTimeStringOutput() { + return "Start Time: " + startDateTime.format(DATE_TIME_PRETTY_FORMATTER); + } + + /** + * Returns a string representation of the end date time. + * @return String representation of the end date time. + */ + public String generateEndDateTimeStringOutput() { + return "End Time: " + endDateTime.format(DATE_TIME_PRETTY_FORMATTER); + } + + /** + * Returns a string representation of the sleep date. + * + * @return String representation of the sleep date. + */ + public String generateSleepDateStringOutput() { + return "Date: " + sleepDate.format(DATE_FORMATTER); + } +} diff --git a/src/main/java/athleticli/data/sleep/SleepGoal.java b/src/main/java/athleticli/data/sleep/SleepGoal.java new file mode 100644 index 0000000000..cc463ca5f5 --- /dev/null +++ b/src/main/java/athleticli/data/sleep/SleepGoal.java @@ -0,0 +1,93 @@ +package athleticli.data.sleep; + +import athleticli.data.Data; +import athleticli.data.Goal; + +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * Represents a sleep goal. + */ +public class SleepGoal extends Goal { + + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm", + Locale.ENGLISH); + + public enum GoalType { + DURATION + } + + private final GoalType goalType; + private int target; + + /** + * Constructs a sleep goal. + * + * @param timeSpan The time span of the sleep goal. + * @param goalType The goal type of the sleep goal. + * @param targetValue The target value of the sleep goal in minutes. (Used if goalType is DURATION) + */ + public SleepGoal(GoalType goalType, TimeSpan timeSpan, int target) { + super(timeSpan); + this.target = target; + this.goalType = goalType; + } + + /** + * Examines whether the sleep goal is achieved. + * + * @param data The data containing the sleep list. + * @return Whether the sleep goal is achieved. + * @throws IllegalStateException if the goal type is invalid + */ + @Override + public boolean isAchieved(Data data) throws IllegalStateException { + int total = getCurrentValue(data); + return total >= target; + } + + /** + * Returns the current value of the sleep goal metric. + * + * @param data The data containing the sleep list. + * @return The current value of the sleep goal metric. + * @throws IllegalStateException if the goal type is invalid + */ + public int getCurrentValue(Data data) throws IllegalStateException { + SleepList sleeps = data.getSleeps(); + int total; + switch(goalType) { + case DURATION: + total = sleeps.getTotalSleepDuration(this.getTimeSpan()); + break; + default: + throw new IllegalStateException("Unexpected value: " + goalType); + } + return total; + } + + /** + * Returns the string representation of the sleep goal. + * + * @param data The data containing the sleep list. + * @return The string representation of the sleep goal. + */ + public String toString(Data data) { + String goalTypeString = goalType.name(); + return(getTimeSpan().name().toLowerCase() + " " + goalTypeString.toLowerCase() + " " + + ": " + getCurrentValue(data) + "/" + target + " minutes"); + } + + public GoalType getGoalType() { + return goalType; + } + + public int getTargetValue() { + return target; + } + + public void setTargetValue(int target) { + this.target = target; + } +} diff --git a/src/main/java/athleticli/data/sleep/SleepGoalList.java b/src/main/java/athleticli/data/sleep/SleepGoalList.java new file mode 100644 index 0000000000..ec03b0d782 --- /dev/null +++ b/src/main/java/athleticli/data/sleep/SleepGoalList.java @@ -0,0 +1,59 @@ +package athleticli.data.sleep; + +import athleticli.data.Goal; +import athleticli.data.StorableList; +import athleticli.exceptions.AthletiException; +import athleticli.parser.SleepParser; +import athleticli.parser.Parameter; + +import static athleticli.common.Config.PATH_SLEEP_GOAL; + +/** + * Represents a list of sleep goals. + */ +public class SleepGoalList extends StorableList { + /** + * Constructs a sleep goal list. + */ + public SleepGoalList() { + super(PATH_SLEEP_GOAL); + } + + /** + * Parses a sleep goal from a string. + * + * @param arguments The string to be parsed. + * @return The sleep goal parsed from the string. + */ + @Override + public SleepGoal parse(String arguments) throws AthletiException { + return SleepParser.parseSleepGoal(arguments.toLowerCase()); + } + + /** + * Unparses a sleep goal to a string. + * + * @param sleepGoal The sleep goal to be parsed. + * @return The string unparsed from the sleep goal. + */ + @Override + public String unparse(SleepGoal sleepGoal) { + String commandArgs = ""; + commandArgs += Parameter.TYPE_SEPARATOR + sleepGoal.getGoalType(); + commandArgs += " " + Parameter.PERIOD_SEPARATOR + sleepGoal.getTimeSpan(); + commandArgs += " " + Parameter.TARGET_SEPARATOR + sleepGoal.getTargetValue(); + return commandArgs; + } + + /** + * Checks if there is a duplicate sleep goal with the same goal type and timespan. + * + * @param goalType Goal type of the sleep goal. + * @param timeSpan Time span of the sleep goal. + * @return Whether the sleep goal is a duplicate. + */ + public boolean isDuplicate(SleepGoal.GoalType goalType, Goal.TimeSpan timeSpan) { + return this.stream().anyMatch(sleepGoal -> sleepGoal.getGoalType() == goalType && + sleepGoal.getTimeSpan() == timeSpan); + } +} diff --git a/src/main/java/athleticli/data/sleep/SleepList.java b/src/main/java/athleticli/data/sleep/SleepList.java new file mode 100644 index 0000000000..f44b2efe98 --- /dev/null +++ b/src/main/java/athleticli/data/sleep/SleepList.java @@ -0,0 +1,108 @@ +package athleticli.data.sleep; + +import static athleticli.common.Config.PATH_SLEEP; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.time.Duration; + +import athleticli.data.Findable; +import athleticli.data.StorableList; +import athleticli.data.Goal; +import athleticli.exceptions.AthletiException; +import athleticli.parser.SleepParser; +import athleticli.parser.Parameter; + +/** + * Represents a list of sleep records. + */ +public class SleepList extends StorableList implements Findable { + /** + * Constructs a sleep list with its storage path. + */ + public SleepList() { + super(PATH_SLEEP); + } + + /** + * Returns a list of sleeps matching the date. + * + * @param date The date to be matched. + * @return A list of sleeps matching the date. + */ + @Override + public ArrayList find(LocalDate date) { + ArrayList result = new ArrayList<>(); + for (Sleep sleep : this) { + if (sleep.getStartDateTime().toLocalDate().equals(date)) { + result.add(sleep); + } + } + return result; + } + + /** + * Sorts the sleep entries in the list by date. + */ + public void sort() { + this.sort(Comparator.comparing(Sleep::getEndDateTime).reversed()); + } + + /** + * Returns a list of sleeps within the time span. + * + * @param timeSpan The time span to be matched. + * @return A list of sleeps within the time span. + */ + public ArrayList filterByTimespan(Goal.TimeSpan timeSpan) { + ArrayList result = new ArrayList<>(); + for (Sleep sleep : this) { + LocalDate sleepDate = sleep.getStartDateTime().toLocalDate(); + if (Goal.checkDate(sleepDate, timeSpan)) { + result.add(sleep); + } + } + return result; + } + + /** + * Returns the average sleep duration of the sleep list. + * @param timeSpan The time span to be matched. + * @return The total sleep duration of the sleep list in seconds. + */ + public int getTotalSleepDuration(Goal.TimeSpan timeSpan) { + ArrayList filteredSleepList = filterByTimespan(timeSpan); + int totalSleepDuration = 0; + for (Sleep sleep : filteredSleepList) { + Duration sleepDuration = sleep.getSleepingDuration(); + totalSleepDuration += sleepDuration.getSeconds(); + } + return totalSleepDuration; + } + + /** + * Parses a sleep from a string. + * + * @param sleep The string to be parsed. + * @return The sleep parsed from the string. + */ + @Override + public Sleep parse(String sleep) throws AthletiException { + return SleepParser.parseSleep(sleep); + } + + /** + * Unparses a sleep to a string. + * + * @param sleep The sleep to be parsed. + * @return The string unparsed from the sleep. + */ + @Override + public String unparse(Sleep sleep) { + String commandArgs = ""; + commandArgs += Parameter.START_TIME_SEPARATOR + sleep.getStartDateTime(); + commandArgs += " " + Parameter.END_TIME_SEPARATOR + sleep.getEndDateTime(); + return commandArgs; + } +} diff --git a/src/main/java/athleticli/exceptions/AthletiException.java b/src/main/java/athleticli/exceptions/AthletiException.java new file mode 100644 index 0000000000..65e2baff34 --- /dev/null +++ b/src/main/java/athleticli/exceptions/AthletiException.java @@ -0,0 +1,10 @@ +package athleticli.exceptions; + +/** + * Represents the exceptions that need to be shown to the user. + */ +public class AthletiException extends Exception { + public AthletiException(String message) { + super(message); + } +} diff --git a/src/main/java/athleticli/exceptions/WrappedAthletiException.java b/src/main/java/athleticli/exceptions/WrappedAthletiException.java new file mode 100644 index 0000000000..8f2247e2bb --- /dev/null +++ b/src/main/java/athleticli/exceptions/WrappedAthletiException.java @@ -0,0 +1,18 @@ +package athleticli.exceptions; + +/** + * Wraps an AthletiException in RuntimeExcpetion + * so that it can be thrown from inside a stream. + */ +public class WrappedAthletiException extends RuntimeException { + private AthletiException cause; + + public WrappedAthletiException(AthletiException cause) { + this.cause = cause; + } + + @Override + public AthletiException getCause() { + return cause; + } +} diff --git a/src/main/java/athleticli/exceptions/WrappedIOException.java b/src/main/java/athleticli/exceptions/WrappedIOException.java new file mode 100644 index 0000000000..ab5e79f086 --- /dev/null +++ b/src/main/java/athleticli/exceptions/WrappedIOException.java @@ -0,0 +1,19 @@ +package athleticli.exceptions; + +import java.io.IOException; + +/** + * Wraps an IOException in RuntimeExcpetion so that it can be thrown from inside a stream. + */ +public class WrappedIOException extends RuntimeException { + private IOException cause; + + public WrappedIOException(IOException cause) { + this.cause = cause; + } + + @Override + public IOException getCause() { + return cause; + } +} diff --git a/src/main/java/athleticli/parser/ActivityParser.java b/src/main/java/athleticli/parser/ActivityParser.java new file mode 100644 index 0000000000..99d3a0e39f --- /dev/null +++ b/src/main/java/athleticli/parser/ActivityParser.java @@ -0,0 +1,697 @@ +package athleticli.parser; + +import java.math.BigInteger; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +import athleticli.data.Goal; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityChanges; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.Cycle; +import athleticli.data.activity.Run; +import athleticli.data.activity.Swim; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import static athleticli.parser.Parser.getValueForMarker; + +public class ActivityParser { + //@@author AlWo223 + /** + * Parses the index of an activity from a string input. + * + * @param commandArgs Raw user input containing the index. + * @return The parsed Integer index. + * @throws AthletiException If the input is empty or not a valid integer. + */ + public static int parseActivityIndex(String commandArgs) throws AthletiException { + final String commandArgsTrimmed = commandArgs.trim(); + if (commandArgsTrimmed.isEmpty()) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_INDEX_EMPTY); + } + + try { + return Integer.parseInt(commandArgsTrimmed); + } catch (NumberFormatException e) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_INDEX_INVALID); + } + } + + /** + * Parses the provided updated activity for the edit command. + * + * @param arguments Raw user input containing the updated activity. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityChanges parseActivityEdit(String arguments) throws AthletiException { + String[] parts = arguments.split("(?<=\\d)(?=\\D)", 2); + if (parts.length < 2) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_EDIT_INVALID); + } + + return parseActivityChanges(parts[1]); + } + + /** + * Parses the provided updated specific activity for the edit command. + * + * @param arguments Raw user input containing the updated activity. + * @param isRunCycle Whether the activity is a (run or cycle) or not. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + private static ActivityChanges parseActivityTypeEdit(String arguments, boolean isRunCycle) throws AthletiException { + String[] parts = arguments.split(" ", 2); + if (parts.length < 2) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_EDIT_INVALID); + } + + if (isRunCycle) { + return parseRunCycleChanges(parts[1]); + } else { + return parseSwimChanges(parts[1]); + } + } + + /** + * Parses the provided updated run or cycle for the edit command. + * + * @param arguments Raw user input containing the updated run or cycle. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityChanges parseRunCycleEdit(String arguments) throws AthletiException { + return parseActivityTypeEdit(arguments, true); + } + + /** + * Parses the provided update swim for the edit command. + * + * @param arguments Raw user input containing the updated swim. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityChanges parseSwimEdit(String arguments) throws AthletiException { + return parseActivityTypeEdit(arguments, false); + } + + /** + * Parses the provided swim arguments of the edit command. + * + * @param arguments The raw user input containing the updated swim. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityChanges parseSwimChanges(String arguments) throws AthletiException { + ActivityChanges activityChanges = new ActivityChanges(); + parseChangeArguments(activityChanges, arguments, + Parameter.CAPTION_SEPARATOR, + Parameter.DURATION_SEPARATOR, + Parameter.DISTANCE_SEPARATOR, + Parameter.DATETIME_SEPARATOR, + Parameter.SWIMMING_STYLE_SEPARATOR); + return activityChanges; + } + + /** + * Parses the provided run or cycle arguments of the edit command. + * + * @param arguments The raw user input containing the updated run or cycle. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityChanges parseRunCycleChanges(String arguments) throws AthletiException { + ActivityChanges activityChanges = new ActivityChanges(); + parseChangeArguments(activityChanges, arguments, + Parameter.CAPTION_SEPARATOR, + Parameter.DURATION_SEPARATOR, + Parameter.DISTANCE_SEPARATOR, + Parameter.DATETIME_SEPARATOR, + Parameter.ELEVATION_SEPARATOR); + return activityChanges; + } + + /** + * Parses the provided activity arguments of the edit command. + * + * @param arguments The raw user input containing the updated activity. + * @return The parsed ActivityChanges object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityChanges parseActivityChanges(String arguments) throws AthletiException { + ActivityChanges activityChanges = new ActivityChanges(); + parseChangeArguments(activityChanges, arguments, + Parameter.CAPTION_SEPARATOR, + Parameter.DURATION_SEPARATOR, + Parameter.DISTANCE_SEPARATOR, + Parameter.DATETIME_SEPARATOR); + return activityChanges; + } + + /** + * Parses the provided arguments based on the list of separators and updates the ActivityChanges object. + * + * @param activityChanges ActivityChanges object which contains the updates. + * @param arguments Raw user arguments containing the updated parameters. + * @param separators List of separators to be used. + * @throws AthletiException If the input format is invalid. + */ + private static void parseChangeArguments(ActivityChanges activityChanges, String arguments, String... separators) + throws AthletiException { + int numChanges = 0; + int previousIndex = -1; + for (int i = 0; i < separators.length; i++) { + String separator = separators[i]; + int currentSeparatorStartIndex = arguments.indexOf(separator); + + if (currentSeparatorStartIndex != -1) { + if (previousIndex > currentSeparatorStartIndex) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_ORDER_INVALID); + } + + previousIndex = currentSeparatorStartIndex; + int currentEndIndex = findNextSeparatorIndex(arguments, currentSeparatorStartIndex, separators, i); + + String segment = + arguments.substring(currentSeparatorStartIndex + separator.length(), currentEndIndex).trim(); + parseSegment(activityChanges, segment, separator); + numChanges++; + } + } + + if (numChanges == 0) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_EDIT_EMPTY); + } + } + + /** + * Finds the index of the next separator in the arguments String. + * + * @param arguments Raw user input containing the arguments. + * @param startIndex The String position index to start searching from. + * @param separators List of separators to search for. + * @param currentSeparatorIndex Index of the current separator, refers to the list of separators. + * @return The String position index of the next separator. + */ + private static int findNextSeparatorIndex(String arguments, int startIndex, String[] separators, + int currentSeparatorIndex) { + int endIndex = arguments.length(); + for (int j = currentSeparatorIndex + 1; j < separators.length; j++) { + int nextIndex = arguments.indexOf(separators[j], + startIndex + separators[currentSeparatorIndex].length()); + if (nextIndex != -1) { + endIndex = nextIndex; + break; + } + } + return endIndex; + } + + /** + * General method to parse a segment of the activity changes. + * + * @param activityChanges The ActivityChanges object which keeps track of the updates. + * @param segment The segment of the arguments to be parsed. + * @param separator The separator used to identify the segment. + * @throws AthletiException If the input is invalid or empty. + */ + public static void parseSegment(ActivityChanges activityChanges, String segment, String separator) + throws AthletiException { + switch (separator) { + case Parameter.CAPTION_SEPARATOR: + checkEmptyCaptionArgument(segment); + activityChanges.setCaption(segment); + break; + case Parameter.DURATION_SEPARATOR: + checkEmptyDurationArgument(segment); + activityChanges.setDuration(parseDuration(segment)); + break; + case Parameter.DISTANCE_SEPARATOR: + checkEmptyDistanceArgument(segment); + activityChanges.setDistance(parseDistance(segment)); + break; + case Parameter.DATETIME_SEPARATOR: + checkEmptyDateTimeArgument(segment); + activityChanges.setStartDateTime(Parser.parseDateTime(segment)); + break; + case Parameter.ELEVATION_SEPARATOR: + checkEmptyElevationArgument(segment); + activityChanges.setElevation(parseElevation(segment)); + break; + case Parameter.SWIMMING_STYLE_SEPARATOR: + checkEmptySwimmingStyleArgument(segment); + activityChanges.setSwimmingStyle(parseSwimmingStyle(segment)); + break; + default: + assert false: "Invalid separator detected during parsing of activity changes"; + } + } + + /** + * Parses the index of an activity update for the edit command from the provided arguments. + * + * @param arguments The raw user input containing the index. + * @return The parsed Integer index. + * @throws AthletiException If the input format is invalid. + */ + public static int parseActivityEditIndex(String arguments) throws AthletiException { + String[] parts = arguments.split("(?<=\\d)(?=\\D)", 2); + if (parts.length == 0 || parts[0].trim().isEmpty()) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_EDIT_INVALID); + } + return parseActivityIndex(parts[0]); + } + + /** + * Parses the raw user input for viewing the activity list and returns whether the user wants the detailed view. + * + * @param commandArgs The raw user input containing the arguments. + * @return Whether the user wants the detailed view. + */ + public static boolean parseActivityListDetail(String commandArgs) { + return commandArgs.toLowerCase().contains(Parameter.DETAIL_FLAG); + } + + /** + * Parses the raw activity duration input provided by the user. + * + * @param duration The raw user input containing the duration. + * @return The parsed LocalTime duration. + * @throws AthletiException If the input is not an integer. + */ + public static LocalTime parseDuration(String duration) throws AthletiException { + LocalTime durationParsed; + try { + durationParsed = LocalTime.parse(duration); + } catch (DateTimeParseException e) { + throw new AthletiException(Message.MESSAGE_DURATION_INVALID); + } + return durationParsed; + } + + /** + * Parses the raw activity distance input provided by the user. + * + * @param distance The raw user input containing the distance. + * @return The parsed Integer distance. + * @throws AthletiException If the input is not an integer. + */ + public static int parseDistance(String distance) throws AthletiException { + final int distanceUpperBoundary = 1000000; + BigInteger distanceParsed; + try { + distanceParsed = new BigInteger(distance); + } catch (NumberFormatException e) { + throw new AthletiException(Message.MESSAGE_DISTANCE_INVALID); + } + if (distanceParsed.compareTo(BigInteger.ZERO) < 0) { + throw new AthletiException(Message.MESSAGE_DISTANCE_NEGATIVE); + } + if (distanceParsed.compareTo(BigInteger.valueOf(distanceUpperBoundary)) > 0) { + throw new AthletiException(Message.MESSAGE_DISTANCE_TOO_LARGE); + } + return distanceParsed.intValue(); + } + + /** + * Checks if the raw user input includes an empty caption argument. + * + * @param caption The caption of the activity. + * @throws AthletiException If the argument is empty. + */ + public static void checkEmptyCaptionArgument(String caption) throws AthletiException { + if (caption.isEmpty()) { + throw new AthletiException(Message.MESSAGE_CAPTION_EMPTY); + } + } + + /** + * Checks if the raw user input includes an empty duration argument. + * + * @param duration The caption of the activity. + * @throws AthletiException If the argument is empty. + */ + public static void checkEmptyDurationArgument(String duration) throws AthletiException { + if (duration.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DURATION_EMPTY); + } + } + + /** + * Checks if the raw user input includes an empty distance argument. + * + * @param distance The distance of the activity. + * @throws AthletiException If the argument is empty. + */ + public static void checkEmptyDistanceArgument(String distance) throws AthletiException { + if (distance.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DISTANCE_EMPTY); + } + } + + /** + * Checks if the raw user input includes an empty elevation argument. + * + * @param elevation The elevation of the cycle or run. + * @throws AthletiException If the argument is empty. + */ + public static void checkEmptyElevationArgument(String elevation) throws AthletiException { + if (elevation.isEmpty()) { + throw new AthletiException(Message.MESSAGE_ELEVATION_EMPTY); + } + } + + public static void checkEmptySwimmingStyleArgument(String swimmingStyle) throws AthletiException { + if (swimmingStyle.isEmpty()) { + throw new AthletiException(Message.MESSAGE_SWIMMINGSTYLE_EMPTY); + } + } + + /** + * Checks if the raw user input includes an empty datetime argument. + * + * @param dateTime The datetime of the activity. + * @throws AthletiException If the argument is empty. + */ + public static void checkEmptyDateTimeArgument(String dateTime) throws AthletiException { + if (dateTime.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DATETIME_EMPTY); + } + } + + /** + * Parses the raw user input for a swimming style and returns the corresponding swimming style object. + * + * @param swimmingStyle The raw user input containing the swimming style. + * @return An object representing the swimming style. + * @throws AthletiException If the input format is invalid. + */ + public static Swim.SwimmingStyle parseSwimmingStyle(String swimmingStyle) throws AthletiException { + try { + return Swim.SwimmingStyle.valueOf(swimmingStyle.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new AthletiException(Message.MESSAGE_SWIMMINGSTYLE_INVALID); + } + } + + /** + * Parses the raw user input for adding an activity goal and returns the corresponding activity goal object. + * + * @param commandArgs The raw user input containing the arguments. + * @return An object representing the activity goal. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityGoal parseActivityGoal(String commandArgs) throws AthletiException { + final int sportIndex = commandArgs.indexOf(Parameter.SPORT_SEPARATOR); + final int typeIndex = commandArgs.indexOf(Parameter.TYPE_SEPARATOR); + final int periodIndex = commandArgs.indexOf(Parameter.PERIOD_SEPARATOR); + final int targetIndex = commandArgs.indexOf(Parameter.TARGET_SEPARATOR); + + checkMissingActivityGoalArguments(sportIndex, typeIndex, periodIndex, targetIndex); + + if (sportIndex > typeIndex || typeIndex > periodIndex || periodIndex > targetIndex) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_ORDER_INVALID); + } + + final String sport = commandArgs.substring(sportIndex + Parameter.SPORT_SEPARATOR.length(), typeIndex).trim(); + final String type = + commandArgs.substring(typeIndex + Parameter.TYPE_SEPARATOR.length(), periodIndex).trim(); + final String period = + commandArgs.substring(periodIndex + Parameter.PERIOD_SEPARATOR.length(), targetIndex).trim(); + final String target = commandArgs.substring(targetIndex + Parameter.TARGET_SEPARATOR.length()).trim(); + + final ActivityGoal.Sport sportParsed = parseSport(sport); + final ActivityGoal.GoalType typeParsed = parseGoalType(type); + final Goal.TimeSpan periodParsed = parsePeriod(period); + final int targetParsed = parseTarget(target); + + return new ActivityGoal(periodParsed, typeParsed, sportParsed, targetParsed); + } + + //@@author nihalzp + /** + * Parses the raw user input for deleting an activity goal and returns the corresponding activity goal + * object. + * + * @param commandArgs The raw user input containing the arguments. + * @return An object representing the activity goal. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityGoal parseDeleteActivityGoal(String commandArgs) throws AthletiException { + final String sport = getValueForMarker(commandArgs, Parameter.SPORT_SEPARATOR); + if (sport.isEmpty()) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_SPORT_MISSING); + } + final String type = getValueForMarker(commandArgs, Parameter.TYPE_SEPARATOR); + if (type.isEmpty()) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_TYPE_MISSING); + } + final String period = getValueForMarker(commandArgs, Parameter.PERIOD_SEPARATOR); + if (period.isEmpty()) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_PERIOD_MISSING); + } + final ActivityGoal.Sport sportParsed = parseSport(sport); + final ActivityGoal.GoalType typeParsed = parseGoalType(type); + final Goal.TimeSpan periodParsed = parsePeriod(period); + return new ActivityGoal(periodParsed, typeParsed, sportParsed, 0); + } + //@@author AlWo223 + + /** + * Parses the sport input provided by the user. + * + * @param sport The raw user input containing the sport. + * @return The parsed Sport object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityGoal.Sport parseSport(String sport) throws AthletiException { + try { + return ActivityGoal.Sport.valueOf(sport.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new AthletiException(Message.MESSAGE_SPORT_INVALID); + } + } + + /** + * Checks if the raw user input is missing any arguments for creating an activity goal. + * + * @param sportIndex The position of the sport separator. + * @param typeIndex The position of the type separator. + * @param periodIndex The position of the period separator. + * @param targetIndex The position of the target separator. + * @throws AthletiException If any of the arguments are missing. + */ + public static void checkMissingActivityGoalArguments(int sportIndex, int typeIndex, int periodIndex, + int targetIndex) throws AthletiException { + if (sportIndex == -1) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_SPORT_MISSING); + } + if (typeIndex == -1) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_TYPE_MISSING); + } + if (periodIndex == -1) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_PERIOD_MISSING); + } + if (targetIndex == -1) { + throw new AthletiException(Message.MESSAGE_ACTIVITYGOAL_TARGET_MISSING); + } + } + + /** + * Parses the raw elevation input provided by the user. + * + * @param elevation The raw user input containing the elevation. + * @return The parsed Integer elevation. + * @throws AthletiException If the input is not an integer. + */ + public static int parseElevation(String elevation) throws AthletiException { + final int elevationUpperBoundary = 10000; + BigInteger elevationParsed; + try { + elevationParsed = new BigInteger(elevation); + } catch (NumberFormatException e) { + throw new AthletiException(Message.MESSAGE_ELEVATION_INVALID); + } + if (elevationParsed.abs().compareTo(BigInteger.valueOf(elevationUpperBoundary)) > 0) { + throw new AthletiException(Message.MESSAGE_ELEVATION_TOO_LARGE); + } + return elevationParsed.intValue(); + } + + /** + * Parses the goal type input provided by the user. + * + * @param type The raw user input containing the goal type. + * @return The parsed GoalType object. + * @throws AthletiException If the input format is invalid. + */ + public static ActivityGoal.GoalType parseGoalType(String type) throws AthletiException { + try { + return ActivityGoal.GoalType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new AthletiException(Message.MESSAGE_TYPE_INVALID); + } + } + + /** + * Parses the period input provided by the user. + * + * @param period The raw user input containing the period. + * @return The parsed Period object. + * @throws AthletiException If the input format is invalid. + */ + public static Goal.TimeSpan parsePeriod(String period) throws AthletiException { + try { + return Goal.TimeSpan.valueOf(period.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new AthletiException(Message.MESSAGE_PERIOD_INVALID); + } + } + + /** + * Parses the target input provided by the user. + * + * @param target The raw user input containing the target value. + * @return The parsed Integer target value. + * @throws AthletiException If the input is not a positive number. + */ + public static int parseTarget(String target) throws AthletiException { + BigInteger targetParsed; + try { + targetParsed = new BigInteger(target); + } catch (NumberFormatException e) { + throw new AthletiException(Message.MESSAGE_TARGET_INVALID); + } + if (targetParsed.compareTo(BigInteger.ZERO) < 0) { + throw new AthletiException(Message.MESSAGE_TARGET_NEGATIVE); + } + if (targetParsed.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new AthletiException(Message.MESSAGE_TARGET_TOO_LARGE); + } + return targetParsed.intValue(); + } + + /** + * Parses the raw user input for an activity and returns the corresponding ActivityChanges object containing the + * data entries for the activity. + * + * @param arguments The raw user input containing the arguments. + * @throws AthletiException If the input format is invalid. + */ + public static void parseActivityArguments(ActivityChanges activityChanges, String arguments, + String... separators) throws AthletiException { + int firstSeparatorIndex = arguments.indexOf(separators[0]); + if (firstSeparatorIndex == -1) { + throw new AthletiException(Message.MESSAGE_DURATION_MISSING); + } + final String caption = arguments.substring(0, firstSeparatorIndex).trim(); + if (caption.isEmpty()) { + throw new AthletiException(Message.MESSAGE_CAPTION_EMPTY); + } + activityChanges.setCaption(caption); + + int previousIndex = -1; + for (int i = 0; i < separators.length; i++) { + String separator = separators[i]; + int currentSeparatorStartIndex = arguments.indexOf(separator); + checkMissingActivityArgument(currentSeparatorStartIndex, separator); + + if (previousIndex > currentSeparatorStartIndex) { + throw new AthletiException(Message.MESSAGE_ACTIVITY_ORDER_INVALID); + } + previousIndex = currentSeparatorStartIndex; + + int currentEndIndex = findNextSeparatorIndex(arguments, currentSeparatorStartIndex, separators, i); + + String segment = + arguments.substring(currentSeparatorStartIndex + separator.length(), currentEndIndex).trim(); + parseSegment(activityChanges, segment, separator); + } + + } + + /** + * Checks if argument related to the separator is missing and throws parameter specific exception. + * + * @param separatorIndex The position of the separator, refers to the list of separators. + * @param separator The separator. + * @throws AthletiException If any of the arguments are missing. + */ + public static void checkMissingActivityArgument(int separatorIndex, String separator) throws AthletiException { + if (separatorIndex == -1) { + switch (separator) { + case Parameter.DURATION_SEPARATOR: + throw new AthletiException(Message.MESSAGE_DURATION_MISSING); + case Parameter.DISTANCE_SEPARATOR: + throw new AthletiException(Message.MESSAGE_DISTANCE_MISSING); + case Parameter.DATETIME_SEPARATOR: + throw new AthletiException(Message.MESSAGE_DATETIME_MISSING); + case Parameter.ELEVATION_SEPARATOR: + throw new AthletiException(Message.MESSAGE_ELEVATION_MISSING); + case Parameter.SWIMMING_STYLE_SEPARATOR: + throw new AthletiException(Message.MESSAGE_SWIMMINGSTYLE_MISSING); + default: + assert false: "Invalid separator detected during parsing of activity"; + } + } + } + + /** + * Parses the raw user input for a run or cycle and returns the corresponding activity object. + * + * @param arguments The raw user input containing the arguments. + * @return An object representing the activity. + * @throws AthletiException If the input format is invalid. + */ + public static Activity parseRunCycle(String arguments, boolean isRun) throws AthletiException { + ActivityChanges activityChanges = new ActivityChanges(); + parseActivityArguments(activityChanges, arguments, + Parameter.DURATION_SEPARATOR, Parameter.DISTANCE_SEPARATOR, + Parameter.DATETIME_SEPARATOR, Parameter.ELEVATION_SEPARATOR); + + if (isRun) { + return new Run(activityChanges.getCaption(), activityChanges.getDuration(), + activityChanges.getDistance(), activityChanges.getStartDateTime(), + activityChanges.getElevation()); + } else { + return new Cycle(activityChanges.getCaption(), activityChanges.getDuration(), + activityChanges.getDistance(), activityChanges.getStartDateTime(), + activityChanges.getElevation()); + } + } + + /** + * Parses the raw user input for an activity and returns the corresponding activity object. + * + * @param arguments The raw user input containing the arguments. + * @return An object representing the activity. + * @throws AthletiException If the input format is invalid. + */ + public static Activity parseActivity(String arguments) throws AthletiException { + ActivityChanges activityChanges = new ActivityChanges(); + parseActivityArguments(activityChanges, arguments, + Parameter.DURATION_SEPARATOR, Parameter.DISTANCE_SEPARATOR, + Parameter.DATETIME_SEPARATOR); + return new Activity(activityChanges.getCaption(), activityChanges.getDuration(), + activityChanges.getDistance(), activityChanges.getStartDateTime()); + } + + /** + * Parses the raw user input for a swim and returns the corresponding activity object. + * + * @param arguments The raw user input containing the arguments. + * @return activity An object representing the activity. + * @throws AthletiException If the input format is invalid. + */ + public static Activity parseSwim(String arguments) throws AthletiException { + ActivityChanges activityChanges = new ActivityChanges(); + parseActivityArguments(activityChanges, arguments, + Parameter.DURATION_SEPARATOR, Parameter.DISTANCE_SEPARATOR, + Parameter.DATETIME_SEPARATOR, Parameter.SWIMMING_STYLE_SEPARATOR); + return new Swim(activityChanges.getCaption(), activityChanges.getDuration(), + activityChanges.getDistance(), activityChanges.getStartDateTime(), + activityChanges.getSwimmingStyle()); + } +} diff --git a/src/main/java/athleticli/parser/CommandName.java b/src/main/java/athleticli/parser/CommandName.java new file mode 100644 index 0000000000..79ff6366e5 --- /dev/null +++ b/src/main/java/athleticli/parser/CommandName.java @@ -0,0 +1,50 @@ +package athleticli.parser; + +/** + * Defines string literals for command names. + */ +public class CommandName { + public static final String COMMAND_BYE = "bye"; + public static final String COMMAND_HELP = "help"; + public static final String COMMAND_SAVE = "save"; + public static final String COMMAND_FIND = "find"; + + /* Sleep Management */ + public static final String COMMAND_SLEEP_ADD = "add-sleep"; + public static final String COMMAND_SLEEP_EDIT = "edit-sleep"; + public static final String COMMAND_SLEEP_DELETE = "delete-sleep"; + public static final String COMMAND_SLEEP_LIST = "list-sleep"; + public static final String COMMAND_SLEEP_FIND = "find-sleep"; + public static final String COMMAND_SLEEP_GOAL_SET = "set-sleep-goal"; + public static final String COMMAND_SLEEP_GOAL_EDIT = "edit-sleep-goal"; + public static final String COMMAND_SLEEP_GOAL_LIST = "list-sleep-goal"; + + /* Activity Management */ + public static final String COMMAND_RUN = "add-run"; + public static final String COMMAND_ACTIVITY = "add-activity"; + public static final String COMMAND_CYCLE = "add-cycle"; + public static final String COMMAND_SWIM = "add-swim"; + public static final String COMMAND_ACTIVITY_DELETE = "delete-activity"; + public static final String COMMAND_ACTIVITY_LIST = "list-activity"; + public static final String COMMAND_ACTIVITY_EDIT = "edit-activity"; + public static final String COMMAND_ACTIVITY_FIND = "find-activity"; + public static final String COMMAND_RUN_EDIT = "edit-run"; + public static final String COMMAND_CYCLE_EDIT = "edit-cycle"; + public static final String COMMAND_SWIM_EDIT = "edit-swim"; + public static final String COMMAND_ACTIVITY_GOAL_SET = "set-activity-goal"; + public static final String COMMAND_ACTIVITY_GOAL_DELETE = "delete-activity-goal"; + + public static final String COMMAND_ACTIVITY_GOAL_EDIT = "edit-activity-goal"; + public static final String COMMAND_ACTIVITY_GOAL_LIST = "list-activity-goal"; + + /* Diet Management */ + public static final String COMMAND_DIET_GOAL_SET = "set-diet-goal"; + public static final String COMMAND_DIET_GOAL_EDIT = "edit-diet-goal"; + public static final String COMMAND_DIET_GOAL_LIST = "list-diet-goal"; + public static final String COMMAND_DIET_GOAL_DELETE = "delete-diet-goal"; + public static final String COMMAND_DIET_ADD = "add-diet"; + public static final String COMMAND_DIET_DELETE = "delete-diet"; + public static final String COMMAND_DIET_EDIT = "edit-diet"; + public static final String COMMAND_DIET_LIST = "list-diet"; + public static final String COMMAND_DIET_FIND = "find-diet"; +} diff --git a/src/main/java/athleticli/parser/DietParser.java b/src/main/java/athleticli/parser/DietParser.java new file mode 100644 index 0000000000..798a594732 --- /dev/null +++ b/src/main/java/athleticli/parser/DietParser.java @@ -0,0 +1,375 @@ +package athleticli.parser; + +import athleticli.data.Goal; +import athleticli.data.diet.Diet; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.HealthyDietGoal; +import athleticli.data.diet.UnhealthyDietGoal; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import static athleticli.parser.Parser.getValueForMarker; +import static athleticli.parser.Parser.parseDateTime; +import static athleticli.parser.Parser.parseNonNegativeInteger; + +/** + * Defines the methods for Diet parser and Diet Goal parser + */ +public class DietParser { + //@@author yicheng-toh + + /** + * @param commandArgsString User provided data to create goals for the nutrients defined. + * @return a list of diet goals for further checking in the Set Diet Goal Command. + * @throws AthletiException Invalid input by the user. + */ + public static ArrayList parseDietGoalSetAndEdit(String commandArgsString) throws AthletiException { + ArrayList dietGoals; + try { + validateCommandArgsString(commandArgsString); + dietGoals = initializeTempDietGoals(commandArgsString); + } catch (NumberFormatException e) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_POSITIVE_INT); + } catch (ArrayIndexOutOfBoundsException e) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INSUFFICIENT_INPUT); + } + return dietGoals; + } + + private static void validateCommandArgsString(String commandArgsString) throws AthletiException { + if (commandArgsString.trim().isEmpty()) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INSUFFICIENT_INPUT); + } + if (!commandArgsString.contains(" ")) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INSUFFICIENT_INPUT); + } + } + + private static ArrayList initializeTempDietGoals( + String commandArgsString) throws AthletiException { + + int nutrientStartingIndex; + boolean isHealthy; + String[] commandArgs = commandArgsString.split(Parameter.SPACE_SEPEARATOR); + + Goal.TimeSpan timespan = parsePeriod(commandArgs[Parameter.DIET_GOAL_TIME_SPAN_INDEX]); + if (commandArgs[Parameter.DIET_GOAL_UNHEALTHY_FLAG_INDEX].equalsIgnoreCase( + Parameter.UNHEALTHY_DIET_GOAL_FLAG)) { + isHealthy = false; + nutrientStartingIndex = Parameter.UNHEALTHY_DIET_GOAL_NUTRIENT_STARTING_INDEX; + } else { + isHealthy = true; + nutrientStartingIndex = Parameter.HEALTHY_DIET_GOAL_NUTRIENT_STARTING_INDEX; + } + return createNewDietGoals(nutrientStartingIndex, commandArgs, isHealthy, timespan); + } + + private static ArrayList createNewDietGoals(int nutrientStartingIndex, String[] commandArgs, + boolean isHealthy, Goal.TimeSpan timespan) throws AthletiException { + + ArrayList dietGoals = new ArrayList<>(); + Set recordedNutrients = new HashSet<>(); + + String nutrient; + String[] nutrientAndTargetValue; + String targetValueString; + int targetValue; + + for (int i = nutrientStartingIndex; i < commandArgs.length; i++) { + + nutrientAndTargetValue = commandArgs[i].split(Parameter.DIET_GOAL_COMMAND_VALUE_SEPARATOR); + nutrient = nutrientAndTargetValue[Parameter.DIET_GOAL_NUTRIENT_STARTING_INDEX]; + targetValueString = nutrientAndTargetValue[Parameter.DIET_GOAL_TARGET_VALUE_STARTING_INDEX]; + if (targetValueString.trim().length() > Parameter.DIET_GOAL_INTEGER_LENGTH_LIMIT) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TARGET_VALUE_INVALID_INTEGER); + } + targetValue = Integer.parseInt(targetValueString); + + validateDietGoalParameters(recordedNutrients, targetValue, nutrient); + DietGoal dietGoal = createNewDietGoal(isHealthy, timespan, nutrient, targetValue); + + dietGoals.add(dietGoal); + recordedNutrients.add(nutrient); + } + if (dietGoals.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INSUFFICIENT_INPUT); + } + return dietGoals; + } + + + private static void validateDietGoalParameters(Set recordedNutrients, int targetValue, String nutrient) + throws AthletiException { + if (targetValue <= 0) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_POSITIVE_INT); + } + if (!NutrientVerifier.verify(nutrient)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INVALID_NUTRIENT); + } + if (recordedNutrients.contains(nutrient)) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_REPEATED_NUTRIENT); + } + } + + private static DietGoal createNewDietGoal(boolean isHealthy, Goal.TimeSpan timespan, String nutrient, + int targetValue) { + DietGoal dietGoal; + if (isHealthy) { + dietGoal = new HealthyDietGoal(timespan, nutrient, targetValue); + } else { + dietGoal = new UnhealthyDietGoal(timespan, nutrient, targetValue); + } + return dietGoal; + } + + /** + * @param deleteIndexString Index of the goal to be deleted in String format + * @return Index of the goal in integer format in users' perspective. + * @throws AthletiException Catch invalid characters and numbers. + */ + public static int parseDietGoalDelete(String deleteIndexString) throws AthletiException { + try { + if (deleteIndexString.trim().length() > Parameter.DIET_GOAL_INTEGER_LENGTH_LIMIT){ + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INCORRECT_INTEGER_FORMAT); + } + int deleteIndex = Integer.parseInt(deleteIndexString.trim()); + if (deleteIndex <= 0) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INCORRECT_INTEGER_FORMAT); + } + + return deleteIndex; + } catch (NumberFormatException e) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_INCORRECT_INTEGER_FORMAT); + } + } + + /** + * Parses the period input provided by the user + * + * @param period The raw user input containing the period. + * @return The parsed Period object. + * @throws AthletiException If the input format is invalid. + */ + public static Goal.TimeSpan parsePeriod(String period) throws AthletiException { + try { + Goal.TimeSpan timePeriod = Goal.TimeSpan.valueOf(period.toUpperCase()); + //Diet goal only support up to period that is less than or equal to DIET_GOAL_TIME_SPAN_LIMIT + if (timePeriod.getDays() > Parameter.DIET_GOAL_TIME_SPAN_LIMIT) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_PERIOD_INVALID); + } + return timePeriod.valueOf(period.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new AthletiException(Message.MESSAGE_DIET_GOAL_PERIOD_INVALID); + } + } + + //@@author nihalzp + + /** + * Parses the raw user input for a diet and returns the corresponding diet object. + * + * @param commandArgs The raw user input containing the arguments. + * @return An object representing the diet. + * @throws AthletiException + */ + public static Diet parseDiet(String commandArgs) throws AthletiException { + checkMissingDietArguments(commandArgs); + + checkDuplicateDietArguments(commandArgs); + + final String calories = getValueForMarker(commandArgs, Parameter.CALORIES_SEPARATOR); + final String protein = getValueForMarker(commandArgs, Parameter.PROTEIN_SEPARATOR); + final String carb = getValueForMarker(commandArgs, Parameter.CARB_SEPARATOR); + final String fat = getValueForMarker(commandArgs, Parameter.FAT_SEPARATOR); + final String datetime = getValueForMarker(commandArgs, Parameter.DATETIME_SEPARATOR); + + checkEmptyDietArguments(calories, protein, carb, fat, datetime); + + int caloriesParsed = parseNonNegativeInteger(calories, Message.MESSAGE_CALORIES_INVALID, + Message.MESSAGE_CALORIES_OVERFLOW); + int proteinParsed = parseNonNegativeInteger(protein, Message.MESSAGE_PROTEIN_INVALID, + Message.MESSAGE_PROTEIN_OVERFLOW); + int carbParsed = + parseNonNegativeInteger(carb, Message.MESSAGE_CARB_INVALID, Message.MESSAGE_CARB_OVERFLOW); + int fatParsed = + parseNonNegativeInteger(fat, Message.MESSAGE_FAT_INVALID, Message.MESSAGE_FAT_OVERFLOW); + LocalDateTime datetimeParsed = parseDateTime(datetime); + return new Diet(caloriesParsed, proteinParsed, carbParsed, fatParsed, datetimeParsed); + } + + /** + * Checks if marker is missing in the user input. + * + * @param commandArgs The raw user input containing the arguments. + * @param marker The marker for the argument. + * @return True if the argument is missing, false otherwise. + */ + public static boolean isArgumentMissing(String commandArgs, String marker) { + int markerPos = commandArgs.indexOf(marker); + return markerPos == -1; + } + + /** + * Checks if marker is duplicated in the user input. + * + * @param commandArgs The raw user input containing the arguments. + * @param marker The marker for the argument. + * @return True if the argument is duplicated, false otherwise. + */ + public static boolean isArgumentDuplicate(String commandArgs, String marker) { + int markerPos = commandArgs.indexOf(marker); + int lastMarkerPos = commandArgs.lastIndexOf(marker); + return markerPos != lastMarkerPos; + } + + /** + * Checks if any of the arguments for a diet is missing. + * + * @param commandArgs The raw user input containing the arguments. + * @throws AthletiException + */ + public static void checkMissingDietArguments(String commandArgs) throws AthletiException { + if (isArgumentMissing(commandArgs, Parameter.CALORIES_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_CALORIES_MISSING); + } + if (isArgumentMissing(commandArgs, Parameter.PROTEIN_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_PROTEIN_MISSING); + } + if (isArgumentMissing(commandArgs, Parameter.CARB_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_CARB_MISSING); + } + if (isArgumentMissing(commandArgs, Parameter.FAT_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_FAT_MISSING); + } + if (isArgumentMissing(commandArgs, Parameter.DATETIME_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_DIET_DATETIME_MISSING); + } + } + + /** + * Checks if any of the arguments for a diet is duplicated. + * + * @param commandArgs The raw user input containing the arguments. + * @throws AthletiException + */ + public static void checkDuplicateDietArguments(String commandArgs) throws AthletiException { + if (isArgumentDuplicate(commandArgs, Parameter.CALORIES_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_CALORIES_ARG_DUPLICATE); + } + if (isArgumentDuplicate(commandArgs, Parameter.PROTEIN_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_PROTEIN_ARG_DUPLICATE); + } + if (isArgumentDuplicate(commandArgs, Parameter.CARB_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_CARB_ARG_DUPLICATE); + } + if (isArgumentDuplicate(commandArgs, Parameter.FAT_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_FAT_ARG_DUPLICATE); + } + if (isArgumentDuplicate(commandArgs, Parameter.DATETIME_SEPARATOR)) { + throw new AthletiException(Message.MESSAGE_DIET_ARG_DATETIME_DUPLICATE); + } + } + + /** + * Checks if the user input for a diet is empty. + * + * @param calories The calories input. + * @param protein The protein input. + * @param carb The carb input. + * @param fat The fat input. + * @param datetime The datetime input. + * @throws AthletiException + */ + public static void checkEmptyDietArguments(String calories, String protein, String carb, String fat, + String datetime) throws AthletiException { + if (calories.isEmpty()) { + throw new AthletiException(Message.MESSAGE_CALORIES_EMPTY); + } + if (protein.isEmpty()) { + throw new AthletiException(Message.MESSAGE_PROTEIN_EMPTY); + } + if (carb.isEmpty()) { + throw new AthletiException(Message.MESSAGE_CARB_EMPTY); + } + if (fat.isEmpty()) { + throw new AthletiException(Message.MESSAGE_FAT_EMPTY); + } + if (datetime.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DIET_DATETIME_EMPTY); + } + } + + /** + * Parses the index of a diet. + * + * @param commandArgs The raw user input containing the index. + * @return The parsed index. + * @throws AthletiException If the input format is invalid. + */ + public static int parseDietIndex(String commandArgs) throws AthletiException { + if (commandArgs == null || commandArgs.trim().isEmpty()) { + throw new AthletiException(Message.MESSAGE_DIET_INDEX_TYPE_INVALID); + } + + String[] words = commandArgs.trim().split("\\s+", 2); // Split into parts + int parsedIndex = parseNonNegativeInteger(words[0], Message.MESSAGE_DIET_INDEX_TYPE_INVALID, + Message.MESSAGE_INVALID_DIET_INDEX); + + if (parsedIndex == 0) { + throw new AthletiException(Message.MESSAGE_DIET_INDEX_TYPE_INVALID); + } + return parsedIndex; + } + + /** + * Parses the raw user input for a sleep and returns the corresponding sleep object. + * + * @param arguments The raw user input containing the arguments. + * @return An object representing the sleep. + * @throws AthletiException If the input format is invalid. + */ + public static HashMap parseDietEdit(String arguments) throws AthletiException { + checkDuplicateDietArguments(arguments); + + HashMap dietMap = new HashMap<>(); + String calories = getValueForMarker(arguments, Parameter.CALORIES_SEPARATOR); + String protein = getValueForMarker(arguments, Parameter.PROTEIN_SEPARATOR); + String carb = getValueForMarker(arguments, Parameter.CARB_SEPARATOR); + String fat = getValueForMarker(arguments, Parameter.FAT_SEPARATOR); + String datetime = getValueForMarker(arguments, Parameter.DATETIME_SEPARATOR); + if (!calories.isEmpty()) { + int caloriesParsed = parseNonNegativeInteger(calories, Message.MESSAGE_CALORIES_INVALID, + Message.MESSAGE_CALORIES_OVERFLOW); + dietMap.put(Parameter.CALORIES_SEPARATOR, Integer.toString(caloriesParsed)); + } + if (!protein.isEmpty()) { + int proteinParsed = parseNonNegativeInteger(protein, Message.MESSAGE_PROTEIN_INVALID, + Message.MESSAGE_PROTEIN_OVERFLOW); + dietMap.put(Parameter.PROTEIN_SEPARATOR, Integer.toString(proteinParsed)); + } + if (!carb.isEmpty()) { + int carbParsed = parseNonNegativeInteger(carb, Message.MESSAGE_CARB_INVALID, + Message.MESSAGE_CARB_OVERFLOW); + dietMap.put(Parameter.CARB_SEPARATOR, Integer.toString(carbParsed)); + } + if (!fat.isEmpty()) { + int fatParsed = + parseNonNegativeInteger(fat, Message.MESSAGE_FAT_INVALID, Message.MESSAGE_FAT_OVERFLOW); + dietMap.put(Parameter.FAT_SEPARATOR, Integer.toString(fatParsed)); + } + if (!datetime.isEmpty()) { + LocalDateTime datetimeParsed = Parser.parseDateTime(datetime); + dietMap.put(Parameter.DATETIME_SEPARATOR, datetimeParsed.toString()); + } + if (dietMap.isEmpty()) { + throw new AthletiException(Message.MESSAGE_DIET_NO_CHANGE_REQUESTED); + } + return dietMap; + } +} diff --git a/src/main/java/athleticli/parser/NutrientVerifier.java b/src/main/java/athleticli/parser/NutrientVerifier.java new file mode 100644 index 0000000000..5a9b225434 --- /dev/null +++ b/src/main/java/athleticli/parser/NutrientVerifier.java @@ -0,0 +1,21 @@ +package athleticli.parser; + +import java.util.Set; + +/** + * Verify the nutrient from a list of approved nutrients to be log in diet and diet goals + */ +public class NutrientVerifier { + public static final Set VERIFIED_NUTRIENTS = Set.of(Parameter.NUTRIENTS_FAT, + Parameter.NUTRIENTS_CARB, Parameter.NUTRIENTS_PROTEIN, Parameter.NUTRIENTS_CALORIES); + + /** + * Verifies if a nutrient is approved. + * + * @param nutrient + * @return boolean value if it is found in the approved list. + */ + public static boolean verify(String nutrient) { + return VERIFIED_NUTRIENTS.contains(nutrient); + } +} diff --git a/src/main/java/athleticli/parser/Parameter.java b/src/main/java/athleticli/parser/Parameter.java new file mode 100644 index 0000000000..57a987304a --- /dev/null +++ b/src/main/java/athleticli/parser/Parameter.java @@ -0,0 +1,74 @@ +package athleticli.parser; + + +public class Parameter { + public static final String SPACE = " "; + + /* For Sleep and Activity */ + public static final String START_TIME_SEPARATOR = "start/"; + public static final String END_TIME_SEPARATOR = "end/"; + + /* For Acitivity */ + public static final String ACTIVITY_INDICATOR = "[Activity]"; + public static final String RUN_INDICATOR = "[Run]"; + public static final String CYCLE_INDICATOR = "[Cycle]"; + public static final String SWIM_INDICATOR = "[Swim]"; + public static final String SPORT_SEPARATOR = "sport/"; + public static final String TYPE_SEPARATOR = "type/"; + public static final String PERIOD_SEPARATOR = "period/"; + public static final String TARGET_SEPARATOR = "target/"; + public static final String DURATION_SEPARATOR = "duration/"; + public static final String CAPTION_SEPARATOR = "caption/"; + public static final String DISTANCE_SEPARATOR = "distance/"; + public static final String DATETIME_SEPARATOR = "datetime/"; + public static final String ELEVATION_SEPARATOR = "elevation/"; + public static final String SWIMMING_STYLE_SEPARATOR = "style/"; + public static final String ACTIVITY_STORAGE_INDICATOR = "[Activity]:"; + public static final String RUN_STORAGE_INDICATOR = "[Run]:"; + public static final String CYCLE_STORAGE_INDICATOR = "[Cycle]:"; + public static final String SWIM_STORAGE_INDICATOR = "[Swim]:"; + public static final String DETAIL_FLAG = "-d"; + public static final String DISTANCE_UNIT_METERS = " m"; + public static final String DISTANCE_UNIT_KILOMETERS = " km"; + public static final String SPEED_UNIT_KILOMETERS_PER_HOUR = " km/h"; + public static final String PACE_UNIT_TIME_PER_KILOMETER = " /km"; + public static final String TIME_UNIT_HOURS = "h"; + public static final String TIME_UNIT_MINUTES = "m"; + public static final String TIME_UNIT_SECONDS = "s"; + public static final String DISTANCE_PREFIX = "Distance: "; + public static final String TIME_PREFIX = "Time: "; + public static final String ELEVATION_PREFIX = "Elevation Gain: "; + public static final String PACE_PREFIX = "Pace: "; + public static final String LAPS_PREFIX = "Laps: "; + public static final String STYLE_PREFIX = "Style: "; + public static final String LAP_TIME_PREFIX = "Lap Time: "; + public static final String AVG_LAP_TIME_PREFIX = "Avg. Lap Time: "; + public static final String SPEED_PREFIX = "Speed: "; + public static final String AVG_SPEED_PREFIX = "Avg. Speed: "; + public static final String ACTIVITY_OVERVIEW_SEPARATOR = " | "; + public static final int KILOMETER_IN_METERS = 1000; + public static final int HOUR_IN_SECONDS = 3600; + public static final int MINUTE_IN_SECONDS = 60; + + /* For Diet */ + public static final String CALORIES_SEPARATOR = "calories/"; + public static final String PROTEIN_SEPARATOR = "protein/"; + public static final String CARB_SEPARATOR = "carb/"; + public static final String FAT_SEPARATOR = "fat/"; + + public static final String NUTRIENTS_CALORIES = "calories"; + public static final String NUTRIENTS_PROTEIN = "protein"; + public static final String NUTRIENTS_FAT = "fat"; + public static final String NUTRIENTS_CARB = "carb"; + public static final String UNHEALTHY_DIET_GOAL_FLAG = "unhealthy"; + public static final String DIET_GOAL_COMMAND_VALUE_SEPARATOR = "/"; + public static final String SPACE_SEPEARATOR = "\\s+"; + public static final int DIET_GOAL_TIME_SPAN_INDEX = 0; + public static final int DIET_GOAL_UNHEALTHY_FLAG_INDEX = 1; + public static final int HEALTHY_DIET_GOAL_NUTRIENT_STARTING_INDEX = 1; + public static final int UNHEALTHY_DIET_GOAL_NUTRIENT_STARTING_INDEX = 2; + public static final int DIET_GOAL_NUTRIENT_STARTING_INDEX = 0; + public static final int DIET_GOAL_TARGET_VALUE_STARTING_INDEX = 1; + public static final int DIET_GOAL_TIME_SPAN_LIMIT = 7; + public static final int DIET_GOAL_INTEGER_LENGTH_LIMIT = 6; +} diff --git a/src/main/java/athleticli/parser/Parser.java b/src/main/java/athleticli/parser/Parser.java new file mode 100644 index 0000000000..fcb7b00911 --- /dev/null +++ b/src/main/java/athleticli/parser/Parser.java @@ -0,0 +1,282 @@ +package athleticli.parser; + +import athleticli.commands.ByeCommand; +import athleticli.commands.Command; +import athleticli.commands.FindCommand; +import athleticli.commands.HelpCommand; +import athleticli.commands.SaveCommand; +import athleticli.commands.activity.AddActivityCommand; +import athleticli.commands.activity.DeleteActivityCommand; +import athleticli.commands.activity.DeleteActivityGoalCommand; +import athleticli.commands.activity.EditActivityCommand; +import athleticli.commands.activity.EditActivityGoalCommand; +import athleticli.commands.activity.FindActivityCommand; +import athleticli.commands.activity.ListActivityCommand; +import athleticli.commands.activity.ListActivityGoalCommand; +import athleticli.commands.activity.SetActivityGoalCommand; +import athleticli.commands.diet.AddDietCommand; +import athleticli.commands.diet.DeleteDietCommand; +import athleticli.commands.diet.DeleteDietGoalCommand; +import athleticli.commands.diet.EditDietCommand; +import athleticli.commands.diet.EditDietGoalCommand; +import athleticli.commands.diet.FindDietCommand; +import athleticli.commands.diet.ListDietCommand; +import athleticli.commands.diet.ListDietGoalCommand; +import athleticli.commands.diet.SetDietGoalCommand; +import athleticli.commands.sleep.AddSleepCommand; +import athleticli.commands.sleep.DeleteSleepCommand; +import athleticli.commands.sleep.EditSleepCommand; +import athleticli.commands.sleep.EditSleepGoalCommand; +import athleticli.commands.sleep.FindSleepCommand; +import athleticli.commands.sleep.ListSleepCommand; +import athleticli.commands.sleep.ListSleepGoalCommand; +import athleticli.commands.sleep.SetSleepGoalCommand; +import athleticli.data.activity.Activity; +import athleticli.data.activity.Cycle; +import athleticli.data.activity.Run; +import athleticli.data.activity.Swim; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static athleticli.common.Config.DATE_FORMATTER; +import static athleticli.common.Config.DATE_TIME_FORMATTER; + +/** + * Defines the basic methods for command parser. + */ +public class Parser { + private static final String INVALID_YEAR = "0000"; + + /** + * Splits the raw user input into two parts, and then returns them. The first part is the command type, + * while the second part is the command arguments. The second part can be empty. + * + * @param rawUserInput The raw user input. + * @return A string array whose first element is the command type and the second element is the command + * arguments. + */ + public static String[] splitCommandWordAndArgs(String rawUserInput) { + assert rawUserInput != null : "`rawUserInput` should not be null"; + final String[] split = rawUserInput.trim().split("\\s+", 2); + return split.length == 2 ? split : new String[]{split[0], ""}; + } + + /** + * Parses the raw user input and returns the corresponding command object. + * + * @param rawUserInput The raw user input. + * @return An object representing the command. + * @throws AthletiException If the command is invalid + */ + public static Command parseCommand(String rawUserInput) throws AthletiException { + assert rawUserInput != null : "`rawUserInput` should not be null"; + final String[] commandTypeAndParams = splitCommandWordAndArgs(rawUserInput); + final String commandType = commandTypeAndParams[0]; + final String commandArgs = commandTypeAndParams[1]; + switch (commandType) { + + /* General */ + case CommandName.COMMAND_BYE: + return new ByeCommand(); + case CommandName.COMMAND_HELP: + return new HelpCommand(commandArgs); + case CommandName.COMMAND_SAVE: + return new SaveCommand(); + case CommandName.COMMAND_FIND: + return new FindCommand(parseDate(commandArgs)); + + /* Sleep Management */ + case CommandName.COMMAND_SLEEP_ADD: + return new AddSleepCommand(SleepParser.parseSleep(commandArgs)); + case CommandName.COMMAND_SLEEP_LIST: + return new ListSleepCommand(); + case CommandName.COMMAND_SLEEP_EDIT: + return new EditSleepCommand(SleepParser.parseSleepIndex(commandArgs), + SleepParser.parseSleep(commandArgs)); + case CommandName.COMMAND_SLEEP_DELETE: + return new DeleteSleepCommand(SleepParser.parseSleepIndex(commandArgs)); + case CommandName.COMMAND_SLEEP_FIND: + return new FindSleepCommand(parseDate(commandArgs)); + + /* Sleep Goal Management */ + case CommandName.COMMAND_SLEEP_GOAL_LIST: + return new ListSleepGoalCommand(); + case CommandName.COMMAND_SLEEP_GOAL_SET: + return new SetSleepGoalCommand(SleepParser.parseSleepGoal(commandArgs)); + case CommandName.COMMAND_SLEEP_GOAL_EDIT: + return new EditSleepGoalCommand(SleepParser.parseSleepGoal(commandArgs)); + + /* Activity Management */ + case CommandName.COMMAND_ACTIVITY: + return new AddActivityCommand(ActivityParser.parseActivity(commandArgs)); + case CommandName.COMMAND_CYCLE: + return new AddActivityCommand(ActivityParser.parseRunCycle(commandArgs, false)); + case CommandName.COMMAND_RUN: + return new AddActivityCommand(ActivityParser.parseRunCycle(commandArgs, true)); + case CommandName.COMMAND_SWIM: + return new AddActivityCommand(ActivityParser.parseSwim(commandArgs)); + case CommandName.COMMAND_ACTIVITY_DELETE: + return new DeleteActivityCommand(ActivityParser.parseActivityIndex(commandArgs)); + case CommandName.COMMAND_ACTIVITY_LIST: + return new ListActivityCommand(ActivityParser.parseActivityListDetail(commandArgs)); + case CommandName.COMMAND_ACTIVITY_EDIT: + return new EditActivityCommand(ActivityParser.parseActivityEditIndex(commandArgs), + ActivityParser.parseActivityEdit(commandArgs), Activity.class); + case CommandName.COMMAND_RUN_EDIT: + return new EditActivityCommand(ActivityParser.parseActivityEditIndex(commandArgs), + ActivityParser.parseRunCycleEdit(commandArgs), Run.class); + case CommandName.COMMAND_CYCLE_EDIT: + return new EditActivityCommand(ActivityParser.parseActivityEditIndex(commandArgs), + ActivityParser.parseRunCycleEdit(commandArgs), Cycle.class); + case CommandName.COMMAND_SWIM_EDIT: + return new EditActivityCommand(ActivityParser.parseActivityEditIndex(commandArgs), + ActivityParser.parseSwimEdit(commandArgs), Swim.class); + case CommandName.COMMAND_ACTIVITY_FIND: + return new FindActivityCommand(parseDate(commandArgs)); + + /* Activity Goal Management */ + case CommandName.COMMAND_ACTIVITY_GOAL_SET: + return new SetActivityGoalCommand(ActivityParser.parseActivityGoal(commandArgs)); + case CommandName.COMMAND_ACTIVITY_GOAL_DELETE: + return new DeleteActivityGoalCommand(ActivityParser.parseDeleteActivityGoal(commandArgs)); + case CommandName.COMMAND_ACTIVITY_GOAL_EDIT: + return new EditActivityGoalCommand(ActivityParser.parseActivityGoal(commandArgs)); + case CommandName.COMMAND_ACTIVITY_GOAL_LIST: + return new ListActivityGoalCommand(); + + /* Diet Management */ + case CommandName.COMMAND_DIET_ADD: + return new AddDietCommand(DietParser.parseDiet(commandArgs)); + case CommandName.COMMAND_DIET_EDIT: + return new EditDietCommand(DietParser.parseDietIndex(commandArgs), + DietParser.parseDietEdit(commandArgs)); + case CommandName.COMMAND_DIET_DELETE: + return new DeleteDietCommand(DietParser.parseDietIndex(commandArgs)); + case CommandName.COMMAND_DIET_LIST: + return new ListDietCommand(); + case CommandName.COMMAND_DIET_FIND: + return new FindDietCommand(parseDate(commandArgs)); + + /* Diet Goal Management */ + case CommandName.COMMAND_DIET_GOAL_SET: + return new SetDietGoalCommand(DietParser.parseDietGoalSetAndEdit(commandArgs)); + case CommandName.COMMAND_DIET_GOAL_EDIT: + return new EditDietGoalCommand(DietParser.parseDietGoalSetAndEdit(commandArgs)); + case CommandName.COMMAND_DIET_GOAL_LIST: + return new ListDietGoalCommand(); + case CommandName.COMMAND_DIET_GOAL_DELETE: + return new DeleteDietGoalCommand(DietParser.parseDietGoalDelete(commandArgs)); + + default: + throw new AthletiException(Message.MESSAGE_UNKNOWN_COMMAND); + } + } + + /** + * Parses the raw date time input provided by the user. + * + * @param datetime The raw user input containing the date time. + * @return datetimeParsed The parsed LocalDateTime object. + * @throws AthletiException If the input format is invalid. + */ + public static LocalDateTime parseDateTime(String datetime) throws AthletiException { + if (datetime.startsWith(INVALID_YEAR)) { + throw new AthletiException(Message.MESSAGE_DATETIME_INVALID); + } + LocalDateTime datetimeParsed; + try { + datetimeParsed = LocalDateTime.parse(datetime.replace("T", " "), DATE_TIME_FORMATTER); + if (datetimeParsed.isAfter(LocalDateTime.now())) { + throw new AthletiException(Message.MESSAGE_DATE_FUTURE); + } + } catch (DateTimeParseException e) { + throw new AthletiException(Message.MESSAGE_DATETIME_INVALID); + } + return datetimeParsed; + } + + public static LocalDate parseDate(String date) throws AthletiException { + if (date.startsWith(INVALID_YEAR)) { + throw new AthletiException(Message.MESSAGE_DATE_INVALID); + } + try { + LocalDate dateParsed = LocalDate.parse(date, DATE_FORMATTER); + if (dateParsed.isAfter(LocalDate.now())) { + throw new AthletiException(Message.MESSAGE_DATE_FUTURE); + } + return dateParsed; + } catch (DateTimeParseException e) { + throw new AthletiException(Message.MESSAGE_DATE_INVALID); + } + } + + /** + * Parses the raw integer input provided by the user. + * + * @param integer The raw user input containing the integer. + * @param invalidMessage The message to be displayed if the input is invalid. + * @param overflowMessage The message to be displayed if the input is too large. + * @return integerParsed The parsed integer. + * @throws AthletiException If the input format is invalid. + */ + public static int parseNonNegativeInteger(String integer, String invalidMessage, + String overflowMessage) throws AthletiException { + java.math.BigInteger integerParsed; + try { + integerParsed = new java.math.BigInteger(integer); + } catch (NumberFormatException e) { + throw new AthletiException(invalidMessage); + } + if (integerParsed.signum() < 0) { + throw new AthletiException(invalidMessage); + } + if (integerParsed.compareTo(java.math.BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { + throw new AthletiException(overflowMessage); + } + return integerParsed.intValue(); + } + + /** + * Parses the value for a specific marker in a given argument string. + * + * @param arguments The raw user input containing the arguments. + * @param marker The marker whose value is to be retrieved. + * @return The value associated with the given marker, or an empty string if the marker is not found. + */ + public static String getValueForMarker(String arguments, String marker) { + String patternString; + + if (marker.equals(Parameter.DATETIME_SEPARATOR)) { + // Capture one or two words following the DATETIME_SEPARATOR + patternString = Pattern.quote(marker) + "(\\S+)(?:\\s+(\\S+))?"; + } else { + patternString = Pattern.quote(marker) + "(\\S+)"; + } + + Pattern pattern = Pattern.compile(patternString); + Matcher matcher = pattern.matcher(arguments); + + if (matcher.find()) { + if (marker.equals(Parameter.DATETIME_SEPARATOR)) { + String firstPart = matcher.group(1); + String secondPart = matcher.group(2); + if (secondPart != null) { + return firstPart + " " + secondPart; + } else { + return firstPart; + } + } else { + return matcher.group(1); + } + } + + // Return empty string if no match is found + return ""; + } +} diff --git a/src/main/java/athleticli/parser/SleepParser.java b/src/main/java/athleticli/parser/SleepParser.java new file mode 100644 index 0000000000..7f00ba5116 --- /dev/null +++ b/src/main/java/athleticli/parser/SleepParser.java @@ -0,0 +1,173 @@ +package athleticli.parser; + +import java.time.LocalDateTime; + +import athleticli.data.Goal; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepGoal; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; + +public class SleepParser { + //@@author DaDevChia + + /* Sleep Management */ + + /** + * Parses the raw user input for an add sleep command and returns the corresponding command object. + * + * @param commandArgs The raw user input containing the arguments. + * @return An object representing the sleep add command. + * @throws AthletiException + */ + public static Sleep parseSleep(String commandArgs) throws AthletiException { + final int startDatetimeIndex = commandArgs.indexOf(Parameter.START_TIME_SEPARATOR); + final int endDatetimeIndex = commandArgs.indexOf(Parameter.END_TIME_SEPARATOR); + + if (startDatetimeIndex == -1 || endDatetimeIndex == -1) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_NO_START_END_DATETIME); + } + + if (startDatetimeIndex > endDatetimeIndex) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_INVALID_START_END_ORDER); + } + + final String startDatetimeStr = + commandArgs.substring(startDatetimeIndex + Parameter.START_TIME_SEPARATOR.length(), endDatetimeIndex) + .trim(); + final String endDatetimeStr = + commandArgs.substring(endDatetimeIndex + Parameter.END_TIME_SEPARATOR.length()).trim(); + + if (startDatetimeStr == null || startDatetimeStr.isEmpty() + || endDatetimeStr == null || endDatetimeStr.isEmpty()) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_NO_START_END_DATETIME); + } + + final LocalDateTime startDatetime = Parser.parseDateTime(startDatetimeStr); + final LocalDateTime endDatetime = Parser.parseDateTime(endDatetimeStr); + + if (startDatetime == null || endDatetime == null) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_INVALID_DATETIME); + } + + if (startDatetime.isEqual(endDatetime) || startDatetime.isAfter(endDatetime)) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_START_END_NON_CHRONOLOGICAL); + } + + return new Sleep(startDatetime, endDatetime); + } + + /** + * Parses the raw user input the sleep index and returns the corresponding index. + * + * @param commandArgs The raw user input containing the arguments. + * @return The index of the sleep to be edited. + * @throws AthletiException If the index is invalid. + */ + public static int parseSleepIndex(String commandArgs) throws AthletiException { + final String indexStr = commandArgs.split("(?<=\\d)(?=\\D)", 2)[0].trim(); + + if (indexStr == null || indexStr.isEmpty()) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_NO_INDEX); + } + + int index; + try { + index = Integer.parseInt(indexStr); + } catch (NumberFormatException e) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_INVALID_INDEX); + } + + if (index <= 0) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_INVALID_INDEX); + } + + return index; + } + + /* Sleep Goal Management */ + + /** + * Parses the raw user input for a sleep goal and returns the corresponding sleep goal object. + * + * @param commandArgs The raw user input containing the arguments. + * @return An object representing the sleep goal. + * @throws AthletiException If the sleep goal is invalid. + */ + public static SleepGoal parseSleepGoal(String commandArgs) throws AthletiException { + final int goalTypeIndex = commandArgs.indexOf(Parameter.TYPE_SEPARATOR); + final int periodIndex = commandArgs.indexOf(Parameter.PERIOD_SEPARATOR); + final int targetValueIndex = commandArgs.indexOf(Parameter.TARGET_SEPARATOR); + + if (goalTypeIndex == -1 || periodIndex == -1 || targetValueIndex == -1) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_GOAL_MISSING_PARAMETERS); + } + + if (goalTypeIndex > periodIndex || periodIndex > targetValueIndex) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_PARAMETERS_ORDER); + } + + final String type = commandArgs.substring(goalTypeIndex + Parameter.TYPE_SEPARATOR.length(), periodIndex) + .trim(); + final String period = commandArgs.substring(periodIndex + Parameter.PERIOD_SEPARATOR.length(), targetValueIndex) + .trim(); + final String target = commandArgs.substring(targetValueIndex + Parameter.TARGET_SEPARATOR.length()).trim(); + + final SleepGoal.GoalType goalType = parseGoalType(type); + final Goal.TimeSpan timeSpan = parsePeriod(period); + final int targetParsed = parseTarget(target); + + return new SleepGoal(goalType, timeSpan, targetParsed); + } + + /** + * Parses the raw user input for a sleep goal index and returns the corresponding index. + * + * @param type The string representing the type of the sleep goal. + * @return The type of the sleep goal. + * @throws AthletiException If the type is invalid. + */ + private static SleepGoal.GoalType parseGoalType(String type) throws AthletiException { + switch (type) { + case "duration": + return SleepGoal.GoalType.DURATION; + default: + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_TYPE); + } + } + + /** + * Parses the raw user input for a sleep goal period and returns the corresponding period. + * + * @param period The string representing the period of the sleep goal. + * @return The period of the sleep goal. + * @throws AthletiException If the period is invalid. + */ + private static Goal.TimeSpan parsePeriod(String period) throws AthletiException { + try { + return Goal.TimeSpan.valueOf(period.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_PERIOD); + } + } + + /** + * Parses the raw user input for a sleep goal target and returns the corresponding target. + * + * @param target The string representing the target of the sleep goal. + * @return The target of the sleep goal. + * @throws AthletiException If the target is invalid. + */ + private static int parseTarget(String target) throws AthletiException { + int targetParsed; + try { + targetParsed = Integer.parseInt(target); + } catch (NumberFormatException e) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_TARGET); + } + if (targetParsed <= 0) { + throw new AthletiException(Message.ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_TARGET); + } + return targetParsed; + } +} diff --git a/src/main/java/athleticli/storage/Storage.java b/src/main/java/athleticli/storage/Storage.java new file mode 100644 index 0000000000..c21830e22c --- /dev/null +++ b/src/main/java/athleticli/storage/Storage.java @@ -0,0 +1,53 @@ +package athleticli.storage; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +import athleticli.exceptions.WrappedIOException; + +/** + * Defines the basic methods for file storage. + */ +public class Storage { + /** + * Saves strings into a file. + * + * @param path The path to the file. + * @param items The stream of strings. + * @throws IOException + */ + public static void save(String path, Stream items) throws IOException { + File file = new File(path); + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + FileWriter fileWriter = new FileWriter(file); + try { + items.filter(Objects::nonNull).forEachOrdered(str -> { + try { + fileWriter.write(str); + } catch (IOException e) { + throw new WrappedIOException(e); + } + }); + } catch (WrappedIOException e) { + throw e.getCause(); + } + fileWriter.close(); + } + + public static Stream load(String path) throws IOException { + File file = new File(path); + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + return Files.lines(Path.of(path)); + } +} diff --git a/src/main/java/athleticli/ui/Message.java b/src/main/java/athleticli/ui/Message.java new file mode 100644 index 0000000000..65763f362a --- /dev/null +++ b/src/main/java/athleticli/ui/Message.java @@ -0,0 +1,327 @@ +package athleticli.ui; + +import athleticli.parser.CommandName; + +public class Message { + public static final String PROMPT = "> "; + public static final String LINE = "____________________________________________________________\n"; + public static final String PREFIX_MESSAGE = " "; + public static final String PREFIX_EXCEPTION = "OOPS!!! "; + public static final String MESSAGE_BYE = "Bye. Hope to see you again soon!"; + public static final String[] MESSAGE_HELLO = {"Hello! I'm AthletiCLI!", "What can I do for you?"}; + public static final String MESSAGE_SAVE = "File saved successfully!"; + public static final String MESSAGE_DURATION_MISSING = + "Please specify the activity duration using \"duration/\"!"; + public static final String MESSAGE_DISTANCE_MISSING = + "Please specify the activity distance using \"distance/\"!"; + public static final String MESSAGE_DATETIME_MISSING = + "Please specify date and time of the activity using \"datetime/\"!"; + public static final String MESSAGE_EMPTY_ACTIVITY_LIST = "You have not tracked any activities yet! Time to do " + + "some sports!"; + public static final String MESSAGE_CALORIES_MISSING = + "Please specify the calories burned using \"calories/\"!"; + public static final String MESSAGE_ACTIVITYGOAL_SPORT_MISSING = "Please specify the sport using \"sport/\"!"; + public static final String MESSAGE_ACTIVITYGOAL_TYPE_MISSING = "Please specify the goal type using \"type/\"!"; + public static final String MESSAGE_ACTIVITYGOAL_PERIOD_MISSING = "Please specify the period using \"period/\"!"; + public static final String MESSAGE_ACTIVITYGOAL_TARGET_MISSING = "Please specify the target value using " + + "\"target/\"!"; + public static final String MESSAGE_PROTEIN_MISSING = + "Please specify the protein intake using \"protein/\"! Use \"protein/0\" if no protein was consumed."; + public static final String MESSAGE_CARB_MISSING = + "Please specify the carbohydrate intake using \"carb/\"! Use \"carb/0\" if no carbohydrate was consumed."; + public static final String MESSAGE_FAT_MISSING = "Please specify the fat intake using \"fat/\"! Use \"fat/0\" if " + + "no fat was consumed."; + public static final String MESSAGE_DIET_DATETIME_MISSING = + "Please specify the datetime of the diet using \"datetime/\"!"; + public static final String MESSAGE_CALORIES_ARG_DUPLICATE = + "Please do not specify the calories burned more than once!"; + public static final String MESSAGE_PROTEIN_ARG_DUPLICATE = + "Please do not specify the protein intake more than once!"; + public static final String MESSAGE_CARB_ARG_DUPLICATE = + "Please do not specify the carbohydrate intake more than once!"; + public static final String MESSAGE_FAT_ARG_DUPLICATE = + "Please do not specify the fat intake more than once!"; + public static final String MESSAGE_DIET_ARG_DATETIME_DUPLICATE = + "Please do not specify the datetime of the diet more than once!"; + public static final String MESSAGE_CALORIES_OVERFLOW = + "The calories consumed cannot be larger than " + Integer.MAX_VALUE + "!"; + public static final String MESSAGE_PROTEIN_OVERFLOW = + "The protein intake cannot be larger than " + Integer.MAX_VALUE + "!"; + public static final String MESSAGE_CARB_OVERFLOW = + "The carbohydrate intake cannot be larger than " + Integer.MAX_VALUE + "!"; + public static final String MESSAGE_FAT_OVERFLOW = + "The fat intake cannot be larger than " + Integer.MAX_VALUE + "!"; + public static final String MESSAGE_CAPTION_EMPTY = "The caption of an activity cannot be empty!"; + public static final String MESSAGE_DURATION_EMPTY = "The duration of an activity cannot be empty!"; + public static final String MESSAGE_DISTANCE_EMPTY = "The distance of an activity cannot be empty!"; + public static final String MESSAGE_DATETIME_EMPTY = "The datetime of an activity cannot be empty!"; + public static final String MESSAGE_CALORIES_EMPTY = "The calories burned cannot be empty!"; + public static final String MESSAGE_PROTEIN_EMPTY = "The protein intake cannot be empty!"; + public static final String MESSAGE_CARB_EMPTY = "The carbohydrate intake cannot be empty!"; + public static final String MESSAGE_FAT_EMPTY = "The fat intake cannot be empty!"; + public static final String MESSAGE_DIET_DATETIME_EMPTY = "The datetime of a diet cannot be empty!"; + public static final String MESSAGE_DIET_UPDATED = "Ok, I've updated this diet:"; + public static final String MESSAGE_DURATION_INVALID = + "The duration of an activity must be in the format \"HH:mm:ss\"!"; + public static final String MESSAGE_DISTANCE_INVALID = + "The distance of an activity must be a non-negative integer!"; + public static final String MESSAGE_DISTANCE_NEGATIVE = + "The distance of an activity cannot be negative!"; + public static final String MESSAGE_TARGET_NEGATIVE = "The target value cannot be negative. " + + "You wanna make progress, not regress ;)"; + public static final String MESSAGE_TARGET_INVALID = "The target value of an activity goal must be a non-negative " + + "integer!"; + public static final String MESSAGE_TARGET_TOO_LARGE = + "The target value of an activity goal cannot be larger than " + Integer.MAX_VALUE + "!"; + public static final String MESSAGE_DATETIME_INVALID = + "The datetime must be valid and in the format \"yyyy-MM-dd HH:mm\"!"; + public static final String MESSAGE_DATE_INVALID = + "The date must be valid and in the format \"yyyy-MM-dd\"!"; + public static final String MESSAGE_CALORIES_INVALID = + "The calories burned must be a non-negative integer!"; + public static final String MESSAGE_SPORT_INVALID = "The sport of an activity must be one of the following: " + + "\"running\", \"cycling\", \"swimming\", \"general\"!"; + public static final String MESSAGE_TYPE_INVALID = "The type of an activity must be either \"distance\" or " + + "\"duration\"!"; + public static final String MESSAGE_PERIOD_INVALID = "The period of an activity must be one of the " + + "following: \"daily\", \"weekly\", \"monthly\", \"yearly\"!"; + public static final String MESSAGE_PROTEIN_INVALID = "The protein intake must be a non-negative integer!"; + public static final String MESSAGE_CARB_INVALID = + "The carbohydrate intake must be a non-negative integer!"; + public static final String MESSAGE_FAT_INVALID = "The fat intake must be a non-negative integer!"; + public static final String MESSAGE_ACTIVITY_FIND = "I've found these activities:"; + public static final String MESSAGE_ACTIVITY_ADDED = "Well done! I've added this activity:"; + public static final String MESSAGE_ACTIVITY_DELETED = "Gotcha, I've deleted this activity:"; + public static final String MESSAGE_ACTIVITY_GOAL_ADDED = "Alright, I've added this activity goal:"; + public static final String MESSAGE_ACTIVITY_GOAL_DELETED = "Alright, I've deleted this activity goal:"; + public static final String MESSAGE_ACTIVITY_GOAL_EDITED = "Alright, I've edited this activity goal:"; + public static final String MESSAGE_NO_SUCH_GOAL_EXISTS = "No such goal exists."; + public static final String MESSAGE_ACTIVITY_GOAL_LIST = "These are your activity goals:"; + public static final String MESSAGE_DIET_ADDED = "Well done! I've added this diet:"; + public static final String MESSAGE_ELEVATION_MISSING = + "Please specify the elevation gain using \"elevation/\"!"; + public static final String MESSAGE_ELEVATION_EMPTY = + "The elevation gain of an activity cannot be empty!"; + public static final String MESSAGE_ELEVATION_INVALID = + "The elevation gain of an activity must be an integer!"; + public static final String MESSAGE_ACTIVITY_INDEX_INVALID = "The activity index must be an integer!"; + public static final String MESSAGE_ACTIVITY_INDEX_OUT_OF_BOUNDS = "The activity index does not exist, check your " + + "list for the correct index!"; + public static final String MESSAGE_SWIMMINGSTYLE_MISSING = + "Please specify the swimming style using \"style/\"!"; + public static final String MESSAGE_SWIMMINGSTYLE_INVALID = + "The swimming style of an activity must be one of " + + "the following: \"butterfly\", \"backstroke\", \"breaststroke\", \"freestyle\"!"; + public static final String MESSAGE_ACTIVITY_COUNT = + "You have tracked a total of %d activities. Keep pushing!"; + public static final String MESSAGE_ACTIVITY_LIST = "These are the activities you have tracked so far:"; + public static final String MESSAGE_ACTIVITY_EDIT_INVALID = "The format of the edit command is wrong! Please" + + " provide the index and the updated parameters!"; + public static final String MESSAGE_ACTIVITY_UPDATED = "Ok, I've updated this activity:"; + public static final String MESSAGE_DIET_COUNT = + "Now you have tracked a total of %d diets. Keep grinding!"; + public static final String MESSAGE_ACTIVITY_FIRST = + "Now you have tracked your first activity. This is just the beginning!"; + public static final String MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_POSITIVE_INT = "The target value for nutrients " + + "must be a positive integer!"; + public static final String MESSAGE_DIET_GOAL_INVALID_NUTRIENT = "Key word to nutrients goals has " + + "to be one of the following: \"calories\", \"protein\", \"carb\", \"fat\"!"; + public static final String MESSAGE_DIET_GOAL_ALREADY_EXISTED = "Diet goal for %s has already existed. " + + "Please edit the goal instead!"; + public static final String MESSAGE_DIET_GOAL_NOT_EXISTED = "Diet goal for %s and time period %s is not present. " + + "Please add the goal before editing it!"; + public static final String MESSAGE_DIET_GOAL_COUNT = "Now you have %d diet goal(s)."; + public static final String MESSAGE_DIET_GOAL_NONE = "There are no goals at the moment. Add a diet goal to start."; + public static final String MESSAGE_DIET_GOAL_LIST_HEADER = "These are your goal(s):\n"; + public static final String MESSAGE_DIET_GOAL_INCORRECT_INTEGER_FORMAT = "Please provide a positive " + + "integer not more than 999999."; + public static final String MESSAGE_DIET_GOAL_EMPTY_DIET_GOAL_LIST = "There is no diet goals at the moment. " + + "Please add one to continue.\n"; + public static final String MESSAGE_DIET_GOAL_DELETE_HEADER = "The following goal has been deleted:\n"; + public static final String MESSAGE_DIET_GOAL_OUT_OF_BOUND = "Unable to fetch diet goal. " + + "Please enter a value from 1 to %d."; + public static final String MESSAGE_DIET_GOAL_INSUFFICIENT_INPUT = "Please input the following keywords " + + "to create or edit your diet goals:\n [unhealthy] followed by \"calories\", \"protein\", " + + "\"carb\", \"fat\" and then followed by the target value.\n" + "\te.g. WEEKLY calories/100\n" + + "\te.g. WEEKLY unhealthy fat/100"; + public static final String MESSAGE_DIET_GOAL_TARGET_VALUE_NOT_SCALING_WITH_TIME_SPAN = + "Please ensure your weekly diet goal target value is greater than your daily diet goal target value!"; + public static final String MESSAGE_DIET_GOAL_REPEATED_NUTRIENT = "Please ensure that there are " + + "no repetitions for your diet goal nutrients."; + public static final String MESSAGE_DIET_GOAL_LOAD_ERROR = "Some error has been encountered " + + "while loading diet goals."; + public static final String MESSAGE_DIET_GOAL_TYPE_CLASH = "You cannot have healthy goals and unhealthy goals " + + "for the same nutrient."; + public static final String MESSAGE_DIET_GOAL_PERIOD_INVALID = "The period of an activity must be one of the " + + "following: \"daily\", \"weekly\"!"; + public static final String MESSAGE_DIET_GOAL_TARGET_VALUE_INVALID_INTEGER = "Please ensure target value is a " + + "positive integer not more than 999999"; + + public static final String MESSAGE_DIET_FIRST = + "Now you have tracked your first diet. This is just the beginning!"; + public static final String MESSAGE_INVALID_DIET_INDEX = + "The diet index is invalid! Please enter a valid diet index!"; + public static final String MESSAGE_DIET_INDEX_TYPE_INVALID = "The diet index must be a positive integer!"; + public static final String MESSAGE_DIET_DELETED = "Noted. I've removed this diet:"; + public static final String MESSAGE_DIET_LIST = "Here are the diets in your list:"; + public static final String MESSAGE_DIET_FIND = "I've found these diets:"; + public static final String MESSAGE_DIET_NO_CHANGE_REQUESTED = "No change requested. Specify the appropriate " + + "parameters to edit the diet."; + + + /* Sleep Messages */ + public static final String MESSAGE_SLEEP_COUNT = "You have tracked a total of %d sleep records. Keep it up!"; + public static final String MESSAGE_SLEEP_FIRST = "You have tracked your first sleep record. This is just the " + + "beginning!"; + + public static final String MESSAGE_SLEEP_ADDED = "Well done! I've added this sleep record:"; + + public static final String MESSAGE_SLEEP_EDITED = "Alright, I've changed this sleep record:"; + + public static final String MESSAGE_SLEEP_DELETED = "Gotcha, I've deleted this sleep record:"; + + public static final String MESSAGE_SLEEP_LIST = "Here are the sleep records in your list:\n"; + public static final String MESSAGE_SLEEP_LIST_EMPTY = "You have no sleep records in your list."; + + public static final String MESSAGE_SLEEP_FIND = "I've found these sleeps:"; + + public static final String MESSAGE_SLEEP_GOAL_ADDED = "Alright, I've added this sleep goal:"; + public static final String MESSAGE_SLEEP_GOAL_EDITED = "Alright, I've edited this sleep goal:"; + public static final String MESSAGE_SLEEP_GOAL_LIST = "These are your sleep goals:"; + + /* Sleep Error Messages */ + public static final String ERRORMESSAGE_SLEEP_EDIT_INDEX_OOBE = + "The index of the sleep record you want to edit is out of bounds."; + public static final String ERRORMESSAGE_SLEEP_DELETE_INDEX_OOBE = + "The index of the sleep record you want to delete is out of bounds."; + + public static final String ERRORMESSAGE_SLEEP_OVERLAP = + "The sleep record you are trying to input overlaps with an existing sleep record."; + public static final String ERRORMESSAGE_DUPLICATE_SLEEP_GOAL = + "You already have a goal for this type and period! Please edit the existing goal instead."; + + public static final String ERRORMESSAGE_PARSER_SLEEP_NO_START_END_DATETIME = + "Please specify both the start and end time of your sleep."; + public static final String ERRORMESSAGE_PARSER_SLEEP_START_END_NON_CHRONOLOGICAL = + "Please specify the start time of your sleep chronologically before the end time."; + public static final String ERRORMESSAGE_PARSER_SLEEP_INVALID_START_END_ORDER = + "Please specify the /start before /end."; + public static final String ERRORMESSAGE_PARSER_SLEEP_INVALID_DATETIME = + "Please specify the start and end time of your sleep in the format \"yyyy-MM-dd HH:mm\"."; + + public static final String ERRORMESSAGE_PARSER_SLEEP_NO_INDEX = + "Please specify the index of the sleep record"; + public static final String ERRORMESSAGE_PARSER_SLEEP_INVALID_INDEX = + "Please specify the index of the sleep record as a positive integer not more than 999999."; + + public static final String ERRORMESSAGE_PARSER_SLEEP_GOAL_MISSING_PARAMETERS = + "Please specify the type, period and target value of your sleep goal."; + public static final String ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_PARAMETERS_ORDER = + "Please specify the type, period and target value of your sleep goal in the correct order."; + public static final String ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_TYPE = + "Please specify the type of your sleep goal as \"duration\"."; + public static final String ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_PERIOD = + "The period must be one of the " + + "following: \"daily\", \"weekly\", \"monthly\", \"yearly\"!"; + public static final String ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_TARGET = + "Please specify the target value of your sleep goal as a positive integer."; + public static final String ERRORMESSAGE_PARSER_SLEEP_GOAL_INVALID_PARAMETERS = + "Please specify the type, period and target value of your sleep goal."; + + public static final String MESSAGE_UNKNOWN_COMMAND = "I'm sorry, but I don't know what that means :-("; + public static final String MESSAGE_IO_EXCEPTION = "An I/O exception occurred."; + public static final String MESSAGE_LOAD_EXCEPTION = "An exception occurred when loading %s.\n" + + "Please quit AthletiCLI with `bye` command and fix it manually,\n" + + "or start from empty lists by entering any other command."; + + /* Help Messages */ + public static final String HELP_ADD_ACTIVITY = CommandName.COMMAND_ACTIVITY + + " CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME"; + public static final String HELP_ADD_RUN = CommandName.COMMAND_RUN + + " CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION"; + public static final String HELP_ADD_SWIM = CommandName.COMMAND_SWIM + + " CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME style/STYLE"; + public static final String HELP_ADD_CYCLE = CommandName.COMMAND_CYCLE + + " CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION"; + public static final String HELP_DELETE_ACTIVITY = CommandName.COMMAND_ACTIVITY_DELETE + + " INDEX"; + public static final String HELP_LIST_ACTIVITY = CommandName.COMMAND_ACTIVITY_LIST + + " [-d]"; + public static final String HELP_EDIT_ACTIVITY = CommandName.COMMAND_ACTIVITY_EDIT + + " INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME]"; + public static final String HELP_EDIT_RUN = CommandName.COMMAND_RUN_EDIT + + " INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] " + + "[elevation/ELEVATION]"; + public static final String HELP_EDIT_SWIM = CommandName.COMMAND_SWIM_EDIT + + " INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [style/STYLE]"; + public static final String HELP_EDIT_CYCLE = CommandName.COMMAND_CYCLE_EDIT + + " INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] " + + "[elevation/ELEVATION]"; + public static final String HELP_FIND_ACTIVITY = CommandName.COMMAND_ACTIVITY_FIND + + " DATE"; + public static final String HELP_SET_ACTIVITY_GOAL = CommandName.COMMAND_ACTIVITY_GOAL_SET + + " sport/SPORT type/TYPE period/PERIOD target/TARGET"; + public static final String HELP_EDIT_ACTIVITY_GOAL = CommandName.COMMAND_ACTIVITY_GOAL_EDIT + + " sport/SPORT type/TYPE period/PERIOD target/TARGET"; + public static final String HELP_LIST_ACTIVITY_GOAL = CommandName.COMMAND_ACTIVITY_GOAL_LIST; + public static final String HELP_DELETE_ACTIVITY_GOAL = CommandName.COMMAND_ACTIVITY_GOAL_DELETE + + " sport/SPORT type/TYPE period/PERIOD"; + public static final String HELP_ADD_DIET = CommandName.COMMAND_DIET_ADD + + " calories/CALORIES protein/PROTEIN carb/CARB fat/FAT datetime/DATETIME"; + public static final String HELP_EDIT_DIET = CommandName.COMMAND_DIET_EDIT + + " INDEX [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT] [datetime/DATETIME]"; + public static final String HELP_DELETE_DIET = CommandName.COMMAND_DIET_DELETE + + " INDEX"; + public static final String HELP_LIST_DIET = CommandName.COMMAND_DIET_LIST; + public static final String HELP_FIND_DIET = CommandName.COMMAND_DIET_FIND + + " DATE"; + public static final String HELP_SET_DIET_GOAL = CommandName.COMMAND_DIET_GOAL_SET + + " [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT]"; + public static final String HELP_EDIT_DIET_GOAL = CommandName.COMMAND_DIET_GOAL_EDIT + + " [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT]"; + public static final String HELP_LIST_DIET_GOAL = CommandName.COMMAND_DIET_GOAL_LIST; + public static final String HELP_DELETE_DIET_GOAL = CommandName.COMMAND_DIET_GOAL_DELETE + + " INDEX"; + public static final String HELP_ADD_SLEEP = CommandName.COMMAND_SLEEP_ADD + + " start/START end/END"; + public static final String HELP_LIST_SLEEP = CommandName.COMMAND_SLEEP_LIST; + public static final String HELP_DELETE_SLEEP = CommandName.COMMAND_SLEEP_DELETE + + " INDEX"; + public static final String HELP_EDIT_SLEEP = CommandName.COMMAND_SLEEP_EDIT + + " INDEX start/START end/END"; + public static final String HELP_FIND_SLEEP = CommandName.COMMAND_SLEEP_FIND + + " DATE"; + public static final String HELP_SET_SLEEP_GOAL = CommandName.COMMAND_SLEEP_GOAL_SET + + " type/TYPE period/PERIOD target/TARGET"; + public static final String HELP_EDIT_SLEEP_GOAL = CommandName.COMMAND_SLEEP_GOAL_EDIT + + " type/TYPE period/PERIOD target/TARGET"; + public static final String HELP_LIST_SLEEP_GOAL = CommandName.COMMAND_SLEEP_GOAL_LIST; + + public static final String HELP_SAVE = CommandName.COMMAND_SAVE; + public static final String HELP_BYE = CommandName.COMMAND_BYE; + public static final String HELP_HELP = CommandName.COMMAND_HELP + + " [COMMAND]"; + public static final String HELP_FIND = CommandName.COMMAND_FIND + + " DATE"; + public static final String HELP_DETAILS = + "Please check our user guide (https://ay2324s1-cs2113-t17-1.github.io/tp/) for details."; + public static final String ACTIVITY_STORAGE_INVALID_INDICATOR = "Invalid activity indicator, file corrupted."; + public static final String ACTIVITY_STORAGE_INVALID_FORMAT = "Invalid activity format, file corrupted."; + public static final String MESSAGE_ACTIVITY_EDIT_EMPTY = "You have not specified any changes to the activity."; + public static final String MESSAGE_SWIMMINGSTYLE_EMPTY = "The swimming style of an activity cannot be empty!"; + public static final String MESSAGE_ACTIVITY_INDEX_EMPTY = "The activity index cannot be empty!"; + public static final String MESSAGE_ACTIVITY_ORDER_INVALID = "The order of the parameters is wrong, please refer " + + "to the User Guide for the correct order."; + public static final String MESSAGE_ACTIVITY_LIST_END = "\nTo see more performance details about an activity, use " + + "the -d flag"; + public static final String MESSAGE_DISTANCE_TOO_LARGE = "The distance of an activity cannot be larger than " + + "1000km! You are not Forrest Gump!"; + public static final String MESSAGE_ELEVATION_TOO_LARGE = "The elevation of an activity cannot be larger than " + + "10km! Mt. Everest is only 8.8km high!"; + public static final String MESSAGE_DUPLICATE_ACTIVITY_GOAL = "You already have a goal for this " + + "sport, type and period! Please edit the existing goal instead."; + public static final String MESSAGE_ACTIVITY_TYPE_MISMATCH = "The edit command does not match the type of " + + "the activity you are trying to edit!"; + public static final String MESSAGE_DATE_FUTURE = + "I like your optimism, but you cannot track events in the future!"; +} diff --git a/src/main/java/athleticli/ui/Ui.java b/src/main/java/athleticli/ui/Ui.java new file mode 100644 index 0000000000..8141813faf --- /dev/null +++ b/src/main/java/athleticli/ui/Ui.java @@ -0,0 +1,90 @@ +package athleticli.ui; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Scanner; + +/** + * Defines the behavior of the CLI. + */ +public class Ui { + private static Ui uiInstance; + private final Scanner in; + private final PrintStream out; + + /** + * Constructs a Ui object, whose input in + * and output out is the standard input and the standard + * output, respectively. + */ + private Ui() { + this(System.in, System.out); + } + + /** + * Constructs a Ui object, whose input is an InputStream + * object in and output is an PrintStream object out. + * + * @param in The InputStream accepting the user's input. + * @param out The PrintStream displaying the program's output. + */ + private Ui(InputStream in, PrintStream out) { + assert in != null : "Input stream `in` should not be null"; + assert out != null : "Print stream `out` should not be null"; + this.in = new Scanner(in); + this.out = out; + } + + /** + * Returns the singleton instance of `Ui`. + * + * @return The singleton instance of `Ui`. + */ + public static Ui getInstance() { + if (uiInstance == null) { + uiInstance = new Ui(); + } + return uiInstance; + } + + /** + * Returns the user's input. + * + * @return The user's input. + */ + public String getUserCommand() { + out.print(Message.PROMPT); + return in.nextLine(); + } + + /** + * Shows the messages in a beautiful format. + * + * @param messages The messages to be shown. + */ + public void showMessages(String... messages) { + assert messages != null : "Messages should not be null"; + out.print(Message.LINE); + for (String message : messages) { + out.println(Message.PREFIX_MESSAGE + message); + } + out.println(Message.LINE); + } + + /** + * Shows message for exception e. + * + * @param e The exception whose message will be shown. + */ + public void showException(Exception e) { + assert e != null : "Exception `e` should not be null"; + showMessages(Message.PREFIX_EXCEPTION + e.getMessage()); + } + + /** + * Shows the welcome message. + */ + public void showWelcome() { + showMessages(Message.MESSAGE_HELLO); + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/test/java/athleticli/commands/activity/AddActivityCommandTest.java b/src/test/java/athleticli/commands/activity/AddActivityCommandTest.java new file mode 100644 index 0000000000..f743981b63 --- /dev/null +++ b/src/test/java/athleticli/commands/activity/AddActivityCommandTest.java @@ -0,0 +1,64 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.Run; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the AddActivityCommand class. + */ +class AddActivityCommandTest { + + private static final String CAPTION = "Night Run"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, 21); + private static final int ELEVATION = 60; + private Run run; + private AddActivityCommand addActivityCommand; + private Data data; + + /** + * Sets up the required objects for each test. + */ + @BeforeEach + void setUp() { + run = new Run(CAPTION, DURATION, DISTANCE, DATE, ELEVATION); + addActivityCommand = new AddActivityCommand(run); + data = new Data(); + } + + /** + * Tests the execute method on a prefilled list and checks if the activity is added to the data. + */ + @Test + void execute_addsActivity_returnsConfirmationMessage() { + String[] expected = {"Well done! I've added this activity:", run.toString(), "You have tracked a total of 2 " + + "activities. Keep pushing!"}; + addActivityCommand.execute(data); + String[] actual = addActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + /** + * Tests the execute method on empty activity list and checks if output is correct. + */ + @Test + void execute_addsFirstActivity_returnsFirstActivityMessage() { + String[] expected = {"Well done! I've added this activity:", run.toString(), "Now you have tracked your " + + "first activity. This is just the beginning!"}; + String[] actual = addActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + +} diff --git a/src/test/java/athleticli/commands/activity/DeleteActivityCommandTest.java b/src/test/java/athleticli/commands/activity/DeleteActivityCommandTest.java new file mode 100644 index 0000000000..9d15dbce6e --- /dev/null +++ b/src/test/java/athleticli/commands/activity/DeleteActivityCommandTest.java @@ -0,0 +1,65 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.Run; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests the DeleteActivityCommand class. + */ +class DeleteActivityCommandTest { + + private static final String CAPTION = "Night Run"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, 21); + private static final int ELEVATION = 60; + private Run run; + private DeleteActivityCommand deleteActivityCommand; + private Data data; + + /** + * Sets up the required objects for each test. + */ + @BeforeEach + void setUp() { + run = new Run(CAPTION, DURATION, DISTANCE, DATE, ELEVATION); + AddActivityCommand addActivityCommand = new AddActivityCommand(run); + data = new Data(); + addActivityCommand.execute(data); + } + + /** + * Tests the delete command when the index is valid. The activity should be deleted and the correct output should + * be returned. + * + * @throws AthletiException if the index is invalid. + */ + @Test + void execute_validIndex_activityDeleted() throws AthletiException { + String[] expected = {"Gotcha, I've deleted this activity:", run.toString(), "You have tracked a total of 0 " + + "activities. Keep pushing!"}; + deleteActivityCommand = new DeleteActivityCommand(1); + String[] actual = deleteActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + /** + * Tests the delete command when the index is invalid. An exception should be thrown. + */ + @Test + void execute_invalidIndex_exceptionThrown() { + deleteActivityCommand = new DeleteActivityCommand(0); + assertThrows(AthletiException.class, () -> deleteActivityCommand.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/activity/DeleteActivityGoalCommandTest.java b/src/test/java/athleticli/commands/activity/DeleteActivityGoalCommandTest.java new file mode 100644 index 0000000000..10d9e3f732 --- /dev/null +++ b/src/test/java/athleticli/commands/activity/DeleteActivityGoalCommandTest.java @@ -0,0 +1,58 @@ +package athleticli.commands.activity; + + +import athleticli.data.Data; +import athleticli.data.Goal.TimeSpan; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.ActivityGoal.GoalType; +import athleticli.data.activity.ActivityGoal.Sport; +import athleticli.data.activity.ActivityGoalList; +import athleticli.exceptions.AthletiException; +import athleticli.ui.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +public class DeleteActivityGoalCommandTest { + + ActivityGoalList activityGoals; + private Data data; + + @BeforeEach + void setUp() { + data = new Data(); + activityGoals = data.getActivityGoals(); + ActivityGoal goal1 = new ActivityGoal(ActivityGoal.TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10); + ActivityGoal goal2 = new ActivityGoal(ActivityGoal.TimeSpan.MONTHLY, ActivityGoal.GoalType.DURATION, + ActivityGoal.Sport.CYCLING, 20); + ActivityGoal goal3 = new ActivityGoal(ActivityGoal.TimeSpan.YEARLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.SWIMMING, 30); + ActivityGoal goal4 = new ActivityGoal(ActivityGoal.TimeSpan.DAILY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.GENERAL, 40); + activityGoals.add(goal1); + activityGoals.add(goal2); + activityGoals.add(goal3); + activityGoals.add(goal4); + } + + @Test + void execute_existingActivityGoal_editsActivityGoal() throws athleticli.exceptions.AthletiException { + ActivityGoal goal = new ActivityGoal(TimeSpan.WEEKLY, GoalType.DISTANCE, Sport.RUNNING, 0); + DeleteActivityGoalCommand command = new DeleteActivityGoalCommand(goal); + String[] expected = + new String[]{Message.MESSAGE_ACTIVITY_GOAL_DELETED, activityGoals.get(0).toString(data)}; + String[] actual = command.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_nonExistingActivityGoal_throwsAthletiException() { + ActivityGoal goal = new ActivityGoal(TimeSpan.YEARLY, GoalType.DISTANCE, Sport.RUNNING, 0); + DeleteActivityGoalCommand command = new DeleteActivityGoalCommand(goal); + assertThrows(AthletiException.class, () -> command.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/activity/EditActivityCommandTest.java b/src/test/java/athleticli/commands/activity/EditActivityCommandTest.java new file mode 100644 index 0000000000..d5b02cb57b --- /dev/null +++ b/src/test/java/athleticli/commands/activity/EditActivityCommandTest.java @@ -0,0 +1,82 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityChanges; +import athleticli.data.activity.Run; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests the EditActivityCommand class. + */ +class EditActivityCommandTest { + private static final String CAPTION = "Night Run"; + private static final String UPDATED_CAPTION = "Morning Run"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final LocalTime UPDATED_DURATION = LocalTime.of(1, 30); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, 21); + private static final LocalDateTime UPDATED_DATE = LocalDateTime.of(2023, 10, 11, 23, 21); + private AddActivityCommand addActivityCommand; + private Data data; + private Run run; + private Run updatedRun; + private ActivityChanges activityChanges; + + /** + * Sets up the required scenario for each test. + */ + @BeforeEach + void setUp() { + Activity activity = new Activity(CAPTION, DURATION, DISTANCE, DATE); + addActivityCommand = new AddActivityCommand(activity); + data = new Data(); + addActivityCommand.execute(data); + run = new Run(CAPTION, DURATION, DISTANCE, DATE, 60); + addActivityCommand = new AddActivityCommand(run); + addActivityCommand.execute(data); + activityChanges = new ActivityChanges(); + activityChanges.setCaption(CAPTION); + activityChanges.setDuration(DURATION); + activityChanges.setStartDateTime(DATE); + updatedRun = new Run(CAPTION, DURATION, DISTANCE, DATE, 60); + } + + /** + * Test the edit command for a valid index and checks if the activity is edited successfully. + * @throws AthletiException If the index is invalid. + */ + @Test + void execute_validIndex_activityEdited() throws AthletiException { + EditActivityCommand editActivityCommand = new EditActivityCommand(2, activityChanges, Run.class); + editActivityCommand.execute(data); + String[] expected = {"Ok, I've updated this activity:", updatedRun.toString(), "You have tracked a total of 2" + + " " + + "activities. Keep pushing!"}; + String[] actual = editActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + assertEquals(updatedRun.getCaption(), data.getActivities().get(1).getCaption()); + assertEquals(updatedRun.getMovingTime(), data.getActivities().get(1).getMovingTime()); + assertEquals(updatedRun.getDistance(), data.getActivities().get(1).getDistance()); + assertEquals(updatedRun.getStartDateTime(), data.getActivities().get(1).getStartDateTime()); + } + + /** + * Test the edit command for an invalid index. An exception should be thrown. + */ + @Test + void execute_invalidIndex_exceptionThrown() { + EditActivityCommand editActivityCommand = new EditActivityCommand(3, activityChanges, Run.class); + assertThrows(AthletiException.class, () -> editActivityCommand.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/activity/EditActivityGoalCommandTest.java b/src/test/java/athleticli/commands/activity/EditActivityGoalCommandTest.java new file mode 100644 index 0000000000..341676f81f --- /dev/null +++ b/src/test/java/athleticli/commands/activity/EditActivityGoalCommandTest.java @@ -0,0 +1,55 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.ActivityGoalList; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class EditActivityGoalCommandTest { + + private Data data; + + @BeforeEach + void setUp() { + data = new Data(); + ActivityGoalList activityGoals = data.getActivityGoals(); + ActivityGoal goal1 = new ActivityGoal(ActivityGoal.TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10); + ActivityGoal goal2 = new ActivityGoal(ActivityGoal.TimeSpan.MONTHLY, ActivityGoal.GoalType.DURATION, + ActivityGoal.Sport.CYCLING, 20); + ActivityGoal goal3 = new ActivityGoal(ActivityGoal.TimeSpan.YEARLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.SWIMMING, 30); + ActivityGoal goal4 = new ActivityGoal(ActivityGoal.TimeSpan.DAILY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.GENERAL, 40); + activityGoals.add(goal1); + activityGoals.add(goal2); + activityGoals.add(goal3); + activityGoals.add(goal4); + } + + @Test + void execute_existingActivityGoal_editsActivityGoal() throws athleticli.exceptions.AthletiException { + ActivityGoal goal = new ActivityGoal(athleticli.data.activity.ActivityGoal.TimeSpan.WEEKLY, + athleticli.data.activity.ActivityGoal.GoalType.DISTANCE, + athleticli.data.activity.ActivityGoal.Sport.RUNNING, 100); + EditActivityGoalCommand command = new EditActivityGoalCommand(goal); + String[] expected = + new String[]{athleticli.ui.Message.MESSAGE_ACTIVITY_GOAL_EDITED, goal.toString(data)}; + String[] actual = command.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_nonExistingActivityGoal_throwsAthletiException() { + ActivityGoal goal = new ActivityGoal(athleticli.data.activity.ActivityGoal.TimeSpan.YEARLY, + athleticli.data.activity.ActivityGoal.GoalType.DISTANCE, + athleticli.data.activity.ActivityGoal.Sport.RUNNING, 100); + EditActivityGoalCommand command = new EditActivityGoalCommand(goal); + assertThrows(AthletiException.class, () -> command.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/activity/FindActivityCommandTest.java b/src/test/java/athleticli/commands/activity/FindActivityCommandTest.java new file mode 100644 index 0000000000..8c14754ebd --- /dev/null +++ b/src/test/java/athleticli/commands/activity/FindActivityCommandTest.java @@ -0,0 +1,66 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityList; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the FindActivityCommand class. + */ +class FindActivityCommandTest { + private static final String CAPTION = "Night Run"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, 21); + private Data data; + private Activity activity; + + /** + * Sets up the activity and data to be used in the tests. + */ + @BeforeEach + void setUp() { + activity = new Activity(CAPTION, DURATION, DISTANCE, DATE); + data = new Data(); + ActivityList activities = data.getActivities(); + activities.add(activity); + } + + /** + * Tests that the correct message is returned when there are matching activities. + * + * @throws AthletiException If there is an error in the execution of the command. + */ + @Test + void execute_matchingDate_returnsMatchingActivity() throws AthletiException { + String[] expected = {"I've found these activities:", activity.toString()}; + FindActivityCommand findActivityCommand = new FindActivityCommand(DATE.toLocalDate()); + String[] actual = findActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + /** + * Tests that the correct message is returned when there are no matching activities. + * + * @throws AthletiException If there is an error in the execution of the command. + */ + @Test + void execute_noMatchingDate_returnsNoMatchingActivityMessage() throws AthletiException { + String[] expected = {"I've found these activities:"}; + FindActivityCommand findActivityCommand = new FindActivityCommand(DATE.toLocalDate().plusDays(1)); + String[] actual = findActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } +} diff --git a/src/test/java/athleticli/commands/activity/ListActivityCommandTest.java b/src/test/java/athleticli/commands/activity/ListActivityCommandTest.java new file mode 100644 index 0000000000..c147e58d58 --- /dev/null +++ b/src/test/java/athleticli/commands/activity/ListActivityCommandTest.java @@ -0,0 +1,98 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityList; +import athleticli.ui.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the ListActivityCommand class. + */ +class ListActivityCommandTest { + private static final String CAPTION = "Night Run"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, 21); + private Data data; + + /** + * Sets up the sample data for each test. + */ + @BeforeEach + void setUp() { + Activity activity = new Activity(CAPTION, DURATION, DISTANCE, DATE); + AddActivityCommand addActivityCommand = new AddActivityCommand(activity); + data = new Data(); + // execute twice for 2 activities + addActivityCommand.execute(data); + addActivityCommand.execute(data); + } + + /** + * Tests the execution method of the ListActivityCommand class. It should print a short list of activities. + */ + @Test + void execute_detailedFalse_printsShortList() { + ListActivityCommand listActivityCommand = new ListActivityCommand(false); + String[] expected = {"These are the activities you have tracked so far:", "1." + new Activity(CAPTION, DURATION, + DISTANCE, DATE), "2." + new Activity(CAPTION, DURATION, DISTANCE, DATE), + Message.MESSAGE_ACTIVITY_LIST_END}; + String[] actual = listActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + /** + * Tests the execution method of the ListActivityCommand class. It should print a detailed list of activities. + */ + @Test + void execute_detailedTrue_printsDetailedList() { + ListActivityCommand listActivityCommand = new ListActivityCommand(true); + ActivityList activities = data.getActivities(); + String[] expected = listActivityCommand.printDetailedList(activities, activities.size()); + String[] actual = listActivityCommand.execute(data); + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + /** + * Tests the printList method. It should print a short list of activities. + */ + @Test + void printList_validInput() { + ActivityList activities = data.getActivities(); + ListActivityCommand listActivityCommand = new ListActivityCommand(false); + String[] actual = listActivityCommand.printList(activities, activities.size()); + String[] expected = {"These are the activities you have tracked so far:", "1." + new Activity(CAPTION, DURATION, + DISTANCE, DATE), "2." + new Activity(CAPTION, DURATION, DISTANCE, DATE), + Message.MESSAGE_ACTIVITY_LIST_END}; + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + /** + * Tests the printDetailedList method. It should print a detailed list of activities. + */ + @Test + void printDetailedList() { + ActivityList activities = data.getActivities(); + ListActivityCommand listActivityCommand = new ListActivityCommand(true); + String[] actual = listActivityCommand.printDetailedList(activities, activities.size()); + String[] expected = {"These are the activities you have tracked so far:", new Activity(CAPTION, DURATION, + DISTANCE, DATE).toDetailedString(), + new Activity(CAPTION, DURATION, DISTANCE, DATE).toDetailedString()}; + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } +} diff --git a/src/test/java/athleticli/commands/activity/ListActivityGoalCommandTest.java b/src/test/java/athleticli/commands/activity/ListActivityGoalCommandTest.java new file mode 100644 index 0000000000..8d1a235db1 --- /dev/null +++ b/src/test/java/athleticli/commands/activity/ListActivityGoalCommandTest.java @@ -0,0 +1,55 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.ActivityGoalList; +import athleticli.ui.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ListActivityGoalCommandTest { + + private ListActivityGoalCommand command; + private Data data; + + @BeforeEach + void setUp() { + command = new ListActivityGoalCommand(); + data = new Data(); + } + + @Test + void execute_noActivityGoal_returnsNoActivityGoalMessage() { + String[] result = command.execute(data); + assertEquals(Message.MESSAGE_ACTIVITY_GOAL_LIST, result[0]); + } + + @Test + void execute_existingActivityGoal_returnsActivityGoalList() { + ActivityGoalList activityGoals = data.getActivityGoals(); + ActivityGoal goal1 = new ActivityGoal(ActivityGoal.TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10); + ActivityGoal goal2 = new ActivityGoal(ActivityGoal.TimeSpan.MONTHLY, ActivityGoal.GoalType.DURATION, + ActivityGoal.Sport.CYCLING, 20); + ActivityGoal goal3 = new ActivityGoal(ActivityGoal.TimeSpan.YEARLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.SWIMMING, 30); + ActivityGoal goal4 = new ActivityGoal(ActivityGoal.TimeSpan.DAILY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.GENERAL, 40); + activityGoals.add(goal1); + activityGoals.add(goal2); + activityGoals.add(goal3); + activityGoals.add(goal4); + String[] actual = command.execute(data); + String[] expected = { + Message.MESSAGE_ACTIVITY_GOAL_LIST, + "1. " + goal1.toString(data), + "2. " + goal2.toString(data), + "3. " + goal3.toString(data), + "4. " + goal4.toString(data) + }; + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/activity/SetActivityGoalCommandTest.java b/src/test/java/athleticli/commands/activity/SetActivityGoalCommandTest.java new file mode 100644 index 0000000000..1fd80e819f --- /dev/null +++ b/src/test/java/athleticli/commands/activity/SetActivityGoalCommandTest.java @@ -0,0 +1,63 @@ +package athleticli.commands.activity; + +import athleticli.data.Data; +import athleticli.data.Goal.TimeSpan; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.Run; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SetActivityGoalCommandTest { + private SetActivityGoalCommand setActivityGoalCommand; + private Data data; + private ActivityGoal activityGoal; + + @BeforeEach + void setUp() { + data = new Data(); + + ActivityGoal.GoalType goalType = ActivityGoal.GoalType.DISTANCE; + ActivityGoal.Sport sport = ActivityGoal.Sport.RUNNING; + TimeSpan period = TimeSpan.WEEKLY; + LocalDate date = LocalDate.now(); + activityGoal = new ActivityGoal(period, goalType, sport, 10000); + + setActivityGoalCommand = new SetActivityGoalCommand(activityGoal); + + String caption = "Sunday = Runday"; + int distance = 3000; + LocalTime duration = LocalTime.of(1, 24); + Run run = new Run(caption, duration, distance, LocalDateTime.now(), 0); + + AddActivityCommand addActivityCommand = new AddActivityCommand(run); + addActivityCommand.execute(data); + } + + @Test + void execute_noDuplicate_executed() throws AthletiException { + String[] actual = setActivityGoalCommand.execute(data); + String[] expected = {"Alright, I've added this activity goal:", activityGoal.toString(data)}; + for (int i = 0; i < actual.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + @Test + void execute_duplicate_exceptionThrown() { + try { + setActivityGoalCommand.execute(data); + setActivityGoalCommand.execute(data); + } catch (AthletiException e) { + String expected = "You already have a goal for this sport, type and period! Please edit the existing " + + "goal instead."; + assertEquals(expected, e.getMessage()); + } + } +} diff --git a/src/test/java/athleticli/commands/diet/AddDietCommandTest.java b/src/test/java/athleticli/commands/diet/AddDietCommandTest.java new file mode 100644 index 0000000000..979c27126b --- /dev/null +++ b/src/test/java/athleticli/commands/diet/AddDietCommandTest.java @@ -0,0 +1,40 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + + +/** + * Tests the add diet commands provided by the user. + */ +public class AddDietCommandTest { + private static final int CALORIES = 100; + private static final int PROTEIN = 20; + private static final int CARB = 30; + private static final int FAT = 40; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2020, 10, 10, 10, 10); + private Diet diet; + private AddDietCommand addDietCommand; + private Data data; + + @BeforeEach + void setUp() { + diet = new Diet(CALORIES, PROTEIN, CARB, FAT, DATE_TIME); + addDietCommand = new AddDietCommand(diet); + data = new Data(); + } + + @Test + void execute() { + String[] expected = {"Well done! I've added this diet:", diet.toString(), + "Now you have tracked your " + "first diet. This is just the beginning!"}; + String[] actual = addDietCommand.execute(data); + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/diet/DeleteDietCommandTest.java b/src/test/java/athleticli/commands/diet/DeleteDietCommandTest.java new file mode 100644 index 0000000000..920334845e --- /dev/null +++ b/src/test/java/athleticli/commands/diet/DeleteDietCommandTest.java @@ -0,0 +1,48 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests the delete diet commands provided by the user. + */ +public class DeleteDietCommandTest { + private static final int CALORIES = 100; + private static final int PROTEIN = 20; + private static final int CARB = 30; + private static final int FAT = 40; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2020, 10, 10, 10, 10); + private Diet diet; + private DeleteDietCommand deleteDietCommand; + private Data data; + + @BeforeEach + void setUp() { + diet = new Diet(CALORIES, PROTEIN, CARB, FAT, DATE_TIME); + deleteDietCommand = new DeleteDietCommand(1); + data = new Data(); + data.getDiets().add(diet); + } + + @Test + void execute() throws AthletiException { + String[] expected = {"Noted. I've removed this diet:", diet.toString(), + "Now you have tracked a total of 0 diets. Keep grinding!"}; + String[] actual = deleteDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_invalidIndex_expectException() { + deleteDietCommand = new DeleteDietCommand(2); + assertThrows(AthletiException.class, () -> deleteDietCommand.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/diet/DeleteDietGoalCommandTest.java b/src/test/java/athleticli/commands/diet/DeleteDietGoalCommandTest.java new file mode 100644 index 0000000000..b3076a4c28 --- /dev/null +++ b/src/test/java/athleticli/commands/diet/DeleteDietGoalCommandTest.java @@ -0,0 +1,65 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.HealthyDietGoal; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +class DeleteDietGoalCommandTest { + + private Data data; + private DietGoal dietGoalFat; + private ArrayList filledInputDietGoals; + + @BeforeEach + void setUp() { + data = new Data(); + + dietGoalFat = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "fat", 10000); + + filledInputDietGoals = new ArrayList<>(); + filledInputDietGoals.add(dietGoalFat); + } + + @Test + void execute_deleteOneItemFromFilledDietGoalList_expectCorrectMessage() { + try { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + setDietGoalCommand.execute(data); + System.out.println(data.getDietGoals()); + DeleteDietGoalCommand deleteDietGoalCommand = new DeleteDietGoalCommand(1); + String[] expectedString = new String[]{"The following goal has been deleted:\n", "[HEALTHY] " + + "WEEKLY fat intake progress: (0/10000)\n",}; + assertArrayEquals(expectedString, deleteDietGoalCommand.execute(data)); + } catch (AthletiException e) { + fail(e); + } + } + + @Test + void execute_deleteOneItemFromEmptyDietGoalList_expectAthletiException() { + DeleteDietGoalCommand deleteDietGoalCommand = new DeleteDietGoalCommand(100); + assertThrows(AthletiException.class, () -> deleteDietGoalCommand.execute(data)); + } + + @Test + void execute_integerExceedListSize_expectAthletiException() { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + DeleteDietGoalCommand deleteDietGoalCommand = new DeleteDietGoalCommand(100); + try { + setDietGoalCommand.execute(data); + } catch (AthletiException e) { + fail(); + } + assertThrows(AthletiException.class, () -> deleteDietGoalCommand.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/diet/EditDietCommandTest.java b/src/test/java/athleticli/commands/diet/EditDietCommandTest.java new file mode 100644 index 0000000000..6fd82bd8fb --- /dev/null +++ b/src/test/java/athleticli/commands/diet/EditDietCommandTest.java @@ -0,0 +1,150 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.exceptions.AthletiException; +import athleticli.parser.Parameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* + * Contains the tests for EditDietCommand. + */ +public class EditDietCommandTest { + private static final int CALORIES = 100; + private static final int PROTEIN = 10; + private static final int CARB = 20; + private static final int FAT = 30; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2023, 10, 10, 23, 21); + private static final int INDEX = 1; + + private Data data; + + @BeforeEach + void setUp() { + Diet diet = new Diet(CALORIES, PROTEIN, CARB, FAT, DATE_TIME); + data = new Data(); + data.getDiets().add(diet); + } + + @Test + void execute_validIndex_dietEdited() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.CALORIES_SEPARATOR, "200"); + dietMap.put(Parameter.PROTEIN_SEPARATOR, "20"); + dietMap.put(Parameter.CARB_SEPARATOR, "30"); + dietMap.put(Parameter.FAT_SEPARATOR, "40"); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + Diet newDiet = new Diet(200, 20, 30, 40, LocalDateTime.of(2021, 10, 10, 23, 21)); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_validIndex_dietEditedNoCaloriesGiven() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.PROTEIN_SEPARATOR, "20"); + dietMap.put(Parameter.CARB_SEPARATOR, "30"); + dietMap.put(Parameter.FAT_SEPARATOR, "40"); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + Diet newDiet = new Diet(CALORIES, 20, 30, 40, LocalDateTime.of(2021, 10, 10, 23, 21)); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_validIndex_dietEditedNoProteinGiven() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.CALORIES_SEPARATOR, "200"); + dietMap.put(Parameter.CARB_SEPARATOR, "30"); + dietMap.put(Parameter.FAT_SEPARATOR, "40"); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + Diet newDiet = new Diet(200, PROTEIN, 30, 40, LocalDateTime.of(2021, 10, 10, 23, 21)); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_validIndex_dietEditedNoCarbGiven() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.CALORIES_SEPARATOR, "200"); + dietMap.put(Parameter.PROTEIN_SEPARATOR, "20"); + dietMap.put(Parameter.FAT_SEPARATOR, "40"); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + Diet newDiet = new Diet(200, 20, CARB, 40, LocalDateTime.of(2021, 10, 10, 23, 21)); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_validIndex_dietEditedNoFatGiven() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.CALORIES_SEPARATOR, "200"); + dietMap.put(Parameter.PROTEIN_SEPARATOR, "20"); + dietMap.put(Parameter.CARB_SEPARATOR, "30"); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + Diet newDiet = new Diet(200, 20, 30, FAT, LocalDateTime.of(2021, 10, 10, 23, 21)); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_validIndex_dietEditedNoCaloriesProteinCarbFatGiven() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + Diet newDiet = new Diet(CALORIES, PROTEIN, CARB, FAT, LocalDateTime.of(2021, 10, 10, 23, 21)); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_validIndex_dietEditedNoDateTimeGiven() throws AthletiException { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.CALORIES_SEPARATOR, "200"); + dietMap.put(Parameter.PROTEIN_SEPARATOR, "20"); + dietMap.put(Parameter.CARB_SEPARATOR, "30"); + dietMap.put(Parameter.FAT_SEPARATOR, "40"); + Diet newDiet = new Diet(200, 20, 30, 40, DATE_TIME); + EditDietCommand editDietCommand = new EditDietCommand(INDEX, dietMap); + editDietCommand.execute(data); + String[] expected = {"Ok, I've updated this diet:", newDiet.toString()}; + String[] actual = editDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_invalidIndex_exceptionThrown() { + HashMap dietMap = new HashMap<>(); + dietMap.put(Parameter.CALORIES_SEPARATOR, "200"); + dietMap.put(Parameter.PROTEIN_SEPARATOR, "20"); + dietMap.put(Parameter.CARB_SEPARATOR, "30"); + dietMap.put(Parameter.FAT_SEPARATOR, "40"); + dietMap.put(Parameter.DATETIME_SEPARATOR, "2021-10-10 23:21"); + EditDietCommand editDietCommand = new EditDietCommand(2, dietMap); + assertThrows(AthletiException.class, () -> editDietCommand.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/diet/EditDietGoalCommandTest.java b/src/test/java/athleticli/commands/diet/EditDietGoalCommandTest.java new file mode 100644 index 0000000000..881f9c17cb --- /dev/null +++ b/src/test/java/athleticli/commands/diet/EditDietGoalCommandTest.java @@ -0,0 +1,104 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.HealthyDietGoal; +import athleticli.data.diet.UnhealthyDietGoal; +import athleticli.exceptions.AthletiException; +import athleticli.parser.Parameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class EditDietGoalCommandTest { + + private ArrayList emptyInputDietGoals; + private ArrayList filledInputDietGoals; + private ArrayList filledValidUpdatedDietGoals; + private ArrayList filledInvalidGoalTypeDietGoals; + private ArrayList filledInconsistentTargetValueWithTimeSpanDietGoals; + private DietGoal dietGoalCarbWeekly; + private DietGoal dietGoalFatWeekly; + private DietGoal newDietGoalFatWeekly; + private DietGoal dietGoalFatDaily; + private DietGoal unhealthyDietGoalFatDaily; + private DietGoal newDietGoalFatWeeklySmall; + private Data data; + + @BeforeEach + void setUp() { + data = new Data(); + + dietGoalCarbWeekly = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "carb", 10000); + dietGoalFatWeekly = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "fat", 10000); + dietGoalFatDaily = new HealthyDietGoal(Goal.TimeSpan.DAILY, "fat", 100); + newDietGoalFatWeekly = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "fat", 100); + newDietGoalFatWeeklySmall = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "fat", 1); + unhealthyDietGoalFatDaily = new UnhealthyDietGoal(Goal.TimeSpan.WEEKLY, + Parameter.NUTRIENTS_FAT, 10000); + + emptyInputDietGoals = new ArrayList<>(); + filledInputDietGoals = new ArrayList<>(); + filledValidUpdatedDietGoals = new ArrayList<>(); + filledInvalidGoalTypeDietGoals = new ArrayList<>(); + + filledInputDietGoals.add(dietGoalFatWeekly); + filledInputDietGoals.add(dietGoalCarbWeekly); + + filledValidUpdatedDietGoals.add(newDietGoalFatWeekly); + filledInvalidGoalTypeDietGoals.add(unhealthyDietGoalFatDaily); + + filledInconsistentTargetValueWithTimeSpanDietGoals = new ArrayList<>(); + filledInconsistentTargetValueWithTimeSpanDietGoals.add(newDietGoalFatWeeklySmall); + + } + + @Test + void execute_emptyInputList_expectCorrectMessage() throws AthletiException { + EditDietGoalCommand editDietGoalCommand = new EditDietGoalCommand(emptyInputDietGoals); + String[] expectedString = {"These are your goal(s):\n", "", "Now you have 0 diet goal(s)."}; + String[] actualString = editDietGoalCommand.execute(data); + assertArrayEquals(expectedString, actualString); + + } + + @Test + void execute_oneNotExistedDietGoal_expectError() { + EditDietGoalCommand editDietGoalCommand = new EditDietGoalCommand(filledInputDietGoals); + assertThrows(AthletiException.class, () -> editDietGoalCommand.execute(data)); + } + + @Test + void execute_invalidDietGoalType_expectError() throws AthletiException { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + EditDietGoalCommand editDietGoalCommand = new EditDietGoalCommand(filledInvalidGoalTypeDietGoals); + setDietGoalCommand.execute(data); + assertThrows(AthletiException.class, () -> editDietGoalCommand.execute(data)); + } + @Test + void execute_inconsistentDietGoal_expectError() throws AthletiException { + filledInputDietGoals.add(dietGoalFatDaily); + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + EditDietGoalCommand editDietGoalCommand = + new EditDietGoalCommand(filledInconsistentTargetValueWithTimeSpanDietGoals); + setDietGoalCommand.execute(data); + assertThrows(AthletiException.class, () -> editDietGoalCommand.execute(data)); + } + + @Test + void execute_changeOneExistingInputDietGoal_expectCorrectMessage() throws AthletiException { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + EditDietGoalCommand editDietGoalCommand = new EditDietGoalCommand(filledValidUpdatedDietGoals); + String[] expectedString = {"These are your goal(s):\n", "\t1. [HEALTHY] " + + "WEEKLY fat intake progress: (0/100)\n\n" + "\t2. [HEALTHY] " + + "WEEKLY carb intake progress: (0/10000)\n", "Now you have 2 diet goal(s)."}; + setDietGoalCommand.execute(data); + assertArrayEquals(expectedString, editDietGoalCommand.execute(data)); + + } +} diff --git a/src/test/java/athleticli/commands/diet/FindDietCommandTest.java b/src/test/java/athleticli/commands/diet/FindDietCommandTest.java new file mode 100644 index 0000000000..bc86a7bb48 --- /dev/null +++ b/src/test/java/athleticli/commands/diet/FindDietCommandTest.java @@ -0,0 +1,54 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + + +public class FindDietCommandTest { + private FindDietCommand findDietCommand; + private Data data; + + @BeforeEach + void setUp() { + Diet diet1 = new Diet(100, 20, 30, 40, LocalDateTime.of(2020, 10, 10, 10, 10)); + Diet diet2 = new Diet(200, 50, 35, 20, LocalDateTime.of(2023, 1, 10, 10, 11)); + Diet diet3 = new Diet(100, 20, 30, 40, LocalDateTime.of(2023, 1, 10, 10, 11)); + data = new Data(); + data.getDiets().add(diet1); + data.getDiets().add(diet2); + data.getDiets().add(diet3); + } + + @Test + void execute_findTwoDiets() throws AthletiException { + findDietCommand = new FindDietCommand(LocalDate.of(2023, 1, 10)); + String[] expected = {"I've found these diets:", data.getDiets().get(1).toString(), + data.getDiets().get(2).toString()}; + String[] actual = findDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_findOneDiet() throws AthletiException { + findDietCommand = new FindDietCommand(LocalDate.of(2020, 10, 10)); + String[] expected = {"I've found these diets:", data.getDiets().get(0).toString()}; + String[] actual = findDietCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + void execute_findNoDiets() throws AthletiException { + findDietCommand = new FindDietCommand(LocalDate.of(2020, 10, 11)); + String[] expected = {"I've found these diets:"}; + String[] actual = findDietCommand.execute(data); + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/diet/ListDietCommandTest.java b/src/test/java/athleticli/commands/diet/ListDietCommandTest.java new file mode 100644 index 0000000000..3d5b491f08 --- /dev/null +++ b/src/test/java/athleticli/commands/diet/ListDietCommandTest.java @@ -0,0 +1,40 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.diet.Diet; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * Tests the list diet commands provided by the user. + */ +public class ListDietCommandTest { + private static final int CALORIES = 100; + private static final int PROTEIN = 20; + private static final int CARB = 30; + private static final int FAT = 40; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2020, 10, 10, 10, 10); + private Diet diet; + private ListDietCommand listDietCommand; + private Data data; + + @BeforeEach + void setUp() { + diet = new Diet(CALORIES, PROTEIN, CARB, FAT, DATE_TIME); + listDietCommand = new ListDietCommand(); + data = new Data(); + data.getDiets().add(diet); + } + + @Test + void execute() { + String[] expected = {"Here are the diets in your list:", "\t1. " + diet.toString(), + "Now you have tracked a total of 1 diets. Keep grinding!"}; + String[] actual = listDietCommand.execute(data); + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/diet/ListDietGoalCommandTest.java b/src/test/java/athleticli/commands/diet/ListDietGoalCommandTest.java new file mode 100644 index 0000000000..8d23ba9f7a --- /dev/null +++ b/src/test/java/athleticli/commands/diet/ListDietGoalCommandTest.java @@ -0,0 +1,51 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.HealthyDietGoal; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class ListDietGoalCommandTest { + + private ArrayList filledInputDietGoals; + private DietGoal dietGoalFat; + private Data data; + + @BeforeEach + void setUp() { + data = new Data(); + + dietGoalFat = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "fat", 10000); + + filledInputDietGoals = new ArrayList<>(); + filledInputDietGoals.add(dietGoalFat); + } + + @Test + void execute_emptyInputList_returnNoDietGoalMessage() { + String[] expectedString = {"There are no goals at the moment. Add a diet goal to start."}; + ListDietGoalCommand listDietGoalCommand = new ListDietGoalCommand(); + assertArrayEquals(expectedString, listDietGoalCommand.execute(data)); + } + + @Test + void execute_filledInputList_returnDietGoalPresentMessage() { + try { + String[] expectedString = {"These are your goal(s):\n", "\t1. [HEALTHY] WEEKLY " + + "fat intake progress: (0/10000)\n", "Now you have 1 diet goal(s)."}; + ListDietGoalCommand listDietGoalCommand = new ListDietGoalCommand(); + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + setDietGoalCommand.execute(data); + assertArrayEquals(expectedString, listDietGoalCommand.execute(data)); + } catch (AthletiException e) { + assert (false); + } + } +} diff --git a/src/test/java/athleticli/commands/diet/SetDietGoalCommandTest.java b/src/test/java/athleticli/commands/diet/SetDietGoalCommandTest.java new file mode 100644 index 0000000000..0e11856eaa --- /dev/null +++ b/src/test/java/athleticli/commands/diet/SetDietGoalCommandTest.java @@ -0,0 +1,105 @@ +package athleticli.commands.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.HealthyDietGoal; +import athleticli.data.diet.UnhealthyDietGoal; +import athleticli.exceptions.AthletiException; + +import athleticli.parser.Parameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +class SetDietGoalCommandTest { + + private ArrayList emptyInputDietGoals; + private ArrayList filledInputDietGoals; + private ArrayList filledUnhealthyInputDietGoals; + private ArrayList filledNewHealthyInputDietGoals; + private DietGoal dietGoalFatWeekly; + private DietGoal dietGoalFatDaily; + private DietGoal dietGoalCarbWeekly; + private DietGoal unhealthyDietGoalFatDaily; + private Data data; + + @BeforeEach + void setUp() { + emptyInputDietGoals = new ArrayList<>(); + filledInputDietGoals = new ArrayList<>(); + filledUnhealthyInputDietGoals = new ArrayList<>(); + filledNewHealthyInputDietGoals = new ArrayList<>(); + + dietGoalFatWeekly = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, Parameter.NUTRIENTS_FAT, 10000); + dietGoalFatDaily = new HealthyDietGoal(Goal.TimeSpan.DAILY, Parameter.NUTRIENTS_FAT, 1000000); + dietGoalCarbWeekly = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, Parameter.NUTRIENTS_CARB, 10000); + unhealthyDietGoalFatDaily = new UnhealthyDietGoal(Goal.TimeSpan.DAILY, + Parameter.NUTRIENTS_FAT, 10000); + data = new Data(); + + filledInputDietGoals.add(dietGoalFatWeekly); + filledInputDietGoals.add(dietGoalCarbWeekly); + filledUnhealthyInputDietGoals.add(unhealthyDietGoalFatDaily); + filledNewHealthyInputDietGoals.add(dietGoalFatDaily); + } + + @Test + void execute_emptyInputList_expectCorrectMessage() { + try { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(emptyInputDietGoals); + String[] expectedString = {"These are your goal(s):\n", "", "Now you have 0 diet goal(s)."}; + String[] actualString = setDietGoalCommand.execute(data); + assertArrayEquals(expectedString, actualString); + } catch (AthletiException e) { + fail(e); + } + } + + @Test + void execute_oneNewInputDietGoal_expectCorrectMessage() { + try { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + String[] expectedString = {"These are your goal(s):\n", "\t1. [HEALTHY] " + + "WEEKLY fat intake progress: (0/10000)\n\n" + "\t2. [HEALTHY] " + + "WEEKLY carb intake progress: (0/10000)\n", "Now you have 2 diet goal(s)."}; + String[] actualString = setDietGoalCommand.execute(data); + assertArrayEquals(expectedString, actualString); + } catch (AthletiException e) { + fail(e); + } + } + + @Test + void execute_dailyTargetValueGreaterThanOrEqualToWeekly_expectAthletiException() throws AthletiException { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + SetDietGoalCommand setDailyDietGoalCommand = new SetDietGoalCommand(filledNewHealthyInputDietGoals); + setDietGoalCommand.execute(data); + + assertThrows(AthletiException.class, () -> setDailyDietGoalCommand.execute(data)); + } + + @Test + void execute_conflictingDietGoalTypes_expectAthletiException() throws AthletiException { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + SetDietGoalCommand setDietGoalCommandUnhealthyGoals = new SetDietGoalCommand(filledUnhealthyInputDietGoals); + setDietGoalCommandUnhealthyGoals.execute(data); + assertThrows(AthletiException.class, () -> setDietGoalCommand.execute(data)); + } + + @Test + void execute_oneExistingInputDietGoal_expectAthletiException() { + SetDietGoalCommand setDietGoalCommand = new SetDietGoalCommand(filledInputDietGoals); + try { + setDietGoalCommand.execute(data); + } catch (AthletiException e) { + fail(e); + } + assertThrows(AthletiException.class, () -> setDietGoalCommand.execute(data)); + } +} diff --git a/src/test/java/athleticli/commands/sleep/AddSleepCommandTest.java b/src/test/java/athleticli/commands/sleep/AddSleepCommandTest.java new file mode 100644 index 0000000000..53906f4cca --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/AddSleepCommandTest.java @@ -0,0 +1,59 @@ +package athleticli.commands.sleep; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import athleticli.data.Data; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; +import athleticli.data.sleep.Sleep; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AddSleepCommandTest { + + private Data data; + private Sleep sleep; + private Sleep sleep2; + private AddSleepCommand addSleepCommand; + private AddSleepCommand addSleepCommand2; + + @BeforeEach + public void setup() throws AthletiException { + data = new Data(); + sleep = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + sleep2 = new Sleep(LocalDateTime.of(2023, 10, 18, 22, 0), + LocalDateTime.of(2023, 10, 19, 6, 0)); + addSleepCommand = new AddSleepCommand(sleep); + addSleepCommand2 = new AddSleepCommand(sleep2); + data.setSleeps(new SleepList()); + } + + @Test + public void testExecuteWithValidInput() throws AthletiException { + String[] expected = { + "Well done! I've added this sleep record:", + "[Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 18, 2023 at 6:00 AM | Sleeping Duration: 8 Hours ", + "You have tracked your first sleep record. This is just the beginning!" + }; + String[] actual = addSleepCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + public void testExecuteCountingSleepRecords() throws AthletiException { + String[] expected = { + "Well done! I've added this sleep record:", + "[Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 18, 2023 at 6:00 AM | Sleeping Duration: 8 Hours ", + "You have tracked a total of 2 sleep records. Keep it up!" + }; + addSleepCommand2.execute(data); + String[] actual2 = addSleepCommand.execute(data); + assertArrayEquals(expected, actual2); + } +} diff --git a/src/test/java/athleticli/commands/sleep/DeleteSleepCommandTest.java b/src/test/java/athleticli/commands/sleep/DeleteSleepCommandTest.java new file mode 100644 index 0000000000..bc99ef7ac8 --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/DeleteSleepCommandTest.java @@ -0,0 +1,67 @@ +package athleticli.commands.sleep; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; + +import java.time.LocalDateTime; + +public class DeleteSleepCommandTest { + + private Data data; + private Sleep sleep1; + private Sleep sleep2; + + @BeforeEach + public void setup() throws AthletiException { + data = new Data(); + SleepList sleepList = new SleepList(); + sleep1 = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + sleep2 = new Sleep(LocalDateTime.of(2023, 10, 18, 22, 0), + LocalDateTime.of(2023, 10, 19, 6, 0)); + sleepList.add(sleep1); + sleepList.add(sleep2); + data.setSleeps(sleepList); + } + + @Test + public void testExecuteWithValidIndex() throws AthletiException { + DeleteSleepCommand command = new DeleteSleepCommand(1); + String[] expected = { + "Gotcha, I've deleted this sleep record:", + "[Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 18, 2023 at 6:00 AM | Sleeping Duration: 8 Hours ", + "You have tracked a total of 1 sleep records. Keep it up!" + }; + String[] actual = command.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + public void testExecuteWithInvalidIndex() throws AthletiException { + DeleteSleepCommand commandNegative = new DeleteSleepCommand(-1); + assertThrows(AthletiException.class, () -> commandNegative.execute(data)); + + DeleteSleepCommand commandZero = new DeleteSleepCommand(0); + assertThrows(AthletiException.class, () -> commandZero.execute(data)); + + DeleteSleepCommand commandBeyond = new DeleteSleepCommand(3); // Only 2 records in the list. + assertThrows(AthletiException.class, () -> commandBeyond.execute(data)); + } + + @Test + public void testExecuteWithEmptyList() throws AthletiException { + data.setSleeps(new SleepList()); // Empty list + DeleteSleepCommand command = new DeleteSleepCommand(1); + assertThrows(AthletiException.class, () -> command.execute(data)); + } + +} diff --git a/src/test/java/athleticli/commands/sleep/EditSleepCommandTest.java b/src/test/java/athleticli/commands/sleep/EditSleepCommandTest.java new file mode 100644 index 0000000000..380c847065 --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/EditSleepCommandTest.java @@ -0,0 +1,57 @@ +package athleticli.commands.sleep; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; + +public class EditSleepCommandTest { + + private Data data; + private Sleep sleep1; + private Sleep sleep2; + + @BeforeEach + public void setup() throws AthletiException { + data = new Data(); + SleepList sleepList = new SleepList(); + sleep1 = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + sleep2 = new Sleep(LocalDateTime.of(2023, 10, 18, 22, 0), + LocalDateTime.of(2023, 10, 19, 6, 0)); + sleepList.add(sleep1); + data.setSleeps(sleepList); + } + + @Test + public void testExecuteWithValidIndex() throws AthletiException { + EditSleepCommand command = new EditSleepCommand(1, sleep2); + String[] expected = { + "Alright, I've changed this sleep record:", + "original: " + sleep1.toString(), + "new: " + sleep2.toString(), + }; + assertArrayEquals(expected, command.execute(data)); + } + + @Test + public void testExecuteWithInvalidIndex() { + EditSleepCommand commandNegative = new EditSleepCommand(-1, sleep1); + assertThrows(AthletiException.class, () -> commandNegative.execute(data)); + + EditSleepCommand commandZero = new EditSleepCommand(0, sleep1); + assertThrows(AthletiException.class, () -> commandZero.execute(data)); + + EditSleepCommand commandBeyond = new EditSleepCommand(3, sleep1); // Only 2 records in the list. + assertThrows(AthletiException.class, () -> commandBeyond.execute(data)); + } +} + diff --git a/src/test/java/athleticli/commands/sleep/EditSleepGoalCommandTest.java b/src/test/java/athleticli/commands/sleep/EditSleepGoalCommandTest.java new file mode 100644 index 0000000000..f9a78e8db0 --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/EditSleepGoalCommandTest.java @@ -0,0 +1,55 @@ +package athleticli.commands.sleep; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import athleticli.data.Data; +import athleticli.data.sleep.SleepGoal; +import athleticli.exceptions.AthletiException; + + +public class EditSleepGoalCommandTest { + + private Data data; + private SleepGoal sleepGoal; + private EditSleepGoalCommand editSleepGoalCommand; + + @BeforeEach + public void setUp() { + data = new Data(); + SleepGoal.GoalType goalType = SleepGoal.GoalType.DURATION; + SleepGoal.TimeSpan timeSpan = SleepGoal.TimeSpan.WEEKLY; + int initialGoalValue = 7; // Original goal value + int newGoalValue = 8; // New goal value for editing + + SleepGoal initialSleepGoal = new SleepGoal(goalType, timeSpan, initialGoalValue); + data.getSleepGoals().add(initialSleepGoal); + + sleepGoal = new SleepGoal(goalType, timeSpan, newGoalValue); + editSleepGoalCommand = new EditSleepGoalCommand(sleepGoal); + } + + @Test + public void execute_goalExists_edited() throws AthletiException { + String[] actual = editSleepGoalCommand.execute(data); + String[] expected = {"Alright, I've edited this sleep goal:", sleepGoal.toString(data)}; + assertArrayEquals(expected, actual); + } + + @Test + public void execute_goalDoesNotExist_exceptionThrown() { + SleepGoal nonExistingSleepGoal = new SleepGoal(SleepGoal.GoalType.DURATION, SleepGoal.TimeSpan.MONTHLY, 9); + EditSleepGoalCommand commandWithNonExistingGoal = new EditSleepGoalCommand(nonExistingSleepGoal); + + AthletiException thrown = assertThrows(AthletiException.class, () -> { + commandWithNonExistingGoal.execute(data); + }); + + String expectedMessage = "No such goal exists."; + assertEquals(expectedMessage, thrown.getMessage()); + } +} diff --git a/src/test/java/athleticli/commands/sleep/FindSleepCommandTest.java b/src/test/java/athleticli/commands/sleep/FindSleepCommandTest.java new file mode 100644 index 0000000000..d3f78612e3 --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/FindSleepCommandTest.java @@ -0,0 +1,74 @@ +package athleticli.commands.sleep; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.time.LocalDateTime; +import java.time.LocalDate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; + +public class FindSleepCommandTest { + + private Data data; + private Sleep sleep1; + private Sleep sleep2; + private Sleep sleep3; + + @BeforeEach + public void setup() throws AthletiException { + data = new Data(); + SleepList sleepList = new SleepList(); + sleep1 = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + sleep2 = new Sleep(LocalDateTime.of(2023, 10, 18, 22, 0), + LocalDateTime.of(2023, 10, 19, 6, 0)); + sleep3 = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 20, 6, 0)); + sleepList.add(sleep1); + sleepList.add(sleep2); + sleepList.add(sleep3); + data.setSleeps(sleepList); + } + + @Test + public void testExecuteWithValidInput_findOneSleep() throws AthletiException { + FindSleepCommand findSleepCommand = new FindSleepCommand(LocalDate.of(2023, 10, 18)); + String[] expected = { + "I've found these sleeps:", + "[Sleep] | Date: 2023-10-18 | Start Time: October 18, 2023 at 10:00 PM " + + "| End Time: October 19, 2023 at 6:00 AM | Sleeping Duration: 8 Hours ", + }; + String[] actual = findSleepCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + public void testExecuteWithValidInput_findTwoSleeps() throws AthletiException { + FindSleepCommand findSleepCommand = new FindSleepCommand(LocalDate.of(2023, 10, 17)); + String[] expected = { + "I've found these sleeps:", + "[Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 18, 2023 at 6:00 AM | Sleeping Duration: 8 Hours ", + "[Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 20, 2023 at 6:00 AM | Sleeping Duration: 2 Days 8 Hours ", + }; + String[] actual = findSleepCommand.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + public void testExecuteWithValidInput_findNoSleeps() throws AthletiException { + FindSleepCommand findSleepCommand = new FindSleepCommand(LocalDate.of(2023, 10, 19)); + String[] expected = { + "I've found these sleeps:", + }; + String[] actual = findSleepCommand.execute(data); + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/sleep/ListSleepCommandTest.java b/src/test/java/athleticli/commands/sleep/ListSleepCommandTest.java new file mode 100644 index 0000000000..dbe6d5bb1a --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/ListSleepCommandTest.java @@ -0,0 +1,58 @@ +package athleticli.commands.sleep; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import athleticli.data.Data; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepList; +import athleticli.exceptions.AthletiException; + +public class ListSleepCommandTest { + + private Data data; + private Sleep sleep1; + private Sleep sleep2; + + @BeforeEach + public void setup() throws AthletiException { + data = new Data(); + SleepList sleepList = new SleepList(); + sleep1 = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + sleep2 = new Sleep(LocalDateTime.of(2023, 10, 18, 22, 0), + LocalDateTime.of(2023, 10, 19, 6, 0)); + sleepList.add(sleep1); + sleepList.add(sleep2); + data.setSleeps(sleepList); + } + + @Test + public void testExecuteWithRecords() { + ListSleepCommand command = new ListSleepCommand(); + String[] expected = { + "Here are the sleep records in your list:\n", + "1. [Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 18, 2023 at 6:00 AM | Sleeping Duration: 8 Hours ", + "2. [Sleep] | Date: 2023-10-18 | Start Time: October 18, 2023 at 10:00 PM " + + "| End Time: October 19, 2023 at 6:00 AM | Sleeping Duration: 8 Hours " + }; + String[] actual = command.execute(data); + assertArrayEquals(expected, actual); + } + + @Test + public void testExecuteWithEmptyList() { + data.setSleeps(new SleepList()); + ListSleepCommand command = new ListSleepCommand(); + String[] expected = { + "You have no sleep records in your list." + }; + String[] actual = command.execute(data); + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/sleep/ListSleepGoalCommandTest.java b/src/test/java/athleticli/commands/sleep/ListSleepGoalCommandTest.java new file mode 100644 index 0000000000..58caefa47c --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/ListSleepGoalCommandTest.java @@ -0,0 +1,51 @@ +package athleticli.commands.sleep; + +import athleticli.data.Data; +import athleticli.data.sleep.SleepGoal; +import athleticli.data.sleep.SleepGoalList; +import athleticli.ui.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ListSleepGoalCommandTest { + + private ListSleepGoalCommand command; + private Data data; + + @BeforeEach + void setup() { + command = new ListSleepGoalCommand(); + data = new Data(); + } + + @Test + void execute_noSleepGoal_returnsNoSleepGoalMessage() { + String[] result = command.execute(data); + assertEquals(Message.MESSAGE_SLEEP_GOAL_LIST, result[0]); + } + + @Test + void execute_existingSleepGoal_returnsSleepGoalList() { + SleepGoalList sleepGoals = data.getSleepGoals(); + SleepGoal goal1 = new SleepGoal(SleepGoal.GoalType.DURATION, SleepGoal.TimeSpan.WEEKLY, 10); + SleepGoal goal2 = new SleepGoal(SleepGoal.GoalType.DURATION, SleepGoal.TimeSpan.MONTHLY, 20); + SleepGoal goal3 = new SleepGoal(SleepGoal.GoalType.DURATION, SleepGoal.TimeSpan.YEARLY, 30); + SleepGoal goal4 = new SleepGoal(SleepGoal.GoalType.DURATION, SleepGoal.TimeSpan.DAILY, 40); + sleepGoals.add(goal1); + sleepGoals.add(goal2); + sleepGoals.add(goal3); + sleepGoals.add(goal4); + String[] actual = command.execute(data); + String[] expected = { + Message.MESSAGE_SLEEP_GOAL_LIST, + "1. " + goal1.toString(data), + "2. " + goal2.toString(data), + "3. " + goal3.toString(data), + "4. " + goal4.toString(data) + }; + assertArrayEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/commands/sleep/SetSleepGoalCommandTest.java b/src/test/java/athleticli/commands/sleep/SetSleepGoalCommandTest.java new file mode 100644 index 0000000000..1b9c148487 --- /dev/null +++ b/src/test/java/athleticli/commands/sleep/SetSleepGoalCommandTest.java @@ -0,0 +1,54 @@ +package athleticli.commands.sleep; + +import athleticli.data.Data; +import athleticli.data.sleep.SleepGoal; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SetSleepGoalCommandTest { + + private SetSleepGoalCommand setSleepGoalCommand; + private Data data; + private SleepGoal sleepGoal; + + @BeforeEach + void setup() { + data = new Data(); + + SleepGoal.GoalType goalType = SleepGoal.GoalType.DURATION; + SleepGoal.TimeSpan timeSpan = SleepGoal.TimeSpan.WEEKLY; + int goalValue = 8; // Representing 8 minutes + + sleepGoal = new SleepGoal(goalType, timeSpan, goalValue); + setSleepGoalCommand = new SetSleepGoalCommand(sleepGoal); + } + + @Test + void execute_noDuplicate_executed() throws AthletiException { + String[] actual = setSleepGoalCommand.execute(data); + String[] expected = {"Alright, I've added this sleep goal:", sleepGoal.toString(data)}; + assertArrayEquals(expected, actual); + } + + @Test + void execute_duplicate_exceptionThrown() throws AthletiException { + // First execution to add the goal + setSleepGoalCommand.execute(data); + + // The second execution should throw an exception + AthletiException thrown = assertThrows(AthletiException.class, () -> { + setSleepGoalCommand.execute(data); + }); + + String expectedMessage = "You already have a goal for this type and period! " + + "Please edit the existing goal instead."; + String actualMessage = thrown.getMessage(); + + assertEquals(expectedMessage, actualMessage); + } +} diff --git a/src/test/java/athleticli/data/GoalTest.java b/src/test/java/athleticli/data/GoalTest.java new file mode 100644 index 0000000000..f6ee73ce07 --- /dev/null +++ b/src/test/java/athleticli/data/GoalTest.java @@ -0,0 +1,34 @@ +package athleticli.data; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.time.LocalDate; +import org.junit.jupiter.api.Test; + +class GoalTest { + + @Test + void checkDate_startDate_returnTrue() { + Goal.TimeSpan timeSpan = Goal.TimeSpan.WEEKLY; + assertTrue(Goal.checkDate(LocalDate.now().minusDays(timeSpan.getDays() - 1), timeSpan)); + } + + @Test + void checkDate_beforeStartDate_returnFalse() { + Goal.TimeSpan timeSpan = Goal.TimeSpan.MONTHLY; + assertFalse(Goal.checkDate(LocalDate.now().minusDays(timeSpan.getDays()), timeSpan)); + } + + @Test + void checkDate_endDate_returnTrue() { + Goal.TimeSpan timeSpan = Goal.TimeSpan.YEARLY; + assertTrue(Goal.checkDate(LocalDate.now(), timeSpan)); + } + + @Test + void checkDate_afterEndDate_returnFalse() { + Goal.TimeSpan timeSpan = Goal.TimeSpan.MONTHLY; + assertFalse(Goal.checkDate(LocalDate.now().plusDays(1), timeSpan)); + } +} diff --git a/src/test/java/athleticli/data/activity/ActivityGoalListTest.java b/src/test/java/athleticli/data/activity/ActivityGoalListTest.java new file mode 100644 index 0000000000..0a0367a762 --- /dev/null +++ b/src/test/java/athleticli/data/activity/ActivityGoalListTest.java @@ -0,0 +1,108 @@ +package athleticli.data.activity; + +import athleticli.data.Goal.TimeSpan; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the ActivityGoalList class. + */ +class ActivityGoalListTest { + private ActivityGoalList activityGoalList; + + /** + * Creates a new ActivityGoalList before each test. + */ + @BeforeEach + void setUp() { + activityGoalList = new ActivityGoalList(); + } + + /** + * Tests the unparsing of an running distance goal. + */ + @Test + void unparse_runningDistanceGoal_unparsed() { + String expected = "sport/RUNNING type/DISTANCE period/WEEKLY target/10000"; + ActivityGoal goal = new ActivityGoal(TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10000); + String actual = activityGoalList.unparse(goal); + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of a swimming duration goal. + */ + @Test + void unparse_swimmingDurationGoal_unparsed() { + String expected = "sport/SWIMMING type/DURATION period/MONTHLY target/120"; + ActivityGoal goal = new ActivityGoal(TimeSpan.MONTHLY, ActivityGoal.GoalType.DURATION, + ActivityGoal.Sport.SWIMMING, 120); + String actual = activityGoalList.unparse(goal); + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of a cycling distance goal. + */ + @Test + void parse_runningDistanceGoal_parsed() throws AthletiException { + ActivityGoal expected = new ActivityGoal(TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10000); + String unparsedActivity = activityGoalList.unparse(expected); + ActivityGoal actual = activityGoalList.parse(unparsedActivity); + assertEquals(expected.getGoalType(), actual.getGoalType()); + assertEquals(expected.getSport(), actual.getSport()); + assertEquals(expected.getTargetValue(), actual.getTargetValue()); + assertEquals(expected.getTimeSpan(), actual.getTimeSpan()); + } + + /** + * Tests the parsing of a swimming duration goal. + * + * @throws AthletiException If the goal is not parsed correctly. + */ + @Test + void parse_swimmingDurationGoal_parsed() throws AthletiException { + ActivityGoal expected = new ActivityGoal(TimeSpan.MONTHLY, ActivityGoal.GoalType.DURATION, + ActivityGoal.Sport.SWIMMING, 120); + String unparsedActivity = activityGoalList.unparse(expected); + ActivityGoal actual = activityGoalList.parse(unparsedActivity); + assertEquals(expected.getGoalType(), actual.getGoalType()); + assertEquals(expected.getSport(), actual.getSport()); + assertEquals(expected.getTargetValue(), actual.getTargetValue()); + assertEquals(expected.getTimeSpan(), actual.getTimeSpan()); + } + + /** + * Tests the find duplicate method when there is no duplicate. It should return false. + */ + @Test + void findDuplicate_noDuplicate_false() { + ActivityGoal goal = new ActivityGoal(TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10000); + activityGoalList.add(goal); + boolean actual = activityGoalList.isDuplicate(ActivityGoal.GoalType.DISTANCE, ActivityGoal.Sport.RUNNING, + TimeSpan.MONTHLY); + assertFalse(actual); + } + + /** + * Tests the find duplicate method when there is a duplicate. It should return true. + */ + @Test + void findDuplicate_duplicate_true() { + ActivityGoal goal = new ActivityGoal(TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10000); + activityGoalList.add(goal); + boolean actual = activityGoalList.isDuplicate(ActivityGoal.GoalType.DISTANCE, ActivityGoal.Sport.RUNNING, + TimeSpan.WEEKLY); + assertTrue(actual); + } + +} diff --git a/src/test/java/athleticli/data/activity/ActivityGoalTest.java b/src/test/java/athleticli/data/activity/ActivityGoalTest.java new file mode 100644 index 0000000000..ba6e5da4c0 --- /dev/null +++ b/src/test/java/athleticli/data/activity/ActivityGoalTest.java @@ -0,0 +1,122 @@ +package athleticli.data.activity; + +import athleticli.data.Data; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static athleticli.data.Goal.TimeSpan; +import static athleticli.data.activity.ActivityGoal.GoalType; +import static athleticli.data.activity.ActivityGoal.Sport; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the ActivityGoal class. + */ +class ActivityGoalTest { + + private ActivityList activityList; + private ActivityGoal activityGoal; + private Data data; + + private TimeSpan period = TimeSpan.WEEKLY; + private final LocalDate date = LocalDate.now(); + private final String caption = "Sunday = Runday"; + private final int distance = 3000; + + /** + * Initializes the data instance before each test. + */ + @BeforeEach + void setUp() { + data = new Data(); + } + + /** + * Tests whether a fully achieved goal is recognized as achieved. + */ + @Test + void isAchieved_activityDistanceGoal_true() { + int targetValue = 8000; + GoalType goalType = GoalType.DISTANCE; + Sport sport = Sport.GENERAL; + activityGoal = new ActivityGoal(period, goalType, sport, targetValue); + + LocalTime duration = LocalTime.of(1, 24); + LocalDateTime date = LocalDateTime.now(); + Activity activity = new Activity(caption, duration, distance, date); + ActivityList activityList = data.getActivities(); + activityList.add(activity); + activityList.add(activity); + activityList.add(activity); + + boolean expected = true; + boolean actual = activityGoal.isAchieved(data); + assertEquals(expected, actual); + } + + /** + * Tests whether a goal is recognized as not achieved when the target value is not reached. + */ + @Test + void isAchieved_runGoalWithNoTrackedRun_false() { + int targetValue = 8000; + GoalType goalType = GoalType.DISTANCE; + Sport sport = Sport.RUNNING; + activityGoal = new ActivityGoal(period, goalType, sport, targetValue); + + LocalTime duration = LocalTime.of(1, 24); + LocalDateTime date = LocalDateTime.now(); + Activity activity = new Activity(caption, duration, distance, date); + ActivityList activityList = data.getActivities(); + activityList.add(activity); + activityList.add(activity); + activityList.add(activity); + + boolean expected = false; + boolean actual = activityGoal.isAchieved(data); + assertEquals(expected, actual); + } + + /** + * Tests whether a goal is detected as not achieved when the target value is reached outside the specified period. + */ + @Test + void isAchieved_goalAchievedOutsidePeriod_false() { + int targetValue = 120; + GoalType goalType = GoalType.DURATION; + Sport sport = Sport.GENERAL; + activityGoal = new ActivityGoal(period, goalType, sport, targetValue); + + LocalTime duration = LocalTime.of(1, 24); + LocalDateTime dateWithinPeriod = LocalDateTime.now(); + LocalDateTime dateOutsidePeriod = LocalDateTime.now().minusDays(15); + Activity activityWithinPeriod = new Activity(caption, duration, distance, dateWithinPeriod); + Activity activityOutsidePeriod = new Activity(caption, duration, distance, dateOutsidePeriod); + + ActivityList activityList = data.getActivities(); + activityList.add(activityWithinPeriod); + activityList.add(activityOutsidePeriod); + + boolean expected = false; + boolean actual = activityGoal.isAchieved(data); + assertEquals(expected, actual); + } + + /** + * Tests the getActivityClass method. + */ + @Test + void getActivityClass() { + GoalType goalType = GoalType.DURATION; + Sport sport = Sport.RUNNING; + activityGoal = new ActivityGoal(period, goalType, sport, 0); + Class expected = Run.class; + Class actual = activityGoal.getActivityClass(); + assertEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/data/activity/ActivityListTest.java b/src/test/java/athleticli/data/activity/ActivityListTest.java new file mode 100644 index 0000000000..77224b35cb --- /dev/null +++ b/src/test/java/athleticli/data/activity/ActivityListTest.java @@ -0,0 +1,178 @@ +package athleticli.data.activity; + +import athleticli.data.Goal.TimeSpan; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the ActivityList class. + */ +class ActivityListTest { + + private static final String CAPTION = "Sunday = Runday"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private ActivityList activityList; + private Activity activityFirst; + private Activity activitySecond; + + /** + * Sets up the ActivityList instance and adds two activities to it before each test. + */ + @BeforeEach + void setUp() { + activityList = new ActivityList(); + LocalDateTime dateSecond = LocalDateTime.now(); + LocalDateTime dateFirst = LocalDateTime.now().minusDays(1); + activityFirst = new Activity(CAPTION, DURATION, DISTANCE, dateFirst); + activitySecond = new Activity(CAPTION, DURATION, DISTANCE, dateSecond); + activityList.add(activityFirst); + activityList.add(activitySecond); + } + + /** + * Tests the find method. The find method should return a list of activities that occurred on the given date. + */ + @Test + void find() { + assertEquals(activityList.find(LocalDate.now()).get(0), activitySecond); + assertEquals(activityList.find(LocalDate.now().minusDays(1)).get(0), activityFirst); + } + + /** + * Tests the sort method. It should sort the activities in the list by date. + */ + @Test + void sort() { + activityList.sort(); + assertEquals(activityList.get(0), activitySecond); + assertEquals(activityList.get(1), activityFirst); + } + + /** + * Tests the filtering of activities by timespan. The filterByTimespan method should return a list of activities + * that occurred within the given timespan. + */ + @Test + void filterByTimespan() { + activityList.sort(); + ArrayList filteredList = activityList.filterByTimespan(TimeSpan.WEEKLY); + assertEquals(filteredList.get(0), activitySecond); + assertEquals(filteredList.get(1), activityFirst); + filteredList = activityList.filterByTimespan(TimeSpan.DAILY); + assertEquals(filteredList.get(0), activitySecond); + assertEquals(filteredList.size(), 1); + } + + /** + * Tests the total distance calculation for activities within a week. + */ + @Test + void getTotalDistance_activity_totalDistance() { + int expected = 2 * DISTANCE; + int actual = activityList.getTotalDistance(Activity.class, TimeSpan.WEEKLY); + assertEquals(expected, actual); + } + + /** + * Tests the total distance calculation for runs within a week. Since the activities in the list are not runs, it + * should return 0. + */ + @Test + void getTotalDistance_run_zero() { + int expected = 0; + int actual = activityList.getTotalDistance(Run.class, TimeSpan.WEEKLY); + assertEquals(expected, actual); + } + + /** + * Tests the total duration calculation for activities within a week. + */ + @Test + void getTotalDuration_activity_totalTime() { + int expected = 2 * DURATION.toSecondOfDay(); + int actual = activityList.getTotalDuration(Activity.class, TimeSpan.WEEKLY); + assertEquals(expected, actual); + } + + /** + * Tests the total duration calculation for runs within a week. Since the activities in the list are not runs, it + * should return 0. + */ + @Test + void getTotalDuration_run_zero() { + int expected = 0; + int actual = activityList.getTotalDuration(Run.class, TimeSpan.WEEKLY); + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of an activity. + */ + @Test + void unparse_activity_unparsed() { + String expected = "[Activity]: Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01T06:00"; + Activity activity = new Activity("Morning Run", LocalTime.of(1, 0), 10000, + LocalDateTime.of(2021, 9, 1, 6, 0)); + String actual = activityList.unparse(activity); + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of a run. + */ + @Test + void unparse_run_unparsed() { + String expected = "[Run]: Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01T06:00 elevation/60"; + Run run = new Run("Morning Run", LocalTime.of(1, 0), 10000, + LocalDateTime.of(2021, 9, 1, 6, 0), 60); + String actual = activityList.unparse(run); + assertEquals(expected, actual); + } + + /** + * Tests the parsing of an activity. The parsed activity should be the same as the original activity. + * + * @throws AthletiException If the activity cannot be parsed + */ + @Test + void parse_activity_parsed() throws AthletiException { + Activity expected = new Activity("Morning Run", LocalTime.of(1, 0), 10000, + LocalDateTime.of(2021, 9, 1, 6, 0)); + ActivityList activities = new ActivityList(); + String unparsedActivity = activities.unparse(expected); + Activity actual = activities.parse(unparsedActivity); + + assertEquals(expected.getCaption(), actual.getCaption()); + assertEquals(expected.getMovingTime(), actual.getMovingTime()); + assertEquals(expected.getDistance(), actual.getDistance()); + assertEquals(expected.getStartDateTime(), actual.getStartDateTime()); + } + + /** + * Tests the parsing of a run. The parsed run should be the same as the original run. + * + * @throws AthletiException If the run cannot be parsed. + */ + @Test + void parse_run_parsed() throws AthletiException { + Run expected = new Run("Morning Run", LocalTime.of(1, 0), 10000, + LocalDateTime.of(2021, 9, 1, 6, 0), 60); + ActivityList activities = new ActivityList(); + String unparsedActivity = activities.unparse(expected); + Run actual = (Run) activities.parse(unparsedActivity); + + assertEquals(expected.getCaption(), actual.getCaption()); + assertEquals(expected.getMovingTime(), actual.getMovingTime()); + assertEquals(expected.getDistance(), actual.getDistance()); + assertEquals(expected.getStartDateTime(), actual.getStartDateTime()); + } +} diff --git a/src/test/java/athleticli/data/activity/ActivityTest.java b/src/test/java/athleticli/data/activity/ActivityTest.java new file mode 100644 index 0000000000..dece604ad0 --- /dev/null +++ b/src/test/java/athleticli/data/activity/ActivityTest.java @@ -0,0 +1,128 @@ +package athleticli.data.activity; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the Activity class. + */ +public class ActivityTest { + + private static final String CAPTION = "Sunday = Runday"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, 21); + private Activity activity; + + /** + * Sets up the Activity object for testing. + */ + @BeforeEach + public void setUp() { + activity = new Activity(CAPTION, DURATION, DISTANCE, DATE); + } + + /** + * Tests the constructor and getters. + */ + @Test + public void testConstructorAndGetters() { + assertEquals(CAPTION, activity.getCaption()); + assertEquals(DURATION, activity.getMovingTime()); + assertEquals(DISTANCE, activity.getDistance()); + assertEquals(DATE, activity.getStartDateTime()); + } + + /** + * Tests the String representation of the Activity object. + */ + @Test + public void testToString() { + String expected = "[Activity] Sunday = Runday | Distance: 18.12 km | Time: 1h 24m | " + + "October 10, 2023 at 11:21 PM"; + assertEquals(expected, activity.toString()); + } + + /** + * Tests the detailed String representation of the Activity object. + * Disabled due to gradle issues. + */ + @Test + @Disabled + public void toDetailedString() { + String expected = "[Activity - Sunday = Runday - October 10, 2023 at 11:21 PM]\n" + + "\tDistance: 18.12 km Time: 01:24:00\n" + + "\tCalories: 0 kcal ..."; + String actual = activity.toDetailedString(); + assertEquals(expected, actual); + } + + /** + * Tests the generation of the distance String output. + */ + @Test + public void generateDistanceStringOutput() { + String actual = activity.generateDistanceStringOutput(); + String expected = "Distance: 18.12 km"; + assertEquals(expected, actual); + } + + /** + * Tests the generation of the moving time String output. + */ + @Test + public void generateMovingTimeStringOutput() { + String actual = activity.generateMovingTimeStringOutput(); + String expected = "Time: 01:24:00"; + assertEquals(expected, actual); + } + + /** + * Tests the generation of the start date and time String output. + */ + @Test + public void generateStartDateTimeStringOutput() { + String actual = activity.generateStartDateTimeStringOutput(); + String expected = "October 10, 2023 at 11:21 PM"; + assertEquals(expected, actual); + } + + /** + * Tests the formatting of two columns. + */ + @Test + public void formatTwoColumns() { + String actual = activity.formatTwoColumns("Distance: 18.12 km", "Time: 1h 24m", 30); + String expected = "Distance: 18.12 km Time: 1h 24m"; + assertEquals(expected, actual); + } + + /** + * Tests the generation of the short representation of the moving time String output when hours are not zero. + * If hours are not zero, the hours should be included in the output and the seconds should be omitted. + */ + @Test + void generateShortMovingTimeStringOutput_hoursNotZero() { + String expected = "Time: 1h 24m"; + String actual = activity.generateShortMovingTimeStringOutput(); + assertEquals(expected, actual); + } + + /** + * Tests the generation of the short representation of the moving time String output when hours are zero. + * If hours are zero, the hours should be omitted from the output and the seconds should be included. + */ + @Test + void generateShortMovingTimeStringOutput_hoursZero() { + activity = new Activity(CAPTION, LocalTime.of(0, 24, 20), DISTANCE, DATE); + String expected = "Time: 24m 20s"; + String actual = activity.generateShortMovingTimeStringOutput(); + assertEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/data/activity/CycleTest.java b/src/test/java/athleticli/data/activity/CycleTest.java new file mode 100644 index 0000000000..82b8452113 --- /dev/null +++ b/src/test/java/athleticli/data/activity/CycleTest.java @@ -0,0 +1,112 @@ +package athleticli.data.activity; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the Cycle class. + */ +public class CycleTest { + + private static final String CAPTION = "Cycling in the afternoon"; + private static final LocalTime DURATION = LocalTime.of(2, 13); + private static final int DISTANCE = 40460; + private static final int ELEVATION = 101; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 7, 14, 0); + private Cycle cycle; + + /** + * Sets up the Cycle object for testing. + */ + @BeforeEach + public void setUp() { + cycle = new Cycle(CAPTION, DURATION, DISTANCE, DATE, ELEVATION); + } + + /** + * Tests the constructor and getters. + */ + @Test + public void testConstructorAndGetters() { + assertEquals(CAPTION, cycle.getCaption()); + assertEquals(DURATION, cycle.getMovingTime()); + assertEquals(DISTANCE, cycle.getDistance()); + assertEquals(DATE, cycle.getStartDateTime()); + assertEquals(ELEVATION, cycle.getElevationGain()); + } + + /** + * Tests the calculation of average speed. + */ + @Test + public void calculateAverageSpeed() { + double expected = 18.25; + double actual = cycle.calculateAverageSpeed(); + assertEquals(expected, actual, 0.005); + } + + + /** + * Tests the String representation of the Cycle object. + */ + @Test + public void testToString() { + String expected = "[Cycle] Cycling in the afternoon | Distance: 40.46 km | Speed: 18.25 km/h | Time: 2h 13m" + + " | " + + "October 7, 2023 at 2:00 PM"; + assertEquals(expected, cycle.toString()); + } + + /** + * Tests the detailed String representation of the Cycle object. + * Disabled due to gradle issues. + */ + @Test + @Disabled + public void testToDetailedString() { + String expected = "[Cycle - Cycling in the afternoon - October 7, 2023 at 2:00 PM]\n" + + "\tDistance: 40.46 km Elevation Gain: 101 m\n" + + "\tTime: 02:13:00 Avg Speed: 18.25 km/h\n" + + "\tCalories: 0 kcal Max Speed: tbd"; + String actual = cycle.toDetailedString(); + assertEquals(expected, actual); + } + + /** + * Tests the generation of the speed String output. + */ + @Test + public void generateSpeedStringOutput() { + String actual = cycle.generateSpeedStringOutput(); + String expected = "18.25 km/h"; + assertEquals(expected, actual); + } + + /** + * Tests the generation of the elevation gain String output. + */ + @Test + void generateElevationGainStringOutput() { + String actual = cycle.generateElevationGainStringOutput(); + String expected = "Elevation Gain: 101 m"; + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of the Cycle object. + */ + @Test + void unparse() { + String actual = cycle.unparse(); + String expected = "[Cycle]: Cycling in the afternoon duration/02:13:00 distance/40460 " + + "datetime/2023-10-07T14:00 elevation/101"; + assertEquals(expected, actual); + } + +} diff --git a/src/test/java/athleticli/data/activity/RunTest.java b/src/test/java/athleticli/data/activity/RunTest.java new file mode 100644 index 0000000000..1d4705c536 --- /dev/null +++ b/src/test/java/athleticli/data/activity/RunTest.java @@ -0,0 +1,98 @@ +package athleticli.data.activity; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the Run class. + */ +public class RunTest { + + private static final String CAPTION = "Night Run"; + private static final LocalTime DURATION = LocalTime.of(1, 24); + private static final int DISTANCE = 18120; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 10, 10, 23, + 21); + private static final int ELEVATION = 60; + private Run run; + + /** + * Sets up the Run object for testing. + */ + @BeforeEach + public void setUp() { + run = new Run(CAPTION, DURATION, DISTANCE, DATE, ELEVATION); + } + + /** + * Tests the constructor and getters. + */ + @Test + public void testConstructorAndGetters() { + assertEquals(CAPTION, run.getCaption()); + assertEquals(DURATION, run.getMovingTime()); + assertEquals(DISTANCE, run.getDistance()); + assertEquals(DATE, run.getStartDateTime()); + assertEquals(ELEVATION, run.getElevationGain()); + } + + /** + * Tests the calculation of average pace. + */ + @Test + public void calculateAveragePace() { + double averagePace = run.calculateAveragePace(); + assertEquals(4.64, averagePace, 0.005); + } + + /** + * Tests the conversion of average pace to a String. + */ + @Test + public void convertAveragePaceToString() { + String expected = "4:38"; + String actual = run.convertAveragePaceToString(); + assertEquals(expected, actual); + } + + /** + * Tests the String representation of the Run object. + */ + @Test + public void testToString() { + String expected = "[Run] Night Run | Distance: 18.12 km | Pace: 4:38 /km | Time: 1h 24m | " + + "October 10, 2023 at 11:21 PM"; + assertEquals(expected, run.toString()); + } + + /** + * Tests the detailed String representation of the Run object. + * Disabled due to gradle issues. + */ + @Test + @Disabled + public void testToDetailedString() { + String expected = "[Run - Night Run - October 10, 2023 at 11:21 PM]\n" + + "\tDistance: 18.12 km Avg Pace: 4:38 /km\n" + + "\tTime: 01:24:00 Elevation Gain: 60 m\n" + + "\tCalories: 0 kcal Steps: 0"; + String actual = run.toDetailedString(); + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of the Run object. + */ + @Test + void unparse() { + String actual = run.unparse(); + String expected = "[Run]: Night Run duration/01:24:00 distance/18120 datetime/2023-10-10T23:21 elevation/60"; + assertEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/data/activity/SwimTest.java b/src/test/java/athleticli/data/activity/SwimTest.java new file mode 100644 index 0000000000..768c8cbb72 --- /dev/null +++ b/src/test/java/athleticli/data/activity/SwimTest.java @@ -0,0 +1,95 @@ +package athleticli.data.activity; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the Swim class. + */ +public class SwimTest { + + private static final String CAPTION = "Afternoon Swim"; + private static final LocalTime DURATION = LocalTime.of(0, 35); + private static final int DISTANCE = 1000; + private static final LocalDateTime DATE = LocalDateTime.of(2023, 8, 29, 9, 45); + private static final Swim.SwimmingStyle STYLE = Swim.SwimmingStyle.BUTTERFLY; + private Swim swim; + + /** + * Sets up the Swim object for testing. + */ + @BeforeEach + public void setUp() { + swim = new Swim(CAPTION, DURATION, DISTANCE, DATE, STYLE); + } + + /** + * Tests the constructor and getters. + */ + @Test + public void testConstructorAndGetters() { + assertEquals(CAPTION, swim.getCaption()); + assertEquals(DURATION, swim.getMovingTime()); + assertEquals(DISTANCE, swim.getDistance()); + assertEquals(DATE, swim.getStartDateTime()); + assertEquals(STYLE, swim.getStyle()); + } + + /** + * Tests the calculation of average lap time. + */ + @Test + public void calculateAverageLapTime() { + assertEquals(105, swim.calculateAverageLapTime()); + } + + /** + * Tests the calculation of laps. + */ + @Test + public void calculateLaps() { + assertEquals(20, swim.calculateLaps()); + } + + /** + * Tests the String representation of the Swim object. + */ + @Test + public void testToString() { + String expected = "[Swim] Afternoon Swim | Distance: 1.00 km | Lap Time: 105s | Time: 35m 0s | " + + "August 29, 2023 at 9:45 AM"; + assertEquals(expected, swim.toString()); + } + + /** + * Tests the detailed String representation of the Swim object. + * Disabled due to gradle issues. + */ + @Test + @Disabled + public void testToDetailedString() { + String expected = "[Swim - Afternoon Swim - August 29, 2023 at 9:45 AM]\n" + + "\tDistance: 1.00 km Time: 00:35:00\n" + + "\tLaps: 20 Style: BUTTERFLY\n" + + "\tAvg Lap Time: 105 s Calories: 0 kcal"; + String actual = swim.toDetailedString(); + assertEquals(expected, actual); + } + + /** + * Tests the unparsing of the swim object. + */ + @Test + void unparse() { + String actual = swim.unparse(); + String expected = "[Swim]: Afternoon Swim duration/00:35:00 distance/1000 datetime/2023-08-29T09:45 " + + "style/BUTTERFLY"; + assertEquals(expected, actual); + } +} diff --git a/src/test/java/athleticli/data/diet/DietGoalListTest.java b/src/test/java/athleticli/data/diet/DietGoalListTest.java new file mode 100644 index 0000000000..b67a424825 --- /dev/null +++ b/src/test/java/athleticli/data/diet/DietGoalListTest.java @@ -0,0 +1,172 @@ +package athleticli.data.diet; + +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DietGoalListTest { + private static final int LARGE_PROTEIN_TARGET_VALUE = 10000; + private static final int SMALL_PROTEIN_TARGET_VALUE = 1; + private HealthyDietGoal weeklyProteinGoal; + private HealthyDietGoal dailyProteinGoal; + private HealthyDietGoal dailyProteinGoalSmall; + private UnhealthyDietGoal unhealthyDailyDietGoalSmall; + private DietGoalList dietGoals; + private Data data; + + @BeforeEach + void setUp() { + dietGoals = new DietGoalList(); + weeklyProteinGoal = new HealthyDietGoal(Goal.TimeSpan.WEEKLY, "protein", LARGE_PROTEIN_TARGET_VALUE); + dailyProteinGoal = new HealthyDietGoal(Goal.TimeSpan.DAILY, "protein", LARGE_PROTEIN_TARGET_VALUE); + dailyProteinGoalSmall = new HealthyDietGoal(Goal.TimeSpan.DAILY, "protein", SMALL_PROTEIN_TARGET_VALUE); + unhealthyDailyDietGoalSmall = new UnhealthyDietGoal(Goal.TimeSpan.DAILY, + "protein", SMALL_PROTEIN_TARGET_VALUE); + data = new Data(); + } + + @Test + void add_addOneGoal_expectSizeOne() { + dietGoals.add(weeklyProteinGoal); + assertEquals(1, dietGoals.size()); + } + + @Test + void remove_removeExistingGoal_expectSizeOne() { + dietGoals.add(weeklyProteinGoal); + dietGoals.remove(0); + assertEquals(0, dietGoals.size()); + } + + @Test + void remove_removeFromZeroGoals_expectIndexOutOfRangeError() { + assertThrows(IndexOutOfBoundsException.class, () -> { + dietGoals.remove(0); + }); + } + + @Test + void get_addOneGoal_expectGetSameGoal() { + dietGoals.add(weeklyProteinGoal); + assertEquals(weeklyProteinGoal, dietGoals.get(0)); + } + + @Test + void size_initializeArgs_expectZero() { + assertEquals(0, dietGoals.size()); + } + + @Test + void size_addTenGoals_expectTen() { + for (int i = 0; i < 10; i++) { + dietGoals.add(weeklyProteinGoal); + } + assertEquals(10, dietGoals.size()); + } + + @Test + void toString_oneExistingGoal_expectCorrectFormat() { + dietGoals.add(weeklyProteinGoal); + assertEquals("\t1. [HEALTHY] WEEKLY protein intake progress: (0/10000)\n", dietGoals.toString(data)); + } + + @Test + void unparse_oneDietGoal_expectCorrectFormat() { + String actualOutput = dietGoals.unparse(weeklyProteinGoal); + assertEquals("dietGoal WEEKLY protein 10000 healthy", actualOutput); + } + + @Test + void parse_validHealthyDietGoal_expectHealthyDietGoal() throws AthletiException { + String validInput = "dietGoal WEEKLY protein 10000 healthy"; + DietGoal newProteinGoal = dietGoals.parse(validInput); + assert newProteinGoal instanceof HealthyDietGoal; + } + + @Test + void parse_validUnhealthyDietGoal_expectUnhealthyDietGoal() throws AthletiException { + String validInput = "dietGoal WEEKLY protein 10000 unhealthy"; + DietGoal newProteinGoal = dietGoals.parse(validInput); + assert newProteinGoal instanceof UnhealthyDietGoal; + } + + @Test + void parse_invalidDietGoal_expectError() throws AthletiException { + String invalidInput = "dietGoal WEEKLY protein 10000 invalid"; + assertThrows(AthletiException.class, () -> { + dietGoals.parse(invalidInput); + }); + } + + @Test + void parse_repeatedDietGoal_expectError() throws AthletiException { + String validHealthyInput = "dietGoal WEEKLY protein 10000 healthy"; + DietGoal newDietGoal = dietGoals.parse(validHealthyInput); + dietGoals.add(newDietGoal); + assertThrows(AthletiException.class, () -> { + dietGoals.parse(validHealthyInput); + }); + } + @Test + void parse_invalidDietGoalType_expectError() throws AthletiException { + String validHealthyInput = "dietGoal WEEKLY protein 10000 healthy"; + String validUnhealthyInput = "dietGoal DAILY protein 10000 unhealthy"; + DietGoal newDietGoal = dietGoals.parse(validHealthyInput); + dietGoals.add(newDietGoal); + assertThrows(AthletiException.class, () -> { + dietGoals.parse(validUnhealthyInput); + }); + } + + @Test + void parse_invalidInput_expectDietGoal() throws AthletiException { + String validInput = "dietGoal WEEKLYprotein10000"; + assertThrows(AthletiException.class, () -> { + dietGoals.parse(validInput); + }); + } + + @Test + void isTargetValueConsistentWithTimeSpan_dailyTargetValueEqualToWeeklyTargetValue_returnFalse() { + dietGoals.add(weeklyProteinGoal); + assertFalse(dietGoals.isTargetValueConsistentWithTimeSpan(dailyProteinGoal)); + + } + + @Test + void isTargetValueConsistentWithTimeSpan_sameTimeSpan_returnTrue() { + dietGoals.add(weeklyProteinGoal); + assertTrue(dietGoals.isTargetValueConsistentWithTimeSpan(weeklyProteinGoal)); + } + + @Test + void isTargetValueConsistentWithTimeSpan_weeklyTargetValueGreaterThanDailyTargetValue_returnTrue() { + dietGoals.add(weeklyProteinGoal); + assertTrue(dietGoals.isTargetValueConsistentWithTimeSpan(dailyProteinGoalSmall)); + } + + @Test + void isTargetValueConsistentWithTimeSpan_dailyTargetValueLessThanWeeklyTargetValue_returnTrue() { + dietGoals.add(dailyProteinGoalSmall); + assertTrue(dietGoals.isTargetValueConsistentWithTimeSpan(weeklyProteinGoal)); + } + + @Test + void isDietGoalTypeValid_sameGoalSameType_returnTrue() { + dietGoals.add(weeklyProteinGoal); + assertTrue(dietGoals.isDietGoalTypeValid(dailyProteinGoalSmall)); + } + + @Test + void isDietGoalTypeValid_sameGoalDifferentType_returnFalse() { + dietGoals.add(weeklyProteinGoal); + assertFalse(dietGoals.isDietGoalTypeValid(unhealthyDailyDietGoalSmall)); + } +} diff --git a/src/test/java/athleticli/data/diet/DietGoalStub.java b/src/test/java/athleticli/data/diet/DietGoalStub.java new file mode 100644 index 0000000000..dbbfbb60e7 --- /dev/null +++ b/src/test/java/athleticli/data/diet/DietGoalStub.java @@ -0,0 +1,17 @@ +package athleticli.data.diet; + +/** + * DietGoalStub is use for isolation testing of the DietGoal abstract class + */ +public class DietGoalStub extends DietGoal { + /** + * Constructs a diet goal. + * + * @param timespan The timespan of the diet goal. + * @param nutrient The nutrients of the diet goal. + * @param targetValue The target value of the diet goal. + */ + public DietGoalStub(TimeSpan timespan, String nutrient, int targetValue) { + super(timespan, nutrient, targetValue); + } +} diff --git a/src/test/java/athleticli/data/diet/DietGoalTest.java b/src/test/java/athleticli/data/diet/DietGoalTest.java new file mode 100644 index 0000000000..1bf1b0dc54 --- /dev/null +++ b/src/test/java/athleticli/data/diet/DietGoalTest.java @@ -0,0 +1,88 @@ +package athleticli.data.diet; + +import athleticli.commands.diet.AddDietCommand; +import athleticli.data.Data; +import athleticli.data.Goal; +import athleticli.parser.Parameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DietGoalTest { + + private DietGoalStub proteinGoal; + private DietGoalStub fatGoal; + private DietGoalStub carbGoal; + private DietGoalStub caloriesGoal; + private Data data; + private Diet diet; + private final int calories = 10000; + private final int protein = 20000; + private final int carb = 30000; + private final int fat = 40000; + private final LocalDateTime dateTime = LocalDateTime.now(); + + @BeforeEach + void setUp() { + proteinGoal = new DietGoalStub(Goal.TimeSpan.WEEKLY, Parameter.NUTRIENTS_PROTEIN, 10000); + fatGoal = new DietGoalStub(Goal.TimeSpan.WEEKLY, Parameter.NUTRIENTS_FAT, 10000); + carbGoal = new DietGoalStub(Goal.TimeSpan.WEEKLY, Parameter.NUTRIENTS_CARB, 10000); + caloriesGoal = new DietGoalStub(Goal.TimeSpan.WEEKLY, Parameter.NUTRIENTS_CALORIES, 10000); + data = new Data(); + diet = new Diet(calories, protein, carb, fat, dateTime); + } + + @Test + void getNutrients_initializeCommonArgs_expectArgs() { + assertEquals("protein", proteinGoal.getNutrient()); + } + + @Test + void setNutrients_setCommonArgs_expectArgs() { + proteinGoal.setNutrient("Advanced Protein"); + assertEquals("Advanced Protein", proteinGoal.getNutrient()); + } + + @Test + void getTargetValue_initializeCommonArgs_expectArgs() { + assertEquals(10000, proteinGoal.getTargetValue()); + } + + @Test + void setTargetValue_initializeCommonArgs_expectArgs() { + proteinGoal.setTargetValue(10); + assertEquals(10, proteinGoal.getTargetValue()); + } + + @Test + void getCurrentValue_newProteinGoal_expectZero() { + assertEquals(0, proteinGoal.getCurrentValue(data)); + } + + + + @Test + void isAchieved_currentValueGreaterThanAndEqualToTargetValue_expectTrue() { + AddDietCommand addDietCommand = new AddDietCommand(diet); + addDietCommand.execute(data); + boolean allGoalsAchieved = fatGoal.isAchieved(data) && caloriesGoal.isAchieved(data) + && carbGoal.isAchieved(data) && proteinGoal.isAchieved(data); + assertTrue(allGoalsAchieved); + } + + @Test + void isAchieved_currentValueLesserThanTargetValue_expectFalse() { + assertFalse(caloriesGoal.isAchieved(data)); + } + + @Test + void toString_initializeCommonArgs_expectCorrectFormat() { + String expectedString = " WEEKLY protein intake progress: (0/10000)\n"; + assertEquals(expectedString, proteinGoal.toString(data)); + } +} diff --git a/src/test/java/athleticli/data/diet/DietListTest.java b/src/test/java/athleticli/data/diet/DietListTest.java new file mode 100644 index 0000000000..d7d3946004 --- /dev/null +++ b/src/test/java/athleticli/data/diet/DietListTest.java @@ -0,0 +1,117 @@ +package athleticli.data.diet; + +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DietListTest { + private static final int CALORIES = 10000; + private static final int PROTEIN = 20000; + private static final int CARB = 30000; + private static final int FAT = 40000; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2020, 10, 10, 10, 10); + + private Diet diet; + private DietList dietList; + + @BeforeEach + void setUp() { + dietList = new DietList(); + diet = new Diet(CALORIES, PROTEIN, CARB, FAT, DATE_TIME); + } + + @Test + void add_addOneDiet_expectSizeOne() { + dietList.add(diet); + assertEquals(1, dietList.size()); + } + + @Test + void remove_removeExistingDiet_expectSizeOne() { + dietList.add(diet); + dietList.remove(0); + assertEquals(0, dietList.size()); + } + + @Test + void remove_removeFromZeroDiets_expectIndexOutOfRangeError() { + assertThrows(IndexOutOfBoundsException.class, () -> { + dietList.remove(0); + }); + } + + @Test + void get_addOneDiet_expectGetSameDiet() { + dietList.add(diet); + assertEquals(diet, dietList.get(0)); + } + + @Test + void size_initializeArgs_expectZero() { + assertEquals(0, dietList.size()); + } + + @Test + void size_addTenDiets_expectTen() { + for (int i = 0; i < 10; i++) { + dietList.add(diet); + } + assertEquals(10, dietList.size()); + } + + @Test + void testToString_oneExistingDiet_expectCorrectFormat() { + dietList.add(diet); + assertEquals("\t1. " + diet, dietList.toString()); + } + + @Test + void testToString_twoExistingDiets_expectCorrectFormat() { + dietList.add(diet); + dietList.add(diet); + assertEquals("\t1. " + diet.toString() + "\n\t2. " + diet.toString(), dietList.toString()); + } + + @Test + void testToString_zeroExistingDiets_expectCorrectFormat() { + assertEquals("", dietList.toString()); + } + + @Test + void testToString_threeExistingDiets_expectCorrectFormat() { + dietList.add(diet); + dietList.add(diet); + dietList.add(diet); + assertEquals("\t1. " + diet.toString() + "\n\t2. " + diet.toString() + "\n\t3. " + diet.toString(), + dietList.toString()); + } + + @Test + void unparse_oneExistingDiet_expectCorrectFormat() { + String commandArgs = "calories/10000 protein/20000 carb/30000 fat/40000 datetime/2020-10-10T10:10"; + assertEquals(commandArgs, dietList.unparse(diet)); + } + + @Test + void parse_oneExistingDiet_expectCorrectFormat() throws AthletiException { + String commandArgs = "calories/10000 protein/20000 carb/30000 fat/40000 datetime/2020-10-10T10:10"; + assertEquals(diet.toString(), dietList.parse(commandArgs).toString()); + } + + @Test + void parse_oneExistingDietWithExtraSpaces_expectCorrectFormat() throws AthletiException { + String commandArgs = "calories/10000 protein/20000 carb/30000 fat/40000 datetime/2020-10-10T10:10"; + assertEquals(diet.toString(), dietList.parse(commandArgs).toString()); + } + + @Test + void parse_dietConstructorWithDietListParse_expectSameDiet() throws AthletiException { + String commandArgs = dietList.unparse(diet); + assertEquals(diet.toString(), dietList.parse(commandArgs).toString()); + } +} diff --git a/src/test/java/athleticli/data/diet/DietTest.java b/src/test/java/athleticli/data/diet/DietTest.java new file mode 100644 index 0000000000..f745e0d5d8 --- /dev/null +++ b/src/test/java/athleticli/data/diet/DietTest.java @@ -0,0 +1,87 @@ +package athleticli.data.diet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DietTest { + private static final int CALORIES = 10000; + private static final int PROTEIN = 20000; + private static final int CARB = 30000; + private static final int FAT = 40000; + private static final LocalDateTime DATE_TIME = LocalDateTime.of(2020, 10, 10, 10, 10); + + private Diet diet; + + @BeforeEach + void setUp() { + diet = new Diet(CALORIES, PROTEIN, CARB, FAT, DATE_TIME); + } + + @Test + void getCalories_initializeCommonArgs_expectArgs() { + assertEquals(CALORIES, diet.getCalories()); + } + + @Test + void setCalories_setCommonArgs_expectArgs() { + diet.setCalories(10); + assertEquals(10, diet.getCalories()); + } + + @Test + void getProtein_initializeCommonArgs_expectArgs() { + assertEquals(PROTEIN, diet.getProtein()); + } + + @Test + void setProtein_setCommonArgs_expectArgs() { + diet.setProtein(20); + assertEquals(20, diet.getProtein()); + } + + @Test + void getCarb_initializeCommonArgs_expectArgs() { + assertEquals(CARB, diet.getCarb()); + } + + @Test + void setCarb_setCommonArgs_expectArgs() { + diet.setCarb(30); + assertEquals(30, diet.getCarb()); + } + + @Test + void getFat_initializeCommonArgs_expectArgs() { + assertEquals(FAT, diet.getFat()); + } + + @Test + void setFat_setCommonArgs_expectArgs() { + diet.setFat(40); + assertEquals(40, diet.getFat()); + } + + @Test + void getDateTime_initializeCommonArgs_expectArgs() { + assertEquals(DATE_TIME, diet.getDateTime()); + } + + @Test + void setDateTime_setCommonArgs_expectArgs() { + LocalDateTime newDateTime = LocalDateTime.of(2020, 10, 10, 10, 11); + diet.setDateTime(newDateTime); + assertEquals(newDateTime, diet.getDateTime()); + } + + @Test + void toString_initializeCommonArgs_expectArgs() { + String expected = + "Calories: 10000 cal | Protein: 20000 mg | Carb: 30000 mg | Fat: 40000 mg | October " + + "10, 2020 at 10:10 AM"; + assertEquals(expected, diet.toString()); + } +} diff --git a/src/test/java/athleticli/data/sleep/SleepGoalListTest.java b/src/test/java/athleticli/data/sleep/SleepGoalListTest.java new file mode 100644 index 0000000000..4d56e71593 --- /dev/null +++ b/src/test/java/athleticli/data/sleep/SleepGoalListTest.java @@ -0,0 +1,75 @@ +package athleticli.data.sleep; + +import athleticli.data.Goal.TimeSpan; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SleepGoalListTest { + + private SleepGoalList sleepGoalList; + + @BeforeEach + void setup() { + sleepGoalList = new SleepGoalList(); + } + + @Test + void parse_sleepDurationGoalDaily_parsed() throws AthletiException { + String arguments = "type/duration period/daily target/50000"; + SleepGoal expected = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.DAILY, 50000); + SleepGoal actual = sleepGoalList.parse(arguments); + assertEquals(expected.getGoalType(), actual.getGoalType()); + assertEquals(expected.getTimeSpan(), actual.getTimeSpan()); + assertEquals(expected.getTargetValue(), actual.getTargetValue()); + } + + @Test + void unparse_sleepDurationGoalDaily_unparsed() { + String expected = "type/DURATION period/DAILY target/50000"; + SleepGoal goal = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.DAILY, 50000); + String actual = sleepGoalList.unparse(goal); + assertEquals(expected, actual); + } + + @Test + void unparse_sleepDurationGoalWeekly_unparsed() { + String expected = "type/DURATION period/WEEKLY target/10000"; + SleepGoal goal = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.WEEKLY, 10000); + String actual = sleepGoalList.unparse(goal); + assertEquals(expected, actual); + } + + @Test + void unparse_sleepDurationGoalMonthly_unparsed() { + String expected = "type/DURATION period/MONTHLY target/0"; + SleepGoal goal = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.MONTHLY, 0); + String actual = sleepGoalList.unparse(goal); + assertEquals(expected, actual); + } + + @Test + void unparse_sleepDurationGoalYearly_unparsed() { + String expected = "type/DURATION period/YEARLY target/-1"; + SleepGoal goal = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.YEARLY, -1); + String actual = sleepGoalList.unparse(goal); + assertEquals(expected, actual); + } + + @Test + void findDuplicate_noDuplicate_false() { + SleepGoal goal = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.DAILY, 50000); + assertFalse(sleepGoalList.isDuplicate(goal.getGoalType(), goal.getTimeSpan())); + } + + @Test + void findDuplicate_duplicate_true() { + SleepGoal goal = new SleepGoal(SleepGoal.GoalType.DURATION, TimeSpan.DAILY, 50000); + sleepGoalList.add(goal); + assertTrue(sleepGoalList.isDuplicate(goal.getGoalType(), goal.getTimeSpan())); + } +} diff --git a/src/test/java/athleticli/data/sleep/SleepGoalTest.java b/src/test/java/athleticli/data/sleep/SleepGoalTest.java new file mode 100644 index 0000000000..a548b8a60a --- /dev/null +++ b/src/test/java/athleticli/data/sleep/SleepGoalTest.java @@ -0,0 +1,61 @@ +package athleticli.data.sleep; + +import athleticli.data.Data; +import athleticli.exceptions.AthletiException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static athleticli.data.Goal.TimeSpan; +import static athleticli.data.sleep.SleepGoal.GoalType; +import static athleticli.data.sleep.SleepGoal.TimeSpan; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SleepGoalTest { + + private SleepGoal sleepGoal; + private Data data; + private GoalType goalType = GoalType.DURATION; + private TimeSpan period = TimeSpan.WEEKLY; + + @BeforeEach + void setup() { + data = new Data(); + } + + @Test + void isAchieved_sleepDurationGoal_true() throws AthletiException { + int targetValue = 8000; + + sleepGoal = new SleepGoal(goalType, period, targetValue); + + LocalDateTime date = LocalDateTime.now(); + Sleep sleep = new Sleep(date, date.plusHours(8)); + SleepList sleepList = data.getSleeps(); + sleepList.add(sleep); + + boolean expected = true; + boolean actual = sleepGoal.isAchieved(data); + assertEquals(expected, actual); + } + + @Test + void isAchieved_sleepDurationGoal_false() throws AthletiException { + int targetValue = 8000; + + sleepGoal = new SleepGoal(goalType, period, targetValue); + + LocalDateTime date = LocalDateTime.now(); + Sleep sleep = new Sleep(date, date.plusHours(1)); + SleepList sleepList = data.getSleeps(); + sleepList.add(sleep); + + boolean expected = false; + boolean actual = sleepGoal.isAchieved(data); + assertEquals(expected, actual); + } + +} diff --git a/src/test/java/athleticli/data/sleep/SleepListTest.java b/src/test/java/athleticli/data/sleep/SleepListTest.java new file mode 100644 index 0000000000..bcb6a3f687 --- /dev/null +++ b/src/test/java/athleticli/data/sleep/SleepListTest.java @@ -0,0 +1,87 @@ +package athleticli.data.sleep; + +import athleticli.data.Goal.TimeSpan; +import athleticli.exceptions.AthletiException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SleepListTest { + + private SleepList sleepList; + private Sleep sleepFirst; + private Sleep sleepSecond; + private Sleep sleepParse; + + @BeforeEach + public void setup() throws AthletiException { + sleepList = new SleepList(); + LocalDateTime dateSecond = LocalDateTime.now(); + LocalDateTime dateFirst = LocalDateTime.now().minusDays(1); + + sleepFirst = new Sleep(dateFirst, dateFirst.plusHours(8)); + sleepSecond = new Sleep(dateSecond, dateSecond.plusHours(8)); + sleepParse = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + + sleepList.add(sleepFirst); + sleepList.add(sleepSecond); + sleepList.add(sleepParse); + } + + @Test + public void testFind() { + assertEquals(sleepList.find(LocalDate.now()).get(0), sleepSecond); + assertEquals(sleepList.find(LocalDate.now().minusDays(1)).get(0), sleepFirst); + } + + @Test + public void testSort() { + sleepList.sort(); + assertEquals(sleepList.get(0), sleepSecond); + assertEquals(sleepList.get(1), sleepFirst); + } + + @Test + public void testFilterByTimespan() { + sleepList.sort(); + ArrayList result = sleepList.filterByTimespan(TimeSpan.WEEKLY); + assertEquals(result.get(0), sleepSecond); + assertEquals(result.get(1), sleepFirst); + result = sleepList.filterByTimespan(TimeSpan.DAILY); + assertEquals(result.get(0), sleepSecond); + assertEquals(result.size(), 1); + } + + @Test + public void testGetTotalSleepDuration() { + int expected = 8 * 60 * 60 * 2; + int actual = sleepList.getTotalSleepDuration(TimeSpan.WEEKLY); + assertEquals(expected, actual); + } + + @Test + public void parse_sleep_parsed() throws AthletiException { + String arguments = "start/2023-10-17 22:00 end/2023-10-18 06:00"; + Sleep expected = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + Sleep actual = sleepList.parse(arguments); + assertEquals(expected.getStartDateTime(), actual.getStartDateTime()); + assertEquals(expected.getEndDateTime(), actual.getEndDateTime()); + } + + @Test + public void unparse_sleep_unparsed() throws AthletiException { + String expected = "start/2023-10-17T22:00 end/2023-10-18T06:00"; + Sleep actual = new Sleep(LocalDateTime.of(2023, 10, 17, 22, 0), + LocalDateTime.of(2023, 10, 18, 6, 0)); + String actualString = sleepList.unparse(actual); + assertEquals(expected, actualString); + } +} diff --git a/src/test/java/athleticli/data/sleep/SleepTest.java b/src/test/java/athleticli/data/sleep/SleepTest.java new file mode 100644 index 0000000000..7a662c6308 --- /dev/null +++ b/src/test/java/athleticli/data/sleep/SleepTest.java @@ -0,0 +1,65 @@ +package athleticli.data.sleep; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import athleticli.exceptions.AthletiException; + +public class SleepTest { + + private static final LocalDateTime START_DATE_TIME = LocalDateTime.of(2023, 10, 17, 22, 0); + private static final LocalDateTime END_DATE_TIME = LocalDateTime.of(2023, 10, 18, 6, 0); + private Sleep sleep; + + @BeforeEach + public void setup() throws AthletiException { + sleep = new Sleep(START_DATE_TIME, END_DATE_TIME); + } + + @Test + public void testToString() { + String expected = "[Sleep] | Date: 2023-10-17 | Start Time: October 17, 2023 at 10:00 PM " + + "| End Time: October 18, 2023 at 6:00 AM | Sleeping Duration: 8 Hours "; + assertEquals(expected, sleep.toString()); + } + + @Test + public void testCalculateSleepingDuration() { + assertEquals(8, sleep.getSleepingDuration().toHours()); + } + + @Test + public void testCalculateSleepDate() throws AthletiException { + LocalDateTime sleepBefore6AM = LocalDateTime.of(2023, 10, 18, 5, 0); + Sleep sleepEarly = new Sleep(sleepBefore6AM, END_DATE_TIME); + assertEquals(sleepBefore6AM.toLocalDate().minusDays(1), sleepEarly.getSleepDate()); + + LocalDateTime sleepAfter6AM = LocalDateTime.of(2023, 10, 17, 7, 0); + Sleep sleepLate = new Sleep(sleepAfter6AM, END_DATE_TIME); + assertEquals(sleepAfter6AM.toLocalDate(), sleepLate.getSleepDate()); + } + + @Test + public void testGenerateSleepingDurationStringOutput() { + assertEquals("Sleeping Duration: 8 Hours ", sleep.generateSleepingDurationStringOutput()); + } + + @Test + public void testGenerateStartDateTimeStringOutput() { + assertEquals("Start Time: October 17, 2023 at 10:00 PM", sleep.generateStartDateTimeStringOutput()); + } + + @Test + public void testGenerateToDateTimeStringOutput() { + assertEquals("End Time: October 18, 2023 at 6:00 AM", sleep.generateEndDateTimeStringOutput()); + } + + @Test + public void testGenerateSleepDateStringOutput() { + assertEquals("Date: 2023-10-17", sleep.generateSleepDateStringOutput()); + } +} diff --git a/src/test/java/athleticli/parser/ActivityParserTest.java b/src/test/java/athleticli/parser/ActivityParserTest.java new file mode 100644 index 0000000000..6ff99a6ef4 --- /dev/null +++ b/src/test/java/athleticli/parser/ActivityParserTest.java @@ -0,0 +1,477 @@ +package athleticli.parser; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import org.junit.jupiter.api.Test; + +import athleticli.data.Goal; +import athleticli.data.activity.Activity; +import athleticli.data.activity.ActivityGoal; +import athleticli.data.activity.Run; +import athleticli.data.activity.Swim; +import athleticli.exceptions.AthletiException; + +/** + * Tests the ActivityParser class. + */ +public class ActivityParserTest { + //@@author AlWo223 + + /** + * Tests the parsing of the activity index for valid input. + * @throws AthletiException if the index is invalid. + */ + @Test + void parseActivityIndex_validIndex_returnIndex() throws AthletiException { + int expected = 5; + int actual = ActivityParser.parseActivityIndex("5"); + assertEquals(expected, actual); + } + + /** + * Tests the parsing of the activity index for invalid input. An AthletiException should be thrown. + */ + @Test + void parseActivityIndex_invalidIndex_throwAthletiException() { + assertThrows(AthletiException.class, () -> ActivityParser.parseActivityIndex("abc")); + } + + /** + * Tests the parsing of valid edit-activity command, which should not throw an exception. + */ + @Test + void parseActivityEdit_validInput_returnActivityEdit() { + String validInput = "1 Morning Run distance/10000 datetime/2021-09-01 06:00"; + assertDoesNotThrow(() -> ActivityParser.parseActivityEdit(validInput)); + } + + /** + * Tests the parsing of an invalid edit-activity command, which should throw an AthletiException. + */ + @Test + void parseActivityEdit_invalidInput_throwAthletiException() { + String invalidInput = "1 Morning Run duration/60"; + assertThrows(AthletiException.class, () -> ActivityParser.parseActivityEdit(invalidInput)); + } + + /** + * Tests the parsing of an invalid edit-run command, which should throw an AthletiException. + */ + @Test + void parseRunEdit_invalidInput_throwAthletiException() { + String invalidInput = "1 Morning Run duration/60"; + assertThrows(AthletiException.class, () -> ActivityParser.parseRunCycleEdit(invalidInput)); + } + + /** + * Tests the parsing of a valid edit-run command, which should not throw an exception. + */ + @Test + void parseRunEdit_validInput_returnRunEdit() { + String validInput = + "2 duration/02:00:00 distance/20000 datetime/2021-09-01 18:00 elevation/1000"; + assertDoesNotThrow(() -> ActivityParser.parseRunCycleEdit(validInput)); + } + + /** + * Tests the parsing of a valid edit-cycle command, which should not throw an exception. + */ + @Test + void parseCycleEdit_validInput_returnRunEdit() { + String validInput = + "2 Evening Ride datetime/2021-09-01 18:00 elevation/1000"; + assertDoesNotThrow(() -> ActivityParser.parseRunCycleEdit(validInput)); + } + + /** + * Tests the parsing of an invalid edit-cycle command, which should throw an exception. + */ + @Test + void parseCycleEdit_invalidInput_throwAthletiException() { + String invalidInput = "1 "; + assertThrows(AthletiException.class, () -> ActivityParser.parseRunCycleEdit(invalidInput)); + } + + /** + * Tests the parsing of a valid edit-swim command, which should not throw an exception. + */ + @Test + void parseSwimEdit_validInput_noExceptionThrown() { + String validInput = + "2 Evening Ride duration/02:00:00 distance/20000 datetime/2021-09-01 18:00 style/freestyle"; + assertDoesNotThrow(() -> ActivityParser.parseSwimEdit(validInput)); + } + + /** + * Tests the parsing of an invalid edit-swim command, which should throw an exception. + */ + @Test + void parseSwimEdit_invalidInput_throwAthletiException() { + String invalidInput = "1 Morning Run duration/60"; + assertThrows(AthletiException.class, () -> ActivityParser.parseRunCycleEdit(invalidInput)); + } + + /** + * Tests the correct parsing of a valid edit-activity index. + * + * @throws AthletiException If the index is invalid. + */ + @Test + void parseActivityEditIndex_validInput_returnIndex() throws AthletiException { + int expected = 5; + int actual = ActivityParser.parseActivityEditIndex("5"); + assertEquals(expected, actual); + } + + /** + * Tests the parsing of the list-activity flag with the detail flag present. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseActivityListDetail_flagPresent_returnTrue() throws AthletiException { + String input = "list-activity -d"; + assertTrue(ActivityParser.parseActivityListDetail(input)); + } + + /** + * Tests the parsing of the list-activity flag with the detail flag absent. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseActivityListDetail_flagAbsent_returnFalse() throws AthletiException { + String input = "list-activity"; + assertFalse(ActivityParser.parseActivityListDetail(input)); + } + + /** + * Tests the parsing of the valid add-activity arguments. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseActivity_validInput_activityParsed() throws AthletiException { + String validInput = "Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01 06:00"; + Activity actual = ActivityParser.parseActivity(validInput); + LocalTime duration = LocalTime.parse("01:00:00", DateTimeFormatter.ofPattern("HH:mm:ss")); + LocalDateTime time = + LocalDateTime.parse("2021-09-01 06:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + Activity expected = new Activity("Morning Run", duration, 10000, time); + assertEquals(actual.getCaption(), expected.getCaption()); + assertEquals(actual.getMovingTime(), expected.getMovingTime()); + assertEquals(actual.getDistance(), expected.getDistance()); + assertEquals(actual.getStartDateTime(), expected.getStartDateTime()); + } + + /** + * Tests the parsing of the valid set-activity-goal arguments. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseActivityGoal_validInput_activityGoalParsed() throws AthletiException { + String validInput = "sport/running type/distance period/weekly target/10000"; + ActivityGoal actual = ActivityParser.parseActivityGoal(validInput); + ActivityGoal expected = new ActivityGoal(Goal.TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 10000); + assertEquals(actual.getTimeSpan(), expected.getTimeSpan()); + assertEquals(actual.getGoalType(), expected.getGoalType()); + assertEquals(actual.getSport(), expected.getSport()); + assertEquals(actual.getTargetValue(), expected.getTargetValue()); + } + + /** + * Tests the parsing of valid sport arguments. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseSport_validInput_sportParsed() throws AthletiException { + String validInput = "running"; + ActivityGoal.Sport actual = ActivityParser.parseSport(validInput); + ActivityGoal.Sport expected = ActivityGoal.Sport.RUNNING; + assertEquals(actual, expected); + } + + /** + * Tests the parsing of aan invalid sport arguments. An AthletiException should be thrown. + */ + @Test + void parseSport_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parseSport(invalidInput)); + } + + /** + * Tests the parsing of a valid goal type argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseGoalType_validInput_goalTypeParsed() throws AthletiException { + String validInput = "distance"; + ActivityGoal.GoalType actual = ActivityParser.parseGoalType(validInput); + ActivityGoal.GoalType expected = ActivityGoal.GoalType.DISTANCE; + assertEquals(actual, expected); + } + + /** + * Tests the parsing of a valid period argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parsePeriod_validInput_periodParsed() throws AthletiException { + String validInput = "weekly"; + Goal.TimeSpan actual = ActivityParser.parsePeriod(validInput); + Goal.TimeSpan expected = Goal.TimeSpan.WEEKLY; + assertEquals(actual, expected); + } + + /** + * Tests the parsing of an invalid period argument. An AthletiException should be thrown. + */ + @Test + void parsePeriod_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parsePeriod(invalidInput)); + } + + /** + * Tests the parsing of a valid target argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseTarget_validInput_targetParsed() throws AthletiException { + String validInput = "10000"; + int actual = ActivityParser.parseTarget(validInput); + int expected = 10000; + assertEquals(actual, expected); + } + + /** + * Tests the parsing of an invalid target argument. An AthletiException should be thrown. + */ + @Test + void parseTarget_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parseTarget(invalidInput)); + } + + @Test + void parseTarget_bigIntegerInput_throwAthletiException() { + String bigIntegerInput1 = "10000000000000000000000"; + assertThrows(AthletiException.class, () -> ActivityParser.parseTarget(bigIntegerInput1)); + } + + /** + * Tests the missing activity goal arguments check for the absence of the sport argument. + */ + @Test + void checkMissingActivityGoalArguments_missingSport_throwAthletiException() { + assertThrows(AthletiException.class, () -> ActivityParser.checkMissingActivityGoalArguments(-1, 1, 1, 1)); + } + + /** + * Tests the missing activity goal arguments check for no missing arguments. + */ + @Test + void checkMissingActivityGoalArguments_noMissingArguments_noExceptionThrown() { + assertDoesNotThrow(() -> ActivityParser.checkMissingActivityGoalArguments(1, 1, 1, 1)); + } + + /** + * Tests the parsing of a valid duration argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseDuration_validInput_durationParsed() throws AthletiException { + String validInput = "01:00:00"; + LocalTime actual = ActivityParser.parseDuration(validInput); + LocalTime expected = LocalTime.parse("01:00:00", DateTimeFormatter.ofPattern("HH:mm:ss")); + assertEquals(actual, expected); + } + + /** + * Tests the parsing of an invalid duration argument. An AthletiException should be thrown. + */ + @Test + void parseDuration_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDuration(invalidInput)); + } + + /** + * Tests the parsing of a valid distance argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseDistance_validInput_distanceParsed() throws AthletiException { + String validInput = "10000"; + int actual = ActivityParser.parseDistance(validInput); + int expected = 10000; + assertEquals(actual, expected); + } + + /** + * Tests the parsing of an invalid distance argument. An AthletiException should be thrown. + */ + @Test + void parseDistance_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDistance(invalidInput)); + } + + /** + * Tests the parsing of valid add-run or add-cycle arguments. + * @throws AthletiException If the input is invalid. + */ + @Test + void parseRunCycle_validInput_activityParsed() throws AthletiException { + String validInput = + "Morning Run duration/01:00:00 distance/10000 datetime/2021-09-01 06:00 elevation/60"; + Run actual = (Run) ActivityParser.parseRunCycle(validInput, true); + LocalTime movingTime = LocalTime.parse("01:00:00", DateTimeFormatter.ofPattern("HH:mm:ss")); + LocalDateTime time = + LocalDateTime.parse("2021-09-01 06:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + Run expected = new Run("Morning Run", movingTime, 10000, time, 60); + assertEquals(actual.getCaption(), expected.getCaption()); + assertEquals(actual.getMovingTime(), expected.getMovingTime()); + assertEquals(actual.getDistance(), expected.getDistance()); + assertEquals(actual.getStartDateTime(), expected.getStartDateTime()); + assertEquals(actual.getElevationGain(), expected.getElevationGain()); + } + + /** + * Tests the parsing of a valid elevation argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseElevation_validInput_elevationParsed() throws AthletiException { + String validInput = "60"; + int actual = ActivityParser.parseElevation(validInput); + int expected = 60; + assertEquals(actual, expected); + } + + /** + * Tests the parsing of an invalid elevation argument. An AthletiException should be thrown. + */ + @Test + void parseElevation_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parseElevation(invalidInput)); + } + + /** + * Tests the parsing of valid add-swim arguments. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseSwim_validInput_swimParsed() throws AthletiException { + String validInput = + "Evening Swim duration/02:00:00 distance/20000 datetime/2021-09-01 18:00 style/freestyle"; + Swim actual = (Swim) ActivityParser.parseSwim(validInput); + LocalTime movingTime = LocalTime.parse("02:00:00", DateTimeFormatter.ofPattern("HH:mm:ss")); + LocalDateTime time = + LocalDateTime.parse("2021-09-01 18:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + Swim expected = new Swim("Evening Swim", movingTime, 20000, time, Swim.SwimmingStyle.FREESTYLE); + assertEquals(actual.getCaption(), expected.getCaption()); + assertEquals(actual.getMovingTime(), expected.getMovingTime()); + assertEquals(actual.getDistance(), expected.getDistance()); + assertEquals(actual.getStartDateTime(), expected.getStartDateTime()); + assertEquals(actual.getStyle(), expected.getStyle()); + } + + /** + * Tests the parsing of a valid swimming style argument. + * + * @throws AthletiException If the input is invalid. + */ + @Test + void parseSwimmingStyle_validInput_styleParsed() throws AthletiException { + String validInput = "freestyle"; + Swim.SwimmingStyle actual = ActivityParser.parseSwimmingStyle(validInput); + Swim.SwimmingStyle expected = Swim.SwimmingStyle.FREESTYLE; + assertEquals(actual, expected); + } + + @Test + void checkMissingActivityArgument_missingDistance_messageDistanceMissing() { + String expected = "Please specify the activity distance using \"distance/\"!"; + try { + ActivityParser.checkMissingActivityArgument(-1, "distance/"); + } catch (AthletiException e) { + assertEquals(expected, e.getMessage()); + } + } + + //@@author nihalzp + @Test + void parseDeleteActivityGoal_validInput_activityGoalParsed() throws AthletiException { + String validInput = "sport/running type/distance period/weekly"; + ActivityGoal actual = ActivityParser.parseDeleteActivityGoal(validInput); + ActivityGoal expected = new ActivityGoal(Goal.TimeSpan.WEEKLY, ActivityGoal.GoalType.DISTANCE, + ActivityGoal.Sport.RUNNING, 0); + assertEquals(actual.getTimeSpan(), expected.getTimeSpan()); + assertEquals(actual.getGoalType(), expected.getGoalType()); + assertEquals(actual.getSport(), expected.getSport()); + assertEquals(actual.getTargetValue(), expected.getTargetValue()); + } + + @Test + void parseDeleteActivityGoal_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } + + @Test + void parseDeleteActivityGoal_missingSport_throwAthletiException() { + String invalidInput = "type/distance period/weekly"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } + + @Test + void parseDeleteActivityGoal_missingType_throwAthletiException() { + String invalidInput = "sport/running period/weekly"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } + + @Test + void parseDeleteActivityGoal_missingPeriod_throwAthletiException() { + String invalidInput = "sport/running type/distance"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } + + @Test + void parseDeleteActivityGoal_missingSportAndType_throwAthletiException() { + String invalidInput = "period/weekly"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } + + @Test + void parseDeleteActivityGoal_missingSportAndPeriod_throwAthletiException() { + String invalidInput = "type/distance"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } + + @Test + void parseDeleteActivityGoal_missingTypeAndPeriod_throwAthletiException() { + String invalidInput = "sport/running"; + assertThrows(AthletiException.class, () -> ActivityParser.parseDeleteActivityGoal(invalidInput)); + } +} diff --git a/src/test/java/athleticli/parser/DietParserTest.java b/src/test/java/athleticli/parser/DietParserTest.java new file mode 100644 index 0000000000..2f5a5617d6 --- /dev/null +++ b/src/test/java/athleticli/parser/DietParserTest.java @@ -0,0 +1,318 @@ +package athleticli.parser; + +import athleticli.data.diet.DietGoal; +import athleticli.data.diet.UnhealthyDietGoal; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +import static athleticli.parser.DietParser.checkDuplicateDietArguments; +import static athleticli.parser.DietParser.checkEmptyDietArguments; +import static athleticli.parser.DietParser.checkMissingDietArguments; +import static athleticli.parser.DietParser.isArgumentDuplicate; +import static athleticli.parser.DietParser.isArgumentMissing; +import static athleticli.parser.DietParser.parseDiet; +import static athleticli.parser.DietParser.parseDietEdit; +import static athleticli.parser.DietParser.parseDietGoalDelete; +import static athleticli.parser.DietParser.parseDietGoalSetAndEdit; +import static athleticli.parser.DietParser.parseDietIndex; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DietParserTest { + //@@author nihalzp + @Test + void checkEmptyDietArguments_emptyCalories_throwAthletiException() { + String emptyCalories = ""; + String nonEmptyProtein = "1"; + String nonEmptyCarb = "2"; + String nonEmptyFat = "3"; + String nonEmptyDatetime = "2021-10-06 10:00"; + assertThrows(AthletiException.class, + () -> checkEmptyDietArguments(emptyCalories, nonEmptyProtein, nonEmptyCarb, nonEmptyFat, + nonEmptyDatetime)); + } + + @Test + void checkEmptyDietArguments_emptyProtein_throwAthletiException() { + String nonEmptyCalories = "1"; + String emptyProtein = ""; + String nonEmptyCarb = "2"; + String nonEmptyFat = "3"; + String nonEmptyDatetime = "2021-10-06 10:00"; + assertThrows(AthletiException.class, + () -> checkEmptyDietArguments(nonEmptyCalories, emptyProtein, nonEmptyCarb, nonEmptyFat, + nonEmptyDatetime)); + } + + @Test + void checkEmptyDietArguments_emptyCarb_throwAthletiException() { + String nonEmptyCalories = "1"; + String nonEmptyProtein = "2"; + String emptyCarb = ""; + String nonEmptyFat = "3"; + String nonEmptyDatetime = "2021-10-06 10:00"; + assertThrows(AthletiException.class, + () -> checkEmptyDietArguments(nonEmptyCalories, nonEmptyProtein, emptyCarb, nonEmptyFat, + nonEmptyDatetime)); + } + + @Test + void checkEmptyDietArguments_emptyFat_throwAthletiException() { + String nonEmptyCalories = "1"; + String nonEmptyProtein = "2"; + String nonEmptyCarb = "3"; + String emptyFat = ""; + String nonEmptyDatetime = "2021-10-06 10:00"; + assertThrows(AthletiException.class, + () -> checkEmptyDietArguments(nonEmptyCalories, nonEmptyProtein, nonEmptyCarb, emptyFat, + nonEmptyDatetime)); + } + + @Test + void checkEmptyDietArguments_emptyDatetime_throwAthletiException() { + String nonEmptyCalories = "1"; + String nonEmptyProtein = "2"; + String nonEmptyCarb = "3"; + String nonEmptyFat = "4"; + String emptyDatetime = ""; + assertThrows(AthletiException.class, + () -> checkEmptyDietArguments(nonEmptyCalories, nonEmptyProtein, nonEmptyCarb, nonEmptyFat, + emptyDatetime)); + } + + + @Test + void parseDietEdit_validInput_returnDietEdit() throws AthletiException { + String validInput = "2 calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + HashMap actual = parseDietEdit(validInput); + HashMap expected = new HashMap<>(); + expected.put(Parameter.CALORIES_SEPARATOR, "1"); + expected.put(Parameter.PROTEIN_SEPARATOR, "2"); + expected.put(Parameter.CARB_SEPARATOR, "3"); + expected.put(Parameter.FAT_SEPARATOR, "4"); + expected.put(Parameter.DATETIME_SEPARATOR, "2023-10-06T10:00"); + assertEquals(expected, actual); + } + + @Test + void parseDietEdit_someMarkersPresent_returnDietEdit() throws AthletiException { + String validInput = "2 calories/1 protein/2 carb/3"; + HashMap actual = parseDietEdit(validInput); + HashMap expected = new HashMap<>(); + expected.put(Parameter.CALORIES_SEPARATOR, "1"); + expected.put(Parameter.PROTEIN_SEPARATOR, "2"); + expected.put(Parameter.CARB_SEPARATOR, "3"); + assertEquals(expected, actual); + } + + @Test + void parseDietEdit_zeroValidInput_throwAthletiException() { + String invalidInput = "2 calorie/1 proteins/2 carbs/3 fat/4 datetime/2023-10-06"; + assertThrows(AthletiException.class, () -> parseDietEdit(invalidInput)); + } + + @Test + void parseDietIndex_validIndex_returnIndex() throws AthletiException { + int expected = 5; + int actual = parseDietIndex("5"); + assertEquals(expected, actual); + } + + @Test + void parseDietIndex_nonIntegerInput_throwAthletiException() { + String nonIntegerInput = "nonInteger"; + assertThrows(AthletiException.class, () -> parseDietIndex(nonIntegerInput)); + } + + @Test + void parseDietIndex_nonPositiveIntegerInput_throwAthletiException() { + String nonIntegerInput = "0"; + assertThrows(AthletiException.class, () -> parseDietIndex(nonIntegerInput)); + } + + @Test + void parseDietIndex_bigIntegerInput_throwAthletiException() { + String bigIntegerInput = "10000000000000000000000"; + assertThrows(AthletiException.class, () -> parseDietIndex(bigIntegerInput)); + } + + @Test + void parseDiet_emptyInput_throwAthletiException() { + String emptyInput = ""; + assertThrows(AthletiException.class, () -> parseDiet(emptyInput)); + } + + @Test + void isArgumentMissing_markerPresent_returnFalse() { + String commandArgs = "protein/1 carb/2 fat/3 datetime/2021-10-06 10:00"; + String marker = "carb/"; + assert (!isArgumentMissing(commandArgs, marker)); + } + + @Test + void isArgumentMissing_markerNotPresent_returnTrue() { + String commandArgs = "protein/1 carb/2 fat/3 datetime/2021-10-06 10:00"; + String marker = "calories/"; + assert (isArgumentMissing(commandArgs, marker)); + } + + @Test + void isArgumentDuplicate_markerDuplicated_returnTrue() { + String commandArgs = "protein/1 carb/2 protein/5 fat/3 datetime/2021-10-06 10:00"; + String marker = "protein/"; + assert (isArgumentDuplicate(commandArgs, marker)); + } + + @Test + void isArgumentDuplicate_markerNotDuplicated_returnFalse() { + String commandArgs = "protein/1 carb/2 calories/5 fat/3 datetime/2021-10-06 10:00"; + String marker = "calories/"; + assert (!isArgumentDuplicate(commandArgs, marker)); + } + + @Test + void isArgumentDuplicate_markerNotPresent_returnFalse() { + String commandArgs = "protein/1 carb/2 fat/3 datetime/2021-10-06 10:00"; + String marker = "calories/"; + assert (!isArgumentDuplicate(commandArgs, marker)); + } + + @Test + void checkMissingDietArguments_noMissingArguments_noExceptionThrown() throws AthletiException { + String noMissingArguments = "calories/1 protein/2 carb/3 fat/4 datetime/2021-10-06 10:00"; + checkMissingDietArguments(noMissingArguments); + } + + @Test + void checkMissingDietArguments_missingCalories_throwAthletiException() { + String missingCalories = "protein/1 carb/2 fat/3 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkMissingDietArguments(missingCalories)); + } + + @Test + void checkMissingDietArguments_missingProtein_throwAthletiException() { + String missingProtein = "calories/1 carb/2 fat/3 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkMissingDietArguments(missingProtein)); + } + + @Test + void checkMissingDietArguments_missingCarb_throwAthletiException() { + String missingCarb = "calories/1 protein/2 fat/3 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkMissingDietArguments(missingCarb)); + } + + @Test + void checkMissingDietArguments_missingFat_throwAthletiException() { + String missingFat = "calories/1 protein/2 carb/3 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkMissingDietArguments(missingFat)); + } + + @Test + void checkMissingDietArguments_missingDatetime_throwAthletiException() { + String missingDatetime = "calories/1 protein/2 carb/3 fat/4"; + assertThrows(AthletiException.class, () -> checkMissingDietArguments(missingDatetime)); + } + + + @Test + void checkDuplicateDietArguments_noDuplicateArguments_noExceptionThrown() throws AthletiException { + String noDuplicateArguments = "calories/1 protein/2 carb/3 fat/4 datetime/2021-10-06 10:00"; + checkMissingDietArguments(noDuplicateArguments); + } + + @Test + void checkDuplicateDietArguments_duplicateCalories_throwAthletiException() { + String duplicateCalories = "calories/1 calories/2 protein/2 carb/3 fat/4 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkDuplicateDietArguments(duplicateCalories)); + } + + @Test + void checkDuplicateDietArguments_duplicateProtein_throwAthletiException() { + String duplicateProtein = "calories/1 protein/2 protein/2 carb/3 fat/4 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkDuplicateDietArguments(duplicateProtein)); + } + + @Test + void checkDuplicateDietArguments_duplicateCarb_throwAthletiException() { + String duplicateCarb = "calories/1 protein/2 carb/3 carb/3 fat/4 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkDuplicateDietArguments(duplicateCarb)); + } + + @Test + void checkDuplicateDietArguments_duplicateFat_throwAthletiException() { + String duplicateFat = "calories/1 protein/2 carb/3 fat/4 fat/4 datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkDuplicateDietArguments(duplicateFat)); + } + + @Test + void checkDuplicateDietArguments_duplicateDatetime_throwAthletiException() { + String duplicateDatetime = + "calories/1 protein/2 carb/3 fat/4 datetime/2021-10-06 10:00 " + "datetime/2021-10-06 10:00"; + assertThrows(AthletiException.class, () -> checkDuplicateDietArguments(duplicateDatetime)); + } + + //@@author yicheng-toh + @Test + void parseDietGoalSetEdit_unhealthyDietGoal_expectUnhealthyDietGoal() throws AthletiException { + String oneValidOneInvalidGoalString = "WEEKLY unhealthy fat/20"; + ArrayList dietGoals = parseDietGoalSetAndEdit(oneValidOneInvalidGoalString); + assert dietGoals.get(0) instanceof UnhealthyDietGoal; + } + + @Test + void parseDietGoalSetEdit_noInput_throwAthletiException() { + String invalidGoalString = " "; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalSetEdit_inputHasNoTimeSpan_throwAthletiException() { + String invalidGoalString = "fat/10"; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalSetEdit_oneValidOneInvalidGoal_throwAthletiException() { + String invalidGoalString = "calories/60 protein/protine"; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalSetEdit_zeroTargetValue_throwAthletiException() { + String invalidGoalString = "WEEKLY calories/0"; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalSetEdit_oneInvalidGoal_throwAthletiException() { + String invalidGoalString = "WEEKLY calories/caloreis protein/protein"; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalSetEdit_repeatedDietGoal_throwAthletiException() { + String invalidGoalString = "WEEKLY calories/1 calories/1"; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalSetEdit_invalidNutrient_throwAthletiException() { + String invalidGoalString = "WEEKLY calorie/1"; + assertThrows(AthletiException.class, () -> parseDietGoalSetAndEdit(invalidGoalString)); + } + + @Test + void parseDietGoalDelete_nonIntegerInput_throwAthletiException() { + String nonIntegerInput = "nonInteger"; + assertThrows(AthletiException.class, () -> parseDietGoalDelete(nonIntegerInput)); + } + + @Test + void parseDietGoalDelete_nonPositiveIntegerInput_throwAthletiException() { + String nonIntegerInput = "0"; + assertThrows(AthletiException.class, () -> parseDietGoalDelete(nonIntegerInput)); + } +} diff --git a/src/test/java/athleticli/parser/ParserTest.java b/src/test/java/athleticli/parser/ParserTest.java new file mode 100644 index 0000000000..f25ab51ea1 --- /dev/null +++ b/src/test/java/athleticli/parser/ParserTest.java @@ -0,0 +1,835 @@ +package athleticli.parser; + +import athleticli.commands.ByeCommand; +import athleticli.commands.activity.DeleteActivityGoalCommand; +import athleticli.commands.activity.EditActivityGoalCommand; +import athleticli.commands.activity.ListActivityGoalCommand; +import athleticli.commands.diet.AddDietCommand; +import athleticli.commands.diet.DeleteDietCommand; +import athleticli.commands.diet.DeleteDietGoalCommand; +import athleticli.commands.diet.EditDietCommand; +import athleticli.commands.diet.EditDietGoalCommand; +import athleticli.commands.diet.FindDietCommand; +import athleticli.commands.diet.ListDietCommand; +import athleticli.commands.diet.ListDietGoalCommand; +import athleticli.commands.diet.SetDietGoalCommand; +import athleticli.commands.sleep.AddSleepCommand; +import athleticli.commands.sleep.DeleteSleepCommand; +import athleticli.commands.sleep.EditSleepCommand; +import athleticli.commands.sleep.ListSleepCommand; +import athleticli.exceptions.AthletiException; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static athleticli.common.Config.DATE_TIME_FORMATTER; +import static athleticli.parser.Parser.getValueForMarker; +import static athleticli.parser.Parser.parseCommand; +import static athleticli.parser.Parser.parseDate; +import static athleticli.parser.Parser.parseNonNegativeInteger; +import static athleticli.parser.Parser.splitCommandWordAndArgs; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ParserTest { + @Test + void splitCommandWordAndArgs_noArgs_expectTwoParts() { + final String commandWithNoArgs = "bye"; + assertEquals(splitCommandWordAndArgs(commandWithNoArgs).length, 2); + } + + @Test + void splitCommandWordAndArgs_multipleArgs_expectTwoParts() { + final String commandWithMultipleArgs = "set-diet-goal calories/1 carb/3"; + assertEquals(splitCommandWordAndArgs(commandWithMultipleArgs).length, 2); + } + + @Test + void parseCommand_unknownCommand_expectAthletiException() { + final String unknownCommand = "hello"; + assertThrows(AthletiException.class, () -> parseCommand(unknownCommand)); + } + + @Test + void parseCommand_byeCommand_expectByeCommand() throws AthletiException { + final String byeCommand = "bye"; + assertInstanceOf(ByeCommand.class, parseCommand(byeCommand)); + } + + @Test + void parseCommand_addSleepCommand_expectAddSleepCommand() throws AthletiException { + final String addSleepCommandString = "add-sleep start/2023-10-06 10:00 end/2023-10-06 11:00"; + assertInstanceOf(AddSleepCommand.class, parseCommand(addSleepCommandString)); + } + + @Test + void parseCommand_addSleepCommand_missingStartExpectAthletiException() { + final String addSleepCommandString = "add-sleep end/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addSleepCommandString)); + } + + @Test + void parseCommand_addSleepCommand_missingEndExpectAthletiException() { + final String addSleepCommandString = "add-sleep start/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addSleepCommandString)); + } + + @Test + void parseCommand_addSleepCommand_missingBothExpectAthletiException() { + final String addSleepCommandString = "add-sleep start/ end/"; + assertThrows(AthletiException.class, () -> parseCommand(addSleepCommandString)); + } + + @Test + void parseCommand_addSleepCommand_invalidDatetimeExpectAthletiException() { + final String addSleepCommandString = "add-sleep start/07-10-2021 06:00 end/07-10-2021 05:00"; + assertThrows(AthletiException.class, () -> parseCommand(addSleepCommandString)); + } + + @Test + void parseCommand_editSleepCommand_expectEditSleepCommand() throws AthletiException { + final String editSleepCommandString = "edit-sleep 1 start/2023-10-06 10:00 end/2023-10-06 11:00"; + assertInstanceOf(EditSleepCommand.class, parseCommand(editSleepCommandString)); + } + + @Test + void parseCommand_editSleepCommand_missingStartExpectAthletiException() { + final String editSleepCommandString = "edit-sleep 1 end/07-10-2021 06:00"; + assertThrows(AthletiException.class, () -> parseCommand(editSleepCommandString)); + } + + @Test + void parseCommand_editSleepCommand_missingEndExpectAthletiException() { + final String editSleepCommandString = "edit-sleep 1 start/07-10-2021 06:00"; + assertThrows(AthletiException.class, () -> parseCommand(editSleepCommandString)); + } + + @Test + void parseCommand_editSleepCommand_missingBothExpectAthletiException() { + final String editSleepCommandString = "edit-sleep 1 start/ end/"; + assertThrows(AthletiException.class, () -> parseCommand(editSleepCommandString)); + } + + @Test + void parseCommand_editSleepCommand_invalidDatetimeExpectAthletiException() { + final String editSleepCommandString = "edit-sleep 1 start/07-10-2021 07:00 end/07-10-2021 06:00"; + assertThrows(AthletiException.class, () -> parseCommand(editSleepCommandString)); + } + + @Test + void parseCommand_editSleepCommand_invalidIndexExpectAthletiException() { + final String editSleepCommandString = "edit-sleep abc start/06-10-2021 10:00 end/07-10-2021 06:00"; + assertThrows(AthletiException.class, () -> parseCommand(editSleepCommandString)); + } + + @Test + void parseCommand_deleteSleepCommand_expectDeleteSleepCommand() throws AthletiException { + final String deleteSleepCommandString = "delete-sleep 1"; + assertInstanceOf(DeleteSleepCommand.class, parseCommand(deleteSleepCommandString)); + } + + @Test + void parseCommand_deleteSleepCommand_invalidIndexExpectAthletiException() { + final String deleteSleepCommandString = "delete-sleep abc"; + assertThrows(AthletiException.class, () -> parseCommand(deleteSleepCommandString)); + } + + @Test + void parseCommand_listSleepCommand_expectListSleepCommand() throws AthletiException { + final String listSleepCommandString = "list-sleep"; + assertInstanceOf(ListSleepCommand.class, parseCommand(listSleepCommandString)); + } + + @Test + void parseCommand_setDietGoalCommand_expectSetDietGoalCommand() throws AthletiException { + final String setDietGoalCommandString = "set-diet-goal weekly calories/1 protein/2 carb/3"; + assertInstanceOf(SetDietGoalCommand.class, parseCommand(setDietGoalCommandString)); + } + + @Test + void parseCommand_editDietCommand_expectEditDietGoalCommand() throws AthletiException { + final String editDietGoalCommandString = "edit-diet-goal weekly calories/1 protein/2 carb/3"; + assertInstanceOf(EditDietGoalCommand.class, parseCommand(editDietGoalCommandString)); + } + + @Test + void parseCommand_listDietGoalCommand_expectListDietGoalCommand() throws AthletiException { + final String listDietCommandString = "list-diet-goal"; + assertInstanceOf(ListDietGoalCommand.class, parseCommand(listDietCommandString)); + } + + @Test + void parseCommand_deleteDietGoalCommand_expectDeleteDietGoalCommand() throws AthletiException { + final String deleteDietGoalCommandString = "delete-diet-goal 1"; + assertInstanceOf(DeleteDietGoalCommand.class, parseCommand(deleteDietGoalCommandString)); + } + + @Test + void parseCommand_addDietCommand_expectAddDietCommand() throws AthletiException { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertInstanceOf(AddDietCommand.class, parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_missingCaloriesExpectAthletiException() { + final String addDietCommandString = "add-diet protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_missingProteinExpectAthletiException() { + final String addDietCommandString = "add-diet calories/1 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_missingCarbExpectAthletiException() { + final String addDietCommandString = "add-diet calories/1 protein/2 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_missingFatExpectAthletiException() { + final String addDietCommandString = "add-diet calories/1 protein/2 carb/3 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_missingDateTimeExpectAthletiException() { + final String addDietCommandString = "add-diet calories/1 protein/2 carb/3 fat/4"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_emptyCaloriesExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/ protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_emptyProteinExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/ carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_emptyCarbExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/ fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_emptyFatExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 fat/ datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_emptyDateTimeExpectAthletiException() { + final String addDietCommandString = "add-diet calories/1 protein/2 carb/3 fat/4 datetime/"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_invalidCaloriesExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/abc protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_invalidProteinExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/abc carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_invalidCarbExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/abc fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_invalidFatExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 fat/abc datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_invalidDateTimeFormatExpectAthletiException() { + final String addDietCommandString1 = "add-diet calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06"; + final String addDietCommandString2 = "add-diet calories/1 protein/2 carb/3 fat/4 datetime/10:00"; + final String addDietCommandString3 = + "add-diet calories/1 protein/2 carb/3 fat/4 datetime/16-10-2023 10:00:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString1)); + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString2)); + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString3)); + } + + @Test + void parseCommand_addDietCommand_negativeCaloriesExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/-1 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_negativeProteinExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/-2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_negativeCarbExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/-3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_negativeFatExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 fat/-4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_duplicatedCaloriesExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 calories/2 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_duplicatedProteinExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 protein/3 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_duplicatedCarbExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 carb/4 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_duplicatedFatExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 fat/4 fat/5 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_addDietCommand_duplicatedDateTimeExpectAthletiException() { + final String addDietCommandString = + "add-diet calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00 datetime/2023-10-06 " + + "11:00"; + assertThrows(AthletiException.class, () -> parseCommand(addDietCommandString)); + } + + @Test + void parseCommand_deleteDietCommand_expectDeleteDietCommand() throws AthletiException { + final String deleteDietCommandString = "delete-diet 1"; + assertInstanceOf(DeleteDietCommand.class, parseCommand(deleteDietCommandString)); + } + + @Test + void parseCommand_deleteDietCommand_invalidIndexExpectAthletiException() { + final String deleteDietCommandString = "delete-diet abc"; + assertThrows(AthletiException.class, () -> parseCommand(deleteDietCommandString)); + } + + @Test + void parseCommand_deleteDietCommand_emptyIndexExpectAthletiException() { + final String deleteDietCommandString = "delete-diet"; + assertThrows(AthletiException.class, () -> parseCommand(deleteDietCommandString)); + } + + @Test + void parseCommand_listDietCommand_expectListDietCommand() throws AthletiException { + final String listDietCommandString = "list-diet"; + assertInstanceOf(ListDietCommand.class, parseCommand(listDietCommandString)); + } + + @Test + void parseCommand_editDietCommand_expectEditDietCommand() throws AthletiException { + final String editDietCommandString = + "edit-diet 1 calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertInstanceOf(EditDietCommand.class, parseCommand(editDietCommandString)); + } + + @Test + void parseCommand_editDietCommand_invalidCaloriesExpectAthletiException() { + final String editDietCommandString = + "edit-diet 1 calories/abc protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString)); + } + + @Test + void parseCommand_editDietCommand_invalidProteinExpectAthletiException() { + final String editDietCommandString = + "edit-diet 1 calories/1 protein/abc carb/3 fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString)); + } + + @Test + void parseCommand_editDietCommand_invalidCarbExpectAthletiException() { + final String editDietCommandString = + "edit-diet 1 calories/1 protein/2 carb/abc fat/4 datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString)); + } + + @Test + void parseCommand_editDietCommand_invalidFatExpectAthletiException() { + final String editDietCommandString = + "edit-diet 1 calories/1 protein/2 carb/3 fat/abc datetime/2023-10-06 10:00"; + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString)); + } + + @Test + void parseCommand_editDietCommandMissingAllArgs_expectAthletiException() { + final String editDietCommandString = "edit-diet 1"; + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString)); + } + + @Test + void parseCommand_findDietCommand_expectFindDietCommand() throws AthletiException { + final String findDietCommandString = "find-diet 2021-09-01"; + assertInstanceOf(FindDietCommand.class, parseCommand(findDietCommandString)); + } + + @Test + void parseCommand_findDietCommand_missingDateExpectAthletiException() { + final String findDietCommandString = "find-diet"; + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString)); + } + + @Test + void parseCommand_findDietCommand_invalidDateExpectAthletiException() { + final String findDietCommandString1 = "find-diet 2021-09-01 06:00"; + final String findDietCommandString2 = "find-diet 2021-09-01 06:00:00"; + final String findDietCommandString3 = "find-diet 2021-09-01 06:00:00.000"; + final String findDietCommandString4 = "find-diet 01-09-2021"; + final String findDietCommandString5 = "find-diet 09-30-2021"; + final String findDietCommandString6 = "find-diet abc"; + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString1)); + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString2)); + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString3)); + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString4)); + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString5)); + assertThrows(AthletiException.class, () -> parseCommand(findDietCommandString6)); + } + + @Test + void parseCommand_editDietCommand_invalidDateTimeFormatExpectAthletiException() { + final String editDietCommandString1 = + "edit-diet 1 calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06"; + final String editDietCommandString2 = "edit-diet 1 calories/1 protein/2 carb/3 fat/4 datetime/10:00"; + final String editDietCommandString3 = + "edit-diet 1 calories/1 protein/2 carb/3 fat/4 datetime/16-10-2023 10:00:00"; + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString1)); + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString2)); + assertThrows(AthletiException.class, () -> parseCommand(editDietCommandString3)); + } + + @Test + void parseDateTime_validInput_dateTimeParsed() throws AthletiException { + String validInput = "2021-09-01 06:00"; + LocalDateTime actual = Parser.parseDateTime(validInput); + LocalDateTime expected = LocalDateTime.parse("2021-09-01 06:00", DATE_TIME_FORMATTER); + assertEquals(actual, expected); + } + + @Test + void parseDateTime_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> Parser.parseDateTime(invalidInput)); + } + + @Test + void parseDateTime_futureDateTime_throwAthletiException() { + LocalDateTime futureDateTime = LocalDateTime.now().plusHours(1); + assertThrows(AthletiException.class, () -> Parser.parseDateTime(futureDateTime.toString())); + } + + @Test + void parseDateTime_invalidInputWithSecond_throwAthletiException() { + String invalidInput = "2021-09-01 06:00:00"; + assertThrows(AthletiException.class, () -> Parser.parseDateTime(invalidInput)); + } + + @Test + void parseDateTime_invalidYear_throwAthletiException() { + String invalidInput = "0000-01-01 00:01"; + assertThrows(AthletiException.class, () -> Parser.parseDateTime(invalidInput)); + } + + @Test + void parseDateTime_invalidLeapYear_throwAthletiException() { + String invalidInput = "2021-02-29 00:01"; + assertThrows(AthletiException.class, () -> Parser.parseDateTime(invalidInput)); + } + + @Test + void parseDate_validInput_dateParsed() throws AthletiException { + String validInput = "2021-09-01"; + LocalDate actual = parseDate(validInput); + LocalDate expected = LocalDate.parse("2021-09-01"); + assertEquals(actual, expected); + } + + @Test + void parseDate_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, () -> parseDate(invalidInput)); + } + + @Test + void parseDate_invalidInputWithTime_throwAthletiException() { + String invalidInput = "2021-09-01 06:00"; + assertThrows(AthletiException.class, () -> parseDate(invalidInput)); + } + + @Test + void parseDate_futureDate_throwAthletiException() { + LocalDate futureDate = LocalDate.now().plusDays(1); + assertThrows(AthletiException.class, () -> parseDate(futureDate.toString())); + } + + @Test + void parseDate_invalidYear_throwAthletiException() { + String invalidInput = "0000-01-01"; + assertThrows(AthletiException.class, () -> parseDate(invalidInput)); + } + + @Test + void parseDate_invalidLeapYear_throwAthletiException() { + String invalidInput = "2021-02-29"; + assertThrows(AthletiException.class, () -> parseDate(invalidInput)); + } + + @Test + void parseCommand_editActivityGoalCommand_expectEditActivityGoalCommand() throws AthletiException { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/weekly target/20000"; + assertInstanceOf(EditActivityGoalCommand.class, parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandMissingSport_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal type/distance period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandMissingType_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandMissingPeriod_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandMissingTarget_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandEmptySport_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/ type/distance period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandEmptyType_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/ period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandEmptyPeriod_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/ target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandEmptyTarget_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/weekly target/"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidSport_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/abc type/distance period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidType_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/abc period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidPeriod_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/abc target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidTarget_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/weekly target/abc"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidSportAndType_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/abc type/abc period/weekly target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidSportAndPeriod_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/abc type/distance period/abc target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidSportAndTarget_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/abc type/distance period/weekly target/abc"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidTypeAndPeriod_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/abc period/abc target/20000"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidTypeAndTarget_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/abc period/weekly target/abc"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_editActivityGoalCommandInvalidPeriodAndTarget_expectAthletiException() { + final String editActivityGoalCommandString = + "edit-activity-goal sport/running type/distance period/abc target/abc"; + assertThrows(AthletiException.class, () -> parseCommand(editActivityGoalCommandString)); + } + + @Test + void parseCommand_listActivityGoalCommand_expectListActivityGoalCommand() throws AthletiException { + final String listActivityGoalCommandString = "list-activity-goal"; + assertInstanceOf(ListActivityGoalCommand.class, parseCommand(listActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommand_expectDeleteActivityGoalCommand() throws AthletiException { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/running type/distance period/weekly"; + assertInstanceOf(DeleteActivityGoalCommand.class, parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandMissingSport_expectAthletiException() { + final String deleteActivityGoalCommandString = "delete-activity-goal type/distance period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandMissingType_expectAthletiException() { + final String deleteActivityGoalCommandString = "delete-activity-goal sport/running period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandMissingPeriod_expectAthletiException() { + final String deleteActivityGoalCommandString = "delete-activity-goal sport/running type/distance"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandEmptySport_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/ type/distance period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandEmptyType_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/running type/ period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandEmptyPeriod_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/running type/distance period/"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidSport_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/abc type/distance period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidType_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/running type/abc period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidPeriod_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/running type/distance period/abc"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidSportAndType_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/abc type/abc period/weekly"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidSportAndPeriod_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/abc type/distance period/abc"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidTypeAndPeriod_expectAthletiException() { + final String deleteActivityGoalCommandString = + "delete-activity-goal sport/running type/abc period/abc"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseCommand_deleteActivityGoalCommandInvalidSportAndTypeAndPeriod_expectAthletiException() { + final String deleteActivityGoalCommandString = "delete-activity-goal sport/abc type/abc period/abc"; + assertThrows(AthletiException.class, () -> parseCommand(deleteActivityGoalCommandString)); + } + + @Test + void parseNonNegativeInteger_validInput_integerParsed() throws AthletiException { + String validInput = "123"; + int actual = parseNonNegativeInteger(validInput, "invalid", "overflow"); + assertEquals(123, actual); + } + + @Test + void parseNonNegativeInteger_invalidInput_throwAthletiException() { + String invalidInput = "abc"; + assertThrows(AthletiException.class, + () -> parseNonNegativeInteger(invalidInput, "invalid", "overflow")); + } + + @Test + void parseNonNegativeInteger_negativeInput_throwAthletiException() { + String negativeInput = "-1"; + assertThrows(AthletiException.class, + () -> parseNonNegativeInteger(negativeInput, "invalid", "overflow")); + } + + @Test + void parseNonNegativeInteger_overflowInput_throwAthletiException() { + String overflowInput = "2147483648"; + assertThrows(AthletiException.class, + () -> parseNonNegativeInteger(overflowInput, "invalid", "overflow")); + } + + @Test + void parseNonNegativeInteger_zeroInput_integerParsed() throws AthletiException { + String zeroInput = "0"; + int actual = parseNonNegativeInteger(zeroInput, "invalid", "overflow"); + assertEquals(0, actual); + } + + @Test + void parseNonNegativeInteger_emptyInput_throwAthletiException() { + String emptyInput = ""; + assertThrows(AthletiException.class, + () -> parseNonNegativeInteger(emptyInput, "invalid", "overflow")); + } + + @Test + void parseNonNegativeInteger_floatingPointInput_throwAthletiException() { + String floatingPointInput = "1.0"; + assertThrows(AthletiException.class, + () -> parseNonNegativeInteger(floatingPointInput, "invalid", "overflow")); + } + + @Test + void getValueForMarker_validInput_returnValue() { + String validInput = "2 calories/1 protein/2 carb/3 fat/4 datetime/2023-10-06 10:00"; + String caloriesActual = getValueForMarker(validInput, Parameter.CALORIES_SEPARATOR); + String proteinActual = getValueForMarker(validInput, Parameter.PROTEIN_SEPARATOR); + String carbActual = getValueForMarker(validInput, Parameter.CARB_SEPARATOR); + String fatActual = getValueForMarker(validInput, Parameter.FAT_SEPARATOR); + String datetimeActual = getValueForMarker(validInput, Parameter.DATETIME_SEPARATOR); + assertEquals("1", caloriesActual); + assertEquals("2", proteinActual); + assertEquals("3", carbActual); + assertEquals("4", fatActual); + assertEquals("2023-10-06 10:00", datetimeActual); + } + + @Test + void getValueForMarker_invalidInput_returnEmptyString() { + String invalidInput = "2 calorie/1 proteins/2 carbs/3 fats/4 date/2023-10-06"; + String caloriesActual = getValueForMarker(invalidInput, Parameter.CALORIES_SEPARATOR); + String proteinActual = getValueForMarker(invalidInput, Parameter.PROTEIN_SEPARATOR); + String carbActual = getValueForMarker(invalidInput, Parameter.CARB_SEPARATOR); + String fatActual = getValueForMarker(invalidInput, Parameter.FAT_SEPARATOR); + String datetimeActual = getValueForMarker(invalidInput, Parameter.DATETIME_SEPARATOR); + assertEquals("", caloriesActual); + assertEquals("", proteinActual); + assertEquals("", carbActual); + assertEquals("", fatActual); + assertEquals("", datetimeActual); + } +} diff --git a/src/test/java/athleticli/parser/SleepParserTest.java b/src/test/java/athleticli/parser/SleepParserTest.java new file mode 100644 index 0000000000..d0aa6a61b2 --- /dev/null +++ b/src/test/java/athleticli/parser/SleepParserTest.java @@ -0,0 +1,64 @@ +package athleticli.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.junit.jupiter.api.Test; + +import athleticli.data.Goal; +import athleticli.data.sleep.Sleep; +import athleticli.data.sleep.SleepGoal; +import athleticli.exceptions.AthletiException; + + + +public class SleepParserTest { + + @Test + void parseSleep_validInput_success() throws AthletiException { + String input = "start/2020-10-10 10:00 end/2020-10-10 11:00"; + Sleep sleep = SleepParser.parseSleep(input); + LocalDateTime start = LocalDateTime.parse("2020-10-10 10:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + LocalDateTime end = LocalDateTime.parse("2020-10-10 11:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + assertEquals(start, sleep.getStartDateTime()); + assertEquals(end, sleep.getEndDateTime()); + } + + @Test + void parseSleep_invalidInput_throwsException() { + String input = "start/2020-10-10 10:00 end/2020-10-10 09:00"; + assertThrows(AthletiException.class, () -> SleepParser.parseSleep(input)); + } + + @Test + void parseSleepIndex_validInput_success() throws AthletiException { + String input = "1 start/2020-10-10 10:00 end/2020-10-10 11:00"; + int index = SleepParser.parseSleepIndex(input); + assertEquals(1, index); + } + + @Test + void parseSleepIndex_invalidInput_throwsException() { + String input = "-1000 start/2020-10-10 10:00 end/2020-10-10 11:00"; + assertThrows(AthletiException.class, () -> SleepParser.parseSleepIndex(input)); + } + + @Test + void parseSleepGoal_validInput_success() throws AthletiException { + String input = "type/duration period/daily target/10000"; + SleepGoal goal = SleepParser.parseSleepGoal(input); + assertEquals(SleepGoal.GoalType.DURATION, goal.getGoalType()); + assertEquals(Goal.TimeSpan.DAILY, goal.getTimeSpan()); + assertEquals(10000, goal.getTargetValue()); + } + + @Test + void parseSleepGoal_invalidInput_throwsException() { + String input = "type/duration period/daily target/-1000"; + assertThrows(AthletiException.class, () -> SleepParser.parseSleepGoal(input)); + } + +} diff --git a/src/test/java/athleticli/ui/NutrientVerifierTest.java b/src/test/java/athleticli/ui/NutrientVerifierTest.java new file mode 100644 index 0000000000..294f7733d5 --- /dev/null +++ b/src/test/java/athleticli/ui/NutrientVerifierTest.java @@ -0,0 +1,20 @@ +package athleticli.ui; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import athleticli.parser.NutrientVerifier; + +class NutrientVerifierTest { + + @Test + void verify_inputApprovedNutrients_expectTrue() { + assertTrue(NutrientVerifier.verify("fat")); + } + @Test + void verify_inputUnapprovedNutrients_expectFalse() { + assertFalse(NutrientVerifier.verify("Vitamin A")); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..c554f41e88 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,622 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +____________________________________________________________ + Hello! I'm AthletiCLI! + What can I do for you? +____________________________________________________________ + +> ____________________________________________________________ + +Activity Management: + add-activity CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME + add-run CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION + add-swim CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME style/STYLE + add-cycle CAPTION duration/DURATION distance/DISTANCE datetime/DATETIME elevation/ELEVATION + delete-activity INDEX + list-activity [-d] + edit-activity INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] + edit-run INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [elevation/ELEVATION] + edit-swim INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [style/STYLE] + edit-cycle INDEX [caption/CAPTION] [duration/DURATION] [distance/DISTANCE] [datetime/DATETIME] [elevation/ELEVATION] + find-activity DATE + set-activity-goal sport/SPORT type/TYPE period/PERIOD target/TARGET + edit-activity-goal sport/SPORT type/TYPE period/PERIOD target/TARGET + delete-activity-goal sport/SPORT type/TYPE period/PERIOD + list-activity-goal + +Diet Management: + add-diet calories/CALORIES protein/PROTEIN carb/CARB fat/FAT datetime/DATETIME + edit-diet INDEX [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT] [datetime/DATETIME] + delete-diet INDEX + list-diet + find-diet DATE + set-diet-goal [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT] + edit-diet-goal [unhealthy] [calories/CALORIES] [protein/PROTEIN] [carb/CARB] [fat/FAT] + delete-diet-goal INDEX + list-diet-goal + +Sleep Management: + add-sleep start/START end/END + list-sleep + delete-sleep INDEX + edit-sleep INDEX start/START end/END + find-sleep DATE + set-sleep-goal type/TYPE period/PERIOD target/TARGET + edit-sleep-goal type/TYPE period/PERIOD target/TARGET + list-sleep-goal + +Misc: + find DATE + save + bye + help [COMMAND] + +Please check our user guide (https://ay2324s1-cs2113-t17-1.github.io/tp/) for details. +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this activity: + [Activity] Morning Run | Distance: 10.00 km | Time: 1h 0m | October 26, 2023 at 6:00 AM + Now you have tracked your first activity. This is just the beginning! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The duration of an activity must be in the format "HH:mm:ss"! +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this activity goal: + monthly general distance: 10000 / 10000 +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this activity: + [Cycle] Evening Ride | Distance: 20.00 km | Speed: 10.00 km/h | Time: 2h 0m | October 26, 2023 at 6:00 PM + You have tracked a total of 2 activities. Keep pushing! +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this activity: + [Cycle] Evening Ride | Distance: 20.00 km | Speed: 10.00 km/h | Time: 2h 0m | October 26, 2023 at 6:00 PM + You have tracked a total of 3 activities. Keep pushing! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify the swimming style using "style/"! +____________________________________________________________ + +> ____________________________________________________________ + Gotcha, I've deleted this activity: + [Cycle] Evening Ride | Distance: 20.00 km | Speed: 10.00 km/h | Time: 2h 0m | October 26, 2023 at 6:00 PM + You have tracked a total of 2 activities. Keep pushing! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The activity index does not exist, check your list for the correct index! +____________________________________________________________ + +> ____________________________________________________________ + These are the activities you have tracked so far: + 1.[Cycle] Evening Ride | Distance: 20.00 km | Speed: 10.00 km/h | Time: 2h 0m | October 26, 2023 at 6:00 PM + 2.[Activity] Morning Run | Distance: 10.00 km | Time: 1h 0m | October 26, 2023 at 6:00 AM + +To see more performance details about an activity, use the -d flag +____________________________________________________________ + +> ____________________________________________________________ + These are the activities you have tracked so far: + [Cycle - Evening Ride - October 26, 2023 at 6:00 PM] + Distance: 20.00 km Elevation Gain: 1000 m + Time: 02:00:00 Avg. Speed: 10.00 km/h + [Activity - Morning Run - October 26, 2023 at 6:00 AM] + Distance: 10.00 km Time: 01:00:00 +____________________________________________________________ + +> ____________________________________________________________ + Ok, I've updated this activity: + [Cycle] Evening Ride | Distance: 22.00 km | Speed: 11.00 km/h | Time: 2h 0m | October 26, 2023 at 6:00 PM + You have tracked a total of 2 activities. Keep pushing! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The caption of an activity cannot be empty! +____________________________________________________________ + +> ____________________________________________________________ + These are the activities you have tracked so far: + [Cycle - Evening Ride - October 26, 2023 at 6:00 PM] + Distance: 22.00 km Elevation Gain: 1000 m + Time: 02:00:00 Avg. Speed: 11.00 km/h + [Activity - Morning Run - October 26, 2023 at 6:00 AM] + Distance: 10.00 km Time: 01:00:00 +____________________________________________________________ + +> ____________________________________________________________ + Ok, I've updated this activity: + [Cycle] Evening Ride | Distance: 12.00 km | Speed: 12.00 km/h | Time: 1h 0m | September 1, 2021 at 6:00 AM + You have tracked a total of 2 activities. Keep pushing! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this activity goal: + weekly running distance: 0 / 10000 +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this activity goal: + monthly swimming duration: 0 / 120 +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've edited this activity goal: + weekly running distance: 0 / 20000 +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've edited this activity goal: + monthly swimming duration: 0 / 60 +____________________________________________________________ + +> ____________________________________________________________ + These are your activity goals: + 1. monthly general distance: 10000 / 10000 + 2. weekly running distance: 0 / 20000 + 3. monthly swimming duration: 0 / 60 +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've deleted this activity goal: + weekly running distance: 0 / 20000 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this sleep record: + [Sleep] | Date: 2021-09-01 | Start Time: September 1, 2021 at 10:00 PM | End Time: September 2, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + You have tracked your first sleep record. This is just the beginning! +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this sleep record: + [Sleep] | Date: 2021-09-02 | Start Time: September 2, 2021 at 10:00 PM | End Time: September 3, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + You have tracked a total of 2 sleep records. Keep it up! +____________________________________________________________ + +> ____________________________________________________________ + Here are the sleep records in your list: + + 1. [Sleep] | Date: 2021-09-02 | Start Time: September 2, 2021 at 10:00 PM | End Time: September 3, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + 2. [Sleep] | Date: 2021-09-01 | Start Time: September 1, 2021 at 10:00 PM | End Time: September 2, 2021 at 6:00 AM | Sleeping Duration: 8 Hours +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify the start time of your sleep chronologically before the end time. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify both the start and end time of your sleep. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify both the start and end time of your sleep. +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this sleep record: + [Sleep] | Date: 2021-09-05 | Start Time: September 5, 2021 at 10:00 PM | End Time: September 6, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + You have tracked a total of 3 sleep records. Keep it up! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The datetime must be valid and in the format "yyyy-MM-dd HH:mm"! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The datetime must be valid and in the format "yyyy-MM-dd HH:mm"! +____________________________________________________________ + +> ____________________________________________________________ + Here are the sleep records in your list: + + 1. [Sleep] | Date: 2021-09-05 | Start Time: September 5, 2021 at 10:00 PM | End Time: September 6, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + 2. [Sleep] | Date: 2021-09-02 | Start Time: September 2, 2021 at 10:00 PM | End Time: September 3, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + 3. [Sleep] | Date: 2021-09-01 | Start Time: September 1, 2021 at 10:00 PM | End Time: September 2, 2021 at 6:00 AM | Sleeping Duration: 8 Hours +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The sleep record you are trying to input overlaps with an existing sleep record. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify the start time of your sleep chronologically before the end time. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify both the start and end time of your sleep. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify both the start and end time of your sleep. +____________________________________________________________ + +> ____________________________________________________________ + Here are the sleep records in your list: + + 1. [Sleep] | Date: 2021-09-05 | Start Time: September 5, 2021 at 10:00 PM | End Time: September 6, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + 2. [Sleep] | Date: 2021-09-02 | Start Time: September 2, 2021 at 10:00 PM | End Time: September 3, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + 3. [Sleep] | Date: 2021-09-01 | Start Time: September 1, 2021 at 10:00 PM | End Time: September 2, 2021 at 6:00 AM | Sleeping Duration: 8 Hours +____________________________________________________________ + +> ____________________________________________________________ + Gotcha, I've deleted this sleep record: + [Sleep] | Date: 2021-09-05 | Start Time: September 5, 2021 at 10:00 PM | End Time: September 6, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + You have tracked a total of 2 sleep records. Keep it up! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify the index of the sleep record as a positive integer not more than 999999. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify the index of the sleep record as a positive integer not more than 999999. +____________________________________________________________ + +> ____________________________________________________________ + Here are the sleep records in your list: + + 1. [Sleep] | Date: 2021-09-02 | Start Time: September 2, 2021 at 10:00 PM | End Time: September 3, 2021 at 6:00 AM | Sleeping Duration: 8 Hours + 2. [Sleep] | Date: 2021-09-01 | Start Time: September 1, 2021 at 10:00 PM | End Time: September 2, 2021 at 6:00 AM | Sleeping Duration: 8 Hours +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please specify both the start and end time of your sleep. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this sleep goal: + yearly duration : 0/8 minutes +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this sleep goal: + monthly duration : 0/800 minutes +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this sleep goal: + daily duration : 0/800 minutes +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've added this sleep goal: + weekly duration : 0/8 minutes +____________________________________________________________ + +> ____________________________________________________________ + These are your sleep goals: + 1. yearly duration : 0/8 minutes + 2. monthly duration : 0/800 minutes + 3. daily duration : 0/800 minutes + 4. weekly duration : 0/8 minutes +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this sleep record: + [Sleep] | Date: 2023-11-04 | Start Time: November 4, 2023 at 10:00 AM | End Time: November 4, 2023 at 6:00 PM | Sleeping Duration: 8 Hours + You have tracked a total of 3 sleep records. Keep it up! +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this sleep record: + [Sleep] | Date: 2023-11-07 | Start Time: November 7, 2023 at 10:00 AM | End Time: November 7, 2023 at 6:00 PM | Sleeping Duration: 8 Hours + You have tracked a total of 4 sleep records. Keep it up! +____________________________________________________________ + +> ____________________________________________________________ + These are your sleep goals: + 1. yearly duration : 57600/8 minutes + 2. monthly duration : 57600/800 minutes + 3. daily duration : 0/800 minutes + 4. weekly duration : 0/8 minutes +____________________________________________________________ + +> ____________________________________________________________ + Alright, I've edited this sleep goal: + monthly duration : 57600/8 minutes +____________________________________________________________ + +> ____________________________________________________________ + These are your sleep goals: + 1. yearly duration : 57600/8 minutes + 2. monthly duration : 57600/8 minutes + 3. daily duration : 0/800 minutes + 4. weekly duration : 0/8 minutes +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + There are no goals at the moment. Add a diet goal to start. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The period of an activity must be one of the following: "daily", "weekly"! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The target value for nutrients must be a positive integer! +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The period of an activity must be one of the following: "daily", "weekly"! +____________________________________________________________ + +> ____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] DAILY fat intake progress: (0/1) + + 2. [HEALTHY] DAILY calories intake progress: (0/1) + + 3. [HEALTHY] DAILY protein intake progress: (0/1) + + Now you have 3 diet goal(s). +____________________________________________________________ + +> ____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] DAILY fat intake progress: (0/1) + + 2. [HEALTHY] DAILY calories intake progress: (0/1) + + 3. [HEALTHY] DAILY protein intake progress: (0/1) + + 4. [UNHEALTHY] WEEKLY carb intake progress: (0/1) + + Now you have 4 diet goal(s). +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this diet: + Calories: 150000 cal | Protein: 500000 mg | Carb: 5000 mg | Fat: 2000 mg | November 4, 2023 at 10:00 AM + Now you have tracked your first diet. This is just the beginning! +____________________________________________________________ + +> ____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] DAILY fat intake progress: (0/1) + + 2. [HEALTHY] DAILY calories intake progress: (0/1) + + 3. [HEALTHY] DAILY protein intake progress: (0/1) + + 4. [UNHEALTHY] WEEKLY carb intake progress: (0/1) + + Now you have 4 diet goal(s). +____________________________________________________________ + +> ____________________________________________________________ + The following goal has been deleted: + + [HEALTHY] DAILY protein intake progress: (0/1) + +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please provide a positive integer not more than 999999. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please provide a positive integer not more than 999999. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please provide a positive integer not more than 999999. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please provide a positive integer not more than 999999. +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] DAILY fat intake progress: (0/100) + + 2. [HEALTHY] DAILY calories intake progress: (0/1) + + 3. [UNHEALTHY] WEEKLY carb intake progress: (0/1) + + Now you have 3 diet goal(s). +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Please input the following keywords to create or edit your diet goals: + [unhealthy] followed by "calories", "protein", "carb", "fat" and then followed by the target value. + e.g. WEEKLY calories/100 + e.g. WEEKLY unhealthy fat/100 +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! Diet goal for carb has already existed. Please edit the goal instead! +____________________________________________________________ + +> ____________________________________________________________ + The following goal has been deleted: + + [HEALTHY] DAILY fat intake progress: (0/100) + +____________________________________________________________ + +> ____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] DAILY calories intake progress: (0/1) + + 2. [UNHEALTHY] WEEKLY carb intake progress: (0/1000) + + Now you have 2 diet goal(s). +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! You cannot have healthy goals and unhealthy goals for the same nutrient. +____________________________________________________________ + +> ____________________________________________________________ + Noted. I've removed this diet: + Calories: 150000 cal | Protein: 500000 mg | Carb: 5000 mg | Fat: 2000 mg | November 4, 2023 at 10:00 AM + Now you have tracked a total of 0 diets. Keep grinding! +____________________________________________________________ + +> ____________________________________________________________ + These are your goal(s): + + 1. [HEALTHY] DAILY calories intake progress: (0/1) + + 2. [UNHEALTHY] WEEKLY carb intake progress: (0/1000) + + Now you have 2 diet goal(s). +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + Well done! I've added this diet: + Calories: 500 cal | Protein: 20 mg | Carb: 50 mg | Fat: 10 mg | September 1, 2021 at 6:00 AM + Now you have tracked your first diet. This is just the beginning! +____________________________________________________________ + +> ____________________________________________________________ + Ok, I've updated this diet: + Calories: 1000 cal | Protein: 100 mg | Carb: 200 mg | Fat: 500 mg | November 4, 2020 at 10:00 PM +____________________________________________________________ + +> ____________________________________________________________ + Here are the diets in your list: + 1. Calories: 1000 cal | Protein: 100 mg | Carb: 200 mg | Fat: 500 mg | November 4, 2020 at 10:00 PM + Now you have tracked a total of 1 diets. Keep grinding! +____________________________________________________________ + +> ____________________________________________________________ + Ok, I've updated this diet: + Calories: 5 cal | Protein: 100 mg | Carb: 200 mg | Fat: 500 mg | November 4, 2020 at 10:00 PM +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! The diet index is invalid! Please enter a valid diet index! +____________________________________________________________ + +> ____________________________________________________________ + I've found these diets: +____________________________________________________________ + +> ____________________________________________________________ + OOPS!!! I'm sorry, but I don't know what that means :-( +____________________________________________________________ + +> ____________________________________________________________ + Bye. Hope to see you again soon! +____________________________________________________________ + +____________________________________________________________ + File saved successfully! +____________________________________________________________ + diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..5981e62400 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,100 @@ -James Gosling \ No newline at end of file +help +add-activity Morning Run duration/01:00:00 distance/10000 datetime/2023-10-26 06:00 +add-activity Morning Run duration/00:60:00 distance/10000 datetime/2023-10-26 06:00 +set-activity-goal sport/general type/distance period/monthly target/10000 +add-cycle Evening Ride duration/02:00:00 distance/20000 datetime/2023-10-26 18:00 elevation/1000 +add-cycle Evening Ride duration/02:00:00 distance/20000 datetime/2023-10-26 18:00 elevation/1000 +add-swim Evening Swim duration/01:00:00 distance/1000 datetime/2023-10-16 20:00 +delete-activity 2 +delete-activity 0 +list-activity +list-activity -d +edit-cycle 1 distance/22000 +edit-cycle 1 caption/ elevation/20 +list-activity -d +edit-activity 1 Morning Run duration/01:00:00 distance/12000 datetime/2021-09-01 06:00 + +set-activity-goal sport/running type/distance period/weekly target/10000 +set-activity-goal sport/swimming type/duration period/monthly target/120 +edit-activity-goal sport/running type/distance period/weekly target/20000 +edit-activity-goal sport/swimming type/duration period/monthly target/60 +list-activity-goal +delete-activity-goal sport/running type/distance period/weekly + +add-sleep start/2021-09-01 22:00 end/2021-09-02 06:00 +add-sleep start/2021-09-02 22:00 end/2021-09-03 06:00 +list-sleep +add-sleep start/2021-09-03 22:00 end/2021-09-03 21:00 +add-sleep start/2021-09-04 22:00 +add-sleep end/2021-09-05 06:00 +add-sleep start/2021-09-05 22:00 end/2021-09-06 06:00 +add-sleep start/2021-09-32 22:00 end/2021-09-07 06:00 +add-sleep start/2021-13-01 22:00 end/2021-09-08 06:00 +list-sleep +edit-sleep 1 start/2021-09-01 23:00 end/2021-09-02 07:00 +edit-sleep 2 start/2021-09-02 23:00 end/2021-09-02 07:00 +edit-sleep 3 start/2021-09-03 23:00 +edit-sleep 4 end/2021-09-04 07:00 +list-sleep +delete-sleep 1 +delete-sleep -1 +delete-sleep a +list-sleep +sleep-add start/2021-09-05 22:00 end/2021-09-06 06:00 +delete-sleeps 5 +edits-sleep 5 start/2021-09-06 23:00 end/2021-09-07 07:00 +add-sleep start/2021-09-07 22:00 ends/2021-09-08 06:00 +edit-sleeps 6 starts/2021-09-08 23:00 end/2021-09-09 07:00 + +set-sleep-goal type/duration period/yearly target/8 +set-sleep-goal type/duration period/monthly target/800 +set-sleep-goal type/duration period/daily target/800 +set-sleep-goal type/duration period/weekly target/8 +list-sleep-goal +add-sleep start/2023-11-04 10:00 end/2023-11-04 18:00 +add-sleep start/2023-11-07 10:00 end/2023-11-07 18:00 +list-sleep-goal +edit-sleep-goal 1 type/duration period/monthly target/8 +list-sleep-goal + +add-diet-goal +list-diet-goal +set-diet-goal +set-diet-goal fat +set-diet-goal fat +set-diet-goal fat/fat +set-diet-goal fat/1 +set-diet-goal fat/1 calories/1 protein/1 +set-diet-goal weekly fat/-1 calories/-1 protein/-1 +set-diet-goal fat/1 calories/1 protein/1 +set-diet-goal daily fat/1 calories/1 protein/1 +set-diet-goal weekly unhealthy carb/1 +add-diet calories/150000 protein/500000 carb/05000 fat/2000 datetime/2023-11-04 10:00 +list-diet-goal +delete-diet-goal 3 +delete-diet-goal 1 2 +delete-diet-goal -1 +delete-diet-goal +delete-diet-goal never gonna let you down +edit-diet-goal carb +edit-diet-goal fat +edit-diet-goal fat/fat +edit-diet-goal daily fat/100 +edit-diet-goal fat/100 +edit-diet-goal carb/100 +set-diet-goal weekly carb/1 +delete-diet-goal 1 +edit-diet-goal weekly unhealthy carb/1000 +edit-diet-goal weekly carb/1 +delete-diet 1 +list-diet-goal + +add-diet calories/500 protein/20 carb/50 fat/10 datetime/2021-09-01 06:00 +edit-diet 1 calories/1000 protein/100 carb/200 fat/500 datetime/2020-11-04 22:00 +list-diet +edit-diet 1 calories/5 +delete-diet 2 +find-diet 2021-09-01 + +bye + diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat index 25ac7a2989..aa6d942fa2 100644 --- a/text-ui-test/runtest.bat +++ b/text-ui-test/runtest.bat @@ -16,4 +16,4 @@ java -jar %jarloc% < ..\..\text-ui-test\input.txt > ..\..\text-ui-test\ACTUAL.TX cd ..\..\text-ui-test -FC ACTUAL.TXT EXPECTED.TXT >NUL && ECHO Test passed! || Echo Test failed! +FC /W ACTUAL.TXT EXPECTED.TXT >NUL && ECHO Test passed! || Echo Test failed! diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh index 1dcbd12021..9431d9744f 100755 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -8,11 +8,13 @@ cd .. cd text-ui-test +rm -f data/* + java -jar $(find ../build/libs/ -mindepth 1 -print -quit) < input.txt > ACTUAL.TXT cp EXPECTED.TXT EXPECTED-UNIX.TXT dos2unix EXPECTED-UNIX.TXT ACTUAL.TXT -diff EXPECTED-UNIX.TXT ACTUAL.TXT +diff -w EXPECTED-UNIX.TXT ACTUAL.TXT if [ $? -eq 0 ] then echo "Test passed!"