diff --git a/.gitignore b/.gitignore index 2873e189e1..76ebd768e8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +META-INF/MANIFEST.MF +data.json diff --git a/README.md b/README.md index f82e2494b7..197cadb45a 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,39 @@ -# 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. +
+

ByteCeps

+

+ BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. +

+ gym fella +

+ 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 +

+
+ +## ByteCeps + +BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. With BYTE-CEPS, compile a list of exercises, build custom workouts, assign workouts to a weekly schedule and log details of each exercise completed in each performed workout. + + +## Features +BYTE-CEPS can track & manage several types of tasks, such as: +1. Exercise +2. Workout +3. Program + +## Usage +### Running ByteCeps +- You are required to install Java 11 onto your computer. +- Download the [latest release](https://github.com/AY2324S2-CS2113-F14-3/tp/releases) from the releases page. +- Run the program in your preferred terminal using the command: java -jar byteceps.jar. diff --git a/build.gradle b/build.gradle index ea82051fab..49cae5507d 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation group: 'org.json', name: 'json', version: '20240303' } test { @@ -29,11 +30,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("byteceps.ByteCeps") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("byteceps") archiveClassifier.set("") } @@ -43,4 +44,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..9e58788e4f 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Display | Name | Github Profile | Portfolio| +|-----------------------------------------------------|:----------:|:----------------------------------------------------:|:---------:| + | ![](https://via.placeholder.com/100.png?text=Photo) | Joshua Lee | [Github](https://github.com/joshualeejunyi) | [Portfolio](./team/joshualeejunyi.md)| +| ![](https://via.placeholder.com/100.png?text=Photo) | Vernon | [Github](https://github.com/V4vern) | [Portfolio](./team/v4vern.md)| + | ![](https://via.placeholder.com/100.png?text=Photo) | Lim Yu An | [Github](https://github.com/pqienso) | [Portfolio](./team/pqienso.md)| + | ![](https://via.placeholder.com/100.png?text=Photo) | Lukas W | [Github](https://github.com/LWachtel1) | [Portfolio](./team/lwachtel1.md)| + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..be4e6e50de 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,37 +2,902 @@ ## 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}) -## Design & implementation +1. [AB-3 Developer Guide](https://se-education.org/addressbook-level3/DeveloperGuide.html) +2. [PlantUML for sequence diagrams](https://plantuml.com/) +3. [JSON-java](https://github.com/stleary/JSON-java) -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +## Setting Up and Getting Started +First, fork [this repo](https://github.com/AY2324S2-CS2113-F14-3/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. + + **Note:** Importing a Gradle project is slightly different from importing a normal Java project. +3. **Verify the setup**: + * Run `ByteCeps.java` and try a few commands. + * Run the tests using `./gradlew check` and ensure they all pass. + +## Table of Contents +- [Developer Guide](#developer-guide) + - [Acknowledgements](#acknowledgements) + - [Setting Up and Getting Started](#setting-up-and-getting-started) + - [Table of Contents](#table-of-contents) + - [Design](#design) + - [Architecture](#architecture) + - [Classes: overview](#classes-overview) + - [`Activity` and child classes](#activity-and-child-classes) + - [`ActivityManager` and child classes](#activitymanager-and-child-classes) + - [The `ExerciseManager` class](#the-exercisemanager-class) + - [The `WorkoutManager` class](#the-workoutmanager-class) + - [The `WeeklyProgramManager` class](#the-weeklyprogrammanager-class) + - [Implementation](#implementation) + - [Exercise Management](#exercise-management) + - [\[Implemented\] Add, Edit, Delete, List, and Search Exercises](#implemented-add-edit-delete-list-and-search-exercises) + - [Workout Management](#workout-management) + - [\[Implemented\] Add, Edit, Delete, List, and Search Workout plan.](#implemented-add-edit-delete-list-and-search-workout-plan) + - [\[Implemented\] Assign and Unassign Workout plan.](#implemented-assign-and-unassign-workout-plan) + - [\[Implemented\] List all exercises in a workout plan.](#implemented-list-all-exercises-in-a-workout-plan) + - [Program management](#program-management) + - [\[Implemented\] Assign, List, Log workouts in Weekly program, and view today's workout plan.](#implemented-assign-list-log-workouts-in-weekly-program-and-view-todays-workout-plan) + - [Logging an exercise](#logging-an-exercise) + - [Assigning a workout to a program](#assigning-a-workout-to-a-program) + - [Viewing today's workout program](#viewing-todays-workout-program) + - [Clearing a day in the program](#clearing-a-day-in-the-program) + - [Help Menu](#help-menu) + - [Viewing a flag's help menu](#viewing-a-flags-help-menu) + - [Viewing a specific command format](#viewing-a-specific-command-format) + - [The `Storage` class](#the-storage-class) + - [Overview: Saving data to `data.json`](#overview-saving-data-to-datajson) + - [Overview: Loading data from `data.json`](#overview-loading-data-from-datajson) + - [Example: Loading data for a specific `ActivityManager` class](#example-loading-data-for-a-specific-activitymanager-class) + - [The `CascadingDeletionProcessor` class](#the-cascadingdeletionprocessor-class) + - [Removing a deleted exercise from a workout](#removing-a-deleted-exercise-from-a-workout) + - [Removing a deleted workout from the weekly program](#removing-a-deleted-workout-from-the-weekly-program) + - [Product scope](#product-scope) + - [Target user profile](#target-user-profile) + - [Value proposition](#value-proposition) + - [User Stories](#user-stories) + - [Non-Functional Requirements](#non-functional-requirements) + - [Glossary](#glossary) + - [Instructions for manual testing](#instructions-for-manual-testing) + - [Initial Launch](#initial-launch) + - [Exercise Management](#exercise-management-1) + - [Workout Management](#workout-management-1) + - [Program Management](#program-management-1) + - [Program Logging Management](#program-logging-management) + - [Miscellaneous](#miscellaneous) + + +## Design + +This section provides a high-level explanation of the design and implementation of ByteCeps, +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 the main components of ByteCeps and how they interact with each other. + +![architectureDiagram.png](diagrams/architectureDiagram.svg) + +**Main components of the architecture** + +[ByteCeps](../src/main/java/byteceps/ByteCeps.java) is the entrypoint for the application to launch and shut down. + +The bulk of ByteCep's work is done by the following components: +- [UserInterface](../src/main/java/byteceps/ui/UserInterface.java): Interacts with the user via the command line. +- [Parser](../src/main/java/byteceps/commands/Parser.java): Parses the user's input and saves their inputs in the class. +- [ExerciseManager](../src/main/java/byteceps/processing/ExerciseManager.java): Manages all the exercises stored in memory. +- [WorkoutManager](../src/main/java/byteceps/processing/WorkoutManager.java): Manages all the workouts in memory. +- [HelpMenuManager](../src/main/java/byteceps/processing/HelpMenuManager.java): Displays the help messages to the user. +- [WeeklyProgramManager](../src/main/java/byteceps/processing/WeeklyProgramManager.java): Manages the weekly program for the user in memory. +- [WorkoutLogsManager](../src/main/java/byteceps/processing/WorkoutLogsManager.java): Managing the logging of workouts. +- [Storage](../src/main/java/byteceps/storage/Storage.java): Reads data from, and writes data to, the hard disk. + +**Other notable components**: +- [Exercise](../src/main/java/byteceps/activities/Exercise.java): Stores an exercise entry by the user in memory. +- [Workout](../src/main/java/byteceps/activities/Workout.java): Stores a workout (collection of exercises) entry by the user in memory. +- [ExerciseLog](../src/main/java/byteceps/activities/ExerciseLog.java): Stores an exercise log entry by the user in memory. +- [Exceptions](../src/main/java/byteceps/errors/Exceptions.java): Represents exceptions used by the components in the application. + + +## Classes: overview +### `Activity` and child classes +The `Activity` class serves as a parent class to `Exercise`, `ExerciseLog`, `Workout`, `WorkoutLog` and `Day` classes for the ease of usage of `ActivityManager` classes (see below). + +**Note:** The `Day` class acts as a container class for `Workout`, for use in `WeeklyProgramManager` +![ActivityClassDiagram](diagrams/ActivityClassDiagram.svg) + +### `ActivityManager` and child classes +The `ActivityManager` and inheritors are responsible for managing an `ArrayList` of activities. The basic functions of an `ActivityManager` include: +1. `add()`: Adding an activity to the `ArrayList` +2. `delete()`: Deleting an activity from the `ArrayList` +3. `retrieve()`: Retrieving an activity from the `ArrayList` by name +4. `getListString()`: Getting the string containing all the activities contained in the `ActivityManager`. +5. `execute()`: Executing all commands related to the `ActivityManager` and returning the required user input. + +![ActivityManagerClassDiagram](diagrams/ActivityManagerClassDiagram.svg) + +#### The `ExerciseManager` class +`ExerciseManager` is responsible for tracking and manipulating all exercises added to `ByteCeps` by the user. + +#### The `WorkoutManager` class +`WorkoutManager` is responsible for tracking and manipulating all workouts created by the user. + +#### The `WeeklyProgramManager` class +`WeeklyProgram` is responsible for tracking and manipulating the weekly training program set by the user. + +## Implementation +### Exercise Management +#### [Implemented] Add, Edit, Delete, List, and Search Exercises +ByteCeps streamlines the management of exercise-related tasks by following a general multi-step pattern. Here’s how these operations are carried out: + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. User input examples include: +- `exercise /add Pushups` for adding an exercise. +- `exercise /edit Pushups /to Pullups` for editing an exercise name from Pushups to Pullups. +- `exercise /delete Pushups` for deleting the Pushups exercise. +- `exercise /list` for listing all exercises. +- `exercise /search Pushups` for finding all instances of the Pushups exercise. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of exercise operation and extracts any necessary parameters. For instance, the `exercise /add` command will be recognized, and the exercise name `Pushups` will be parsed as the parameter. + +**Step 3 - Command Validation**: The input is then validated using `ExerciseValidator` class to ensure that the command and parameters provided meet the expected format and criteria for processing. + +**Step 4 - Command Execution**: The appropriate action is taken by the `ExerciseManager` class. +- Adding: If the user wants to add a new exercise, `ExerciseManager` creates a new `Exercise` instance and adds it to the `ExerciseManager's activitySet`. +- Editing: When editing an exercise, `ExerciseManager` locates the existing `Exercise`, updates its details, and then updates the `activitySet` accordingly. +- Deleting: To delete an exercise, `ExerciseManager` finds the targeted `Exercise` in the `activitySet` and removes it. +- Listing: For listing exercises, `ExerciseManager` retrieves all the exercises from the `activitySet` and formats them into a list for display. +- Searching: Searching is handled by querying the `activitySet` for exercises that match the search criteria provided by the user, and presenting the results. + +**Step 5 - Result Display**: After the command is executed, a message indicating the success or failure of the operation is generated and displayed to the user. This feedback is crucial for confirming the effect of the user's command on the system. + +Here is the sequence diagram for the `exercise /add pushups` command to illustrate the five-step process: + +![AddExercise](diagrams/addExercise.svg) + +### Workout Management +#### [Implemented] Add, Edit, Delete, List, and Search Workout plan. +ByteCeps streamlines the management of exercise-related tasks by following a general multi-step pattern. Here’s how these operations are carried out: + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. User input examples include: +- `workout /create LegDay` for creating a workout plan. +- `workout /edit LegDay /to CardioBlast` for editing a workout plan name from LegDay to CardioBlast. +- `workout /delete LegDay` for deleting the LegDay workout plan. +- `workout /list` for listing all workout plans. +- `workout /search HighIntensity` for finding all workout plans containing HighIntensity. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of workout operation and extracts any necessary parameters. For instance, the `workout /create` command will be recognized, and the workout plan name `LegDay` will be parsed as the parameter. + +**Step 3 - Command Validation**: The input is then validated using `WorkoutValidator` class to ensure that the command and parameters provided meet the expected format and criteria for processing. + +**Step 4 - Command Execution**: The appropriate action is taken by the `WorkoutManager` class. +- Creating: If the user wants to create a new workout plan, `WorkoutManager` creates a new `Workout` instance and adds it to the `WorkoutManager's activitySet`. +- Editing: When editing a workout plan, `WorkoutManager` locates the existing `Workout`, updates its details, and then updates the `activitySet` accordingly. +- Deleting: To delete a workout plan, `WorkoutManager` finds the targeted `Workout` in the `activitySet` and removes it. +- Listing: For listing workout plans, `WorkoutManager` retrieves all the workouts from the `activitySet` and formats them into a list for display. +- Searching: Searching is handled by querying the `activitySet` for workouts that match the search criteria provided by the user, and presenting the results. + +**Step 5 - Result Display**: After the command is executed, a message indicating the success or failure of the operation is generated and displayed to the user. This feedback is crucial for confirming the effect of the user's command on the system. + +Here is the sequence diagram for the `workout /delete LegDay` command to illustrate the five-step process: + +![deleteWorkout](diagrams/deleteWorkout.svg) + +#### [Implemented] Assign and Unassign Workout plan. +The ByteCeps application facilitates workout management, including the assignment and unassignment of exercises to workout plans. The process is outlined in the sequence diagram provided and follows a standard operational pattern as described below: + +ByteCeps streamlines the management of exercise-related tasks by following a general multi-step pattern. Here’s how these operations are carried out: + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. User input examples include: +- `workout /assign Pushups /to LegDay` to assign the exercise `Pushups` to the workout plan `LegDay`. +- `workout /unassign Pushups /from LegDay` to unassign the exercise `Pushups` to the workout plan `LegDay`. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of workout operation and extracts any necessary parameters. For instance, the `workout /assign` command will be recognized, workout plan name `LegDay` and exercise name `Pushups` will be parsed as the parameter. + +**Step 3 - Command Validation**: The input is then validated using `WorkoutValidator` class to ensure that the command and parameters provided meet the expected format and criteria for processing. + +**Step 4 - Command Execution**: The appropriate action is taken by the `WorkoutManager` class. +- Assigning: The `WorkoutManager` calls `executeAssignAction` which initiates the process to assign an exercise to a workout plan. It communicates with the `ExerciseManager` to retrieve the specified `Exercise` object. Simultaneously, it retrieves the specified `Workout` object to which the exercise will be added. The `Workout` object’s `addExercise` method is called to include the exercise within the workout plan. +- Unassigning: The `WorkoutManager` calls `executeUnassignAction` which initiates the process to unassign an exercise to a workout plan. It first retrieves the `Workout` object corresponding to `LegDay` by calling the retrieve method on the `WorkoutManager`. With the `Workout` object obtained, it attempts to find and remove the `Exercise` object representing `Pushups`. If the `Exercise` is present in the `Workout`, it is removed from the workout's exercise list. + +**Step 5 - Result Display**: After the command is executed, a message indicating the success or failure of the operation is generated and displayed to the user. This feedback is crucial for confirming the effect of the user's command on the system. + +Here is the sequence diagram for the `workout /assign Pushups /to LegDay` command to illustrate the five-step process: + +![assignExercise](diagrams/assignExercise.svg) + +#### [Implemented] List all exercises in a workout plan. + +The feature to list all exercises within a specific workout plan is crucial for users to review their workout regimen. This section outlines the sequence of operations triggered by the `workout /info workoutplan` command, culminating in the display of all associated exercises to the user. + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. The user initiates the process by inputting the command `workout /info workoutplan`. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of workout operation and extracts any necessary parameters. For instance, the `workout /info` command will be recognized, workout plan name `workoutplan` will be parsed as the parameter. + +**Step 3 - Command Validation**: The input is then validated using `WorkoutValidator` class to ensure that the command and parameters provided meet the expected format and criteria for processing. + +**Step 4 - Command Execution**: The appropriate action is taken by the `WorkoutManager` class. +- Execute Info Action: The `WorkoutManager` proceeds to execute the `executeInfoAction`, specifically tailored for fetching details about the workout plan named `workoutplan`. +- Retrieve Workout Plan: The `WorkoutManager`retrieves the `Workout` object corresponding to `workoutplan`. The `WorkoutManager` then searches its records and returns the `Workout` object to the `WorkoutManager`. +- Fetch Exercise List: The `WorkoutManager` then invokes the `getExerciseList` method on the retrieved `Workout` object to obtain a list of all exercises included in the workout plan. +- Compile Exercise Information: For each `Exercise` in the list, the `WorkoutManager` calls the `getName` method to retrieve the name of the exercise. These names are compiled into a comprehensive message detailing all exercises within the workout plan. + +**Step 5 - Result Display**: After the command is executed, a message indicating the success or failure of the operation is generated and displayed to the user. This feedback is crucial for confirming the effect of the user's command on the system. +- Success Path: The compiled list of exercises is presented to the user, providing a clear overview of the workout plan's contents. +- Validation Failure: If the initial validation fails, the user is informed of the invalid command format without proceeding further into the sequence. + +Here is the sequence diagram for the `workout /info workoutplan` command to illustrate the five-step process: + +![listExerciseInWorkoutPlan](diagrams/listExerciseInWorkoutPlan.svg) + + +### Program management +#### [Implemented] Assign, List, Log workouts in Weekly program, and view today's workout plan. +ByteCeps streamlines the management of the user's weekly program by following the same general multi-step pattern as above for workout and exercise management. +Namely, the steps consist: +1. Input processing +2. Command identification +3. Command validation +4. Command execution +5. Result display + +The first 2 steps will be omitted in the sequence diagrams following this overview for brevity purposes, as they are similar to the explanations offered in +workout and exercise management. + +The following are the possible commands the `WeeklyProgramManager` object can run: +- `program /list` to list out the weekly program. +- `program /assign LegDay /to Monday` for assigning a workout to a specific day. +- `program /log Squats /weight 90 100 110 /reps 12 10 8 /sets 3` for logging a specific exercise done today. +- `program /clear` for clearing the entire weekly workout plan. +- `program /clear Monday` for clearing a specific day's workout plan. +- `program /today` for viewing today's workout. + +#### Logging an exercise +The sequence diagram below gives the high-level overview of the command `program /log /weight /sets /reps /date ` being run: + +![](./diagrams/addExerciseLog.svg) + +1. After input validation, the `execute()` method of `WeeklyProgramManager` calls the `executeLogAction()` method +2. This method then calls the `addWorkoutLog()` function of the `WorkoutLogManager`, of which is elaborated below. +3. Finally, the `messageToUser` is returned to the `UserInterface`. + +To dive deeper into how the `WorkoutLogsManager` works, we must first understand the several layers that are required to be implemented in order for this feature to work. +1. Exercises need to be logged, including the weight(s) that the user has completed, as well as the number of sets and repetitions completed for the exercises. +2. These exercises exist as a set (referred to as "exercise logs"), supposedly tied to a workout plan that the user has created in the application, and have a unique date that the user did their workout on. +3. These workouts exist again as a set (referred to as "workout logs"), with their unique identifiers being the date that the user completed the workout. + +The implementation thus is as follows: + +**Step 1 - Parsing & Validation** +- When the `executeLogAction()` method is called, it must first extract the various parameters of the command. +- The workoutDate, exercise name, the weights, as well as the number of sets and repetitions completed. +- The exercise is validated to check if it currently exists as a created exercise. + - One consideration noted is for the logging of an exercise under a workout in the past (i.e. a historical workout), however, the user has swapped the exercise and no longer has it in his exercise list. + - However, we believe that in this case, even though the user no longer needs the exercise in the database, the user should still add in the exercise to log it. +- If the user specifies a date (for the logging of historical workouts), then the date is parsed and the workout name is retrieved for that day. + - If no date is specified, it is assumed to be the current date. + +**Step 2 - Adding a Workout Log** +- The workout date and name is then passed to the `addWorkoutLog()` method of `WorkoutLogsManager` +- A new instance of a `WorkoutLog` is created and added into the `LinkedHashSet` of the `WorkoutLogsManager` + - As this method might be called multiple times, because a workout has multiple exercise logs, the adding of the `WorkoutLog` silently fails + - This `WorkoutLog` contains a `LinkedHashSet` of a variable number of `ExerciseLog`. + +**Step 3 - Adding an Exercise Log** +- The exercise information such as the workout date, exercise name, weight, sets and repetitions is then passed to the `addExerciseLog()` method of `WorkoutLogsManager` +- The method then instantiates a new `ExerciseLog` with the information, retrieves the `WorkoutLog` based on the given date, and finally calls the `addExerciseLog` method of the `WorkoutLog` instance with the new `ExerciseLog` instance. + +**Step 4 - Feedback to User** +- If no errors were encountered throughout the process, a success message is returned and printed through the `printMessage()` method + +Below shows the sequence diagram of the process, focusing on the flow after the `executeLogAction()` method is called. +![](./diagrams/workoutLog.svg) + +#### Assigning a workout to a program +Below is the sequence diagram of the command `program /assign /to ` being run: +![](./diagrams/assignWorkoutToProgram.svg) +1. After input validation, the `execute()` method of `WeeklyProgramManager` calls the `executeAssignAction()` method. +2. This method then retrieves the appropriate `Workout` object, and assigns it to be contained in the appropriate `Day` object. +3. Finally, the `messageToUser` is returned to the `UserInterface`. + +#### Viewing today's workout program +Below is the sequence diagram of the command `program /today` being run. +The validation of user input has been omitted for purposes of brevity. + +![](./diagrams/programToday.svg) + +1. Today's date is retrieved in the form of a `Date` object. +2. This is used to retrieve the appropriate `Day` object. +3. The `Workout` contained in the `Day` object is retrieved. +4. The `Workout` , along with `Date` and `Day` is then converted to the `messageToUser:String`, which is returned to `execute()` and `ByteCeps` for printing. + +#### Clearing a day in the program +This is the sequence diagram of the command `program /clear ` being run. +The validation of user input has been omitted for purposes of brevity. + +![](./diagrams/clearProgram.svg) + +1. If no day has been assigned to the user, the `executeClearAction()` method clears all workouts in the `WeeklyProgramManager` object. +2. Otherwise, the specified `Day` object is removed from `WeeklyProgramManager` object, and a new `Day` object with no workout assigned is constructed in its place. + + +### Help Menu +To implement a help menu for the user, where they can view the formatting of any command corresponding to any specific BYTE-CEPS functionality, 3 classes work together: +- [HelpMenuManager](../src/main/java/byteceps/processing/HelpMenuManager.java) : Returns a help menu access guidance message or help menus to be shown to the user or, if requested, a specific functionality's command format. +- [HelpStrings](../src/main/java/byteceps/ui/strings/HelpStrings.java): Stores all static Strings including the help menu guidance message, numbered help menu items, command formats and help menu error messages. +- [HelpValidator](../src/main/java/byteceps/validators/HelpValidator.java): Parses the input to HelpMenuManager's execute() method to ensure input validity before the rest of the method executes. + +#### Viewing help command guidance message +If the user enters the command `help` alone, they will be shown the following guidance message for accessing help menus: +``` +[BYTE-CEPS]> To access the help menu for command guidance, please type: +help /COMMAND_TYPE_FLAG +Available command types (type exactly as shown): +exercise +workout +program +To view this message again, enter 'help' alone +``` + +How the command `help` is processed and executed will be described below. This is to demonstrate how the 3 aforementioned classes interact to show a user the guidance message for accessing help menus: + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. The user initiates the process by inputting the command `help`. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of help operation and extracts any necessary parameters. In this case, the `help` is recognised as the command. + +**Step 3 - Command Validation**: The input is then validated using `HelpValidator` class to ensure that the parameters provided meet the expected format and criteria for processing. +Here, validation will fail as `help` is not accompanied by any parameters. An exception, with an error message specifying this, is thrown. + +**Step 4 - Command Execution**: The appropriate action is taken by the `HelpMenuManager` class. +- Catch Validation Exception: The `HelpMenuManager` proceeds to catch this exception, and check the error message is as expected. +- Expected Error Message Path: The `HelpMenuManager` calls getHelpGuidanceString(), which returns the desired guidance message String. +- Different Error Message Path: The `HelpMenuManager` rethrows the exception. + +**Step 5 - Result Display** +- Success Path: The guidance message String is presented to the user. +- Failure Path: If the exception's message did not match that caused by the user command `help`, the user is shown the received error message, informing them of the invalid command format without proceeding further into the sequence. + +This is a sequence diagram of the command `help` provided to visually illustrate the described example above. +![](./diagrams/helpGuidanceMessage.svg) + +#### Viewing a flag's help menu +If the user enters the command `help /COMMAND_TYPE` where `COMMAND_TYPE` is one of the 3 possible flags: +1. `exercise` +2. `workout` +3. `program` + +They will be shown a numbered list of functionalities associated with the specific flag. + +How the command `help /program` is processed and executed will be described below. This is to demonstrate how the 3 aforementioned classes interact to show a user a help menu which details the associated functionalities of a flag (for which they can see command formats). + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. The user initiates the process by inputting the command `help /program`. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of help operation and extracts any necessary parameters. In this case, the `help /program` is recognised as the command. + +**Step 3 - Command Validation**: The input is then validated using `HelpValidator` class to ensure that the parameters provided meet the expected format and criteria for processing. +If validation fails, an exception is thrown with an accompanying error message. If validation succeeds, command execution proceeds. + +**Step 4 - Command Execution**: The appropriate action is taken by the `HelpMenuManager` class. +- Execute generateAllActions: The `HelpMenuManager` proceeds to execute the `generateAllActions` method, which retrieves the array of `program` help menu items, `PROGRAM_FLAG_FUNCTIONS`, from the static `HelpStrings` class and appends each String into a single String that contains a numbered list. This is then returned. + +**Step 5 - Result Display** +- Success Path: The String containing the numbered `program` help menu is presented to the user. +- Validation Failure: If the initial validation fails, the user is shown the validation failure's error message, informing them of the invalid command format without proceeding further into the sequence. + + +This is a sequence diagram of the command `help /program` provided to visually illustrate the described example above. +![](./diagrams/helpMenuWholeMenu.svg) + +#### Viewing a specific command format +How the command `help /exercise 1` is processed and executed will be described below to demonstrate how the 3 aforementioned classes interact to show a user command formats. + +**Step 1 - Input Processing:** +The user’s input is received and processed by ByteCeps, which involves parsing the command through the `Parser` class. The user initiates the process by inputting the command `help /exercise 1`. + +**Step 2 - Command Identification:** +The `Parser` class determines the type of help operation and extracts any necessary arguments. In this case, the `help /exercise` is recognised as the command, and `1` is recognised as the parameter. + +**Step 3 - Command Validation**: The input is then validated using `HelpValidator` class to ensure that the parameters provided meet the expected format and criteria for processing. +If validation fails, an exception is thrown with an accompanying error message. If validation succeeds, command execution proceeds. + +**Step 4 - Command Execution**: The appropriate action is taken by the `HelpMenuManager` class. +- Execute getFlagFormat: The `HelpMenuManager` proceeds to execute the `getFlagFormat` method, which first converts the String parameter `1` to its corresponding Integer index `0` then calls the `getExerciseFlagFormats` method for retrieving a single String command format from the `exercise` command formats menu. +- Retrieve command format: The `HelpMenuManager`retrieves the specific String command format at the index `0` in the list of `exercise` command formats found in the static `HelpStrings` class. + +**Step 5 - Result Display** +- Success Path: The String of the desired command format (item at position `1`/index `0` in the `exercise` help menu) is presented to the user. +- Validation Failure: If the initial validation fails, the user is shown the validation failure's error message, informing them of the invalid command format without proceeding further into the sequence. + +This is a sequence diagram of the command `help /exercise 1` provided to visually illustrate the described example above. +![](./diagrams/helpMenuCommandFormat.svg) + +### The `Storage` class + A `Storage` object is responsible for reading from and writing to `.json` files, so that user data is saved in between sessions. + +#### Overview: Saving data to `data.json` +The `storage.save()` method is called with the `ExerciseManager`, `WorkoutManager`, `WeeklyProgramManager` and `WorkoutLogsManager` objects being passed in as input. +![](./diagrams/saveStorage.svg) +**NOTE**: plantUML does not allow for termination of lifelines after destroying an object (`:FileWriter`), but note that the lifeline should end after the red cross. +1. An empty `JSONObject`, `jsonArchive`, is created. +2. `ExerciseManager` and `WorkoutManager` objects have their list of multiple `Activity` classes converted into an `Array`, which is then `.put()` into `jsonArchive`. +3. `WeeklyProgramManager` and `WorkoutLogsManager` objects have their own `exportToJSON` method which is called. The results are again `.put()` into `jsonArchive`. +4. A `FileWriter` object is created, which writes `jsonArchive` converted to a `String` to the appropriate `filePath`. +5. The `Storage` object calls the `UserInterface` directly to print the success message. + +#### Overview: Loading data from `data.json` +The `storage.load()` method is called with the empty `ExerciseManager`, `WorkoutManager`, `WeeklyProgramManager` and `WorkoutLogsManager` objects being passed in as input. +These objects are to be updated in the method. +![](./diagrams/loadStorage.svg) +1. If there has been no `data.json` file detected, a new `File` is created and the empty `ExerciseManager`, `WorkoutManager`, `WeeklyProgramManager` and `WorkoutLogsManager` + is returned without modification. +2. Else, a new `JSONObject` called `jsonArchive`, loaded from `data.json` is created. +3. Each `ActivityManager` object is then loaded sequentially using `jsonArchive` as input. + +#### Example: Loading data for a specific `ActivityManager` class +From the last sequence diagram, we see that each `ActivityManager` class is loaded from `jsonArchive` via its own method. +For example, the `WorkoutManager` object is loaded from the `loadWorkouts()` method. The below sequence diagram shows how `loadWorkouts()` is run. +The loading of other `ActivityManager` objects is similar in nature. +![](./diagrams/loadWorkouts.svg) +1. The `jsonWorkoutArray` is first retrieved from `jsonArchive`. +2. Then, the workout name of each `jsonWorkout` in `jsonWorkoutArray` is retrieved. +3. These workout names are used to create new `Workout` objects contained in `WorkoutManager`. +4. The exercise list, `jsonExercisesInWorkout` inside each `jsonWorkout` is retrieved. +5. The exercise name of each `jsonExercise` in `jsonExercisesInWorkout` is used to assign the correct exercises in `ExerciseManager` to each `Workout` object. + + +### The `CascadingDeletionProcessor` class +This class is a utility class that is responsible for handling cascading deletions (e.g., when an exercise assigned to an existing workout is deleted from `ByteCeps` by the user). +It removes the required `Workout`/`Exercise` objects from the `Workout`/`WeeklyProgramManager` silently whenever a `delete` command is called. +Its only public method, `checkForCascadingDeletions()`, is run after executing a parsed command. + +#### Removing a deleted exercise from a workout +If the command entered by the user starts with `exercise /delete` and is executed successfully, the private method `removeDeletedExerciseFromWorkouts()` is run: +![](./diagrams/deleteExerciseFromWorkouts.svg) +1. `removeDeletedExerciseFromWorkouts()` iterates through every `Workout` in `WorkoutManager`. +2. If the deleted `exerciseName` matches that of an exercise in the `Workout`, the exercise is deleted from the workout too. + +#### Removing a deleted workout from the weekly program +If the command entered by the user starts with `workout /delete` and is executed successfully, the private method `removeDeletedWorkoutsFromProgram()` is run: +![](./diagrams/deleteWorkoutFromProgram.svg) +1. A copy of all 7 `Days` in `WeeklyProgramManager` is stored as `oldWorkoutsInProgram`. +2. `removeDeletedWorkoutsFromProgram()` iterates through every `Day` in `oldWorkoutsInProgram`. +3. If the name of the `Workout` assigned to a particular `Day` matches that of the deleted `Workout`, + that particular `Day` is deleted from `newWorkoutsInProgram` . +4. A new `Day` with no `Workout` assigned to it is added to `newWorkoutsInProgram` in replacement of the deleted `Day`. + + ## Product scope ### Target user profile -{Describe the target user profile} +BYTE-CEPS, a CLI-based all-in-one tool for setting and tracking fitness goals. Whether you're a tech-savvy fitness enthusiast or just starting your fitness journey, BYTE-CEPS offers the simplicity and efficiency of a CLI interface to help you maintain or improve your fitness through self-managed routines. ### Value proposition -{Describe the value proposition: what problem does it solve?} +ByteCeps offers a streamlined and comprehensive platform to manage exercise routines, track workout progress, and design personalized fitness programs with ease and efficiency for fitness enthusiasts and professionals. + +1. Streamlined Exercise Management: ByteCeps simplifies the organization of exercise routines by providing a user-friendly interface to add, edit, delete, list and search exercises effortlessly. +2. Effortless Workout Planning: Create personalized workout plans by assigning exercises to specific days with intuitive CLI commands, ensuring organized and effective training sessions tailored to your needs. +3. Flexible Program Adaptation: Seamlessly adjust workout plans and schedules as needed, with the ability to add, remove, or modify exercises on the fly, providing flexibility and adaptability to your evolving fitness journey. +4. Comprehensive Progress Tracking: Log and monitor workout performance, including weights, sets, and reps, with detailed exercise logs and historical data, enabling you to track progress, identify trends, and stay motivated. + +With ByteCeps, achieve your fitness objectives efficiently, effectively, and enjoyably, unlocking your full potential for a healthier, fitter lifestyle. + ## 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 | user | create an exercise entry | begin tracking my exercises | +| v1.0 | user | create edit an exercise entry | modify an exercise to suit my needs | +| v1.0 | user | delete an exercise entry | remove unwanted exercises that I will not do | +| v1.0 | user | add an exercise to a workout plan | customise my workout plan | +| v1.0 | user | edit an exercise in a workout plan | modify the workout plan to suit my needs | +| v1.0 | user | delete an exercise from workout plan | remove unwanted exercises from a workout plan | +| v1.0 | user | list all exercises in a workout plan | see the details of my planned exercises | +| v1.0 | user | choose the workout plan for a day | organise and structure my daily workout routine | +| v1.0 | user | display my workout for the day | know what exercises I should be doing today | +| v1.0 | user | display my workout for the week | have a weekly overview of what I should do | +| v2.0 | user | export my workout plan to Json | share with other fitness enthusiasts | +| v2.0 | user | import my workout plan to Json | bring my progress across devices | +| v2.0 | user | search for exercises | build my workout plan faster | +| v2.0 | user | search for workout plans | identify which is the suitable workout for me | +| v2.0 | fitness enthusiast | record the amount of weight lifted | track my progress over time | +| v2.0 | fitness enthusiast | track the number of sets performed for each exercise session | follow my workout plan effectively | +| v2.0 | fitness professional | monitor the repetitions completed for each exercise | evaluate my performance | +| v2.0 | fitness professional | log my exercise data for a specific date | accurately track my progress over time | +| v2.0 | fitness professional | view a list of dates on which I have logged exercise entries | track my consistency and adherence to my workout routine | +| v2.0 | fitness professional | review specific exercise logs for a particular date | analyze my workout details and progress on that specific day | +| v2.1 | fitness professional | log multiple sets of an exercise, including different weights and reps for each set | have a comprehensive log of my exercise sessions to monitor variations in my performance and strength training progress | +| v2.1 | fitness professional | access and review historical workout data with detailed breakdowns by exercise, set, weight, and repetition | analyze trends in my performance and identify areas for improvement or adjustment in my training regime | +| v2.1 | fitness enthusiast | be able to overwrite an incorrect log entry for a workout | ensure my workout history is accurate and reflects what I actually performed | ## Non-Functional Requirements -{Give non-functional requirements} +1. BYTE-CEPS should work on Windows, macOS and Linux where Java 11 is installed. +2. BYTE-CEPS should be able to store data locally. +3. BYTE-CEPS should be able to work offline. +4. BYTE-CEPS should be easy to use. -## Glossary -* *glossary item* - Definition +## Glossary +* **Reps (Repetitions)** - Refers to the number of times an exercise is performed in one set. For example, doing ten pushups in a row counts as ten reps. +* **Sets** - A group of consecutive repetitions. For example, if you do ten pushups and rest, then another ten pushups and rest again, you have completed two sets of ten reps each. +* **Weight** - The amount of resistance used during an exercise, typically measured in pounds (lbs) or kilograms (kg). It is used to quantify the load lifted or moved in strength training and bodybuilding exercises. + ## 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 BYTE-CEPS. This list is not exhaustive. +Developers are expected to conduct more extensive tests. + +### Initial Launch + +* ✅ Download the latest BYTE-CEPS from the official repository. +* ✅ Copy the downloaded file to a folder you want to designate as the home for BYTE-CEPS. +* ✅ Open a command terminal, cd into the folder where you copied the file, and run `java -jar byteceps.jar`. + +### Exercise Management + +1. Adding an Exercise: + - Test case 1: + * Add a new exercise. + * Command: `exercise /add pushups` + * Expected Outcome: The system should confirm that the exercise `pushups`has been added. + - Test case 2: + * Add an exercise with special characters. + * Command: `exercise /add push-ups!` + * Expected Outcome: The system should display an error message indicating that the exercise name cannot contain special characters. + - Test case 3: + * Add a duplicate exercise. + * Command: `exercise /add pushups` + * Expected Outcome: The system should display an error message indicating that the exercise already exists. + - Test case 4: + * Add an exercise with a case variation in name. + * Command: `exercise /add PUSHUPS` + * Expected Outcome: The system should display an error message indicating that the exercise already exists, as the exercise name is case insensitive. +2. Deleting an Exercise: + - Test case 1: + * Delete an existing exercise. + * Command: `exercise /delete pushups` + * Expected Outcome: The system should confirm that the exercise `pushup` has been deleted. + - Test case 2: + * Attempt to delete a non-existent exercise. + * Command: `exercise /delete situps` + * Expected Outcome: The system should display an error message indicating that the exercise does not exist. + - Test case 3: + * Delete an exercise with a case variation in name. + * Command: `exercise /delete PUSHUPS` + * Expected Outcome: Since exercise names are case insensitive, the system should successfully delete the 'pushups' exercise, confirming that case sensitivity is handled correctly. +3. Listing All Exercises: + - Test case 1: + * List all exercises. + * Command: `exercise /list` + * Expected Outcome: The system should display all current exercises stored in the system, regardless of the order they were added. + - Test case 2: + * List exercises when no exercises have been added. + * Command: `exercise /list` + * Expected Outcome: The system should display a message indicating that there are no exercises to display. +4. Editing an Exercise: + - Test case 1: + * Edit an existing exercise name. + * Command: `exercise /edit pushups /to Decline pushups` + * Expected Outcome: The system should confirm that the exercise name has been changed from `pushups` to `Decline pushups` + - Test case 2: + * Attempt to edit a non-existent exercise. + * Command: `exercise /edit crunches /to Incline crunches` + * Expected Outcome: The system should display an error message indicating that the original exercise does not exist. + - Test case 3: + * Edit an exercise to have a special character in the new name. + * Command: `exercise /edit Decline pushups /to Decline-pushups!` + * Expected Outcome: The system should display an error message indicating that the new name cannot contain special characters. + - Test case 4: + * Edit an exercise using the same existing name. + * Command: `exercise /edit Decline pushups /to Decline pushups` + * Expected Outcome: The system should notify that the new name is the same as the old name. +5. Searching for Exercises: + - Test case 1: + * Search for an exercise by partial name match. + * Command: `exercise /search push` + * Expected Outcome: The system should return all exercises that partially match `pushups`, including `pushups` and `Decline pushups` + - Test case 2: + * Search for an exercise with no matching entries. + * Command: `exercise /search pullups` + * Expected Outcome: The system should display a message indicating no search results. + - Test case 3: + * Search for an exercise using a complete name. + * Command: `exercise /search Decline pushups` + * Expected Outcome: The system should display only the `Decline` pushups exercise, ensuring that exact matches are correctly prioritized over partial matches. + - Test case 4: + * Search for an exercise immediately after deletion. + * Command: `exercise /search decline pushups` after deleting `decline pushups` + * Expected Outcome: The system should indicate that there are no results for `decline pushups`, confirming that the deletion was processed correctly. + +### Workout Management + +1. Adding a Workout Plan: + - Test case 1: + * Create a new workout plan. + * Command: `workout /create Leg Day` + * Expected Outcome: The system should confirm that the workout plan `leg day` has been created. + - Test case 2: + * Add a new workout plan with special characters. + * Command: `exercise /add push-ups!` + * Expected Outcome: The system should display an error message stating that the workout plan name cannot contain special characters. + - Test case 3: + * Add a duplicate workout plan. + * Command: `workout /create Arm-Day` + * Expected Outcome: The system should display an error message indicating that the workout plan already exists. + - Test case 4: + * Add a Workout plan with a case variation in name. + * Command: `workout /create LEG DAY` + * Expected Outcome: The system should display an error message indicating that the workout plan already exists, as the workout plan name is case insensitive. +2. Deleting a Workout Plan: + - Test case 1: + * Delete an existing workout plan. + * Command: `workout /delete leg day` + * Expected Outcome: The system should confirm that the workout plan `leg day` has been deleted. + - Test case 2: + * Attempt to delete a non-existent exercise. + * Command: `workout /delete back day` + * Expected Outcome: The system should display an error message indicating that the workout plan does not exist. + - Test case 3: + * Delete a workout plan with a case variation in name. + * Command: `workout /delete LEG DAY` + * Expected Outcome: Since workout names are case insensitive, the system should successfully delete the 'leg day' workout, confirming that case sensitivity is handled correctly. +3. Listing All Workout Plan: + - Test case 1: + * List all workout plan. + * Command: `workout /list` + * Expected Outcome: The system should display all workout plans stored in the system, regardless of the order they were added. + - Test case 2: + * List exercises when no exercises have been added. + * Command: `workout /list` + * Expected Outcome: The system should display a message indicating that there are no workout plans to display. +4. Editing a Workout Plan: + - Test case 1: + * Edit an existing workout plan name. + * Command: `workout /edit leg day /to back day` + * Expected Outcome: The system should confirm that the workout plan name has been changed from `leg day` to `back day` + - Test case 2: + * Attempt to edit a non-existent workout plan. + * Command: `workout /edit chest day /to pull day` + * Expected Outcome: The system should display an error message indicating that the original workout plan `chest day` does not exist. + - Test case 3: + * Edit a workout plan to have a special character in the new name. + * Command: `workout /edit Full Body Day /to Full Body Day-` + * Expected Outcome: The system should display an error message indicating that the new name cannot contain special characters. + - Test case 4: + * Edit a workout plan using the same existing name. + * Command: `workout /edit Full Body Day /to Full Body Day` + * Expected Outcome: The system should notify that the new name is the same as the old name. +5. Searching for workout plan: + - Test case 1: + * Search for an exercise by partial name match. + * Command: `workout /search day` + * Expected Outcome: The system should return all workout plans that partially match `day`, including `leg day` and `back day` + - Test case 2: + * Search for a workout plan with no matching entries. + * Command: `exercise /search chest` + * Expected Outcome: The system should display a message indicating no search results. + - Test case 3: + * Search for an exercise using a complete name. + * Command: `exercise /search leg day` + * Expected Outcome: The system should display only the `leg day` workout plan, ensuring that exact matches are correctly prioritized over partial matches. + - Test case 4: + * Search for an exercise immediately after deletion. + * Command: `workout /search leg day` after deleting `leg day` + * Expected Outcome: The system should indicate that there are no results for `leg day`, confirming that the deletion was processed correctly. +6. Assigning Exercises to Workout plan: + - Test case 1: + * Assign an exercise to a workout plan. + * Command: `workout /assign pushups /to Push Day` + * Expected Outcome: The system should confirm that `pushups` have been assigned to `Push Day`. + - Test case 2: + * Attempt to assign an exercise to a non-existent workout plan. + * Command: `workout /assign squats /to Nonexistent Plan` + * Expected Outcome: The system should indicate that the workout plan does not exist. + - Test case 3: + * Attempt to assign a non-existent exercise to a workout plan. + * Command: `workout /assign Nonexistent exercise /to Push Day` + * Expected Outcome: The system should indicate that the exercise does not exist. + - Test case 4: + * Assign an exercise to a workout plan with a case variation in name. + * Command: `workout /assign PUSHUPS /to PUSH DAY` + * Expected Outcome: The system should confirm that `pushups` have been assigned to `Push Day` as both the workout plan & exercise name are case insensitive. +7. Unassigning Exercises to Workout plan: + - Test case 1: + * Unassign an exercise from a workout plan. + * Command: `workout /unassign pushups /from Push Day` + * Expected Outcome: The system should confirm that `pushups` have been removed from `Push Day`. + - Test case 2: + * Attempt to unassign an exercise from a non-existent workout plan. + * Command: `workout /unassign squats /from Nonexistent Plan` + * Expected Outcome: The system should indicate that the workout plan does not exist. + - Test case 3: + * Attempt to unassign a non-existent exercise from a workout plan. + * Command: `workout /unassign Nonexistent exercise /from Push Day` + * Expected Outcome: The system should indicate that the exercise does not exist. + - Test case 4: + * Unassign an exercise to a workout plan with a case variation in name. + * Command: `workout /unassign PUSHUPS /from PUSH DAY` + * Expected Outcome: The system should confirm that `pushups` have been unassigned from `Push Day` as both the workout plan & exercise name are case insensitive. +8. Viewing Exercises in a Workout Plan: + - Test case 1: + * List all exercises in a specific workout plan. + * Command: `workout /info Push Day` + * Expected Outcome: The system should list all exercises included in `Push Day`. If the workout plan is empty, the system should indicate that there are no exercises. + - Test case 2: + * View an empty workout plan. + * Command: `workout /info Newbie Plan` (assuming no exercises have been assigned to `Newbie Plan`) + * Expected Outcome: The system should indicate that there are no exercises listed in `Newbie Plan`. + - Test case 3: + * View a workout plan with a case variation in name. + * Command: `workout /info PUSH DAY` + * Expected Outcome: The system should either display the details for `push day` confirming case insensitivity. + +### Program Management + +1. Assigning Workout Plans to Days: + - Test case 1: + * Assign a workout plan to a specific day. + * Command: `program /assign Push Day /to Monday` + * Expected Outcome: The system should confirm that `Push Day` has been assigned to Monday. + - Test case 2: + * Attempt to assign multiple workout plans to the same day. + * Command: `program /assign Leg Day /to Monday` + * Expected Outcome: The system should display an error message indicating that a workout is already assigned to Monday, as only one workout can be assigned per day. + - Test case 3: + * Assign workout plans to non-standard day formats. + * Command: `program /assign Push Day /to Mon` + * Expected Outcome: The system should recognize 'Mon' as Monday and successfully assign the workout plan, reflecting flexibility in day input. + - Test case 4: + * Assign empty workout plans to a specific day + * Command: `program /assign test /to Mon` + * Expected Outcome: The system should indicate that there are no workout plan called `test` +2. Viewing Today's Workout Plans: + - Test case 1: + * View today's workout plan when one is assigned + * Command: Assume today is Monday and `Push Day` is assigned to `Monday`, then execute `program /today` + * Expected Outcome: The system should display the exercises scheduled for `Push Day`. + - Test case 2: + * View today's workout when no workout is assigned + * Command: `program /today` + * Expected Outcome: The system should display a message indicating no workout is assigned for today. +3. Viewing Weekly's Workout Plans: + - Test case 1: + * View the weekly workout schedule + * Command:`program /list` + * Expected Outcome: The system should display the workout plan assigned to each day of the week, including any `Rest days` where no workouts are assigned. +4. Removing Workout Plans from Days: + - Test case 1: + * Remove a workout plan from a specific day + * Command:`program /clear Monday` + * Expected Outcome: The system should confirm that Monday's workout plan has been cleared, and subsequent checks for Monday should show no assigned workout. + - Test case 2: + * Attempt to clear a day with no workout assigned + * Command:`program /clear Sunday` (assuming no workout is assigned to Sunday) + * Expected Outcome: The system should notify that there was no workout to clear for Sunday + - Test case 3: + * Remove all workout plans in a week + * Command:`program /clear ` + * Expected Outcome: The system should confirm that all workouts for that week has been cleared. + +### Program Logging Management + +1. Adding Exercise Logs: + - Test case 1: + * Log a single set of an exercise. + * Command: `program /log benchpress /weight 125 /sets 1 /reps 5` (MUST create exercise & workout plan first & have a workout plan assigned for the day you are logging.) + * Expected Outcome: The system should confirm that the log entry for benchpress has been successfully created with the specified weight, sets, and reps. + - Test case 2: + * Log multiple sets with varying weights and reps. + * Command: `program /log benchpress /weight 100 110 120 /sets 3 /reps 5 4 3` + * Expected Outcome: The system should display an error message indicating that a workout is already assigned to Monday, as only one workout can be assigned per day. + - Test case 3: + * Attempt to log an exercise not created in the system. + * Command: `program /log nonexistent /weight 100 /sets 1 /reps 10` + * Expected Outcome: The system should display an error message indicating that the exercise does not exist, ensuring only valid exercises can be logged. + - Test case 4: + * Log an exercise for a specific past date + * Command: `program /log benchpress /weight 130 120 /sets 2 /reps 8 9 /date 2024-03-25` (There must be a workout assigned to the date first) + * Expected Outcome: The system should confirm that the exercise has been logged for the specified date, showing flexibility in recording workouts on different dates + - Test case 5: + * Log an exercise for an invalid date + * Command: `program /log benchpress /weight 130 120 /sets 2 /reps 8 9 /date 2024-2-31` (There must be a workout assigned to the date first) + * Expected Outcome: The system should confirm that the date does not exists and returns an error telling the user of the invalid date entered. + - Test case 6: + * Attempt to log an exercise not created in the system. + * Command: `program /log nonexistent /weight 100 /sets 1 /reps 10` + * Expected Outcome: The system should display an error message indicating that the exercise does not exist, ensuring only valid exercises can be logged. + - Test case 7: + * Log an exercise for a specific past date + * Command: `program /log benchpress /weight 130 120 /sets 2 /reps 8 9 /date 2024-03-25` (There must be a workout assigned to the date first) + * Expected Outcome: The system should confirm that the exercise has been logged for the specified date, showing flexibility in recording workouts on different dates + - Test case 8: + * Log an exercise for an invalid date + * Command: `program /log benchpress /weight 130 120 /sets 2 /reps 8 9 /date 2024-2-31` (There must be a workout assigned to the date first) + * Expected Outcome: The system should confirm that the date does not exists and returns an error telling the user of the invalid date entered. + - Test case 9: + * Log an exercise without specifying one or more required parameters + * Command: `program /log benchpress /weight 100 /sets 3` (missing reps) + * Expected Outcome: The system should display an error message requiring all parameters (weight, sets, reps) to be specified. + - Test case 10: + * Attempt to log with unrealistic or invalid values + * Command: `program /log benchpress /weight -10 /sets 3 /reps 100` + * Expected Outcome: The system should reject negative weights, ensuring realistic and valid data entry. + + 2. Viewing Exercise Logs: + - Test case 1: + * View the dates with logged workouts + * Command: `program /history` + * Expected Outcome: The system should list all the dates for which logs have been recorded, providing an overview of active workout days. + - Test case 2: + * View detailed logs for a specific date. + * Command: `program /history 2024-03-27` + * Expected Outcome: The system should display all exercises logged on that date along with their weights, sets, and reps, giving detailed insights into the workout for that day. + +### Help Menu Access +1. Viewing Help Messages: + - Test case 1: + * Incorrect Help Command Usage. + * Command: `help /exercis` + * Expected Outcome: The system should display an error message indicating incorrect command usage + - Test case 2: + * Accessing Help Menu for a Category. + * Command: `help /exercise` + * Expected Outcome: The system should display a list of exercise category-related commands and prompt the user to enter a specific list number to get detailed command formats. + - Test case 3: + * Request Specific Command Format from Category + * Command: `help /exercise 3` + * Expected Outcome: The system should display a specific command format, in this case, the format for editing an exercise's name, as specified by exercise category list number 3. + - Test case 4: + * Invalid List Number for Help Command (Out of Bounds) + * Command: `help /program 100` + * Expected Outcome: The system should notify the user that the list number is invalid or out of range and prompt them to select a valid number. + - Test case 5: + * Invalid List Number for Help Command (Non-numerical) + * Command: `help /workout abc` + * Expected Outcome: The system should notify the user that the list number is invalid or out of range and prompt them to select a valid number. + - Test case 6: + * Accessing Help Without Specifying a Category + * Command: `help /` + * Expected Outcome: The system should display an error message indicating that the user must specify a valid command. + - Test case 7: + * Accessing Help With Too Many Arguments i.e., too many `/`s + * Command: `help /exercise 1 /list` + * Expected Outcome: The system should display an error message indicating that additional (unneccesary) arguments have been provided. + - Test case 8: + * Accessing Help Menu Guidance Message + * Command: `help` + * Expected Outcome: The system should display a message explaining the commands for accessing each of the category-specific help menus. + +### Miscellaneous + +1. Exiting BYTE-CEPS: + * Test case 1: + * During normal execution. + * Command: `bye` + * Expected Outcome: BYTE-CEPS is exited and the files are safely saved. + * Test case 2: + * During normal execution. + * Command: `exit` + * Expected Outcome: BYTE-CEPS is exited and the files are safely saved. + diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..a2bd8667d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# Duke - -{Give product intro here} +# Project: ByteCeps +BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. +The user interacts with the tool using commands entered via the CLI interface. With BYTE-CEPS, they can compile a list of exercises, build custom workouts, assign workouts to a weekly schedule and log details of each exercise completed in each performed workout. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..12f648f4a3 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,699 @@ -# User Guide +# BYTE-CEPS User Guide ## Introduction +Welcome to BYTE-CEPS, your CLI-based all-in-one tool for setting and tracking fitness goals. Whether you're a tech-savvy fitness enthusiast or just starting your fitness journey, BYTE-CEPS offers the simplicity and efficiency of a CLI interface to help you maintain or improve your fitness through self-managed routines. -{Give a product intro} +Please note that, as of the current version, BYTE-CEPS supports tracking both weight-related exercises and body weight exercises. However, cardio tracking is not yet supported. Stay tuned for future updates as we strive to expand BYTE-CEPS to encompass a broader range of fitness activities. -## Quick Start +- [BYTE-CEPS User Guide](#byte-ceps-user-guide) + - [Introduction](#introduction) + - [Features](#features) + - [Usage](#usage) + - [Running ByteCeps](#running-byteceps) + - [Exercise Management](#exercise-management) + - [Add an exercise](#add-an-exercise) + - [Delete an exercise](#delete-an-exercise) + - [Edit an exercise](#edit-an-exercise) + - [List all exercises](#list-all-exercises) + - [Search exercises](#search-exercises) + - [Workout Plan Management](#workout-plan-management) + - [Add a workout plan](#add-a-workout-plan) + - [Delete a workout plan](#delete-a-workout-plan) + - [Edit Workout Plan](#edit-workout-plan) + - [List workout plan](#list-workout-plan) + - [Search workout plans](#search-workout-plans) + - [Assign an exercise to a workout plan](#assign-an-exercise-to-a-workout-plan) + - [Remove an exercise from a workout plan](#remove-an-exercise-from-a-workout-plan) + - [List all exercises in a workout plan](#list-all-exercises-in-a-workout-plan) + - [Program Management](#program-management) + - [Choose a workout plan for a day](#choose-a-workout-plan-for-a-day) + - [View Today's workout plan:](#view-todays-workout-plan) + - [View Weekly workout plan](#view-weekly-workout-plan) + - [Remove a workout plan for a day](#remove-a-workout-plan-for-a-day) + - [Remove all workouts in weekly program](#remove-all-workouts-in-weekly-program) + - [Logging Workouts](#logging-workouts) + - [Adding an exercise log](#adding-an-exercise-log) + - [To log a single set of an exercise](#to-log-a-single-set-of-an-exercise) + - [Logging Multiple Sets with Varying Weights and Reps](#logging-multiple-sets-with-varying-weights-and-reps) + - [Overwriting an Existing Log](#overwriting-an-existing-log) + - [Adding an exercise log for a separate date](#adding-an-exercise-log-for-a-separate-date) + - [Viewing logs](#viewing-logs) + - [Viewing historic logs](#viewing-historic-logs) + - [Help Menu](#help-menu) + - [Accessing Help Menu](#accessing-help-menu) + - [Displaying Help Menu Category: Exercise](#displaying-help-menu-category-exercise) + - [Displaying Help Menu Category: Workout](#displaying-help-menu-category-workout) + - [Displaying Help Menu Category: Program](#displaying-help-menu-category-program) + - [Exiting program](#exiting-program) + - [Saving the data](#saving-the-data) + - [Editing the data](#editing-the-data) + - [Command summary](#command-summary) -{Give steps to get started quickly} -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +## Features +BYTE-CEPS can track & manage several types of tasks, such as: +1. Exercise +2. Workout +3. Program + +## Usage +### Running ByteCeps +- You are required to install Java 11 onto your computer. +- Download the [latest release](https://github.com/AY2324S2-CS2113-F14-3/tp/releases) from the releases page. +- Run the program, in its own folder, from your preferred terminal using the command: java -jar byteceps.jar. -## Features +## Exercise Management +Using the `exercise` command, you may manage your exercises that have been stored in BYTE-CEPS. +- Do take note that exercise names are `case insensitive` + +### Add an exercise +You may add a new exercise using the `/add` flag. +``` +exercise /add +``` -{Give detailed description of each feature} +Example of usage: +``` +exercise /add pushups +``` + +Expected outcome: +``` +[BYTE-CEPS]> Added Exercise: pushups +``` +**Note:** Exercise names cannot contain special characters: { } [ ] / \\ : , # - + +### Delete an exercise +You may also delete an existing exercise using the `/delete` flag. +``` +exercise /delete +``` + +Example of usage: +``` +exercise /delete pushups +``` + +Expected outcome: +``` +[BYTE-CEPS]> Deleted Exercise: pushups +``` + +**Note:** Deleting an exercise also removes it from all workouts containing that exercise. +### Edit an exercise +If you ever need to edit an exercise name, you may do so using the `/edit` flag. +``` +exercise /edit /to +``` + +Example of usage: +``` +exercise /edit pushups /to Decline pushups +``` + +Expected outcome: +``` +[BYTE-CEPS]> Edited Exercise from pushups to Decline pushups +``` + +### List all exercises +You may list all existing exercises by using the `/list` flag. +``` +exercise /list +``` + +Example of usage: +``` +exercise /list +``` + +Expected outcome: +``` +[BYTE-CEPS]> Listing Exercises: + 1. Decline pushups +``` +**Note**: The exercises may not be listed in the order you added them to ByteCeps. + +### Search exercises +You may search exercises by using the `/search` flag. +``` +exercise /search +``` + +Example of usage: +``` +exercise /search pushup +``` + +Expected outcome: +``` +[BYTE-CEPS]> Search Results: + 1. pushup +``` + +## Workout Plan Management +A workout plan is a curated list of exercises that you would like to do in a single session. You may use the `workout` command to manage your workout plans. +- Do take note that workout plan names are `case insensitive` + +### Add a workout plan +In order to assign a exercise to a workout plan, you must first create a workout using the `/create` flag. +``` +workout /create +``` + +Example of usage: +``` +workout /create push day +``` + +Expected outcome: +``` +[BYTE-CEPS]> Added Workout Plan: push day +``` +**Note:** Workout Plan names cannot contain special characters: { } [ ] / \\ : , # - + +### Delete a workout plan +To delete an existing workout plan, use the `/delete` flag. +``` +workout /delete +``` + +Example of usage: +``` +workout /delete push day +``` + +Expected outcome: +``` +[BYTE-CEPS]> Deleted Workout: push day +``` +**Note:** Deleting a workout assigned to a day in your training program will also cause it to be removed from the training program. + +### Edit Workout Plan +If you ever need to edit a workout plan, you may do so using the `/edit` flag. +``` +workout /edit /to +``` + +Example of usage: +``` +workout /edit push day /to pull day +``` + +Expected outcome: +``` +[BYTE-CEPS]> Edited Workout Plan from push day to pull day +``` + +### List workout plan +You may list all your workout plans by using the `/list` flag. +``` +workout /list +``` + +Example of usage: +``` +workout /list +``` + +Expected outcome: +``` +[BYTE-CEPS]> Listing Workouts: + 1. test + 2. push day +``` +**Note**: The workouts may not be listed in the order you added them to ByteCeps. + +### Search workout plans +To search existing workout plans, use the `/search` flag. +``` +workout /search +``` + +Example of usage: +``` +workout /search day +``` + +Expected outcome: +``` +[BYTE-CEPS]> Search Results: + 1. leg day + 2. push day +``` + +### Assign an exercise to a workout plan +You may assign an exercise to a specified workout plan using the `/assign` flag. +``` +workout /assign /to +``` + +Example of usage: +``` +workout /assign pushups /to push day +``` +Expected outcome: +``` +[BYTE-CEPS]> Assigned Exercise 'pushups' to Workout Plan 'push day' +``` + +### Remove an exercise from a workout plan +You may remove an exercise from a specified workout plan using the `/unassign` flag. +``` +workout /unassign /from +``` + +Example of usage: +``` +workout /unassign pushups /from push day +``` + +Expected outcome: +``` +[BYTE-CEPS]> Unassigned Exercise 'pushups' from Workout Plan 'push day' +``` + +### List all exercises in a workout plan +You may see all the exercises in a given workout plan by using the `/info` flag. +``` +workout /info +``` + +Example of usage: +``` +workout /info push day +``` + +Expected outcome: +``` +[BYTE-CEPS]> Listing exercises in workout plan 'push day': + 1. pushups +``` +**Note**: The exercises may not be listed in the order you added them to the workout. + +## Program Management +The `program` command not only allows you to assign a workout to a given day, but it allows you to log your completed exercises. + +### Choose a workout plan for a day +You may assign a workout plan to a specific day of the week using the `/assign` flag. +``` +program /assign /to +``` + +The `` parameter must be one of the listed variants of a day of the week, and is case insensitive: +1. Monday / Mon +2. Tuesday / Tues / Tue +3. Wednesday / Wed +4. Thursday / Thurs / Thu +5. Friday / Fri +6. Saturday / Sat +7. Sunday / Sun + +Example of usage: +``` +program /assign push day /to monday +``` + +Expected outcome: +``` +[BYTE-CEPS]> Workout push day assigned to monday +``` + +**Note**: You can only assign ONE workout plan to any given day + +### View Today's workout plan: +You may see the workout plan for today using the `/today` flag. +``` +program /today +``` + +Example of usage: +``` +program /today +``` + +Expected outcome: +``` +[BYTE-CEPS]> Listing Exercises on 2024-03-28: + 1. benchpress + 2. overhead press + 3. chest fly +``` +OR +``` +[BYTE-CEPS]> There is no workout assigned today (MONDAY) +``` -### Adding a todo: `todo` -Adds a new item to the list of todo items. +### View Weekly workout plan +You may see all workout plans assigned to each day of the week by using the `/list` flag. +``` +program /list +``` -Format: `todo n/TODO_NAME d/DEADLINE` +Example of usage: +``` +program /list +``` + +Expected outcome: +``` +[BYTE-CEPS]> Your workouts for the week: + MONDAY: full body + 1. benchpress + 2. barbell squat + 3. deadlift + + TUESDAY: Rest day + + WEDNESDAY: legs + 1. leg extensions + 2. barbell squat + + THURSDAY: Rest day + + FRIDAY: push day + 1. benchpress + 2. overhead press + 3. chest fly + + SATURDAY: Rest day + + SUNDAY: Rest day +``` + +### Remove a workout plan for a day +You can also remove a workout plan from a given day of the week by using the `/clear` flag. +``` +program /clear +``` +As stated, the DAY parameter must follow the format above. + +Example of usage: +``` +program /clear Tuesday +``` + +Expected outcome: +``` +[BYTE-CEPS]> Your workout on Tuesday has been cleared +``` + +### Remove all workouts in weekly program +You can also remove all workout plans in a week by using the `/clear` flag without specifying the date. + +Example of usage: +``` +program /clear +``` + +Expected outcome: +``` +[BYTE-CEPS]> All your workouts have been cleared from the week +``` + +## Logging Workouts +You can log the amount of weight, sets, and repetitions you have completed for an exercise on a given day using the logging functionality. This allows for comprehensive tracking of your workout progress over time. + +To log your exercises, you must first have a workout plan assigned for the day you are logging. It's possible to log an exercise not originally in your workout plan, providing flexibility in your programs. However, the exercise must be created beforehand. + +### Adding an exercise log +You may create a workout log using the `/log` flag in the program command. Now, with enhanced functionality, you can log multiple weights and repetitions for each set, allowing for a more detailed recording of your workout. + +#### To log a single set of an exercise +``` +program /log /weight /sets /reps +``` + +Example of usage: +``` +program /log benchpress /weight 125 /sets 1 /reps 5 +``` + +Expected outcome: +``` +[BYTE-CEPS]> Successfully logged benchpress with a weight of 125kg and 5 reps across 1 set on 2024-03-28 +``` + +#### Logging Multiple Sets with Varying Weights and Reps +``` +program /log /weight /sets /reps +``` + +Example of usage: +``` +program /log benchpress /weight 100 110 120 /sets 3 /reps 5 4 3 +``` + +Expected outcome: +``` +[BYTE-CEPS]> Successfully logged benchpress with weights of 100kg, 110kg, 120kg and reps of 5, 4, 3 across 3 sets on 2024-03-28 +``` + +#### Overwriting an Existing Log +You may overwrite an existing logged exercise by logging an exercise with the same name. + +For example, you may have logged an exercise incorrectly. +``` +program /log benchpress /weight 100 11 120 /sets 3 /reps 5 4 3 +``` + +You may wish to overwrite it by sending the same command with the fixed values. +``` +program /log benchpress /weight 100 110 120 /sets 3 /reps 5 4 3 +``` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Expected outcome: +``` +[BYTE-CEPS]> It seems like benchpress already exists, overwriting... +[BYTE-CEPS]> Successfully logged benchpress with weights of 100kg, 110kg, 120kg and 5, 4, 3 reps across 3 sets on 2024-04-14 +``` + + +### Adding an exercise log for a separate date +You may also create a workout log for a specified date. +``` +program /log /weight /sets /reps /date +``` Example of usage: +``` +program /log benchpress /weight 125 /sets 1 /reps 5 /date 2024-03-25 +``` + +Expected outcome: +``` +[BYTE-CEPS]> Successfully logged 125kg benchpress with 3 sets and 5 reps on 2024-03-25 +``` + +### Viewing logs +You may see all the dates for which you have entered at least a single log entry by using the `/history` flag in the program command. +``` +program /history +``` + +Example of usage: +``` +program /history +``` + +Expected outcome: +``` +[BYTE-CEPS]> Listing Workout Logs: + 1. 2024-03-27 + 2. 2024-03-06 + 3. 2024-03-28 + 4. 2024-03-25 +``` + +### Viewing historic logs +You may view the logs that you have added on a given date by specifying a date in the `/history` flag. +``` +program /history +``` + +Example of usage: +``` +program /history 2024-03-27 +``` + +Expected outcome: +``` +[BYTE-CEPS]> Listing Exercises on 2024-03-27: + 1. barbell squat (weight: 70, sets: 3, reps: 5) + 2. leg extensions (weight: 55, sets: 3, reps: 15) +``` +## Help Menu +You are able to access an in-program help menu that provides you with command formats for all of ByteCeps's functionality. + +### Accessing Help Menu +ByteCeps provides the command format to access the help menu upon initial program execution: +``` +[BYTE-CEPS]> To access the help menu for command guidance, please type: +help /COMMAND_TYPE_FLAG +Available command types (type exactly as shown): +exercise +workout +program +To view this message again, enter 'help' alone +``` + +To view this message that provides guidance for how to access the help menu, a user only needs to type `help` with no additional flags or parameters. + +Command formats are shown according to 3 separate categories: `exercise`, `workout` & `program` + +### Displaying Help Menu Category: Exercise +You may access this portion of the help menu using the `/exercise` flag with the `help command`: +``` +help /exercise +``` + +Outcome: +``` +[BYTE-CEPS]> Please enter 'help /exercise LIST_NUMBER'. LIST_NUMBER corresponds to the exercise command format you want to see + 1. add an exercise + 2. delete an existing exercise + 3. edit an existing exercise's name + 4. list all existing exercises +``` + +To see a specific `exercise`-related command's format, enter `help /exercise `. -`todo n/Write the rest of the User Guide d/next week` +The `` parameter refers to the desired command's corresponding list number as displayed by the help menu. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +Example of usage: +``` +help /exercise 3 +``` + +Expected outcome: +``` +[BYTE-CEPS]> exercise /edit /to +``` + +### Displaying Help Menu Category: Workout +You may access this portion of the help menu using the `/workout` flag with the `help command`: +``` +help /workout +``` + +Outcome: +``` +[BYTE-CEPS]> Please enter 'help /workout LIST_NUMBER'. LIST_NUMBER corresponds to the workout command format you want to see + 1. create a workout plan + 2. delete an existing workout plan + 3. list all existing workout plans + 4. assign an exercise to a specified workout plan + 5. remove an exercise from a specified workout plan + 6. list all exercises in a given workout plan +``` + +To see a specific `workout`-related command's format, enter `help /workout `. + +The `` parameter refers to the desired command's corresponding list number as displayed by the help menu. + +Example of usage: +``` +help /workout 4 +``` + +Expected outcome: +``` +[BYTE-CEPS]> workout /assign /to +``` + +### Displaying Help Menu Category: Program +You may access this portion of the help menu using the `/program` flag with the `help command`: +``` +help /program +``` + +Outcome: +``` +[BYTE-CEPS]> Please enter 'help /program LIST_NUMBER'. LIST_NUMBER corresponds to the program command format you want to see + 1. assign a workout plan to a specific day of the week + 2. view today's workout plan + 3. see all workout plans assigned to each day of the week + 4. remove a workout plan from a given day of the week + 5. create a log for the amount of weight, sets & reps completed for an exercise on a given day + which already has an assigned workout plan + 6. create a log for a specified date + 7. see all the dates that you have entered at least 1 log entry + 8. view the logs that you have added on a specific date +``` + +To see a specific `program`-related command's format, enter `help /program `. + +The `` parameter refers to the desired command's corresponding list number as displayed by the help menu. + +Example of usage: +``` +help /program 7 +``` + +Expected outcome: +``` +[BYTE-CEPS]> program /history +``` + +## Exiting program +You may exit the program using the `exit` or the `bye` command. +``` +exit +``` +Example of usage: +``` +exit +``` +Expected outcome: +``` +[BYTE-CEPS]> All your workouts and exercises have been saved. +------------------------------------------------- +------------------------------------------------- +GOODBYE FOR NOW. STAY HARD! +------------------------------------------------- +``` -## FAQ +## Saving the data +BYTE-CEPS data are saved on the hard disk automatically after the `exit` command . There is no need to save manually. -**Q**: How do I transfer my data to another computer? +## Editing the data +BYTE-CEPS data are saved automatically as a JSON file `data.json` in the same directory as the Jar File. Advanced users are welcome to update data directly by editing that data file. -**A**: {your answer here} +> Caution: If your changes to the data file make its format invalid, BYTE-CEPS will save the current data file as `data.json.old_YYYY_MMDD_HHSS_MM` and start with an empty data file at the next run. +Furthermore, certain edits can cause BYTE-CEPS to behave in unexpected ways (e.g., if a date entered is of a different format). Therefore, only edit the data file if you are confident that you can do so correctly. -## Command Summary +## Command summary -{Give a 'cheat sheet' of commands here} +| Action | Format | Example | +|-----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------| +| Add an exercise | `exercise /add ` | `exercise /add pushups` | +| Delete an exercise | `exercise /delete ` | `exercise /delete pushups ` | +| Edit an exercise | `exercise /edit /to ` | `exercise /edit pushups /to Decline pushups` | +| List all exercises | `exercise /list ` | `exercise /list` | +| Search exercise | `exercise /search ` | `exercise /search pushups` | +| Add a workout plan | `workout /create ` | `workout /create push day ` | +| Delete a workout plan | `workout /delete ` | `workout /delete push day` | +| List all workout plans | `workout /list` | `workout /list` | +| Search workout plans | `workout /search ` | `workout /search push day ` | +| Assign an exercise to a workout plan | `workout /assign /to ` | `workout /assign pushups /to push day` | +| Remove an exercise from a workout plan | `workout /unassign /from ` | `workout /unassign pushups /from push day` | +| List all exercises in a workout plan | `workout /info ` | `workout /info push day` | +| Choose a workout plan for a day | `program /assign /to ` | `program /assign push day /to monday` | +| View Today's workout plan | `program /today ` | `program /today` | +| View Weekly workout plan | `program /list` | `program /list` | +| Remove a workout plan for a day | `program /clear ` | `program /clear Tuesday` | +| Remove all workouts in weekly program | `program /clear` | `program /clear` | +| Adding an exercise log | `program /log /weight /sets /reps ` | `program /log benchpress /weight 100 110 120 /sets 3 /reps 5 4 3` | +| Adding an exercise log for a separate date | `program /log /weight /sets /reps /date ` | `program /log benchpress /weight 125 /sets 3 /reps 5 /date 2024-03-25` | +| Viewing historic logs | `program /history` | `program /history` | +| Viewing historic logs | `program /history ` | `program /history ` | +| Displaying Help Menu Guidance Message | `help` | `help` +| Displaying Help Menu Category: Exercise | `help /exercise` | `help /exercise` | +| Displaying Help Menu Category: Workout | `help /workout` | `help /workout` | +| Displaying Help Menu Category: Program | `help /program` | `help /program ` | +| Displaying Command Format from Help Menu Category: Exercise | `help /exercise ` | `help /exercise 3` | +| Displaying Command Format from Help Menu Category: Workout | `help /workout ` | `help /workout 4` | +| Displaying Command Format from Help Menu Category: Program | `help /program ` | `help /program 7` | +| Exit the program | `bye ` | `bye ` | +| Exit the program | `bye ` | `bye ` | -* Add todo `todo n/TODO_NAME d/DEADLINE` diff --git a/docs/diagrams/ActivityClassDiagram.svg b/docs/diagrams/ActivityClassDiagram.svg new file mode 100644 index 0000000000..04ad53f0f2 --- /dev/null +++ b/docs/diagrams/ActivityClassDiagram.svg @@ -0,0 +1 @@ +Activity#activityName : String+Activity(activityName : String)+getActivityName() : String+setActivityName(activityName : String) : void+hashCode() : int+equals(obj : Object) : booleanExerciseLog-weights : List<Integer>-sets : int-repetitions : List<Integer>+ExerciseLog(activityName : String, weights : List<Integer>, sets : int, repetitions : List<Integer>)+getSets() : int+getRepetitions() : List<Integer>+getWeights() : List<Integer>WorkoutLog#workoutName : StringexerciseLogs : LinkedHashSet<ExerciseLog>+WorkoutLog(workoutDate : String, workoutName : String)+addExerciseLog(exerciseLog : ExerciseLog) : void+getWorkoutName() : String+getWorkoutDate() : String+getExerciseLogs() : LinkedHashSet<ExerciseLog>WorkoutexerciseList : ArrayList<Exercise>+Workout(workoutName : String)+getExerciseList() : ArrayList<Exercise>+getExerciseSet() : LinkedHashSet<Exercise>+addExercise(exercise : Exercise) : void+toString(numTabs : int) : String+editWorkoutName(newWorkoutName : String, activityManager : ActivityManager) : voidDay#assignedWorkout : Workout+Day(activityName : String)+setAssignedWorkout(assignedWorkout : Workout) : void+getAssignedWorkout() : WorkoutExercise+Exercise(exerciseName : String)+toString() : String+editExerciseName(newExerciseName : String, activityManager : ActivityManager) : voidassignedWorkout 0..1exercises *exerciseLogs * \ No newline at end of file diff --git a/docs/diagrams/ActivityManagerClassDiagram.svg b/docs/diagrams/ActivityManagerClassDiagram.svg new file mode 100644 index 0000000000..2b11f7fe6c --- /dev/null +++ b/docs/diagrams/ActivityManagerClassDiagram.svg @@ -0,0 +1 @@ +ExerciseManager+execute(parser : Parser) : String-executeEditAction(parser : Parser) : String-executeListAction(parser : Parser) : String-executeDeleteAction(parser : Parser) : String-executeAddAction(parser : Parser) : String-executeSearchAction(parser : Parser) : StringActivityManager#activityType : String#activitySet : LinkedHashSet<Activity>+ActivityManager()+execute(parser : Parser) : String+add(activity : Activity) : void+delete(activity : Activity) : void+retrieve(activityName : String) : Activity+getListString() : StringWorkoutManager-exerciseManager : ExerciseManager+WorkoutManager(exerciseManager : ExerciseManager)+execute(parser : Parser) : String-executeInfoAction(parser : Parser) : String-executeListAction(parser : Parser) : String-executeUnassignAction(parser : Parser) : String-executeEditAction(parser : Parser) : String-executeAssignAction(parser : Parser) : String-executeDeleteAction(parser : Parser) : String-executeCreateAction(parser : Parser) : String-executeSearchAction(parser : Parser) : StringWorkoutLogsManager+WorkoutLogsManager()+execute(parser : Parser) : String+addWorkoutLog(workoutLogDate : String, workoutName : String) : void+addExerciseLog(workoutLogDate : String, exerciseName : String, weight : String, sets : String, repetitions : String) : void+getWorkoutLogString(date : String, workoutLinkedHashSet : LinkedHashSet<Exercise>) : String+exportToJSON() : JSONArray-getWorkoutJson(exercises : LinkedHashSet<ExerciseLog>, workoutName : String, workoutDate : String) : JSONObjectWeeklyProgramManager-exerciseManager : ExerciseManager-workoutManager : WorkoutManager-workoutLogsManager : WorkoutLogsManager+WeeklyProgramManager(exerciseManager : ExerciseManager, workoutManager : WorkoutManager, workoutLogsManager : WorkoutLogsManager)+execute(parser : Parser) : String-executeListAction() : String-executeAssignAction(parser : Parser) : String-executeLogAction(parser : Parser) : String-executeTodayAction() : String-executeHistoryAction(parser : Parser) : String-executeClearAction(parser : Parser) : String+exportToJSON() : JSONObject1 exerciseManager1 exerciseManager1 workoutManager1 workoutLogsManager \ No newline at end of file diff --git a/docs/diagrams/addExercise.svg b/docs/diagrams/addExercise.svg new file mode 100644 index 0000000000..15e83364e6 --- /dev/null +++ b/docs/diagrams/addExercise.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:ExerciseManager:ExerciseValidatorparseInput(userInput)execute(parser)validateCommand(Parser)alt[Validation successful]executeAddAction(Parser)new(name='pushups'):Exercisereturn newExerciseadd(newExercise)return messageToUsermessageToUserprintMessage(messageToUser)[Validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/addExerciseLog.png b/docs/diagrams/addExerciseLog.png new file mode 100644 index 0000000000..54b753c6c6 Binary files /dev/null and b/docs/diagrams/addExerciseLog.png differ diff --git a/docs/diagrams/addExerciseLog.svg b/docs/diagrams/addExerciseLog.svg new file mode 100644 index 0000000000..6fac28a25e --- /dev/null +++ b/docs/diagrams/addExerciseLog.svg @@ -0,0 +1 @@ +ByteCeps:UserInterface:Parser:WeeklyProgramManagerWeeklyProgramValidator:WorkoutLogsManagerparseInput(userInput)execute(parser)validateCommand(parser)alt[Input validation successful]executeLogActiongetDayFromDate(workoutDate)selectedDaygetWorkoutName(selectedDay, workoutDate)workoutNameaddWorkoutLog(workoutDate, workoutName)addExerciseLog(workoutDate, exerciseName, weight, sets, repetition)messageToUsermessageToUserprintMessage(messageToUser)[Input validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/architectureDiagram.svg b/docs/diagrams/architectureDiagram.svg new file mode 100644 index 0000000000..20ce135d88 --- /dev/null +++ b/docs/diagrams/architectureDiagram.svg @@ -0,0 +1 @@ +ByteCeps AppByteCepsUserInterfaceParserExerciseManagerWorkoutManagerWeeklyProgramManagerWorkoutLogsManagerHelpMenuManagerStorageUser \ No newline at end of file diff --git a/docs/diagrams/assignExercise.svg b/docs/diagrams/assignExercise.svg new file mode 100644 index 0000000000..629418e2c9 --- /dev/null +++ b/docs/diagrams/assignExercise.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:WorkoutManager:WorkoutValidator:ExerciseManager:Exercise:WorkoutparseInput(userInput)execute(parser)validateCommand(Parser)alt[Validation successful]executeAssignAction(Parser)retrieve('Pushups')retrieve('Pushups')findByName('Pushups')return existingExerciseexistingExercisereturn existingExerciseretrieve('LegDay')findByName('LegDay')return existingWorkoutaddExercise(existingExercise)return existingWorkoutreturn messageToUsermessageToUserprintMessage(messageToUser)[Validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/assignWorkoutToProgram.svg b/docs/diagrams/assignWorkoutToProgram.svg new file mode 100644 index 0000000000..8c4b519533 --- /dev/null +++ b/docs/diagrams/assignWorkoutToProgram.svg @@ -0,0 +1 @@ +ByteCeps:UserInterface:Parser:WeeklyProgramManagerWeeklyProgramValidator:WorkoutManagerparseInput(userInput)execute(parser)validateCommand(parser)alt[Input validation successful]executeAssignAction(parser)retrieve(workoutName)workoutassignWorkoutToDay(workout, day)messageToUsermessageToUsermessageToUserprintMessage(messageToUser)[Input validation fails]printMessage(exceptionMessage) \ No newline at end of file diff --git a/docs/diagrams/banner.jpeg b/docs/diagrams/banner.jpeg new file mode 100644 index 0000000000..f6803ca92f Binary files /dev/null and b/docs/diagrams/banner.jpeg differ diff --git a/docs/diagrams/clearProgram.svg b/docs/diagrams/clearProgram.svg new file mode 100644 index 0000000000..fd825f681d --- /dev/null +++ b/docs/diagrams/clearProgram.svg @@ -0,0 +1 @@ +ByteCeps:UserInterFace:WeeklyProgramManagerexecute(parser)executeClearAction(parser)alt[No specific day specified by user]clearActivitySet()initializeDays()messageToUsermessageToUser[Specific day entered by user]remove(selectedDay)new Day(selectedDayString):DaynewDaysetAssignedWorkout(null)add(newDay)messageToUserprintMessage(messageToUser) \ No newline at end of file diff --git a/docs/diagrams/deleteExerciseFromWorkouts.svg b/docs/diagrams/deleteExerciseFromWorkouts.svg new file mode 100644 index 0000000000..796fa3339d --- /dev/null +++ b/docs/diagrams/deleteExerciseFromWorkouts.svg @@ -0,0 +1 @@ +CascadingDeletionProcessor:WorkoutManager:WorkoutworkoutExerciseList:ArrayList<Exercise>removeDeletedExerciseFromWorkouts(exerciseName, workoutManager)getActivityList()workoutListloop[for every workout in workoutList]opt[deleted exercise is in workout]getExerciseList()workoutExerciseList()removeIf(exercise -> exercise.equals(exerciseName)) \ No newline at end of file diff --git a/docs/diagrams/deleteWorkout.svg b/docs/diagrams/deleteWorkout.svg new file mode 100644 index 0000000000..b6afc8d24b --- /dev/null +++ b/docs/diagrams/deleteWorkout.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:WorkoutManager:WorkoutValidator:WorkoutparseInput(userInput)execute(parser)validateCommand(Parser)alt[Validation successful]executeDeleteAction(Parser)retrieve('LegDay')findByName('LegDay')return existingWorkoutexistingWorkoutdelete(existingWorkout)return messageToUsermessageToUserprintMessage(messageToUser)[Validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/deleteWorkoutFromProgram.svg b/docs/diagrams/deleteWorkoutFromProgram.svg new file mode 100644 index 0000000000..5eb805d6bc --- /dev/null +++ b/docs/diagrams/deleteWorkoutFromProgram.svg @@ -0,0 +1 @@ +CascadingDeletionProcessor:WeeklyProgramManagernewWorkoutsInProgram:LinkedHashSet<Workout>currentDay:DayremoveDeletedWorkoutsFromProgram(workoutName, weeklyProgramManager)getDaySet()newWorkoutsInProgramnew LinkedHashSet(newWorkoutsInProgram)oldWorkoutsInProgram:LinkedHashSet<Workout>oldWorkoutsInProgramloop[for every day in oldWorkoutsInProgram]getAssignedWorkout()workoutopt[workout name matches currentDay.getAssignedWorkout()]remove(currentDay)new Day(currentDayString)newDay:DaynewDaysetAssignedWorkout(null)add(newDay) \ No newline at end of file diff --git a/docs/diagrams/helpGuidanceMessage.svg b/docs/diagrams/helpGuidanceMessage.svg new file mode 100644 index 0000000000..e5c5d089c0 --- /dev/null +++ b/docs/diagrams/helpGuidanceMessage.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:HelpMenuManager«static»HelpStringsHelpValidatorparseInput('help')execute(parser)validateCommand(Parser)Invalid Input Exception thrownalt[Expected error message for 'help']getHelpGuidanceString()retrieve HELP_GUIDANCE_MESSAGEHELP_GUIDANCE_MESSAGEHELP_GUIDANCE_MESSAGEmessageToUser = HELP_GUIDANCE_MESSAGEprintMessage(messageToUser)[Unexpected error message]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/helpMenuCommandFormat.svg b/docs/diagrams/helpMenuCommandFormat.svg new file mode 100644 index 0000000000..c3491650d0 --- /dev/null +++ b/docs/diagrams/helpMenuCommandFormat.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:HelpMenuManager«static»HelpStringsHelpValidatorparseInput('help /exercise 1')execute(parser)validateCommand(Parser)Validation successalt[Validation successful]getParamFormat('1', 'exercise')getExerciseParamFormats(0)retrieve EXERCISE_PARAM_FORMAT[0]EXERCISE_PARAM_FORMAT[0]EXERCISE_PARAM_FORMAT[0]EXERCISE_PARAM_FORMAT[0]messageToUser = EXERCISE_PARAM_FORMAT[0]printMessage(messageToUser)[Validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/helpMenuWholeMenu.svg b/docs/diagrams/helpMenuWholeMenu.svg new file mode 100644 index 0000000000..40a51c1bf5 --- /dev/null +++ b/docs/diagrams/helpMenuWholeMenu.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:HelpMenuManager«static»HelpStringsHelpValidatorparseInput('help /program')execute(parser)validateCommand(Parser)Validation successalt[Validation successful]generateAllActions('program')retrieve PROGRAM_FLAG_FUNCTIONSresult = PROGRAM_FLAG_FUNCTIONSresultmessageToUser = resultprintMessage(messageToUser)[Validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/listExerciseInWorkoutPlan.svg b/docs/diagrams/listExerciseInWorkoutPlan.svg new file mode 100644 index 0000000000..388f3300dc --- /dev/null +++ b/docs/diagrams/listExerciseInWorkoutPlan.svg @@ -0,0 +1 @@ +ByteCepsUserInterface:Parser:WorkoutManager:WorkoutValidator:Workout:ExerciseparseInput(userInput)execute(parser)validateCommand(Parser)alt[Validation successful]executeInfoAction(Parser)retrieve('workoutplan')findByName('workoutplan')return existingWorkoutexistingWorkoutgetExerciseList()return exerciseListloop[Each Exercise in exerciseList]getName()contribute to messagereturn messageToUsermessageToUserprintMessage(messageToUser)[Validation fails]printMessage(exceptionMessage : String) \ No newline at end of file diff --git a/docs/diagrams/loadStorage.svg b/docs/diagrams/loadStorage.svg new file mode 100644 index 0000000000..92794c0795 --- /dev/null +++ b/docs/diagrams/loadStorage.svg @@ -0,0 +1 @@ +ByteCeps:Storage:UserInterfaceload(exerciseManager, workoutManager, weeklyProgramManager, workoutLogsManager)alt[data.json exists]new JSONObject(jsonScanner.nextLine()):JSONObjectjsonArchiveloadExercises(allExercises, jsonArchive)loadWorkouts(allExercises, allWorkouts, jsonArchive)loadWeeklyProgram(allWorkouts, weeklyProgram, jsonArchive)loadWorkoutLogs(allExercises, allWorkouts, jsonArchive, workoutLogsManager)printMessage(LOAD_SUCCESS)[data.json does not exist]createNewFile():FilenewFileprintMessage(NO_SAVE_DATA); \ No newline at end of file diff --git a/docs/diagrams/loadWorkouts.svg b/docs/diagrams/loadWorkouts.svg new file mode 100644 index 0000000000..9836c2e76e --- /dev/null +++ b/docs/diagrams/loadWorkouts.svg @@ -0,0 +1 @@ +ByteCeps:Storage:UserInterfacejsonArchive:JSONObjectjsonWorkoutArray:JSONArrayjsonWorkout:JSONObjectjsonExercisesInWorkout:JSONArrayjsonExercise:JSONObjectloadWorkouts(allExercises, allWorkouts, jsonArchive)getJSONArray("workoutManager")jsonWorkoutArray:JSONArrayloop[for every jsonWorkout in jsonWorkoutArray]getJSONObject(index)jsonWorkout:JSONObjectgetString("activityName")workoutName:Stringnew Workout(workoutName):Workoutworkout:WorkoutgetJSONArray("exerciseList")jsonExercisesInWorkout:JSONArrayloop[for every jsonExercise in jsonExercisesInWorkout]getJSONObject(index)jsonExercise:JSONObjectgetString("activityName")exerciseInWorkout:StringallExercises.retrieve(exerciseInWorkout)retrievedExercise:ExerciseaddExercise(retrievedExercise) \ No newline at end of file diff --git a/docs/diagrams/programToday.svg b/docs/diagrams/programToday.svg new file mode 100644 index 0000000000..e4873542ba --- /dev/null +++ b/docs/diagrams/programToday.svg @@ -0,0 +1 @@ +ByteCeps:UserInterface:WeeklyProgramManagertoday:Dayexecute(parser)executeTodayAction(parser)LocalDate.now()currentDate:LocalDatecurrentDategetDayFromDate(currentDate)today:DaygetAssignedWorkout()todaysWorkout:WorkouttoString()todayDate:StringgetTodaysWorkoutString(todaysWorkout, todayDate, today)messageToUsermessageToUsermessageToUserprintMessage(messageToUser) \ No newline at end of file diff --git a/docs/diagrams/saveStorage.svg b/docs/diagrams/saveStorage.svg new file mode 100644 index 0000000000..81905ed4ad --- /dev/null +++ b/docs/diagrams/saveStorage.svg @@ -0,0 +1 @@ +ByteCeps:UserInterface:Storage:ExerciseManager:WorkoutManager:WeeklyProgramManager:WorkoutLogsManagersave(exerciseManager, workoutManager, weeklyProgramManager, workoutLogsManager)new JSONObject()jsonArchive:JSONObjectjsonArchivegetActivityList().toArray()exerciseList:Arrayput(exerciseList)getActivityList().toArray()workoutList:Arrayput(workoutList)exportToJSON()weeklyProgram:JSONObjectput(weeklyProgram)exportToJSON()workoutLogs:JSONObjectput(workoutLogs)new FileWriter(filePath):FileWritertoString()archiveStringwrite(archiveString)close()printMessage(WORKOUTS_SAVED) \ No newline at end of file diff --git a/docs/diagrams/workoutLog.svg b/docs/diagrams/workoutLog.svg new file mode 100644 index 0000000000..765e81e19c --- /dev/null +++ b/docs/diagrams/workoutLog.svg @@ -0,0 +1 @@ +:WeeklyProgramManager:WorkoutLogsManagerexecuteLogActionaddWorkoutLog(workoutDate, workoutName)new(workoutDate, workoutName):WorkoutLogreturn newWorkoutLogadd(newWorkoutLog)addExerciseLog(workoutDate, exerciseName, weight, sets, repetition)new(exerciseName, weightsList, setsInt, repsList):ExerciseLogreturn newExerciseLogretrieve(workoutLogDate)workoutLogaddExerciseLog(newExerciseLog)messageToUser \ No newline at end of file diff --git a/docs/plantuml/ActivityClassDiagram.puml b/docs/plantuml/ActivityClassDiagram.puml new file mode 100644 index 0000000000..20b09f67be --- /dev/null +++ b/docs/plantuml/ActivityClassDiagram.puml @@ -0,0 +1,59 @@ +@startuml +skinparam classAttributeIconSize 0 +skinparam style strictuml +hide circle +class Activity{ +# activityName : String ++ Activity(activityName : String) ++ getActivityName() : String ++ setActivityName(activityName : String) : void ++ hashCode() : int ++ equals(obj : Object) : boolean +} +class ExerciseLog{ +- weights : List +- sets : int +- repetitions : List ++ ExerciseLog(activityName : String, weights : List, sets : int, repetitions : List) ++ getSets() : int ++ getRepetitions() : List ++ getWeights() : List +} +ExerciseLog --|> Activity +class WorkoutLog{ +# workoutName : String +exerciseLogs : LinkedHashSet ++ WorkoutLog(workoutDate : String, workoutName : String) ++ addExerciseLog(exerciseLog : ExerciseLog) : void ++ getWorkoutName() : String ++ getWorkoutDate() : String ++ getExerciseLogs() : LinkedHashSet +} +WorkoutLog --|> Workout +class Day{ +# assignedWorkout : Workout ++ Day(activityName : String) ++ setAssignedWorkout(assignedWorkout : Workout) : void ++ getAssignedWorkout() : Workout +} +Day --|> Activity +class Exercise{ ++ Exercise(exerciseName : String) ++ toString() : String ++ editExerciseName(newExerciseName : String, activityManager : ActivityManager) : void +} +Exercise --|> Activity +class Workout{ +exerciseList : ArrayList ++ Workout(workoutName : String) ++ getExerciseList() : ArrayList ++ getExerciseSet() : LinkedHashSet ++ addExercise(exercise : Exercise) : void ++ toString(numTabs : int) : String ++ editWorkoutName(newWorkoutName : String, activityManager : ActivityManager) : void +} +Workout --|> Activity +Day-->"assignedWorkout 0..1" Workout +Workout-->"exercises *" Exercise +WorkoutLog --> "exerciseLogs *"ExerciseLog +@enduml \ No newline at end of file diff --git a/docs/plantuml/ActivityManagerClassDiagram.puml b/docs/plantuml/ActivityManagerClassDiagram.puml new file mode 100644 index 0000000000..05f8e311da --- /dev/null +++ b/docs/plantuml/ActivityManagerClassDiagram.puml @@ -0,0 +1,68 @@ +@startuml +skinparam classAttributeIconSize 0 +skinparam style strictuml +hide circle + +class ExerciseManager{ ++ execute(parser : Parser) : String +- executeEditAction(parser : Parser) : String +- executeListAction(parser : Parser) : String +- executeDeleteAction(parser : Parser) : String +- executeAddAction(parser : Parser) : String +- executeSearchAction(parser : Parser) : String +} +ExerciseManager --|> ActivityManager +class WorkoutManager{ +- exerciseManager : ExerciseManager ++ WorkoutManager(exerciseManager : ExerciseManager) ++ execute(parser : Parser) : String +- executeInfoAction(parser : Parser) : String +- executeListAction(parser : Parser) : String +- executeUnassignAction(parser : Parser) : String +- executeEditAction(parser : Parser) : String +- executeAssignAction(parser : Parser) : String +- executeDeleteAction(parser : Parser) : String +- executeCreateAction(parser : Parser) : String +- executeSearchAction(parser : Parser) : String +} +WorkoutManager --|> ActivityManager +class WorkoutLogsManager{ ++ WorkoutLogsManager() ++ execute(parser : Parser) : String ++ addWorkoutLog(workoutLogDate : String, workoutName : String) : void ++ addExerciseLog(workoutLogDate : String, exerciseName : String, weight : String, sets : String, repetitions : String) : void ++ getWorkoutLogString(date : String, workoutLinkedHashSet : LinkedHashSet) : String ++ exportToJSON() : JSONArray +- {static} getWorkoutJson(exercises : LinkedHashSet, workoutName : String, workoutDate : String) : JSONObject +} +WorkoutLogsManager --|> ActivityManager +class WeeklyProgramManager{ +- exerciseManager : ExerciseManager +- workoutManager : WorkoutManager +- workoutLogsManager : WorkoutLogsManager ++ WeeklyProgramManager(exerciseManager : ExerciseManager, workoutManager : WorkoutManager, workoutLogsManager : WorkoutLogsManager) ++ execute(parser : Parser) : String +- executeListAction() : String +- executeAssignAction(parser : Parser) : String +- executeLogAction(parser : Parser) : String +- executeTodayAction() : String +- executeHistoryAction(parser : Parser) : String +- executeClearAction(parser : Parser) : String ++ exportToJSON() : JSONObject +} +WeeklyProgramManager --|> ActivityManager +abstract class ActivityManager{ +# activityType : String +# activitySet : LinkedHashSet ++ ActivityManager() ++ {abstract} execute(parser : Parser) : String ++ add(activity : Activity) : void ++ delete(activity : Activity) : void ++ retrieve(activityName : String) : Activity ++ getListString() : String +} +WorkoutManager-->"1 exerciseManager" ExerciseManager +WeeklyProgramManager-->"1 exerciseManager" ExerciseManager +WeeklyProgramManager-->"1 workoutManager" WorkoutManager +WeeklyProgramManager-->"1 workoutLogsManager" WorkoutLogsManager +@enduml \ No newline at end of file diff --git a/docs/plantuml/ByteCep.puml b/docs/plantuml/ByteCep.puml new file mode 100644 index 0000000000..f72b349863 --- /dev/null +++ b/docs/plantuml/ByteCep.puml @@ -0,0 +1,78 @@ +@startuml +skinparam classAttributeIconSize 0 +skinparam style strictuml +package byteceps { + + class Storage { + + FILE_PATH: String + + load(exerciseManager: ExerciseManager, workoutManager: WorkoutManager, weeklyProgramManager: WeeklyProgramManager, workoutLogsManager: WorkoutLogsManager): void + + save(exerciseManager: ExerciseManager, workoutManager: WorkoutManager, weeklyProgramManager: WeeklyProgramManager, workoutLogsManager: WorkoutLogsManager): void + } + + class UserInterface { + + printWelcomeMessage(): void + + printGoodbyeMessage(): void + + getUserInput(): String + + printMessage(message: String): void + } + + class Parser { + + parseInput(userInput: String): void + + getCommand(): String + } + + class Exceptions { + + ActivityExistsException + + ErrorAddingActivity + + InvalidInput + + ActivityDoesNotExists + + IllegalStateException + } + + class ExerciseManager { + + execute(parser: Parser): void + } + + class WorkoutManager { + + WorkoutManager(exerciseManager: ExerciseManager) + + execute(parser: Parser): void + } + + class WeeklyProgramManager { + + WeeklyProgramManager(exerciseManager: ExerciseManager, workoutManager: WorkoutManager, workoutLogsManager: WorkoutLogsManager) + + execute(parser: Parser): void + } + + class WorkoutLogsManager { + } + + class ByteCeps { + + exerciseManager: ExerciseManager (static) + + workoutManager: WorkoutManager (static) + + weeklyProgramManager: WeeklyProgramManager (static) + + workoutLogsManager: WorkoutLogsManager (static) + + parser: Parser (static) + + ui: UserInterface (static) + + storage: Storage (static) + + ByteCeps() + + run(): void + + commandLine(): void + } + + ByteCeps <-> Storage : uses + ByteCeps <-> UserInterface : uses + ByteCeps <-> Parser : uses + ByteCeps o-- ExerciseManager + ByteCeps o-- WorkoutManager + WorkoutManager o-- ExerciseManager + ByteCeps o-- WeeklyProgramManager + WeeklyProgramManager o-- ExerciseManager + WeeklyProgramManager o-- WorkoutManager + WeeklyProgramManager o-- WorkoutLogsManager + ByteCeps o-- WorkoutLogsManager + +} + + @enduml + + diff --git a/docs/plantuml/WorkoutLog.puml b/docs/plantuml/WorkoutLog.puml new file mode 100644 index 0000000000..a1129807e0 --- /dev/null +++ b/docs/plantuml/WorkoutLog.puml @@ -0,0 +1,42 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ":WeeklyProgramManager" + +activate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : executeLogAction + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WorkoutLogsManager" : addWorkoutLog(workoutDate, workoutName) + activate ":WorkoutLogsManager" + create ":WorkoutLog" as WorkoutLog + ":WorkoutLogsManager" -> WorkoutLog : new(workoutDate, workoutName) + activate WorkoutLog + WorkoutLog --> ":WorkoutLogsManager" : return newWorkoutLog + deactivate WorkoutLog + ":WorkoutLogsManager" -> ":WorkoutLogsManager" : add(newWorkoutLog) + activate ":WorkoutLogsManager" + ":WorkoutLogsManager" --> ":WorkoutLogsManager" + deactivate ":WorkoutLogsManager" + ":WorkoutLogsManager" --> ":WeeklyProgramManager" + deactivate ":WorkoutLogsManager" + ":WeeklyProgramManager" -> ":WorkoutLogsManager" : addExerciseLog(workoutDate, exerciseName, weight, sets, repetition) + activate ":WorkoutLogsManager" + create ":ExerciseLog" as ExerciseLog + ":WorkoutLogsManager" -> ExerciseLog : new(exerciseName, weightsList, setsInt, repsList) + activate ExerciseLog + ExerciseLog --> ":WorkoutLogsManager" : return newExerciseLog + deactivate ExerciseLog + ":WorkoutLogsManager" -> ":WorkoutLogsManager" : retrieve(workoutLogDate) + activate ":WorkoutLogsManager" + ":WorkoutLogsManager" --> ":WorkoutLogsManager": workoutLog + deactivate ":WorkoutLogsManager" + ":WorkoutLogsManager" -> WorkoutLog : addExerciseLog(newExerciseLog) + activate WorkoutLog + WorkoutLog --> ":WorkoutLogsManager" + deactivate WorkoutLog + ":WorkoutLogsManager" --> ":WeeklyProgramManager" + deactivate ":WorkoutLogsManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": messageToUser + deactivate ":WeeklyProgramManager" +@enduml diff --git a/docs/plantuml/addExercise.puml b/docs/plantuml/addExercise.puml new file mode 100644 index 0000000000..78536c34ad --- /dev/null +++ b/docs/plantuml/addExercise.puml @@ -0,0 +1,59 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":ExerciseManager" as ExerciseManager +participant ":ExerciseValidator" as Validator + +User -> Parser : parseInput(userInput) +activate Parser +User <-- Parser +deactivate Parser + +User -> ExerciseManager : execute(parser) +activate ExerciseManager + +ExerciseManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> ExerciseManager : +deactivate Validator + +alt Validation successful + ExerciseManager -> ExerciseManager : executeAddAction(Parser) + activate ExerciseManager #FFBBBB + + create ":Exercise" as Exercise + ExerciseManager -> Exercise : new(name='pushups') + activate Exercise + Exercise --> ExerciseManager : return newExercise + deactivate Exercise + + ExerciseManager -> ExerciseManager : add(newExercise) + activate ExerciseManager + ExerciseManager --> ExerciseManager : + deactivate ExerciseManager #FFBBBB + + ExerciseManager --> ExerciseManager : return messageToUser + deactivate ExerciseManager + + ExerciseManager --> User: messageToUser + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User + deactivate UserInterface + +else Validation fails + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +deactivate ExerciseManager +deactivate Parser + +@enduml \ No newline at end of file diff --git a/docs/plantuml/addExerciseLog.puml b/docs/plantuml/addExerciseLog.puml new file mode 100644 index 0000000000..f0023f6700 --- /dev/null +++ b/docs/plantuml/addExerciseLog.puml @@ -0,0 +1,61 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":UserInterface" as UserInterface +participant ":Parser" +participant ":WeeklyProgramManager" +participant WeeklyProgramValidator +participant ":WorkoutLogsManager" + +activate ByteCeps +ByteCeps -> ":Parser": parseInput(userInput) +activate ":Parser" +ByteCeps <-- ":Parser": +deactivate ":Parser" +ByteCeps -> ":WeeklyProgramManager": execute(parser) +activate ":WeeklyProgramManager" +":WeeklyProgramManager" -> WeeklyProgramValidator: validateCommand(parser) + +activate WeeklyProgramValidator +":WeeklyProgramManager" <-- WeeklyProgramValidator : +deactivate WeeklyProgramValidator + +alt Input validation successful + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : executeLogAction + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : getDayFromDate(workoutDate) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager" : selectedDay + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : getWorkoutName(selectedDay, workoutDate) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager" : workoutName + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WorkoutLogsManager" : addWorkoutLog(workoutDate, workoutName) + activate ":WorkoutLogsManager" + ":WorkoutLogsManager" --> ":WeeklyProgramManager" : + deactivate ":WorkoutLogsManager" + ":WeeklyProgramManager" -> ":WorkoutLogsManager" : addExerciseLog(workoutDate, exerciseName, weight, sets, repetition) + activate ":WorkoutLogsManager" + ":WorkoutLogsManager" --> ":WeeklyProgramManager" : + deactivate ":WorkoutLogsManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": messageToUser + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ByteCeps: messageToUser + ByteCeps -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> ByteCeps : + deactivate UserInterface + +else Input validation fails + ":WeeklyProgramManager" --> ByteCeps: + deactivate ":WeeklyProgramManager" + ByteCeps -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> ByteCeps: + deactivate UserInterface +end +deactivate WeeklyProgramValidator +@enduml diff --git a/docs/plantuml/architectureDiagram.puml b/docs/plantuml/architectureDiagram.puml new file mode 100644 index 0000000000..7f1717a972 --- /dev/null +++ b/docs/plantuml/architectureDiagram.puml @@ -0,0 +1,48 @@ +@startuml +hide footbox + +skinparam classAttributeIconSize 0 +skinparam style strictuml +actor User +'box #white +'frame ""f" +'participant user2 +'participant AthletiCLI +'participant Ui +'participant Parser +'participant Data +'participant Storage +'participant Commands +frame "ByteCeps App"{ +rectangle ByteCeps +rectangle UserInterface +rectangle Parser +rectangle ExerciseManager +rectangle WorkoutManager +rectangle WeeklyProgramManager +rectangle WorkoutLogsManager +rectangle HelpMenuManager +rectangle Storage +'end rectangle + +} +'end frame +'end box + +User -d-> UserInterface +UserInterface -r-> ByteCeps +ByteCeps -d-> Parser +Parser -d-> ExerciseManager +Parser -d-> WorkoutManager +Parser -d-> WeeklyProgramManager +Parser -d-> HelpMenuManager +ExerciseManager -d-> WeeklyProgramManager +WorkoutManager -d-> WeeklyProgramManager +WeeklyProgramManager -r-> WorkoutLogsManager +WorkoutLogsManager -d-> Storage +WeeklyProgramManager -d-> Storage +ExerciseManager -d-> Storage +WorkoutManager -d-> Storage + + +@enduml \ No newline at end of file diff --git a/docs/plantuml/assignExercise.puml b/docs/plantuml/assignExercise.puml new file mode 100644 index 0000000000..4e88f25467 --- /dev/null +++ b/docs/plantuml/assignExercise.puml @@ -0,0 +1,80 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":WorkoutManager" as WorkoutManager +participant ":WorkoutValidator" as Validator +participant ":ExerciseManager" as ExerciseManager +participant ":Exercise" as Exercise +participant ":Workout" as Workout + +User -> Parser : parseInput(userInput) +activate Parser +User <-- Parser +deactivate Parser + +User -> WorkoutManager : execute(parser) +activate WorkoutManager + +WorkoutManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> WorkoutManager : +deactivate Validator + +alt Validation successful + + WorkoutManager -> WorkoutManager : executeAssignAction(Parser) + activate WorkoutManager #FFBBBB + + WorkoutManager -> ExerciseManager : retrieve('Pushups') + activate ExerciseManager + ExerciseManager -> ExerciseManager : retrieve('Pushups') + activate ExerciseManager + ExerciseManager -> Exercise : findByName('Pushups') + activate Exercise + Exercise --> ExerciseManager : return existingExercise + ExerciseManager --> ExerciseManager : existingExercise + deactivate ExerciseManager + deactivate Exercise + ExerciseManager --> WorkoutManager : return existingExercise + + + WorkoutManager -> WorkoutManager : retrieve('LegDay') + activate WorkoutManager + WorkoutManager -> Workout : findByName('LegDay') + activate Workout + Workout --> WorkoutManager : return existingWorkout + deactivate Workout + + WorkoutManager -> Workout : addExercise(existingExercise) + activate Workout + Workout --> WorkoutManager : + deactivate Workout + + WorkoutManager --> WorkoutManager : return existingWorkout + deactivate WorkoutManager #FFBBBB + + WorkoutManager --> WorkoutManager : return messageToUser + deactivate WorkoutManager #FFBBBB + WorkoutManager --> User : messageToUser + + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User + deactivate UserInterface + +else Validation fails + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +deactivate WorkoutManager +deactivate Parser + +@enduml \ No newline at end of file diff --git a/docs/plantuml/assignWorkoutToProgram.puml b/docs/plantuml/assignWorkoutToProgram.puml new file mode 100644 index 0000000000..3549593e41 --- /dev/null +++ b/docs/plantuml/assignWorkoutToProgram.puml @@ -0,0 +1,51 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":UserInterface" as UserInterface +participant ":Parser" +participant ":WeeklyProgramManager" +participant WeeklyProgramValidator +participant ":WorkoutManager" + +ByteCeps -> ":Parser": parseInput(userInput) +activate ":Parser" +ByteCeps <-- ":Parser": +deactivate ":Parser" +ByteCeps -> ":WeeklyProgramManager": execute(parser) +activate ":WeeklyProgramManager" +":WeeklyProgramManager" -> WeeklyProgramValidator: validateCommand(parser) + +activate WeeklyProgramValidator +":WeeklyProgramManager" <-- WeeklyProgramValidator : +deactivate WeeklyProgramValidator + +alt Input validation successful + ":WeeklyProgramManager" -> ":WeeklyProgramManager": executeAssignAction(parser) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WorkoutManager": retrieve(workoutName) + activate ":WorkoutManager" + ":WorkoutManager" --> ":WeeklyProgramManager": workout + deactivate ":WorkoutManager" + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : assignWorkoutToDay(workout, day) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager" : messageToUser + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": messageToUser + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ByteCeps : messageToUser + ByteCeps -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> ByteCeps : + deactivate UserInterface +else Input validation fails + ":WeeklyProgramManager" --> ByteCeps: + deactivate ":WeeklyProgramManager" + ByteCeps -> UserInterface: printMessage(exceptionMessage) + activate UserInterface + UserInterface --> ByteCeps: + deactivate UserInterface +end +deactivate WeeklyProgramValidator +@enduml diff --git a/docs/plantuml/clearProgram.puml b/docs/plantuml/clearProgram.puml new file mode 100644 index 0000000000..6e085be29b --- /dev/null +++ b/docs/plantuml/clearProgram.puml @@ -0,0 +1,54 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":UserInterFace" as UserInterface +participant ":WeeklyProgramManager" + + activate ByteCeps + ByteCeps -> ":WeeklyProgramManager" : execute(parser) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : executeClearAction(parser) + activate ":WeeklyProgramManager" + alt No specific day specified by user + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : clearActivitySet() + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : initializeDays() + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": messageToUser + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ByteCeps: messageToUser + else Specific day entered by user + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : remove(selectedDay) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": + deactivate ":WeeklyProgramManager" + create Day as ":Day" + ":WeeklyProgramManager" -> Day : new Day(selectedDayString) + activate Day + Day --> ":WeeklyProgramManager" : newDay + deactivate Day + ":WeeklyProgramManager" -> Day : setAssignedWorkout(null) + activate Day + Day --> ":WeeklyProgramManager" : + deactivate Day + ":WeeklyProgramManager" -> ":WeeklyProgramManager" : add(newDay) + activate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ":WeeklyProgramManager": + deactivate ":WeeklyProgramManager" + ":WeeklyProgramManager" --> ByteCeps: messageToUser + deactivate ":WeeklyProgramManager" + end + + ByteCeps -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> ByteCeps : + deactivate UserInterface + + +@enduml diff --git a/docs/plantuml/deleteExerciseFromWorkouts.puml b/docs/plantuml/deleteExerciseFromWorkouts.puml new file mode 100644 index 0000000000..a91c832784 --- /dev/null +++ b/docs/plantuml/deleteExerciseFromWorkouts.puml @@ -0,0 +1,36 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant CascadingDeletionProcessor as c +participant ":WorkoutManager" as WorkoutManager +participant ":Workout" as Workout +participant "workoutExerciseList:ArrayList" as exerciseList + + +activate c +c -> c : removeDeletedExerciseFromWorkouts(exerciseName, workoutManager) +activate c + +c-> WorkoutManager : getActivityList() +activate WorkoutManager +WorkoutManager --> c: workoutList +deactivate WorkoutManager + +loop for every workout in workoutList + opt deleted exercise is in workout + c -> Workout : getExerciseList() + activate Workout + Workout --> c: workoutExerciseList() + deactivate Workout + + c-> exerciseList : removeIf(exercise -> exercise.equals(exerciseName)) + activate exerciseList + exerciseList --> c: + deactivate exerciseList + end +end +c--> c: +deactivate c + +@enduml \ No newline at end of file diff --git a/docs/plantuml/deleteWorkout.puml b/docs/plantuml/deleteWorkout.puml new file mode 100644 index 0000000000..3467b460bc --- /dev/null +++ b/docs/plantuml/deleteWorkout.puml @@ -0,0 +1,63 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":WorkoutManager" as WorkoutManager +participant ":WorkoutValidator" as Validator +participant ":Workout" as Workout + +User -> Parser : parseInput(userInput) +activate Parser +User <-- Parser +deactivate Parser + +User -> WorkoutManager : execute(parser) +activate WorkoutManager + +WorkoutManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> WorkoutManager : +deactivate Validator + +alt Validation successful + WorkoutManager -> WorkoutManager : executeDeleteAction(Parser) + activate WorkoutManager #FFBBBB + + WorkoutManager -> WorkoutManager : retrieve('LegDay') + activate WorkoutManager + WorkoutManager -> Workout : findByName('LegDay') + activate Workout + Workout --> WorkoutManager : return existingWorkout + deactivate Workout + WorkoutManager --> WorkoutManager : existingWorkout + deactivate WorkoutManager #FFBBBB + + WorkoutManager -> WorkoutManager : delete(existingWorkout) + activate WorkoutManager + WorkoutManager --> WorkoutManager : + deactivate WorkoutManager + + + WorkoutManager --> WorkoutManager : return messageToUser + deactivate WorkoutManager #FFBBBB + WorkoutManager --> User : messageToUser + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User + deactivate UserInterface + +else Validation fails + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +deactivate WorkoutManager +deactivate Parser + +@enduml diff --git a/docs/plantuml/deleteWorkoutFromProgram.puml b/docs/plantuml/deleteWorkoutFromProgram.puml new file mode 100644 index 0000000000..feb7777a10 --- /dev/null +++ b/docs/plantuml/deleteWorkoutFromProgram.puml @@ -0,0 +1,59 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant CascadingDeletionProcessor as c +participant ":WeeklyProgramManager" as WeeklyProgramManager +participant "newWorkoutsInProgram:LinkedHashSet" as newWorkouts +participant "currentDay:Day" as currentDay + +activate c + +c-> c: removeDeletedWorkoutsFromProgram(workoutName, weeklyProgramManager) +activate c + +c->WeeklyProgramManager: getDaySet() +activate WeeklyProgramManager +WeeklyProgramManager --> c: newWorkoutsInProgram +deactivate WeeklyProgramManager + +create "oldWorkoutsInProgram:LinkedHashSet" as oldWorkouts +c-> oldWorkouts: new LinkedHashSet(newWorkoutsInProgram) +activate oldWorkouts +oldWorkouts --> c: oldWorkoutsInProgram +deactivate oldWorkouts + +loop for every day in oldWorkoutsInProgram + c->currentDay : getAssignedWorkout() + activate currentDay + currentDay --> c: workout + deactivate currentDay + + opt workout name matches currentDay.getAssignedWorkout() + c->newWorkouts: remove(currentDay) + activate newWorkouts + newWorkouts --> c: + deactivate newWorkouts + + create "newDay:Day" as newDay + c->newDay: new Day(currentDayString) + activate newDay + newDay --> c: newDay + deactivate newDay + + c->newDay: setAssignedWorkout(null) + activate newDay + newDay --> c: + deactivate newDay + + c-> newWorkouts: add(newDay) + activate newWorkouts + newWorkouts --> c: + deactivate newWorkouts + end +end +c--> c: +deactivate c + + +@enduml \ No newline at end of file diff --git a/docs/plantuml/helpGuidanceMessage.puml b/docs/plantuml/helpGuidanceMessage.puml new file mode 100644 index 0000000000..607f46b4e1 --- /dev/null +++ b/docs/plantuml/helpGuidanceMessage.puml @@ -0,0 +1,50 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":HelpMenuManager" as HelpMenuManager +participant HelpStrings as "<> \n HelpStrings" +participant HelpValidator as Validator + + +User -> Parser : parseInput('help') +activate Parser +User <-- Parser +deactivate Parser + +User -> HelpMenuManager : execute(parser) +activate HelpMenuManager + +HelpMenuManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> HelpMenuManager : Invalid Input Exception thrown +deactivate Validator + +alt Expected error message for 'help' + HelpMenuManager -> HelpMenuManager : getHelpGuidanceString() + activate HelpMenuManager #FFBBBB + + + HelpMenuManager -> HelpStrings : retrieve HELP_GUIDANCE_MESSAGE + activate HelpStrings + HelpMenuManager <-- HelpStrings : HELP_GUIDANCE_MESSAGE + deactivate HelpStrings + HelpMenuManager --> HelpMenuManager : HELP_GUIDANCE_MESSAGE + deactivate HelpMenuManager #FFBBBB + + User <-- HelpMenuManager : messageToUser = HELP_GUIDANCE_MESSAGE + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User: + deactivate UserInterface + +else Unexpected error message + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +@enduml \ No newline at end of file diff --git a/docs/plantuml/helpMenuCommandFormat.puml b/docs/plantuml/helpMenuCommandFormat.puml new file mode 100644 index 0000000000..b4c34e1b5d --- /dev/null +++ b/docs/plantuml/helpMenuCommandFormat.puml @@ -0,0 +1,54 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":HelpMenuManager" as HelpMenuManager +participant HelpStrings as "<> \n HelpStrings" +participant HelpValidator as Validator + + +User -> Parser : parseInput('help /exercise 1') +activate Parser +User <-- Parser +deactivate Parser + +User -> HelpMenuManager : execute(parser) +activate HelpMenuManager + +HelpMenuManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> HelpMenuManager : Validation success +deactivate Validator + +alt Validation successful + HelpMenuManager -> HelpMenuManager : getParamFormat('1', 'exercise') + activate HelpMenuManager #FFBBBB + + HelpMenuManager -> HelpMenuManager : getExerciseParamFormats(0) + activate HelpMenuManager + HelpMenuManager -> HelpStrings : retrieve EXERCISE_PARAM_FORMAT[0] + activate HelpStrings + HelpMenuManager <-- HelpStrings : EXERCISE_PARAM_FORMAT[0] + deactivate HelpStrings + HelpMenuManager --> HelpMenuManager: EXERCISE_PARAM_FORMAT[0] + deactivate HelpMenuManager + HelpMenuManager --> HelpMenuManager : EXERCISE_PARAM_FORMAT[0] + deactivate HelpMenuManager #FFBBBB + + User <-- HelpMenuManager : messageToUser = EXERCISE_PARAM_FORMAT[0] + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User: + deactivate UserInterface + + +else Validation fails + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +@enduml \ No newline at end of file diff --git a/docs/plantuml/helpMenuWholeMenu.puml b/docs/plantuml/helpMenuWholeMenu.puml new file mode 100644 index 0000000000..daa8ac92b4 --- /dev/null +++ b/docs/plantuml/helpMenuWholeMenu.puml @@ -0,0 +1,51 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":HelpMenuManager" as HelpMenuManager +participant HelpStrings as "<> \n HelpStrings" +participant HelpValidator as Validator + + +User -> Parser : parseInput('help /program') +activate Parser +User <-- Parser +deactivate Parser + +User -> HelpMenuManager : execute(parser) +activate HelpMenuManager + +HelpMenuManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> HelpMenuManager : Validation success +deactivate Validator + +alt Validation successful + HelpMenuManager -> HelpMenuManager : generateAllActions('program') + activate HelpMenuManager #FFBBBB + + + HelpMenuManager -> HelpStrings : retrieve PROGRAM_FLAG_FUNCTIONS + activate HelpStrings + HelpMenuManager <-- HelpStrings : result = PROGRAM_FLAG_FUNCTIONS + deactivate HelpStrings + HelpMenuManager --> HelpMenuManager : result + deactivate HelpMenuManager #FFBBBB + + User <-- HelpMenuManager : messageToUser = result + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User: + deactivate UserInterface + + +else Validation fails + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +@enduml \ No newline at end of file diff --git a/docs/plantuml/listExerciseInWorkoutPlan.puml b/docs/plantuml/listExerciseInWorkoutPlan.puml new file mode 100644 index 0000000000..c8be6edd32 --- /dev/null +++ b/docs/plantuml/listExerciseInWorkoutPlan.puml @@ -0,0 +1,72 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps as User +participant UserInterface +participant ":Parser" as Parser +participant ":WorkoutManager" as WorkoutManager +participant ":WorkoutValidator" as Validator +participant ":Workout" as Workout +participant ":Exercise" as Exercise + +User -> Parser : parseInput(userInput) +activate Parser +User <-- Parser +deactivate Parser + +User -> WorkoutManager : execute(parser) +activate WorkoutManager + +WorkoutManager -> Validator : validateCommand(Parser) +activate Validator +Validator --> WorkoutManager : +deactivate Validator + +alt Validation successful + + WorkoutManager -> WorkoutManager : executeInfoAction(Parser) + activate WorkoutManager #FFBBBB + + WorkoutManager -> WorkoutManager : retrieve('workoutplan') + activate WorkoutManager + WorkoutManager -> Workout : findByName('workoutplan') + activate Workout + Workout --> WorkoutManager : return existingWorkout + deactivate Workout + WorkoutManager --> WorkoutManager : existingWorkout + deactivate WorkoutManager #FFBBBB + + WorkoutManager -> Workout : getExerciseList() + activate Workout + Workout --> WorkoutManager : return exerciseList + deactivate Workout + + loop Each Exercise in exerciseList + WorkoutManager -> Exercise : getName() + activate Exercise + Exercise --> WorkoutManager : contribute to message + deactivate Exercise + end + + WorkoutManager --> WorkoutManager : return messageToUser + deactivate WorkoutManager + WorkoutManager --> User : messageToUser + + User -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> User + deactivate UserInterface + +else Validation fails + User -> UserInterface: printMessage(exceptionMessage : String) + activate UserInterface + UserInterface --> User: + deactivate UserInterface +end + +deactivate WorkoutManager +deactivate Parser + +@enduml \ No newline at end of file diff --git a/docs/plantuml/loadStorage.puml b/docs/plantuml/loadStorage.puml new file mode 100644 index 0000000000..0c21d827b8 --- /dev/null +++ b/docs/plantuml/loadStorage.puml @@ -0,0 +1,65 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":Storage" as Storage +participant ":UserInterface" as UserInterface + +activate ByteCeps +ByteCeps -> Storage : load(exerciseManager, workoutManager, weeklyProgramManager, workoutLogsManager) +activate Storage +alt data.json exists + create ":JSONObject" as JSONObject + Storage -> JSONObject : new JSONObject(jsonScanner.nextLine()) + activate JSONObject + JSONObject --> Storage : jsonArchive + deactivate JSONObject + + Storage -> Storage : loadExercises(allExercises, jsonArchive) + activate Storage + Storage --> Storage : + deactivate Storage + + Storage -> Storage : loadWorkouts(allExercises, allWorkouts, jsonArchive) + activate Storage + Storage --> Storage : + deactivate Storage + + Storage -> Storage : loadWeeklyProgram(allWorkouts, weeklyProgram, jsonArchive) + activate Storage + Storage --> Storage : + deactivate Storage + + Storage -> Storage : loadWorkoutLogs(allExercises, allWorkouts, jsonArchive, workoutLogsManager) + activate Storage + Storage --> Storage : + deactivate Storage + + Storage -> UserInterface : printMessage(LOAD_SUCCESS) + activate UserInterface + UserInterface --> Storage: + deactivate UserInterface + + Storage --> ByteCeps: +else data.json does not exist + create ":File" as File + Storage -> File : createNewFile() + activate File + File --> Storage : newFile + deactivate File + Storage -> UserInterface : printMessage(NO_SAVE_DATA); + activate UserInterface + UserInterface --> Storage + deactivate UserInterface + Storage --> ByteCeps : + deactivate Storage +end + + + + + + +@enduml \ No newline at end of file diff --git a/docs/plantuml/loadWorkouts.puml b/docs/plantuml/loadWorkouts.puml new file mode 100644 index 0000000000..efee2fdb7d --- /dev/null +++ b/docs/plantuml/loadWorkouts.puml @@ -0,0 +1,69 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":Storage" as Storage +participant ":UserInterface" as UserInterface +participant "jsonArchive:JSONObject" as jsonArchive +participant "jsonWorkoutArray:JSONArray" as jsonWorkoutArray +participant "jsonWorkout:JSONObject" as jsonWorkout +participant "jsonExercisesInWorkout:JSONArray" as jsonExerciseList +participant "jsonExercise:JSONObject" as jsonExercise + +activate Storage +Storage -> Storage : loadWorkouts(allExercises, allWorkouts, jsonArchive) +activate Storage +Storage -> jsonArchive : getJSONArray("workoutManager") +activate jsonArchive +jsonArchive --> Storage : jsonWorkoutArray:JSONArray +deactivate jsonArchive +loop for every jsonWorkout in jsonWorkoutArray + Storage -> jsonWorkoutArray : getJSONObject(index) + activate jsonWorkoutArray + jsonWorkoutArray --> Storage : jsonWorkout:JSONObject + deactivate jsonWorkoutArray + + Storage -> jsonWorkout : getString("activityName") + activate jsonWorkout + jsonWorkout -> Storage : workoutName:String + deactivate jsonWorkout + + create ":Workout" as Workout + Storage -> Workout: new Workout(workoutName) + activate Workout + Workout --> Storage : workout:Workout + deactivate Workout + + Storage -> jsonWorkout : getJSONArray("exerciseList") + activate jsonWorkout + jsonWorkout --> Storage : jsonExercisesInWorkout:JSONArray + deactivate jsonWorkout + + loop for every jsonExercise in jsonExercisesInWorkout + Storage -> jsonExerciseList : getJSONObject(index) + activate jsonExerciseList + jsonExerciseList --> Storage : jsonExercise:JSONObject + deactivate jsonExerciseList + + Storage -> jsonExercise : getString("activityName") + activate jsonExercise + jsonExercise --> Storage : exerciseInWorkout:String + deactivate jsonExercise + + Storage -> Storage : allExercises.retrieve(exerciseInWorkout) + activate Storage + Storage --> Storage : retrievedExercise:Exercise + deactivate Storage + + Storage -> Workout : addExercise(retrievedExercise) + activate Workout + Workout --> Storage: + deactivate Workout + end +end +Storage --> Storage : +deactivate Storage + +@enduml \ No newline at end of file diff --git a/docs/plantuml/programToday.puml b/docs/plantuml/programToday.puml new file mode 100644 index 0000000000..0755efe47a --- /dev/null +++ b/docs/plantuml/programToday.puml @@ -0,0 +1,55 @@ +@startuml SequenceDiagram +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":UserInterface" as UserInterface +participant ":WeeklyProgramManager" as WeeklyProgramManager +participant "today:Day" as today + + + activate ByteCeps + ByteCeps -> WeeklyProgramManager : execute(parser) + activate WeeklyProgramManager + WeeklyProgramManager -> WeeklyProgramManager : executeTodayAction(parser) + activate WeeklyProgramManager + + create "currentDate:LocalDate" as currentDate + WeeklyProgramManager -> currentDate : LocalDate.now() + activate currentDate + currentDate --> WeeklyProgramManager : currentDate + deactivate currentDate + + WeeklyProgramManager -> WeeklyProgramManager : getDayFromDate(currentDate) + activate WeeklyProgramManager + WeeklyProgramManager --> WeeklyProgramManager : today:Day + deactivate WeeklyProgramManager + + WeeklyProgramManager -> today : getAssignedWorkout() + activate today + today --> WeeklyProgramManager : todaysWorkout:Workout + deactivate today + + WeeklyProgramManager -> currentDate : toString() + activate currentDate + currentDate --> WeeklyProgramManager : todayDate:String + deactivate currentDate + + WeeklyProgramManager -> WeeklyProgramManager : getTodaysWorkoutString(todaysWorkout, todayDate, today) + activate WeeklyProgramManager + WeeklyProgramManager --> WeeklyProgramManager : messageToUser + deactivate WeeklyProgramManager + + WeeklyProgramManager --> WeeklyProgramManager : messageToUser + deactivate WeeklyProgramManager + + WeeklyProgramManager --> ByteCeps : messageToUser + deactivate WeeklyProgramManager + + ByteCeps -> UserInterface: printMessage(messageToUser) + activate UserInterface + UserInterface --> ByteCeps : + deactivate UserInterface + + +@enduml diff --git a/docs/plantuml/saveStorage.puml b/docs/plantuml/saveStorage.puml new file mode 100644 index 0000000000..ffa4a8d44a --- /dev/null +++ b/docs/plantuml/saveStorage.puml @@ -0,0 +1,94 @@ +@startuml SequenceDiagram + +skinparam classAttributeIconSize 0 +skinparam style strictuml + +participant ByteCeps +participant ":UserInterface" as UserInterface +participant ":Storage" as Storage + + + +activate ByteCeps +ByteCeps -> Storage : save(exerciseManager, workoutManager, weeklyProgramManager, workoutLogsManager) +activate Storage + +create "jsonArchive:JSONObject" as jsonArchive +Storage -> jsonArchive : new JSONObject() +activate jsonArchive +jsonArchive --> Storage : jsonArchive +deactivate jsonArchive + +participant ":ExerciseManager" as ExerciseManager +participant ":WorkoutManager" as WorkoutManager +participant ":WeeklyProgramManager" as WeeklyProgramManager +participant ":WorkoutLogsManager" as WorkoutLogsManager + +Storage -> ExerciseManager : getActivityList().toArray() +activate ExerciseManager +ExerciseManager --> Storage : exerciseList:Array +deactivate ExerciseManager + +Storage -> jsonArchive : put(exerciseList) +activate jsonArchive +jsonArchive --> Storage : +deactivate jsonArchive + +Storage -> WorkoutManager : getActivityList().toArray() +activate WorkoutManager +WorkoutManager --> Storage : workoutList:Array +deactivate WorkoutManager + +Storage -> jsonArchive : put(workoutList) +activate jsonArchive +jsonArchive --> Storage : +deactivate jsonArchive + +Storage -> WeeklyProgramManager : exportToJSON() +activate WeeklyProgramManager +WeeklyProgramManager --> Storage : weeklyProgram:JSONObject +deactivate WeeklyProgramManager + +Storage -> jsonArchive : put(weeklyProgram) +activate jsonArchive +jsonArchive --> Storage : +deactivate jsonArchive + +Storage -> WorkoutLogsManager : exportToJSON() +activate WorkoutLogsManager +WorkoutLogsManager --> Storage : workoutLogs:JSONObject +deactivate WorkoutLogsManager + +Storage -> jsonArchive : put(workoutLogs) +activate jsonArchive +jsonArchive --> Storage : +deactivate jsonArchive + +create ":FileWriter" as FileWriter +Storage -> FileWriter: new FileWriter(filePath) +activate FileWriter +FileWriter --> Storage: +deactivate FileWriter + +Storage -> jsonArchive : toString() +activate jsonArchive +jsonArchive --> Storage : archiveString +deactivate jsonArchive + +Storage -> FileWriter: write(archiveString) +activate FileWriter +FileWriter --> Storage : +deactivate FileWriter + +Storage -> FileWriter : close() +destroy FileWriter + +Storage -> UserInterface : printMessage(WORKOUTS_SAVED) +activate UserInterface +UserInterface --> Storage : +deactivate UserInterface + +Storage --> ByteCeps : +deactivate Storage + +@enduml \ No newline at end of file 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/joshualeejunyi.md b/docs/team/joshualeejunyi.md new file mode 100644 index 0000000000..16cf057842 --- /dev/null +++ b/docs/team/joshualeejunyi.md @@ -0,0 +1,75 @@ +--- +layout: page +title: Lee Jun Yi, Joshua - Project Portfolio Page +--- + +# Project: ByteCeps +BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. +The user interacts with the tool using commands entered via the CLI interface. With BYTE-CEPS, they can compile a list of exercises, build custom workouts, assign workouts to a weekly schedule and log details of each exercise completed in each performed workout. + +## Summary of Contributions +### Code contributed +* Do check out the RepoSense link [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs%7Efunctional-code%7Etest-code%7Eother&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=joshualeejunyi&tabRepo=AY2324S2-CS2113-F14-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs%7Efunctional-code%7Etest-code%7Eother&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false)! + +### Skeleton of the project +* As we were discussing and preparing for the implementation of ByteCeps, I drafted and wrote the abstractions that most of the features were built on top of. +Namely, the Activity and ActivityManager abstract base classes that are used for all other features in the project. + +### Features implemented +#### New feature: Users have the ability to log their workouts +* For all users on ByteCeps, they are encouraged to keep to their workout plan by logging their exercises as they go about their workouts. +Through the `program /log` command, with the appropriate parameters, they will then be able to log an exercise. +I planned and implemented this feature which has several layers to implement, and is more elaborated on in the Developer Guide. + +#### New feature: Users have the ability to see the past logs that they have added +* Users are able to specify a date to see the logged exercises, using the `program /history` command. +This will list all the exercises that they have done, along with the weights, number of sets and repetitions that they have completed. +I implemented this history feature on top of the ability to log as users would always want to keep track of their progress. + +#### New feature: Logged workouts are stored in the save file +* As users log their workouts to keep track of their progress, it is pivot that their data is saved properly. +I implemented the processing of all the workout log data into the json file. + +### Enhancements made +### Enhancement: Add usage messages on incomplete commands +* After a round of user testing, an important feedback from the users was that incomplete commands did not guide them to what the correct commands were to be inputted. +As such, I tweaked all print messages to reflect the commands that they needed to input instead of a generic error message. + +### Enhancement: Validation of Workout Logs +* I wrote WorkoutLogsValidator to ensure that inputs are validated when logging exercises, and to enhance the SLAP of the code. + +### Enhancement: Bug fixes +* I fixed several bugs that can be seen in the [GitHub repo here](https://github.com/AY2324S2-CS2113-F14-3/tp/pulls?q=is%3Apr+is%3Aclosed+author%3Ajoshualeejunyi+label%3Abug). + +## Contributions to the UG +* Added the Table of Contents for the UG +* Authored the Program Management / Workout Logging section of the UG +* Contributed to standardizing the language and format of the UG + +## Contributions to the DG +### Sections contributed: +* Contributed to the Architecture section +* Described in detail regarding the flow and process of Logging an exercise + +### Added UML diagrams +* Class Diagram for ByteCeps Architecture: + + ![](../diagrams/architectureDiagram.svg) + +* Sequence diagram for logging workouts: + + ![](../diagrams/workoutLog.svg) + +## Contributions to team-based tasks +* Setup organization in GitHub +* Regularly followed up on issues and PRs +* Submitted and managed latest release version `v2.1` +* Regularly reviewed [PRs](https://github.com/AY2324S2-CS2113-F14-3/tp/pulls?q=is%3Apr+is%3Aclosed+reviewed-by%3A%40me), some examples: + * [Add Storage class and functionality](https://github.com/AY2324S2-CS2113-F14-3/tp/pull/60#pullrequestreview-1956391203) + * [Refactor code to use UserInterface.printMessage() ](https://github.com/AY2324S2-CS2113-F14-3/tp/pull/30#discussion_r1527492503) + +## Contributions beyond the project team +* PE dry Run: + * [Screenshot captured during PE dry run](https://github.com/joshualeejunyi/ped/tree/main/files) +* DG review for other teams: + * [Timetable Comparer](https://github.com/nus-cs2113-AY2324S2/tp/pull/39) diff --git a/docs/team/lwachtel1.md b/docs/team/lwachtel1.md new file mode 100644 index 0000000000..a0d508faab --- /dev/null +++ b/docs/team/lwachtel1.md @@ -0,0 +1,52 @@ +# Lukas Ethan Wachtel (LWachtel1) Project Portfolio Page + +## Project: ByteCeps +BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. +The user interacts with the tool using commands entered via the CLI interface. With BYTE-CEPS, they can compile a list of exercises, build custom workouts, assign workouts to a weekly schedule and log details of each exercise completed in each performed workout. + +## Summary of Contributions: [LWachtel1 RepoSense](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=lwachtel1&breakdown=true) + +### New Features ++ **Help menu**: Shows the user how to correctly format each possible BYTE-CEPS command. + + What it does: Upon initial program execution, a guidance message detailing the commands to access the help menu is displayed. A user can enter 1 of these commands to view 3 different lists, each one with numbered items describing the functionalities associated with a category tracked by BYTE-CEPS. They can then see a given functionality's specific command formatting by entering the previous command along with the number from its help menu list position as a parameter. + + Credits: I wrote the initial HelpMenuManager class & added Javadocs, and **_joshualeejunyi_** & **_pqienso_** refined my code. I then added functionality to view the access guidance message whenever the user enters `help` alone. ++ **Editing an exercise**: Allows user to change name of an already created exercise. + +### Enhancements to Existing Features ++ **`validators` package & its accompanying classes**: Ensures our classes within the processing package follow the single responsibility principle (SRP). + + What it does: Each `Validator`-type class parses the inputs provided for the methods in its corresponding `Manager` class, checking input validity according to conditional statements and throwing exceptions if invalid. + + Justification: This abstracts the input parsing & validation process out of the `Manager`-type classes, thus ensuring they have a single responsibility, which is user command execution. + + Credits: I wrote the initial validators package. Each class method within a `Validator`-type class was public and corresponded to a `Manager` method by which it was called to parse & validate input, before the `Manager` method executed its task. + **_pqienso_** significantly improved said package by changing each `Validator`-type class to have only one public method, which is called from the `Validator`-type class `execute()` method. This public `Validator` method calls another private method that validates input for the specific private `Manager` method that `execute()` has called to execute the user's command. ++ **Testing `Storage` methods**: 75% of methods within `Storage` are covered. + + Credits: I wrote all test methods within the `StorageTest` class and the helper methods for file handling. **_joshualeejunyi_** wrote `setup()`, `setUpStreams()`and `restoreStreams()`, which are integral to the functioning of the test methods. ++ **Testing `ExerciseManager` method for editing an exercise**: + + Credits: I wrote all test methods for editing an exercise. However, other tests written by my teammates acted as helpful guidance. ++ **Testing `HelpMenuManager` and `HelpValidator` methods**: 87% of methods within `HelpMenuManager` and 100% within `HelpValidator` are covered. + + Credits: I wrote all test methods. I reused and adapted **_joshualeejunyi_**'s `setup()`, `setUpStreams()`and `restoreStreams()` methods. ++ **Bug fixes**: + + [Issue #114](https://github.com/AY2324S2-CS2113-F14-3/tp/issues/114): Implemented a [fix](https://github.com/AY2324S2-CS2113-F14-3/tp/pull/139) for a logging bug where a user was unable to log two of the same exercise on the same day; I did this by overriding the existing `equals()` method used by `ExerciseLog`, which was the bug's cause. + + [Issue #69](https://github.com/AY2324S2-CS2113-F14-3/tp/issues/69): Use of the `HashSet` class was causing incorrect ordering in exercise & workout lists, so I implemented a [fix](https://github.com/AY2324S2-CS2113-F14-3/tp/pull/92) by using the `LinkedHashSet` class instead. + + [Issue #47](https://github.com/AY2324S2-CS2113-F14-3/tp/issues/47): Implemented [fixes](https://github.com/AY2324S2-CS2113-F14-3/tp/pull/49) for unhandled exceptions for `week` (now called `program`). + +### Documentation ++ **UG**: Added all documentation for the help menu and related commands. ++ **DG**: + + Entire help menu section, including descriptions and sequence diagrams + + Added to **_v4vern_**'s manual testing section regarding the help menu. + +### Community (Contributions to the Team and Beyond): ++ Review my teammates' PRs ++ Reported an above-average number of bugs during the PE dry run, 8 bugs, for the team working on MediTracker. See [proof here](https://github.com/LWachtel1/ped/tree/main/files). + +
+ +## Contributions to DG: Extracts +`help` command sequence diagram:\ +![](../diagrams/helpGuidanceMessage.svg) + +`help /FLAG` command sequence diagram:\ +![](../diagrams/helpMenuWholeMenu.svg) + +`help /FLAG PARAMETER` command sequence diagram:\ +![](../diagrams/helpMenuCommandFormat.svg) diff --git a/docs/team/pqienso.md b/docs/team/pqienso.md new file mode 100644 index 0000000000..338d0e8268 --- /dev/null +++ b/docs/team/pqienso.md @@ -0,0 +1,109 @@ +# Lim Yu An - Project Portfolio Page + +## Project: ByteCeps +BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. +The user interacts with the tool using commands entered via the CLI interface. With BYTE-CEPS, they can compile a list of exercises, build custom workouts, assign workouts to a weekly schedule and log details of each exercise completed in each performed workout. + +### Summary of Contributions +All code contributed can be seen on the tP dashboard + [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=totalCommits%20dsc&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=pqienso&tabRepo=AY2324S2-CS2113-F14-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false). + +### Features implemented + +#### Parser and User Interface + - Created initial iteration of `Parser` and `UserInterface` classes + - Created the `Parser` class that takes in user input and outputs the command actions + arguments in the form of a `HashSet`. + - Created the `UserInterface` class that is responsible for taking in user input and + printing to `System.out`. + +#### New feature: Weekly Program Management + - Added the `program` functionality. + - Created the class `WeeklyProgramManager` that handles all commands starting with + `program`, allowing users to create and customise their weekly workout plan. + - Commands implemented: + - List: `program /list` + - Assign workout: `program /assign /to ` + - Clear program: `program /clear ` + - View today's program: `program /today` + +#### New feature: saving to storage + - Added the functionality of saving user data to a `.json` file + - Created the initial iteration of the `Storage` class which loads and saves user data + to and from a `.json` file using [this JSON-java library](https://github.com/stleary/JSON-java). + +#### New feature: cascading deletion management + - Added the ability for `ByteCeps` to handle cascading deletions + - Created the `CascadingDeletionProcessor` class that checks for cascading deletions + (eg. when an exercise assigned to an existing workout is deleted from `ByteCeps` by the user) + , allowing for the .json files to load properly after such deletions. + - This utility class is called only after successful execution of a command in `ByteCeps.runCommandLine()`. + - This is because `CascadingDeletionProcessor` involves multiple `ActivityManager` classes. Hence, its public + method is only called in `ByteCeps`, which already handles these classes, to minimise coupling. + +#### Refactoring: Refactored `ActivityManager` classes to reduce coupling +- Previously, `ActivityManager` classes had scattered calls to `UserInterface.printMessage()` in all of their methods, causing high coupling and low maintainability of code. +- Thus, the `ActivityManager`'s `execute()` classes were refactored such that they returned the `messageToUser` to `ByteCeps`, + and only then is `UserInterface.printMessage()` is called. +- This greatly reduced coupling, as `UserInterface` was now only associated with `ByteCeps` and `Storage` classes, + as compared to previously where there were associations with all `ActivityManager` classes. + +#### Refactoring: Abstraction of user input validation + - Refactored all `Validator` public methods, standardizing them to return `void` and only throw `Exceptions.InvalidInput` when validation fails. + - This is to better adhere to SRP, since the `Validator` methods were previously returning processed inputs if the inputs were valid. + This led to an overlap in responsibilities between the `ActivityManager` and `Validator` classes. + +#### Contributions to team-based tasks + - Managed releases [`v1.0`](https://github.com/AY2324S2-CS2113-F14-3/tp/releases/tag/v1.0), + [`v2.0`](https://github.com/AY2324S2-CS2113-F14-3/tp/releases/tag/v2.0) + - Reviewed PRs ([example](https://github.com/AY2324S2-CS2113-F14-3/tp/pull/153)) + - [Raised issues](https://github.com/AY2324S2-CS2113-F14-3/tp/issues?q=is%3Aissue+is%3Aclosed+author%3Apqienso) + - [Fixed bugs](https://github.com/AY2324S2-CS2113-F14-3/tp/issues?q=is%3Aissue+is%3Aclosed+assignee%3Apqienso+label%3Abug) + +#### User Guide contributions + - Added precautions of cascading deletions to user guide + - Contributed to summary table of all commands + +#### Developer Guide contributions + - Created class diagrams of all `Activity` classes and `ActivityManager` classes, along with their overview and explanation. + - Documented all implementation details for `WeeklyProgramManager`. + - Documented all implementation details for the `Storage` class. + - Documented all implementation details for the `CascadingDeletionProcessor` class. + +
+ +#### Contributions to DG: extracts +`Activity` class diagram:\ +drawing\ +`ActivityManager` class diagram:\ +drawing + +
+ +`program /assign ...` sequence diagram: \ +![](../diagrams/assignWorkoutToProgram.svg)\ +`program /log ...` sequence diagram:\ +![](../diagrams/addExerciseLog.svg)\ + +
+ +`program /clear ...` sequence diagram:\ +drawing\ +`program /today` sequence diagram:\ +drawing + +
+ +`storage.save()` sequence diagram:\ +![](../diagrams/saveStorage.svg) +`storage.load()` sequence diagram:\ +![](../diagrams/loadStorage.svg) + +
+ +`storage.loadWorkouts()` sequence diagram:\ +![](../diagrams/loadWorkouts.svg) +`CascadingDeletionProcessor.removeDeletedExerciseFromWorkouts` sequence diagram:\ +![](../diagrams/deleteExerciseFromWorkouts.svg) +`CascadingDeletionProcessor.removeDeletedWorkoutsFromProgram` sequence diagram:\ +![](../diagrams/deleteWorkoutFromProgram.svg) \ No newline at end of file diff --git a/docs/team/v4vern.md b/docs/team/v4vern.md new file mode 100644 index 0000000000..0bec8c3b8d --- /dev/null +++ b/docs/team/v4vern.md @@ -0,0 +1,78 @@ +# Tay Jun Feng Vernon - Project Portfolio Page + +## Project: ByteCeps +BYTE-CEPS is a CLI-based tool for setting and tracking fitness goals. +The user interacts with the tool using commands entered via the CLI interface. With BYTE-CEPS, they can compile a list of exercises, build custom workouts, assign workouts to a weekly schedule and log details of each exercise completed in each performed workout. + +## Summary of Contributions +### Code contributed +All code contributed can be seen on the tP dashboard +[here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=zoom&zA=V4Vern&zR=AY2324S2-CS2113-F14-3%2Ftp%5Bmaster%5D&zACS=132.79018044481745&zS=2024-02-23&zFS=&zU=2024-04-12&zMG=false&zFTF=commit&zFGS=groupByRepos&zFR=false)! + + +### Features implemented +#### New feature: Exercise Management System +- What it does: Implements the functionality for users to manage their exercises within BYTE-CEPS, including adding, deleting, listing, and searching for exercises. +- Justification: Enhances user experience by providing robust management tools, allowing users to tailor exercise lists to their needs, which is crucial for maintaining an organized fitness regimen. +- Commands Implemented + - Add: `exercise /add ` + - Delete: `exercise /delete ` + - List: `exercise /list` + - Search: `exercise /search ` +- Highlights: Created comprehensive command interfaces for exercise management (Exercise & ExerciseManager.java). Worked on debugging and refining the editing functionality to ensure reliable user interactions. +- Credits: Utilized Java standard libraries extensively; no third-party libraries were heavily used. + +#### New feature: Workout Management System +- What it does: Allows users to create, manage, and customize workout plans comprising various exercises, enhancing the planning of fitness activities within BYTE-CEPS. +- Justification: Provides essential functionality for users aiming to structure their fitness programs. It directly impacts user satisfaction by enabling more organized and accessible workout planning. +- Commands Implemented + - Create: `workout /create ` + - Delete: `workout /delete ` + - Edit: `workout /edit /to ` + - Search: `workout /search ` + - List: `workout /list` + - Assign Exercise: `workout /assign /to ` + - Unassign Exercise: `workout /unassign /from ` + - Info: `workout /info ` +- Highlights: Implemented a system to create, edit, delete, and list workout plans, and manage their association with specific exercises (workout & WorkoutManager.java). Ensured flexibility and user-friendliness in workout plan management, such as case insensitivity and special character handling in plan names. Addressed challenges in linking exercises to workout plans, enabling dynamic updates and modifications, which involved intricate logic to maintain data integrity and user workflow. +- Credits: Leveraged Java collections framework for managing relationships between data entities; no third-party libraries were heavily used. + +### Enhancements made +### Enhancement: Logging Workouts with Multiple Sets and Weights +Functionality: Expanded the workout logging capabilities to support multiple sets with varying weights and repetitions for each exercise. +- Commands Updated: + - Newly introduced functionality that allows logging multiple sets with different weights and repetitions. + - Command: `program /log /weight /sets /reps ` + - Example: `program /log benchpress /weight 100 110 120 /sets 3 /reps 5 4 3` + - Outcome: Logs benchpress with three sets at varying weights and reps, providing a detailed record of the workout performance on a given day. + +### Enhancement: Wrote Test cases for the following files +- Wrote majority of the test cases in `ExerciseTest`, `WorkoutTest`, `ActivityTest`, `WorkoutManagerTest`, `ExerciseManagerTest`, `CascadingDeletion` & `UserInterface`. +- Added some test cases in `WeeklyProgramManagerTest`, `ExerciseLog` , `WorkoutLog`, `exercisevalidator`, `workoutvalidator` & `parser`. + +#### User Guide contributions + - Authored the Exercise Management section of the UG. + - Authored the Workout Management section of the UG. + - Authored the Exit/Saving and Edit data section of the UG. + - Created a summary table of all commands. + +#### Developer Guide contributions + - Added implementation details of all features of `Exercise Management`. + - Added implementation details of all features of `Workout Management`. + - Created sequence diagram of `addExercise` under `Exercise Management`. + - Created sequence diagrams of `deleteWorkout`,`assignExerciseToWorkout`, `listExercisesinWorkoutPlan` under `Workout Management`. + - Added Product Scope Information (Target User Profile, Value Proposition, User Stories & Non-Functional Requirements) & Glossary + - Added Instructions for manual testing. + +### Contributions to team-based tasks +- Setup issue tracker for V1.0 & V2.0. +- Updating user/developer docs that are not specific to a feature e.g. documenting the target user profile. +- Review PRs. +- Increased code test coverage from 66% to 82%. +- [Fixed bugs](https://github.com/AY2324S2-CS2113-F14-3/tp/issues?q=is%3Aissue+is%3Aclosed+label%3Abug+assignee%3AV4Vern) + +### Contributions beyond the project team +* PE dry Run: + * [Screenshot captured during PE dry run](https://github.com/V4vern/ped/tree/main/files) +* DG review for other teams: + * [BookMarked](https://github.com/nus-cs2113-AY2324S2/tp/pull/19) diff --git a/jsons/corrupted.json b/jsons/corrupted.json new file mode 100644 index 0000000000..3b0c1a4a24 --- /dev/null +++ b/jsons/corrupted.json @@ -0,0 +1 @@ +anager":[],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pushups"},{"activityName":"pullups"}],"WorkoutLogManager":[]} \ No newline at end of file diff --git a/jsons/duplicateExercise.json b/jsons/duplicateExercise.json new file mode 100644 index 0000000000..5bf9ac56a1 --- /dev/null +++ b/jsons/duplicateExercise.json @@ -0,0 +1 @@ +{"workoutManager":[],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pushups"},{"activityName":"pushups"}],"WorkoutLogManager":[]} \ No newline at end of file diff --git a/jsons/duplicateWorkout.json b/jsons/duplicateWorkout.json new file mode 100644 index 0000000000..703c094ac0 --- /dev/null +++ b/jsons/duplicateWorkout.json @@ -0,0 +1 @@ +{"workoutManager":[{"exerciseList":[{"activityName":"pushups"},{"activityName":"pullups"}],"activityName":"bodyweight","exerciseSet":[{"activityName":"pushups"},{"activityName":"pullups"}]},{"exerciseList":[{"activityName":"lat pulldown"},{"activityName":"leg press"}],"activityName":"bodyweight","exerciseSet":[{"activityName":"lat pulldown"},{"activityName":"leg press"}]}],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pushups"},{"activityName":"pullups"},{"activityName":"lat pulldown"},{"activityName":"leg press"}],"WorkoutLogManager":[]} \ No newline at end of file diff --git a/jsons/logsExerciseFail.json b/jsons/logsExerciseFail.json new file mode 100644 index 0000000000..cc26202bfb --- /dev/null +++ b/jsons/logsExerciseFail.json @@ -0,0 +1 @@ +{"workoutManager":[{"exerciseList":[{"activityName":"pushups"},{"activityName":"pullups"}],"activityName":"bodyweight","exerciseSet":[{"activityName":"pushups"},{"activityName":"pullups"}]}],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"bodyweight","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pressups"},{"activityName":"chinups"}],"WorkoutLogManager":[{"exercises":[{"exerciseName":"pullups","reps":8,"sets":3,"weight":58}],"workoutDate":"2024-04-04","workoutName":"bodyweight"}]} \ No newline at end of file diff --git a/jsons/logsWorkoutFail.json b/jsons/logsWorkoutFail.json new file mode 100644 index 0000000000..0c5848f434 --- /dev/null +++ b/jsons/logsWorkoutFail.json @@ -0,0 +1 @@ +{"workoutManager":[{"exerciseList":[{"activityName":"pushups"},{"activityName":"pullups"}],"activityName":"machines","exerciseSet":[{"activityName":"pushups"},{"activityName":"pullups"}]}],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"bodyweight","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pushups"},{"activityName":"pullups"}],"WorkoutLogManager":[{"exercises":[{"exerciseName":"pullups","reps":8,"sets":3,"weight":58}],"workoutDate":"2024-04-04","workoutName":"bodyweight"}]} \ No newline at end of file diff --git a/jsons/test.json b/jsons/test.json new file mode 100644 index 0000000000..6b8e6f9073 --- /dev/null +++ b/jsons/test.json @@ -0,0 +1 @@ +{"workoutManager":[],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[],"WorkoutLogManager":[]} \ No newline at end of file diff --git a/jsons/workoutExercisesMissing.json b/jsons/workoutExercisesMissing.json new file mode 100644 index 0000000000..da0af74a8c --- /dev/null +++ b/jsons/workoutExercisesMissing.json @@ -0,0 +1 @@ +{"workoutManager":[{"exerciseList":[{"activityName":"pushups"},{"activityName":"pullups"}],"activityName":"bodyweight","exerciseSet":[{"activityName":"pushups"},{"activityName":"pullups"}]},{"exerciseList":[{"activityName":"lat pulldown"},{"activityName":"leg press"}],"activityName":"machine","exerciseSet":[{"activityName":"lat pulldown"},{"activityName":"leg press"}]}],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"","THURSDAY":"","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pressups"},{"activityName":"chinups"},{"activityName":"lat machine"},{"activityName":"leg machine"}],"WorkoutLogManager":[]} \ No newline at end of file diff --git a/jsons/workoutMissing.json b/jsons/workoutMissing.json new file mode 100644 index 0000000000..744435e43e --- /dev/null +++ b/jsons/workoutMissing.json @@ -0,0 +1 @@ +{"workoutManager":[],"weeklyProgram":{"WEDNESDAY":"","MONDAY":"bodyweight","THURSDAY":"","SUNDAY":"","TUESDAY":"","FRIDAY":"","SATURDAY":""},"exerciseManager":[{"activityName":"pullups"},{"activityName":"pushups"}],"WorkoutLogManager":[]} \ No newline at end of file diff --git a/src/main/java/byteceps/ByteCeps.java b/src/main/java/byteceps/ByteCeps.java new file mode 100644 index 0000000000..f7342d6638 --- /dev/null +++ b/src/main/java/byteceps/ByteCeps.java @@ -0,0 +1,93 @@ +package byteceps; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.processing.ExerciseManager; +import byteceps.processing.WorkoutManager; +import byteceps.processing.WeeklyProgramManager; +import byteceps.processing.WorkoutLogsManager; +import byteceps.processing.HelpMenuManager; +import byteceps.processing.CascadingDeletionProcessor; +import byteceps.storage.Storage; +import byteceps.ui.strings.UiStrings; +import byteceps.ui.UserInterface; +import byteceps.ui.strings.CommandStrings; + +import java.io.IOException; + +public class ByteCeps { + private static ExerciseManager exerciseManager = null; + private static WorkoutManager workoutManager = null; + private static WeeklyProgramManager weeklyProgramManager = null; + private static WorkoutLogsManager workoutLogsManager = null; + private static HelpMenuManager helpMenuManager = null; + private static Parser parser; + private static Storage storage; + + private static final String FILE_PATH = "data.json"; + private final UserInterface ui = UserInterface.getInstance(); + + + public ByteCeps() { + exerciseManager = new ExerciseManager(); + workoutManager = new WorkoutManager(exerciseManager); + workoutLogsManager = new WorkoutLogsManager(); + weeklyProgramManager = new WeeklyProgramManager(exerciseManager, workoutManager, workoutLogsManager); + parser = new Parser(); + storage = new Storage(FILE_PATH, ui); + helpMenuManager = new HelpMenuManager(); + } + + public static void main(String[] args) { + new ByteCeps().run(); + } + + public void runCommandLine() { + while (true) { + try { + String userInput = ui.getUserInput(); + parser.parseInput(userInput); + + String messageToUser; + switch (parser.getCommand()) { + case CommandStrings.COMMAND_EXERCISE: + messageToUser = exerciseManager.execute(parser); + break; + case CommandStrings.COMMAND_WORKOUT: + messageToUser = workoutManager.execute(parser); + break; + case CommandStrings.COMMAND_PROGRAM: + messageToUser = weeklyProgramManager.execute(parser); + break; + case CommandStrings.COMMAND_HELP: + messageToUser = helpMenuManager.execute(parser); + break; + case CommandStrings.COMMAND_BYE: + case CommandStrings.COMMAND_EXIT: + return; + default: + messageToUser = CommandStrings.UNKNOWN_COMMAND; + } + ui.printMessage(messageToUser); + CascadingDeletionProcessor.checkForCascadingDeletions(parser, workoutManager, weeklyProgramManager); + } catch (Exceptions.ActivityExistsException | Exceptions.ErrorAddingActivity | + Exceptions.InvalidInput | Exceptions.ActivityDoesNotExist | IllegalStateException e) { + ui.printMessage(String.format(UiStrings.ERROR_STRING, e.getMessage())); + } + } + } + + public void run() { + ui.printWelcomeMessage(); + try { + storage.load(exerciseManager, workoutManager, weeklyProgramManager, workoutLogsManager); + ui.printMessage(helpMenuManager.getHelpGuidanceString()); + runCommandLine(); + storage.save(exerciseManager, workoutManager, weeklyProgramManager, workoutLogsManager); + } catch (IOException e) { + ui.printMessage(String.format(UiStrings.ERROR_STRING, e.getMessage())); + } + ui.printGoodbyeMessage(); + } + +} diff --git a/src/main/java/byteceps/activities/Activity.java b/src/main/java/byteceps/activities/Activity.java new file mode 100644 index 0000000000..6fb59f9bd7 --- /dev/null +++ b/src/main/java/byteceps/activities/Activity.java @@ -0,0 +1,67 @@ +package byteceps.activities; + +/** + * Represents an activity in the byteceps application. + * Each Activity object has a name. + */ +public class Activity { + protected String activityName; + + /** + * Constructs a new Activity object with the specified activity name. + * + * @param activityName The name of the activity. + */ + public Activity(String activityName) { + this.activityName = activityName; + } + + /** + * Returns the name of the activity. + * + * @return The name of the activity. + */ + public String getActivityName() { + return activityName; + } + + public void setActivityName(String activityName) { + this.activityName = activityName; + } + + + /** + * Computes a hash code for the Activity object based on its name. + * + * @return The hash code value for this Activity. + */ + @Override + public int hashCode() { + return activityName.hashCode(); + } + + /** + * Indicates whether some other object is "equal to" this one. + * Two Activity objects are considered equal if they have the same name. + * + * @param obj The reference object with which to compare. + * @return true if this Activity is the same as the obj argument; false otherwise. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + Activity other = (Activity) obj; + return (activityName.equalsIgnoreCase(other.getActivityName())); + } +} diff --git a/src/main/java/byteceps/activities/Day.java b/src/main/java/byteceps/activities/Day.java new file mode 100644 index 0000000000..03eb70fab0 --- /dev/null +++ b/src/main/java/byteceps/activities/Day.java @@ -0,0 +1,16 @@ +package byteceps.activities; + +public class Day extends Activity { + protected Workout assignedWorkout; + public Day(String activityName) { + super(activityName); + } + + public void setAssignedWorkout(Workout assignedWorkout) { + this.assignedWorkout = assignedWorkout; + } + + public Workout getAssignedWorkout() { + return this.assignedWorkout; + } +} diff --git a/src/main/java/byteceps/activities/Exercise.java b/src/main/java/byteceps/activities/Exercise.java new file mode 100644 index 0000000000..c34a5d594b --- /dev/null +++ b/src/main/java/byteceps/activities/Exercise.java @@ -0,0 +1,44 @@ +package byteceps.activities; + +import byteceps.processing.ActivityManager; + +/** + * Represents an exercise activity in the byteceps application. + * Each Exercise object corresponds to a specific exercise with a name. + */ +public class Exercise extends Activity { + /** + * Constructs a new Exercise object with the specified exercise name. + * + * @param exerciseName The name of the exercise. + */ + public Exercise(String exerciseName) { + super(exerciseName); + } + + /** + * Returns a string representation of the Exercise object. + * + * @return The name of the exercise. + */ + public String toString() { + return super.getActivityName(); + } + + /** + * Modifies the name of the exercise. If the new name is not null, it updates the activity name + * and updates the activity set in the provided activity manager with the new exercise name. + * If the new name is null, it sets the activity name to null. + * + * @param activityManager The activity manager responsible for managing activities and updating activity sets. + * @param newExerciseName The new name for the exercise. + */ + public void editExerciseName(String newExerciseName, ActivityManager activityManager) { + if (newExerciseName != null) { + activityManager.updateActivitySet(this, new Exercise(newExerciseName)); + this.activityName = newExerciseName; + } else { + activityName = null; + } + } +} diff --git a/src/main/java/byteceps/activities/ExerciseLog.java b/src/main/java/byteceps/activities/ExerciseLog.java new file mode 100644 index 0000000000..2782d7d41b --- /dev/null +++ b/src/main/java/byteceps/activities/ExerciseLog.java @@ -0,0 +1,45 @@ +package byteceps.activities; + +import java.util.List; +public class ExerciseLog extends Activity { + private final List weights; + private final int sets; + private final List repetitions; + + public ExerciseLog(String activityName, List weights, int sets, List repetitions) { + super(activityName); + this.weights = weights; + this.sets = sets; + this.repetitions = repetitions; + } + + + public int getSets() { + return sets; + } + + public List getRepetitions() { + return repetitions; + } + + public List getWeights() { + return weights; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + + return false; + } +} diff --git a/src/main/java/byteceps/activities/Workout.java b/src/main/java/byteceps/activities/Workout.java new file mode 100644 index 0000000000..d915b3fc0e --- /dev/null +++ b/src/main/java/byteceps/activities/Workout.java @@ -0,0 +1,93 @@ +//@@author V4vern +package byteceps.activities; + +import byteceps.processing.ActivityManager; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.ListIterator; + +/** + * Represents a workout plan in the byteceps application. + * It extends the Activity class and contains a list of exercises. + */ +public class Workout extends Activity { + /** + * The list of exercises in this workout plan. + */ + ArrayList exerciseList; + + /** + * Constructs a new Workout object with the specified workout name. + * + * @param workoutName The name of the workout plan. + */ + public Workout(String workoutName) { + super(workoutName); + exerciseList = new ArrayList<>(); + } + + /** + * Returns the list of exercises in this workout plan. + * + * @return The list of exercises. + */ + public ArrayList getExerciseList() { + return exerciseList; + } + + /** + * Returns a set of exercises in this workout plan. + * + * @return A set of exercises. + */ + public LinkedHashSet getExerciseSet() { + return new LinkedHashSet<>(exerciseList); + } + + /** + * Adds an exercise to this workout plan. + * + * @param exercise The exercise to add. + */ + public void addExercise(Exercise exercise) { + exerciseList.add(exercise); + } + + /** + * Returns a string representation of this workout plan. + * The representation includes the workout name and the list of exercises. + * + * @param numTabs The number of tabs for indentation. + * @return A string representation of this workout plan. + */ + public String toString(int numTabs) { + assert numTabs > 0 : "numTabs cannot be negative"; + + StringBuilder result = new StringBuilder(); + result.append("\t".repeat(numTabs)).append(activityName).append(System.lineSeparator()); + for (ListIterator it = exerciseList.listIterator(); it.hasNext(); ) { + Activity currentExercise = it.next(); + result.append("\t".repeat(numTabs + 1)); + result.append(String.format("%d. %s%s", + it.nextIndex(), currentExercise.toString(), System.lineSeparator())); + } + return result.toString(); + } + + /** + * Edits the name of the workout. + * + * @param newWorkoutName The new name for the workout. If null, the workout name will be set to null. + * @param activityManager The ActivityManager responsible for managing activities. + * @throws NullPointerException If the activityManager is null. + */ + public void editWorkoutName(String newWorkoutName, ActivityManager activityManager) { + if (newWorkoutName != null) { + activityManager.updateActivitySet(this, new Workout(newWorkoutName)); + this.activityName = newWorkoutName; + } else { + activityName = null; + } + } +} diff --git a/src/main/java/byteceps/activities/WorkoutLog.java b/src/main/java/byteceps/activities/WorkoutLog.java new file mode 100644 index 0000000000..e3b9613284 --- /dev/null +++ b/src/main/java/byteceps/activities/WorkoutLog.java @@ -0,0 +1,63 @@ +package byteceps.activities; + +import byteceps.errors.Exceptions; +import byteceps.ui.strings.ManagerStrings; + +import java.util.Iterator; +import java.util.LinkedHashSet; + + +public class WorkoutLog extends Workout { + protected final String workoutName; + LinkedHashSet exerciseLogs; + public WorkoutLog(String workoutDate, String workoutName) { + super(workoutDate); + this.workoutName = workoutName; + this.exerciseLogs = new LinkedHashSet<>(); + } + + public void addExerciseLog(ExerciseLog exerciseLog) { + exerciseLogs.remove(exerciseLog); + exerciseLogs.add(exerciseLog); + } + + public boolean hasExerciseName(String exerciseName) { + for (Iterator iterator = exerciseLogs.iterator(); iterator.hasNext(); ) { + ExerciseLog currentExerciseLog = iterator.next(); + if (currentExerciseLog.activityName.equalsIgnoreCase(exerciseName)) { + return true; + } + } + return false; + } + + public void removeExistingLogEntry(String exerciseName) throws Exceptions.ActivityDoesNotExist { + ExerciseLog exerciseLogToFind = null; + for (Iterator iterator = exerciseLogs.iterator(); iterator.hasNext(); ) { + ExerciseLog currentExerciseLog = iterator.next(); + if (currentExerciseLog.activityName.equalsIgnoreCase(exerciseName)) { + exerciseLogToFind = currentExerciseLog; + break; + } + } + + if (exerciseLogToFind == null) { + throw new Exceptions.ActivityDoesNotExist(ManagerStrings.LOG_ENTRY_EXERCISE_DOES_NOT_EXIST); + } + + exerciseLogs.remove(exerciseLogToFind); + } + + + public String getWorkoutName() { + return workoutName; + } + + public String getWorkoutDate() { + return activityName; + } + + public LinkedHashSet getExerciseLogs() { + return exerciseLogs; + } +} diff --git a/src/main/java/byteceps/commands/InputArguments.java b/src/main/java/byteceps/commands/InputArguments.java new file mode 100644 index 0000000000..8f0fb5c41a --- /dev/null +++ b/src/main/java/byteceps/commands/InputArguments.java @@ -0,0 +1,21 @@ +package byteceps.commands; + +public class InputArguments { + private final String flag; + private final String parameter; + + public InputArguments(String flag, String parameter) { + this.flag = flag.trim(); + this.parameter = parameter.trim(); + } + + + public String getFlag() { + return flag; + } + + public String getParameter() { + return parameter; + } + +} diff --git a/src/main/java/byteceps/commands/Parser.java b/src/main/java/byteceps/commands/Parser.java new file mode 100644 index 0000000000..838a07f845 --- /dev/null +++ b/src/main/java/byteceps/commands/Parser.java @@ -0,0 +1,113 @@ +package byteceps.commands; + +import byteceps.errors.Exceptions; +import byteceps.ui.strings.ManagerStrings; +import byteceps.ui.strings.UiStrings; + +import java.util.HashMap; + +public class Parser { + private String command; + private InputArguments commandAction; + private HashMap additionalArguments; + + public Parser() { + flush(); + } + + private void flush() { + command = ""; + commandAction = null; + additionalArguments = new HashMap<>(); + } + + //@@author pqienso + public void parseInput(String line) throws Exceptions.InvalidInput { + // flush the old input + flush(); + + //@@author joshualeejunyi + assert command.isEmpty() : "Command should be empty after flush"; + assert commandAction == null : "CommandAction should be null after flush"; + assert additionalArguments.isEmpty() : "AdditionalArguments should be empty after flush"; + + //@@author pqienso + int indexOfFirstSlash = line.indexOf('/'); + + // input does not have parameters + if (indexOfFirstSlash == -1) { + command = line.trim().toLowerCase(); + return; + } + + command = line.substring(0, indexOfFirstSlash).trim().toLowerCase(); + String[] argumentKeyValuePairs = line.substring(indexOfFirstSlash + 1).split("/"); + + long numberOfSlashes = line.chars().filter(ch -> ch == '/').count(); + + if (numberOfSlashes != argumentKeyValuePairs.length) { + throw new Exceptions.InvalidInput(UiStrings.MORE_SLASHES_THAN_ARGS); + } + + for (String keyValuePair : argumentKeyValuePairs) { + String[] currentKV = keyValuePair.split( " ", 2); + String flag = currentKV[0].trim(); + + String parameter; + if (currentKV.length > 1) { + parameter = currentKV[1].trim(); + } else { + parameter = ""; + } + + if (commandAction == null) { + commandAction = new InputArguments(flag, parameter); + } else { + additionalArguments.put(flag, parameter); + } + } + } + + public String getCommand() { + return command; + } + + public String getAction() throws Exceptions.InvalidInput { + if (commandAction == null) { + throw new Exceptions.InvalidInput(ManagerStrings.NO_ACTION_EXCEPTION); + } + return commandAction.getFlag(); + } + + public String getActionParameter() { + return commandAction.getParameter(); + } + + public int getAdditionalArgumentsLength() { + return additionalArguments.size(); + } + + public boolean hasAdditionalArguments() { + return !additionalArguments.isEmpty(); + } + + public String getAdditionalArguments(String key) { + String value = additionalArguments.get(key); + if (value == null) { + return ""; + } + + return value; + } + + public int getNumAdditionalArguments() { + return additionalArguments.size(); + } + + //@@author pqienso + @Override + public String toString() { + return "COMMAND: " + System.lineSeparator() + command + System.lineSeparator() + + "ARGUMENTS: " + System.lineSeparator() + additionalArguments.toString(); + } +} diff --git a/src/main/java/byteceps/errors/Exceptions.java b/src/main/java/byteceps/errors/Exceptions.java new file mode 100644 index 0000000000..0b323d42bc --- /dev/null +++ b/src/main/java/byteceps/errors/Exceptions.java @@ -0,0 +1,27 @@ +package byteceps.errors; + +public class Exceptions { + public static class ActivityExistsException extends Exception { + public ActivityExistsException(String errorMessage) { + super(errorMessage); + } + } + + public static class ActivityDoesNotExist extends Exception { + public ActivityDoesNotExist(String errorMessage) { + super(errorMessage); + } + } + + public static class ErrorAddingActivity extends Exception { + public ErrorAddingActivity(String errorMessage) { + super(errorMessage); + } + } + + public static class InvalidInput extends Exception { + public InvalidInput(String errorMessage) { + super(errorMessage); + } + } +} diff --git a/src/main/java/byteceps/processing/ActivityManager.java b/src/main/java/byteceps/processing/ActivityManager.java new file mode 100644 index 0000000000..afaea795da --- /dev/null +++ b/src/main/java/byteceps/processing/ActivityManager.java @@ -0,0 +1,237 @@ +package byteceps.processing; + +import byteceps.activities.Activity; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.ManagerStrings; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.Iterator; + +/** + * Abstract base class for managing activities. + */ +public abstract class ActivityManager { + protected final String activityType; + protected final LinkedHashSet activitySet; + + public ActivityManager() { + this.activityType = getActivityType(false); + this.activitySet = new LinkedHashSet<>(); + } + + /** + * Executes the specified command. + * + * @param parser Parser containing user input. + * @return Message to user after executing the command. + * @throws Exceptions.InvalidInput if no command action specified. + * @throws Exceptions.ErrorAddingActivity If there's an error adding the activity. + * @throws Exceptions.ActivityExistsException If the activity already exists. + * @throws Exceptions.ActivityDoesNotExist If the activity does not exist. + */ + public abstract String execute(Parser parser) throws Exceptions.InvalidInput, + Exceptions.ErrorAddingActivity, Exceptions.ActivityExistsException, + Exceptions.ActivityDoesNotExist; + + /** + * Adds an activity to the manager. + * + * @param activity The activity to add. + * @throws Exceptions.ActivityExistsException If the activity already exists. + */ + public void add(Activity activity) throws Exceptions.ActivityExistsException { + boolean setReturn = activitySet.add(activity); + + if (!setReturn) { + String activityName = activity.getActivityName(); + throw new Exceptions.ActivityExistsException( + String.format(ManagerStrings.ACTIVITY_EXISTS_EXCEPTION, this.activityType, activityName) + ); + } + } + + /** + * Deletes an activity from the manager. + * + * @param activity The activity to delete. + * @throws Exceptions.ActivityDoesNotExist If the activity does not exist. + */ + public void delete(Activity activity) throws Exceptions.ActivityDoesNotExist { + boolean setReturn = activitySet.remove(activity); + + if (!setReturn) { + String activityName = activity.getActivityName(); + throw new Exceptions.ActivityDoesNotExist( + String.format(ManagerStrings.ACTIVITY_DELETE_EXCEPTION, + this.activityType, activityName) + ); + } + } + + /** + * Retrieves an activity from the manager by its name. + * + * @param activityName The name of the activity to retrieve. + * @return The retrieved activity. + * @throws Exceptions.ActivityDoesNotExist if the activity does not exist. + */ + public Activity retrieve(String activityName) throws Exceptions.ActivityDoesNotExist { + if (activitySet.isEmpty()) { + throw new Exceptions.ActivityDoesNotExist( + String.format(ManagerStrings.ACTIVITY_EMPTY_LIST_EXCEPTION, + this.activityType) + ); + } + + for (Activity currentActivity : activitySet) { + if (currentActivity.getActivityName().equals(activityName)) { + return currentActivity; + } + } + + // throw error as activity not found in the set + throw new Exceptions.ActivityDoesNotExist( + String.format(ManagerStrings.ACTIVITY_DOES_NOT_EXIST_EXCEPTION, + this.activityType, activityName) + ); + } + + /** + * Gets a string representation of the list of activities. + * + * @return A string representation of the list of activities. + */ + public String getListString() { + if (activitySet.isEmpty()) { + return String.format(ManagerStrings.ACTIVITY_EMPTY_LIST, getActivityType(true)); + } + StringBuilder result = new StringBuilder(); + result.append(String.format(ManagerStrings.ACTIVITY_LIST, getActivityType(true), System.lineSeparator())); + + int index = 1; + for (Iterator it = activitySet.iterator(); it.hasNext(); index++) { + Activity currentActivity = it.next(); + result.append(String.format(ManagerStrings.ACTIVITY_LIST_ITEM, index, currentActivity.getActivityName())); + } + + return result.toString(); + } + + /** + * Checks if an activity with a given name exists in the manager. + * + * @param activityName The name of the activity to check. + * @return true if the activity does not exist, false otherwise. + */ + public boolean doesNotHaveActivity(String activityName) { + if (activitySet.isEmpty()) { + return true; + } + + for (Activity currentActivity : activitySet) { + if (currentActivity.getActivityName().equals(activityName)) { + return false; + } + } + + return true; + } + + /** + * Gets the list of activities. + * + * @return The list of activities. + */ + public ArrayList getActivityList() { + return new ArrayList<>(activitySet); + } + + /** + * Gets the type of activity managed by this manager. + * + * @param plural true if the plural form of the activity type is requested, false otherwise. + * @return The type of activity. + */ + public abstract String getActivityType(boolean plural); + + + /** + * Gets a string representation of the search results. + * + * @param searchTerm The search term. + * @return A string representation of the search results. + */ + public String getSearchResultsString(String searchTerm){ + ArrayList searchResults = searchActivities(searchTerm); + return stringify(searchResults); + } + + /** + * Checks if an activity matches the search term. + * + * @param activity The activity to check. + * @param searchTerm The search term. + * @return true if the activity matches the search term, false otherwise. + */ + private boolean activityMatchesSearchTerm(Activity activity, String searchTerm) { + String activityName = activity.getActivityName().toLowerCase(); + String searchTermLowerCase = searchTerm.toLowerCase(); + return activityName.contains(searchTermLowerCase); + } + + /** + * Searches for activities that match the search term. + * + * @param searchTerm The search term. + * @return A list of activities that match the search term. + */ + private ArrayList searchActivities(String searchTerm){ + ArrayList searchResults = new ArrayList<>(); + for (Activity activity : activitySet){ + if (activityMatchesSearchTerm(activity, searchTerm)) { + searchResults.add(activity); + } + } + return searchResults; + } + + /** + * Converts a list of activities to a string representation. + * + * @param searchResults The list of activities. + * @return A string representation of the list of activities. + */ + private String stringify(ArrayList searchResults){ + if (searchResults.isEmpty()) { + return ManagerStrings.NO_RESULTS; + } + StringBuilder result = new StringBuilder(); + result.append(String.format(ManagerStrings.SEARCH_RESULTS, System.lineSeparator())); + + int index = 1; + for (Activity currentActivity : searchResults) { + result.append(String.format(ManagerStrings.ACTIVITY_LIST_ITEM, index, currentActivity.getActivityName())); + index++; + } + + return result.toString(); + } + + /** + * Updates the activity set by removing the specified activity to remove and adding the specified + * activity to add. + * + * @param activityToRemove The activity to remove from the activity set. + * @param activityToAdd The activity to add to the activity set. + */ + public void updateActivitySet(Activity activityToRemove, Activity activityToAdd) { + activitySet.remove(activityToRemove); + activitySet.add(activityToAdd); + } + + public void reset() { + activitySet.clear(); + } +} diff --git a/src/main/java/byteceps/processing/CascadingDeletionProcessor.java b/src/main/java/byteceps/processing/CascadingDeletionProcessor.java new file mode 100644 index 0000000000..e20d7cc2eb --- /dev/null +++ b/src/main/java/byteceps/processing/CascadingDeletionProcessor.java @@ -0,0 +1,74 @@ +package byteceps.processing; + +import byteceps.activities.Activity; +import byteceps.activities.Day; +import byteceps.activities.Exercise; +import byteceps.activities.Workout; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; + +import java.util.ArrayList; +import java.util.LinkedHashSet; + +public class CascadingDeletionProcessor { + /** + * Checks if any deletion of an exercise/workout causes cascading effects on the container classes. + * If yes, removes the exercise/workout from the workout/program respectively, silently. + * + * @param parser User input in the form of a Parser object. + * @param workoutManager The activity manager containing all workouts + * @param weeklyProgramManager The activity manager containing the training program + */ + public static void checkForCascadingDeletions (Parser parser, WorkoutManager workoutManager, + WeeklyProgramManager weeklyProgramManager) { + try { + String parserAction = parser.getAction(); + if (!parserAction.equals(CommandStrings.ACTION_DELETE)) { + return; + } + + String parserCommand = parser.getCommand(); + if(parserCommand.equals(CommandStrings.COMMAND_EXERCISE)) { + removeDeletedExerciseFromWorkouts(parser.getActionParameter(), workoutManager); + } else if(parserCommand.equals(CommandStrings.COMMAND_WORKOUT)) { + removeDeletedWorkoutsFromProgram(parser.getActionParameter(), weeklyProgramManager); + } + + } catch (Exceptions.InvalidInput | Exceptions.ActivityDoesNotExist e) { + return; + } + + } + + private static void removeDeletedExerciseFromWorkouts (String exerciseName, WorkoutManager workoutManager) { + ArrayList workoutList = workoutManager.getActivityList(); + for (Activity item : workoutList) { + Workout workout = (Workout) item; + ArrayList workoutExerciseList = ((Workout) workout).getExerciseList(); + workoutExerciseList.removeIf(exercise -> exercise.getActivityName().equalsIgnoreCase(exerciseName)); + } + } + + private static void removeDeletedWorkoutsFromProgram (String workoutName, + WeeklyProgramManager weeklyProgramManager) + throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + LinkedHashSet newWorkoutsInProgram = weeklyProgramManager.getDaySet(); + LinkedHashSet oldWorkoutsInProgram = new LinkedHashSet<>(newWorkoutsInProgram); + + for (Activity item : oldWorkoutsInProgram) { + Day currentDay = (Day) item; + Workout workout = currentDay.getAssignedWorkout(); + if (workout == null) { + continue; + } else if (workout.getActivityName().equals(workoutName)) { + String currentDayString = currentDay.getActivityName(); + + newWorkoutsInProgram.remove(weeklyProgramManager.getDay(currentDayString)); + Day newDay = new Day(currentDayString); + newDay.setAssignedWorkout(null); + newWorkoutsInProgram.add(newDay); + } + } + } +} diff --git a/src/main/java/byteceps/processing/ExerciseManager.java b/src/main/java/byteceps/processing/ExerciseManager.java new file mode 100644 index 0000000000..e2eb5e6736 --- /dev/null +++ b/src/main/java/byteceps/processing/ExerciseManager.java @@ -0,0 +1,111 @@ +package byteceps.processing; + +import byteceps.activities.Exercise; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.ManagerStrings; +import byteceps.validators.ExerciseValidator; + +/** + * Manages operations related to exercises, such as adding, deleting, editing, listing, and searching exercises. + */ +public class ExerciseManager extends ActivityManager { + //@@author V4vern + /** + * Executes all commands that start with the keyword "exercise". + * + * @param parser Parser containing user input. + * @return Message to user after executing the command. + * @throws Exceptions.InvalidInput if no command action specified + * @throws Exceptions.ErrorAddingActivity If there is an error adding an activity. + * @throws Exceptions.ActivityDoesNotExist if user inputs name of an activity that does not exist. + * @throws Exceptions.ActivityExistsException if user attempts to create an existing exercise. + */ + @Override + public String execute(Parser parser) throws Exceptions.InvalidInput, + Exceptions.ErrorAddingActivity, Exceptions.ActivityExistsException, + Exceptions.ActivityDoesNotExist { + + String command = ExerciseValidator.validateCommand(parser); + + String messageToUser = ""; + switch (command) { + case CommandStrings.ACTION_ADD: + messageToUser = executeAddAction(parser); + break; + case CommandStrings.ACTION_DELETE: + messageToUser = executeDeleteAction(parser); + break; + //@@author LWachtel1 + case CommandStrings.ACTION_EDIT: + messageToUser = executeEditAction(parser); + break; + //@@author V4vern + case CommandStrings.ACTION_LIST: + messageToUser = executeListAction(parser); + break; + case CommandStrings.ACTION_SEARCH: + messageToUser = executeSearchAction(parser); + break; + default: + assert false : "user input should have been validated beforehand"; + } + + return messageToUser; + } + //@@author LWachtel1 + private String executeEditAction(Parser parser) throws Exceptions.ActivityDoesNotExist { + String oldExerciseName = parser.getActionParameter().toLowerCase(); + String newExerciseName = parser.getAdditionalArguments(CommandStrings.ARG_TO).toLowerCase(); + + if (oldExerciseName.equals(newExerciseName)) { + return String.format(ManagerStrings.EXERCISE_NAME_SAME, oldExerciseName); + } + + Exercise retrievedExercise = retrieveExercise(parser); + retrievedExercise.editExerciseName(newExerciseName.toLowerCase(), this); + newExerciseName = newExerciseName.toLowerCase(); + + return String.format( + ManagerStrings.EXERCISE_EDITED, parser.getActionParameter().toLowerCase(), newExerciseName + ); + } + //@@author V4vern + private String executeListAction(Parser parser) { + return getListString(); + } + //@@author V4vern + private String executeDeleteAction(Parser parser) throws Exceptions.ActivityDoesNotExist { + Exercise retrievedExercise = retrieveExercise(parser); + delete(retrievedExercise); + return String.format(ManagerStrings.EXERCISE_DELETED, retrievedExercise.getActivityName()); + } + + //@@author V4vern + private String executeAddAction(Parser parser) throws Exceptions.ActivityExistsException { + String exerciseName = parser.getActionParameter().toLowerCase(); + Exercise newExercise = new Exercise(exerciseName); + add(newExercise); + return String.format(ManagerStrings.EXERCISE_ADDED, newExercise.getActivityName()); + } + + + //@@author V4vern + private Exercise retrieveExercise(Parser parser) throws Exceptions.ActivityDoesNotExist { + String exerciseName = parser.getActionParameter().toLowerCase(); + return (Exercise) retrieve(exerciseName); + } + + //@@author joshualeejunyi + @Override + public String getActivityType(boolean plural) { + return plural ? ManagerStrings.EXERCISES : ManagerStrings.EXERCISE; + } + + //@@author V4vern + private String executeSearchAction(Parser parser) { + String searchTerm = parser.getActionParameter(); + return getSearchResultsString(searchTerm); + } +} diff --git a/src/main/java/byteceps/processing/HelpMenuManager.java b/src/main/java/byteceps/processing/HelpMenuManager.java new file mode 100644 index 0000000000..fcb75d4c43 --- /dev/null +++ b/src/main/java/byteceps/processing/HelpMenuManager.java @@ -0,0 +1,134 @@ +package byteceps.processing; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.HelpStrings; +import byteceps.ui.strings.CommandStrings; +import byteceps.validators.HelpValidator; + +import static byteceps.ui.strings.ManagerStrings.NO_ACTION_EXCEPTION; + +/** + * Displays the correct command formatting for all BYTE-CEPS functionalities. Command formats are divided into 3 + * categories, each one corresponding to one of the 3 main commands (exercise, workout & program). + */ +//@@author LWachtel1 +public class HelpMenuManager { + + public HelpMenuManager() { + } + /** + * Returns String that explains to user how to access each of the 3 "help menus" for the + * 3 main commands (exercise, workout & program). This can be accessed by user using 'help'. + * + * @return String informing user how to access each "help menu" for the main commands (exercise, workout & program). + */ + public String getHelpGuidanceString() { + return HelpStrings.HELP_GUIDANCE_MESSAGE; + } + /** + * Displays either (1) a command help menu (if no valid numerical parameter is provided) or (2) the specific + * command format for a specific BYTE-CEPS functionality, which corresponds to the help menu entry specified by + * the provided valid numerical parameter or (3) the message containing guidance for accessing help menus (if + * 'help' is entered alone). + * + * @param parser Parser containing required user input. + * @return String of a command help menu, a specific command formatting or guidance for accessing help menus. + */ + public String execute(Parser parser) throws Exceptions.InvalidInput { + + try { + HelpValidator.validateCommand(parser); + } catch (Exceptions.InvalidInput e) { + if (e.getMessage().equals(NO_ACTION_EXCEPTION)) { + return getHelpGuidanceString(); + } else { + throw e; + } + } + + String commandToShow = parser.getAction().toLowerCase(); + boolean showAllActions = parser.getActionParameter().isEmpty(); + + if (showAllActions) { + return generateAllActions(commandToShow); + } + String parameter = parser.getActionParameter(); + return getParamFormat(parameter, commandToShow); + + } + /** + * Builds a String containing a command's entire help menu (either exercise, workout or program) i.e., a command's + * entire list of associated functionalities + * + * @param command Command for which user wants to view help menu. + * @return String of a command's help menu as an indented list + */ + private String generateAllActions(String command) throws Exceptions.InvalidInput { + String[] flagFunctions; + StringBuilder result = new StringBuilder(); + + switch (command) { + case CommandStrings.COMMAND_EXERCISE: + flagFunctions = HelpStrings.EXERCISE_FLAG_FUNCTIONS; + result.append(String.format(HelpStrings.HELP_LIST_ITEM, HelpStrings.EXERCISE_MESSAGE, + System.lineSeparator())); + break; + case CommandStrings.COMMAND_WORKOUT: + flagFunctions = HelpStrings.WORKOUT_FLAG_FUNCTIONS; + result.append(String.format(HelpStrings.HELP_LIST_ITEM, HelpStrings.WORKOUT_MESSAGE, + System.lineSeparator())); + break; + case CommandStrings.COMMAND_PROGRAM: + flagFunctions = HelpStrings.PROGRAM_FLAG_FUNCTIONS; + result.append(String.format(HelpStrings.HELP_LIST_ITEM, HelpStrings.PROGRAM_MESSAGE, + System.lineSeparator())); + break; + default: + throw new Exceptions.InvalidInput(HelpStrings.INVALID_COMMAND_TYPE); + } + + for (String flagFunction : flagFunctions) { + result.append(String.format(HelpStrings.HELP_LIST_ITEM, flagFunction, System.lineSeparator())); + } + result.delete(0, 4); + return result.toString(); + } + /** + * Returns a String containing the specific command format for a specific BYTE-CEPS functionality, corresponding + * to the help menu entry specified by the user-provided numerical parameter. + * + * @param userParam String corresponding to numerical position of help menu entry of user-desired command format. + * @param commandType String specifying which command help menu to find command formatting from. + * @return String containing the specific command format corresponding to provided parameter. + */ + private String getParamFormat(String userParam, String commandType) throws Exceptions.InvalidInput { + try { + int paramChoice = Integer.parseInt(userParam); + int paramIndex = paramChoice - 1; + + switch (commandType) { + case CommandStrings.COMMAND_EXERCISE: + return getExerciseParamFormats(paramIndex); + case CommandStrings.COMMAND_WORKOUT: + return getWorkoutParamFormats(paramIndex); + case CommandStrings.COMMAND_PROGRAM: + return getProgramParamFormats(paramIndex); + default: + throw new Exceptions.InvalidInput(HelpStrings.INVALID_COMMAND_TYPE); + } + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new Exceptions.InvalidInput(HelpStrings.INVALID_COMMAND); + } + } + private String getExerciseParamFormats(int index) { + return HelpStrings.EXERCISE_PARAM_FORMAT[index]; + } + private String getWorkoutParamFormats(int index) { + return HelpStrings.WORKOUT_PARAM_FORMAT[index]; + } + private String getProgramParamFormats(int index) { + return HelpStrings.PROGRAM_PARAM_FORMAT[index]; + } + +} diff --git a/src/main/java/byteceps/processing/WeeklyProgramManager.java b/src/main/java/byteceps/processing/WeeklyProgramManager.java new file mode 100644 index 0000000000..0ab91ab996 --- /dev/null +++ b/src/main/java/byteceps/processing/WeeklyProgramManager.java @@ -0,0 +1,368 @@ +package byteceps.processing; + +import byteceps.activities.Day; +import byteceps.activities.Workout; +import byteceps.activities.Exercise; +import byteceps.activities.WorkoutLog; +import byteceps.activities.Activity; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.DayStrings; +import byteceps.ui.strings.ManagerStrings; +import byteceps.validators.WeeklyProgramValidator; + +import byteceps.validators.WorkoutLogsValidator; +import org.json.JSONObject; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class WeeklyProgramManager extends ActivityManager { + private final ExerciseManager exerciseManager; + private final WorkoutManager workoutManager; + private final WorkoutLogsManager workoutLogsManager; + + public WeeklyProgramManager(ExerciseManager exerciseManager, WorkoutManager workoutManager, + WorkoutLogsManager workoutLogsManager) { + this.exerciseManager = exerciseManager; + this.workoutManager = workoutManager; + this.workoutLogsManager = workoutLogsManager; + initializeDays(); + } + + private static String getWorkoutName(Day selectedDay, String workoutDate) throws Exceptions.ActivityDoesNotExist { + Workout assignedWorkout = selectedDay.getAssignedWorkout(); + if (assignedWorkout == null) { + throw new Exceptions.ActivityDoesNotExist( + String.format(ManagerStrings.NO_WORKOUT_ASSIGNED, workoutDate, selectedDay.getActivityName()) + ); + } + return assignedWorkout.getActivityName(); + } + + private static String formatDateString(String workoutDate) throws Exceptions.ActivityDoesNotExist { + if (workoutDate == null || workoutDate.isEmpty()) { + workoutDate = LocalDate.now().toString(); + } else { + LocalDate currentDate = LocalDate.now(); + LocalDate inputDate = LocalDate.parse(workoutDate); + if (inputDate.isAfter(currentDate)) { + throw new Exceptions.ActivityDoesNotExist(DayStrings.FUTURE_DATE); + } + } + return workoutDate; + } + + private void initializeDays() { + for (String day : DayStrings.DAYS) { + Day newDay = new Day(day); + newDay.setAssignedWorkout(null); + activitySet.add(newDay); + } + } + + @Override + public void reset() { + super.reset(); + initializeDays(); + } + + public Day getDay(String day) throws Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist { + switch (day.toLowerCase()) { + case DayStrings.DAY_MON: + case DayStrings.DAY_MONDAY: + return (Day) retrieve(DayStrings.DAYS[0]); + case DayStrings.DAY_TUE: + case DayStrings.DAY_TUES: + case DayStrings.DAY_TUESDAY: + return (Day) retrieve(DayStrings.DAYS[1]); + case DayStrings.DAY_WED: + case DayStrings.DAY_WEDNESDAY: + return (Day) retrieve(DayStrings.DAYS[2]); + case DayStrings.DAY_THU: + case DayStrings.DAY_THURS: + case DayStrings.DAY_THURSDAY: + return (Day) retrieve(DayStrings.DAYS[3]); + case DayStrings.DAY_FRI: + case DayStrings.DAY_FRIDAY: + return (Day) retrieve(DayStrings.DAYS[4]); + case DayStrings.DAY_SAT: + case DayStrings.DAY_SATURDAY: + return (Day) retrieve(DayStrings.DAYS[5]); + case DayStrings.DAY_SUN: + case DayStrings.DAY_SUNDAY: + return (Day) retrieve(DayStrings.DAYS[6]); + default: + throw new Exceptions.InvalidInput(DayStrings.INVALID_DAY); + } + } + + /** + * Executes all commands that start with the keyword "program". + * + * @param parser Parser containing user input + * @return Message to user after executing the command + * @throws Exceptions.InvalidInput if no command action specified + * @throws Exceptions.ActivityDoesNotExist if user inputs name of an activity that does not exist + * @throws Exceptions.ActivityExistsException if user assigns a workout to an occupied day + */ + public String execute(Parser parser) throws Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist, + Exceptions.ActivityExistsException { + + String commandAction = WeeklyProgramValidator.validateCommand(parser); + String messageToUser; + + switch (commandAction) { + case CommandStrings.ACTION_ASSIGN: + messageToUser = executeAssignAction(parser); + break; + case CommandStrings.ACTION_CLEAR: + messageToUser = executeClearAction(parser); + break; + case CommandStrings.ACTION_TODAY: + messageToUser = executeTodayAction(); + break; + case CommandStrings.ACTION_LOG: + messageToUser = executeLogAction(parser); + break; + case CommandStrings.ACTION_LIST: + messageToUser = executeListAction(); + break; + case CommandStrings.ACTION_HISTORY: + messageToUser = executeHistoryAction(parser); + break; + default: + throw new IllegalStateException(String.format(ManagerStrings.UNEXPECTED_ACTION, parser.getAction())); + } + + return messageToUser; + } + + private String executeListAction() { + return getListString(); + } + + /** + * Executes the command "program /assign {workout} /to {day}". + * + * @param parser Parser containing user input + * @return Message to user after executing the command + * @throws Exceptions.InvalidInput if user does not specify the day to assign the workout to + * @throws Exceptions.ActivityDoesNotExist if user inputs name of a workout that does not exist + * @throws Exceptions.ActivityExistsException if user assigns a workout to an occupied day + */ + private String executeAssignAction(Parser parser) throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput, + Exceptions.ActivityExistsException { + String day = parser.getAdditionalArguments(CommandStrings.ARG_TO); + String workoutName = parser.getActionParameter(); + Activity workout = workoutManager.retrieve(workoutName); + return assignWorkoutToDay(workout, day); + } + + /** + * Assigns a workout to a given day. + * + * @param workout Workout to be assigned + * @param day The day the workout is to be assigned to + * @return Message to user after executing the command + * @throws Exceptions.ActivityDoesNotExist if user inputs name of a workout that does not exist + * @throws Exceptions.ActivityExistsException if user assigns a workout to an occupied day + */ + public String assignWorkoutToDay(Activity workout, String day) + throws Exceptions.ActivityExistsException, Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + Day selectedDay = getDay(day); + Workout chosenDayWorkout = selectedDay.getAssignedWorkout(); + + if (chosenDayWorkout != null) { + throw new Exceptions.ActivityExistsException( + String.format(ManagerStrings.WORKOUT_ALREADY_ASSIGNED, + chosenDayWorkout.getActivityName(), selectedDay.getActivityName() + ) + ); + } + selectedDay.setAssignedWorkout((Workout) workout); + return String.format(ManagerStrings.WORKOUT_ASSIGNED, workout.getActivityName(), day); + } + + private Day getDayFromDate(LocalDate date) throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + DayOfWeek dayFromDate = date.getDayOfWeek(); + return getDay(dayFromDate.toString()); + } + + private Day getDayFromDate(String dateString) + throws Exceptions.ActivityDoesNotExist, DateTimeParseException, Exceptions.InvalidInput { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DayStrings.YEAR_FORMAT); + formatter = formatter.withLocale(formatter.getLocale()); + LocalDate date = LocalDate.parse(dateString, formatter); + DayOfWeek dayFromDate = date.getDayOfWeek(); + return getDay(dayFromDate.toString()); + } + + private String executeLogAction(Parser parser) throws Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist { + String exerciseName = parser.getActionParameter(); + String sets = parser.getAdditionalArguments(CommandStrings.ARG_SETS); + String repetition = parser.getAdditionalArguments(CommandStrings.ARG_REPS); + String weight = parser.getAdditionalArguments(CommandStrings.ARG_WEIGHT); + + WorkoutLogsValidator.exerciseExists(exerciseManager, exerciseName); + + String workoutDate = parser.getAdditionalArguments(CommandStrings.ARG_DATE); + workoutDate = formatDateString(workoutDate); + Day selectedDay = getDayFromDate(workoutDate); + + String workoutName = getWorkoutName(selectedDay, workoutDate); + workoutLogsManager.addWorkoutLog(workoutDate, workoutName); + + workoutLogsManager.addExerciseLog(workoutDate, exerciseName, weight, sets, repetition); + + int setsInt = Integer.parseInt(sets); + List weightList = Arrays.asList(weight.split(" ")); + List repetitionList = Arrays.stream(repetition.split(" ")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + String formattedWeights = weightList.stream() + .map(w -> w + "kg") + .collect(Collectors.joining(", ")); + + String formattedReps = repetitionList.size() == 1 ? repetitionList.get(0).toString() : + repetitionList.stream().map(String::valueOf).collect(Collectors.joining(", ")); + + String setWord = setsInt == 1 ? "set" : "sets"; + String weightWord = weightList.size() == 1 ? "weight of" : "weights of"; + String repWord = (repetitionList.size() == 1 && repetitionList.get(0) == 1) ? "rep" : "reps"; + + return String.format(ManagerStrings.LOG_SUCCESS, + exerciseName, weightWord, formattedWeights, formattedReps, repWord, setsInt, setWord, workoutDate); + } + + private String executeTodayAction() throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + LocalDate currentDate = LocalDate.now(); + + Day today = getDayFromDate(currentDate); + Workout todaysWorkout = today.getAssignedWorkout(); + String todayDate = currentDate.toString(); + + if (todaysWorkout == null) { + return String.format(ManagerStrings.NO_WORKOUT_ASSIGNED_TODAY, today.getActivityName()); + } + return getTodaysWorkoutString(todaysWorkout, todayDate, today); + } + + private String getTodaysWorkoutString(Workout givenWorkout, String workoutDate, Day workoutDay) + throws Exceptions.ActivityDoesNotExist { + Workout workout = workoutDay.getAssignedWorkout(); + if (workout == null) { + throw new Exceptions.ActivityDoesNotExist("Workout does not exist"); + } + String workoutName = workout.getActivityName(); + LinkedHashSet workoutLinkedHashSet = givenWorkout.getExerciseSet(); + workoutLogsManager.addWorkoutLog(workoutDate, workoutName); + return workoutLogsManager.getWorkoutLogString(workoutDate, workoutLinkedHashSet); + } + + private String executeHistoryAction(Parser parser) + throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + String parameter = parser.getActionParameter(); + if (parameter.isBlank()) { + return getHistoryString(); + } + + String workoutDate = parser.getActionParameter(); + WorkoutLog retrievedWorkout = (WorkoutLog) workoutLogsManager.retrieve(workoutDate); + + try { + Day day = getDayFromDate(workoutDate); + return getTodaysWorkoutString(retrievedWorkout, workoutDate, day); + } catch (DateTimeParseException e) { + throw new Exceptions.InvalidInput(ManagerStrings.INVALID_DATE_ENTERED); + } + } + + private String getHistoryString() { + return workoutLogsManager.getListString(); + } + + public LinkedHashSet getDaySet() { + return activitySet; + } + + private String executeClearAction(Parser parser) throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + String day = parser.getActionParameter(); + if (day == null || day.isEmpty()) { + activitySet.clear(); + initializeDays(); + return ManagerStrings.PROGRAMS_CLEARED; + } + Day selectedDay = getDay(day); + Workout currentWorkout = selectedDay.getAssignedWorkout(); + + if (currentWorkout == null) { + return String.format(ManagerStrings.NO_WORKOUT_TO_CLEAR, day); + } + + selectedDay.setAssignedWorkout(null); + activitySet.remove(selectedDay); + activitySet.add(selectedDay); + return String.format(ManagerStrings.WORKOUT_CLEARED, day); + } + + + public JSONObject exportToJSON() { + JSONObject json = new JSONObject(); + try { + for (String day : DayStrings.DAYS) { + Day currentDay = getDay(day); + Workout assignedWorkout = currentDay.getAssignedWorkout(); + String workoutName = ""; + + if (assignedWorkout != null) { + workoutName = assignedWorkout.getActivityName(); + } + json.put(day, workoutName); + } + } catch (Exceptions.InvalidInput | Exceptions.ActivityDoesNotExist ignored) { + // should not get an exception as it is generated + } + + return json; + } + + @Override + public String getActivityType(boolean plural) { + return ManagerStrings.WEEKLY_PROGRAM; + } + + @Override + public String getListString() { + StringBuilder message = new StringBuilder(); + message.append(ManagerStrings.PROGRAM_LIST).append(System.lineSeparator()); + for (String day : DayStrings.DAYS) { + try { + Day dayObj = getDay(day); + String dayString = dayObj.getActivityName(); + Workout dayWorkout = dayObj.getAssignedWorkout(); + message.append(String.format(ManagerStrings.PROGRAM_LIST_ITEM, dayString)); + + if (dayWorkout == null) { + message.append(DayStrings.REST_DAY).append(System.lineSeparator().repeat(2)); + } else { + message.append(dayWorkout.toString(1)).append(System.lineSeparator()); + } + + } catch (Exceptions.ActivityDoesNotExist | Exceptions.InvalidInput ignored) { + // should not get an exception as it is generated + } + + } + return message.toString(); + } + +} diff --git a/src/main/java/byteceps/processing/WorkoutLogsManager.java b/src/main/java/byteceps/processing/WorkoutLogsManager.java new file mode 100644 index 0000000000..19a0c81c11 --- /dev/null +++ b/src/main/java/byteceps/processing/WorkoutLogsManager.java @@ -0,0 +1,142 @@ +package byteceps.processing; + +import byteceps.activities.Activity; +import byteceps.activities.Exercise; +import byteceps.activities.ExerciseLog; +import byteceps.activities.WorkoutLog; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.ManagerStrings; +import byteceps.ui.strings.StorageStrings; +import byteceps.validators.WorkoutLogsValidator; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +public class WorkoutLogsManager extends ActivityManager { + public WorkoutLogsManager() { + } + + @Override + public String execute(Parser parser) throws Exceptions.InvalidInput { + throw new Exceptions.InvalidInput(ManagerStrings.LOG_INVALID_STATE); + } + + public void addWorkoutLog(String workoutLogDate, String workoutName) { + WorkoutLog newWorkoutLog = new WorkoutLog(workoutLogDate, workoutName); + try { + add(newWorkoutLog); + } catch (Exceptions.ActivityExistsException e) { + // silently fail as duplicates are okay + } + } + + public void addExerciseLog(String workoutLogDate, String exerciseName, + String weight, String sets, String repetitions) + throws Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist { + try { + List weightsList = Arrays.stream(weight.split(" ")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + int setsInt = Integer.parseInt(sets); + List repsList = Arrays.stream(repetitions.split(" ")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + WorkoutLogsValidator.hasNegativeInput(weightsList, repsList, setsInt); + + ExerciseLog newExerciseLog = new ExerciseLog(exerciseName, weightsList, setsInt, repsList); + WorkoutLog workoutLog = (WorkoutLog) retrieve(workoutLogDate); + WorkoutLogsValidator.removeExerciseIfLogExists(workoutLog, exerciseName); + workoutLog.addExerciseLog(newExerciseLog); + } catch (NumberFormatException e) { + throw new Exceptions.InvalidInput(ManagerStrings.INVALID_REPS_SETS); + } + } + + public String getWorkoutLogString(String date, LinkedHashSet workoutLinkedHashSet) + throws Exceptions.ActivityDoesNotExist { + WorkoutLog retrievedWorkout = (WorkoutLog) retrieve(date); + LinkedHashSet exerciseLogs = retrievedWorkout.getExerciseLogs(); + LinkedHashSet tempSet = new LinkedHashSet<>(workoutLinkedHashSet); + StringBuilder result = new StringBuilder(); + result.append(String.format( + ManagerStrings.LOG_LIST, date)); + + int index = 1; + for (ExerciseLog currentExerciseLog : exerciseLogs) { + String exerciseName = currentExerciseLog.getActivityName(); + int setCount = currentExerciseLog.getSets(); + List repsList = currentExerciseLog.getRepetitions(); + List weightsList = currentExerciseLog.getWeights(); + + + result.append(String.format("\t\t\t %d. %s\n", index, exerciseName)); + for (int setIndex = 0; setIndex < setCount; setIndex++) { + int weight = weightsList.get(setIndex); + int reps = repsList.get(setIndex); + result.append(String.format(ManagerStrings.LOG_LIST_ITEM, setIndex + 1, weight, reps)); + } + + tempSet.removeIf(p -> p.getActivityName().equals(exerciseName)); + index++; + } + + for (Exercise currentExercise : tempSet) { + String exerciseName = currentExercise.getActivityName(); + result.append(String.format(ManagerStrings.ACTIVITY_LIST_ITEM, index, exerciseName)); + index++; + } + + return result.toString(); + } + + @Override + public String getActivityType(boolean plural) { + return plural ? ManagerStrings.WORKOUT_LOGS : ManagerStrings.WORKOUT_LOG; + } + + public JSONArray exportToJSON() { + ArrayList workoutLogs = getActivityList(); + JSONArray workouts = new JSONArray(); + for (Activity currentActivity : workoutLogs) { + WorkoutLog currentWorkout = (WorkoutLog) currentActivity; + String workoutDate = currentWorkout.getWorkoutDate(); + String workoutName = currentWorkout.getWorkoutName(); + + LinkedHashSet exercises = currentWorkout.getExerciseLogs(); + JSONObject workoutJson = getWorkoutJson(exercises, workoutName, workoutDate); + + workouts.put(workoutJson); + } + return workouts; + } + + private static JSONObject getWorkoutJson(LinkedHashSet exercises, + String workoutName, String workoutDate) { + JSONArray workoutExercises = new JSONArray(); + for (ExerciseLog currentExercise : exercises) { + JSONObject exercise = new JSONObject(); + String exerciseName = currentExercise.getActivityName(); + + exercise.put(StorageStrings.EXERCISE_NAME, exerciseName); + exercise.put(StorageStrings.WEIGHT, currentExercise.getWeights()); + exercise.put(StorageStrings.SETS, currentExercise.getSets()); + exercise.put(StorageStrings.REPS, currentExercise.getRepetitions()); + + workoutExercises.put(exercise); + } + + JSONObject workoutJson = new JSONObject(); + workoutJson.put(StorageStrings.WORKOUT_DATE, workoutDate); + workoutJson.put(StorageStrings.WORKOUT_NAME, workoutName); + workoutJson.put(StorageStrings.EXERCISES, workoutExercises); + return workoutJson; + } + +} diff --git a/src/main/java/byteceps/processing/WorkoutManager.java b/src/main/java/byteceps/processing/WorkoutManager.java new file mode 100644 index 0000000000..756b71062f --- /dev/null +++ b/src/main/java/byteceps/processing/WorkoutManager.java @@ -0,0 +1,193 @@ +package byteceps.processing; + +import byteceps.activities.Exercise; +import byteceps.activities.Workout; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.ManagerStrings; +import byteceps.validators.WorkoutValidator; + +import java.util.ArrayList; + +/** + * Manages operations related to workout, such as adding, deleting, + * assigning, unassigning, listing, and searching workout. + */ +public class WorkoutManager extends ActivityManager { + private final ExerciseManager exerciseManager; + + public WorkoutManager(ExerciseManager exerciseManager) { + this.exerciseManager = exerciseManager; + } + + //@@author V4vern + /** + * Executes all commands that start with the keyword "workout". + * + * @param parser Parser containing user input. + * @return Message to user after executing the command. + * @throws Exceptions.InvalidInput if no command action specified. + * @throws Exceptions.ActivityDoesNotExist if user inputs name of an activity that does not exist. + * @throws Exceptions.ActivityExistsException if user attempts to create an existing workout. + */ + @Override + public String execute(Parser parser) throws Exceptions.ActivityExistsException, + Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist { + String command = WorkoutValidator.validateCommand(parser); + + String messageToUser; + switch (command) { + case CommandStrings.ACTION_CREATE: + messageToUser = executeCreateAction(parser); + break; + case CommandStrings.ACTION_DELETE: + messageToUser = executeDeleteAction(parser); + break; + case CommandStrings.ACTION_EDIT: + messageToUser = executeEditAction(parser); + break; + case CommandStrings.ACTION_ASSIGN: + messageToUser = executeAssignAction(parser); + break; + case CommandStrings.ACTION_UNASSIGN: + messageToUser = executeUnassignAction(parser); + break; + case CommandStrings.ACTION_INFO: + messageToUser = executeInfoAction(parser); + break; + case CommandStrings.ACTION_LIST: + messageToUser = executeListAction(parser); + break; + case CommandStrings.ACTION_SEARCH: + messageToUser = executeSearchAction(parser); + break; + default: + messageToUser = ""; + assert false : "input should have been validated before switch"; + } + + return messageToUser; + } + + private String executeInfoAction(Parser parser) throws Exceptions.ActivityDoesNotExist { + String workoutName = parser.getActionParameter().toLowerCase(); + return getFullWorkoutString(workoutName); + } + + private String executeListAction(Parser parser) { + return getListString(); + } + + private String executeUnassignAction(Parser parser) throws Exceptions.ActivityDoesNotExist { + String workoutName = unassignExerciseFromWorkout(parser); + return String.format( + ManagerStrings.UNASSIGNED_EXERCISE, parser.getActionParameter().toLowerCase(), workoutName + ); + } + + private String executeEditAction(Parser parser) throws Exceptions.ActivityDoesNotExist { + String oldWorkoutName = parser.getActionParameter().toLowerCase(); + String newWorkoutName = parser.getAdditionalArguments(CommandStrings.ARG_TO).toLowerCase(); + + if (oldWorkoutName.equals(newWorkoutName)) { + return String.format(ManagerStrings.WORKOUT_NAME_SAME, oldWorkoutName); + } + + Workout workoutToEdit = (Workout) retrieve(oldWorkoutName); + workoutToEdit.editWorkoutName(newWorkoutName, this); + return String.format( + ManagerStrings.WORKOUT_EDITED, oldWorkoutName, newWorkoutName + ); + } + + private String executeAssignAction(Parser parser) + throws Exceptions.ActivityExistsException, Exceptions.ActivityDoesNotExist { + String workoutPlan = assignExerciseToWorkout(parser); + return String.format(ManagerStrings.ASSIGNED_EXERCISE, parser.getActionParameter().toLowerCase(), workoutPlan); + } + + private String executeDeleteAction(Parser parser) throws Exceptions.ActivityDoesNotExist { + String workoutName = parser.getActionParameter().toLowerCase(); + Workout workoutToDelete = (Workout) retrieve(workoutName); + delete(workoutToDelete); + return String.format(ManagerStrings.WORKOUT_DELETED, workoutToDelete.getActivityName()); + } + + private String executeCreateAction(Parser parser) throws Exceptions.ActivityExistsException { + String newWorkoutName = parser.getActionParameter().toLowerCase(); + Workout newWorkout = new Workout(newWorkoutName); + add(newWorkout); + return String.format(ManagerStrings.WORKOUT_ADDED, newWorkout.getActivityName()); + } + + //@@author V4vern + private String assignExerciseToWorkout(Parser parser) throws + Exceptions.ActivityExistsException, Exceptions.ActivityDoesNotExist { + String exerciseName = parser.getActionParameter().toLowerCase(); + String workoutPlanName = parser.getAdditionalArguments(CommandStrings.ARG_TO).toLowerCase(); + + Exercise exercise = (Exercise) exerciseManager.retrieve(exerciseName); + assert exercise != null : "Exercise does not exist"; + Workout workoutPlan = (Workout) retrieve(workoutPlanName); + assert workoutPlan != null : "Workout plan does not exist"; + + if (workoutPlan.getExerciseList().contains(exercise)) { + throw new Exceptions.ActivityExistsException(ManagerStrings.EXERCISE_ALREADY_ASSIGNED); + } + + workoutPlan.addExercise(exercise); + + return workoutPlanName; + } + + //@@author V4vern + private String getFullWorkoutString(String workoutPlanName) throws Exceptions.ActivityDoesNotExist { + assert workoutPlanName != null : "Workout plan name cannot be null"; + Workout workout = (Workout) retrieve(workoutPlanName); + assert workout != null : "Workout plan does not exist"; + StringBuilder message = new StringBuilder(); + ArrayList workoutList = workout.getExerciseList(); + + if (workoutList.isEmpty()) { + return String.format(ManagerStrings.EMPTY_WORKOUT_PLAN, workoutPlanName); + } + + message.append(String.format(ManagerStrings.LIST_WORKOUT_PLAN, workoutPlanName)); + + int index = 1; + for (Exercise exercise : workoutList) { + message.append(String.format(ManagerStrings.ACTIVITY_LIST_ITEM, index++, exercise.getActivityName())); + } + return message.toString(); + } + + //@@author V4vern + private String unassignExerciseFromWorkout(Parser parser) throws Exceptions.ActivityDoesNotExist { + String workoutPlanName = parser.getAdditionalArguments(CommandStrings.ARG_FROM).toLowerCase(); + String exerciseName = parser.getActionParameter(); + Workout workoutPlan = (Workout) retrieve(workoutPlanName); + assert workoutPlan != null : "Workout plan does not exist"; + ArrayList workoutList = workoutPlan.getExerciseList(); + + boolean exerciseIsInWorkout = + workoutList.removeIf(exercise -> exercise.getActivityName().equalsIgnoreCase(exerciseName)); + if (!exerciseIsInWorkout) { + throw new Exceptions.ActivityDoesNotExist(ManagerStrings.EXERCISE_WORKOUT_DOES_NOT_EXIST); + } + + return workoutPlanName; + } + + @Override + public String getActivityType(boolean plural) { + return plural ? ManagerStrings.WORKOUTS : ManagerStrings.WORKOUT; + } + + //@@author V4vern + private String executeSearchAction(Parser parser) { + String searchTerm = parser.getActionParameter(); + return getSearchResultsString(searchTerm); + } + +} diff --git a/src/main/java/byteceps/storage/Storage.java b/src/main/java/byteceps/storage/Storage.java new file mode 100644 index 0000000000..8d1a9fab4c --- /dev/null +++ b/src/main/java/byteceps/storage/Storage.java @@ -0,0 +1,214 @@ +package byteceps.storage; + +import byteceps.activities.Day; +import byteceps.activities.Exercise; +import byteceps.activities.Workout; +import byteceps.errors.Exceptions; +import byteceps.processing.ExerciseManager; +import byteceps.processing.WorkoutLogsManager; +import byteceps.processing.WeeklyProgramManager; +import byteceps.processing.WorkoutManager; +import byteceps.ui.strings.DayStrings; +import byteceps.ui.strings.StorageStrings; +import byteceps.ui.UserInterface; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Date; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Scanner; + +public class Storage { + private final Path filePath; + private final UserInterface ui; + public Storage(String filePath, UserInterface ui) { + this.filePath = Path.of(filePath); + this.ui = ui; + } + + public void save(ExerciseManager allExercises, WorkoutManager allWorkouts, + WeeklyProgramManager weeklyProgram, WorkoutLogsManager workoutLogsManager) + throws IOException { + JSONObject jsonArchive = new JSONObject().put( + StorageStrings.EXERCISE_MANAGER, allExercises.getActivityList().toArray()); + jsonArchive.put(StorageStrings.WORKOUT_MANAGER, allWorkouts.getActivityList().toArray()); + jsonArchive.put(StorageStrings.WEEKLY_PROGRAM, weeklyProgram.exportToJSON()); + jsonArchive.put(StorageStrings.WORKOUT_LOG_MANAGER, workoutLogsManager.exportToJSON()); + + FileWriter fileWriter = new FileWriter(filePath.toFile()); + fileWriter.write(jsonArchive.toString()); + fileWriter.close(); + + ui.printMessage(StorageStrings.WORKOUTS_SAVED); + } + + public void load(ExerciseManager allExercises, WorkoutManager allWorkouts, + WeeklyProgramManager weeklyProgram, WorkoutLogsManager workoutLogsManager) + throws IOException { + boolean exerciseManagerIsEmpty = allExercises.getActivityList().isEmpty(); + boolean workoutManagerIsEmpty = allWorkouts.getActivityList().isEmpty(); + boolean weeklyProgramIsEmpty = weeklyProgram.getActivityList().stream(). + allMatch(day-> ((Day) day).getAssignedWorkout() == null); + assert exerciseManagerIsEmpty && workoutManagerIsEmpty && weeklyProgramIsEmpty + : "Must load from a clean state"; + File jsonFile = filePath.toFile(); + + if (jsonFile.createNewFile()) { + ui.printMessage(StorageStrings.NO_SAVE_DATA); + return; + } + + ui.printMessage(StorageStrings.LOADING); + + try (Scanner jsonScanner = new Scanner(jsonFile)) { + JSONObject jsonArchive = new JSONObject(jsonScanner.nextLine()); + loadExercises(allExercises, jsonArchive); + loadWorkouts(allExercises, allWorkouts, jsonArchive); + loadWeeklyProgram(allWorkouts, weeklyProgram, jsonArchive); + loadWorkoutLogs(allExercises, allWorkouts, jsonArchive, workoutLogsManager); + ui.printMessage(StorageStrings.LOAD_SUCCESS); + } catch (Exceptions.ActivityExistsException | Exceptions.ErrorAddingActivity | + Exceptions.ActivityDoesNotExist | Exceptions.InvalidInput | JSONException | NoSuchElementException e) { + ui.printMessage(StorageStrings.LOAD_ERROR); + try { + renameCorruptedFile(jsonFile); + } catch (IOException ex) { + ui.printMessage(StorageStrings.NEW_JSON_ERROR); + } + allExercises.reset(); + allWorkouts.reset(); + weeklyProgram.reset(); + workoutLogsManager.reset(); + } + + } + + private static void renameCorruptedFile(File jsonFile) throws IOException { + String timestamp = new SimpleDateFormat(StorageStrings.BACKUP_DATE_FORMAT) + .format(new Date()); + File oldFile = new File(jsonFile.getParent(), + jsonFile.getName() + StorageStrings.OLD_SUFFIX + timestamp); + jsonFile.renameTo(oldFile); + jsonFile.createNewFile(); + } + + private static void loadWeeklyProgram(WorkoutManager allWorkouts, WeeklyProgramManager weeklyProgram, + JSONObject jsonArchive) + throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput, Exceptions.ActivityExistsException { + JSONObject jsonWeeklyProgram = jsonArchive.getJSONObject(StorageStrings.WEEKLY_PROGRAM); + + assert jsonWeeklyProgram.length() == 7 : "Weekly program array must be length 7"; + for (Iterator it = jsonWeeklyProgram.keys(); it.hasNext(); ) { + String day = it.next(); + String workout = (String) jsonWeeklyProgram.get(day); + if (!workout.isBlank()) { + Workout dayWorkout = (Workout) allWorkouts.retrieve(workout); + weeklyProgram.assignWorkoutToDay(dayWorkout, day); + } + } + } + + private static void loadWorkouts(ExerciseManager allExercises, WorkoutManager allWorkouts, JSONObject jsonArchive) + throws Exceptions.ActivityExistsException, Exceptions.ErrorAddingActivity, + Exceptions.ActivityDoesNotExist { + JSONArray jsonWorkoutArray = jsonArchive.getJSONArray(StorageStrings.WORKOUT_MANAGER); + for (int i = 0; i < jsonWorkoutArray.length(); i++) { + JSONObject jsonWorkout = jsonWorkoutArray.getJSONObject(i); + String workoutName = jsonWorkout.getString(StorageStrings.ACTIVITY_NAME); + Workout workout = new Workout(workoutName); + allWorkouts.add(workout); + JSONArray jsonExercisesInWorkout = jsonWorkout.getJSONArray( + StorageStrings.EXERCISE_LIST); + for (int j = 0; j < jsonExercisesInWorkout.length(); j++) { + String exerciseInWorkout = jsonExercisesInWorkout.getJSONObject(j) + .getString(StorageStrings.ACTIVITY_NAME); + workout.addExercise((Exercise) allExercises.retrieve(exerciseInWorkout)); + } + } + } + + private static void loadExercises(ExerciseManager allExercises, JSONObject jsonArchive) + throws Exceptions.ActivityExistsException, Exceptions.ErrorAddingActivity { + JSONArray jsonExerciseArray = jsonArchive.getJSONArray(StorageStrings.EXERCISE_MANAGER); + for (int i = 0; i < jsonExerciseArray.length(); i++) { + String exerciseName = jsonExerciseArray.getJSONObject(i).getString(StorageStrings.ACTIVITY_NAME); + allExercises.add(new Exercise(exerciseName)); + } + } + + private void loadWorkoutLogs(ExerciseManager allExercises, WorkoutManager allWorkouts, + JSONObject jsonArchive, WorkoutLogsManager workoutLogsManager) + throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + JSONArray jsonWorkoutLogs = jsonArchive.getJSONArray(StorageStrings.WORKOUT_LOG_MANAGER); + for (int i = 0; i < jsonWorkoutLogs.length(); i++) { + loadWorkoutLog(workoutLogsManager, jsonWorkoutLogs, i); + } + } + + private static void loadWorkoutLog(WorkoutLogsManager workoutLogsManager, JSONArray jsonWorkoutLogs, int index) + throws Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist { + JSONObject currentWorkout = jsonWorkoutLogs.getJSONObject(index); + JSONArray exercisesArray = currentWorkout.getJSONArray(StorageStrings.EXERCISES); + String workoutDate = currentWorkout.getString(StorageStrings.WORKOUT_DATE); + String workoutName = currentWorkout.getString(StorageStrings.WORKOUT_NAME); + + validateDateString(workoutDate); + workoutLogsManager.addWorkoutLog(workoutDate, workoutName); + + for (int j = 0; j < exercisesArray.length(); j++) { + loadExerciseLog(workoutLogsManager, exercisesArray, j, workoutDate); + } + } + + private static void loadExerciseLog(WorkoutLogsManager workoutLogsManager, + JSONArray exercisesArray, int index, String workoutDate) + throws Exceptions.InvalidInput, Exceptions.ActivityDoesNotExist { + JSONObject currentExercise = exercisesArray.getJSONObject(index); + String exerciseName = currentExercise.getString(StorageStrings.EXERCISE_NAME); + JSONArray weightArray = currentExercise.getJSONArray(StorageStrings.WEIGHT); + String setsString = String.valueOf(currentExercise.getInt((StorageStrings.SETS))); + JSONArray repsArray = currentExercise.getJSONArray(StorageStrings.REPS); + + String weights = weightArray.join(" ").replaceAll("\"", ""); + String reps = repsArray.join(" ").replaceAll("\"", ""); + + int sets = Integer.parseInt(setsString); + validateLogCounts(weightArray.length(), repsArray.length(), sets); + + workoutLogsManager.addExerciseLog(workoutDate, exerciseName, + weights, setsString , reps); + } + + private static void validateLogCounts(int weightCount, int repCount, int sets) + throws Exceptions.InvalidInput { + if (weightCount != sets || repCount != sets) { + throw new Exceptions.InvalidInput(""); + } + } + + private static void validateDateString(String workoutDate) throws Exceptions.InvalidInput { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DayStrings.YEAR_FORMAT); + formatter = formatter.withLocale(formatter.getLocale()); + LocalDate parsedDate = LocalDate.parse(workoutDate, formatter); //ignore result, just catch exception + if (!parsedDate.toString().equals(workoutDate)) { + throw new Exceptions.InvalidInput(""); + } + } catch (DateTimeParseException e) { + throw new Exceptions.InvalidInput(""); //no need for error message, LOAD_ERROR will be printed + } + } + + +} diff --git a/src/main/java/byteceps/ui/UserInterface.java b/src/main/java/byteceps/ui/UserInterface.java new file mode 100644 index 0000000000..1f10a87d5a --- /dev/null +++ b/src/main/java/byteceps/ui/UserInterface.java @@ -0,0 +1,68 @@ +package byteceps.ui; + +import byteceps.ui.strings.UiStrings; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Scanner; + +public class UserInterface { + private static UserInterface uiInstance; + private final Scanner in; + private final PrintStream out; + + public UserInterface() { + this(System.in, System.out); + } + public UserInterface(InputStream in, PrintStream out) { + this.in = new Scanner(in); + this.out = out; + } + + public static UserInterface getInstance() { + if (uiInstance == null) { + uiInstance = new UserInterface(); + } + return uiInstance; + } + + public void printMessage(String message) { + System.out.printf(UiStrings.BYTECEP_PROMPT_FORMAT, UiStrings.BYTECEP_PROMPT, message, System.lineSeparator()); + System.out.println(UiStrings.SEPARATOR); + } + + public void printMessageNoSeparator(String message) { + System.out.printf(UiStrings.BYTECEP_PROMPT_FORMAT, UiStrings.BYTECEP_PROMPT, message, System.lineSeparator()); + } + + //@@author pqienso + public void printWelcomeMessage() { + out.println(UiStrings.SEPARATOR); + out.println(UiStrings.MESSAGE_WELCOME); + out.println(UiStrings.SEPARATOR); + } + + //@@author pqienso + public void printGoodbyeMessage() { + out.println(UiStrings.SEPARATOR); + out.println(UiStrings.MESSAGE_GOODBYE); + out.println(UiStrings.SEPARATOR); + } + + //@@author joshualeejunyi + public String getUserInput() { + String userInput; + do { + out.printf(UiStrings.USER_PROMPT_FORMAT, UiStrings.USER_PROMPT); + userInput = in.nextLine().trim(); + } while (userInput.trim().isEmpty()); + + return userInput; + } + + // Reset method for testing + static void resetInstance() { + uiInstance = null; + } + +} diff --git a/src/main/java/byteceps/ui/strings/CommandStrings.java b/src/main/java/byteceps/ui/strings/CommandStrings.java new file mode 100644 index 0000000000..c1d487a8a9 --- /dev/null +++ b/src/main/java/byteceps/ui/strings/CommandStrings.java @@ -0,0 +1,39 @@ +package byteceps.ui.strings; + +public class CommandStrings { + // Top level commands + public static final String COMMAND_EXERCISE = "exercise"; + public static final String COMMAND_WORKOUT = "workout"; + public static final String COMMAND_PROGRAM = "program"; + public static final String COMMAND_HELP = "help"; + public static final String COMMAND_BYE = "bye"; + public static final String COMMAND_EXIT = "exit"; + public static final String UNKNOWN_COMMAND = "Unknown Command!"; + + // Common Actions + public static final String ACTION_LIST = "list"; + public static final String ACTION_SEARCH = "search"; + public static final String ACTION_DELETE = "delete"; + public static final String ACTION_ASSIGN = "assign"; + + // Exercise Actions + public static final String ACTION_ADD = "add"; + public static final String ACTION_EDIT = "edit"; + + // Workout Actions + public static final String ACTION_CREATE = "create"; + public static final String ACTION_UNASSIGN = "unassign"; + public static final String ACTION_INFO = "info"; + + // WeeklyProgram + public static final String ACTION_CLEAR = "clear"; + public static final String ACTION_TODAY = "today"; + public static final String ACTION_LOG = "log"; + public static final String ACTION_HISTORY = "history"; + public static final String ARG_TO = "to"; + public static final String ARG_FROM = "from"; + public static final String ARG_SETS = "sets"; + public static final String ARG_REPS = "reps"; + public static final String ARG_WEIGHT = "weight"; + public static final String ARG_DATE = "date"; +} diff --git a/src/main/java/byteceps/ui/strings/DayStrings.java b/src/main/java/byteceps/ui/strings/DayStrings.java new file mode 100644 index 0000000000..ed1606b740 --- /dev/null +++ b/src/main/java/byteceps/ui/strings/DayStrings.java @@ -0,0 +1,25 @@ +package byteceps.ui.strings; + +public class DayStrings { + public static final String[] DAYS = {"MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"}; + public static final String DAY_MONDAY = "monday"; + public static final String DAY_MON = "mon"; + public static final String DAY_TUESDAY = "tuesday"; + public static final String DAY_TUE = "tue"; + public static final String DAY_TUES = "tues"; + public static final String DAY_WEDNESDAY = "wednesday"; + public static final String DAY_WED = "wed"; + public static final String DAY_THURSDAY = "thursday"; + public static final String DAY_THU = "thu"; + public static final String DAY_THURS = "thurs"; + public static final String DAY_FRIDAY = "friday"; + public static final String DAY_FRI = "fri"; + public static final String DAY_SATURDAY = "saturday"; + public static final String DAY_SAT = "sat"; + public static final String DAY_SUNDAY = "sunday"; + public static final String DAY_SUN = "sun"; + public static final String FUTURE_DATE = "future dates are not allowed"; + public static final String INVALID_DAY = "Not a valid day"; + public static final String REST_DAY = "Rest day"; + public static final String YEAR_FORMAT = "yyyy-M-dd"; +} diff --git a/src/main/java/byteceps/ui/strings/HelpStrings.java b/src/main/java/byteceps/ui/strings/HelpStrings.java new file mode 100644 index 0000000000..86e265e081 --- /dev/null +++ b/src/main/java/byteceps/ui/strings/HelpStrings.java @@ -0,0 +1,88 @@ +package byteceps.ui.strings; + +//@@author LWachtel1 +public class HelpStrings { + public static final String HELP_LIST_INDENT = "\t\t\t "; + public static final String HELP_LIST_ITEM = "\t\t\t %s%s"; + + public static final String HELP_GUIDANCE_MESSAGE = + String.format("%s%s%s%s%s%s%s%s%s%s%s%s%s", "To access the help menu for command guidance, please type:", + System.lineSeparator(), "help /COMMAND_TYPE_FLAG", System.lineSeparator(), + "Available command types (type exactly as shown):", System.lineSeparator(), + CommandStrings.COMMAND_EXERCISE, System.lineSeparator(), CommandStrings.COMMAND_WORKOUT, + System.lineSeparator(), CommandStrings.COMMAND_PROGRAM, System.lineSeparator(), + "To view this message again, enter 'help' alone"); + + public static final String[] EXERCISE_FLAG_FUNCTIONS = { + "1. add an exercise", + "2. delete an existing exercise", + "3. edit an existing exercise's name", + "4. list all existing exercises" + }; + + public static final String[] EXERCISE_PARAM_FORMAT = { + "exercise /add ", + "exercise /delete ", + "exercise /edit /to ", + "exercise /list" + }; + + public static final String[] WORKOUT_FLAG_FUNCTIONS = { + "1. create a workout plan", + "2. delete an existing workout plan", + "3. list all existing workout plans", + "4. assign an exercise to a specified workout plan", + "5. remove an exercise from a specified workout plan", + "6. list all exercises in a given workout plan" + }; + + public static final String[] WORKOUT_PARAM_FORMAT = { + "workout /create ", + "workout /delete ", + "workout /list", + "workout /assign /to ", + "workout /unassign /from ", + "workout /info " + }; + + public static final String[] PROGRAM_FLAG_FUNCTIONS = { + "1. assign a workout plan to a specific day of the week", + "2. view today's workout plan", + "3. see all workout plans assigned to each day of the week", + "4. remove a workout plan from a given day of the week", + String.format ("%s%n%s%s", "5. create a log for the amount of weight, sets & reps completed for an exercise on" + + " a given day", HELP_LIST_INDENT, "which already has an assigned workout plan"), + "6. create a log for a specified date", + "7. see all the dates that you have entered at least 1 log entry", + "8. view the logs that you have added on a specific date" + }; + public static final String DAY_STRING = String.format("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", System.lineSeparator(), + "The parameter must be a day of the week, and is case insensitive:", System.lineSeparator(), + "Monday/Mon", System.lineSeparator(), "Tuesday/Tues/Tue", System.lineSeparator(), + "Wednesday/Wed", System.lineSeparator(), "Thursday/Thurs/Thu", System.lineSeparator(), + "Friday/Fri", System.lineSeparator(), "Saturday/Sat", System.lineSeparator(), + "Sunday/Sun"); + + public static final String[] PROGRAM_PARAM_FORMAT = { + "program /assign /to " + DAY_STRING, + "program /today", + "program /list", + "program /clear " + DAY_STRING, + "program /log /weight /sets " + + "/reps ", + "program /log /weight /sets " + + "/reps /date ", "program /history", + "program /history " + }; + + public static final String EXERCISE_MESSAGE = "Please enter 'help /exercise LIST_NUMBER'. " + + "LIST_NUMBER corresponds to the exercise command format you want to see"; + public static final String WORKOUT_MESSAGE ="Please enter 'help /workout LIST_NUMBER'. " + + "LIST_NUMBER corresponds to the workout command format you want to see"; + public static final String PROGRAM_MESSAGE = "Please enter 'help /program LIST_NUMBER'. " + + "LIST_NUMBER corresponds to the program command format you want to see"; + public static final String NO_COMMAND_EXCEPTION = "No Command Type Specified"; + public static final String ADDITIONAL_ARGUMENTS_EXCEPTION = "Help menu does not accept additional arguments"; + public static final String INVALID_COMMAND_TYPE = "Command Type Not Recognised"; + public static final String INVALID_COMMAND = "Invalid choice entered: please enter a number from the list"; +} diff --git a/src/main/java/byteceps/ui/strings/ManagerStrings.java b/src/main/java/byteceps/ui/strings/ManagerStrings.java new file mode 100644 index 0000000000..b81497a7cc --- /dev/null +++ b/src/main/java/byteceps/ui/strings/ManagerStrings.java @@ -0,0 +1,110 @@ +package byteceps.ui.strings; + + +public class ManagerStrings { + public static final String ACTIVITY_EXISTS_EXCEPTION = "The %s entry: %s already exists"; + public static final String ACTIVITY_DELETE_EXCEPTION = "The %s entry: %s does not exist and cannot be deleted"; + public static final String ACTIVITY_EMPTY_LIST_EXCEPTION = "Unable to retrieve any %s. It is empty!"; + public static final String ACTIVITY_DOES_NOT_EXIST_EXCEPTION = "The %s entry: %s does not exist"; + public static final String ACTIVITY_EMPTY_LIST = "Your List of %s is Empty"; + public static final String ACTIVITY_LIST = "Listing %s:%s"; + public static final String ACTIVITY_LIST_ITEM = "\t\t\t%d. %s%n"; + public static final String NO_ACTION_EXCEPTION = "No action specified"; + public static final String UNEXPECTED_ACTION = "Unexpected value: %s"; + public static final String SPECIAL_CHARS_PATTERN = ".*[{}\\[\\]/\\\\:,#\\-].*"; + public static final String SPEC_CHAR_EXCEPTION = "%s name cannot contain special characters: " + + "{ } [ ] / \\\\ : , # -"; + public static final String EMPTY_SEARCH = "Search term cannot be empty."; + public static final String NO_RESULTS = "No results found"; + public static final String SEARCH_RESULTS = "Search Results:%s"; + public static final String INVALID_LIST = "Invalid command. Use '%s /list' to list all exercises."; + + // Exercise strings + public static final String EXERCISE = "Exercise"; + public static final String EXERCISES = "Exercises"; + public static final String EXERCISE_EDITED = "Edited Exercise from %s to %s"; + public static final String EXERCISE_DELETED = "Deleted Exercise: %s"; + public static final String EXERCISE_ADDED = "Added Exercise: %s"; + public static final String EXERCISE_NAME_SAME = "No change needed: the exercise name '%s' is already in use."; + public static final String EMPTY_EXCERCISE_NAME = "Exercise name cannot be empty"; + public static final String INCOMPLETE_EDIT = "Edit command not complete. " + + "Please use: exercise /edit /to "; + + public static final String INCOMPLETE_DELETE_EXERCISE = "Delete command not complete. " + + "Please use: exercise /delete "; + + // Workout strings + public static final String WORKOUT = "Workout"; + public static final String WORKOUTS = "Workouts"; + + public static final String WORKOUT_NAME_SAME = "No change needed: the workout name '%s' is already in use."; + public static final String INCOMPLETE_INFO = "Info command not complete. " + + "Please use: workout /info "; + public static final String INCOMPLETE_CREATE = "Create command not complete. " + + "Please use: workout /create "; + public static final String INCOMPLETE_DELETE_WORKOUT = "Delete command not complete." + + "Please use: workout /delete "; + public static final String INCOMPLETE_SEARCH = "Search term cannot be left blank." + + "Please use: workout /search "; + public static final String UNASSIGNED_EXERCISE = "Unassigned Exercise '%s' from Workout Plan '%s'"; + public static final String ASSIGNED_EXERCISE = "Assigned Exercise '%s' to Workout Plan '%s'"; + public static final String WORKOUT_EDITED = "Edited Workout Plan from %s to %s"; + public static final String WORKOUT_DELETED = "Deleted Workout: %s"; + public static final String WORKOUT_ADDED = "Added Workout Plan: %s"; + public static final String INCOMPLETE_ASSIGN = "assign command not complete. " + + "Please use: workout /assign /to "; + public static final String EXERCISE_ALREADY_ASSIGNED = "Exercise already assigned to workout plan"; + public static final String EMPTY_WORKOUT_PLAN = "Your workout plan %s is empty"; + public static final String LIST_WORKOUT_PLAN = "Listing exercises in workout plan '%s':%n"; + public static final String INCOMPLETE_UNASSIGN = "Unassign command not complete. " + + "Please use: workout /unassign /from "; + public static final String EXERCISE_WORKOUT_DOES_NOT_EXIST = "The exercise is not in the workout"; + + // Weekly Program strings + public static final String WEEKLY_PROGRAM = "Weekly Program"; + public static final String INCOMPLETE_PROGRAM_ASSIGN = "Program /assign command not complete. " + + "Please use: program /assign /to "; + public static final String WORKOUT_ALREADY_ASSIGNED = "Workout %s is already assigned to %s. " + + "Please clear it first."; + public static final String WORKOUT_ASSIGNED = "Workout %s assigned to %s"; + public static final String LOG_INCOMPLETE = "Log command not complete. " + + "Please use: program /log /weight " + + "/sets /reps /date ."; + public static final String LOG_SUCCESS = "Successfully logged %s with %s %s and %s %s across %d %s on %s"; + public static final String NO_WORKOUT_ASSIGNED = "There does not seem to be a workout assigned to the date " + + "%s (day: %s). Please assign one first!"; + public static final String NO_WORKOUT_ASSIGNED_TODAY = "There is no workout assigned today (%s)"; + public static final String INVALID_DATE_ENTERED = "Invalid date entered. " + + "The date must be in the format yyyy-mm-dd."; + public static final String PROGRAMS_CLEARED = "All your workouts have been cleared from the week"; + public static final String WORKOUT_CLEARED = "Your workout on %s has been cleared"; + public static final String PROGRAM_LIST = "Your workouts for the week:"; + public static final String PROGRAM_LIST_ITEM = "\t%s: "; + + public static final String INVALID_PROGRAM_LIST = "Invalid command. Use '%s /today' to view the " + + "workout plan for today."; + + public static final String NO_WORKOUT_TO_CLEAR = "There are no workouts scheduled for %s that require clearing."; + + // Logs strings + public static final String WORKOUT_LOGS = "Workout Logs"; + public static final String WORKOUT_LOG = "Workout Log"; + public static final String LOG_INVALID_STATE = "RepsSetsManager is not meant to be executed"; + public static final String INVALID_REPS_SETS = "Invalid weight/reps/sets entered!"; + public static final String LOG_ENTRY_EXERCISE_DOES_NOT_EXIST = "The exercise does not exist and " + + "cannot be removed from log. Actually, you should not be seeing this message hmm..."; + public static final String LOG_LIST = "Listing Exercises on %s:%n"; + public static final String LOG_LIST_ITEM = "\t\t\t\tSet %d: %d kg, %d reps" + + System.lineSeparator(); + public static final String TOO_MANY_ARGS = "Extra arguments detected. Make sure you are not using" + + System.lineSeparator() + "the special character '/' in any of your exercise and workout names," + + System.lineSeparator() + "or in any part of your input outside command arguments."; + public static final String INVALID_WEIGHTS_SETS_MISMATCH = "The number of weights provided (%d) does not match " + + System.lineSeparator() + "the declared number of sets (%d). Each set should have a corresponding weight."; + public static final String INVALID_REPS_SETS_MISMATCH = "The number of repetitions provided (%d) does not match " + + System.lineSeparator() + + "the declared number of sets (%d). Each set should have a corresponding number of repetitions."; + + public static final String OVERWRITE_EXERCISE_LOG = "It seems like %s already exists, overwriting..."; + +} diff --git a/src/main/java/byteceps/ui/strings/StorageStrings.java b/src/main/java/byteceps/ui/strings/StorageStrings.java new file mode 100644 index 0000000000..451a2262d6 --- /dev/null +++ b/src/main/java/byteceps/ui/strings/StorageStrings.java @@ -0,0 +1,26 @@ +package byteceps.ui.strings; + +public class StorageStrings { + public static final String EXERCISE_MANAGER = "exerciseManager"; + public static final String WORKOUT_MANAGER = "workoutManager"; + public static final String WEEKLY_PROGRAM = "weeklyProgram"; + public static final String WORKOUT_LOG_MANAGER = "WorkoutLogManager"; + public static final String WORKOUTS_SAVED = "All your workouts and exercises have been saved."; + public static final String NO_SAVE_DATA = "Looks like you're starting fresh!"; + public static final String LOADING = "Loading your exercises..."; + public static final String LOAD_SUCCESS = "Data loaded successfully!"; + public static final String LOAD_ERROR = "Error: Error processing JSON file. Starting with a fresh JSON file."; + public static final String BACKUP_DATE_FORMAT = "yyyyMMdd_HHmmss"; + public static final String OLD_SUFFIX = ".old_"; + public static final String NEW_JSON_ERROR = "Error: Unable to create a new JSON file."; + public static final String ACTIVITY_NAME = "activityName"; + public static final String EXERCISE_LIST = "exerciseList"; + public static final String EXERCISE_NAME = "exerciseName"; + public static final String WEIGHT = "weight"; + public static final String SETS = "sets"; + public static final String REPS = "reps"; + public static final String WORKOUT_DATE = "workoutDate"; + public static final String WORKOUT_NAME = "workoutName"; + public static final String EXERCISES = "exercises"; + +} diff --git a/src/main/java/byteceps/ui/strings/UiStrings.java b/src/main/java/byteceps/ui/strings/UiStrings.java new file mode 100644 index 0000000000..e34497061e --- /dev/null +++ b/src/main/java/byteceps/ui/strings/UiStrings.java @@ -0,0 +1,14 @@ +package byteceps.ui.strings; + +public class UiStrings { + public static final String BYTECEP_PROMPT = "[BYTE-CEPS]> "; + public static final String BYTECEP_PROMPT_FORMAT = "%s%s%s"; + public static final String USER_PROMPT = "[User]> "; + public static final String USER_PROMPT_FORMAT = "%s"; + public static final String MESSAGE_WELCOME = "WELCOME TO BYTECEPS"; + public static final String MESSAGE_GOODBYE = "GOODBYE FOR NOW. STAY HARD!"; + public static final String SEPARATOR = "-------------------------------------------------"; + public static final String ERROR_STRING = "Error: %s"; + public static final String MORE_SLASHES_THAN_ARGS = "More slashes than arguments"; + +} diff --git a/src/main/java/byteceps/validators/ExerciseValidator.java b/src/main/java/byteceps/validators/ExerciseValidator.java new file mode 100644 index 0000000000..9b274efb6e --- /dev/null +++ b/src/main/java/byteceps/validators/ExerciseValidator.java @@ -0,0 +1,86 @@ +package byteceps.validators; + + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.ManagerStrings; + + +public class ExerciseValidator extends Validator { + //@@author V4vern + public static String validateCommand(Parser parser) throws Exceptions.InvalidInput { + assert parser != null : "Parser must not be null"; + assert parser.getAction() != null : "Command action must not be null"; + + if (parser.getAction().isEmpty()) { + throw new Exceptions.InvalidInput(ManagerStrings.NO_ACTION_EXCEPTION); + } + + String action = parser.getAction(); + switch (action) { + case CommandStrings.ACTION_EDIT: + validateEditAction(parser); + break; + case CommandStrings.ACTION_ADD: + validateAddAction(parser); + break; + case CommandStrings.ACTION_DELETE: + validateDeleteAction(parser); + break; + case CommandStrings.ACTION_LIST: + validateListAction(parser); + break; + case CommandStrings.ACTION_SEARCH: + validateSearchAction(parser); + break; + default: + throw new Exceptions.InvalidInput(String.format(ManagerStrings.UNEXPECTED_ACTION, parser.getAction())); + } + return action; + } + + private static void validateDeleteAction(Parser parser) throws Exceptions.InvalidInput { + String exerciseToBeDeleted = parser.getActionParameter(); + if (hasNoInput(exerciseToBeDeleted)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_DELETE_EXERCISE); + } + validateNumAdditionalArgs(0, 0, parser); + } + + private static void validateAddAction(Parser parser) throws Exceptions.InvalidInput { + String exerciseName = parser.getActionParameter(); + if (hasNoInput(exerciseName)) { + throw new Exceptions.InvalidInput(ManagerStrings.EMPTY_EXCERCISE_NAME); + } + + if (exerciseName.matches(ManagerStrings.SPECIAL_CHARS_PATTERN)) { + throw new Exceptions.InvalidInput( + String.format(ManagerStrings.SPEC_CHAR_EXCEPTION, CommandStrings.COMMAND_EXERCISE)); + } + validateNumAdditionalArgs(0, 0, parser); + } + + //@@author LWachtel1 + private static void validateEditAction(Parser parser) throws Exceptions.InvalidInput { + String oldExerciseName = parser.getActionParameter(); + String newExerciseName = parser.getAdditionalArguments(CommandStrings.ARG_TO); + if (hasNoInput(oldExerciseName) || hasNoInput(newExerciseName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_EDIT); + } + if (newExerciseName.matches(ManagerStrings.SPECIAL_CHARS_PATTERN)) { + throw new Exceptions.InvalidInput( + String.format(ManagerStrings.SPEC_CHAR_EXCEPTION, CommandStrings.COMMAND_EXERCISE)); + } + validateNumAdditionalArgs(1, 1, parser); + } + + //@@author V4vern + private static void validateSearchAction(Parser parser) throws Exceptions.InvalidInput { + String searchTerm = parser.getActionParameter(); + if (hasNoInput(searchTerm)) { + throw new Exceptions.InvalidInput(ManagerStrings.EMPTY_SEARCH); + } + validateNumAdditionalArgs(0, 0, parser); + } +} diff --git a/src/main/java/byteceps/validators/HelpValidator.java b/src/main/java/byteceps/validators/HelpValidator.java new file mode 100644 index 0000000000..3585e4debf --- /dev/null +++ b/src/main/java/byteceps/validators/HelpValidator.java @@ -0,0 +1,36 @@ +package byteceps.validators; + +import byteceps.errors.Exceptions; +import byteceps.ui.strings.HelpStrings; +import byteceps.commands.Parser; + +/** + * Parses user input to help menu to ensure input validity for help menu methods and throw exceptions upon detection of + * invalid input. + * */ +//@@author LWachtel1 +public class HelpValidator extends Validator{ + + /** + * Parses user input for HelpMenuManager's execute() method to ensure a non-empty command action and the correct + * number of arguments. + * + * @param parser Provides user input to be parsed by method. + * */ + //@@author LWachtel1 + public static void validateCommand(Parser parser) throws Exceptions.InvalidInput { + assert parser != null : "Parser must not be null"; + assert parser.getAction() != null : "Command action must not be null"; + + String commandAction = parser.getAction(); + if (commandAction.isEmpty()) { + throw new Exceptions.InvalidInput(HelpStrings.NO_COMMAND_EXCEPTION); + } + + try { + validateNumAdditionalArgs(0, 0, parser); + } catch (Exceptions.InvalidInput e) { + throw new Exceptions.InvalidInput(HelpStrings.ADDITIONAL_ARGUMENTS_EXCEPTION); + } + } +} diff --git a/src/main/java/byteceps/validators/Validator.java b/src/main/java/byteceps/validators/Validator.java new file mode 100644 index 0000000000..a224802abc --- /dev/null +++ b/src/main/java/byteceps/validators/Validator.java @@ -0,0 +1,28 @@ +package byteceps.validators; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.ManagerStrings; + +public abstract class Validator { + protected static boolean hasNoInput(String input) { + return input == null || input.isEmpty(); + } + + protected static void validateListAction(Parser parser) throws Exceptions.InvalidInput { + validateNumAdditionalArgs(0, 0, parser); + if (!parser.getActionParameter().isEmpty()) { + throw new Exceptions.InvalidInput( + String.format(ManagerStrings.INVALID_LIST, parser.getCommand()) + ); + } + } + + protected static void validateNumAdditionalArgs(int minNumArgs, int maxNumArgs, Parser parser) + throws Exceptions.InvalidInput { + int numArgs = parser.getNumAdditionalArguments(); + if(numArgs < minNumArgs || numArgs > maxNumArgs) { + throw new Exceptions.InvalidInput(ManagerStrings.TOO_MANY_ARGS); + } + } +} diff --git a/src/main/java/byteceps/validators/WeeklyProgramValidator.java b/src/main/java/byteceps/validators/WeeklyProgramValidator.java new file mode 100644 index 0000000000..d9b80e9256 --- /dev/null +++ b/src/main/java/byteceps/validators/WeeklyProgramValidator.java @@ -0,0 +1,122 @@ +package byteceps.validators; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.ManagerStrings; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +public class WeeklyProgramValidator extends Validator { + //@@author pqienso + public static String validateCommand(Parser parser) throws Exceptions.InvalidInput { + assert parser != null : "Parser must not be null"; + String commandAction = parser.getAction(); + assert commandAction != null : "Command action must not be null"; + if (commandAction.isEmpty()) { + throw new Exceptions.InvalidInput("No action specified"); + } + + switch (commandAction) { + case CommandStrings.ACTION_ASSIGN: + validateAssignAction(parser); + break; + case CommandStrings.ACTION_CLEAR: + validateClearAction(parser); + break; + case CommandStrings.ACTION_TODAY: + validateTodayAction(parser); + break; + case CommandStrings.ACTION_LOG: + validateLogAction(parser); + break; + case CommandStrings.ACTION_LIST: + validateListAction(parser); + break; + case CommandStrings.ACTION_HISTORY: + validateHistoryAction(parser); + break; + default: + throw new Exceptions.InvalidInput(String.format(ManagerStrings.UNEXPECTED_ACTION, commandAction)); + } + + return commandAction; + } + //@@author joshualeejunyi + private static void validateAssignAction(Parser parser) throws Exceptions.InvalidInput { + String workoutName = parser.getActionParameter(); + String day = parser.getAdditionalArguments(CommandStrings.ARG_TO); + if (hasNoInput(workoutName) || hasNoInput(day)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_PROGRAM_ASSIGN); + } + validateNumAdditionalArgs(1, 1, parser); + } + + private static void validateClearAction(Parser parser) throws Exceptions.InvalidInput { + validateNumAdditionalArgs(0, 0, parser); + } + + private static void validateLogAction(Parser parser) throws Exceptions.InvalidInput { + String weight = parser.getAdditionalArguments(CommandStrings.ARG_WEIGHT); + String reps = parser.getAdditionalArguments(CommandStrings.ARG_REPS); + String sets = parser.getAdditionalArguments(CommandStrings.ARG_SETS); + String exerciseName = parser.getActionParameter(); + String date = parser.getAdditionalArguments(CommandStrings.ARG_DATE); + + if (hasNoInput(weight) || hasNoInput(reps) || hasNoInput(sets) || hasNoInput(exerciseName)) { + throw new Exceptions.InvalidInput(ManagerStrings.LOG_INCOMPLETE); + } + + if(!hasNoInput(date)) { + try { + LocalDate.parse(date); + } catch (DateTimeParseException e) { + throw new Exceptions.InvalidInput(ManagerStrings.INVALID_DATE_ENTERED); + } + } + validateWeightsRepsSets(sets, weight, reps); + validateNumAdditionalArgs(3, 4, parser); + } + + private static void validateWeightsRepsSets(String sets, String weight, String reps) + throws Exceptions.InvalidInput { + int setsInt = 0; + try { + setsInt = Integer.parseInt(sets); + if (setsInt < 0) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + throw new Exceptions.InvalidInput(ManagerStrings.INVALID_REPS_SETS); + } + + int weightsCount = weight.split(" ").length; + int repsCount = reps.split(" ").length; + + // Validate sets against weights + if (weightsCount != setsInt) { + throw new Exceptions.InvalidInput(String.format(ManagerStrings.INVALID_WEIGHTS_SETS_MISMATCH, + weightsCount, setsInt)); + } + + // Validate sets against repetitions + if (repsCount != setsInt) { + throw new Exceptions.InvalidInput(String.format(ManagerStrings.INVALID_REPS_SETS_MISMATCH, + repsCount, setsInt)); + } + } + + private static void validateTodayAction(Parser parser) throws Exceptions.InvalidInput { + if (!parser.getActionParameter().isEmpty()) { + throw new Exceptions.InvalidInput( + String.format(ManagerStrings.INVALID_PROGRAM_LIST, parser.getCommand()) + ); + } + validateNumAdditionalArgs(0, 0, parser); + } + + private static void validateHistoryAction(Parser parser) throws Exceptions.InvalidInput { + validateNumAdditionalArgs(0, 0, parser); + } +} diff --git a/src/main/java/byteceps/validators/WorkoutLogsValidator.java b/src/main/java/byteceps/validators/WorkoutLogsValidator.java new file mode 100644 index 0000000000..4a0745eff8 --- /dev/null +++ b/src/main/java/byteceps/validators/WorkoutLogsValidator.java @@ -0,0 +1,43 @@ +package byteceps.validators; + +import byteceps.activities.WorkoutLog; +import byteceps.errors.Exceptions; +import byteceps.processing.ExerciseManager; +import byteceps.ui.UserInterface; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.ManagerStrings; + +import java.util.List; + +public class WorkoutLogsValidator extends Validator { + public static void exerciseExists(ExerciseManager exerciseManager, String exerciseName) + throws Exceptions.ActivityDoesNotExist { + if (exerciseManager.doesNotHaveActivity(exerciseName)) { + throw new Exceptions.ActivityDoesNotExist( + String.format(ManagerStrings.ACTIVITY_DOES_NOT_EXIST_EXCEPTION, + CommandStrings.COMMAND_EXERCISE, exerciseName) + ); + } + } + + public static void hasNegativeInput(List weightsList, List repsList, int setsInt) { + boolean hasNegativeWeights = weightsList.stream().anyMatch(weightInt -> weightInt < 0); + boolean hasNegativeReps = repsList.stream().anyMatch(repInt -> repInt < 0); + + if (hasNegativeReps || setsInt < 0 || hasNegativeWeights) { + throw new NumberFormatException(); + } + } + + public static void removeExerciseIfLogExists(WorkoutLog workoutLog, String exerciseName) + throws Exceptions.ActivityDoesNotExist { + if (!workoutLog.hasExerciseName(exerciseName)) { + return; // does not exist and should not do anything + } + + // technically should not print here, but it would be too many layers back to return a message + UserInterface ui = UserInterface.getInstance(); + ui.printMessageNoSeparator(String.format(ManagerStrings.OVERWRITE_EXERCISE_LOG, exerciseName)); + workoutLog.removeExistingLogEntry(exerciseName); + } +} diff --git a/src/main/java/byteceps/validators/WorkoutValidator.java b/src/main/java/byteceps/validators/WorkoutValidator.java new file mode 100644 index 0000000000..ed7e480825 --- /dev/null +++ b/src/main/java/byteceps/validators/WorkoutValidator.java @@ -0,0 +1,128 @@ +package byteceps.validators; + + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.CommandStrings; +import byteceps.ui.strings.ManagerStrings; + + +public class WorkoutValidator extends Validator { + //@@author V4vern + public static String validateCommand(Parser parser) throws Exceptions.InvalidInput { + assert parser != null : "Parser must not be null"; + assert parser.getAction() != null : "Command action must not be null"; + String command = parser.getAction(); + if (command.isEmpty()) { + throw new Exceptions.InvalidInput(ManagerStrings.NO_ACTION_EXCEPTION); + } + + switch (command) { + case CommandStrings.ACTION_CREATE: + validateCreateAction(parser); + break; + case CommandStrings.ACTION_DELETE: + validateDeleteAction(parser); + break; + case CommandStrings.ACTION_EDIT: + validateEditAction(parser); + break; + case CommandStrings.ACTION_ASSIGN: + validateAssignAction(parser); + break; + case CommandStrings.ACTION_UNASSIGN: + validateUnassignAction(parser); + break; + case CommandStrings.ACTION_INFO: + validateInfoAction(parser); + break; + case CommandStrings.ACTION_LIST: + validateListAction(parser); + break; + case CommandStrings.ACTION_SEARCH: + validateSearchAction(parser); + break; + default: + throw new Exceptions.InvalidInput(String.format(ManagerStrings.UNEXPECTED_ACTION, parser.getAction())); + } + + return command; + } + + //@@author pqienso + private static void validateInfoAction(Parser parser) throws Exceptions.InvalidInput { + assert parser.getAction().equals(CommandStrings.ACTION_INFO) : "Action must be info"; + String workoutName = parser.getActionParameter(); + if (hasNoInput(workoutName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_INFO); + } + validateNumAdditionalArgs(0, 0, parser); + } + + private static void validateCreateAction(Parser parser) throws Exceptions.InvalidInput { + String createdWorkoutName = parser.getActionParameter(); + if (hasNoInput(createdWorkoutName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_CREATE); + } + + if (createdWorkoutName.matches(ManagerStrings.SPECIAL_CHARS_PATTERN)) { + throw new Exceptions.InvalidInput( + String.format(ManagerStrings.SPEC_CHAR_EXCEPTION, CommandStrings.COMMAND_WORKOUT)); + } + + validateNumAdditionalArgs(0, 0, parser); + } + + private static void validateDeleteAction(Parser parser) throws Exceptions.InvalidInput { + String toDeleteWorkoutName = parser.getActionParameter(); + if (hasNoInput(toDeleteWorkoutName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_DELETE_WORKOUT); + } + validateNumAdditionalArgs(0, 0, parser); + } + + //@@author V4vern + private static void validateEditAction(Parser parser) throws Exceptions.InvalidInput { + String newWorkoutName = parser.getAdditionalArguments(CommandStrings.ARG_TO); + String oldWorkoutName = parser.getActionParameter(); + if (hasNoInput(newWorkoutName) || hasNoInput(oldWorkoutName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_EDIT); + } + + if (newWorkoutName.matches(ManagerStrings.SPECIAL_CHARS_PATTERN)) { + throw new Exceptions.InvalidInput( + String.format(ManagerStrings.SPEC_CHAR_EXCEPTION, CommandStrings.COMMAND_WORKOUT)); + } + validateNumAdditionalArgs(1, 1, parser); + } + + //@@author V4vern + private static void validateAssignAction(Parser parser) throws Exceptions.InvalidInput { + String exerciseName = parser.getActionParameter(); + String workoutPlanName = parser.getAdditionalArguments(CommandStrings.ARG_TO); + if (hasNoInput(exerciseName) || hasNoInput(workoutPlanName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_ASSIGN); + } + validateNumAdditionalArgs(1, 1, parser); + } + + //@@author V4vern + private static void validateUnassignAction(Parser parser) throws Exceptions.InvalidInput { + String workoutPlanName = parser.getAdditionalArguments(CommandStrings.ARG_FROM); + String exerciseName = parser.getActionParameter(); + if (hasNoInput(workoutPlanName) || hasNoInput(exerciseName)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_UNASSIGN); + } + validateNumAdditionalArgs(1, 1, parser); + } + + //@@author V4vern + private static void validateSearchAction(Parser parser) throws Exceptions.InvalidInput { + String searchTerm = parser.getActionParameter(); + if (hasNoInput(searchTerm)) { + throw new Exceptions.InvalidInput(ManagerStrings.INCOMPLETE_SEARCH); + } + validateNumAdditionalArgs(0, 0, parser); + } + +} 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/byteceps/activities/ActivityTest.java b/src/test/java/byteceps/activities/ActivityTest.java new file mode 100644 index 0000000000..3322f6f2a4 --- /dev/null +++ b/src/test/java/byteceps/activities/ActivityTest.java @@ -0,0 +1,93 @@ +package byteceps.activities; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ActivityTest { + @Test + public void getActivityName_validName_setNewName() { + String activityName = "Running"; + Activity activity = new Activity(activityName); + assertEquals(activityName, activity.getActivityName()); + } + + @Test + public void getActivityName_nullName_setNull() { + String activityName = null; + Activity activity = new Activity(activityName); + assertNull(activity.getActivityName()); + } + + @Test + public void getActivityName_emptyName_setEmptyName() { + String activityName = ""; + Activity activity = new Activity(activityName); + assertEquals(activityName, activity.getActivityName()); + } + + @Test + public void setActivityName_validName_setNewName() { + Activity activity = new Activity("Initial Name"); + activity.setActivityName("Updated Name"); + assertEquals("Updated Name", activity.getActivityName()); + } + + @Test + public void setActivityName_nullName_setNull() { + Activity activity = new Activity("Initial Name"); + activity.setActivityName(null); + assertNull(activity.getActivityName()); + } + + @Test + public void setActivityName_emptyName_setEmptyName() { + Activity activity = new Activity("Initial Name"); + activity.setActivityName(""); + assertEquals("", activity.getActivityName()); + } + + @Test + public void hashCode_sameName_sameHashCode() { + Activity activity1 = new Activity("Running"); + Activity activity2 = new Activity("Running"); + assertEquals(activity1.hashCode(), activity2.hashCode()); + } + + @Test + public void equals_sameName_true() { + Activity activity1 = new Activity("Running"); + Activity activity2 = new Activity("Running"); + assertTrue(activity1.equals(activity2)); + } + + @Test + public void equals_differentName_false() { + Activity activity1 = new Activity("Running"); + Activity activity2 = new Activity("Swimming"); + assertTrue(!activity1.equals(activity2)); + } + + @Test + public void equals_null_false() { + Activity activity1 = new Activity("Running"); + assertTrue(!activity1.equals(null)); + } + + @Test + public void equals_sameObject_true() { + Activity activity1 = new Activity("Running"); + assertTrue(activity1.equals(activity1)); + } + + @Test + public void equals_differentClass_false() { + Activity activity1 = new Activity("Running"); + assertTrue(!activity1.equals(new Object())); + } + + + +} diff --git a/src/test/java/byteceps/activities/ExerciseLogTest.java b/src/test/java/byteceps/activities/ExerciseLogTest.java new file mode 100644 index 0000000000..2b232fb0b2 --- /dev/null +++ b/src/test/java/byteceps/activities/ExerciseLogTest.java @@ -0,0 +1,84 @@ +package byteceps.activities; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class ExerciseLogTest { + + private ExerciseLog exerciseLog; + private List weights; + private List repetitions; + private final int sets = 3; + private final String exerciseName = "Deadlift"; + + @BeforeEach + void setUp() { + weights = Arrays.asList(100, 105, 110); + repetitions = Arrays.asList(10, 8, 6); + exerciseLog = new ExerciseLog(exerciseName, weights, sets, repetitions); + } + + @Test + public void getSets_whenCalled_returnsCorrectNumberOfSets() { + ExerciseLog exerciseLog = new ExerciseLog("Bench Press", List.of(100, 100, 100), 3, List.of(10, 10, 10)); + assertEquals(3, exerciseLog.getSets()); + } + + @Test + public void getWeights_whenCalled_returnsCorrectWeightsList() { + assertEquals(weights, exerciseLog.getWeights()); + } + + @Test + public void getRepetitions_whenCalled_returnsCorrectRepetitionsList() { + assertEquals(repetitions, exerciseLog.getRepetitions()); + } + + + @Test + public void equals_withSameExerciseLog_returnsTrue() { + ExerciseLog otherLog = new ExerciseLog(exerciseName, weights, sets, repetitions); + assertEquals(otherLog, otherLog, "ExerciseLogs should be equal"); + } + + @Test + public void equals_withDifferentWeights_returnsFalse() { + List differentWeights = Arrays.asList(90, 95, 100); + ExerciseLog otherLog = new ExerciseLog(exerciseName, differentWeights, sets, repetitions); + assertNotEquals(exerciseLog, otherLog); + } + + @Test + void equals_withDifferentRepetitions_returnsFalse() { + List differentRepetitions = Arrays.asList(12, 10, 8); + ExerciseLog otherLog = new ExerciseLog(exerciseName, weights, sets, differentRepetitions); + assertNotEquals(exerciseLog, otherLog); + } + + @Test + void equals_withDifferentSets_returnsFalse() { + ExerciseLog otherLog = new ExerciseLog(exerciseName, weights, 4, repetitions); + assertNotEquals(exerciseLog, otherLog); + } + + @Test + void equals_withDifferentExerciseName_returnsFalse() { + ExerciseLog otherLog = new ExerciseLog("Squat", weights, sets, repetitions); + assertNotEquals(exerciseLog, otherLog); + } + + @Test + void equals_withDifferentObject_returnsFalse() { + assertNotEquals(exerciseLog, new Object()); + } + + @Test + void equals_withNull_returnsFalse() { + assertNotEquals(exerciseLog, null); + } +} diff --git a/src/test/java/byteceps/activities/ExerciseTest.java b/src/test/java/byteceps/activities/ExerciseTest.java new file mode 100644 index 0000000000..0a87650126 --- /dev/null +++ b/src/test/java/byteceps/activities/ExerciseTest.java @@ -0,0 +1,75 @@ +package byteceps.activities; +import byteceps.commands.Parser; +import byteceps.processing.ActivityManager; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +class ExerciseTest { + + private static class TestActivityManager extends ActivityManager { + @Override + public String execute(Parser parser) { + return null; + } + + @Override + public String getActivityType(boolean plural) { + return "Exercise"; + } + } + + + @Test + public void toString_activityName_returnsActivityName() { + String exerciseName = "Bench Press"; + Exercise exercise = new Exercise(exerciseName); + + assertEquals(exerciseName, exercise.toString()); + } + + + @Test + public void editExerciseName_validName_setNewName() { + String initialName = "Push-ups"; + Exercise exercise = new Exercise(initialName); + + String newName = "Pull-ups"; + ActivityManager activityManager = new TestActivityManager(); + exercise.editExerciseName(newName,activityManager); + assertEquals(newName, exercise.getActivityName()); + } + + @Test + public void editExerciseName_emptyName_setEmptyName() { + String initialName = "Squats"; + Exercise exercise = new Exercise(initialName); + + String newName = ""; + ActivityManager activityManager = new TestActivityManager(); + exercise.editExerciseName(newName,activityManager); + assertEquals(newName, exercise.getActivityName()); + } + + @Test + public void editExerciseName_nullName_setNull() { + String initialName = "Deadlifts"; + Exercise exercise = new Exercise(initialName); + + String newName = null; + ActivityManager activityManager = new TestActivityManager(); + exercise.editExerciseName(newName,activityManager); + assertNull(exercise.getActivityName()); + } + + @Test + public void editExerciseName_sameName_noChange() { + ActivityManager activityManager = new TestActivityManager(); + Exercise exercise = new Exercise("Walking"); + exercise.editExerciseName("Walking", activityManager); + assertEquals("Walking", exercise.getActivityName()); + } + +} diff --git a/src/test/java/byteceps/activities/WorkoutLogTest.java b/src/test/java/byteceps/activities/WorkoutLogTest.java new file mode 100644 index 0000000000..70adc53b8f --- /dev/null +++ b/src/test/java/byteceps/activities/WorkoutLogTest.java @@ -0,0 +1,65 @@ +package byteceps.activities; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashSet; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WorkoutLogTest { + private WorkoutLog workoutLog; + + @BeforeEach + void setUp() { + workoutLog = new WorkoutLog("2024-03-28", "Leg Day"); + } + + @Test + void addExerciseLog_addingExerciseLogs_exerciseLogsUpdated() { + ExerciseLog exerciseLog1 = new ExerciseLog("Squat", List.of(100, 105), 2, List.of(10, 8)); + ExerciseLog exerciseLog2 = new ExerciseLog("Deadlift", List.of(120, 125), 2, List.of(5, 5)); + workoutLog.addExerciseLog(exerciseLog1); + workoutLog.addExerciseLog(exerciseLog2); + assertTrue(workoutLog.getExerciseLogs().contains(exerciseLog1)); + assertTrue(workoutLog.getExerciseLogs().contains(exerciseLog2)); + assertEquals(2, workoutLog.getExerciseLogs().size()); + } + + @Test + void addExerciseLog_addingDuplicateLogs_onlyUniqueLogsStored() { + ExerciseLog exerciseLog = new ExerciseLog("Squat", List.of(100, 105), 2, List.of(10, 8)); + workoutLog.addExerciseLog(exerciseLog); + workoutLog.addExerciseLog(exerciseLog); + assertEquals(1, workoutLog.getExerciseLogs().size()); + } + + @Test + void getWorkoutName_workoutName_workoutNameReturned() { + assertEquals("Leg Day", workoutLog.getWorkoutName()); + } + + + @Test + void getWorkoutDate_whenCalled_returnsCorrectWorkoutDate() { + assertEquals("2024-03-28", workoutLog.getWorkoutDate()); + } + + @Test + void getExerciseLogs_whenCalled_returnsExerciseLogs() { + ExerciseLog exerciseLog = new ExerciseLog("Squat", List.of(100), 1, List.of(10)); + workoutLog.addExerciseLog(exerciseLog); + + LinkedHashSet expectedLogs = new LinkedHashSet<>(); + expectedLogs.add(exerciseLog); + + assertEquals(expectedLogs, workoutLog.getExerciseLogs()); + } + + @Test + void getExerciseLogs_noLogsAdded_returnsEmptySet() { + assertTrue(workoutLog.getExerciseLogs().isEmpty()); + } +} diff --git a/src/test/java/byteceps/activities/WorkoutTest.java b/src/test/java/byteceps/activities/WorkoutTest.java new file mode 100644 index 0000000000..651e91304f --- /dev/null +++ b/src/test/java/byteceps/activities/WorkoutTest.java @@ -0,0 +1,167 @@ +package byteceps.activities; + +import byteceps.processing.ActivityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WorkoutTest { + + private Workout workout; + private TestActivityManager testManager; + + @BeforeEach + void setUp() { + workout = new Workout("InitialName"); + testManager = new TestActivityManager(); + } + + @Nested + class WorkoutToStringTests { + @Test + void testToStringWithNoExercises() { + Workout emptyWorkout = new Workout("Empty Routine"); + String expected = "\tEmpty Routine" + System.lineSeparator(); // Using system-dependent newline + assertEquals(expected, emptyWorkout.toString(1)); + } + + @Test + void testToStringWithExercises() { + workout.addExercise(new Exercise("Push-ups")); + workout.addExercise(new Exercise("Sit-ups")); + String expected = "\tInitialName" + System.lineSeparator() + + "\t\t1. Push-ups" + System.lineSeparator() + + "\t\t2. Sit-ups" + System.lineSeparator(); + assertEquals(expected, workout.toString(1)); + } + + @Test + void testToStringWithMultipleTabs() { + workout.addExercise(new Exercise("Push-ups")); + workout.addExercise(new Exercise("Sit-ups")); + String expected = "\t\t\tInitialName" + System.lineSeparator() + + "\t\t\t\t1. Push-ups" + System.lineSeparator() + + "\t\t\t\t2. Sit-ups" + System.lineSeparator(); + assertEquals(expected, workout.toString(3)); + } + } + + @Nested + public class TestActivityManager extends ActivityManager { + public LinkedHashSet updates = new LinkedHashSet<>(); + + @Override + public String execute(byteceps.commands.Parser parser) { + return null; + } + + @Override + public String getActivityType(boolean plural) { + return plural ? "workouts" : "workout"; + } + + @Override + public void updateActivitySet(Activity activityToRemove, Activity activityToAdd) { + super.updateActivitySet(activityToRemove, activityToAdd); + updates.add(activityToAdd); + } + + @BeforeEach + void setUp() { + workout = new Workout("InitialName"); + testManager = new TestActivityManager(); + } + + @Test + public void editWorkoutName_workoutWithInitialName_success() { + workout.editWorkoutName("NewName", testManager); + assertEquals("NewName", workout.getActivityName()); + } + + @Test + public void editWorkoutName_emptyStringName_success() { + workout.editWorkoutName("", testManager); + assertEquals("", workout.getActivityName()); + assertTrue(testManager.updates.contains(new Workout(""))); + } + + @Test + public void editWorkoutName_multipleValidChanges_success() { + String[] names = {"Cardio Blast", "Strength Training", "Endurance"}; + for (String name : names) { + workout.editWorkoutName(name, testManager); + assertEquals(name, workout.getActivityName()); + assertTrue(testManager.updates.contains(new Workout(name))); + } + } + + @Test + public void editWorkoutName_nullName_setsNameToNull() { + workout.editWorkoutName(null, testManager); + assertNull(workout.getActivityName()); + } + + + @Test + public void addExercise_addSingleExerciseToWorkout_success() { + Workout workout = new Workout("Push Day"); + Exercise exercise = new Exercise("Bench Press"); + + workout.addExercise(exercise); + + ArrayList expectedList = new ArrayList<>(); + expectedList.add(exercise); + assertEquals(expectedList, workout.getExerciseList()); + } + + @Test + public void addExercise_addMultipleExercisesToWorkout_success() { + Workout workout = new Workout("Push Day"); + + Exercise exercise1 = new Exercise("Bench Press"); + Exercise exercise2 = new Exercise("Overhead Press"); + Exercise exercise3 = new Exercise("Chest Fly"); + + + workout.addExercise(exercise1); + workout.addExercise(exercise2); + workout.addExercise(exercise3); + + + ArrayList expectedList = new ArrayList<>(); + expectedList.add(exercise1); + expectedList.add(exercise2); + expectedList.add(exercise3); + assertEquals(expectedList, workout.getExerciseList()); + } + + @Test + public void getWorkoutList_workoutWithNoExercises_returnsEmptyList() { + Workout workout = new Workout("Push Day"); + assertTrue(workout.getExerciseList().isEmpty()); + } + + @Test + public void getWorkoutList_workoutWithExercises_returnsListOfExercises() { + Workout workout = new Workout("Push Day"); + Exercise exercise1 = new Exercise("Bench Press"); + Exercise exercise2 = new Exercise("Overhead Press"); + workout.addExercise(exercise1); + workout.addExercise(exercise2); + + ArrayList expectedList = new ArrayList<>(); + expectedList.add(exercise1); + expectedList.add(exercise2); + assertEquals(expectedList, workout.getExerciseList()); + } + + } +} + diff --git a/src/test/java/byteceps/commands/ParserTest.java b/src/test/java/byteceps/commands/ParserTest.java new file mode 100644 index 0000000000..88b9621afb --- /dev/null +++ b/src/test/java/byteceps/commands/ParserTest.java @@ -0,0 +1,109 @@ +package byteceps.commands; + +import byteceps.errors.Exceptions; +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.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ParserTest { + + private Parser testParser; + + @BeforeEach + public void setUp() { + testParser = new Parser(); + } + + @Test + public void parseInput_validCommand_success() throws Exceptions.InvalidInput { + String validInput = "exercise /add deadlift"; + testParser.parseInput(validInput); + + String command = testParser.getCommand(); + String action = testParser.getAction(); + String parameter = testParser.getActionParameter(); + + assertEquals(command, "exercise"); + assertEquals(action, "add"); + assertEquals(parameter, "deadlift"); + } + + @Test + public void parseCommand_addExercise_exerciseCommand() { + String validInput = "exercise /add deadlift"; + assertDoesNotThrow(() -> testParser.parseInput(validInput)); + + String outputCommand = testParser.getCommand(); + assertEquals(outputCommand, "exercise"); + } + + @Test + public void hasAdditionalArguments_noAdditionalArguments_false() { + String input = "exercise"; + assertDoesNotThrow(() -> testParser.parseInput(input)); + assertFalse(testParser.hasAdditionalArguments()); + } + + @Test + public void hasAdditionalArguments_multipleAdditionalArguments_true() { + String input = "exercise /add deadlift /reps 10 /weight 150"; + assertDoesNotThrow(() -> testParser.parseInput(input)); + assertTrue(testParser.hasAdditionalArguments()); + assertEquals(2, testParser.getNumAdditionalArguments()); + } + + @Test + public void getAdditionalArguments_validKey_correctValue() { + String input = "exercise /add deadlift /reps 10"; + assertDoesNotThrow(() -> testParser.parseInput(input)); + assertEquals("10", testParser.getAdditionalArguments("reps")); + } + + @Test + public void getAdditionalArguments_multipleKeys_correctValues() { + String input = "exercise /add deadlift /reps 10 /weight 150"; + assertDoesNotThrow(() -> testParser.parseInput(input)); + assertEquals("10", testParser.getAdditionalArguments("reps")); + assertEquals("150", testParser.getAdditionalArguments("weight")); + } + + @Test + public void getAdditionalArgumentsLength_noAdditionalArguments_zero() { + String input = "exercise"; + assertDoesNotThrow(() -> testParser.parseInput(input)); + assertEquals(0, testParser.getAdditionalArgumentsLength()); + } + + @Test + public void getAdditionalArgumentsLength_multipleAdditionalArguments_correctNumber() { + String input = "exercise /add deadlift /reps 10 /weight 150"; + assertDoesNotThrow(() -> testParser.parseInput(input)); + assertEquals(2, testParser.getAdditionalArgumentsLength()); + } + + @Test + public void toString_emptyCommand_emptyDetails() { + assertEquals("COMMAND: " + System.lineSeparator() + System.lineSeparator() + + "ARGUMENTS: " + System.lineSeparator() + "{}", testParser.toString()); + } + + @Test + public void toString_commandWithoutAdditionalArguments_correctOutput() { + assertDoesNotThrow(() -> testParser.parseInput("exercise")); + assertEquals("COMMAND: " + System.lineSeparator() + "exercise" + System.lineSeparator() + + "ARGUMENTS: " + System.lineSeparator() + "{}", testParser.toString()); + } + + @Test + public void toString_commandWithMultipleAdditionalArguments_correctOutput() { + assertDoesNotThrow(() -> testParser.parseInput("exercise /add deadlift /reps 10 /weight 150")); + assertEquals("COMMAND: " + System.lineSeparator() + "exercise" + System.lineSeparator() + + "ARGUMENTS: " + System.lineSeparator() + "{reps=10, weight=150}", testParser.toString()); + } + + +} diff --git a/src/test/java/byteceps/processing/CascadingDeletionProcessorTest.java b/src/test/java/byteceps/processing/CascadingDeletionProcessorTest.java new file mode 100644 index 0000000000..555bc2590a --- /dev/null +++ b/src/test/java/byteceps/processing/CascadingDeletionProcessorTest.java @@ -0,0 +1,115 @@ +package byteceps.processing; + +import byteceps.activities.Workout; +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class CascadingDeletionProcessorTest { + + private Parser parser; + private WorkoutManager workoutManager; + private WeeklyProgramManager weeklyProgramManager; + private ExerciseManager exerciseManager; + + + @BeforeEach + public void setUp() { + parser = new Parser(); + exerciseManager = new ExerciseManager(); + workoutManager = new WorkoutManager(exerciseManager); + WorkoutLogsManager workoutLogsManager = new WorkoutLogsManager(); + weeklyProgramManager = new WeeklyProgramManager(exerciseManager, workoutManager, workoutLogsManager); + } + + @Test + void checkForCascadingDeletions_nonDeleteAction_noActionTaken() { + String validInput = "workout /create test"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + CascadingDeletionProcessor.checkForCascadingDeletions(parser, workoutManager, weeklyProgramManager); + assertEquals(0, workoutManager.getActivityList().size()); + } + + @Test + void checkForCascadingDeletions_deleteExerciseFromWorkouts_exerciseRemoved() throws Exceptions.ErrorAddingActivity, + Exceptions.ActivityExistsException, Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + // Create an exercise and add it to the system + String validInput = "exercise /add test"; + parser.parseInput(validInput); + exerciseManager.execute(parser); + + // Create two workouts + validInput = "workout /create workout1"; + parser.parseInput(validInput); + workoutManager.execute(parser); + validInput = "workout /create workout2"; + parser.parseInput(validInput); + workoutManager.execute(parser); + + // Assign the created exercise 'test' to both workouts + validInput = "workout /assign test /to workout1"; + parser.parseInput(validInput); + workoutManager.execute(parser); + validInput = "workout /assign test /to workout2"; + parser.parseInput(validInput); + workoutManager.execute(parser); + + // Delete the exercise + validInput = "exercise /delete test"; + parser.parseInput(validInput); + exerciseManager.execute(parser); + + // Check cascading deletions in workoutManager + CascadingDeletionProcessor.checkForCascadingDeletions(parser, workoutManager, weeklyProgramManager); + + // Assertions to verify that the exercise 'test' has been removed from all workouts + assertEquals(0, ((Workout) workoutManager.getActivityList().get(0)).getExerciseList().size(), + "Workout1 should have no exercises"); + assertEquals(0, ((Workout) workoutManager.getActivityList().get(1)).getExerciseList().size(), + "Workout2 should have no exercises"); + } + + @Test + void checkForCascadingDeletions_deleteExerciseFromWorkouts_workoutRemoved() throws Exceptions.InvalidInput, + Exceptions.ActivityDoesNotExist, Exceptions.ActivityExistsException, Exceptions.ErrorAddingActivity { + // Create an exercise and add it to the system + String validInput = "exercise /add test"; + parser.parseInput(validInput); + exerciseManager.execute(parser); + + // Create two workouts + validInput = "workout /create workout1"; + parser.parseInput(validInput); + workoutManager.execute(parser); + validInput = "workout /create workout2"; + parser.parseInput(validInput); + workoutManager.execute(parser); + + // Assign the created exercise 'test' to both workouts + validInput = "workout /assign test /to workout1"; + parser.parseInput(validInput); + workoutManager.execute(parser); + validInput = "workout /assign test /to workout2"; + parser.parseInput(validInput); + workoutManager.execute(parser); + + // Delete the workout1 + validInput = "workout /delete workout1"; + parser.parseInput(validInput); + workoutManager.execute(parser); + + // Check cascading deletions in workoutManager + CascadingDeletionProcessor.checkForCascadingDeletions(parser, workoutManager, weeklyProgramManager); + + // Assertions to verify that workout1 has been removed and workout2 remains unaffected + assertEquals(1, workoutManager.getActivityList().size(), "Only workout2 should remain"); + assertEquals(1, ((Workout) workoutManager.getActivityList().get(0)).getExerciseList().size(), + "Workout2 should still have its exercise"); + } + +} diff --git a/src/test/java/byteceps/processing/ExerciseManagerTest.java b/src/test/java/byteceps/processing/ExerciseManagerTest.java new file mode 100644 index 0000000000..7ed4e99065 --- /dev/null +++ b/src/test/java/byteceps/processing/ExerciseManagerTest.java @@ -0,0 +1,315 @@ +package byteceps.processing; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.UserInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ExerciseManagerTest { + private Parser parser; + private ExerciseManager exerciseManager; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private final UserInterface ui = new UserInterface(); + + @BeforeEach + public void setup() { + parser = new Parser(); + exerciseManager = new ExerciseManager(); + } + + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + public void execute_emptyExerciseAction_throwsInvalidInput() { + String emptyInput = ""; + assertDoesNotThrow(() -> parser.parseInput(emptyInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void execute_addValidExercise_success() { + String validInput = "exercise /add Pushups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + } + + @Test + public void execute_addEmptyNameExercise_throwsInvalidInput() { + String emptyInput = "exercise /add"; + assertDoesNotThrow(() -> parser.parseInput(emptyInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void execute_addSpecialCharacterExercise_throwsInvalidInput() { + String invalidInput = "exercise /add Pushups.-"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + + @Test + public void execute_addDuplicateExercise_throwsActivityExists() { + String validInput = "exercise /add Pushups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + assertThrows(Exceptions.ActivityExistsException.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void execute_deleteValidExercise_success() { + String validInput = "exercise /add Pushups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String deleteInput = "exercise /delete Pushups"; + assertDoesNotThrow(() -> parser.parseInput(deleteInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + } + + @Test + public void execute_deleteEmptyNameExercise_throwsInvalidInput() { + String emptyInput = "exercise /delete"; + assertDoesNotThrow(() -> parser.parseInput(emptyInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void execute_deleteInvalidExercise_throwsActivityDoesNotExist() { + String invalidInput = "exercise /delete Run"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput)); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void execute_listExercises_success() { + setUpStreams(); + + String validInput1 = "exercise /add Pushups"; + String validInput2 = "exercise /add Deadlifts"; + + assertDoesNotThrow(() -> parser.parseInput(validInput1)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + assertDoesNotThrow(() -> parser.parseInput(validInput2)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + + ui.printMessage(exerciseManager.getListString()); + String expectedOutput = "[BYTE-CEPS]> Added Exercise: pushups\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Added Exercise: deadlifts\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Listing Exercises:\n" + + "\t\t\t1. pushups\n" + + "\t\t\t2. deadlifts\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+",""), + outContent.toString().replaceAll("\\s+","")); + + restoreStreams(); + } + + @Test + public void execute_invalidExerciseAction_throwsIllegalState() { + String invalidInput = "exercise /unknown"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput)); + + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void executeListAction_noExercises_success() { + setUpStreams(); + + String listInput = "exercise /list"; + assertDoesNotThrow(() -> parser.parseInput(listInput)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]>YourListofExercisesisEmpty\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + + + //@@author LWachtel1 + @Test + public void execute_validExerciseEdit_success() { + setUpStreams(); + + String validInput = "exercise /add Pushups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + ui.printMessage(exerciseManager.getListString()); + + String editedInput = "exercise /edit Pushups /to Push Ups"; + assertDoesNotThrow(() -> parser.parseInput(editedInput)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + ui.printMessage(exerciseManager.getListString()); + + String expectedOutput = "[BYTE-CEPS]> Added Exercise: pushups\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Listing Exercises:\n" + + "\t\t\t1. pushups\n" + + "\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Edited Exercise from pushups to push ups\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Listing Exercises:\n" + + "\t\t\t1. push ups\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+",""), + outContent.toString().replaceAll("\\s+","")); + + restoreStreams(); + } + + //@@author LWachtel1 + @Test + public void execute_invalidExerciseEdit_throwsInvalidInput() { + String invalidInput = "exercise /edit"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + + String invalidInput2 = "exercise /edit non existent"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput2)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + + } + + //@@author LWachtel1 + @Test + public void invalidExerciseEdit_emptyNewExercise_throwsInvalidInput() { + String validInput = "exercise /add Push ups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String editedInput = "exercise /edit Push ups /to"; + assertDoesNotThrow(() -> parser.parseInput(editedInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + //@@author LWachtel1 + @Test + public void invalidExerciseEdit_invalidPreviousName_throwsActivityDoesNotExists() { + String validInput = "exercise /add Push ups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String editedInput = "exercise /edit Pull ups /to Decline Push ups"; + assertDoesNotThrow(() -> parser.parseInput(editedInput)); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> exerciseManager.execute(parser)); + } + + //@@author LWachtel1 + @Test + public void invalidExerciseEdit_emptyPreviousName_throwsActivityDoesNotExists() { + String validInput = "exercise /add Push ups"; + assertDoesNotThrow(() -> parser.parseInput(validInput)); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String editedInput = "exercise /edit /to Decline Push ups"; + assertDoesNotThrow(() -> parser.parseInput(editedInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + //@@author LWachtel1 + @Test + public void execute_invalidFlag_throwsIllegalStateException() { + String invalidInput = "exercise /change Push ups"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + + @Test + public void execute_searchExistingExercise_success() { + setUpStreams(); + + String addInput = "exercise /add Pushups"; + assertDoesNotThrow(() -> parser.parseInput(addInput)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + + String searchInput = "exercise /search Pushups"; + assertDoesNotThrow(() -> parser.parseInput(searchInput)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]> AddedExercise: \n" + + "\t\t\t pushups\n" + + "\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> SearchResults:\n" + + "\t\t\t1. pushups\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void execute_searchNonexistentExercise_emptyResult() { + setUpStreams(); + + String searchInput = "exercise /search Nonexistent"; + assertDoesNotThrow(() -> parser.parseInput(searchInput)); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]>Noresultsfound\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void execute_searchEmptyQuery_throwsInvalidInput() { + String emptyInput = "exercise /search "; + assertDoesNotThrow(() -> parser.parseInput(emptyInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } + + @Test + public void execute_searchInvalidQuery_throwsInvalidInput() { + String invalidInput = "exercise /search /"; + assertThrows(Exceptions.InvalidInput.class, () -> parser.parseInput(invalidInput)); + } + + @Test + public void execute_searchInvalidFlag_throwsInvalidInput() { + String invalidInput = "exercise /search Pushups /to"; + assertDoesNotThrow(() -> parser.parseInput(invalidInput)); + assertThrows(Exceptions.InvalidInput.class, () -> exerciseManager.execute(parser)); + } +} + diff --git a/src/test/java/byteceps/processing/HelpMenuManagerTest.java b/src/test/java/byteceps/processing/HelpMenuManagerTest.java new file mode 100644 index 0000000000..8deb23a8b7 --- /dev/null +++ b/src/test/java/byteceps/processing/HelpMenuManagerTest.java @@ -0,0 +1,136 @@ +package byteceps.processing; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.UserInterface; +import byteceps.ui.strings.HelpStrings; +import byteceps.ui.strings.UiStrings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + + +public class HelpMenuManagerTest { + + private Parser parser; + private HelpMenuManager helpMenuManager; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final UserInterface ui = new UserInterface(); + + @BeforeEach + public void setup() { + parser = new Parser(); + helpMenuManager = new HelpMenuManager(); + } + + public void setupStreams() { + System.setOut(new PrintStream(outContent)); + } + + public void restoreStreams() { + System.setOut(originalOut); + } + + @Test + public void execute_helpOnly_success() { + String helpOnlyInput = "help"; + assertDoesNotThrow(() -> parser.parseInput(helpOnlyInput)); + + setupStreams(); + assertDoesNotThrow(() -> ui.printMessage(helpMenuManager.execute(parser))); + String viewResponse = String.format("%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, HelpStrings.HELP_GUIDANCE_MESSAGE, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(viewResponse, outContent.toString()); + restoreStreams(); + } + + + @Test + public void execute_viewFlagMenu_success() { + String validInputFlagMenu = "help /exercise"; + assertDoesNotThrow(() -> parser.parseInput(validInputFlagMenu)); + + setupStreams(); + assertDoesNotThrow(() -> ui.printMessage(helpMenuManager.execute(parser))); + String flagMenu = String.format("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, + HelpStrings.EXERCISE_MESSAGE, System.lineSeparator(), HelpStrings.HELP_LIST_INDENT, + HelpStrings.EXERCISE_FLAG_FUNCTIONS[0], System.lineSeparator(), HelpStrings.HELP_LIST_INDENT, + HelpStrings.EXERCISE_FLAG_FUNCTIONS[1], System.lineSeparator(), HelpStrings.HELP_LIST_INDENT, + HelpStrings.EXERCISE_FLAG_FUNCTIONS[2], System.lineSeparator(), HelpStrings.HELP_LIST_INDENT, + HelpStrings.EXERCISE_FLAG_FUNCTIONS[3], System.lineSeparator(), System.lineSeparator(), + UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(flagMenu, outContent.toString()); + restoreStreams(); + } + + + @Test + public void execute_viewSpecificCommandFormat_success() { + String validInputSpecificCommandFormat = "help /program 1"; + assertDoesNotThrow(() -> parser.parseInput(validInputSpecificCommandFormat)); + + setupStreams(); + assertDoesNotThrow(() -> ui.printMessage(helpMenuManager.execute(parser))); + String commandFormat = String.format("%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, + HelpStrings.PROGRAM_PARAM_FORMAT[0], System.lineSeparator(), UiStrings.SEPARATOR, + System.lineSeparator()); + assertEquals(commandFormat, outContent.toString()); + restoreStreams(); + } + + @Test + public void execute_invalidFlagNoParam_throwsInvalidInput() { + String invalidFlagInput = "help /schedule"; + assertDoesNotThrow(() -> parser.parseInput(invalidFlagInput)); + + setupStreams(); + String errorMessage = HelpStrings.INVALID_COMMAND_TYPE; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + helpMenuManager.execute(parser)).getMessage()); + restoreStreams(); + } + + @Test + public void execute_invalidFlagValidParam_throwsInvalidInput() { + String invalidFlagInput = "help /schedule 1"; + assertDoesNotThrow(() -> parser.parseInput(invalidFlagInput)); + + setupStreams(); + String errorMessage = HelpStrings.INVALID_COMMAND_TYPE; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + helpMenuManager.execute(parser)).getMessage()); + restoreStreams(); + } + + + @Test + public void execute_outOfBoundsParameterSpecificCommandFormat_returnInvalidCommand() { + String outOfBoundsParamInput = "help /exercise 10"; + assertDoesNotThrow(() -> parser.parseInput(outOfBoundsParamInput)); + + setupStreams(); + String errorMessage = HelpStrings.INVALID_COMMAND; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + helpMenuManager.execute(parser)).getMessage()); + restoreStreams(); + } + + @Test + public void execute_nonNumericalParameterSpecificCommandFormat_returnInvalidCommand() { + String outOfBoundsParamInput = "help /exercise abc"; + assertDoesNotThrow(() -> parser.parseInput(outOfBoundsParamInput)); + + setupStreams(); + String errorMessage = HelpStrings.INVALID_COMMAND; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + helpMenuManager.execute(parser)).getMessage()); + restoreStreams(); + } +} diff --git a/src/test/java/byteceps/processing/WeeklyProgramManagerTest.java b/src/test/java/byteceps/processing/WeeklyProgramManagerTest.java new file mode 100644 index 0000000000..7e426c6934 --- /dev/null +++ b/src/test/java/byteceps/processing/WeeklyProgramManagerTest.java @@ -0,0 +1,446 @@ +package byteceps.processing; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.UserInterface; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.TestInstantiationException; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class WeeklyProgramManagerTest { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private final UserInterface ui = new UserInterface(); + private Parser parser; + private ExerciseManager exerciseManager; + private WorkoutManager workoutManager; + private WeeklyProgramManager weeklyProgramManager; + + @BeforeEach + void setUp() { + parser = new Parser(); + exerciseManager = new ExerciseManager(); + workoutManager = new WorkoutManager(exerciseManager); + WorkoutLogsManager workoutLogsManager = new WorkoutLogsManager(); + weeklyProgramManager = new WeeklyProgramManager(exerciseManager, workoutManager, workoutLogsManager); + + try { + // create dummy exercises and workouts + String[] exerciseInput = {"exercise /add benchpress", "exercise /add deadlift", + "exercise /add barbell squat"}; + for (String input : exerciseInput) { + parser.parseInput(input); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + } + + String[] workoutInput = {"workout /create leg day", "workout /create full day"}; + for (String input : workoutInput) { + parser.parseInput(input); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + } + } catch (Exceptions.InvalidInput e) { + throw new TestInstantiationException("Could not instantiate tests"); + } + } + + @AfterEach + void tearDown() { + } + + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + void execute_assignValidWorkout_success() { + String assignWorkoutInput = "program /assign leg day /to thurs"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_assignDuplicateWorkout_throwsActivityExistsException() { + String assignWorkoutInput = "program /assign leg day /to thurs"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> weeklyProgramManager.execute(parser)); + assertThrows(Exceptions.ActivityExistsException.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_assignInvalidWorkout_throwsActivityDoesNotExist() { + String assignWorkoutInput = "program /assign laze day /to thurs"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_assignBlankWorkout_throwsActivityDoesNotExist() { + String assignWorkoutInput = "program /assign/to thurs"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_assignInvalidDate_throwsInvalidInput() { + String assignWrongWorkoutInput = "program /assign leg day /to wrong day"; + assertDoesNotThrow(() -> parser.parseInput(assignWrongWorkoutInput)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + + String assignWorkoutInput = "program /assign leg day /to 2024-03-11"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_incompleteAssignCommand_throwsInvalidInput() { + String assignWorkoutInput = "program /assign"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_clearWorkout_success() { + setUpStreams(); + String assignWorkoutInput = "program /assign leg day /to thurs"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> weeklyProgramManager.execute(parser)); + + outContent.reset(); + ui.printMessage(weeklyProgramManager.getListString()); + + String expectedAssignedOutput = "[BYTE-CEPS]> Your workouts for the week:\n" + + "\tMONDAY: Rest day\n" + + "\n" + + "\tTUESDAY: Rest day\n" + + "\n" + + "\tWEDNESDAY: Rest day\n" + + "\n" + + "\tTHURSDAY: leg day\n" + + "\n" + + "\tFRIDAY: Rest day\n" + + "\n" + + "\tSATURDAY: Rest day\n" + + "\n" + + "\tSUNDAY: Rest day\n" + + "\n" + + "\n" + + "-------------------------------------------------"; + + assertEquals(expectedAssignedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + String clearWorkoutInput = "program /clear thurs"; + assertDoesNotThrow(() -> parser.parseInput(clearWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + outContent.reset(); + + + ui.printMessage(weeklyProgramManager.getListString()); + String expectedClearOutput = "[BYTE-CEPS]> Your workouts for the week:\n" + + "\tMONDAY: Rest day\n" + + "\n" + + "\tTUESDAY: Rest day\n" + + "\n" + + "\tWEDNESDAY: Rest day\n" + + "\n" + + "\tTHURSDAY: Rest day\n" + + "\n" + + "\tFRIDAY: Rest day\n" + + "\n" + + "\tSATURDAY: Rest day\n" + + "\n" + + "\tSUNDAY: Rest day\n" + + "\n" + + "\n" + + "-------------------------------------------------"; + assertEquals(expectedClearOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + restoreStreams(); + } + + @Test + void execute_clearInvalidDay_throwsInvalidInput() { + String clearWorkoutInput = "program /clear noday"; + assertDoesNotThrow(() -> parser.parseInput(clearWorkoutInput)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + } + + + @Test + void executeHistoryAction_validDate_returnsFormattedWorkout() { + setUpStreams(); + String dateString = LocalDate.now().toString(); + String todayString = LocalDate.now().getDayOfWeek().toString(); + String assignWorkoutInput = String.format("program /assign full day /to %s", todayString); + + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String logInput = "program /log benchpress /weight 50 /sets 1 /reps 5"; + assertDoesNotThrow(() -> parser.parseInput(logInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + outContent.reset(); + + String todayInput = "program /today"; + assertDoesNotThrow(() -> parser.parseInput(todayInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String expectedOutput = String.format("[BYTE-CEPS]> Listing Exercises on %s:\n" + + "1. benchpress\n" + + " Set 1: 50kg, 5 reps\n" + + "-------------------------------------------------", dateString); + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + restoreStreams(); + } + + @Test + void execute_clearAll_success() { + setUpStreams(); + String assignWorkoutInput = "program /assign leg day /to thurs"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String assignWorkoutInput2 = "program /assign full day /to mon"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput2)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + outContent.reset(); + ui.printMessage(weeklyProgramManager.getListString()); + + String expectedAssignedOutput = "[BYTE-CEPS]> Your workouts for the week:\n" + + "\tMONDAY: full day\n" + + "\n" + + "\tTUESDAY: Rest day\n" + + "\n" + + "\tWEDNESDAY: Rest day\n" + + "\n" + + "\tTHURSDAY: leg day\n" + + "\n" + + "\tFRIDAY: Rest day\n" + + "\n" + + "\tSATURDAY: Rest day\n" + + "\n" + + "\tSUNDAY: Rest day\n" + + "\n" + + "\n" + + "-------------------------------------------------"; + + assertEquals(expectedAssignedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + String clearWorkoutInput = "program /clear"; + assertDoesNotThrow(() -> parser.parseInput(clearWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + outContent.reset(); + + ui.printMessage(weeklyProgramManager.getListString()); + String expectedClearOutput = "[BYTE-CEPS]> Your workouts for the week:\n" + + "\tMONDAY: Rest day\n" + + "\n" + + "\tTUESDAY: Rest day\n" + + "\n" + + "\tWEDNESDAY: Rest day\n" + + "\n" + + "\tTHURSDAY: Rest day\n" + + "\n" + + "\tFRIDAY: Rest day\n" + + "\n" + + "\tSATURDAY: Rest day\n" + + "\n" + + "\tSUNDAY: Rest day\n" + + "\n" + + "\n" + + "-------------------------------------------------"; + assertEquals(expectedClearOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + restoreStreams(); + } + + @Test + void log_validLog_success() { + setUpStreams(); + String dateString = LocalDate.now().toString(); + String todayString = LocalDate.now().getDayOfWeek().toString(); + String assignWorkoutInput = String.format("program /assign full day /to %s", todayString); + + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String logInput = "program /log benchpress /weight 60 70 80 /sets 3 /reps 5 8 10"; + assertDoesNotThrow(() -> parser.parseInput(logInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String expectedOutput = String.format("[BYTE-CEPS]> Workout full day assigned to %s\n" + + "-------------------------------------------------" + + "[BYTE-CEPS]> Successfully logged benchpress with weights of 60kg,70kg,80kg and 5,8,10 reps across 3 " + + "sets on %s\n" + + "-------------------------------------------------\n", todayString, dateString); + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + outContent.reset(); + + String todayInput = "program /today"; + assertDoesNotThrow(() -> parser.parseInput(todayInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + expectedOutput = String.format("[BYTE-CEPS]> Listing Exercises on %s:\n" + + "1. benchpress\n" + + " Set 1: 60kg, 5 reps\n" + + " Set 2: 70kg, 8 reps\n" + + " Set 3: 80kg, 10 reps\n" + + "-------------------------------------------------", dateString); + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + restoreStreams(); + } + + @Test + void log_incompleteLog_throwsInvalidInput() { + String todayString = LocalDate.now().getDayOfWeek().toString(); + String assignWorkoutInput = String.format("program /assign full day /to %s", todayString); + + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> weeklyProgramManager.execute(parser)); + + String[] invalidInputs = {"program /log benchpress /weight 500 /sets 5", "program /log benchpress " + + "/weight 500 /reps 5", "program /log benchpress /sets 5 /reps 5", "program /log /weight 500 /sets 5 " + + "/reps 5", "program /log benchpress /weight /sets 5 /reps 5", "program /log benchpress /weight /sets 5" + + " /reps 5", "program /log benchpress /weight 2 /sets /reps 5", "program /log benchpress /weight 2 " + + "/sets 5 /reps ", "program /log benchpress /weight /sets /reps ", "program /log benchpress /weight " + + "/sets /reps abc", "program /log benchpress /weight /sets test /reps 4", "program /log benchpress " + + "/weight abc /sets 3 /reps 4"}; + + for (String input : invalidInputs) { + assertDoesNotThrow(() -> parser.parseInput(input)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + } + } + + + @Test + void log_invalidExerciseLog_throwsActivityDoesNotExist() { + String todayString = LocalDate.now().getDayOfWeek().toString(); + String assignWorkoutInput = String.format("program /assign full day /to %s", todayString); + + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + String logInput = "program /log snooze /weight 500 /sets 1 /reps 5"; + assertDoesNotThrow(() -> parser.parseInput(logInput)); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void log_history_success() { + setUpStreams(); + String dateString = LocalDate.now().toString(); + String todayString = LocalDate.now().getDayOfWeek().toString(); + String assignWorkoutInput = String.format("program /assign full day /to %s", todayString); + String finalAssignWorkoutInput = assignWorkoutInput; + assertDoesNotThrow(() -> parser.parseInput(finalAssignWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String logInput = "program /log benchpress /weight 50 /sets 1 /reps 5"; + assertDoesNotThrow(() -> parser.parseInput(logInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + outContent.reset(); + + String historyInput = "program /history"; + assertDoesNotThrow(() -> parser.parseInput(historyInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String expectedHistory = String.format("[BYTE-CEPS]> Listing Workout Logs: 1. %s\n" + + "-------------------------------------------------", dateString); + + assertEquals(expectedHistory.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + + if (!todayString.equalsIgnoreCase("monday")) { + assignWorkoutInput = "program /assign full day /to monday"; + String finalAssignWorkoutInput1 = assignWorkoutInput; + assertDoesNotThrow(() -> parser.parseInput(finalAssignWorkoutInput1)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + } + + + String logHistoryInput = "program /log benchpress /weight 50 /sets 1 /reps 5 /date 2024-03-25"; + assertDoesNotThrow(() -> parser.parseInput(logHistoryInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + outContent.reset(); + assertDoesNotThrow(() -> parser.parseInput(historyInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + boolean checkContains = outContent.toString().contains(dateString) + && outContent.toString().contains("2024-03-25"); + assertTrue(checkContains); + restoreStreams(); + } + + @Test + void log_historyInvalidDate_throwsInvalidInput() { + String assignWorkoutInput = "program /assign full day /to monday"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> weeklyProgramManager.execute(parser)); + + String logHistoryInput = "program /log benchpress /weight 500 /sets 5 /reps 5 /date 2024-2323-23"; + assertDoesNotThrow(() -> parser.parseInput(logHistoryInput)); + assertThrows(Exceptions.InvalidInput.class, () -> weeklyProgramManager.execute(parser)); + } + + @Test + void execute_list_success() { + setUpStreams(); + String assignWorkoutInput = "program /assign full day /to monday"; + assertDoesNotThrow(() -> parser.parseInput(assignWorkoutInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String listInput = "program /list"; + assertDoesNotThrow(() -> parser.parseInput(listInput)); + assertDoesNotThrow(() -> ui.printMessage(weeklyProgramManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]>Workoutfulldayassignedtomonday\n" + + "-------------------------------------------------"; + + expectedOutput += "[BYTE-CEPS]> Your workouts for the week:\n" + + "\tMONDAY: full day\n" + + "\tTUESDAY: Rest day\n" + + "\tWEDNESDAY: Rest day\n" + + "\tTHURSDAY: Rest day\n" + + "\tFRIDAY: Rest day\n" + + "\tSATURDAY: Rest day\n" + + "\tSUNDAY: Rest day\n" + + "-------------------------------------------------"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + restoreStreams(); + } + +} diff --git a/src/test/java/byteceps/processing/WorkoutLogsManagerTest.java b/src/test/java/byteceps/processing/WorkoutLogsManagerTest.java new file mode 100644 index 0000000000..b56e24f4fa --- /dev/null +++ b/src/test/java/byteceps/processing/WorkoutLogsManagerTest.java @@ -0,0 +1,145 @@ +package byteceps.processing; + +import byteceps.activities.Exercise; +import byteceps.activities.WorkoutLog; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.StorageStrings; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashSet; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + + + +class WorkoutLogsManagerTest { + private WorkoutLogsManager workoutLogsManager; + + + @BeforeEach + void setUp() { + workoutLogsManager = new WorkoutLogsManager(); + } + + + @Test + public void addWorkoutLog_nonDuplicate_success() throws Exceptions.ActivityDoesNotExist { + assertDoesNotThrow(() -> workoutLogsManager.addWorkoutLog("2023-04-01", + "Morning Fitness")); + WorkoutLog log = (WorkoutLog) workoutLogsManager.retrieve("2023-04-01"); + assertNotNull(log); + assertEquals("Morning Fitness", log.getWorkoutName()); + } + + @Test + public void addWorkoutLog_duplicate_success() throws Exceptions.ActivityDoesNotExist { + assertDoesNotThrow(() -> workoutLogsManager.addWorkoutLog("2023-04-01", + "Morning Fitness")); + assertDoesNotThrow(() -> workoutLogsManager.addWorkoutLog("2023-04-01", + "Morning Fitness")); + WorkoutLog log = (WorkoutLog) workoutLogsManager.retrieve("2023-04-01"); + assertNotNull(log); + assertEquals("Morning Fitness", log.getWorkoutName()); + } + + @Test + public void addExerciseLog_validInput_success() throws Exceptions.ActivityDoesNotExist { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + assertDoesNotThrow(() -> + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "100 200", + "2", "10 10") + ); + WorkoutLog log = (WorkoutLog) workoutLogsManager.retrieve("2023-04-01"); + assertFalse(log.getExerciseLogs().isEmpty()); + } + + @Test + public void addExerciseLog_negativeWeight_exceptionThrown() { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + assertThrows(Exceptions.InvalidInput.class, () -> + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "-100 200", + "2", "10 10") + ); + } + + @Test + public void addExerciseLog_negativeReps_exceptionThrown() { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + assertThrows(Exceptions.InvalidInput.class, () -> + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "100 200", + "2", "-10 10") + ); + } + + @Test + public void addExerciseLog_negativeSets_exceptionThrown() { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + assertThrows(Exceptions.InvalidInput.class, () -> + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "100 200", + "-2", "10 10") + ); + } + + @Test + public void addExerciseLog_nonExistentWorkoutLog_throwsActivityDoesNotExists() { + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "100 200", + "2", "10 10") + ); + } + + @Test + public void getWorkoutLog_nonExistentWorkoutLog_throwsActivityDoesNotExists() { + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> + workoutLogsManager.getWorkoutLogString("2023-04-01", new LinkedHashSet<>()) + ); + } + + @Test + public void getWorkoutLog_validInput_success() throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "100 200", + "2", "10 10"); + String workoutLogString = workoutLogsManager.getWorkoutLogString("2023-04-01", new LinkedHashSet<>()); + assertTrue(workoutLogString.contains("Squats")); + } + + @Test + public void getWorkoutLogString_unloggedExercises_returnsCorrectlyAppendedUnloggedExercises() throws + Exceptions.ActivityDoesNotExist { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + LinkedHashSet exercises = new LinkedHashSet<>(); + exercises.add(new Exercise("Squats")); + exercises.add(new Exercise("Deadlifts")); + String workoutLogString = workoutLogsManager.getWorkoutLogString("2023-04-01", exercises); + assertTrue(workoutLogString.contains("Squats")); + assertTrue(workoutLogString.contains("Deadlifts")); + } + + @Test + public void execute_invalidInput_throwsInvalidInput() { + assertThrows(Exceptions.InvalidInput.class, () -> workoutLogsManager.execute(null)); + } + + + @Test + public void exportToJSON_validInput_success() throws Exceptions.ActivityDoesNotExist, Exceptions.InvalidInput { + workoutLogsManager.addWorkoutLog("2023-04-01", "Leg Day"); + workoutLogsManager.addExerciseLog("2023-04-01", "Squats", "100 200", "2", "10 10"); + JSONArray jsonOutput = workoutLogsManager.exportToJSON(); + assertFalse(jsonOutput.isEmpty()); + JSONObject workout = jsonOutput.getJSONObject(0); + assertEquals("2023-04-01", workout.getString(StorageStrings.WORKOUT_DATE)); + JSONArray exercises = workout.getJSONArray(StorageStrings.EXERCISES); + assertEquals(1, exercises.length()); + } + +} diff --git a/src/test/java/byteceps/processing/WorkoutManagerTest.java b/src/test/java/byteceps/processing/WorkoutManagerTest.java new file mode 100644 index 0000000000..8866f85088 --- /dev/null +++ b/src/test/java/byteceps/processing/WorkoutManagerTest.java @@ -0,0 +1,461 @@ +package byteceps.processing; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; + + +import byteceps.ui.UserInterface; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WorkoutManagerTest { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private Parser parser; + private WorkoutManager workoutManager; + private ExerciseManager exerciseManager; + private final UserInterface ui = new UserInterface(); + + + @BeforeEach + void setUp() { + parser = new Parser(); + exerciseManager = new ExerciseManager(); + workoutManager = new WorkoutManager(exerciseManager); + } + + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + @Test + public void execute_createValidWorkout_success() throws Exceptions.InvalidInput { + String validInput = "workout /create LegDay"; + parser.parseInput(validInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + } + + @Test + public void execute_createEmptyNameWorkout_throwsInvalidInput() throws Exceptions.InvalidInput { + String emptyInput = "workout /create"; + parser.parseInput(emptyInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_createDuplicateWorkout_throwsActivityExists() throws Exceptions.InvalidInput { + String validInput = "workout /create LegDay"; + parser.parseInput(validInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + assertThrows(Exceptions.ActivityExistsException.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_deleteExistingWorkoutPlan_success() throws Exceptions.InvalidInput { + String workoutInput = "workout /create chest day"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String deleteInput = "workout /delete chest day"; + parser.parseInput(deleteInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + // Ensure the workout plan is deleted + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.retrieve("LegDay")); + } + + @Test + public void execute_deleteNonExistingWorkoutPlan_throwsActivityDoesNotExists() throws Exceptions.InvalidInput { + String deleteInput = "workout /delete NonExistingWorkout"; + parser.parseInput(deleteInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_deleteEmptyWorkoutPlan_throwsInvalidInput() throws Exceptions.InvalidInput { + String deleteInput = "workout /delete"; + parser.parseInput(deleteInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + + @Test + public void execute_editExistingWorkoutPlan_success() throws Exceptions.InvalidInput { + String workoutInput = "workout /create chest day"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String editInput = "workout /edit chest day /to chest day 2"; + parser.parseInput(editInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + assertDoesNotThrow(() -> workoutManager.retrieve("chest day 2")); + } + + @Test + public void execute_editNonExistingWorkoutPlan_throwsActivityDoesNotExists() throws Exceptions.InvalidInput { + String editInput = "workout /edit NonExistingWorkout /to chest day 2"; + parser.parseInput(editInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_editEmptyWorkoutPlan_throwsInvalidInput() throws Exceptions.InvalidInput { + String editInput = "workout /edit /to chest day 2"; + parser.parseInput(editInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + + @Test + public void execute_assignExerciseToWorkout_success() throws Exceptions.InvalidInput { + String exerciseInput = "exercise /add Squat"; + parser.parseInput(exerciseInput); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String workoutInput = "workout /create legday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String assignInput = "workout /assign Squat /to legday"; + parser.parseInput(assignInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + } + + @Test + public void execute_assignExerciseToNonexistentWorkout_throwsActivityDoesNotExist() throws Exceptions.InvalidInput { + String exerciseInput = "exercise /add Squat"; + parser.parseInput(exerciseInput); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String validInput = "workout /assign Squat /to NonexistentWorkout"; + parser.parseInput(validInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_assignExerciseAlreadyAssigned_throwsActivityExistsException() throws Exceptions.InvalidInput { + String exerciseInput = "exercise /add Squat"; + parser.parseInput(exerciseInput); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String workoutInput = "workout /create legday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String assignInput = "workout /assign Squat /to legday"; + parser.parseInput(assignInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + parser.parseInput(assignInput); + assertThrows(Exceptions.ActivityExistsException.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_assignExerciseNonexistentExercise_throwsActivityDoesNotExists() throws Exceptions.InvalidInput { + String workoutInput = "workout /create legday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String assignInput = "workout /assign NonexistentExercise /to legday"; + parser.parseInput(assignInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_assignEmptyExercise_throwsInvalidInput() throws Exceptions.InvalidInput { + String assignInput = "workout /assign /to legday"; + parser.parseInput(assignInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_unassignExerciseFromWorkout_success() throws Exceptions.InvalidInput { + String exerciseInput = "exercise /add Pushups"; + parser.parseInput(exerciseInput); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String workoutInput = "workout /create chestday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String assignInput = "workout /assign Pushups /to chestday"; + parser.parseInput(assignInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String unassignInput = "workout /unassign Pushups /from chestday"; + parser.parseInput(unassignInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + } + + @Test + public void execute_unassignNonexistentExerciseFromWorkout_throwsActivityDoesNotExist() + throws Exceptions.InvalidInput { + String validInput = "workout /unassign NonexistentExercise /from LegDay"; + parser.parseInput(validInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_unassignNonexistentWorkout_throwsActivityDoesNotExist() throws Exceptions.InvalidInput { + String validInput = "workout /unassign Pushups /from NonexistentWorkout"; + parser.parseInput(validInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_unassignNonexistentExerciseFromNonexistentWorkout_throwsActivityDoesNotExist() + throws Exceptions.InvalidInput { + String validInput = "workout /unassign NonexistentExercise /from NonexistentWorkout"; + parser.parseInput(validInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_unassignExerciseNotAssigned_throwsActivityDoesNotExists() throws Exceptions.InvalidInput { + String exerciseInput = "exercise /add Pushups"; + parser.parseInput(exerciseInput); + assertDoesNotThrow(() -> exerciseManager.execute(parser)); + + String workoutInput = "workout /create chestday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> workoutManager.execute(parser)); + + String unassignInput = "workout /unassign Pushups /from chestday"; + parser.parseInput(unassignInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_unassignEmptyExercise_throwsInvalidInput() throws Exceptions.InvalidInput { + String unassignInput = "workout /unassign /from chestday"; + parser.parseInput(unassignInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + + + @Test + public void execute_listWorkouts_success() throws Exceptions.InvalidInput { + setUpStreams(); + + String validInput1 = "workout /create LegDay"; + String validInput2 = "workout /create ArmDay"; + + parser.parseInput(validInput1); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + parser.parseInput(validInput2); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + ui.printMessage(workoutManager.getListString()); + String expectedOutput = "[BYTE-CEPS]> Added Workout Plan: legday\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Added Workout Plan: armday\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Listing Workouts:\n" + + "\t\t\t1. legday\n" + + "\t\t\t2. armday\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void executeListAction_noWorkouts_returnsEmptyMessage() throws Exceptions.InvalidInput { + setUpStreams(); + + String validInput = "workout /list"; + parser.parseInput(validInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]> YourListofWorkoutsisEmpty\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void executeListAction_withWorkouts_returnsCorrectListing() throws Exceptions.InvalidInput { + setUpStreams(); + + String validInput1 = "workout /create LegDay"; + String validInput2 = "workout /create ArmDay"; + + parser.parseInput(validInput1); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + parser.parseInput(validInput2); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String listInput = "workout /list"; + parser.parseInput(listInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]> Added Workout Plan: legday\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Added Workout Plan: armday\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> Listing Workouts:\n" + + "\t\t\t1. legday\n" + + "\t\t\t2. armday\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + + @Test + public void execute_info_success() throws Exceptions.InvalidInput { + String exerciseInput1 = "exercise /add Squat"; + String exerciseInput2 = "exercise /add lunges"; + parser.parseInput(exerciseInput1); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + parser.parseInput(exerciseInput2); + assertDoesNotThrow(() -> ui.printMessage(exerciseManager.execute(parser))); + + String workoutInput = "workout /create legday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String assignInput1 = "workout /assign Squat /to legday"; + String assignInput2 = "workout /assign lunges /to legday"; + parser.parseInput(assignInput1); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + parser.parseInput(assignInput2); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String infoInput = "workout /info legday"; + parser.parseInput(infoInput); + + setUpStreams(); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + String expectedOutput = "[BYTE-CEPS]> Listing exercises in workout plan 'legday':\n" + + "\t\t\t1. squat\n" + + "\t\t\t2. lunges\n" + + "-------------------------------------------------\n"; + + + assertEquals(expectedOutput.replaceAll("\\s+", ""), outContent.toString().replaceAll("\\s+", "")); + restoreStreams(); + } + + @Test + public void execute_infoNonexistentWorkout_throwsActivityDoesNotExists() throws Exceptions.InvalidInput { + String validInput = "workout /info NonexistentWorkout"; + parser.parseInput(validInput); + assertThrows(Exceptions.ActivityDoesNotExist.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_infoActionEmptyWorkout_returnsEmptyMessage() throws Exceptions.InvalidInput { + setUpStreams(); + + String workoutInput = "workout /create legday"; + parser.parseInput(workoutInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String infoInput = "workout /info legday"; + parser.parseInput(infoInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]>AddedWorkoutPlan:legday-------------------------------------------------\n" + + "[BYTE-CEPS]>Yourworkoutplanlegdayisempty-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void execute_infoActionEmptyWorkout_throwsInvalidInput() throws Exceptions.InvalidInput { + String infoInput = "workout /info"; + parser.parseInput(infoInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + + @Test + public void execute_invalidWorkoutAction_throwsIllegalState() throws Exceptions.InvalidInput { + String invalidInput = "workout /unknown"; + parser.parseInput(invalidInput); + + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } + + @Test + public void execute_searchExistingWorkout_success() throws Exceptions.InvalidInput { + setUpStreams(); + + String addInput = "workout /create LegDay"; + parser.parseInput(addInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String searchInput = "workout /search LegDay"; + parser.parseInput(searchInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]> Added Workout Plan: legday\n" + + "-------------------------------------------------\n" + + "[BYTE-CEPS]> SearchResults:\n" + + "\t\t\t1. legday\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void execute_searchNonExistingWorkout_returnsEmptyResults() throws Exceptions.InvalidInput { + setUpStreams(); + + String searchInput = "workout /search NonExistentWorkout"; + parser.parseInput(searchInput); + assertDoesNotThrow(() -> ui.printMessage(workoutManager.execute(parser))); + + String expectedOutput = "[BYTE-CEPS]>Noresultsfound\n" + + "\n" + + "-------------------------------------------------\n"; + + assertEquals(expectedOutput.replaceAll("\\s+", ""), + outContent.toString().replaceAll("\\s+", "")); + + restoreStreams(); + } + + @Test + public void execute_searchEmptyQuery_throwsInvalidInput() throws Exceptions.InvalidInput { + String searchInput = "workout /search"; + parser.parseInput(searchInput); + assertThrows(Exceptions.InvalidInput.class, () -> workoutManager.execute(parser)); + } +} diff --git a/src/test/java/byteceps/storage/StorageTest.java b/src/test/java/byteceps/storage/StorageTest.java new file mode 100644 index 0000000000..461cf95c03 --- /dev/null +++ b/src/test/java/byteceps/storage/StorageTest.java @@ -0,0 +1,510 @@ +package byteceps.storage; + +import byteceps.processing.ExerciseManager; +import byteceps.processing.WorkoutManager; +import byteceps.processing.WeeklyProgramManager; +import byteceps.processing.WorkoutLogsManager; +import byteceps.ui.UserInterface; +import byteceps.ui.strings.UiStrings; +import byteceps.ui.strings.StorageStrings; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.io.File; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class StorageTest { + + private static final String FOLDER_PATH = "./jsons/"; + private static final String FILE_PATH = "./jsons/test.json"; + private static final String RENAMED_PATH = "./jsons/hidden.json"; + private static Storage storage; + + private static ExerciseManager exerciseManager = null; + private static WorkoutManager workoutManager = null; + private static WorkoutLogsManager workoutLogsManager = null; + private static WeeklyProgramManager weeklyProgramManager = null; + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + private final UserInterface ui = new UserInterface(); + + @BeforeEach + public void setup() { + storage = new Storage(FILE_PATH, ui); + exerciseManager = new ExerciseManager(); + workoutManager = new WorkoutManager(exerciseManager); + workoutLogsManager = new WorkoutLogsManager(); + weeklyProgramManager = new WeeklyProgramManager(exerciseManager, workoutManager, workoutLogsManager); + } + + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + public void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + } + + public boolean checkFile(String path) throws SecurityException, NullPointerException { + //Solution below inspired by https://www.geeksforgeeks.org/file-exists-method-in-java-with-examples/ + try { + File jsonFile = new File(path); + if (jsonFile.exists()) { + return true; + } + return false; + } catch (SecurityException | NullPointerException e) { + throw e; + } + } + + + public boolean deleteFile(String path) { + try { + File jsonFile = new File(path); + return jsonFile.delete(); + } catch (SecurityException e) { + return false; + } + } + + public boolean renameOriginalFile(String originalPath, String newPath) { + //Solution below inspired by https://www.geeksforgeeks.org/file-renameto-method-in-java-with-examples/ + File originalFile = new File(originalPath); + File renamedFile = new File(newPath); + try { + if (originalFile.renameTo(renamedFile)) { + return true; + } + return false; + } catch (SecurityException e) { + return false; + } + } + + public boolean restoreOriginalFile(String originalPath, String renamedPath) { + File testGenFile = new File(originalPath); + File originalFileRenamed = new File(renamedPath); + + assertDoesNotThrow(() -> checkFile(originalPath)); + assertDoesNotThrow(() -> checkFile(renamedPath)); + + boolean testGenFileExists = checkFile(originalPath); + boolean originalFileRenamedExists = checkFile(renamedPath); + + try { + if (testGenFileExists && originalFileRenamedExists) { + assertTrue(testGenFile.delete()); + File originalFileRestored = new File(originalPath); + assertTrue(originalFileRenamed.renameTo(originalFileRestored)); + return true; + } + return false; + } catch (SecurityException e) { + return false; + } + + } + + public String findFileName (String partialPath) { + //Solution below adapted by https://www.baeldung.com/java-current-directory + String directoryPath = new File("").getAbsolutePath(); + String jsonsFolder = "/jsons/"; + File currentDir = new File(directoryPath + jsonsFolder); + + //Solution below inspired by https://www.geeksforgeeks.org/file-listfiles-method-in-java-with-examples/ + File[] files = currentDir.listFiles(); + + for (File f:files) { + String currentName = f.getName(); + if (currentName.contains(partialPath)) { + return currentName; + } + } + + return ""; + } + + public boolean isEmptyFile(String path) throws SecurityException { + try { + File jsonFile = new File(path); + if(jsonFile.length() == 0) { + return true; + } + return false; + } catch (SecurityException e) { + throw e; + } + } + + public boolean isCorruptedFile(String path) throws SecurityException, FileNotFoundException { + File jsonFile = new File(path); + try (Scanner jsonScanner = new Scanner(jsonFile)) { + JSONObject jsonArchive = new JSONObject(jsonScanner.nextLine()); + } catch (FileNotFoundException|SecurityException e) { + throw e; + } catch (JSONException e) { + return true; + } + return false; + } + + + @Test + public void save_expectedManagers_success() { + setUpStreams(); + + assertDoesNotThrow(() -> storage.save(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + String expectedOutput = String.format("%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.WORKOUTS_SAVED, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + restoreStreams(); + } + + @Test + public void load_noPreExistingJsonFile_success() { + assertDoesNotThrow(() -> checkFile(FILE_PATH)); + boolean fileExists = checkFile(FILE_PATH); + if (fileExists) { + assertTrue(renameOriginalFile(FILE_PATH, RENAMED_PATH)); + } + + setUpStreams(); + assertDoesNotThrow(() -> storage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + String expectedOutput = String.format("%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.NO_SAVE_DATA, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + restoreStreams(); + + if (fileExists) { + assertTrue(restoreOriginalFile(FILE_PATH, RENAMED_PATH)); + } + } + + @Test + public void load_preExistingJsonFile_success() { + assertDoesNotThrow(() -> checkFile(FILE_PATH)); + boolean fileExists = checkFile(FILE_PATH); + if (!fileExists) { + assertDoesNotThrow(() -> storage.save(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + } + setUpStreams(); + assertDoesNotThrow(() -> storage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_SUCCESS, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + if (!fileExists) { + assertTrue(deleteFile(FILE_PATH)); + } + restoreStreams(); + } + + @Test + public void load_preExistingJsonFileBlankBug_success() { + assertDoesNotThrow(() -> checkFile(FILE_PATH)); + boolean fileExists = checkFile(FILE_PATH); + + if (!fileExists) { + assertDoesNotThrow(() -> storage.save(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + } + if (assertDoesNotThrow(() -> isEmptyFile(FILE_PATH))) { + assertTrue(deleteFile(FILE_PATH)); + assertDoesNotThrow(() -> storage.save(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + } + setUpStreams(); + assertDoesNotThrow(() -> storage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_SUCCESS, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + if (!fileExists) { + assertTrue(deleteFile(FILE_PATH)); + } + restoreStreams(); + } + + @Test + public void load_preExistingJsonFileCorruptBug_success() { + assertDoesNotThrow(() -> checkFile(FILE_PATH)); + boolean fileExists = checkFile(FILE_PATH); + + if (!fileExists) { + assertDoesNotThrow(() -> storage.save(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + } + + + if (assertDoesNotThrow(() -> isCorruptedFile(FILE_PATH))) { + assertTrue(deleteFile(FILE_PATH)); + assertDoesNotThrow(() -> storage.save(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + } + + setUpStreams(); + assertDoesNotThrow(() -> storage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_SUCCESS, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + if (!fileExists) { + assertTrue(deleteFile(FILE_PATH)); + } + restoreStreams(); + } + + + + @Test + public void load_corruptedJSON_failure() { + String corruptFailureFileName = "corrupted.json.old"; + String corruptFilePath = "./jsons/corrupted.json"; + String corruptFailureFilePath = "./jsons/corrupted.json.old"; + + assertDoesNotThrow( () -> checkFile(corruptFilePath)); + assertTrue(checkFile(corruptFilePath)); + + Storage failureStorage = new Storage(corruptFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(corruptFilePath)); + assertTrue(checkFile(corruptFilePath)); + + assertNotEquals("", corruptFailureFileName = findFileName(corruptFailureFileName)); + corruptFailureFilePath = FOLDER_PATH + corruptFailureFileName; + assertTrue(restoreOriginalFile(corruptFilePath, corruptFailureFilePath)); + + restoreStreams(); + + } + + + @Test + public void load_duplicateExercise_failure() { + String duplicateExerciseFailureFileName = "duplicateExercise.json.old"; + String duplicateExerciseFilePath = "./jsons/duplicateExercise.json"; + String duplicateExerciseFailureFilePath = "./jsons/duplicateExercise.json.old"; + + assertDoesNotThrow( () -> checkFile(duplicateExerciseFilePath)); + assertTrue(checkFile(duplicateExerciseFilePath)); + + Storage failureStorage = new Storage(duplicateExerciseFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(duplicateExerciseFilePath)); + assertTrue(checkFile(duplicateExerciseFilePath)); + + assertNotEquals("",duplicateExerciseFailureFileName = findFileName(duplicateExerciseFailureFileName)); + duplicateExerciseFailureFilePath = FOLDER_PATH + duplicateExerciseFailureFileName; + assertTrue(restoreOriginalFile(duplicateExerciseFilePath, duplicateExerciseFailureFilePath)); + + restoreStreams(); + + } + + @Test + public void load_duplicateWorkout_failure() { + String duplicateWorkoutFailureFileName = "duplicateWorkout.json.old"; + String duplicateWorkoutFilePath = "./jsons/duplicateWorkout.json"; + String duplicateWorkoutFailureFilePath = "./jsons/duplicateWorkout.json.old"; + + assertDoesNotThrow( () -> checkFile(duplicateWorkoutFilePath)); + assertTrue(checkFile(duplicateWorkoutFilePath)); + + Storage failureStorage = new Storage(duplicateWorkoutFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(duplicateWorkoutFilePath)); + assertTrue(checkFile(duplicateWorkoutFilePath)); + + assertNotEquals("",duplicateWorkoutFailureFileName = findFileName(duplicateWorkoutFailureFileName)); + duplicateWorkoutFailureFilePath = FOLDER_PATH + duplicateWorkoutFailureFileName; + assertTrue(restoreOriginalFile(duplicateWorkoutFilePath, duplicateWorkoutFailureFilePath)); + + restoreStreams(); + + } + + @Test + public void load_workoutMissing_failure() { + String workoutMissingFailureFileName = "workoutMissing.json.old"; + String workoutMissingFilePath = "./jsons/workoutMissing.json"; + String workoutMissingFailureFilePath = "./jsons/workoutMissing.json.old"; + + assertDoesNotThrow( () -> checkFile(workoutMissingFilePath)); + assertTrue(checkFile(workoutMissingFilePath)); + + Storage failureStorage = new Storage(workoutMissingFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(workoutMissingFilePath)); + assertTrue(checkFile(workoutMissingFilePath)); + + assertNotEquals("",workoutMissingFailureFileName = findFileName(workoutMissingFailureFileName)); + workoutMissingFailureFilePath = FOLDER_PATH + workoutMissingFailureFileName; + assertTrue(restoreOriginalFile(workoutMissingFilePath, workoutMissingFailureFilePath)); + + restoreStreams(); + + } + + + @Test + public void load_workoutExercisesMissing_failure() { + String workoutExercisesMissingFailureFileName = "workoutExercisesMissing.json.old"; + String workoutExercisesMissingFilePath = "./jsons/workoutExercisesMissing.json"; + String workoutExercisesMissingFailureFilePath = "./jsons/workoutExercisesMissing.json.old"; + + assertDoesNotThrow( () -> checkFile(workoutExercisesMissingFilePath)); + assertTrue(checkFile(workoutExercisesMissingFilePath)); + + Storage failureStorage = new Storage(workoutExercisesMissingFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(workoutExercisesMissingFilePath)); + assertTrue(checkFile(workoutExercisesMissingFilePath)); + + assertNotEquals("", + workoutExercisesMissingFailureFileName = findFileName(workoutExercisesMissingFailureFileName)); + workoutExercisesMissingFailureFilePath = FOLDER_PATH + workoutExercisesMissingFailureFileName; + assertTrue(restoreOriginalFile(workoutExercisesMissingFilePath, workoutExercisesMissingFailureFilePath)); + + restoreStreams(); + + } + + + @Test + public void load_nonExistingExercisesInLogs_failure() { + String logsExerciseFailFailureFileName = "logsExerciseFail.json.old"; + String logsExerciseFailFilePath = "./jsons/logsExerciseFail.json"; + String logsExerciseFailFailureFilePath = "./jsons/logsExerciseFail.json.old"; + + assertDoesNotThrow( () -> checkFile(logsExerciseFailFilePath)); + assertTrue(checkFile(logsExerciseFailFilePath)); + + Storage failureStorage = new Storage(logsExerciseFailFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(logsExerciseFailFilePath)); + assertTrue(checkFile(logsExerciseFailFilePath)); + + assertNotEquals("",logsExerciseFailFailureFileName = findFileName(logsExerciseFailFailureFileName)); + logsExerciseFailFailureFilePath = FOLDER_PATH + logsExerciseFailFailureFileName; + assertTrue(restoreOriginalFile(logsExerciseFailFilePath, logsExerciseFailFailureFilePath)); + + restoreStreams(); + + } + + @Test + public void load_nonExistingWorkoutInLogs_failure() { + String logsWorkoutFailFailureFileName = "logsWorkoutFail.json.old"; + String logsWorkoutFailFilePath = "./jsons/logsWorkoutFail.json"; + String logsWorkoutFailFailureFilePath = "./jsons/logsWorkoutFail.json.old"; + + assertDoesNotThrow( () -> checkFile(logsWorkoutFailFilePath)); + assertTrue(checkFile(logsWorkoutFailFilePath)); + + Storage failureStorage = new Storage(logsWorkoutFailFilePath, new UserInterface()); + setUpStreams(); + + assertDoesNotThrow( () -> failureStorage.load(exerciseManager, workoutManager, weeklyProgramManager, + workoutLogsManager)); + String expectedOutput = String.format("%s%s%s%s%s%s%s%s%s%s", UiStrings.BYTECEP_PROMPT, StorageStrings.LOADING, + System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator(), UiStrings.BYTECEP_PROMPT, + StorageStrings.LOAD_ERROR, System.lineSeparator(), UiStrings.SEPARATOR, System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + + assertDoesNotThrow( () -> checkFile(logsWorkoutFailFilePath)); + assertTrue(checkFile(logsWorkoutFailFilePath)); + + assertNotEquals("",logsWorkoutFailFailureFileName = findFileName(logsWorkoutFailFailureFileName)); + logsWorkoutFailFailureFilePath = FOLDER_PATH + logsWorkoutFailFailureFileName; + assertTrue(restoreOriginalFile(logsWorkoutFailFilePath, logsWorkoutFailFailureFilePath)); + + restoreStreams(); + } + + +} diff --git a/src/test/java/byteceps/ui/UserInterfaceTest.java b/src/test/java/byteceps/ui/UserInterfaceTest.java new file mode 100644 index 0000000000..d6932fecbf --- /dev/null +++ b/src/test/java/byteceps/ui/UserInterfaceTest.java @@ -0,0 +1,82 @@ +package byteceps.ui; + +import byteceps.ui.strings.UiStrings; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; + +class UserInterfaceTest { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final InputStream originalIn = System.in; + + private UserInterface ui; + + @BeforeEach + public void setUp() { + outContent.reset(); + System.setOut(new PrintStream(outContent)); + System.setIn(originalIn); + UserInterface.resetInstance(); + ui = UserInterface.getInstance(); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + System.setIn(originalIn); + } + + @Test + public void printMessage_message_success() { + String message = "Test message"; + ui.printMessage(message); + String expectedOutput = String.format(UiStrings.BYTECEP_PROMPT_FORMAT, UiStrings.BYTECEP_PROMPT, message, + System.lineSeparator()) + UiStrings.SEPARATOR + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString()); + } + + @Test + public void printMessageNoSeparator_message_success() { + String message = "Test message"; + ui.printMessageNoSeparator(message); + String expectedOutput = String.format(UiStrings.BYTECEP_PROMPT_FORMAT, UiStrings.BYTECEP_PROMPT, message, + System.lineSeparator()); + assertEquals(expectedOutput, outContent.toString()); + } + + @Test + public void printWelcomeMessage_message_success() { + ui.printWelcomeMessage(); + String expectedOutput = UiStrings.SEPARATOR + System.lineSeparator() + + UiStrings.MESSAGE_WELCOME + System.lineSeparator() + + UiStrings.SEPARATOR + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString()); + } + + @Test + public void printGoodbyeMessage_message_success() { + ui.printGoodbyeMessage(); + String expectedOutput = UiStrings.SEPARATOR + System.lineSeparator() + + UiStrings.MESSAGE_GOODBYE + System.lineSeparator() + + UiStrings.SEPARATOR + System.lineSeparator(); + assertEquals(expectedOutput, outContent.toString()); + } + + @Test + public void getUserInput_returnsTrimmedInput_success() { + String simulatedUserInput = " hello \n"; + System.setIn(new ByteArrayInputStream(simulatedUserInput.getBytes())); + // Create a new UI instance if necessary after setting System.in + ui = new UserInterface(System.in, System.out); + String input = ui.getUserInput(); + assertEquals("hello", input); + } +} diff --git a/src/test/java/byteceps/validators/HelpValidatorTest.java b/src/test/java/byteceps/validators/HelpValidatorTest.java new file mode 100644 index 0000000000..7fd60617fb --- /dev/null +++ b/src/test/java/byteceps/validators/HelpValidatorTest.java @@ -0,0 +1,76 @@ +package byteceps.validators; + +import byteceps.commands.Parser; +import byteceps.errors.Exceptions; +import byteceps.ui.strings.HelpStrings; +import byteceps.ui.strings.ManagerStrings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +//@@author LWachtel1 +public class HelpValidatorTest { + + private Parser parser; + + @BeforeEach + public void setup() { + parser = new Parser(); + } + + @Test + public void validateCommand_hasFlagNoAdditionalArgs_success() { + String flagNoAdditionalArgs = "help /exercise"; + assertDoesNotThrow(() -> parser.parseInput(flagNoAdditionalArgs)); + assertDoesNotThrow( () -> HelpValidator.validateCommand(parser)); + } + + @Test + public void validateCommand_hasFlagParamNoAdditionalArgs_success() { + String flagParamNoAdditionalArgs = "help /exercise 1"; + assertDoesNotThrow(() -> parser.parseInput(flagParamNoAdditionalArgs)); + assertDoesNotThrow( () -> HelpValidator.validateCommand(parser)); + } + + @Test + public void validateCommand_hasFlagAdditionalArgs_throwsInvalidInput() { + String flagAdditionalArgs = "help /exercise /error"; + assertDoesNotThrow(() -> parser.parseInput(flagAdditionalArgs)); + + String errorMessage = HelpStrings.ADDITIONAL_ARGUMENTS_EXCEPTION; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + HelpValidator.validateCommand(parser)).getMessage()); + } + + @Test + public void validateCommand_hasFlagParamAdditionalArgs_throwsInvalidInput() { + String flagParamAdditionalArgs = "help /exercise 1 /error"; + assertDoesNotThrow(() -> parser.parseInput(flagParamAdditionalArgs)); + + String errorMessage = HelpStrings.ADDITIONAL_ARGUMENTS_EXCEPTION; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + HelpValidator.validateCommand(parser)).getMessage()); + } + + @Test + public void validateCommand_emptyFlag_throwsInvalidInput() { + String emptyFlag = "help / "; + assertDoesNotThrow(() -> parser.parseInput(emptyFlag)); + + String errorMessage = HelpStrings.NO_COMMAND_EXCEPTION; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + HelpValidator.validateCommand(parser)).getMessage()); + } + + @Test + public void validateCommand_noFlag_throwsInvalidInput() { + String noFlag = "help"; + assertDoesNotThrow(() -> parser.parseInput(noFlag)); + + String errorMessage = ManagerStrings.NO_ACTION_EXCEPTION; + assertEquals(errorMessage, assertThrowsExactly(Exceptions.InvalidInput.class,() -> + HelpValidator.validateCommand(parser)).getMessage()); + } +} diff --git a/src/test/java/byteceps/validators/WorkoutLogsValidatorTest.java b/src/test/java/byteceps/validators/WorkoutLogsValidatorTest.java new file mode 100644 index 0000000000..4356acbd29 --- /dev/null +++ b/src/test/java/byteceps/validators/WorkoutLogsValidatorTest.java @@ -0,0 +1,80 @@ +package byteceps.validators; + +import byteceps.activities.Exercise; +import byteceps.activities.ExerciseLog; +import byteceps.activities.WorkoutLog; +import byteceps.errors.Exceptions; +import byteceps.processing.ExerciseManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.TestInstantiationException; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class WorkoutLogsValidatorTest { + private ExerciseManager exerciseManager; + private WorkoutLog workoutLog; + + void setUp() { + exerciseManager = new ExerciseManager(); + Exercise newExercise = new Exercise("test"); + try { + exerciseManager.add(newExercise); + } catch (Exceptions.ActivityExistsException e) { + throw new TestInstantiationException("Unable to add exercise to test"); + } + + workoutLog = new WorkoutLog("2024-03-01", "workout"); + ExerciseLog newExerciseLog = new ExerciseLog("test", + Arrays.asList(1, 1, 1), 3, Arrays.asList(1, 1, 1)); + workoutLog.addExerciseLog(newExerciseLog); + } + @Test + void exerciseExists_exists_success() { + setUp(); + assertDoesNotThrow(() -> WorkoutLogsValidator.exerciseExists(exerciseManager, "test")); + } + + @Test + void exerciseExists_doesNotExist_throwsAcitivityDoesNotExistException() { + setUp(); + assertThrows(Exceptions.ActivityDoesNotExist.class, + () -> WorkoutLogsValidator.exerciseExists(exerciseManager, "test1")); + } + + @Test + void hasNegativeInput_negativeWeight_throwsNumberFormatException() { + List weightsList = Arrays.asList(-1, 0, 3); + List repsList = Arrays.asList(1, 1, 1); + int setsInt = 1; + assertThrows(NumberFormatException.class, + () -> WorkoutLogsValidator.hasNegativeInput(weightsList, repsList, setsInt)); + } + + @Test + void hasNegativeInput_negativeRep_throwsNumberFormatException() { + List weightsList = Arrays.asList(1, 0, 3); + List repsList = Arrays.asList(1, -1, 1); + int setsInt = 1; + assertThrows(NumberFormatException.class, + () -> WorkoutLogsValidator.hasNegativeInput(weightsList, repsList, setsInt)); + } + + @Test + void hasNegativeInput_negativeSet_throwsNumberFormatException() { + List weightsList = Arrays.asList(1, 0, 3); + List repsList = Arrays.asList(1, 1, 1); + int setsInt = -1; + assertThrows(NumberFormatException.class, + () -> WorkoutLogsValidator.hasNegativeInput(weightsList, repsList, setsInt)); + } + + @Test + void removeExerciseIfLogExists_exists_success() { + setUp(); + assertDoesNotThrow(()-> WorkoutLogsValidator.removeExerciseIfLogExists(workoutLog, "test")); + } +} 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..fbf484d657 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,18 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +------------------------------------------------- +WELCOME TO BYTECEPS +------------------------------------------------- +[BYTE-CEPS]> Looks like you're starting fresh! +------------------------------------------------- +[BYTE-CEPS]> To access the help menu for command guidance, please type: +help /COMMAND_TYPE_FLAG +Available command types (type exactly as shown): +exercise +workout +program +To view this message again, enter 'help' alone +------------------------------------------------- +[User]> [BYTE-CEPS]> All your workouts and exercises have been saved. +------------------------------------------------- +------------------------------------------------- +GOODBYE FOR NOW. STAY HARD! +------------------------------------------------- diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..b023018cab 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1 @@ -James Gosling \ No newline at end of file +bye