diff --git a/.gitignore b/.gitignore index 2873e189e1..aea736f6f0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,8 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +pulsepilot_log* +pulsepilot_data* +pulsepilot_hash* +META-INF/ + diff --git a/build.gradle b/build.gradle index ea82051fab..1c94bd2137 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("seedu.pulsepilot.PulsePilot") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("pulsepilot") archiveClassifier.set("") } @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..e4caa98548 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,11 @@ # 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) | Justin Soh | [Github](https://github.com/JustinSoh) | [Portfolio](https://justinsoh.github.io/) +![](https://via.placeholder.com/100.png?text=Photo) | Rouvin Erh | [Github](https://github.com/rouvinerh) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Alfaatih | [Github](https://github.com/L5-Z) | [Portfolio](https://l5-z.github.io) +![](https://via.placeholder.com/100.png?text=Photo) | Jolene | [Github](https://github.com/j013n3) | [Portfolio](docs/team/j013n3.md) +![](https://via.placeholder.com/100.png?text=Photo) | Ying Jia | [Github](https://github.com/syj02) | [Portfolio](docs/team/syj02.md) +![](https://via.placeholder.com/100.png?text=Photo) | R M Raajamani | [Github](https://github.com/raajamani) | [Portfolio](docs/team/raajamani.md) + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..2e81c0d80e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,1114 @@ # Developer Guide +![Logo](img/logo.jpg) + +## Table of Contents + +* [Acknowledgements](#acknowledgements) +* [Introduction](#introduction) +* [Design](#design) +* [Implementation of Commands](#implementation-of-commands) +* [Appendix: DG Requirements](#appendix-requirements) + * [Product Scope](#target-user-profile) + * [User Stories](#user-stories) + * [Non-Functional Requirements](#non-functional-requirements) + * [Glossary](#glossary) + * [Manual Testing](#manual-testing) + +--- + +
+ + + ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +Our team has referenced [Address Book (Level-3)](https://github.com/se-edu/addressbook-level3) and used their [Developer Guide (DG)](https://se-education.org/addressbook-level3/DeveloperGuide.html) to better structure our own Developer Guide. + +--- + +## Introduction + +The purpose of this guide is to provide an explanation for all the functions and internal workings in PulsePilot. This enables any technical readers to get a detailed understanding of the application's implementation, making it easier for them to contribute to the project or adapt it according to their preferences. This is made to complement the User Guide. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +## Design + +* [Overview of Components](#overview-of-components) +* [UI](#ui-package) + * [Handler](#handler) + * [Output](#output) +* [Workouts](#workouts-package) + * [Workout List](#workouts-list) + * [Gym](#gym) + * [GymStation](#gym-station) + * [GymSet](#gym-set) + * [Run](#run) +* [Health](#health-package) + * [HealthList](#health-list) + * [Bmi](#bmi) + * [Period](#period) + * [Appointment](#appointment) +* [Utility](#utility-package) + * [Parser](#parser) + * [Validation](#validation) + * [CustomExceptions](#custom-exceptions) + * [Filters](#filters) +* [Storage](#storage-package) +* [Constants](#constants-package) + +--- + +
+ +### Overview of Components + +This part of the guide provides a high-level overview of each package and its classes via a class or sequence diagram. A brief description of each class is given as well. + +PulsePilot follows an **Object-Oriented Design** approach, with separate packages for handling different components of the application, such as user input, output, workout logging, and health data management. + +The **_Architecture Diagram_** is given below: + +![Architecture Diagram](img/architecture_diagram.png) + +The `seedu.pulsepilot` package contains the `Main` method, which is the entry point of the application. It is responsible for initialising and processing of user input, and the termination of PulsePilot. + +- `Ui`: The user interface of PulsePilot used for handling user input and printing messages. +- `Storage`: Contains the data storage and logging components for PulsePilot. +- `Health`: Stores health-related information. +- `Workouts`: Stores workout-related information. +- `Utility`: Contains utility functions, such as input parsing and validation. +- `Constants`: Contains all constants used in PulsePilot. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### UI Package + +The `UI` package contains the `Handler` and `Output` classes, which are responsible for handling user input and printing of output to the screen respectively. + +#### Handler + +The main entry point of the application is the `Handler` class. The user's input is read here and passed to the corresponding handler method. + +The sequence diagram below shows how it works: + +![Handler Sequence Diagram](img/sequence_diagrams/handler_sequence_diagram.png) + +1. PulsePilot is started via `handler.initialiseBot()`, which checks whether the data file is present and its integrity if applicable. How this is done will be covered [here](#storage-of-data). + +2. `handler.processInput()` is then used to get the user's input, determining what command being used and passes the input to the right `handler` method. + +3. When PulsePilot exits gracefully via the `exit` command, `terminateBot()` is called to write to the data and hash files. + - If a user exits without calling terminateBot(), **data will be lost!** Likewise, this is covered [here](#storage-of-data). + +The `Handler` class creates other classes: + +- `Scanner` as `in` to read user input. +- `Validation` as `validation` for validating user input. +- `Parser` as `parser` for parsing user input. +- Static `LogFile` as `logFile` to write logs. +- `DataFile` as `dataFile` to write data stored. +- `Output` as `output` to print messages to the user. + +The creation of the above classes will be left out of other sequence diagrams to prevent cluttering the diagram. **It is assumed in other class diagrams for `Handler` that the classes have already been created.** + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Output + +The `Output` class is responsible for printing messages, prompts, errors and other information to the terminal for the user. + +The class diagram for `Output` has been omitted, since a developer can read the code itself to gain a better understanding of its methods. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Workouts Package + +The `Workout` package is responsible for tracking run and gym workouts from the user. + +#### Workouts List + +`WorkoutLists` is a class that contains the `ArrayList` objects of `Run`, `Gym` and `Workout`. The class diagram is as follows: + +![WorkoutLists Class Diagram](img/class_diagrams/workoutlist_class_diagram.png) + +The class contains methods to retrieve all or the latest added object, and delete objects. + +The `clearWorkoutsRunGym()` method is used to clear all the data stored within each `ArrayList`, which is mainly used for unit testing. + +###### [Back to table of contents](#table-of-contents) + +--- + + + +#### Gym + +`Gym` is a class that represents a gym session that the user has recorded. It contains the following attributes: + +- `date`: An **optional** `LocalDate` attribute representing the date of the workout. Implemented via an overloaded `Gym()` constructor. + +**A `Gym` object contains 0 or more `GymStation` objects.** A `Gym` with 0 `GymStation` objects can exist through using `workout /e:gym` and then `back`, but is deleted right after. + + + +The class diagram for gym is as follows: + +![Gym Class Diagram](img/class_diagrams/gym_class_diagram.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +##### Gym Station + +`GymStation` is a class that represents one gym station the user has done in a particular gym session. It contains the following attributes: + +- `stationName`: Name of the gym station as a `String`. +- `ArrayList`: An `ArrayList` of `GymSet` object, representing the varying number of sets done at one station. +- `numberOfSets`: The number of sets done as an `int`. + +**A `GymStation` object contains 1 or more `GymSet` objects.** + +###### [Back to table of contents](#table-of-contents) + +--- + +##### Gym Set + +`GymSet` is a class that represents one gym set the user has done in one gym station. It contains the following attributes: + +- `weight`: The weight done for a gym set represented as a `double`. +- `numberOfRepetitions`: The number of repetitions for a gym set represented as an `int`. + +###### [Back to table of contents](#table-of-contents) + +--- + + + +#### Run + +`Run` is a class that represents a run workout the user has recorded. It contains the following attributes: + +- `times`: An `Integer[]` variable representing the hours, minutes and seconds taken for a run. +- `distance`: The distance ran in **kilometers** represented as a `double`. +- `date`: An **optional** `LocalDate` parameter representing the date of the workout. Implemented via an overloaded `Run()` constructor. +- `pace`: The pace of the run in minutes/km represented as a `String`. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Health Package + +The `Health` package is responsible for tracking user's BMI, period cycle, and medical appointments. + +#### Health List + +`HealthList` is a class that contains the `ArrayList` objects of `Bmi`, `Period`, and `Appointment`. The class diagram is as follows: + +![HealthList Class Diagram](img/class_diagrams/healthlist_class_diagram.png) + +The class contains methods to retrieve the different objects. Additionally, it contains the methods for: + +- **Deleting** an object from PulsePilot, which is used for the `delete` command implementation. + + +The `clearHealthLists()` method is used to clear all the data stored within each `ArrayList`, which is mainly used for unit testing. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### BMI + +`Bmi` is a class that represents the Body Mass Index (BMI) calculated using the height and weight specified. It contains the following attributes: + +- `height`: The height of the user in metres represented as a `double`. +- `weight`: The weight of the user in kilograms represented as a `double`. +- `bmiValue`: The calculate BMI value of the user derived from the height and weight given, also represented as a `double`. +- `date`: A `LocalDate` parameter representing the date of the recorded/added BMI value. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Period + +`Period` is a class that represents the menstrual cycle of the user. It contains the following attributes: + +- `startDate`: The date of the first day of menstrual flow (aka period flow), which is also the first day of menstrual cycle, represented using a `LocalDate`. +- `endDate`: The date of the last day of menstrual flow, represented using a `LocalDate`. +- `periodLength`: The number of days of menstrual flow (i.e. between the first and last day of flow, inclusive of the first day), represented as `long`. +- `cycleLength`: The number of days in a menstrual cycle (i.e. between the first and last day of the cycle, inclusive of the first day), represented as a `long`. The cycle ends on the day before the first day of the next menstrual flow/cycle. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Appointment + +`Appointment` is a class that represents the past and upcoming medical appointments of the user. It contains the following attributes: + +- `date`: The date of the medical appointment, represented using a `LocalDate`. +- `time`: The time of the medical appointment in 24-hour format, represented using a `LocalTime`. +- `description`: The details of the appointment, which can include things like the healthcare professional to consult, the type of appointment such as consultation, checkup, rehabilitation, therapy etc. This parameter is represented as a `String`. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Utility Package + +The `Utility` package includes classes and methods that handle exceptions, user input parsing, user input validation, and the various filter strings using enumerations. + +It consists of `CustomExceptions`, `Filters`, `Parser` and `Validation` classes, which are covered below. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Parser + +The `Parser` class is responsible for splitting the user's input into lists of parameters. + +The input **must contain the flags required for each command**, else an exception will be thrown. The number of `/` characters is checked as well, as it can trigger errors. Afterwards, the split input is validated using methods within the `Validated` class as `String[]` variables. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Validation + +The `Validation` class is responsible for validating the user's split input. The split input comes from the `Parser` class in `String[]` variables. + +Each variable is then checked to ensure that it follows the format needed. This is done by ensuring there are no empty strings, whether it matches regex, etc. If not, the methods in this class throws exceptions. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Custom Exceptions + +The `CustomExceptions` class inherits from the `Exception` class from Java. This class is responsible for printing formatted errors. + +The exceptions are further broken down into the following: + +- `OutOfBounds`: When access with an illegal index is made. +- `InvalidInput`: When user enters input that does not conform with required format or is malformed. +- `FileReadError`: Unable to read the files for `Storage`. +- `FileWriteError`: Unable to write files for `Storage`. +- `FileCreateError`: Unable to create files for `Storage`. +- `InsufficientInput`: When not enough parameters or blank parameters for a command are detected. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Filters + +The `Filters` class contains all the filter strings for different use cases, such as when adding a workout or viewing the history. + +This is represented as enumerations. Attempts to use an invalid filter results in `IllegalArgumentException` being thrown. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Storage Package + +`Storage` package contains `DataFile` and `LogFile`. This component handles all logging of commands used and writing of data stored within PulsePilot to an external data file. The reading of the data file is also done here, allowing PulsePilot to resume a previous saved state. + +- `DataFile` is responsible for the writing of data to `pulsepilot_data.txt`, and generating the hash for it in `pulsepilot_hash.txt`. It also checks whether the data has been tampered with or files are missing, and creates or deletes files if needed. + +- `LogFile` writes the logs to `pulsepilot_log.txt`, tracking each command and error thrown. + +This package also checks whether the application has read and write permissions over its current directory. If not, it throws an exception and exits. + +###### [Back to table of contents](#table-of-contents) + +--- + +### Constants Package + +This package contains all the different constants used throughout PulsePilot to prevent the usage of magic strings and numbers. + +The constants are broken down into the following 4 classes: + +- `HealthConstant`: All constant strings and numbers related to all `Health` commands and methods. +- `WorkoutConstant`: All constant strings and numbers related to all `Workout` commands and methods. +- `ErrorConstant`: All strings used when exceptions are thrown. +- `UiConstant`: All other constants and numbers that are not within the above three classes, such as file names, flags, and other general purpose constants. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +## Implementation of Commands + +**NOTE**: Not every single line of code is explained here, as any developer can read the source code to find out all the specifics. + +* [Workout](#workout) + * [Add Run](#add-run) + * [Add Gym](#add-gym) +* [Health](#health) + * [Add BMI](#add-bmi) + * [Add Period](#add-period) + * [Make Period Prediction](#make-period-prediction) + * [Add Appointment](#add-appointment) +* [View History](#view-history) +* [View Latest](#view-latest) +* [Delete Item](#delete-item) +* [Storage of Data](#storage-of-data) + +
+ +### Workout + +User input is passed to `handler.processInput()`, determining the command used is `workout`. Then, `Handler.handleHealth()` determines type of `workout` command used. + +#### Add Run + +The user's input is processed to add a run as follows: + +1. `parser.parseRunInput()` splits the input using `parser.splitRunInput()` using the flags, returning a `String[]` variable. Extracts the optional `date` parameter if present. + +2. `validation.validateRunInput()` is called to validate each parameter. If no exceptions caused by invalid parameters are thrown, the validated parameters are used to create the new `Run` object. + +3. The `Run` constructor checks whether the distance given and pace calculated is within the valid range. If not, it throws an exception. + +4. Afterwards, a `Workout` object is implicitly created since `Run` inherits from `Workout`. `workout.addIntoWorkoutList()` is called, creating a new `WorkoutLists` object, and `workoutLists.addRun()` is used to add the `Run`. + +5. The new `Run` object is then passed to `output.printAddRun()` and a message acknowledging the successful adding is printed to the screen. + +This is the sequence diagram for adding a run: + +![Run Sequence Diagram](img/sequence_diagrams/run_sequence_diagram.png) + +###### [Back to table of contents](#table-of-contents) + +--- + + + +#### Add Gym + +The user's input is processed to add a gym is as follows: + +1. `parser.parseGymInput()` splits the input using `parser.splitGymInput()` using the flags, returning a `String[]` variable. Extracts the optional `date` parameter if present. + +2. `validation.validateGymInput()` is called to validate each parameter. If no exceptions caused by invalid parameters are thrown, the validated parameters are used to create the new `Gym` object. + +3. Afterwards, a `Workout` object is implicitly created since `Gym` inherits from `Workout`. `workout.addIntoWorkoutList()` is called, creating a new `WorkoutLists` object, and `workoutLists.addGym()` is used to add the `Gym`. + +4. Afterwards, `parser.parseGymStationInput()` is called to retrieve input for each gym station. + +This is the sequence diagram for adding a `Gym` thus far: + +![Gym Sequence Diagram](img/sequence_diagrams/gym_overall_sequence_diagram.png) + +##### Add Gym Station + +After adding a `Gym` object, the user is then prompted for input for the gym station. The gym station input is processed as follows: + +1. `parser.parseGymStationInput()` is called, which starts a loop that iterates `NUMBER_OF_STATION` times. + +2. `output.printGymStationPrompt()` is used to print the prompt for the user, and user input is retrieved. + +3. User input is split using `Parser.splitGymStationInput()` which as the name suggests, splits the parameters from the user, returning a `String[]` variable. + +4. After splitting the input, the parameters are passed to `newGym.addStation()`. + +5. `newGym.addStation()` will then create a `GymStation` object during which the input is checked within the `GymStation` class. + +6. If the values are valid, the `GymStation` object is appended to an `ArrayList` stored in the `newGym` object. + +7. Steps 2 to 6 repeats until all stations have been added. + +8. The final `Gym` object is passed to `output.printAddGym()` and a message acknowledging the successful adding is printed to the screen. + +This is the sequence diagram for adding a `GymStation` object: + +![Gym Station Sequence](img/sequence_diagrams/gym_station_sequence_diagram.png) + +If the user types `back` at any given point when taking in `GymStation` input, this method returns and the `Gym` object is deleted. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Health + +User input is passed to `handler.processInput()`, determining the command used is `health`. Then, `Handler.handleHealth()` determines type of `health` command used. + +#### Add BMI + +The user's input is processed to add a `Bmi` as follows: + +1. `parser.parseBmiInput()` splits the user's input string using `parser.splitBmiInput()` using the flags, returning a `String[]` variable. + +2. `validation.validateBmiInput()` is called to validate each parameter. If no exceptions caused by invalid parameters are thrown, the validated parameters are used to create the new `Bmi` object. + +3. The `Bmi` constructor creates a `HealthList` object, and adds the newly created object into `HealthList.BMIS` via `healthlist.addBmi()`. The BMI value and BMI category are determined from `Bmi.calculateBmiValue()` methods , and only the BMI Value is stored. + +4. The `Bmi` object is passed to `Output.printAddBmi()` and a message acknowledging the successful adding is printed to the screen. + +This is the sequence diagram for adding a BMI entry: + +![Bmi Sequence Diagram](img/sequence_diagrams/bmi_sequence.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Add Period + +The user's input is processed to add a `Period` as follows: + +1. `parser.parsePeriodInput()` splits the input using `parser.splitPeriodInput()` using the flags, returning a `String[]` variable. + - Method also extracts the optional end date parameter if present. + +2. `validation.validatePeriodInput()` is called to validate each parameter. If no exceptions caused by invalid parameters are thrown, the validated parameters are used to create the new `Period` object. + +3. The `Period` constructor adds the newly created object into `healthlist.PERIODS`. + +4. The `Period` object is passed to `output.printAddPeriod()` and a message acknowledging the successful adding is printed to the screen. + +Overloaded constructor is used to add the optional end date. + +This is the sequence diagram for adding a period entry: + +![Period Sequence Diagram](img/sequence_diagrams/period_sequence.png) + +##### Make Period Prediction + +The user's input is processed to make a period prediction if there are **at least 4 `Period` objects recorded** as follows: + +1. The `parser.parsePredictionInput()` method to process the user's prediction input. + +2. If the size of `PeriodList` is larger or equals to 4, `printLatestThreeCycles()` is called to print the latest three cycles. Else, an exception is thrown. + +3. `period.predictNextPeriodStartDate()` calls `period.nextCyclePrediction()` which further calls `period.getLastThreeCycleLengths()` to calculate the `sumOfCycleLengths` of latest three cycles. The `sumOfCycleLengths` is divided by `3` to find the average cycle length. + +4. The static `Period.printNextCyclePrediction()` method prints the predicted start date to the screen. + +![Period Prediction Sequence Diagram](img/sequence_diagrams/prediction_sequence_diagram.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +#### Add Appointment + +The user's input is processed to add an Appointment as follows: + +1. `parser.parseAppointmentInput()` splits the input using `parser.splitAppointmentDetails()` using the flags, returning a `String[]` variable. + +2. `validation.validateAppointmentDetails()` is called to validate each parameter. If no exceptions caused by invalid parameters are thrown, the validated parameters are used to create the new `Appointment` object. + +3. The `Appointment` constructor creates a new `HealthList` object and adds the newly created object into `HealthList.APPOINTMENTS` via `healthlist.addAppointment()`. + +4. The new `Appointment` object is passed to `Output.printAddAppointment()` and a message acknowledging the successful adding is printed to the screen. + +This is the sequence diagram for adding an Appointment from `parseAppointmentInput()`: + +![Appointment Sequence Diagram](img/sequence_diagrams/appointment_sequence.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### View History + +1. `parser.parseHistory()` is called to extract and validate the filter string entered via the `item` flag from user input. + +2. `validation.validateHistoryFilter()` is called to check whether the filter string is valid. If not, it throws an exception. + +3. `handler.handleHistory()` will then call `output.printHistory(filter)` which uses the `filter` string to determine which method to use, as shown below: + + - `workouts`: `output.printWorkoutHistory()` + - `run`: `output.printRunHistory()` + - `gym`: `output.printGymHistory()` + - `bmi`: `output.printBmiHistory()` + - `period`: `output.printPeriodHistory()` + - `appointment`: `output.printAppointmentHistory()` + +This is the sequence diagram for `history`: + +![History Sequence Diagram](img/sequence_diagrams/history_sequence.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +### View Latest + +1. `parser.parseLatest()` is called to extract and validate the filter string entered via the `item` flag from user input. + +2. `validation.validateDeleteAndLatestFilter()` is called to check whether the filter string is valid. If not, it throws an exception. Both `delete` and `latest` have the same set of filters. + +3. `handler.handleHistory()` will then call `output.printLatest(filter)` which uses the `filter` string to determine which method to use, as shown below: + + - `run`: `output.printLatestRun()` + - `gym`: `output.printLatestGym()` + - `bmi`: `output.printLatestBmi()` + - `period`: `output.printLatestPeriod()` + - `appointment`: `output.printLatestAppointment()` + +This is the sequence diagram for `latest`: + +![Latest Sequence Diagram](img/sequence_diagrams/latest_sequence.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Delete Item + +Deleting an item follows this sequence: + +1. User input is passed to `parser.parseDeleteInput()`, and the input is split by `Parser.splitDeleteInput()` using the flags, returning a `String[] deleteDetails` variable containing the `filter` string and `index` integer for the item to delete. + +2. Split `deleteDetails` are passed to `validation.validateDeleteInput()`. If no exceptions caused by invalid parameters are thrown, `String[] validDeleteDetails` is returned. + +3. `validDeleteDetails` is passed back to `Handler`, which calls the respective `deleteItem()` method from either `HealthList` or `WorkoutList` depending on the details passed. + + - `run`: `WorkoutList.deleteRun()` + - `gym`: `WorkoutList.deleteGym()` + - `bmi`: `HealthList.deleteBmi()` + - `period`: `HealthList.deletePeriod()` + - `appointment`: `Healthlist.deleteAppointment()` + +![Delete Sequence](img/sequence_diagrams/delete_sequence.png) + + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Storage of Data + +The storage is split into `DataFile` for the reading and saving of user data, and `LogFile` for writing logs. + +#### Saving Data + +Saving of data happens only when the `exit` command is used: + +1. Upon `exit` command being used, `handler.terminateBot()` is called, which calls `dataFile.saveDataFile()`. + +2. The name of the user, health data and workout data are written to `pulsepilot_data.txt` via `dataFile.writeName()`, `dataFile.writeHealthData()` and `dataFile.writeWorkoutData()`. + +3. To prevent tampering of the file, the SHA-256 hash of the data file is calculated via `dataFile.generateFileHash()` and written to `pulsepilot_hash.txt` via `writeHashToFile()`. + +![Storage Sequence](img/sequence_diagrams/storage_sequence.png) + +#### Reading Data + +The reading of files has been implemented as follows: + +1. The file hash from `pulsepilot_hash.txt` is read, and the SHA-256 hash of `pulsepilot_data.txt` is calculated. + - If the hashes do not match, the files have been tampered with. The data and hash file are deleted (if present), and PulsePilot exits. + - If one of the files is missing, PulsePilot attempts to delete both, and exits. User must run bot again to re-create both files. + +2. The first line is read and split to get the user's name. + +3. Subsequent lines contain the health and workout data stored, which is split and added to `HealthList` and `WorkoutList` respectively. + +#### Log File + +`pulsepilot_log.txt` is created when the bot starts if not present, and logs are added to it each time the user interacts with it. If the file is already present, PulsePilot appends to it. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +## Appendix: Requirements + +* [Product Scope](#product-scope) + * [Target User Profile](#target-user-profile) + * [Value Proposition](#value-proposition) +* [User Stores](#user-stories) +* [Non-Functional Requirements](#non-functional-requirements) +* [Glossary](#glossary) +* [Manual Testing](#manual-testing) + +
+ +### Product scope + +#### Target user profile + +PulsePilot is built for both patients and healthcare professionals. + +- Patients can upload data related to their well-being via the health tracker and progress on recovery exercises through the workout tracker. +- Healthcare professionals can use PulsePilot to easily monitor their patient's recovery progress and general well-being outside the hospital using the storage features the app provides. +- For users that are familiar with the CLI and can type fast. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Value proposition + +PulsePilot is a health monitoring application designed to bridge the information gap between medical professionals and patients during outpatient recovery. + +PulsePilot offers outpatients the capability to input and track a range of workouts for fitness or rehabilitation purposes, alongside crucial health parameters such as BMI and menstrual cycles. There is also a medical appointment tracker to monitor past and upcoming appointments. + +Simultaneously, PulsePilot facilitates access to this vital data for various healthcare professionals, ensuring comprehensive and seamless support in guiding outpatient recovery processes. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +### User Stories + +| Version | As a ... | So that I can ... | I want to ... | +|---------|-----------------------|-------------------------------------------------------|-----------------------------------| +| 1.0 | Gym enthusiast | Track my progress in the gym | Record my gym session details | +| 1.0 | Runner | See my relative speed for each run | See my running pace | +| 1.0 | Runner | Track my running progress over time | Record my runs | +| 1.0 | Health conscious user | Track change in my weight over time | Calculate my BMI | +| 1.0 | Female user | Monitor any deviations from my normal menstrual cycle | Track my menstrual cycle | +| 2.0 | Runner | Quickly view my most recent run details | See my latest run | +| 2.0 | Gym enthusiast | Quickly view my most recent gym session | See my latest gym session | +| 2.0 | Gym enthusiast | Accurately track my progress and strength gains | Enter varying weights for sets | +| 2.0 | Female user | Plan ahead and better manage my health | Predict my next period start date | +| 2.0 | Injured user | Remember the appointments I have | Track my medical appointments | +| 2.0 | Careless user | Remove erroneous entries caused by typos | Be able to delete items tracked | + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +### Non-Functional Requirements + +- **Usability**: The application should have a user-friendly command-line interface with clear instructions and prompts for user input. +- **Reliability**: The application should handle invalid or incomplete user input gracefully, providing appropriate error messages and prompting the user for correct input. +- **Maintainability**: The codebase should follow best practices for Object-Oriented Programming, including proper separation of concerns, modularization, and code documentation. +- **Testability**: The application should have comprehensive unit tests and integration tests to ensure correct functionality. +- **Data Integrity**: The application checks whether the data file has been tampered with, and deletes or creates files as needed. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +### Glossary + +| Term | Explanation | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Flags | The strings used by PulsePilot to differentiate parameters. For example, `/date:` is the date flag, used to specify the date for a given command. | +| UI | The User Interface (UI), which is the point of contact between users and our application. This component handles the user input, and prints messages or errors. | +| Storage | Responsible for saving data, and reading the data file to resume a previous save state. For our application, this also involves checking the integrity of the file and throwing errors if needed. | +| Lock File | A `.lck` file, created to prevent multiple processes or users from accessing a file. | + +###### [Back to table of contents](#table-of-contents) + +--- + + + +
+ +### Manual Testing + +* [Launching and Termination](#launching-and-termination) +* [Run](#run-testing) +* [Gym](#gym-testing) +* [Period](#period-testing) +* [Prediction](#prediction-testing) +* [BMI](#bmi-testing) +* [Appointment](#appointment-testing) +* [History](#history-testing) +* [Latest](#latest-testing) +* [Delete](#delete-testing) +* [Storage](#storage-testing) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +#### Launching and Termination + +##### Launching + +With Java 11 and the latest `pulsepilot.jar` installed from [here](https://github.com/AY2324S2-CS2113T-T09-4/tp/releases/tag/v2.1), PulsePilot should show the following output when first started: + +![Opening Prompt from PulsePilot](img/output/start_prompt.png) + +##### Termination + +1. Exit PulsePilot using the `exit` command. +2. A farewell message is printed as shown below: +3. `pulsepilot_hash.txt` is created and `pulsepilot_data.txt` will be written to upon exit. + +![Shutdown](img/output/shutdown.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +#### Run Testing + +**Adding a run:** + +- Test Case: `workout /e:run /d:5.15 /t:25:00` + **Expected Result**: Run added. Successful adding message is printed. + +- Test Case: `workout /e:run /d:15.15 /t:01:25:00 /date:25-02-2024` + **Expected Result**: Run added. Successful adding message is printed. + +- Test Case: `workout /e:run /d:25.00 /t:00:25:00` + **Expected Result**: Run not added. Error message asking user to use `MM:SS` as hours cannot be `00` is printed in red. + +- Test Case: `workout /e:run /d:30.00 /t:28:00` + **Expected Result**: Run not added. Error message stating that pace cannot be faster than `1.00/km` is printed in red. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +#### Gym Testing + +**Adding a gym:** + +- Test Case: + +This test case for gym has **multiple lines of input**. + +``` +workout /e:gym /n:1 +bench press /s:2 /r:4 /w:10,20 +``` + +**Expected Result**: Gym added successfully. Successful adding message is printed. + +- Test Case: `workout /e:gym /n:0` + + **Expected Result**: Gym not added. Error message stating that number of sets cannot be 0 is printed in red. + +- Test Case: + +This test case for gym has **multiple lines of input**. + +``` +workout /e:gym /n:2 +bench press /s:1 /r:4 /w:10 +smith's press /s:1 /r:4 /w:10 +``` + +**Expected Result**: Gym not added. Error message stating that gym station name can only have letters, and gym station prompt for station 2 is printed again. + +- Test Case: + +This test case for gym has **multiple lines of input**. + +``` +workout /e:gym /n:1 +bench press /s:2 /r:4 /w:10 +``` + +**Expected Result**: Gym not added. Error message stating that number of weight values must be the same as the number of sets is printed in red. + +- Test Case: + +This test case for gym has **multiple lines of inputs**. + +``` +workout /e:gym /n:2 +bench press /s:2 /r:4 /w:10,20 +back +``` + +**Expected Results**: The `Gym` object was not added because the `back` command was invoked. A message will be displayed stating that the latest Gym object has been removed, and you have been redirected to the main menu. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +#### BMI Testing + +**Adding a BMI:** + +- Test Case: `health /h:bmi /height:1.70 /weight:70.00 /date:29-04-2023` + - **Expected Result**: BMI added successfully. Successful adding message is printed. + +- Test Case: `health /h:bmi /height:1.70 /weight:0.00 /date:29-04-2023` + - **Expected Result**: BMI not added. Error message stating height and weight must be more than 0 is printed in red. + +- Test Case: `health /h:bmi /height:100.00 /weight:50.00 /date:29-04-2023` + - **Expected Result**: BMI not added. Error message stating that the tallest human being ever was 2.72m and to specify a height less than 2.75m is printed in red. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +#### Period Testing + +**Adding a Period:** + +Note that PulsePilot's stored items are cleared after each test case for **period testing only**. This can be done using the `delete` command. + +- Test Case: `health /h:period /start:10-03-2024 /end:17-03-2024` + **Expected Result**: Period is added. Successful adding message is printed. Notification that period length is out of healthy range is printed in red. + +- Test Case: `health /h:period /start:10-03-2024` + **Expected Result**: Period is added. Successful adding message is printed with end date set to NA. + +- Test Case: + +This test case for period has **multiple lines of input**. + +``` +health /h:period /start:10-03-2024 +health /h:period /start:10-03-2024 /end:16-03-2024 +health /h:period /start:10-04-2024 /end:16-04-2024 +``` + +**Expected Result**: Only 1 Period is added, with successful message printing twice. Error message stating that date specified cannot be later than today's date is printed in red. + +- Test Case: + +This test case for period has **multiple lines of input**. + +``` +health /h:period /start:10-03-2024 +health /h:period /start:10-04-2024 /end:16-04-2024 +``` + +**Expected Result**: 1 period is added, with successful message printing once. Second command causes an error message stating that either end date for previous period is still empty, or start dates of current period do not tally. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +#### Prediction Testing + +**Checking prediction with 4 valid periods added:** + +- Test Case: + +This test case for prediction has **multiple lines of input**. + +``` +health /h:period /start:10-12-2023 /end:16-12-2023 +health /h:period /start:10-01-2024 /end:16-01-2024 +health /h:period /start:10-02-2024 /end:16-02-2024 +health /h:period /start:10-03-2024 /end:16-03-2024 +health /h:prediction +``` + +**Expected Result**: Prediction successful. Last 3 periods and predicted start date is printed. + +**Checking prediction without 4 valid periods:** + +- Test Case: `health /h:prediction` + **Expected Result**: Prediction not made. Error message stating that there are not enough period cycles recorded is printed in red. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +#### Appointment Testing + +**Adding an appointment:** + +- Test Case: `health /h:appointment /date:19-03-2023 /description:surgery /time:19:00` + - **Expected Result**: Appointment added. Successful adding message is printed. + +- Test Case `health /h:appointment /date:19-03-2023 /description:;;; /time:19:00` + - **Expected Result**: Appointment not added. Error stating that description can only contain alphanumeric characters, spaces, inverted commas and quotes is printed. + + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +#### History Testing + +**Viewing History with 1 valid run and 1 valid gym:** + +- Test Case: `history /item:workouts` + - **Expected Result**: Run and Gym information is printed. + + +**Viewing history with no valid objects:** + +- Test Case: `history /item:appointment` + - **Expected Result**: Error message stating that no appointments have been found is printed in red. + +###### [Back to table of contents](#table-of-contents) + +--- + +#### Latest Testing + +**Viewing Latest with 1 valid BMI entry and 1 valid run:** + +- Test Case: + +This test case for latest has **multiple lines of input**. + +``` +latest /item:run +latest /item:bmi +latest /item:appointment +``` + +**Expected Result**: Run and BMI printed normally. Error message stating that no appointments found is printed in red. + +**Viewing Latest with no invalid string:** + +- Test Case: `latest /item:test` + - **Expected Result**: Error message stating that invalid item has been specified is printed in red. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +#### Delete Testing + +**Deleting a run:** + +- Test Case: + +This test case for delete has **multiple lines of input**. + +``` +workout /e:run /d:5.15 /t:25:00 +delete /item:run /index:1 +``` + +**Expected Result**: Run is deleted and delete message is printed. + +**Deleting gym that does not exist:** + +- Test Case: `delete /item:gym /index:1` + **Expected Result**: Error message stating invalid index to delete is printed in red. + + +###### [Back to table of contents](#table-of-contents) -## Design & implementation +--- -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +
+ -## Product scope -### Target user profile +#### Storage Testing -{Describe the target user profile} +**PulsePilot placed in a directory where read and write permissions are given**: -### Value proposition +- Test Case: Launching for first time: + **Expected Result**: Log file, log file lock and data file are created. -{Describe the value proposition: what problem does it solve?} +- Test Case: Missing hash file: + **Expected Result**: Error message stating key files for integrity are missing is printed in red, and bot exits. -## User Stories +- Test Case: Data file not present but hash file present: + **Expected Result**: Error message stating key files for integrity are missing is printed in red, and bot exits. -|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| +- Test Case: Data file hash does not match hash in hash file: + **Expected Result**: Error message stating data file integrity is compromised is printed in red, and bot exits. -## Non-Functional Requirements +**PulsePilot placed in a directory with no read or write permissions**: -{Give non-functional requirements} +- Test Case: Launching PulsePilot: -## Glossary + **Expected Result**: Error message stating that the application cannot read or write to the current directory is printed in red, and bot exits. -* *glossary item* - Definition -## Instructions for manual testing +###### [Back to table of contents](#table-of-contents) -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +--- diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..c97ce80ba0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# Duke +# PulsePilot -{Give product intro here} +PulsePilot is a **desktop app for tracking health-related information, optimised for users via a Command Line Interface (CLI)**. With the ability to type fast, users will be able to track their gym and run workouts, as well as their Body Mass Index (BMI), medical appointments and menstrual cycles. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..3450a59317 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,606 @@ # User Guide +![Logo](img/logo.jpg) + + + ## Introduction -{Give a product intro} +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + +
+ +## Table of Contents + +* [Quick Start](#quick-start) +* [Notes About Command Format](#notes-about-command-format) +* [User Induction](#user-induction) +* [Commands](#commands) + * [Workout: Run](#workout-run) + * [Workout: Gym](#workout-gym) + * [Adding Gym Stations](#adding-gym-stations) + * [Health: BMI](#health-bmi) + * [Health: Period](#health-period) + * [Health: Prediction](#health-prediction) + * [Health: Appointment](#health-appointment) + * [History](#history) + * [Latest](#latest) + * [Delete](#delete) + * [Help](#help) + * [Exit](#exit) +* [Logging](#logging) +* [Saving Data](#saving-data) +* [Known Issues](#known-issues) +* [Frequently Asked Questions (FAQ)](#faq) +* [Command Summary](#command-summary) + +
## Quick Start -{Give steps to get started quickly} +1. Ensure that you have Java 11 installed. +2. Download the latest `pulsepilot.jar` from [here](https://github.com/AY2324S2-CS2113T-T09-4/tp/releases/tag/v2.1). +3. Copy the file to the folder you want to use as the home folder for PulsePilot. +4. Open a command terminal: + - `cd` to the folder with `pulsepilot.jar` in it. + - Run `java -jar pulsepilot.jar`. +5. The application will display a welcome message if started successfully. + +**The bot will prompt you for your name before starting.** + +![Opening Prompt from PulsePilot](img/output/start_prompt.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +## Notes About Command Format + +* Parameters in `UPPER_CASE` are to be **supplied by the user**. +* Parameters in square brackets are **optional**. + * `[/date:DATE]` means that the `DATE` parameter is **optional**. +* If you are using a **PDF version of this document**, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. + * This can result in errors despite valid commands being used! + +> ⚠️ PulsePilot commands are **not case-sensitive**. All commands are converted to upper case before being processed. + +> ⚠️ The order of flags can be changed (for example, `/t: /d:` and `/d: /t:`) **unless mentioned otherwise**. + +> ⚠️ Ensure that the syntax is **exactly the same** as provided in the user guide. Avoid using extra characters in the commands, such as blank space, newline, etc. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +## User Induction + +1. When you first run the PulsePilot application, the bot will prompt you to enter your `name` to create a new user profile. + - Your `name` can only contain **alphanumeric characters (letters and numbers) and spaces**. + > ❗ **WARNING:** If you enter a name that does not follow this convention, the bot will display an error message and prompt you to try again. + + ![Non-compliance of naming convention](img/output/wrong_username.png) + +2. After entering a valid `name`, the bot will create a new user profile associated with your `name`. +This profile will be used to store all your health and workout data. +3. Once your user profile is created, you can start using the various commands in PulsePilot to track your progress and health. + - You may enter commands after receiving this message prompt: + + ![Accepting commands](img/output/accepting_commands.png) + +4. All your data will be saved and associated with your user profile + - You can continue tracking your information across multiple devices. Find out more [here](#faq). + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +## Commands + + + + +### Workout: Run + +Adds a new run session to track. + +Format: workout /e:run /d:DISTANCE /t:TIME [/date:DATE] + +* `DISTANCE` is a **2 decimal point positive number** (i.e. `15.24`) representing the distance ran in **kilometers**. +* `TIME` is in `[HH]:MM:SS` format (i.e. `25:30`). The `HH` representing hours is **optional**. +* `DATE` is in `DD-MM-YYYY` format (i.e. `19-03-2024`). The date is **optional**, and if not specified, defaults to `NA`. + +> ⚠️ If `HH` is set to `00`, the bot will throw an error. Please use `MM:SS` if the `HH` field is not needed! +> ⚠️ Date specified cannot be later than the current date! + +Examples: +- workout /e:run /d:5.15 /t:25:03 /date:25-03-2023 +- workout /e:run /d:5.15 /t:25:03 + +Expected Output: + +![Adding Runs](img/output/adding_runs.png) + +> ⚠️ **Minimum and Maximum inputs:** +> +> Maximum Pace Value: 30:00/km, Minimum Pace Value: 1:00/km +> +> Maximum Run Time: 99:59:59, Minimum Run Time: 00:01 +> +> Maximum Distance: 5000.00km, Minimum Distance: 0.01km +> +> **Note that exceeding these bounds will trigger an error!** + +###### [Back to table of contents](#table-of-contents) + +--- + + + +### Workout: Gym + +Adds a new gym session to track. + +Format: workout /e:gym /n:NUMBER_OF_STATIONS [/date:DATE] + +* `NUMBER_OF_STATIONS` is a **positive integer of at least 1** representing the number of stations for one Gym session. +* `DATE` is in `DD-MM-YYYY` format (i.e. `19-03-2024`). The date is **optional**, and if not specified, defaults to `NA`. + +> ⚠️ Date specified cannot be later than the current date! + +> ⚠️ Please input positive integers for `NUMBER_OF_STATIONS` without leading zeros. Entering a number with a leading zero, such as `01`, will trigger an error. + +Examples: +- workout /e:gym /n:2 /date:25-03-2023 +- workout /e:gym /n:4 + +#### Adding Gym Stations + +Upon entry of the workout /e:gym command, the bot will prompt for further details for each station done: + +Format: STATION_NAME /s:SET /r:REPS /w:WEIGHT + +* `STATION_NAME` is a **string** representing the name of the gym station. +* `SET` is a **positive integer** representing the number of sets done for one station. +* `REPS` is a **positive integer** representing the number of repetitions done per set for one station. +* `WEIGHT` is a **list of positive numbers** separated by commas. It represents the weights used for all the sets in the station. + +> ⚠️ `STATION_NAME` must always be the first parameter. The order of the other parameters can be in any order. `STATION_NAME` can **only contain letters and spaces**, and can be up to **25 characters long**. + +> ⚠️ `WEIGHT` must be in **multiples of 0.125 KG**! This is because the minimum weight increment in a gym is 0.125kg. Example `bench press /s:2 /r:10 /w:10.333,12.5` is not valid as 10.333 is not a multiple of 0.125kg. + +> ⚠️ Note that the **number of weights must equal to the number of sets**! For example, if you have done 2 sets at 10 kg, PulsePilot still expects 2 weights to be specified like this `squats /s:2 /r:5 /w:10,10`. + +> ⚠️ Please input positive integers for `SETS` and `REPS` without leading zeros. Entering a number with a leading zero, such as `01`, will trigger an error. + + +Examples: +- bench press /s:2 /r:4 /w:10,20 +- squat /r:2 /s:2 /w:10.5,20.5 + +Expected Output: + +![Adding Gyms](img/output/adding_gym.png) + +If you want to exit the gym station input prompt and go back to the main menu to use other commands, use `back` to do so. PulsePilot will delete the latest gym added, and you can then use the other commands. + +Expected Output: + +![Going Back](img/output/gym_station_back.png) + +> ⚠️ **Minimum and Maximum inputs:** +> +> Minimum number of stations: 1, Maximum number of stations: 50 +> +> Minimum Weight: 0kg, Maximum Weight: 2850kg +> +> 0kg is meant for exercises that **do not use any weights!** +> +> **Note that exceeding these bounds will trigger an error!** + +###### [Back to table of contents](#table-of-contents) + +___ + +
+ + + +### Health: BMI + +Calculates user's Body Mass Index (BMI) based on height and weight from user's input, and tracks it. + +Format: health /h:bmi /height:HEIGHT /weight:WEIGHT /date:DATE + +* `HEIGHT` is a **2 decimal point number in metres** (i.e. `1.71`) representing the user's height. +* `WEIGHT` is a **2 decimal point number in kilograms** (i.e. `60.50`) representing the user’s weight. +* `DATE` is in `DD-MM-YYYY` format (i.e. `19-03-2024`). + +> ⚠️ Date specified cannot be later than the current date! + +Examples: +* health /h:bmi /height:1.70 /weight:75.42 /date:19-03-2024 +* health /h:bmi /date:19-03-2024 /height:1.70 /weight:75.42 + +PulsePilot will categorize your BMI as follows: + +- BMI < 18.5 (less than 18.5): **Underweight** +- 18.5 <= BMI < 25.0 (more than or equal to 18.5 and less than 25.0): **Normal** +- 25.0 <= BMI < 30.0 (more than or equal to 25.0 and less than 30.0): **Overweight** +- 30.0 <= BMI < 40.0 (more than or equal to 30.0 and less than 40.0): **Obese** +- BMI >= 40.0 (more than 40.0): **Severely Obese** + +Expected Output: + +![Adding BMI](img/output/adding_bmi.png) + +> ⚠️ **Minimum and Maximum inputs:** +> +> Maximum Height: 2.75m, Minimum Height: 0.01m +> +> Maximum Weight: 640.00kg, Minimum Weight: 0.01kg +> +> **Note that exceeding these bounds will trigger an error!** + +###### [Back to table of contents](#table-of-contents) + +___ + +### Health: Period + +Tracks the start and end of user's menstrual cycle. + +Format: health /h:period /start:START_DATE [/end:END_DATE] + + +* `START_DATE` is `DD-MM-YYYY` format (i.e. `19-03-2024`) representing the first day of period flow, which is also the first day of the cycle. + +* `END_DATE` is `DD-MM-YYYY` format (i.e. `19-03-2024`) representing the last day of period flow. This parameter is **optional** and can be added once the period flow ends. To add the end date, use the command with `START_DATE` being the corresponding start date and `END_DATE` being the end date to be added. + +> ⚠️ Both start and end dates specified cannot be later than the current date! + +> ⚠️ The start date of a new period entry must come after the end date of the previous period entry. + +> ⚠️ An outstanding period entry must have an end date specified before a new entry can be added. + +> ⚠️ PulsePilot **does not** impose minimum or maximum length requirements for menstrual cycles, as underlying medical conditions can cause variations in cycle lengths. +> +> PulsePilot will only **notify** you if your cycle length is beyond the healthy range of **2 - 7 days**. + +Examples: + +* health /h:period /start:09-03-2022 /end:16-03-2022 +* health /start:09-03-2022 /end:16-03-2022 /h:period +* health /h:period /start:09-03-2022 + +Expected Output: + +![Adding Periods](img/output/adding_period.png) + +###### [Back to table of contents](#table-of-contents) + +___ + +
+ + + +### Health: Prediction + +Predicts user's next period start date. + +Format: health /h:prediction + +* There must be at least **4 periods** added before a prediction can be made. + +Expected Output: + +![Viewing Prediction](img/output/viewing_prediction.png) + +###### [Back to table of contents](#table-of-contents) + +___ + +
+ +### Health: Appointment + +Tracks both **previous and upcoming** medical appointments. + +Format: health /h:appointment /date:DATE /time:TIME /description:DESCRIPTION + +* `DATE` is a `DD-MM-YYYY` format (i.e. `03-04-2024`) representing the date of the appointment. + +* `TIME` is a `HH:mm` format (i.e. `14:15`) representing the time of the appointment in 24-hour format. -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +* `DESCRIPTION` is a string (i.e. `review checkup with surgeon`) representing the details of the appointment. The description can **only contain alphanumeric characters, spaces, inverted commas and quotes**. -## Features +> ⚠️ Any characters that are **NOT** mentioned above used in the description will trigger an error! Please only use the characters allowed. -{Give detailed description of each feature} +Examples: -### Adding a todo: `todo` -Adds a new item to the list of todo items. +* health /h:appointment /date:03-04-2024 /time:14:15 /description:review checkup with surgeon -Format: `todo n/TODO_NAME d/DEADLINE` +* health /date:03-04-2024 /description:review checkup with surgeon /time:14:15 /h:appointment -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Expected Output: -Example of usage: +![Adding Appointment](img/output/adding_appointment.png) -`todo n/Write the rest of the User Guide d/next week` +###### [Back to table of contents](#table-of-contents) -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +--- + +
+ + + +### History + +Prints all tracked instances of `run`, `gym`, `workouts`, `bmi`, `period`, `appointment`. + +Format: history /item:TYPE + +* `TYPE` is either `run`, `gym`, `workouts`, `bmi`, `period`, or `appointment`. + - `run` shows all entries of runs. + - `gym` shows all entries of gym. + - `workouts` shows all entries of gym and runs. + - `bmi` shows all BMI entries. + - `period` shows all Period entries. + - `appointment` shows all Appointment entries. + +> 💡 `workouts` prints a summary of the `run` and `gym` objects. Full details can be viewed using `history /item:run/gym` respectively. + +Examples: +* history /item:workouts +* history /item:appointment + +Expected Output: + +![Viewing History](img/output/viewing_history.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +### Latest + +Prints the **most recently added** instance of `run`, `gym`, `bmi`, `period`, `appointment`. + +Format: latest /item:TYPE + +* `TYPE` is either `run`, `gym`, `bmi`, `period` or `appointment`. + - `run` shows the latest run. + - `gym` shows the latest gym. + - `bmi` shows the latest BMI. + - `period` shows the latest Period. + - `appointment` shows the latest Appointment, which returns the **largest** date and time sorted by their numerical values. + +Examples: +* latest /item:appointment + +Expected Output: + +![Viewing Latest](img/output/viewing_latest.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +### Delete + +Delete a tracked item. + +Format: delete /item:TYPE /index:INDEX + +* `TYPE` is either `run`, `gym`, `bmi`, `period` or `appointment`. +* `INDEX` represents the index of the item to delete. + +> ⚠️ Please input positive integers for `INDEX` without leading zeros. Entering a number with a leading zero, such as `01`, will trigger an error. + +> ⚠️ The `INDEX` is based on the respective item lists. Use `history` to view the lists. + +Examples: + +* delete /item:run /index:2 + +Expected output: + +![Deleting Object](img/output/deleting.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + + +### Help + +Prints the help message. + +Format: help + +Expected output: + +![img.png](img/output/viewing_help.png) + +###### [Back to table of contents](#table-of-contents) + +--- + +### Exit + +Exits the bot **and writes to data file**. + +Format: exit + +Expected Output: + +![Exiting Bot](img/output/exit_bot.png) + +> ⚠️ Exiting the bot by closing the terminal or with Ctrl + C **will result in data being lost!** + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +## Logging + +When you exit PulsePilot, the latest logs are written to the `pulsepilot_log.txt` file. + +###### [Back to table of contents](#table-of-contents) + +--- + +## Saving Data + +Your data is saved to the `pulsepilot_data.txt` when you exit PulsePilot. Every time you exit the application via the `exit` command, `pulsepilot_data.txt` file is updated. + +> ❗ **_WARNING_:** If the `pulsepilot_data.txt` file becomes corrupted, there is a very low chance of recovering the data. + +> 💡 Ensure that you always have a _backup copy stored safely_ to prevent permanent data loss. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ +## Known Issues + +### Colours Not Rendering + +In some instances, the output from an error will result in odd characters being printed on screen: + +![Colour not rendering](img/output/colour_not_rendered.png) + +This color rendering issue is specific to Windows machines. The odd characters you see are actually escape sequences used to display color in the terminal. + +Windows 10 terminals do not have this color rendering feature enabled by default, whereas Windows 11 terminals do, hence displaying the colors correctly. + +This is what the output is supposed to look like when the same command is used on a Windows 11 machine: + +![Colour Rendered](img/output/correct_colour_render.png) + +This is a visual bug, and it can be safely ignored by the user. + +###### [Back to table of contents](#table-of-contents) + +--- + +
## FAQ -**Q**: How do I transfer my data to another computer? +**1.** How do I transfer my data to another computer? + +To transfer your data to another computer, make sure that `pulsepilot.jar` is placed in the **same folder** as `pulsepilot_data.txt` **and** `pulsepilot_hash.txt`. If done correctly, PulsePilot will recognize and synchronize your data. + +> ⚠️ Create a _backup copy_ of both `pulsepilot_data.txt` and `pulsepilot_hash.txt` prior to file transfer to avoid data corruption. +> +> The _backup copies_ should be stored in a **separate** folder location from where the original `pulsepilot.jar` is saved. + + +**2.** What happens if my data is corrupted or tampered with? + +> ❗ **_WARNING_: DO NOT** tamper with either `pulsepilot_data.txt` or `pulsepilot_hash.txt` to prevent **permanent** and **unrecoverable** loss of data. + + + +You may experience 2 scenarios: + +- A data file content corruption: -**A**: {your answer here} +![Data Corruption](img/output/data_corruption.png) + +Corruption of the `pulsepilot_data.txt` or `pulsepilot_hash.txt` files will result in **permanent and complete data loss**. + +- A missing file error: + +![Missing Files](img/output/missing_files.png) + +A missing file error occurs when either `pulsepilot_data.txt` or `pulsepilot_hash.txt` is missing when PulsePilot is run. For safety and security reasons, PulsePilot will automatically delete any remaining data files before exiting the application. + +**Both cases will inevitably result in permanent and complete data loss.** + +> ❗ **DATA RECOVERY:** In both cases, you may want to recover data by utilising **both** your _backup_ copies of `pulsepilot_data.txt` and `pulsepilot_hash.txt` to restore your data. + +Otherwise, if you have lost your data, you can reinitialize a new save file by running the command `java -jar pulsepilot.jar` again. + +**3.** Is my tracking data private and confidential? + +Yes, your data is secure and stored locally on your machine. PulsePilot does not have any features that would allow it to send your data elsewhere. + +**4.** What happens if I specify extra flags on accident? + +Note that if you add duplicate or extra flags, the bot **will read the first instance only**. + +**All other parameters will be ignored.** + +For example: + +``` +workout /e:run /d:5.25 /t:59:50 /d:10.55 +``` + +In the above output, the bot will read `5.25` as the distance. The second `/d:10.55` is ignored. + +**5.** What if I keep receiving an error message even though my input seems to follow the instructions given in the user guide? + +Please ensure that you follow the command syntax given **exactly** in the user guide. Some examples of mistakes that could be easily overlooked: + +Example of the correct command: + +![correct_command.png](img/correct_command.png) + +- Error of adding extra space(s) in fixed parameters: + - In this case, the altered fixed parameter is `/date:`, which was written as `/ date:` instead. + +![extra_space_error_command.png](img/extra_space_error_command.png) + +- Error of adding extra newline(s) after command: + +![extra_newline_error_command.png](img/extra_newline_error_command.png) + +Avoid using extra characters in the commands, such as blank space, newline, etc. + +###### [Back to table of contents](#table-of-contents) + +--- + +
+ + ## Command Summary -{Give a 'cheat sheet' of commands here} +| Action | Format, Examples | +|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Print help | `help` | +| Add Run | `workout /e:run /d:DISTANCE /t:TIME [/date:DATE]`
Example: `workout /e:run /d:5.24 /t:25:23 /date:19-03-2024` | +| Add Gym | `workout /e:gym /n:NUMBER_OF_STATIONS [/date:DATE]`
Example: `workout /e:gym /n:4` | +| Add BMI | `health /h:bmi /height:HEIGHT /weight:WEIGHT /date:DATE`
Example: `health /h:bmi /height:1.70 /weight:75.42 /date:19-03-2024` | +| Add Period | `health /h:period /start:START_DATE [/end:END_DATE]`
Example: `health /h:period /start:09-03-2024 /end:16-03-2024` | +| Show Period Prediction | `health /h:prediction` | +| Add Appointment | `health /h:appointment /date:DATE /time:TIME /description:DESCRIPTION`
Example: `health /h:appointment /date:29-04-2025 /time:12:00 /description:knee surgery` | +| View history | `history /item:TYPE`
Example: `history /item:run` | +| View latest | `latest /item:TYPE`
Example: `latest /item:bmi` | +| Deleting item | `delete /item:TYPE /index:INDEX`
Example: `delete /item:run /index:1` | +| Exit bot | `exit` | + +###### [Back to table of contents](#table-of-contents) -* Add todo `todo n/TODO_NAME d/DEADLINE` +--- \ No newline at end of file diff --git a/docs/img/architecture_diagram.png b/docs/img/architecture_diagram.png new file mode 100644 index 0000000000..0304e74041 Binary files /dev/null and b/docs/img/architecture_diagram.png differ diff --git a/docs/img/class_diagrams/gym_class_diagram.png b/docs/img/class_diagrams/gym_class_diagram.png new file mode 100644 index 0000000000..ccbf49fd96 Binary files /dev/null and b/docs/img/class_diagrams/gym_class_diagram.png differ diff --git a/docs/img/class_diagrams/healthlist_class_diagram.png b/docs/img/class_diagrams/healthlist_class_diagram.png new file mode 100644 index 0000000000..7679439c07 Binary files /dev/null and b/docs/img/class_diagrams/healthlist_class_diagram.png differ diff --git a/docs/img/class_diagrams/workoutlist_class_diagram.png b/docs/img/class_diagrams/workoutlist_class_diagram.png new file mode 100644 index 0000000000..748e46170a Binary files /dev/null and b/docs/img/class_diagrams/workoutlist_class_diagram.png differ diff --git a/docs/img/correct_command.png b/docs/img/correct_command.png new file mode 100644 index 0000000000..861a844c3a Binary files /dev/null and b/docs/img/correct_command.png differ diff --git a/docs/img/extra_newline_error_command.png b/docs/img/extra_newline_error_command.png new file mode 100644 index 0000000000..b21f2e7c69 Binary files /dev/null and b/docs/img/extra_newline_error_command.png differ diff --git a/docs/img/extra_space_error_command.png b/docs/img/extra_space_error_command.png new file mode 100644 index 0000000000..005c6e7ddc Binary files /dev/null and b/docs/img/extra_space_error_command.png differ diff --git a/docs/img/logo.jpg b/docs/img/logo.jpg new file mode 100644 index 0000000000..3087d4701f Binary files /dev/null and b/docs/img/logo.jpg differ diff --git a/docs/img/output/accepting_commands.png b/docs/img/output/accepting_commands.png new file mode 100644 index 0000000000..8b4c930e49 Binary files /dev/null and b/docs/img/output/accepting_commands.png differ diff --git a/docs/img/output/adding_appointment.png b/docs/img/output/adding_appointment.png new file mode 100644 index 0000000000..c8b7c1f429 Binary files /dev/null and b/docs/img/output/adding_appointment.png differ diff --git a/docs/img/output/adding_bmi.png b/docs/img/output/adding_bmi.png new file mode 100644 index 0000000000..bac09e5748 Binary files /dev/null and b/docs/img/output/adding_bmi.png differ diff --git a/docs/img/output/adding_gym.png b/docs/img/output/adding_gym.png new file mode 100644 index 0000000000..1c23a5f1f7 Binary files /dev/null and b/docs/img/output/adding_gym.png differ diff --git a/docs/img/output/adding_period.png b/docs/img/output/adding_period.png new file mode 100644 index 0000000000..0611888e61 Binary files /dev/null and b/docs/img/output/adding_period.png differ diff --git a/docs/img/output/adding_runs.png b/docs/img/output/adding_runs.png new file mode 100644 index 0000000000..927f8ee17f Binary files /dev/null and b/docs/img/output/adding_runs.png differ diff --git a/docs/img/output/colour_not_rendered.png b/docs/img/output/colour_not_rendered.png new file mode 100644 index 0000000000..9bcd234d71 Binary files /dev/null and b/docs/img/output/colour_not_rendered.png differ diff --git a/docs/img/output/correct_colour_render.png b/docs/img/output/correct_colour_render.png new file mode 100644 index 0000000000..b06bc0edba Binary files /dev/null and b/docs/img/output/correct_colour_render.png differ diff --git a/docs/img/output/data_corruption.png b/docs/img/output/data_corruption.png new file mode 100644 index 0000000000..b355e74864 Binary files /dev/null and b/docs/img/output/data_corruption.png differ diff --git a/docs/img/output/deleting.png b/docs/img/output/deleting.png new file mode 100644 index 0000000000..bf7df0a972 Binary files /dev/null and b/docs/img/output/deleting.png differ diff --git a/docs/img/output/exit_bot.png b/docs/img/output/exit_bot.png new file mode 100644 index 0000000000..e9287f022d Binary files /dev/null and b/docs/img/output/exit_bot.png differ diff --git a/docs/img/output/gym_station_back.png b/docs/img/output/gym_station_back.png new file mode 100644 index 0000000000..3b9bea4238 Binary files /dev/null and b/docs/img/output/gym_station_back.png differ diff --git a/docs/img/output/missing_files.png b/docs/img/output/missing_files.png new file mode 100644 index 0000000000..8b1d05a34f Binary files /dev/null and b/docs/img/output/missing_files.png differ diff --git a/docs/img/output/shutdown.png b/docs/img/output/shutdown.png new file mode 100644 index 0000000000..a1eff01d84 Binary files /dev/null and b/docs/img/output/shutdown.png differ diff --git a/docs/img/output/start_prompt.png b/docs/img/output/start_prompt.png new file mode 100644 index 0000000000..92d0e0fdd8 Binary files /dev/null and b/docs/img/output/start_prompt.png differ diff --git a/docs/img/output/viewing_help.png b/docs/img/output/viewing_help.png new file mode 100644 index 0000000000..ba721084a0 Binary files /dev/null and b/docs/img/output/viewing_help.png differ diff --git a/docs/img/output/viewing_history.png b/docs/img/output/viewing_history.png new file mode 100644 index 0000000000..b78b68ffbf Binary files /dev/null and b/docs/img/output/viewing_history.png differ diff --git a/docs/img/output/viewing_latest.png b/docs/img/output/viewing_latest.png new file mode 100644 index 0000000000..77557a8a2e Binary files /dev/null and b/docs/img/output/viewing_latest.png differ diff --git a/docs/img/output/viewing_prediction.png b/docs/img/output/viewing_prediction.png new file mode 100644 index 0000000000..c52c6da030 Binary files /dev/null and b/docs/img/output/viewing_prediction.png differ diff --git a/docs/img/output/wrong_username.png b/docs/img/output/wrong_username.png new file mode 100644 index 0000000000..eb1896c488 Binary files /dev/null and b/docs/img/output/wrong_username.png differ diff --git a/docs/img/sequence_diagrams/appointment_sequence.png b/docs/img/sequence_diagrams/appointment_sequence.png new file mode 100644 index 0000000000..912889f4e4 Binary files /dev/null and b/docs/img/sequence_diagrams/appointment_sequence.png differ diff --git a/docs/img/sequence_diagrams/bmi_sequence.png b/docs/img/sequence_diagrams/bmi_sequence.png new file mode 100644 index 0000000000..84a3d9c2ee Binary files /dev/null and b/docs/img/sequence_diagrams/bmi_sequence.png differ diff --git a/docs/img/sequence_diagrams/delete_sequence.png b/docs/img/sequence_diagrams/delete_sequence.png new file mode 100644 index 0000000000..8c47701a31 Binary files /dev/null and b/docs/img/sequence_diagrams/delete_sequence.png differ diff --git a/docs/img/sequence_diagrams/gym_overall_sequence_diagram.png b/docs/img/sequence_diagrams/gym_overall_sequence_diagram.png new file mode 100644 index 0000000000..15fa217f41 Binary files /dev/null and b/docs/img/sequence_diagrams/gym_overall_sequence_diagram.png differ diff --git a/docs/img/sequence_diagrams/gym_station_sequence_diagram.png b/docs/img/sequence_diagrams/gym_station_sequence_diagram.png new file mode 100644 index 0000000000..e36fea0243 Binary files /dev/null and b/docs/img/sequence_diagrams/gym_station_sequence_diagram.png differ diff --git a/docs/img/sequence_diagrams/handler_sequence_diagram.png b/docs/img/sequence_diagrams/handler_sequence_diagram.png new file mode 100644 index 0000000000..809ee17cfc Binary files /dev/null and b/docs/img/sequence_diagrams/handler_sequence_diagram.png differ diff --git a/docs/img/sequence_diagrams/history_sequence.png b/docs/img/sequence_diagrams/history_sequence.png new file mode 100644 index 0000000000..5137bfafe0 Binary files /dev/null and b/docs/img/sequence_diagrams/history_sequence.png differ diff --git a/docs/img/sequence_diagrams/latest_sequence.png b/docs/img/sequence_diagrams/latest_sequence.png new file mode 100644 index 0000000000..677ced2cc2 Binary files /dev/null and b/docs/img/sequence_diagrams/latest_sequence.png differ diff --git a/docs/img/sequence_diagrams/period_sequence.png b/docs/img/sequence_diagrams/period_sequence.png new file mode 100644 index 0000000000..bc226604d2 Binary files /dev/null and b/docs/img/sequence_diagrams/period_sequence.png differ diff --git a/docs/img/sequence_diagrams/prediction_sequence_diagram.png b/docs/img/sequence_diagrams/prediction_sequence_diagram.png new file mode 100644 index 0000000000..46587f07c4 Binary files /dev/null and b/docs/img/sequence_diagrams/prediction_sequence_diagram.png differ diff --git a/docs/img/sequence_diagrams/run_sequence_diagram.png b/docs/img/sequence_diagrams/run_sequence_diagram.png new file mode 100644 index 0000000000..f542f6a1b8 Binary files /dev/null and b/docs/img/sequence_diagrams/run_sequence_diagram.png differ diff --git a/docs/img/sequence_diagrams/storage_sequence.png b/docs/img/sequence_diagrams/storage_sequence.png new file mode 100644 index 0000000000..52376b842c Binary files /dev/null and b/docs/img/sequence_diagrams/storage_sequence.png differ diff --git a/docs/team/j013n3.md b/docs/team/j013n3.md new file mode 100644 index 0000000000..848b905e9a --- /dev/null +++ b/docs/team/j013n3.md @@ -0,0 +1,36 @@ +# j013n3 - Project Portfolio Page + +## Overview + +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + +### Summary of Contributions + +The code I have written is documented [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=j013n3&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other) + +#### Features Implemented + +- Implemented Health and HealthList with the help of [@syj02](https://github.com/syj02). + +- Implemented the Bmi class with the help of [@syj02](https://github.com/syj02). + - Users can obtain their BMI value and the corresponding BMI category based on the height and weight inputted. + - Added validation to prevent users from adding BMI inputs of the same date. + +- Implemented the second stage of the Period class. + - Users can use the predictive feature to predict their next period's start date. + - Enhancement: Allow user to have the flexibility of input the start and end dates of their period flow either at once or at different times. + - Added validation to prevent users from starting a new Period entry before completing the outstanding one. + - Added validation to restrict users from inputting period entries dated before the end date of the latest Period input in PulsePilot. + +#### Contributions to the UG + +- Wrote UG portion for adding new Bmi and new Period inputs. + +#### Contributions to the DG + +- Crafted sequence diagrams for Bmi, Period, Appointment and Prediction. +- Wrote the DG portion for add Bmi, add Period and make prediction. + +#### Contributions beyond the project team + +- Reported an above-average number of bugs in PE_D (11 bugs in total). 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/justinsoh.md b/docs/team/justinsoh.md new file mode 100644 index 0000000000..65a60d499e --- /dev/null +++ b/docs/team/justinsoh.md @@ -0,0 +1,48 @@ +# JustinSoh - Project Portfolio Page + +## Overview + +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + +### Summary of Contributions + +The code I have written is documented [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=JustinSoh&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23). + +Below is the breakdown of what I have done. + +#### Features Added + +- Implemented the `WorkoutList` class, to help track and manage newly created `Run` and `Gym` objects. + +- Implemented the final `Workout`, `Gym`, `GymStation`, and `GymSet` classes. + +#### Enhancements Implemented + +- Implemented the `Parser.parseGymStationInput()` method, taking gym station input from the user. + +- Assisted with `Storage` through the implementation of the `Gym.toFileString()` and `Parser.parseGymFileInput()`, allowing [@L5-Z](https://github.com/L5-Z) to easily integrate the saving/loading of Gym classes. + +- Refactored static methods into non-static methods where instances are used instead to adopt a more Object-Oriented approach. + +- Wrote integration test cases for the `Health` package supported with the `TestHelper` package to simulate end-to-end testing. + +- Wrote test cases for the `Gym`, `GymStation`, `WorkoutList`, `Output`, and `Parser` to help increase coverage. + +#### Documentation Written + +##### User Guide + +- Wrote UG portion for the `Gym` and `GymStation` sections. + +##### Developer Guide + +- Wrote DG portion for the `Gym` and `GymStation` sections. +- Created sequence diagrams for `Gym` and `GymStation`. +- Worked with [@rouvinerh](https://github.com/rouvinerh) to review sequence diagrams for `Run`, `Latest`, `History`, and `Handler` sections. + +#### Other Contributions + +##### Project Management + +- Liaised with [@L5_Z](https://github.com/L5-Z) and [@rouvinerh](https://github.com/rouvinerh) to integrate the `Handler` and `Storage` classes with the `Run` and `Gym` classes. +- Created integration tests to ensure better coverage. diff --git a/docs/team/l5-z.md b/docs/team/l5-z.md new file mode 100644 index 0000000000..ee349ae7a5 --- /dev/null +++ b/docs/team/l5-z.md @@ -0,0 +1,66 @@ +# L5-Z - Project Portfolio Page + +## Overview + +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + + +### Summary of Contributions + +The code I have written is documented [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=l5-z&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other). + +Below is the breakdown of what I have done. + +#### Features Added + +- Implemented the main points of the `Handler` class, to help direct user inputs to all other classes and support creation of all objects. + - Performs substring extraction to pass as parameters to the relevant classes and successfully execute commands + +- Implemented the `DataFile` class, to help allow for persistence through a stored and loaded `.txt` file. + - File is highly resistant to tampering through a hashing function and hash comparison. + +- Implemented all outputs methods relating to UI such as ASCII Art within the `Output` class. + + +#### Enhancements Implemented + +- Implemented the `Parser.extractSubstringFromSpecificIndex()` method, which allows for flexible use case and implementation across all classes that require substring attached to relevant flags in order to function. Allows for flags and substrings to not be declared in order. + +- Implemented the `LogFile.getInstance()` allowing the team to instantiate a singular instance of LogFile to be used across all classes. + +- Refactored Constants into smaller, specific classes to unify all similar constant usages. + - Supports readability and searching of constants specific to a class + - Common constants used across all classes in `ErrorConstant` and `UiConstant`. + +- Worked with [@JustinSoh](https://github.com/JustinSoh) to easily integrate the saving/loading of Gym classes. + +- Wrote the skeletal structure of various classes for other team members to fill, including `Appointment`. + +- Refactored long methods where applicable, and moved author tags around. + +- Wrote Integration Test cases for the bulk of `Handler` and `Datafile` classes. + +#### Documentation Written + + +##### User Guide + +- Reviewed and improved clarity of the language used in UG and ensured consistency throughout the document. +- Replaced expected output with images. +- Wrote UG portion for `Help`, `Exit`, `DataFile`(Storage), `Logging` and **FAQ**. + + +##### Developer Guide + +- Wrote DG portion for `DataFile`(Storage) and `Handler`. +- Worked with [@rouvinerh](https://github.com/rouvinerh) to create sequence diagrams for Handler and DataFile. + + +#### Other Contributions + +##### Project Management + +- Helped to ensure that the dashboard icons for our team members and our team is green. + - Worked with our TA and [@JustinSoh](https://github.com/JustinSoh) to resolve an issue with the dashboard's weekly requirement. +- Liaised between [@JustinSoh](https://github.com/JustinSoh) and [@rouvinerh](https://github.com/rouvinerh) to integrate the `Handler` and `DataFile` class with the `Run` and `Gym` classes. +- Liaised between [@syj02](https://github.com/syj02) and [@j013n3](https://github.com/j013n3) to integrate the `Handler` and `DataFile` class with the `Bmi`, `Period` and `Appointment` classes. \ No newline at end of file diff --git a/docs/team/raajamani.md b/docs/team/raajamani.md new file mode 100644 index 0000000000..0303d0b8c0 --- /dev/null +++ b/docs/team/raajamani.md @@ -0,0 +1,25 @@ +# raajamani - Project Portfolio Page + +## Overview + +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + +### Summary of Contributions + +The code I have written is documented [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=raajamani&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other). + +#### Features Implemented + + +#### Contributions to the UG + + +#### Contributions to the DG + + +##### Review/mentoring contributions + + +#### Contributions beyond the project team + + diff --git a/docs/team/rouvinerh.md b/docs/team/rouvinerh.md new file mode 100644 index 0000000000..058b159677 --- /dev/null +++ b/docs/team/rouvinerh.md @@ -0,0 +1,66 @@ +# rouvinerh - Project Portfolio Page + +## Overview + +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + + +### Summary of Contributions + +The code I have written is documented [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=rouvinerh&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other). + +Below is the breakdown of what I have done. + +#### Features Added + +- Implemented the first iteration of the `Run`, `Gym` and `Workout` classes. + - This allows PulsePilot to track the user's workout information. +- Implemented the `latest`, `delete` and `history` commands. + - Allows user to view either their latest or all objects added to PulsePilot based on a filter string. + - Allows user to delete items from PulsePilot. + +#### Enhancements Implemented + +- Added `Validation` class to handle input validation from both user and data file. This involved refactoring my teammates' code into one central class for data validation, and moving the author tags, and adding other methods. + - This allows our team to easily handle all input validation, since methods are abstracted in a separate class. + - The `Validation` class prevents users from entering malformed input and makes our code more defensive against bugs or unintended use. + - It also allows for different error messages to be printed for the user, ensuring they are clear on what is wrong. + +- Wrote a majority of `ValidationTest` test cases. + +- Worked [@JustinSoh](https://github.com/JustinSoh) to finalise `Workout` and `WorkoutLists`. +- Wrote test cases for `Validation`, `Handler`, `Parser`, and `Output` to increase coverage. +- Implemented the final version of `Run`. + - Split user input validation and calculation to prevent doing double work. + +#### Documentation Written + +##### User Guide + +- Wrote the skeletal structure of the UG for other team members to fill. +- Replaced all expected output with images. +- Wrote UG portion for Run, History, Latest and Delete. + +##### Developer Guide + +- Wrote the skeletal structure of the DG for other team members to fill. +- Worked with [@JustinSoh](https://github.com/JustinSoh) to create sequence diagrams for the Run, Gym, Latest, History, and Handler sections. +- Worked with [@L5-Z](https://github.com/L5-Z) to create sequence diagrams for Storage section. +- Wrote DG for Run, Latest, History and Delete sections. +- Wrote manual testing portion of DG. +- Reduced complexity of diagrams and guide to ensure that it is not over cluttered. + +#### Other Contributions + +- Editing of Javadocs where needed. +- Adding author tags. +- Writing PlantUML code for diagrams in DG and replacing images where needed. +- Helped teammates with whatever code issues they were facing when needed. +- Refactored magic strings and numbers where needed. + +##### Project Management + +- Helped to ensure that the dashboard icons for our team members and our team is green. +- Worked with our TA and [@JustinSoh](https://github.com/JustinSoh) to make sure our sequence and class diagrams were implemented properly and were not too complicated. +- Reviewed PRs where needed. +- Tagged issues to teammates, and ensured that it closed before moving on. \ No newline at end of file diff --git a/docs/team/syj02.md b/docs/team/syj02.md new file mode 100644 index 0000000000..81a6af8c60 --- /dev/null +++ b/docs/team/syj02.md @@ -0,0 +1,49 @@ +# syj02 - Project Portfolio Page + +## Overview + +**PulsePilot** is a desktop application designed for **efficiently tracking health-related information** through a **Command Line Interface (CLI)**. For users who can type quickly, the CLI allows for faster data entry compared to traditional Graphical User Interface (GUI) applications on phones or computers. + +### Summary of Contributions + +The code I have contributed is [here](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=syj02&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-02-23&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other). + +#### Features Implemented + +- Implemented Health and HealthList with the help of [@j013n3](https://github.com/j013n3) + - Health is the superclass of Bmi, Period, and Appointment that is managed by HealthList class. + - HealhtList consists of methods to add new Health object, delete and print all Health objects recorded. + +- Implemented Bmi class and its tests with the help of [@j013n3](https://github.com/j013n3) + - For users to obtain their BMI value and which category that falls under (i.e. underweight, overweight, etc), from their height and weight input + +- Implemented the first stage of the Period class and its tests. + - The recording of Period object and the calculation of period length. + +- Implemented the Appointment class and its tests. + - The recording of Appointment object with date, time, and description to be specified. + - Add parse time method for users to add their time in LocalTime type. + +#### Enhancements Implemented + +- In health package: Modified all the add health object methods to sort the respective health list according to the given date input. + - BMI and Period in descending order (i.e. The most recent date at the top) for users to see their most updated record and not just the last record added, just in case, they forgot to add a past record. + - Appointment in ascending order (i.e. The earliest date and time at the top) for users to have a view of all appointments history. + +#### Contributions to the UG + +I wrote the UG portion of Appointment and Period prediction, including the description of each parameter, example commands, expected output, warnings and tips for users. + +#### Contributions to the DG + +- Overview of components and the Architecture diagram. +- Design of Health package and HealthList class diagram. +- Add descriptions of add Appointment command. + +#### Contributions beyond the project team + +- Reported a high number of bugs in PE-D (Top 10%). + +#### Project management + +- Interfaced between [@l5-z](https://github.com/l5-z) and [@j013n3](https://github.com/j013n3) to integrate the `Handler` and `DataFile` class with the `Bmi`, `Period` and `Appointment` classes. diff --git a/src/main/java/constants/ErrorConstant.java b/src/main/java/constants/ErrorConstant.java new file mode 100644 index 0000000000..319049e64b --- /dev/null +++ b/src/main/java/constants/ErrorConstant.java @@ -0,0 +1,278 @@ +package constants; + +/** + * ErrorConstant class contains constants for various types of errors that may occur in the application. + * The constants are used to provide descriptive error messages to the user when errors occur. + */ +public class ErrorConstant { + public static final String COLOR_HEADING = "\u001b[31m"; + + public static final String COLOR_ENDING = "\u001b[0m"; + public static final String INVALID_INPUT_HEADER = "Invalid Input Exception: "; + + public static final String INSUFFICIENT_INPUT_HEADER = "Insufficient Input Exception: "; + + public static final String OUT_OF_BOUND_HEADER = "Out of Bounds Error: "; + public static final String NEGATIVE_VALUE_ERROR = "Requires a positive integer!"; + public static final String INVALID_INDEX_DELETE_ERROR = "Invalid index to delete!"; + public static final String INVALID_INDEX_SEARCH_ERROR = "Given index is invalid."; + + public static final String INVALID_INDEX_ERROR = "Index must be a valid positive integer."; + public static final String SAVE_ERROR = "File save failed. Write error occurred:"; + public static final String LOAD_ERROR = "File read error:" + "Error at "; + public static final String CREATE_FILE_ERROR = "Unable to create file."; + public static final String CORRUPT_ERROR = "File is corrupted!" + + System.lineSeparator() + "Deleting 'pulsepilot_data.txt' and 'pulsepilot_hash.txt'. Try running again!" + + System.lineSeparator() + UiConstant.PARTITION_LINE; + public static final String DATA_INTEGRITY_ERROR = "Data file integrity compromised. Exiting."; + public static final String MISSING_INTEGRITY_ERROR = "Key files for integrity missing. Exiting."; + public static final String HASH_ERROR = "Error occurred while processing file hash."; + public static final String LOAD_GYM_FORMAT_ERROR = LOAD_ERROR + "Format of gym entry is incorrect/corrupted"; + public static final String LOAD_GYM_TYPE_ERROR = LOAD_ERROR + "Format of gym type is incorrect/corrupted"; + public static final String LOAD_NUMBER_OF_STATION_ERROR = LOAD_ERROR + "Number of stations is corrupted"; + public static final String INVALID_COMMAND_ERROR = "Invalid command. Enter 'help' to view " + + "available commands."; + public static final String NO_DATE_SPECIFIED_ERROR = "NA"; + public static final String INVALID_DATE_ERROR = "Invalid date format. Format is DD-MM-YYYY in integers. " + + "Make sure a valid date is entered (take note of leap years)!"; + public static final String INVALID_YEAR_ERROR = "Year has to be after 1967!"; + public static final String INVALID_LEAP_YEAR_ERROR = "29 Feb does not exist in this year!"; + public static final String PARSING_DATE_ERROR ="Error parsing date!"; + public static final String INVALID_ACTUAL_TIME_ERROR = "Invalid time format. Format is HH:MM in 24 hours format!"; + public static final String INVALID_ACTUAL_TIME_MINUTE_ERROR = "Minutes must be a positive integer " + + " 00 and 59."; + public static final String INVALID_ACTUAL_TIME_HOUR_ERROR = "Hours must be a positive integer between 00 and 23"; + public static final String PARSING_TIME_ERROR = "Error parsing time!"; + public static final String INSUFFICIENT_DELETE_PARAMETERS_ERROR = "Insufficient parameters for delete! " + + "Example input: /item:item /index:index" + + System.lineSeparator() + + "Only input what is required! Additional characters between flags will cause errors."; + public static final String RUN_EMPTY_ERROR = "No runs found! You need to add a run entry first!"; + public static final String GYM_EMPTY_ERROR = "No gyms found! You need to add a gym entry first!"; + public static final String WORKOUTS_EMPTY_ERROR = "No workouts found! You need to add " + + "either a run or a gym entry first!"; + public static final String APPOINTMENT_EMPTY_ERROR = "No appointments found! You need to add an " + + "appointment first!"; + public static final String BMI_EMPTY_ERROR = "No BMI entries found! You need to add a BMI entry first!"; + public static final String PERIOD_EMPTY_ERROR = "No periods found! You need to add a period entry first!"; + public static final String INSUFFICIENT_RUN_PARAMETERS_ERROR = "Insufficient parameters for run! " + + "Example input: /e:run /d:5.25 /t:25:23 [/date:DATE]" + + System.lineSeparator() + + "Only input what is required! Additional characters between flags will cause errors."; + public static final String INVALID_RUN_DISTANCE_ERROR = "Distance is a 2 decimal point positive number!"; + public static final String INVALID_RUN_TIME_ERROR = "Invalid time format. Format is either HH:MM:SS or " + + "MM:SS with integers!"; + public static final String INVALID_SECOND_ERROR = "Seconds must be a positive integer between 00 and 59!"; + public static final String INVALID_HOUR_ERROR = "Hours is excluded if set to 00. Use MM:SS instead!"; + public static final String INVALID_MINUTE_ERROR = "Minutes must be a positive integer between 01 and 59!"; + public static final String INVALID_GYM_STATION_FORMAT_ERROR = "Remember that you are now adding gym station input!" + + System.lineSeparator() + + "Expected format: [Station Name] /s:[SETS] /r:[REPS] /w:[WEIGHTS]"; + public static final String INSUFFICIENT_GYM_PARAMETERS_ERROR = "Insufficient parameters for gym! " + + "Example input: /e:gym /n:2 [/date:DATE]" + + System.lineSeparator() + + "Only input what is required! Additional characters between flags will cause errors."; + public static final String INVALID_NUMBER_OF_STATIONS_ERROR = "Number of stations is a positive number!" + + System.lineSeparator() + + "For instance, '/n:a' is invalid as it is not a number" + + INVALID_GYM_STATION_FORMAT_ERROR; + public static final String INVALID_GYM_STATION_EMPTY_NAME_ERROR = "Gym station name cannot be blank!" + + System.lineSeparator() + + "Please input an station name" + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + + public static final String INVALID_GYM_STATION_NAME_ERROR = "Gym station name can only have letters and cannot " + + "be more than 25 characters!" + + System.lineSeparator() + + "Please input a shorter name." + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + + public static final String INVALID_SETS_POSITIVE_DIGIT_ERROR = "Number of sets must be a positive integer!" + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + public static final String INVALID_REPS_POSITIVE_DIGIT_ERROR = "Number of reps must be a positive integer!" + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + public static final String INVALID_WEIGHTS_VALUE_ERROR = "The weight done for each set must " + + "be a multiple of 0.125." + + System.lineSeparator() + + "This is because the smallest weight increment in most gyms is 0.125kg." + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + public static final String MAX_STATIONS_ERROR = "Number of stations done cannot be more than 50!"; + + public static final String INVALID_WEIGHTS_ARRAY_FORMAT_ERROR = "Weights array format is incorrect!" + + System.lineSeparator() + + "Weights must be separated by commas (with no whitespaces) " + + "and be a positive decimal (up to 3 decimal places)" + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + public static final String INVALID_WEIGHTS_EMPTY_ERROR = "Weights array cannot be empty" + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + + + public static final String INVALID_WEIGHTS_NUMBER_ERROR = "Number of weight values must be the same as" + + " the number of sets!" + + System.lineSeparator() + + "Please check the number of sets (/s:[value]) and the number of weight values (/w:value1,value2,...)" + + System.lineSeparator() + + INVALID_GYM_STATION_FORMAT_ERROR; + public static final String INVALID_HEALTH_INPUT_ERROR = "Invalid input for health type! " + + "Please input either /h:bmi, /h:period, /h:prediction or /h:appointment"; + public static final String INSUFFICIENT_BMI_PARAMETERS_ERROR = "Insufficient parameters for bmi! " + + "Example input: /h:bmi /height:height /weight:weight /date:date" + + System.lineSeparator() + + "Only input what is required! Additional characters between flags will cause errors."; + public static final String NEGATIVE_BMI_ERROR = "Bmi must be a positive value"; + public static final String NULL_BMI_ERROR = "Bmi object cannot be null."; + public static final String EMPTY_BMI_LIST_ERROR = "BMI List is empty."; + public static final String BMI_LIST_UNCLEARED_ERROR = "Bmi list is not cleared."; + public static final String INVALID_HEIGHT_WEIGHT_INPUT_ERROR = + "Height and weight should be 2 decimal place positive numbers!"; + public static final String DATE_ALREADY_EXISTS_ERROR = "A Bmi input with the same date already exists."; + public static final String INSUFFICIENT_PERIOD_PARAMETERS_ERROR = "Insufficient parameters for period! " + + System.lineSeparator() + + "Example inputs: '/h:period /start:startDate' or '/h:period /start:startDate /end:endDate'" + + System.lineSeparator() + + "Only input what is required! Additional characters between flags will cause errors."; + public static final String END_DATE_NOT_FOUND_ERROR = "Start date already exists! " + + "Please add an end date to the latest period input in order to input a new period."; + public static final String NULL_PERIOD_ERROR = "Period object cannot be null."; + public static final String NULL_START_DATE_ERROR = "Start date of period cannot be empty."; + public static final String INVALID_START_DATE_ERROR = "Invalid start date!"; + public static final String INVALID_END_DATE_ERROR = "Invalid end date!"; + public static final String EMPTY_PERIOD_LIST_ERROR = "Period List is empty."; + public static final String PERIOD_LIST_UNCLEARED_ERROR = "Period list is not cleared."; + public static final String DATE_IN_FUTURE_ERROR = "Date specified cannot be later than today's date."; + public static final String PERIOD_END_BEFORE_START_ERROR = "Start date of period must be before end date."; + public static final String UNABLE_TO_MAKE_PREDICTIONS_ERROR = "Insufficient period cycles to make prediction." + + System.lineSeparator() + + "Enter at least four period inputs for prediction of the next period's start date."; + public static final String CURRENT_START_BEFORE_PREVIOUS_END = + "The start date of your current period input needs to be after the end date of your previous period input." + + System.lineSeparator() + + "You may enter 'history /item:period' to check your period history."; + public static final String INVALID_START_DATE_INPUT_ERROR = "Error is resulted by two possible reasons. " + +System.lineSeparator() + + "1. End date for previous period is still empty. " + + "Add an end date before starting a new period input!" + + System.lineSeparator() + + "2. If you're adding an end date to the latest period input, the start dates do not match! " + + "Enter 'history /item:period' to view existing period inputs."; + public static final String LENGTH_MUST_BE_POSITIVE_ERROR = "Length cannot be less than 1 day."; + public static final String INSUFFICIENT_APPOINTMENT_PARAMETERS_ERROR = "Insufficient parameters for appointment! " + + "Example input: /h:appointment /date:date /time:time /description:description /place:place" + + System.lineSeparator() + + "Only input what is required! Additional characters between flags will cause errors."; + public static final String NULL_APPOINTMENT_ERROR = "Appointment object cannot be null."; + public static final String EMPTY_APPOINTMENT_LIST_ERROR = "Appointment list is empty."; + public static final String APPOINTMENT_LIST_UNCLEARED_ERROR = "Appointment list is not cleared."; + public static final String START_INDEX_NEGATIVE_ERROR = "Start index for prediction must be positive"; + public static final String END_INDEX_SMALLER_THAN_START_ERROR = + "End index must be smaller than start index"; + public static final String NULL_DATE_ERROR = "Date of appointment cannot be empty."; + public static final String NULL_TIME_ERROR = "Time of appointment cannot be empty."; + public static final String DESCRIPTION_LENGTH_ERROR = "Description cannot be more than 100 characters"; + public static final String INVALID_DESCRIPTION_ERROR = "Appointment description can only " + + "contain alphanumeric characters, spaces, inverted commas and quotes!"; + + public static final String INSUFFICIENT_HISTORY_FILTER_ERROR = "Filter is missing!" + + System.lineSeparator() + + "Please use the 'latest' command followed by the '/item:' flag and one of the following options:" + + System.lineSeparator() + + "- run" + + System.lineSeparator() + + "- gym" + + System.lineSeparator() + + "- workouts" + + System.lineSeparator() + + "- period" + + System.lineSeparator() + + "- bmi" + + System.lineSeparator() + + "- appointment"; + + public static final String INSUFFICIENT_LATEST_FILTER_ERROR = "Filter is missing!" + + System.lineSeparator() + + "Please use the 'latest' command followed by the '/item:' flag and one of the following options:" + + System.lineSeparator() + + "- run" + + System.lineSeparator() + + "- gym" + + System.lineSeparator() + + "- period" + + System.lineSeparator() + + "- bmi" + + System.lineSeparator() + + "- appointment"; + + public static final String INVALID_HISTORY_FILTER_ERROR = "Filter is invalid!" + + System.lineSeparator() + + "Please only use the following flags" + + System.lineSeparator() + + "- run" + + System.lineSeparator() + + "- gym" + + System.lineSeparator() + + "- workouts" + + System.lineSeparator() + + "- period" + + System.lineSeparator() + + "- bmi" + + System.lineSeparator() + + "- appointment"; + + public static final String INVALID_LATEST_OR_DELETE_FILTER = "Filter is invalid!" + + System.lineSeparator() + + "Please only use the following flags" + + System.lineSeparator() + + "- run" + + System.lineSeparator() + + "- gym" + + System.lineSeparator() + + "- period" + + System.lineSeparator() + + "- bmi" + + System.lineSeparator() + + "- appointment"; + + public static final String TOO_MANY_SLASHES_ERROR = "Too many '/' characters specified within input. " + + "Parameters cannot contain any '/' characters!"; + + public static final String DISTANCE_TOO_LONG_ERROR = "The world's longest foot race is the Self-Transcendence " + + "3100 Mile Race." + + System.lineSeparator() + + "Please enter a more realistic distance less than 5000km!"; + + public static final String ZERO_HEIGHT_AND_WEIGHT_ERROR = "Height and weight must be more than 0."; + public static final String MAX_HEIGHT_ERROR = "The tallest man, Robert Wadlow was 2.72m." + + System.lineSeparator() + + "Please enter a more realistic height less than 2.75m!"; + public static final String MAX_WEIGHT_ERROR = "The heaviest human being, Jon Brower Minnnoch weighed in at 635kg." + + System.lineSeparator() + + "Please enter a more realistic weight less than 640kg!"; + + public static final String INVALID_WEIGHT_MAX_ERROR = "The heaviest object ever lifted by a human (Paul Anderson) " + + "was 2840kg." + + System.lineSeparator() + + "Please enter a more realistic gym weight less than 2850kg!"; + public static final String ZERO_DISTANCE_ERROR = "Distance run cannot be 0!"; + public static final String ZERO_TIME_ERROR = "Time cannot be set to 00:00!"; + public static final String MAX_PACE_ERROR = "The calculated pace is too slow!" + + System.lineSeparator() + + "Pace calculated cannot be slower than 30:00/km!"; + public static final String MIN_PACE_ERROR = "The calculated pace is too fast!" + + System.lineSeparator() + + "Pace calculated cannot be faster than 1:00/km!"; + public static final String INVALID_USERNAME_ERROR = "\u001b[31mUsername can only contain alphanumeric characters " + + "and spaces!\u001b[0m"; + public static final String NO_PERMISSIONS_ERROR = "Cannot read or write to current directory. Exiting."; + public static final String INVALID_WORKOUT_TYPE_ERROR = "Invalid workout type! Please input" + + " either /e:run or /e:gym!"; + public static final String FILE_READ_HEADER = "File Read Error: "; + public static final String FILE_WRITE_HEADER = "File Write Error: "; + public static final String FILE_CREATE_HEADER = "File Create Error: "; +} diff --git a/src/main/java/constants/HealthConstant.java b/src/main/java/constants/HealthConstant.java new file mode 100644 index 0000000000..9830ddfc01 --- /dev/null +++ b/src/main/java/constants/HealthConstant.java @@ -0,0 +1,86 @@ +package constants; + +/** + * HealthConstant class contains constants related to health-related functionalities in the application. + * It includes headers, flags, parameters, thresholds, formatted strings/messages, and split indices. + */ +public class HealthConstant { + public static final String BMI = "bmi"; + public static final String PERIOD = "period"; + public static final String APPOINTMENT = "appointment"; + public static final String HEALTH_FLAG = "/h:"; + public static final String HEIGHT_FLAG = "/height:"; + public static final String WEIGHT_FLAG = "/weight:"; + public static final String DATE_FLAG = "/date:"; + public static final String START_FLAG = "/start:"; + public static final String END_FLAG = "/end:"; + public static final String TIME_FLAG = "/time:"; + public static final String DESCRIPTION_FLAG = "/description:"; + public static final Integer NUM_BMI_PARAMETERS = 3; + public static final Integer NUM_PERIOD_PARAMETERS = 2; + public static final Integer NUM_APPOINTMENT_PARAMETERS = 3; + public static final double UNDERWEIGHT_BMI_THRESHOLD = 18.5; + public static final double NORMAL_BMI_THRESHOLD = 25.0; + public static final double OVERWEIGHT_BMI_THRESHOLD = 30.0; + public static final double OBESE_BMI_THRESHOLD = 40.0; + public static final double MIN_WEIGHT = 0; + public static final double MIN_HEIGHT = 0; + public static final double MAX_HEIGHT = 2.75; + public static final double MAX_WEIGHT = 640; + public static final double MIN_BMI = 0; + public static final String LOG_DELETE_BMI_FORMAT = "Removed BMI entry of %.2f from %s"; + public static final String TWO_DECIMAL_PLACE_FORMAT = "%.2f"; + public static final String BMI_ADDED_MESSAGE_PREFIX = "Added: bmi | "; + public static final String BMI_REMOVED_MESSAGE_PREFIX = "Removed BMI with index: "; + public static final String UNDERWEIGHT_MESSAGE = "You're underweight."; + public static final String NORMAL_WEIGHT_MESSAGE = "Great! You're within normal range."; + public static final String OVERWEIGHT_MESSAGE = "You're overweight."; + public static final String OBESE_MESSAGE = "You're obese."; + public static final String SEVERELY_OBESE_MESSAGE = "You're severely obese."; + public static final String BMI_HISTORY_HEADER = "Your BMI history:"; + public static final int MIN_SIZE_FOR_COMPARISON = 1; + public static final String PRINT_PERIOD_FORMAT = "Period Start: %s Period End: %s" + + System.lineSeparator() + + "Period Length: %d %s"; + public static final String PRINT_BMI_FORMAT = "%s" + + System.lineSeparator() + + "Your BMI is %.2f" + + System.lineSeparator() + + "%s"; + public static final String LOG_DELETE_PERIOD_FORMAT = "Removed period entry with start date: %s and end date: %s"; + public static final String PERIOD_ADDED_MESSAGE_PREFIX = "Added: period | "; + public static final String PERIOD_REMOVED_MESSAGE_PREFIX = "Removed period with index: "; + public static final String PERIOD_HISTORY_HEADER = "Your Period history:"; + public static final String PERIOD_TOO_LONG_MESSAGE = "Your period length is out of the healthy range. " + + "Please consult a gynaecologist if this persists."; + public static final String PRINT_CYCLE_FORMAT = "Cycle Length: %d day(s)"; + public static final Integer LATEST_THREE_CYCLE_LENGTHS = 3; + public static final Integer FIRST_CYCLE_INDEX = 3; + public static final Integer LAST_CYCLE_INDEX = 1; + public static final Integer MIN_SIZE_FOR_PREDICTION = 4; + public static final Integer MIN_LENGTH = 0; + public static final String PREDICTED_START_DATE_MESSAGE = "Your next cycle's predicted start date is "; + public static final String COUNT_DAYS_MESSAGE = ", in "; + public static final String PERIOD_IS_LATE = ". Your period is late by "; + public static final String PREDICTED_DATE_IS_TODAY_MESSAGE = ", which is today! "; + public static final String DAYS_MESSAGE = "day(s)"; + public static final String PRINT_APPOINTMENT_FORMAT = "On %s at %s: %s"; + public static final String LOG_DELETE_APPOINTMENT_FORMAT = "Removed appointment on %s at %s: %s"; + public static final String APPOINTMENT_ADDED_MESSAGE_PREFIX = "Added: appointment | "; + public static final String APPOINTMENT_REMOVED_MESSAGE_PREFIX = "Removed appointment with index: "; + public static final Integer MAX_DESCRIPTION_LENGTH = 100; + public static final String APPOINTMENT_HISTORY_HEADER = "Your Appointment history:"; + public static final int BMI_HEIGHT_INDEX = 0; + public static final int BMI_WEIGHT_INDEX = 1; + public static final int BMI_DATE_INDEX = 2; + public static final int PERIOD_START_DATE_INDEX = 0; + public static final int PERIOD_END_DATE_INDEX = 1; + public static final int APPOINTMENT_DATE_INDEX = 0; + public static final int APPOINTMENT_TIME_INDEX = 1; + public static final int APPOINTMENT_DESCRIPTION_INDEX = 2; + + public static final int NUM_OF_SLASHES_FOR_PERIOD = 3; + public static final int NUM_OF_SLASHES_FOR_BMI = 4; + public static final int NUM_OF_SLASHES_FOR_APPOINTMENT = 4; + public static final int FIRST_ITEM = 0; +} diff --git a/src/main/java/constants/UiConstant.java b/src/main/java/constants/UiConstant.java new file mode 100644 index 0000000000..5690e892c5 --- /dev/null +++ b/src/main/java/constants/UiConstant.java @@ -0,0 +1,63 @@ +package constants; + +import java.io.File; + +/** + * UiConstants class contains constants related to user-interaction-related functionalities in the application. + * It includes constants for special characters, regular expressions, UI replies, storage paths, + * numerical values, history management, delete operations, and split indices. + */ +public class UiConstant { + public static final String SPLIT_BY_SLASH = "/"; + public static final String SPLIT_BY_COLON = ":"; + public static final String SPLIT_BY_WHITESPACE = " "; + public static final String SPLIT_BY_COMMAS = ","; + public static final String DASH = "-"; + public static final String LINE = " | "; + public static final String PARTITION_LINE = "____________________________________________________________"; + public static final String EMPTY_STRING = ""; + public static final String FULL_STOP = "."; + public static final String VALID_DATE_REGEX = "^\\d{2}-\\d{2}-\\d{4}$"; + public static final String VALID_TWO_DP_NUMBER_REGEX = "^\\d+\\.\\d{2}$"; + public static final String VALID_TIME_REGEX = "^\\d{2}:\\d{2}$"; + public static final String VALID_TIME_WITH_HOURS_REGEX = "^\\d{2}:\\d{2}:\\d{2}$"; + public static final String VALID_POSITIVE_INTEGER_REGEX = "^[1-9]\\d*$"; + public static final String VALID_APPOINTMENT_DESCRIPTION_REGEX = "^[0-9a-zA-Z\\s'\"]+$"; + public static final String VALID_GYM_STATION_NAME_REGEX = "^[A-Za-z\\s]+$"; + public static final String VALID_USERNAME_REGEX = "^[0-9A-Za-z\\s]+$"; + public static final String VALID_WEIGHTS_ARRAY_REGEX = "^\\d+(\\.\\d{1,3})?(,\\d+(\\.\\d{1,3})?)*$"; + public static final String EXIT_MESSAGE = "Initiating PulsePilot landing sequence..."; + public static final int DATA_TYPE_INDEX = 0; + public static final int NAME_INDEX = 1; + public static final String NAME_LABEL = "NAME"; + public static final String LOG_FILE_PATH = "./pulsepilot_log.txt"; + public static String dataFilePath = "./pulsepilot_data.txt"; + public static String hashFilePath = "./pulsepilot_hash.txt"; + public static File saveFile = new File(UiConstant.dataFilePath); + public static final int FILE_FOUND = 0; + public static final int FILE_NOT_FOUND = 1; + public static final String FILE_FOUND_MESSAGE = "Welcome back, Captain "; + public static final String FILE_MISSING_MESSAGE = "What is your name, voyager?"; + public static final String SUCCESSFUL_LOAD = "Prior data found. Orbit has been synchronised."; + public static final String ITEM_FLAG = "/item:"; + public static final String INDEX_FLAG = "/index:"; + public static final int NUM_SECONDS_IN_MINUTE = 60; + public static final int NUM_SECONDS_IN_HOUR = 3600; + public static final int MIN_MINUTES = 0; + public static final int MAX_MINUTES = 59; + public static final int MAX_SECONDS = 59; + public static final int MIN_HOURS = 0; + public static final int MAX_HOURS = 23; + public static final double POWER_OF_TWO = 2.0; + public static final double ROUNDING_FACTOR = 100.0; + public static final int NUM_DELETE_PARAMETERS = 2; + public static final int MINIMUM_PERIOD_COUNT = 1; + public static final int MIN_SECONDS = 0; + public static final int NUM_OF_SLASHES_FOR_DELETE = 2; + public static final int NUM_OF_SLASHES_FOR_LATEST_AND_HISTORY = 1; + public static final int DELETE_ITEM_STRING_INDEX = 0; + public static final int DELETE_ITEM_NUMBER_INDEX = 1; + public static final int SPLIT_TIME_HOUR_INDEX = 0; + public static final int SPLIT_TIME_MINUTES_INDEX = 1; + +} diff --git a/src/main/java/constants/WorkoutConstant.java b/src/main/java/constants/WorkoutConstant.java new file mode 100644 index 0000000000..83bffd9005 --- /dev/null +++ b/src/main/java/constants/WorkoutConstant.java @@ -0,0 +1,102 @@ +package constants; + +/** + * WorkoutConstants class contains constants related to workout-related functionalities in the application. + * It includes constants for workout types, flags, parameters, indices, file loading, history display, + * and formatted strings. + */ +public class WorkoutConstant { + public static final String NUMBER_OF_STATIONS_FLAG = "/n:"; + public static final String EXERCISE_FLAG = "/e:"; + public static final String DISTANCE_FLAG = "/d:"; + public static final String RUN_TIME_FLAG = "/t:"; + public static final String DATE_FLAG = "/date:"; + public static final String SETS_FLAG = "/s:"; + public static final String REPS_FLAG = "/r:"; + public static final String WEIGHTS_FLAG = "/w:"; + public static final String COLON = ":"; + public static final int NUMBER_OF_RUN_PARAMETERS = 3; + public static final int NUMBER_OF_GYM_PARAMETERS = 2; + public static final int NUMBER_OF_GYM_STATION_PARAMETERS = 4; + public static final int NUMBER_OF_PARTS_FOR_RUN_TIME = 2; + public static final int NUMBER_OF_PARTS_FOR_RUN_TIME_WITH_HOURS = 3; + public static final int MAX_GYM_STATION_NAME_LENGTH = 25; + public static final double MAX_RUN_DISTANCE = 5000.00; + public static final double MIN_RUN_DISTANCE = 0; + public static final double MAX_PACE = 30; + public static final double MIN_PACE = 1; + public static final double MAX_GYM_WEIGHT = 2850.000; + public static final double WEIGHT_MULTIPLE = 0.125; + public static final int MAX_GYM_STATION_NUMBER = 50; + public static final int RUN_TIME_HOUR_INDEX = 0; + public static final int RUN_TIME_MINUTE_INDEX = 1; + public static final int RUN_TIME_SECOND_INDEX = 2; + public static final int RUN_TIME_NO_HOURS_MINUTE_INDEX = 0; + public static final int RUN_TIME_NO_HOURS_SECOND_INDEX = 1; + public static final Integer STATION_NAME_INDEX = 0; + public static final int NO_HOURS_PRESENT = -1; + + public static final int GYM_NUMBER_OF_STATIONS_INDEX = 0; + public static final int GYM_DATE_INDEX = 1; + public static final int GYM_STATION_NAME_INDEX = 0; + public static final int GYM_STATION_SET_INDEX = 1; + public static final int GYM_STATION_REPS_INDEX = 2; + public static final int GYM_STATION_WEIGHTS_INDEX = 3; + + public static final int RUN_TIME_INDEX = 0; + public static final int RUN_DISTANCE_INDEX = 1; + public static final int RUN_DATE_INDEX = 2; + public static final String BACK = "back"; + public static final String RUN = "run"; + public static final String GYM = "gym"; + public static final String ALL = "workouts"; + public static final String TWO_DECIMAL_PLACE_FORMAT = "%.2f"; + public static final String TWO_DIGIT_PLACE_FORMAT = "%02d"; + public static final int GYM_FILE_INDEX = 0; + public static final int NUM_OF_STATIONS_FILE_INDEX = 1; + public static final int DATE_FILE_INDEX = 2; + + public static final int GYM_FILE_BASE_COUNTER = 3; + public static final int SETS_OFFSET = 1; + public static final int REPS_OFFSET = 2; + public static final int WEIGHTS_OFFSET = 3; + public static final int INCREMENT_OFFSET = 4; + public static final String HISTORY_WORKOUTS_HEADER = "Showing all workouts (runs and gyms):"; + public static final String HISTORY_WORKOUTS_DATA_FORMAT = "%-5s\t%-12s\t%-25s\t%-20s\t%-8s"; + public static final String HISTORY_WORKOUTS_HEADER_FORMAT = String.format( + "%-6s\t%-5s\t%-12s\t%-25s\t%-20s\t%-8s", "Index", + "Type", "Date", "[Distance (km) / Station]", "[Duration / Sets]", "Pace (min/km)"); + public static final String HISTORY_WORKOUTS_DATA_HEADER_FORMAT = "%-6s\t%s"; + public static final String RUN_DATA_FORMAT = "%-6s\t%-10s\t%-10s\t%-10s\t%-12s"; + public static final String RUN_DATA_INDEX_FORMAT = "%-6d\t%-6s"; + public static final String RUN_HEADER_INDEX_FORMAT = String.format("%-6s\t%-6s\t%-10s\t%-10s\t%-10s\t%-12s", + "Index", "Type", "Time", "Distance", "Pace", "Date"); + public static final String RUN_DELETE_MESSAGE_FORMAT = "Removed Run entry with %s km at %s."; + public static final String GYM_STATION_FORMAT = "%s: "; + public static final String GYM_SET_FORMAT = "%d reps at %.3f KG"; + public static final String GYM_SET_INDEX_FORMAT = "\t- Set %d. %s"; + public static final String GYM_DELETE_MESSAGE_FORMAT = "Removed Gym entry with %d station(s)."; + + public static final String INDIVIDUAL_GYM_STATION_FORMAT = "%d sets"; + public static final String RUN_HEADER = "Type\tTime\t\tDistance\tPace\t\tDate"; + public static final String ADD_RUN = "Successfully added a new run session"; + public static final String ADD_GYM = "Successfully added a new gym session"; + public static final String STATION_GYM_FORMAT = "e.g. Bench Press /s:2 /r:4 " + + "/w:10,20"; + + public static final String TIME_WITH_HOURS_FORMAT = TWO_DIGIT_PLACE_FORMAT + + COLON + TWO_DIGIT_PLACE_FORMAT + + COLON + TWO_DIGIT_PLACE_FORMAT; + public static final String TIME_WITHOUT_HOURS_FORMAT = TWO_DIGIT_PLACE_FORMAT + + COLON + TWO_DIGIT_PLACE_FORMAT; + + public static final String RUN_PACE_FORMAT = "%d:%02d/km"; + + public static final int NUM_OF_SLASHES_FOR_GYM_WITH_DATE = 3; + public static final int NUM_OF_SLASHES_FOR_GYM_WITHOUT_DATE = 2; + public static final int NUM_OF_SLASHES_FOR_GYM_STATION = 3; + + public static final int NUM_OF_SLASHES_FOR_RUN_WITH_DATE = 4; + public static final int NUM_OF_SLASHES_FOR_RUN_WITHOUT_DATE = 3; + +} diff --git a/src/main/java/health/Appointment.java b/src/main/java/health/Appointment.java new file mode 100644 index 0000000000..030d5300b3 --- /dev/null +++ b/src/main/java/health/Appointment.java @@ -0,0 +1,77 @@ +package health; + +import constants.ErrorConstant; +import constants.HealthConstant; +import utility.Parser; + +import java.time.LocalDate; +import java.time.LocalTime; + +/** + * The Appointment class inherits from the Health class. + * It contains information about the date, time, and description of the appointment. + */ +public class Appointment extends Health { + //@@author syj_02 + private LocalDate date; + private LocalTime time; + private String description; + private final Parser parser = new Parser(); + private final HealthList healthList = new HealthList(); + + /** + * Constructor for Appointment object. + * + * @param stringDate A string representing the date of the appointment. + * @param stringTime A string representing the time of the appointment. + * @param description A string describing the appointment. + */ + public Appointment(String stringDate, String stringTime, String description) { + this.date = parser.parseDate(stringDate); + this.time = parser.parseTime(stringTime); + this.description = description; + healthList.addAppointment(this); + } + + /** + * Retrieves the date of the appointment of LocalDate type. + * + * @return The date of appointment. + */ + public LocalDate getDate() { + assert date != null : ErrorConstant.NULL_DATE_ERROR; + return this.date; + } + + /** + * Retrieves the time of the appointment of LocalTime type. + * + * @return The time of appointment. + */ + public LocalTime getTime() { + assert time != null : ErrorConstant.NULL_TIME_ERROR; + return this.time; + } + + /** + * Retrieves the description of the appointment of String type. + * + * @return The description of appointment. + */ + public String getDescription() { + return this.description; + } + + /** + * Returns the string representation of an Appointment object. + * + * @return A formatted string representing an Appointment object. + */ + @Override + public String toString() { + return String.format(HealthConstant.PRINT_APPOINTMENT_FORMAT, + getDate(), + getTime(), + getDescription()); + } +} diff --git a/src/main/java/health/Bmi.java b/src/main/java/health/Bmi.java new file mode 100644 index 0000000000..210a74bc4e --- /dev/null +++ b/src/main/java/health/Bmi.java @@ -0,0 +1,139 @@ +package health; + +import utility.Parser; +import constants.ErrorConstant; +import constants.UiConstant; +import constants.HealthConstant; + +import java.time.LocalDate; + +/** + * The Bmi class inherits from the Health class. + * It contains information about the height, weight, and bmi value of the user, and provides functionalities + * to calculate and categories the BMI values. + */ +public class Bmi extends Health { + //@@author j013n3 + private double bmiValue; + + private LocalDate date; + + private HealthList healthList = new HealthList(); + private double height; + private double weight; + private Parser parser = new Parser(); + + /** + * Constructor for Bmi object. + * + * @param height A string representing the user's height. + * @param weight A string representing the user's weight. + * @param date A string representing the date of the record. + * @throws AssertionError If height or weight values are not positive. + */ + public Bmi(String height, String weight, String date) { + this.height = Double.parseDouble(height); + this.weight = Double.parseDouble(weight); + + assert this.height > HealthConstant.MIN_HEIGHT && this.weight > HealthConstant.MIN_WEIGHT + : ErrorConstant.NEGATIVE_VALUE_ERROR; + + this.date = parser.parseDate(date); + + this.bmiValue = calculateBmiValue(); + healthList.addBmi(this); + } + + /** + * Retrieves height recorded in Bmi object of String type. + * + * @return The height recorded in the Bmi object. + */ + public String getHeight() { + return String.format(HealthConstant.TWO_DECIMAL_PLACE_FORMAT, height); + } + + /** + * Retrieves weight recorded in Bmi object of String type. + * + * @return The weight recorded in the Bmi object. + */ + public String getWeight() { + return String.format(HealthConstant.TWO_DECIMAL_PLACE_FORMAT, weight); + } + + /** + * Retrieves BMI value recorded in Bmi object of String type. + * + * @return The BMI value recorded in the Bmi object. + */ + public String getBmiValueString() { + return String.format(HealthConstant.TWO_DECIMAL_PLACE_FORMAT, bmiValue); + } + + /** + * Retrieves BMI value recorded in Bmi object of Double type. + * + * @return The BMI value recorded in the Bmi object. + */ + public Double getBmiValueDouble() { + return this.bmiValue; + } + + /** + * Prints the BMI category based on the calculated bmiValue. + * + * @param bmiValue The Bmi value to categorize. + * @return A string presenting the Bmi category. + * @throws AssertionError If calculated value is not positive. + */ + public static String getBmiCategory(double bmiValue) { + assert bmiValue > HealthConstant.MIN_BMI: ErrorConstant.NEGATIVE_BMI_ERROR; + if (bmiValue < HealthConstant.UNDERWEIGHT_BMI_THRESHOLD) { + return HealthConstant.UNDERWEIGHT_MESSAGE; + } else if (bmiValue < HealthConstant.NORMAL_BMI_THRESHOLD) { + return HealthConstant.NORMAL_WEIGHT_MESSAGE; + } else if (bmiValue < HealthConstant.OVERWEIGHT_BMI_THRESHOLD) { + return HealthConstant.OVERWEIGHT_MESSAGE; + } else if (bmiValue < HealthConstant.OBESE_BMI_THRESHOLD) { + return HealthConstant.OBESE_MESSAGE; + } else { + return HealthConstant.SEVERELY_OBESE_MESSAGE; + } + } + + /** + * Retrieves date recorded in Bmi object of LocalDate type. + * + * @return The date recorded in the Bmi object. + */ + public LocalDate getDate() { + return date; + } + + /** + * Returns the string presentation of a Bmi object. + * + * @return A formatted string representing a Bmi object. + */ + @Override + public String toString() { + return String.format(HealthConstant.PRINT_BMI_FORMAT, + this.getDate(), + this.calculateBmiValue(), + getBmiCategory(bmiValue)); + } + + /** + * Calculates bmiValue based on the height and weight recorded. + * + * @return The calculated Bmi value. + * @throws AssertionError If calculated value is not positive. + */ + private double calculateBmiValue() { + double bmi = Math.round((weight / (Math.pow(height, UiConstant.POWER_OF_TWO))) * UiConstant.ROUNDING_FACTOR) + / UiConstant.ROUNDING_FACTOR; + assert bmi > HealthConstant.MIN_BMI: ErrorConstant.NEGATIVE_BMI_ERROR; + return bmi; + } +} diff --git a/src/main/java/health/Health.java b/src/main/java/health/Health.java new file mode 100644 index 0000000000..bd49a4afff --- /dev/null +++ b/src/main/java/health/Health.java @@ -0,0 +1,33 @@ +package health; + +import java.time.LocalDate; + +/** + * The Health class represents a Health object to track user's health information. + */ +public class Health { + //@@author j013n3 + private LocalDate date = null; + + public Health() { + } + + /** + * Retrieves the date of Health object of LocalDate type. + * + * @return The date of the Health object. + */ + public LocalDate getDate() { + return date; + } + + /** + * Returns a string containing the date of the Health object. + * + * @return A formatted string representing a Health object. + */ + @Override + public String toString(){ + return getDate().toString(); + } +} diff --git a/src/main/java/health/HealthList.java b/src/main/java/health/HealthList.java new file mode 100644 index 0000000000..b0d544bd87 --- /dev/null +++ b/src/main/java/health/HealthList.java @@ -0,0 +1,381 @@ +package health; + +import constants.UiConstant; +import storage.LogFile; +import utility.CustomExceptions; +import constants.ErrorConstant; +import constants.HealthConstant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import ui.Output; + +/** + * The HealthList class inherits from ArrayList. + * It contains the individual lists of Bmi, Appointment, and Period objects. + * Methods to get, add and print Health objects are listed here. + */ +public class HealthList extends ArrayList { + //@@author j013n3 + static LogFile logFile = LogFile.getInstance(); + private static final ArrayList BMIS = new ArrayList<>(); + private static final ArrayList PERIODS = new ArrayList<>(); + + private static final ArrayList APPOINTMENTS = new ArrayList<>(); + + public HealthList() { + + } + + /** + * Retrieves all Bmi objects within BMIS. + * + * @return The BMIS array list. + */ + public static ArrayList getBmis() { + return BMIS; + } + + /** + * Retrieves all Period objects within PERIODS. + * + * @return The PERIODS array list. + */ + public static ArrayList getPeriods() { + return PERIODS; + } + + /** + * Retrieves all Appointment objects within APPOINTMENTS. + * + * @return The APPOINTMENTS array list. + */ + public static ArrayList getAppointments() { + return APPOINTMENTS; + } + + /** + * Retrieves the Period object at a specified index. + * + * @param index The index of the Period object. + * @return The Period object at the specified index, or null if the index is out of bounds. + */ + public static Period getPeriod(int index) { + if (index < HealthConstant.FIRST_ITEM || index >= PERIODS.size()) { + return null; + } + return PERIODS.get(index); + } + + /** + * Retrieves the number of Period objects recorded. + * + * @return The number of Period objects recorded. + */ + public static int getPeriodSize() { + return PERIODS.size(); + } + + //@@author L5-Z + /** + * Retrieves size of BMIS list. + * + * @return Size of BMIS list. + */ + public static int getBmisSize() { + return BMIS.size(); + } + + /** + * Retrieves size of PERIODS list. + * + * @return Size of PERIODS list. + */ + public static int getPeriodsSize() { + return PERIODS.size(); + } + + //@@author L5-Z + /** + * Deletes Bmi object based on a specified index and prints delete message if successful. + * + * @param index Index of the Bmi object to be deleted. + * @throws CustomExceptions.OutOfBounds If the index of the Bmi object given does not exist. + */ + public static void deleteBmi(int index) throws CustomExceptions.OutOfBounds { + if (index < HealthConstant.FIRST_ITEM) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.BMI_EMPTY_ERROR); + } else if (index >= BMIS.size()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.INVALID_INDEX_DELETE_ERROR); + } + assert !BMIS.isEmpty() : ErrorConstant.EMPTY_BMI_LIST_ERROR; + Bmi deletedBmi = BMIS.get(index); + Output.printLine(); + System.out.printf((HealthConstant.LOG_DELETE_BMI_FORMAT) + System.lineSeparator(), + deletedBmi.getBmiValueDouble(), + deletedBmi.getDate()); + Output.printLine(); + BMIS.remove(index); + LogFile.writeLog(HealthConstant.BMI_REMOVED_MESSAGE_PREFIX + index, false); + } + + /** + * Deletes Period object based on a specified index and prints delete message if successful. + * + * @param index Index of the Period object to be deleted. + * @throws CustomExceptions.OutOfBounds If the index of the Period object given does not exist. + */ + public static void deletePeriod(int index) throws CustomExceptions.OutOfBounds { + if (index < HealthConstant.FIRST_ITEM) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.PERIOD_EMPTY_ERROR); + } else if(index >= PERIODS.size()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.INVALID_INDEX_DELETE_ERROR); + } + assert !PERIODS.isEmpty() : ErrorConstant.EMPTY_PERIOD_LIST_ERROR; + Period deletedPeriod = PERIODS.get(index); + String endDateUnit = (deletedPeriod.getEndDate() == null) ? + ErrorConstant.NO_DATE_SPECIFIED_ERROR : deletedPeriod.getEndDate().toString(); + Output.printLine(); + System.out.printf((HealthConstant.LOG_DELETE_PERIOD_FORMAT) + System.lineSeparator(), + deletedPeriod.getStartDate(), + endDateUnit); + PERIODS.remove(index); + Output.printLine(); + LogFile.writeLog(HealthConstant.PERIOD_REMOVED_MESSAGE_PREFIX + index, false); + } + + //@@author syj02 + + /** + * Deletes Appointment object based on a specified index and prints delete message if successful. + * + * @param index Index of the Appointment object to be deleted. + * @throws CustomExceptions.OutOfBounds If the index of the Appointment object given does not exist. + */ + public static void deleteAppointment(int index) throws CustomExceptions.OutOfBounds { + if (index < HealthConstant.FIRST_ITEM) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.APPOINTMENT_EMPTY_ERROR); + } else if (index >= APPOINTMENTS.size()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.INVALID_INDEX_DELETE_ERROR); + } + assert !APPOINTMENTS.isEmpty() : ErrorConstant.EMPTY_APPOINTMENT_LIST_ERROR; + Appointment deletedAppointment = APPOINTMENTS.get(index); + Output.printLine(); + System.out.printf((HealthConstant.LOG_DELETE_APPOINTMENT_FORMAT) + System.lineSeparator(), + deletedAppointment.getDate(), + deletedAppointment.getTime(), + deletedAppointment.getDescription()); + Output.printLine(); + APPOINTMENTS.remove(index); + LogFile.writeLog(HealthConstant.APPOINTMENT_REMOVED_MESSAGE_PREFIX + index, false); + if (!APPOINTMENTS.isEmpty()) { + printAppointmentHistory(); + } + } + + /** + * Prints the latest Bmi object added. + * + * @throws CustomExceptions.OutOfBounds if BMIS is empty. + * @throws AssertionError If BMIS is empty. + */ + public static void printLatestBmi() throws CustomExceptions.OutOfBounds { + if (BMIS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.BMI_EMPTY_ERROR); + } + assert !BMIS.isEmpty() : ErrorConstant.EMPTY_BMI_LIST_ERROR; + System.out.println(BMIS.get(HealthConstant.FIRST_ITEM)); + } + + //@@author j013n3 + /** + * Prints the latest Period object added. + * + * @throws CustomExceptions.OutOfBounds If PERIODS is empty. + * @throws AssertionError If PERIODS is empty. + */ + public static void printLatestPeriod() throws CustomExceptions.OutOfBounds { + if (PERIODS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.PERIOD_EMPTY_ERROR); + } + assert !PERIODS.isEmpty() : ErrorConstant.EMPTY_PERIOD_LIST_ERROR; + System.out.println(PERIODS.get(HealthConstant.FIRST_ITEM)); + } + + //@@author syj_02 + /** + * Prints the latest Appointment object added. + * + * @throws CustomExceptions.OutOfBounds If Appointment list is empty. + * @throws AssertionError If APPOINTMENTS is empty. + */ + public static void printLatestAppointment() throws CustomExceptions.OutOfBounds { + if (APPOINTMENTS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.APPOINTMENT_EMPTY_ERROR); + } + assert !APPOINTMENTS.isEmpty() : ErrorConstant.EMPTY_APPOINTMENT_LIST_ERROR; + int index = APPOINTMENTS.size() - 1; + System.out.println(APPOINTMENTS.get(index)); + } + + /** + * Prints all the Bmi objects recorded. + * + * @throws CustomExceptions.OutOfBounds if BMI list is empty. + * @throws AssertionError If BMIS list is empty. + */ + public static void printBmiHistory() throws CustomExceptions.OutOfBounds { + if (BMIS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.BMI_EMPTY_ERROR); + } + assert !BMIS.isEmpty() : ErrorConstant.EMPTY_BMI_LIST_ERROR; + int index = 1; + System.out.println(HealthConstant.BMI_HISTORY_HEADER); + for (Bmi bmi : BMIS) { + System.out.print(index + UiConstant.FULL_STOP + UiConstant.SPLIT_BY_WHITESPACE); + System.out.println(bmi); + index += 1; + } + } + + //@@author j013n3 + /** + * Prints all the Period objects recorded. + * + * @throws CustomExceptions.OutOfBounds If PERIODS list is empty. + * @throws AssertionError If PERIODS list is empty. + */ + public static void printPeriodHistory() throws CustomExceptions.OutOfBounds { + if (PERIODS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.PERIOD_EMPTY_ERROR); + } + assert !PERIODS.isEmpty() : ErrorConstant.EMPTY_PERIOD_LIST_ERROR; + int index = 1; + System.out.println(HealthConstant.PERIOD_HISTORY_HEADER); + for (Period period : PERIODS) { + System.out.print(index + UiConstant.FULL_STOP + UiConstant.SPLIT_BY_WHITESPACE); + System.out.println(period); + index += 1; + } + } + + //@@author syj02 + /** + * Prints all the Appointment objects recorded. + * + * @throws utility.CustomExceptions.OutOfBounds If APPOINTMENTS list is empty. + * @throws AssertionError If APPOINTMENTS list is empty. + */ + public static void printAppointmentHistory() throws CustomExceptions.OutOfBounds { + if (APPOINTMENTS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.APPOINTMENT_EMPTY_ERROR); + } + assert !APPOINTMENTS.isEmpty() : ErrorConstant.EMPTY_APPOINTMENT_LIST_ERROR; + int index = 1; + System.out.println(HealthConstant.APPOINTMENT_HISTORY_HEADER); + for (Appointment appointment : APPOINTMENTS) { + System.out.print(index + UiConstant.FULL_STOP + UiConstant.SPLIT_BY_WHITESPACE); + System.out.println(appointment); + index += 1; + } + } + + //@@author L5-Z + /** + * Clears PERIODS, BMIS and APPOINTMENTS lists. + * + * @throws AssertionError If PERIODS, BMIS and APPOINTMENTS lists are not empty. + */ + public static void clearHealthLists() { + PERIODS.clear(); + BMIS.clear(); + APPOINTMENTS.clear(); + assert BMIS.isEmpty() : ErrorConstant.BMI_LIST_UNCLEARED_ERROR; + assert PERIODS.isEmpty() : ErrorConstant.PERIOD_LIST_UNCLEARED_ERROR; + assert APPOINTMENTS.isEmpty() : ErrorConstant.APPOINTMENT_LIST_UNCLEARED_ERROR; + } + + //@@author j013n3 + /** + * Prints the last three Period objects added to PERIODS. + */ + public static void printLatestThreeCycles() { + Output.printLine(); + int startIndex = HealthConstant.FIRST_ITEM; + int endIndex = HealthConstant.LATEST_THREE_CYCLE_LENGTHS; + assert startIndex >= HealthConstant.FIRST_ITEM : ErrorConstant.START_INDEX_NEGATIVE_ERROR; + + for (int i = startIndex; i < endIndex; i++) { + System.out.println(PERIODS.get(i)); + } + + } + + /** + * Predicts the start date of the next period based on the average cycle length of the last three cycles. + * + * @return The predicted start date of the next period. + * @throws AssertionError If PERIODS is empty. + * @throws CustomExceptions.OutOfBounds If PERIODS is empty. + */ + public static LocalDate predictNextPeriodStartDate() throws CustomExceptions.OutOfBounds { + if (PERIODS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.PERIOD_EMPTY_ERROR); + } + assert !PERIODS.isEmpty() : ErrorConstant.EMPTY_PERIOD_LIST_ERROR; + Period latestPeriod = PERIODS.get(HealthConstant.FIRST_ITEM); + return latestPeriod.nextCyclePrediction(); + } + + //@@author j013n3 + /** + * Adds a Bmi object to BMIS. + * + * @param bmi Bmi object. + * @throws AssertionError If Bmi object is null. + */ + protected void addBmi(Bmi bmi) { + assert bmi != null : ErrorConstant.NULL_BMI_ERROR; + BMIS.add(bmi); + // bmi sorted from latest to earliest date + BMIS.sort(Comparator.comparing(Bmi::getDate).reversed()); + } + + //@@author syj02 + /** + * Adds a Period object to PERIODS. + * + * @param period Period object to be added. + * @throws AssertionError If Period object is null. + */ + protected void addPeriod(Period period) { + assert period != null : ErrorConstant.NULL_PERIOD_ERROR; + + PERIODS.add(period); + + PERIODS.sort(Comparator.comparing(Period::getStartDate).reversed()); + + int size = PERIODS.size(); + if (size > HealthConstant.MIN_SIZE_FOR_COMPARISON) { + for (int i = size - 1; i > HealthConstant.FIRST_ITEM; i--) { + Period newerPeriod = PERIODS.get(i - 1); + Period olderPeriod = PERIODS.get(i); + olderPeriod.setCycleLength(newerPeriod.getStartDate()); + } + } + } + + /** + * Adds an Appointment to APPOINTMENTS. + * Sorts all Appointment objects in APPOINTMENTS by date and time of the appointments with + * the earliest appointment at the top. + * + * @param appointment Appointment object. + * @throws AssertionError If Appointment object is null. + */ + protected void addAppointment(Appointment appointment) { + assert appointment != null : ErrorConstant.NULL_APPOINTMENT_ERROR; + APPOINTMENTS.add(appointment); + APPOINTMENTS.sort(Comparator.comparing(Appointment::getDate).thenComparing(Appointment::getTime)); + } +} diff --git a/src/main/java/health/Period.java b/src/main/java/health/Period.java new file mode 100644 index 0000000000..991237ad54 --- /dev/null +++ b/src/main/java/health/Period.java @@ -0,0 +1,210 @@ +package health; + +import constants.ErrorConstant; +import constants.HealthConstant; +import ui.Output; +import utility.Parser; +import constants.UiConstant; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +/** + * The Period class inherits from Health class. + * It contains information about the start date, end date, time, period length, and cycle length of the period, + * and the methods to calculate period and cycle length and predict the next period. + */ +public class Period extends Health { + //@@author syj02 + private LocalDate startDate; + private LocalDate endDate; + private long periodLength; + private long cycleLength; + + private final Parser parser = new Parser(); + private final HealthList healthList = new HealthList(); + + /** + * Constructs a new Period object with only the start date provided. + * + * @param stringStartDate A string representing the start date of the period. + */ + public Period(String stringStartDate) { + this.startDate = parser.parseDate(stringStartDate); + this.endDate = null; + this.periodLength = 1; + this.cycleLength = 0; + healthList.addPeriod(this); + } + + /** + * Constructor for Period object. + * + * @param stringStartDate A string representing the start date of the period. + * @param stringEndDate A string representing the end date of the period. + */ + public Period(String stringStartDate, String stringEndDate) { + this.startDate = parser.parseDate(stringStartDate); + this.endDate = parser.parseDate(stringEndDate); + this.periodLength = calculatePeriodLength(); + this.cycleLength = 0; + healthList.addPeriod(this); + } + + /** + * Gets cycle length. + * + * @return Cycle length as long. + */ + public long getCycleLength() { + return cycleLength; + } + + /** + * Updates the end date of the period and calculates the period length. + * + * @param stringEndDate A String representing the new end date of the period. + */ + public void updateEndDate(String stringEndDate) { + this.endDate = parser.parseDate(stringEndDate); + this.periodLength = calculatePeriodLength(); + } + + /** + * Retrieves the start date of the period of LocalDate type. + * + * @return The start date of period. + * @throws AssertionError if the start date is null. + */ + public LocalDate getStartDate() { + assert startDate != null : ErrorConstant.NULL_START_DATE_ERROR; + return startDate; + } + + /** + * Retrieves the end date of the period of LocalDate type. + * + * @return The end date of period. + */ + public LocalDate getEndDate() { + return endDate; + } + + /** + * Retrieves the length of the period of long type. + * + * @return The period length. + */ + public long getPeriodLength() { + assert periodLength > HealthConstant.MIN_LENGTH : ErrorConstant.LENGTH_MUST_BE_POSITIVE_ERROR; + return periodLength; + } + + //@@author j013n3 + /** + * Calculates the sum of the cycle lengths of the latest three menstrual cycles. + * + * @return The sum of the cycle lengths of the latest three menstrual cycles. + */ + public long getLastThreeCycleLengths() { + long sumOfCycleLengths = 0; + int startIndexForPrediction = HealthConstant.LAST_CYCLE_INDEX; + assert startIndexForPrediction >= HealthConstant.FIRST_ITEM : ErrorConstant.START_INDEX_NEGATIVE_ERROR; + + int endIndexForPrediction = HealthConstant.FIRST_CYCLE_INDEX; + assert endIndexForPrediction >= startIndexForPrediction : ErrorConstant.END_INDEX_SMALLER_THAN_START_ERROR; + + for (int i = startIndexForPrediction; i <= endIndexForPrediction; i++) { + sumOfCycleLengths += Objects.requireNonNull(HealthList.getPeriod(i)).cycleLength; + } + return sumOfCycleLengths; + } + + /** + * Predicts the start date of the next period based on the average cycle length. + * + * @return The predicted start date of the next period. + */ + public LocalDate nextCyclePrediction() { + long averageCycleLength = getLastThreeCycleLengths() / HealthConstant.LATEST_THREE_CYCLE_LENGTHS; + return getStartDate().plusDays(averageCycleLength); + } + + /** + * Sets the cycle length of the current period based on the start date of the next period. + * + * @param nextStartDate The start date of the next period. + */ + public void setCycleLength(LocalDate nextStartDate) { + this.cycleLength = ChronoUnit.DAYS.between(getStartDate(), nextStartDate); + } + + /** + * Prints a message indicating the number of days until the predicted start date of the next period, + * or how many days late the period is if the current date is after the predicted start date. + * + * @param nextPeriodStartDate The predicted start date of the next period. + */ + public static void printNextCyclePrediction(LocalDate nextPeriodStartDate) { + LocalDate today = LocalDate.now(); + long daysUntilNextPeriod = today.until(nextPeriodStartDate, ChronoUnit.DAYS); + if (today.isBefore(nextPeriodStartDate)) { + System.out.println(HealthConstant.PREDICTED_START_DATE_MESSAGE + + nextPeriodStartDate + + HealthConstant.COUNT_DAYS_MESSAGE + + daysUntilNextPeriod + + UiConstant.SPLIT_BY_WHITESPACE + + HealthConstant.DAYS_MESSAGE + + UiConstant.FULL_STOP); + } + + if (today.isEqual(nextPeriodStartDate)) { + System.out.println(HealthConstant.PREDICTED_START_DATE_MESSAGE + + nextPeriodStartDate + + HealthConstant.PREDICTED_DATE_IS_TODAY_MESSAGE); + } + + if (today.isAfter(nextPeriodStartDate)) { + System.out.println(HealthConstant.PREDICTED_START_DATE_MESSAGE + + nextPeriodStartDate + + HealthConstant.PERIOD_IS_LATE + + -daysUntilNextPeriod + + UiConstant.SPLIT_BY_WHITESPACE + + HealthConstant.DAYS_MESSAGE + + UiConstant.FULL_STOP); + } + Output.printLine(); + } + + /** + * Returns the string representation of a Period object. + * + * @return A formatted string representing a Period object. + */ + @Override + public String toString() { + String endDateUnit = (getEndDate() == null) ? ErrorConstant.NO_DATE_SPECIFIED_ERROR : getEndDate().toString(); + return String.format(HealthConstant.PRINT_PERIOD_FORMAT, + getStartDate(), + endDateUnit, + getPeriodLength(), + HealthConstant.DAYS_MESSAGE) + + (this.cycleLength > HealthConstant.MIN_LENGTH ? System.lineSeparator() + + String.format(HealthConstant.PRINT_CYCLE_FORMAT, this.cycleLength) : UiConstant.EMPTY_STRING); + } + + /** + * Calculates the length of the period in days. + * + * @return The length of the period. + */ + protected long calculatePeriodLength() { + if (endDate == null || startDate == null) { + return 0; + } else { + // Add 1 to include both start and end dates + return ChronoUnit.DAYS.between(getStartDate(), getEndDate()) + 1; + } + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/main/java/seedu/pulsepilot/PulsePilot.java b/src/main/java/seedu/pulsepilot/PulsePilot.java new file mode 100644 index 0000000000..7f6419247d --- /dev/null +++ b/src/main/java/seedu/pulsepilot/PulsePilot.java @@ -0,0 +1,21 @@ +package seedu.pulsepilot; + +import ui.Handler; + +/** + * Main class representing the entry-point for PulsePilot. + */ +public class PulsePilot { + //@@author L5-Z + /** + * Main entry-point for PulsePilot. + * + * @param args Command-line arguments. + */ + public static void main(String[] args) { + Handler handler = new Handler(); + handler.initialiseBot(); + handler.processInput(); + handler.terminateBot(); + } +} diff --git a/src/main/java/storage/DataFile.java b/src/main/java/storage/DataFile.java new file mode 100644 index 0000000000..6178d41eb5 --- /dev/null +++ b/src/main/java/storage/DataFile.java @@ -0,0 +1,508 @@ +//@@author L5-Z +package storage; + +import java.io.FileWriter; +import java.nio.file.Files; +import java.io.IOException; +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Scanner; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import health.Appointment; +import health.Bmi; +import health.Period; +import constants.ErrorConstant; +import ui.Output; +import utility.Parser; +import utility.Validation; +import workouts.Gym; +import workouts.Run; +import workouts.Workout; +import utility.CustomExceptions; +import constants.UiConstant; +import utility.Filters.DataType; + +/** + * Represents a DataFile object used to read and write data stored in PulsePilot to a file. + * Handles the reading and writing of various data related to PulsePilot, including user's name, + * health data like BMI, appointments, periods, and workout data like runs and gym sessions. + * It provides methods to load, save, and process different types of data as well as prevent tampering via hashing. + */ +public class DataFile { + public static String userName = null; + private static DataFile instance = null; + + private final Output output; + private final Validation validation; + + + /** + * Private constructor to prevent instantiation from outside the class. + * Initializes the data file. + */ + public DataFile() { + output = new Output(); + validation = new Validation(); + } + + /** + * Verifies the integrity of the data file by checking its existence and hs value. + * If the data file already exists, checks its hash value against the expected hash value. + * If the data file does not exist, creates a new file and logs the event. + * + * @param dataFile The data file to verify integrity. + * @return Returns 0 if the file is found. Else, returns 1. + * @throws CustomExceptions.FileCreateError If there is an error creating the data file. + */ + public int verifyIntegrity(File dataFile) throws CustomExceptions.FileCreateError { + try { + if (dataFile.createNewFile()) { + LogFile.writeLog("Created new data file", false); + return UiConstant.FILE_NOT_FOUND; + } else { + LogFile.writeLog("Reading from existing data file", false); + return UiConstant.FILE_FOUND; + } + } catch (IOException e) { + throw new CustomExceptions.FileCreateError(ErrorConstant.CREATE_FILE_ERROR); + } + } + + /** + * Initializes the data file to be used. Or loads the existing data file, verifies its integrity, and processes + * its content. Exits if the file cannot be created or loaded. + * + * @return Returns 0 if the file is found. Else, returns 1. + */ + public int loadDataFile() { + int status = UiConstant.FILE_NOT_FOUND; + validation.validateDirectoryPermissions(); + try { + File dataFile = UiConstant.saveFile; + File hashFile = new File(UiConstant.hashFilePath); + + if (dataFile.exists() && hashFile.exists()) { + String expectedHash = generateFileHash(dataFile); + String actualHash = readHashFromFile(hashFile); + + if (expectedHash.equals(actualHash)) { + status = verifyIntegrity(dataFile); + } else { + processFail(ErrorConstant.DATA_INTEGRITY_ERROR); + System.exit(1); + } + } else if (!dataFile.exists() && !hashFile.exists()) { + status = verifyIntegrity(dataFile); + } else { + processFail(ErrorConstant.MISSING_INTEGRITY_ERROR); + System.exit(1); + } + } catch (CustomExceptions.FileCreateError e) { + System.err.println(ErrorConstant.CREATE_FILE_ERROR); + LogFile.writeLog(ErrorConstant.CREATE_FILE_ERROR, true); + System.exit(1); + } catch (IOException | NoSuchAlgorithmException e) { + LogFile.writeLog("Error occurred while processing file hash: " + e.getMessage(), true); + output.printException(ErrorConstant.HASH_ERROR); + System.exit(1); + } + + Path dataFilePath = Path.of(UiConstant.dataFilePath); + assert Files.exists(dataFilePath) : "Data file does not exist."; + return status; + } + + /** + * Reads data from the existing data file and processes it. + * + * @throws CustomExceptions.FileReadError If there is an error reading the data file. + */ + public void readDataFile() throws CustomExceptions.FileReadError { + int lineNumberCount = 0; // just for getting lineNumber, no other use + try (final Scanner readFile = new Scanner(UiConstant.saveFile)) { + LogFile.writeLog("Read begins", false); + try { + String[] input = readFile.nextLine().split(UiConstant.SPLIT_BY_COLON); + String name = input[UiConstant.NAME_INDEX].trim(); + LogFile.writeLog("Processing Name", false); + processName(name); + LogFile.writeLog("Name Loaded", false); + + } catch (Exception e) { + LogFile.writeLog("Data file is missing name, exiting." + e, true); + processFail(ErrorConstant.CORRUPT_ERROR); + System.exit(1); + } + + while (readFile.hasNextLine()) { + String rawInput = readFile.nextLine(); + LogFile.writeLog("Read String: " + rawInput, false); + String[] input = rawInput.split(UiConstant.SPLIT_BY_COLON); + String dataType = input[UiConstant.DATA_TYPE_INDEX].trim(); + + LogFile.writeLog("Current DataType:" + dataType, false); + DataType filter = DataType.valueOf(dataType); + switch (filter) { + + case APPOINTMENT: + processAppointment(input); + break; + + case PERIOD: + processPeriod(input); + break; + + case BMI: + processBmi(input); + break; + + case GYM: + processGym(rawInput); + break; + + case RUN: + processRun(input); + break; + + default: + break; // valueOf results in immediate exception for non-match with enum DataType + } + lineNumberCount += 1; + } + } catch (Exception e) { + LogFile.writeLog("Data file is missing content at line " + lineNumberCount + ", exiting." + e, + true); + processFail(ErrorConstant.CORRUPT_ERROR); + System.exit(1); + } + } + + /** + * Processes the username from the data file. + * + * @param name The username read from the data file. + * @throws CustomExceptions.InvalidInput If the username is invalid. + */ + public void processName(String name) throws CustomExceptions.InvalidInput { + if (validation.validateIfUsernameIsValid(name)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_USERNAME_ERROR); + } + userName = name.trim(); + } + + /** + * Processes an appointment entry from the input string array and adds it to the health list. + * + * @param input The input string array containing appointment data. + * @throws CustomExceptions.InsufficientInput If there is insufficient input data. + * @throws CustomExceptions.InvalidInput If there is an error in the input data format. + */ + public void processAppointment(String[] input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String date = input[1].trim(); // date + String time = input[2].trim(); // time + String formattedTime = time.replace(".", ":"); + String description = input[3].trim(); // description + String[] checkAppointmentDetails = {date, formattedTime, description}; + validation.validateAppointmentDetails(checkAppointmentDetails); + Appointment appointment = new Appointment(date, formattedTime, description); + } + + /** + * Processes a period entry from the input string array and adds it to the health list. + * + * @param input The input string array containing period data. + * @throws CustomExceptions.InsufficientInput If there is insufficient input data. + * @throws CustomExceptions.InvalidInput If there is an error in the input data format. + */ + public void processPeriod(String[] input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String startDate = input[1].trim(); // start + String endDate = input[2].trim(); // end, skip 3 duration + String[] checkPeriodInput = {startDate, endDate}; + boolean isParser = false; + validation.validatePeriodInput(checkPeriodInput, isParser); + if (endDate.equals(ErrorConstant.NO_DATE_SPECIFIED_ERROR)) { + new Period(startDate); + } else { + new Period(startDate, endDate); + } + } + + /** + * Processes a BMI entry from the input string array and adds it to the health list. + * + * @param input The input string array containing BMI data. + * @throws CustomExceptions.InsufficientInput If there is insufficient input data. + * @throws CustomExceptions.InvalidInput If there is an error in the input data format. + */ + public void processBmi(String[] input) throws CustomExceptions.InsufficientInput, CustomExceptions.InvalidInput { + String height = input[1].trim(); // height + String weight = input[2].trim(); // weight + String date = input[4].trim();// skip 3, bmi score, 4 is date + String[] checkBmiInput = {height, weight, date}; + validation.validateBmiInput(checkBmiInput); + new Bmi(height, weight, date); + } + + /** + * Processes a run entry from the input string array and adds it to the workout list. + * + * @param input The input string array containing run data. + * @throws CustomExceptions.InvalidInput If there is an error in the input data format. + * @throws CustomExceptions.InsufficientInput If there is insufficient input data. + */ + public void processRun(String[] input) throws CustomExceptions.InvalidInput, CustomExceptions.InsufficientInput { + String distance = input[1].trim(); // distance + String time = input[2].trim(); // time + String formattedTime = time.replace(".", ":"); + String date = input[3].trim(); // 3 is date + String[] checkRunInput = {formattedTime, distance, date}; + validation.validateRunInput(checkRunInput); + if (date.equals(ErrorConstant.NO_DATE_SPECIFIED_ERROR)) { + new Run(formattedTime, distance); + } else { + new Run(formattedTime, distance, date); + } + } + + /** + * Processes a gym entry from the raw input string and delegates parsing to Parser class. + * + * @param rawInput The raw input string containing gym data. + * @throws CustomExceptions.InvalidInput If there is an error in the input data format. + * @throws CustomExceptions.FileReadError If there is an error reading the gym file. + * @throws CustomExceptions.InsufficientInput If there is insufficient input data. + */ + public void processGym(String rawInput) throws CustomExceptions.InvalidInput, CustomExceptions.FileReadError, + CustomExceptions.InsufficientInput { + + Parser newParser = new Parser(); + newParser.parseGymFileInput(rawInput); + } + + /** + * Saves data to the data file. + * + * @param name The username to be saved. + * @param bmiArrayList List of BMI entries to be saved. + * @param appointmentArrayList List of appointment entries to be saved. + * @param periodArrayList List of period entries to be saved. + * @param workoutArrayList List of workout entries to be saved. + * @throws CustomExceptions.FileWriteError If there is an error writing to the data file. + */ + public void saveDataFile(String name, + ArrayList bmiArrayList, + ArrayList appointmentArrayList, + ArrayList periodArrayList, + ArrayList workoutArrayList + ) throws CustomExceptions.FileWriteError { + + try (FileWriter dataFile = new FileWriter(UiConstant.dataFilePath)) { + LogFile.writeLog("Attempting to write name: " + name, false); + writeName(dataFile, name); + + writeHealthData(dataFile, bmiArrayList, + appointmentArrayList, + periodArrayList); + + writeWorkoutData(dataFile, workoutArrayList); + + LogFile.writeLog("Write end", false); + + } catch (IOException e) { + throw new CustomExceptions.FileWriteError(ErrorConstant.SAVE_ERROR); + } + + try (FileWriter hashFile = new FileWriter(UiConstant.hashFilePath)) { + LogFile.writeLog("Attempting to write hash", false); + File dataFile = UiConstant.saveFile; + writeHashToFile(generateFileHash(dataFile)); + + LogFile.writeLog("Write end", false); + + } catch (IOException | NoSuchAlgorithmException e) { + throw new CustomExceptions.FileWriteError(ErrorConstant.SAVE_ERROR); + } + } + + /** + * Writes the user's name to the data file. + * + * @param dataFile The FileWriter object for writing to the data file. + * @param name The user's name to be written to the file. + * @throws IOException If an I/O error occurs while writing to the file. + */ + public void writeName(FileWriter dataFile, String name) throws IOException { + dataFile.write(UiConstant.NAME_LABEL + UiConstant.SPLIT_BY_COLON + name.trim() + System.lineSeparator()); + LogFile.writeLog("Wrote name to file", false); + } + + /** + * Writes health-related data (BMI, appointments, periods) to the data file. + * + * @param dataFile The FileWriter object for writing to the data file. + * @param bmiArrayList The list of BMI entries to be written. + * @param appointmentArrayList The list of appointment entries to be written. + * @param periodArrayList The list of period entries to be written. + * @throws IOException If an I/O error occurs while writing to the file. + */ + public void writeHealthData(FileWriter dataFile, ArrayList bmiArrayList, + ArrayList appointmentArrayList, + ArrayList periodArrayList) throws IOException { + Parser newParser = new Parser(); + // Write each bmi entry in a specific format + // bmi format: bmi:HEIGHT:WEIGHT:BMI_SCORE:DATE (NA if no date) + if (!bmiArrayList.isEmpty()) { + for (Bmi bmiEntry : bmiArrayList) { + String formattedDate = newParser.parseFormattedDate(bmiEntry.getDate()); + + dataFile.write(DataType.BMI + UiConstant.SPLIT_BY_COLON + bmiEntry.getHeight() + + UiConstant.SPLIT_BY_COLON + bmiEntry.getWeight() + + UiConstant.SPLIT_BY_COLON + bmiEntry.getBmiValueString() + + UiConstant.SPLIT_BY_COLON + formattedDate + System.lineSeparator()); + } + } + + // Write each appointment entry in a specific format + // appointment format: appointment:DATE:TIME:DESCRIPTION + if (!appointmentArrayList.isEmpty()) { + for (Appointment appointmentEntry : appointmentArrayList) { + String formattedDate = newParser.parseFormattedDate(appointmentEntry.getDate()); + String formattedTime = String.valueOf(appointmentEntry.getTime()); + formattedTime = formattedTime.replace(":", "."); + + dataFile.write(DataType.APPOINTMENT + UiConstant.SPLIT_BY_COLON + formattedDate + + UiConstant.SPLIT_BY_COLON + formattedTime + + UiConstant.SPLIT_BY_COLON + appointmentEntry.getDescription() + System.lineSeparator()); + } + } + + // Write each period entry in a specific format + // period format: period:START:END:DURATION + if (!periodArrayList.isEmpty()) { + for (Period periodEntry : periodArrayList) { + LogFile.writeLog("Writing period to file", false); + String formattedStartDate = newParser.parseFormattedDate(periodEntry.getStartDate()); + String formattedEndDate = newParser.parseFormattedDate(periodEntry.getEndDate()); + + dataFile.write(DataType.PERIOD + UiConstant.SPLIT_BY_COLON + formattedStartDate + + UiConstant.SPLIT_BY_COLON + formattedEndDate + + UiConstant.SPLIT_BY_COLON + periodEntry.getPeriodLength() + System.lineSeparator()); + LogFile.writeLog("Wrote period to file", false); + } + } + + } + + /** + * Writes workout-related data (runs and gym sessions) to the data file. + * + * @param dataFile The FileWriter object for writing to the data file. + * @param workoutArrayList The list of workout entries to be written. + * @throws IOException If an I/O error occurs while writing to the file. + */ + public void writeWorkoutData(FileWriter dataFile, + ArrayList workoutArrayList) throws IOException { + + // Write each run entry in a specific format + // run format: run:DISTANCE:TIME:DATE + if (!workoutArrayList.isEmpty()) { + for (Workout workoutEntry : workoutArrayList) { + if (workoutEntry instanceof Run) { + Run runEntry = (Run) workoutEntry; + String formattedDate = runEntry.getDateForFile(); + String formattedTime = runEntry.getTimes().replace(":", "."); + + dataFile.write(DataType.RUN + UiConstant.SPLIT_BY_COLON + runEntry.getDistance() + + UiConstant.SPLIT_BY_COLON + formattedTime + + UiConstant.SPLIT_BY_COLON + formattedDate + System.lineSeparator()); + } else if (workoutEntry instanceof Gym) { + Gym gymEntry = (Gym) workoutEntry; + String gymString = gymEntry.toFileString(); + dataFile.write(gymString + System.lineSeparator()); + } + } + } + } + //@@author L5-Z + /** + * Generates the SHA-256 hash value of the pulsepilot_data.txt file. + * + * @param file The file for which to generate the hash. + * @return A String representing the SHA-256 hash value of the pulsepilot_data.txt file. + * @throws NoSuchAlgorithmException If the SHA-256 algorithm is not available. + * @throws IOException If an I/O error occurs while reading the file. + */ + protected String generateFileHash(File file) throws NoSuchAlgorithmException, IOException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + FileInputStream fis = new FileInputStream(file); + byte[] dataBytes = new byte[1024]; + + int bytesRead; + while ((bytesRead = fis.read(dataBytes)) != -1) { + md.update(dataBytes, 0, bytesRead); + } + + byte[] digest = md.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b & 0xff)); + } + + fis.close(); + return sb.toString(); + } + + /** + * Handles the failure of file hash verification. + * This method is called when the hash value of the data file does not match the expected value. + * It logs the error, prints the exception and deletes the data file and hash file. + * + * @param errorString The error message to be logged and printed. + */ + protected void processFail(String errorString) { + File dataFile = UiConstant.saveFile; + File hashFile = new File(UiConstant.hashFilePath); + + LogFile.writeLog(errorString, true); + output.printException(errorString); + + hashFile.delete(); + dataFile.delete(); + } + + /** + * Reads the hash value from a hash file. + * + * @param hashFile The hash file to read from. + * @return The hash value read from the file. + * @throws IOException If an I/O error occurs. + */ + protected String readHashFromFile(File hashFile) throws IOException { + StringBuilder sb = new StringBuilder(); + Scanner scanner = new Scanner(hashFile); + while (scanner.hasNextLine()) { + sb.append(scanner.nextLine()); + } + scanner.close(); + return sb.toString(); + } + + /** + * Writes the hash value to the hash file. + * + * @param hash The hash value to write. + * @throws IOException If an I/O error occurs. + */ + private void writeHashToFile(String hash) throws IOException { + FileOutputStream fos = new FileOutputStream(UiConstant.hashFilePath); + fos.write(hash.getBytes()); + fos.close(); + } +} + diff --git a/src/main/java/storage/LogFile.java b/src/main/java/storage/LogFile.java new file mode 100644 index 0000000000..d81512a19e --- /dev/null +++ b/src/main/java/storage/LogFile.java @@ -0,0 +1,98 @@ +package storage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.FileHandler; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + + +import constants.UiConstant; +import utility.Validation; + +//@@author L5-Z +/** + * Represents a Logfile object used to write information and error logs for PulsePilot. + */ +public class LogFile { + protected static FileHandler logFileHandler = null; + private static LogFile instance = null; + private static final Logger logger = Logger.getLogger(LogFile.class.getName()); + + /** + * Private constructor to prevent instantiation from outside the class. + */ + private LogFile() { + initializeLogFile(); + } + + /** + * Returns a singular instance of the LogFile class. + * If the instance is null, it creates a new instance. + * + * @return An instance of the LogFile class. + */ + public static LogFile getInstance() { + if (instance == null) { + instance = new LogFile(); + } + return instance; + } + + //@@author rouvinerh + /** + * Initialises the log file to be used. Creates the log file if needed, then sets formatters. + * Parent handlers are set to false to prevent printing of logs to terminal. + */ + public static void initializeLogFile() { + Validation validation = new Validation(); + validation.validateDirectoryPermissions(); + try { + if (logFileHandler == null) { + logFileHandler = new FileHandler(UiConstant.LOG_FILE_PATH); + logFileHandler.setFormatter(new SimpleFormatter()); + logger.addHandler(logFileHandler); + logger.setUseParentHandlers(false); + } + } catch (IOException e) { + throw new RuntimeException("Error setting up log file", e); + } + assert(logFileHandler != null); + } + + //@@author L5-Z + /** + * Writes information or warning logs to the log file. + * + * @param input String representing the user's input. + * @param isError Boolean variable to determine if log is an error. + */ + public static void writeLog(String input, boolean isError) { + if (isError) { + logger.log(Level.WARNING, input); + } else { + logger.log(Level.INFO, input); + } + } + + /** + * Reads the log file content. + * + * @return Log file contents. + */ + public static String readLogContent() { + StringBuilder logContent = new StringBuilder(); + try { + List lines = Files.readAllLines(Path.of(UiConstant.LOG_FILE_PATH)); + for (String line : lines) { + logContent.append(line).append(System.lineSeparator()); + } + } catch (IOException e) { + System.err.println("Error reading log file: " + e.getMessage()); + } + return logContent.toString(); + } +} diff --git a/src/main/java/ui/Handler.java b/src/main/java/ui/Handler.java new file mode 100644 index 0000000000..c3e7ef95d9 --- /dev/null +++ b/src/main/java/ui/Handler.java @@ -0,0 +1,339 @@ +//@@author L5-Z +package ui; + +import health.Appointment; +import health.Bmi; +import health.HealthList; +import health.Period; +import storage.DataFile; +import utility.CustomExceptions; +import constants.ErrorConstant; +import constants.UiConstant; +import constants.HealthConstant; +import constants.WorkoutConstant; +import utility.Filters.Command; +import utility.Filters.DeleteFilters; +import utility.Filters.HealthFilters; +import utility.Parser; +import utility.Filters.WorkoutFilters; +import utility.Validation; +import workouts.Workout; +import workouts.WorkoutLists; + +import java.util.ArrayList; +import java.util.Scanner; +import storage.LogFile; + +/** + * Represents user input parsing and handling before providing feedback to the user. + */ +public class Handler { + + //@@author JustinSoh + static LogFile logFile = LogFile.getInstance(); + private final Scanner in; + private final Parser parser; + private final DataFile dataFile; + private final Output output; + private final Validation validation; + + public Handler(){ + in = new Scanner(System.in); + parser = new Parser(in); + dataFile = new DataFile(); + output = new Output(); + validation = new Validation(); + } + + public Handler(String input){ + in = new Scanner(input); // use for JUnit Testing + parser = new Parser(in); + dataFile = new DataFile(); + output = new Output(); + validation = new Validation(); + } + + //@@author L5-Z + /** + * Processes user input and filters for valid command words from enum Command, + * then creates the relevant object based on details entered. + * + * @throws IllegalArgumentException If an error occurs during command processing. + */ + public void processInput() { + while (in.hasNextLine()) { + String userInput = in.nextLine().trim(); + String instruction = userInput.toUpperCase().split(UiConstant.SPLIT_BY_WHITESPACE)[0]; + LogFile.writeLog("User Input: " + userInput, false); + assert userInput != null : "Object cannot be null"; + + try { + Command command = Command.valueOf(instruction); + switch (command) { + case EXIT: + System.out.println(UiConstant.EXIT_MESSAGE); + return; + + case WORKOUT: + handleWorkout(userInput); + break; + + case HEALTH: + handleHealth(userInput); + break; + + case HISTORY: + handleHistory(userInput); + break; + + case LATEST: + handleLatest(userInput); + break; + + case DELETE: + handleDelete(userInput); + break; + + case HELP: + output.printHelp(); + break; + + default: + break; // valueOf results in immediate exception for non-match with enum Command + } + } catch (CustomExceptions.InvalidInput e) { + output.printException(e.getMessage()); + } catch (IllegalArgumentException e) { + LogFile.writeLog("Invalid Command Error: " + userInput, true); + output.printException(ErrorConstant.INVALID_COMMAND_ERROR); + } + } + } + + //@@author JustinSoh + /** + * Handles workout command. + * Adds a Run or Gym object to PulsePilot. + * + * @param userInput The user input string. + */ + public void handleWorkout(String userInput) { + try { + String typeOfWorkout = parser.extractSubstringFromSpecificIndex(userInput, + WorkoutConstant.EXERCISE_FLAG); + WorkoutFilters filter = WorkoutFilters.valueOf(typeOfWorkout.toUpperCase()); + switch(filter) { + case RUN: + parser.parseRunInput(userInput); + break; + + case GYM: + parser.parseGymInput(userInput); + break; + + default: + break; + } + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + output.printException(e.getMessage()); + } catch (IllegalArgumentException e) { + output.printException(ErrorConstant.INVALID_WORKOUT_TYPE_ERROR); + } + } + + //@@author rouvinerh + /** + * Handles history command. + * Show history of all valid objects recorded. + * + * @param userInput The user input string. + */ + public void handleHistory(String userInput) { + String filter = parser.parseHistory(userInput); + if (filter != null) { + output.printHistory(filter); + LogFile.writeLog("Viewed history for " + filter, false); + } + } + + /** + * Handles the delete command. + * Deletes an item recorded. + * + * @param userInput The user input string. + * @throws CustomExceptions.InvalidInput If the user input is invalid. + */ + public void handleDelete(String userInput) throws CustomExceptions.InvalidInput { + String[] parsedInputs = parser.parseDeleteInput(userInput); + if (parsedInputs == null) { + return; + } + try { + DeleteFilters filter = DeleteFilters.valueOf(parsedInputs[0].toUpperCase()); + int index = Integer.parseInt(parsedInputs[1]) - 1; + switch (filter) { + case BMI: + HealthList.deleteBmi(index); + break; + + case PERIOD: + HealthList.deletePeriod(index); + break; + + case GYM: + WorkoutLists.deleteGym(index); + break; + + case RUN: + WorkoutLists.deleteRun(index); + break; + + case APPOINTMENT: + HealthList.deleteAppointment(index); + break; + + default: + break; + } + } catch (CustomExceptions.OutOfBounds e) { + output.printException(e.getMessage()); + } + } + + //@@author syj02 + /** + * Handles Health command. + * Parses the user input to determine the type of health data and processes it accordingly. + * + * @param userInput The user input string. + */ + public void handleHealth(String userInput) { + try { + String typeOfHealth = parser.extractSubstringFromSpecificIndex(userInput, HealthConstant.HEALTH_FLAG); + HealthFilters filter = HealthFilters.valueOf(typeOfHealth.toUpperCase()); + switch(filter) { + case BMI: + parser.parseBmiInput(userInput); + break; + + case PERIOD: + parser.parsePeriodInput(userInput); + break; + + case PREDICTION: + parser.parsePredictionInput(); + break; + + case APPOINTMENT: + parser.parseAppointmentInput(userInput); + break; + + default: + break; + } + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput | CustomExceptions.OutOfBounds e) { + output.printException(e.getMessage()); + } catch (IllegalArgumentException e) { + output.printException(ErrorConstant.INVALID_HEALTH_INPUT_ERROR); + } + } + + //@@author JustinSoh + + /** + * Prints the latest valid object recorded. + * + * @param userInput The user input string. + */ + public void handleLatest(String userInput) { + String filter = parser.parseLatest(userInput); + if (filter != null) { + output.printLatest(filter); + LogFile.writeLog("Viewed latest for " + filter, false); + } + } + + //@@author L5-Z + + /** + * Get user's name, and print profile induction messages. + */ + public void userInduction() { + String name; + while (true) { + name = this.in.nextLine().trim(); + if (validation.validateIfUsernameIsValid(name)) { + System.err.println(ErrorConstant.INVALID_USERNAME_ERROR); + } else { + break; + } + } + + DataFile.userName = name; + System.out.println("Welcome aboard, Captain " + name); + Output.printLine(); + + System.out.println("Tips: Enter 'help' to view the pilot manual!"); + System.out.println("Initiating FTL jump sequence..."); + + // DataFile.saveName(name); + LogFile.writeLog("Name Entered: " + name, false); + System.out.println("FTL jump completed."); + } + + //@@author L5-Z + /** + * Initializes PulsePilot by printing a welcome message, loading tasks from storage, + * and returning the tasks list. + */ + public void initialiseBot() { + output.printWelcomeBanner(); + LogFile.writeLog("Started bot", false); + + int status = dataFile.loadDataFile(); + + if (status == 0) { + try { + dataFile.readDataFile(); // File read + output.printGreeting(status, DataFile.userName); + } catch (CustomExceptions.FileReadError e) { + output.printException(e.getMessage()); + } + } else { + output.printGreeting(status, DataFile.userName); + userInduction(); + } + + System.out.println("Terminal primed. Command inputs are now accepted..."); + Output.printLine(); + } + + /** + * Terminates PulsePilot by saving tasks to storage, printing a goodbye message, + * and indicating the filename where tasks are saved. + */ + public void terminateBot() { + LogFile.writeLog("User terminating PulsePilot", false); + + try { + LogFile.writeLog("Attempting to save data file", false); + + String userName = DataFile.userName; + ArrayList workoutList = WorkoutLists.getWorkouts(); + ArrayList bmiList = HealthList.getBmis(); + ArrayList appointmentList = HealthList.getAppointments(); + ArrayList periodList = HealthList.getPeriods(); + dataFile.saveDataFile(userName, bmiList, appointmentList, periodList, workoutList); + + } catch (CustomExceptions.FileWriteError e) { + LogFile.writeLog("File write error", true); + output.printException(e.getMessage()); + } + + output.printGoodbyeMessage(); + // Yet to implement : Reply.printReply("Saved tasks as: " + Constant.FILE_NAME); + LogFile.writeLog("Bot exited gracefully", false); + System.exit(0); + } + +} diff --git a/src/main/java/ui/Output.java b/src/main/java/ui/Output.java new file mode 100644 index 0000000000..a0b7a2a540 --- /dev/null +++ b/src/main/java/ui/Output.java @@ -0,0 +1,617 @@ +package ui; + +import constants.ErrorConstant; +import constants.UiConstant; +import constants.WorkoutConstant; +import constants.HealthConstant; +import utility.CustomExceptions; + +import workouts.Gym; +import workouts.GymStation; +import workouts.Run; +import workouts.Workout; +import workouts.WorkoutLists; +import health.HealthList; +import health.Bmi; +import health.Period; +import health.Appointment; +import utility.Filters.HistoryAndLatestFilters; + +import java.util.ArrayList; + +/** + * The Output class handles printing various messages, data, and ASCII art for the user interface. + */ +public class Output { + + //@@author L5-Z + /** + * Prints a horizontal line. + */ + public static void printLine() { + System.out.println(UiConstant.PARTITION_LINE); + } + + /** + * Prints the help message. + */ + public void printHelp() { + printLine(); + System.out.println("Commands List:"); + System.out.println(); + System.out.println("workout /e:run /d:DISTANCE /t:TIME [/date:DATE] - Add a new run"); + System.out.println("workout /e:gym /n:NUMBER_OF_STATIONS [/date:DATE] - Add a new gym workout"); + System.out.println("health /h:bmi /height:HEIGHT /weight:WEIGHT /date:DATE - Add new BMI data"); + System.out.println("health /h:period /start:START_DATE [/end:END_DATE] - Add new period data"); + System.out.println("health /h:prediction - Predicts next period's start date"); + System.out.println("health /h:appointment /date:DATE /time:TIME /description:DESCRIPTION" + + " - Add new appointment data"); + + System.out.println("history /item:[run/gym/workouts/bmi/period/appointment] - " + + "Shows history of run/gym/workouts/bmi/period/appointment records"); + System.out.println("latest /item:[run/gym/bmi/period/appointment] - " + + "Shows latest entry of run/gym/bmi/period/appointment records"); + System.out.println("delete /item:[run/gym/bmi/period/appointment] /index:INDEX - " + + "Deletes a run/gym/bmi/period/appointment record"); + + System.out.println("help - Show this help message"); + System.out.println("exit - Exit the program"); + printLine(); + } + + //@@author JustinSoh + /** + * Prints the gym station prompt. + * + * @param stationNumber Integer representing the current gym station index. + */ + public void printGymStationPrompt(int stationNumber) { + printLine(); + System.out.println("Please enter the details of station " + + stationNumber + + ". (Format: " + WorkoutConstant.STATION_GYM_FORMAT + ")"); + System.out.println("Enter 'back' to go back to the main menu!"); + printLine(); + } + + /** + * Prints the text header when adding a new Run. + * + * @param newRun The new Run object added. + */ + public void printAddRun(Run newRun) { + printLine(); + System.out.println(WorkoutConstant.ADD_RUN); + System.out.println(WorkoutConstant.RUN_HEADER); + System.out.println(newRun); + printLine(); + } + + //@@author j013n3 + /** + * Prints the message when a new Bmi is added. + * + * @param newBmi The new Bmi object added. + */ + public void printAddBmi(Bmi newBmi) { + printLine(); + System.out.println(HealthConstant.BMI_ADDED_MESSAGE_PREFIX + + newBmi.getHeight() + + UiConstant.LINE + + newBmi.getWeight() + + UiConstant.LINE + + newBmi.getDate()); + System.out.println(newBmi); + printLine(); + } + + /** + * Prints the message when a new Period is added. + * + * @param newPeriod The new Period object added. + */ + public void printAddPeriod(Period newPeriod) { + printLine(); + System.out.println(HealthConstant.PERIOD_ADDED_MESSAGE_PREFIX + + newPeriod.getStartDate() + + UiConstant.LINE + + (newPeriod.getEndDate() == null ? ErrorConstant.NO_DATE_SPECIFIED_ERROR : newPeriod.getEndDate())); + System.out.println(newPeriod); + if (newPeriod.getEndDate() != null) { + printPeriodWarning(newPeriod); + } + printLine(); + } + + /** + * Prints the message when period length is not within the healthy range. + * + * @param newPeriod The new Period object added. + */ + public void printPeriodWarning(Period newPeriod) { + if (newPeriod.getPeriodLength() < 2 || newPeriod.getPeriodLength() > 7) { + System.out.println("\u001b[31m" + HealthConstant.PERIOD_TOO_LONG_MESSAGE + "\u001b[0m"); + } + } + + //@@author syj02 + /** + * Prints the message when a new Appointment is added. + * + * @param newAppointment The new Appointment object added. + */ + public void printAddAppointment(Appointment newAppointment) { + printLine(); + System.out.println(HealthConstant.APPOINTMENT_ADDED_MESSAGE_PREFIX + + newAppointment.getDate() + + UiConstant.LINE + + newAppointment.getTime() + + UiConstant.LINE + + newAppointment.getDescription()); + System.out.println(newAppointment); + printLine(); + } + + //@@author JustinSoh + /** + * Prints the text header when adding a new Gym. + * + * @param gym The new Gym object added. + */ + public void printAddGym(Gym gym) { + printLine(); + System.out.println(WorkoutConstant.ADD_GYM); + printGymStats(gym); + printLine(); + } + + /** + * Prints the message when user exits from entering gym station input. + */ + public void printGymStationExit() { + printLine(); + System.out.println("No longer entering gym input."); + System.out.println("Exiting to main menu!"); + printLine(); + } + + //@@author rouvinerh + /** + * Prints the output for the history command. + * + * @param filter The type of item, which is set to Workouts, Run, Gym, Bmi, Period, or Appointment. + */ + public void printHistory(String filter) { + try { + HistoryAndLatestFilters parsedFilter = HistoryAndLatestFilters.valueOf(filter.toUpperCase()); + switch (parsedFilter) { + case WORKOUTS: + printWorkoutHistory(); + break; + + case RUN: + printRunHistory(); + break; + + case GYM: + printGymHistory(); + break; + + case BMI: + printBmiHistory(); + break; + + case PERIOD: + printPeriodHistory(); + break; + + case APPOINTMENT: + printAppointmentHistory(); + break; + + default: + break; + } + } catch (CustomExceptions.OutOfBounds | CustomExceptions.InvalidInput e ) { + printException(e.getMessage()); + } catch (IllegalArgumentException e) { + printException(ErrorConstant.INVALID_HISTORY_FILTER_ERROR); + } + } + + //@@author JustinSoh + /** + * Prints a specified message and the exception error message. + * + * @param message The custom error to be printed. + */ + public void printException(String message) { + System.err.println("\u001b[31mException Caught!" + System.lineSeparator() + message + "\u001b[0m"); + } + + //@@author L5-Z + /** + * Prints the welcome banner for PulsePilot. + */ + public void printWelcomeBanner() { + printLine(); + printArt(); + System.out.println("Engaging orbital thrusters..."); + System.out.println("PulsePilot on standby"); + printLine(); + } + + //@@author rouvinerh + /** + * Prints delete run message. + * + * @param run Run to delete. + */ + public static void printDeleteRunMessage(Run run){ + printLine(); + String messageString = String.format(WorkoutConstant.RUN_DELETE_MESSAGE_FORMAT, + run.getDistance(), + run.getPace()); + System.out.println(messageString); + printLine(); + } + + //@@author JustinSoh + /** + * Prints delete gym message. + * + * @param gym Gym to delete + */ + public static void printDeleteGymMessage(Gym gym){ + Output.printLine(); + String messageString = String.format(WorkoutConstant.GYM_DELETE_MESSAGE_FORMAT, + gym.getStations().size()); + + System.out.println(messageString); + Output.printLine(); + } + //@@author L5-Z + /** + * Checks whether storage file is present, and prints corresponding message. + * + * @param status Integer representing whether the storage file has been loaded. If set to 0, file is present. Else, + * file is not present. + * @param name String representing the name of the user. + */ + protected void printGreeting(int status, String name) { + if (status == UiConstant.FILE_FOUND) { + System.out.println(UiConstant.FILE_FOUND_MESSAGE + name); + System.out.println(UiConstant.SUCCESSFUL_LOAD); + } else { + System.out.println(UiConstant.FILE_MISSING_MESSAGE); + } + printLine(); + } + + /** + * Prints the goodbye message for PulsePilot. + */ + protected void printGoodbyeMessage() { + printLine(); + System.out.println("PulsePilot successful touchdown"); + System.out.println("See you soon, Captain!"); + printLine(); + } + + //@@author rouvinerh + /** + * Prints all Period entries recorded. + * + * @throws CustomExceptions.OutOfBounds If there is access to a Period object that does not exist. + */ + protected void printPeriodHistory() throws CustomExceptions.OutOfBounds { + try { + printLine(); + HealthList.printPeriodHistory(); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + //@@author rouvinerh + /** + * Prints all Appointment entries recorded. + * + * @throws CustomExceptions.OutOfBounds If there is access to an Appointment object that does not exist. + */ + protected void printAppointmentHistory() throws CustomExceptions.OutOfBounds { + try { + printLine(); + HealthList.printAppointmentHistory(); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + /** + * Prints the latest Run entry recorded. + */ + protected void printLatestRun() { + try { + printLine(); + Run latestRun = WorkoutLists.getLatestRun(); + String latestRunString = getFormattedRunWithIndex(WorkoutLists.getRunSize(), latestRun); + System.out.println("Your latest run:"); + System.out.println(WorkoutConstant.RUN_HEADER_INDEX_FORMAT); + System.out.println(latestRunString); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + //@@author JustinSoh + /** + * Prints the latest Gym entry recorded. + */ + protected void printLatestGym() { + try { + printLine(); + Gym latestGym = WorkoutLists.getLatestGym(); + int index = WorkoutLists.getGymSize(); + System.out.println("Your latest gym:"); + System.out.println("Gym Session " + index + latestGym); + printGymStats(latestGym); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException((e.getMessage())); + } + } + + //@@author j013n3 + /** + * Prints the latest BMI entry recorded. + */ + protected void printLatestBmi() { + try { + printLine(); + HealthList.printLatestBmi(); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + /** + * Prints the latest Period entry recorded. + */ + protected void printLatestPeriod() { + try { + printLine(); + HealthList.printLatestPeriod(); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + //@@author syj_02 + /** + * Prints the latest Appointment entry recorded. + */ + protected void printLatestAppointment(){ + try { + printLine(); + HealthList.printLatestAppointment(); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + //@@author rouvinerh + /** + * Prints the output for the latest command. + * + * @param filter The type of item, which is set to Run, Gym, Bmi, Period, or Appointment. + */ + protected void printLatest(String filter) { + try { + HistoryAndLatestFilters parsedFilter = HistoryAndLatestFilters.valueOf(filter.toUpperCase()); + switch (parsedFilter) { + case RUN: + printLatestRun(); + break; + + case GYM: + printLatestGym(); + break; + + case BMI: + printLatestBmi(); + break; + + case PERIOD: + printLatestPeriod(); + break; + + case APPOINTMENT: + printLatestAppointment(); + break; + + default: + break; + } + } catch (IllegalArgumentException e) { + printException(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER); + } + } + + //@@author rouvinerh + /** + * Prints all Bmi entries recorded. + * + * @throws CustomExceptions.OutOfBounds If there is access to a Bmi object that does not exist. + */ + protected void printBmiHistory() throws CustomExceptions.OutOfBounds { + try { + printLine(); + HealthList.printBmiHistory(); + printLine(); + } catch (CustomExceptions.OutOfBounds e) { + printException(e.getMessage()); + } + } + + //@@author JustinSoh + /** + * Prints all Workout objects (Run and Gym) based on the time it was added. + * The list is sorted in descending order. (Latest one first) + * + * @throws CustomExceptions.OutOfBounds If index is out of bounds. + * @throws CustomExceptions.InvalidInput If user input is invalid. + */ + protected void printWorkoutHistory() throws CustomExceptions.OutOfBounds, CustomExceptions.InvalidInput { + printLine(); + System.out.println(WorkoutConstant.HISTORY_WORKOUTS_HEADER); + System.out.println(WorkoutConstant.HISTORY_WORKOUTS_HEADER_FORMAT); + + ArrayList workoutList = WorkoutLists.getWorkouts(); + if (workoutList.isEmpty()) { + printWorkoutEmptyMessage(); + } else { + for (int i = 0; i < workoutList.size(); i++) { + Workout workout = workoutList.get(i); + if (workout instanceof Run) { + Run run = (Run) workout; + String formattedRunString = run.getFormatForAllHistory(); + System.out.printf((WorkoutConstant.HISTORY_WORKOUTS_DATA_HEADER_FORMAT) + "%n", + (i + 1), formattedRunString); + } else { + Gym gym = (Gym) workout; + int numberOfStation = gym.getStations().size(); + for (int j = 0; j < numberOfStation; j++) { + String gymString; + if (j == 0) { + gymString = String.format(WorkoutConstant.HISTORY_WORKOUTS_DATA_HEADER_FORMAT, + (i + 1), gym.getHistoryFormatForSpecificGymStation(j)); + } else { + gymString = String.format(WorkoutConstant.HISTORY_WORKOUTS_DATA_HEADER_FORMAT, + "", gym.getHistoryFormatForSpecificGymStation(j)); + } + System.out.println(gymString); + } + } + } + } + printLine(); + } + + /** + * Prints all the Run objects added to the list. + * + * @throws CustomExceptions.OutOfBounds If index is out of bounds. + */ + protected void printRunHistory() throws CustomExceptions.OutOfBounds { + printLine(); + System.out.println("Your run history:"); + ArrayList runList = WorkoutLists.getRuns(); + + if(runList.isEmpty()){ + printRunEmptyMessage(); + } else { + String runHeader = String.format(WorkoutConstant.RUN_HEADER_INDEX_FORMAT); + System.out.println(runHeader); + + for (int i = 0; i < runList.size(); i++) { + int index = i + 1; + Run currentRun = runList.get(i); + String output = getFormattedRunWithIndex(index, currentRun); + System.out.println(output); + } + } + printLine(); + } + + /** + * Prints all the stations within a specified Gym object. + * + * @param gym The Gym object containing the GymStation objects to be printed. + */ + protected void printGymStats(Gym gym) { + ArrayList allStations = gym.getStations(); + for (int i = 0; i < allStations.size(); i++) { + System.out.printf("Station %d %s%n", i + 1, allStations.get(i).toString()); + } + } + + /** + * Prints all the information for all Gym objects within the list. + */ + protected void printGymHistory() { + printLine(); + System.out.println("Your gym history:"); + ArrayList gymList = WorkoutLists.getGyms(); + if (gymList.isEmpty()) { + printGymEmptyMessage(); + } else { + printGymList(gymList); + } + printLine(); + } + + /** + * Prints a message when the Gym list is empty. + */ + private void printGymEmptyMessage(){ + printException(ErrorConstant.GYM_EMPTY_ERROR); + } + + /** + * Prints a message when the Run list is empty. + */ + private void printRunEmptyMessage(){ + printException(ErrorConstant.RUN_EMPTY_ERROR); + } + + /** + * Prints a message when workouts list is empty. + */ + private void printWorkoutEmptyMessage(){ + printException(ErrorConstant.WORKOUTS_EMPTY_ERROR); + } + + //@@author L5-Z + /** + * Prints an ASCII Art depicting the word 'PulsePilot'. + */ + private void printArt() { + System.out.println(" _ _"); + System.out.println("|_) | _ _ |_) o | _ _|_"); + System.out.println("| |_| | _> (/_| | | (_) |_"); + } + + //@@author JustinSoh + /** + * Prints all Gym entries. + */ + private void printGymList(ArrayList gymList){ + for (int i = 0; i < gymList.size(); i++) { + int index = i + 1; + Gym currentWorkout = gymList.get(i); + System.out.println("Gym Session " + index + currentWorkout); + printGymStats(currentWorkout); + if (i != gymList.size() - 1) { + printLine(); + } + } + } + + /** + * Returns the formatted string for printing runs. + * + * @param index The index of the run. + * @return Formatted string for the run. + */ + private String getFormattedRunWithIndex(int index, Run currentRun) { + return String.format(WorkoutConstant.RUN_DATA_INDEX_FORMAT, index, currentRun); + } + +} diff --git a/src/main/java/utility/CustomExceptions.java b/src/main/java/utility/CustomExceptions.java new file mode 100644 index 0000000000..08af2e7454 --- /dev/null +++ b/src/main/java/utility/CustomExceptions.java @@ -0,0 +1,83 @@ +package utility; + +import constants.ErrorConstant; +import storage.LogFile; +/** + * Represents a custom exception class designed for PulsePilot to handle errors during command processing. + */ +public class CustomExceptions extends Exception { + + //@@author JustinSoh + /** + * Prints the error for an OutOfBounds error, and logs it in the log file as an error. + */ + public static class OutOfBounds extends Exception { + public OutOfBounds(String message) { + super(ErrorConstant.COLOR_HEADING + + ErrorConstant.OUT_OF_BOUND_HEADER + + message + + ErrorConstant.COLOR_ENDING); + LogFile.writeLog(ErrorConstant.OUT_OF_BOUND_HEADER + message, true); + } + } + + /** + * Prints the error for an InvalidInput error, and logs it in the log file as an error. + */ + public static class InvalidInput extends Exception { + public InvalidInput(String message) { + super(ErrorConstant.COLOR_HEADING + + ErrorConstant.INVALID_INPUT_HEADER + + message + + ErrorConstant.COLOR_ENDING); + LogFile.writeLog(ErrorConstant.INVALID_INPUT_HEADER + message, true); + } + } + + //@@author L5-Z + /** + * Prints the error for an FileReadError error, and logs it in the log file as an error. + */ + public static class FileReadError extends Exception{ + public FileReadError(String message) { + super(ErrorConstant.COLOR_HEADING + ErrorConstant.FILE_READ_HEADER + message + ErrorConstant.COLOR_ENDING); + LogFile.writeLog(ErrorConstant.FILE_READ_HEADER + message, true); + } + } + + /** + * Prints the error for an FileWriteError error, and logs it in the log file as an error. + */ + public static class FileWriteError extends Exception{ + public FileWriteError(String message) { + super( ErrorConstant.COLOR_HEADING + ErrorConstant.FILE_WRITE_HEADER + + message + ErrorConstant.COLOR_ENDING); + LogFile.writeLog(ErrorConstant.FILE_WRITE_HEADER + message, true); + } + } + + /** + * Prints the error for an FileCreateError error, and logs it in the log file as an error. + */ + public static class FileCreateError extends Exception{ + public FileCreateError(String message) { + super(ErrorConstant.COLOR_HEADING + ErrorConstant.FILE_CREATE_HEADER + + message + ErrorConstant.COLOR_ENDING); + LogFile.writeLog(ErrorConstant.FILE_CREATE_HEADER + message, true); + } + } + + //@@author JustinSoh + /** + * Prints the error for an InsufficientInput error, and logs it in the log file as an error. + */ + public static class InsufficientInput extends Exception { + public InsufficientInput(String message) { + super(ErrorConstant.COLOR_HEADING + + ErrorConstant.INSUFFICIENT_INPUT_HEADER + + message + + ErrorConstant.COLOR_ENDING); + LogFile.writeLog(ErrorConstant.INSUFFICIENT_INPUT_HEADER + message, true); + } + } +} diff --git a/src/main/java/utility/Filters.java b/src/main/java/utility/Filters.java new file mode 100644 index 0000000000..30bff02263 --- /dev/null +++ b/src/main/java/utility/Filters.java @@ -0,0 +1,56 @@ +//@@author L5-Z +package utility; + +/** + * Class representing the filters used for PulsePilot. + */ + +public class Filters { + public enum Command { + WORKOUT, + HISTORY, + LATEST, + HEALTH, + DELETE, + HELP, + EXIT + } + + public enum DeleteFilters { + RUN, + GYM, + PERIOD, + BMI, + APPOINTMENT, + } + + public enum HealthFilters { + PERIOD, + BMI, + APPOINTMENT, + PREDICTION, + } + + public enum WorkoutFilters { + RUN, + GYM, + } + + public enum HistoryAndLatestFilters { + RUN, + GYM, + PERIOD, + BMI, + APPOINTMENT, + WORKOUTS + } + + public enum DataType { + BMI, + APPOINTMENT, + PERIOD, + GYM, + RUN + } + +} diff --git a/src/main/java/utility/Parser.java b/src/main/java/utility/Parser.java new file mode 100644 index 0000000000..be702b9c82 --- /dev/null +++ b/src/main/java/utility/Parser.java @@ -0,0 +1,739 @@ +package utility; + +import constants.ErrorConstant; +import constants.HealthConstant; +import constants.UiConstant; +import constants.WorkoutConstant; +import health.Appointment; +import health.Bmi; +import health.HealthList; +import health.Period; +import storage.LogFile; +import ui.Output; + +import workouts.Gym; +import workouts.Run; +import workouts.WorkoutLists; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Objects; +import java.util.Scanner; + +/** + * Represents the parser used to parse and split input for PulsePilot. + */ +public class Parser { + //@@author JustinSoh + private final Scanner in; + private final Validation validation; + private final Output output; + + public Parser(Scanner inputScanner) { + in = inputScanner; + validation = new Validation(); + output = new Output(); + } + + public Parser() { + in = new Scanner(System.in); + validation = new Validation(); + output = new Output(); + } + + //@@author rouvinerh + /** + * Parses and converts String date to a LocalDate variable. + * + * @param date String representing the date. + * @return LocalDate variable representing the date. + * @throws DateTimeParseException If there is an error parsing the date. + */ + public LocalDate parseDate(String date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate formattedDate = null; + try { + formattedDate = LocalDate.parse(date, formatter); + } catch (DateTimeParseException e) { + output.printException(ErrorConstant.PARSING_DATE_ERROR); + } + return formattedDate; + } + + //@@author L5-Z + /** + * Converts a LocalDate object to a formatted String representation. Returns "NA" if date is null. + * + * @param date LocalDate object representing the date. + * @return Formatted String representation of the date in the format "dd-MM-yyyy". + */ + public String parseFormattedDate(LocalDate date) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + if (date == null) { + return "NA"; + } + return date.format(formatter); + } + + //@@author syj02 + /** + * Parses and converts String time to a LocalDate variable. + * + * @param stringTime String representing the time. + * @return LocalTime variable representing the time. + * @throws DateTimeParseException If there is an error parsing the time. + */ + public LocalTime parseTime(String stringTime) throws DateTimeParseException { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + LocalTime formattedTime = null; + try { + formattedTime = LocalTime.parse(stringTime, formatter); + } catch (DateTimeParseException e) { + output.printException(ErrorConstant.PARSING_TIME_ERROR); + } + return formattedTime; + } + + //@@author rouvinerh + /** + * Parses and validates user input for the delete command. Returns an array of parsed user input + * containing the filter string and the index to delete. + * + * @param userInput The user input string. + * @return An array of strings containing the filter string and index to delete. + */ + public String[] parseDeleteInput(String userInput) { + try { + String[] deleteDetails = splitDeleteInput(userInput); + validation.validateDeleteInput(deleteDetails); + return deleteDetails; + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + output.printException(e.getMessage()); + return null; + } + } + + //@@author JustinSoh + /** + * Function validates and parses the user input for the history command. + * + * @param userInput The user input string. + * @return The filter string, set to either 'gym', 'run', 'workouts', 'bmi', 'appointment' or 'period'. + */ + public String parseHistory(String userInput) { + try { + if (countForwardSlash(userInput) > UiConstant.NUM_OF_SLASHES_FOR_LATEST_AND_HISTORY) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + String filter = extractSubstringFromSpecificIndex(userInput, UiConstant.ITEM_FLAG); + + if (filter.isBlank()) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_HISTORY_FILTER_ERROR); + } + validation.validateHistoryFilter(filter.toLowerCase()); + return filter.toLowerCase(); + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + output.printException(e.getMessage()); + return null; + } + } + + //@@author JustinSoh + /** + * Function validates and parses the user input for the latest command. + * + * @param userInput The user input string. + * @return The filter string, set to either 'gym', 'run', 'bmi' , 'appointment', or 'period'. + */ + public String parseLatest(String userInput) { + try { + if (countForwardSlash(userInput) > UiConstant.NUM_OF_SLASHES_FOR_LATEST_AND_HISTORY) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + String filter = extractSubstringFromSpecificIndex(userInput, UiConstant.ITEM_FLAG); + + if (filter.isBlank()) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_LATEST_FILTER_ERROR); + } + validation.validateDeleteAndLatestFilter(filter.toLowerCase()); + return filter.toLowerCase(); + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + output.printException(e.getMessage()); + return null; + } + } + + //@@author syj02 + /** + * Parses input for Bmi command. Adds Bmi object to HealthList if valid. + * + * @param userInput The user input string. + * @throws CustomExceptions.InvalidInput If input is invalid. + * @throws CustomExceptions.InsufficientInput If the height, weight or date parameters are missing. + */ + public void parseBmiInput(String userInput) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + String[] bmiDetails = splitBmiInput(userInput); + validation.validateBmiInput(bmiDetails); + Bmi newBmi = new Bmi( + bmiDetails[HealthConstant.BMI_HEIGHT_INDEX], + bmiDetails[HealthConstant.BMI_WEIGHT_INDEX], + bmiDetails[HealthConstant.BMI_DATE_INDEX]); + output.printAddBmi(newBmi); + LogFile.writeLog("Added BMI", false); + } + + //@@author j013n3 + /** + * Parses input for Period command. Adds Period object to HealthList if valid. + * + * @param userInput The user input string. + * @throws CustomExceptions.InvalidInput If input is invalid. + * @throws CustomExceptions.InsufficientInput If the start date or end date parameters are missing. + */ + public void parsePeriodInput(String userInput) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + int size = HealthList.getPeriodSize(); + String[] periodDetails = splitPeriodInput(userInput); + boolean isParser = true; + validation.validatePeriodInput(periodDetails, isParser); + + if (userInput.contains(HealthConstant.END_FLAG)) { + if ((size == 0) || (size > 0 && + Objects.requireNonNull(HealthList.getPeriod(HealthConstant.FIRST_ITEM).getEndDate() != null))) { + Period newPeriod = new Period( + periodDetails[HealthConstant.PERIOD_START_DATE_INDEX], + periodDetails[HealthConstant.PERIOD_END_DATE_INDEX]); + output.printAddPeriod(newPeriod); + LogFile.writeLog("Added Period", false); + } else if (size > 0 && + Objects.requireNonNull(HealthList.getPeriod(HealthConstant.FIRST_ITEM)).getEndDate() == null) { + Period latestPeriod = Objects.requireNonNull(HealthList.getPeriod(HealthConstant.FIRST_ITEM)); + latestPeriod.updateEndDate(periodDetails[HealthConstant.PERIOD_END_DATE_INDEX]); + output.printAddPeriod(latestPeriod); + LogFile.writeLog("Added Period", false); + } + } else { + Period newPeriod = new Period(periodDetails[HealthConstant.PERIOD_START_DATE_INDEX]); + output.printAddPeriod(newPeriod); + LogFile.writeLog("Added Period", false); + } + } + + /** + * Parses input for Prediction command. + * Prints period prediction if possible. + * + * @throws CustomExceptions.InsufficientInput If prediction cannot be made. + * @throws CustomExceptions.OutOfBounds If period list is empty + */ + public void parsePredictionInput() throws CustomExceptions.InsufficientInput, CustomExceptions.OutOfBounds { + if (HealthList.getPeriodSize() >= HealthConstant.MIN_SIZE_FOR_PREDICTION) { + HealthList.printLatestThreeCycles(); + LocalDate nextPeriodStartDate = HealthList.predictNextPeriodStartDate(); + Period.printNextCyclePrediction(nextPeriodStartDate); + LogFile.writeLog("Used prediction", false); + } else { + throw new CustomExceptions.InsufficientInput(ErrorConstant.UNABLE_TO_MAKE_PREDICTIONS_ERROR); + } + } + + //@@author syj_02 + /** + * Parses input for Appointment command. Adds Appointment object to HealthList if valid. + * + * @param userInput The user input string. + * @throws CustomExceptions.InvalidInput If input is invalid. + * @throws CustomExceptions.InsufficientInput If the date, time or description parameters are missing. + */ + public void parseAppointmentInput(String userInput) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + String[] appointmentDetails = splitAppointmentDetails(userInput); + validation.validateAppointmentDetails(appointmentDetails); + Appointment newAppointment = new Appointment( + appointmentDetails[HealthConstant.APPOINTMENT_DATE_INDEX], + appointmentDetails[HealthConstant.APPOINTMENT_TIME_INDEX], + appointmentDetails[HealthConstant.APPOINTMENT_DESCRIPTION_INDEX]); + output.printAddAppointment(newAppointment); + LogFile.writeLog("Added appointment", false); + } + + //@@author L5-Z + /** + * Extracts a substring from the given input string based on the provided delimiter. + * + * @param input The input string from which to extract the substring. + * @param delimiter The delimiter to search for in the input string. + * @return The extracted substring, or an empty string if the delimiter is not found. + */ + public String extractSubstringFromSpecificIndex(String input, String delimiter) { + int index = input.indexOf(delimiter); + if (index == -1 || index == input.length() - delimiter.length()) { + return ""; + } + int startIndex = index + delimiter.length(); + int endIndex = input.indexOf("/", startIndex); + if (endIndex == -1) { + endIndex = input.length(); + } + return input.substring(startIndex, endIndex).trim(); + } + + //@@author JustinSoh + /** + * Parses input for the Gym command. Adds Gym object if valid. + * + * @param userInput The user input string. + * @throws CustomExceptions.InvalidInput If input is invalid. + * @throws CustomExceptions.InsufficientInput If number of station parameter is missing. + */ + public void parseGymInput(String userInput) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String[] gymDetails = splitGymInput(userInput); + validation.validateGymInput(gymDetails); + Gym newGym; + if (gymDetails[WorkoutConstant.GYM_DATE_INDEX] == null) { + newGym = new Gym(); + } else { + newGym = new Gym(gymDetails[WorkoutConstant.GYM_DATE_INDEX]); + } + + int numberOfStations = Integer.parseInt(gymDetails[WorkoutConstant.GYM_NUMBER_OF_STATIONS_INDEX]); + parseGymStationInput(numberOfStations, newGym); + } + + //@@author rouvinerh + /** + * Parses input for the Run command. Adds a Run object if valid. + * + * @param input The user input string. + * @throws CustomExceptions.InvalidInput If input is invalid. + * @throws CustomExceptions.InsufficientInput If the run time or run distance parameters are missing. + */ + public void parseRunInput(String input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String[] runDetails = splitRunInput(input); + validation.validateRunInput(runDetails); + Run newRun; + if (runDetails[WorkoutConstant.RUN_DATE_INDEX] == null) { + newRun = new Run( + runDetails[WorkoutConstant.RUN_TIME_INDEX], + runDetails[WorkoutConstant.RUN_DISTANCE_INDEX]); + } else { + newRun = new Run( + runDetails[WorkoutConstant.RUN_TIME_INDEX], + runDetails[WorkoutConstant.RUN_DISTANCE_INDEX], + runDetails[WorkoutConstant.RUN_DATE_INDEX]); + } + output.printAddRun(newRun); + LogFile.writeLog("Added Run", false); + } + + //@@author JustinSoh + /** + * Parses the gym station input from the user and adds it to the Gym object. + * This method is used in the parseGymInput method. + * User can input 'back' to exit the gym station input. + * The gym object will be deleted and control returned to handler. + * + * @param numberOfStations The number of stations in one gym session. + * @param gym The Gym object. + */ + public void parseGymStationInput(int numberOfStations, Gym gym) { + for (int i = 0; i < numberOfStations; i++) { + try { + // Prompt user for gym station details + output.printGymStationPrompt(i + 1); + String userInput = this.in.nextLine(); + + // If user wants to exit the gym station input + if (userInput.equals(WorkoutConstant.BACK)) { + output.printGymStationExit(); + WorkoutLists.deleteGym(WorkoutLists.getGymSize() - 1); + return; + } + + // Split the gym station input + String[] splitGymStationInputs = splitGymStationInput(userInput); + String exerciseName = splitGymStationInputs[WorkoutConstant.GYM_STATION_NAME_INDEX]; + String numberOfSets = splitGymStationInputs[WorkoutConstant.GYM_STATION_SET_INDEX]; + String numberOfReps = splitGymStationInputs[WorkoutConstant.GYM_STATION_REPS_INDEX]; + String weights = splitGymStationInputs[WorkoutConstant.GYM_STATION_WEIGHTS_INDEX]; + + // Create a new GymStation object and add it to the Gym + gym.addStation(exerciseName, numberOfSets, numberOfReps, weights); + + } catch (CustomExceptions.InsufficientInput | CustomExceptions.InvalidInput + | CustomExceptions.OutOfBounds e) { + i -= 1; + output.printException(e.getMessage()); + } + } + output.printAddGym(gym); + LogFile.writeLog("Added Gym", false); + } + + /** + * Parses the gym input from the storage file and returns a Gym object. + * The input of the storage file needs to be in the following format + * gym:NUM_STATIONS:DATE:STATION1_NAME:NUM_SETS:REPS:WEIGHT1,WEIGHT2,WEIGHT3,WEIGHT4 + * :STATION2_NAME:NUM_SETS:REPS:WEIGHT1,WEIGHT2,WEIGHT3,WEIGHT4 .... + * + * @param input The line read from the file. + * @return New gym object created from the input. + * @throws CustomExceptions.InvalidInput If there is invalid input from the file. + * @throws CustomExceptions.InsufficientInput If there is insufficient input from the file. + * @throws CustomExceptions.FileReadError If the file data is invalid or cannot be read. + */ + public Gym parseGymFileInput(String input) + throws CustomExceptions.InvalidInput, + CustomExceptions.FileReadError, CustomExceptions.InsufficientInput { + + String[] gymDetails = splitGymFileInput(input); + String[] checkGymDetails = new String[WorkoutConstant.NUMBER_OF_GYM_PARAMETERS]; + checkGymDetails[0] = gymDetails[1]; + checkGymDetails[1] = gymDetails[2]; + validation.validateGymInput(checkGymDetails); + Gym gym; + String date = gymDetails[WorkoutConstant.DATE_FILE_INDEX]; + + if (date.equals(ErrorConstant.NO_DATE_SPECIFIED_ERROR)) { + gym = new Gym(); + } else { + gym = new Gym(date); + } + + int counter = WorkoutConstant.GYM_FILE_BASE_COUNTER; + while (counter < gymDetails.length) { + counter = addStationFromFile(gym, gymDetails, counter); + } + return gym; + } + + //@@author rouvinerh + /** + * Splits user input for Delete command into item and index. + * + * @param input The user input string. + * @return An array of strings containing the extracted delete command parameters. + * @throws CustomExceptions.InsufficientInput If not enough parameters are specified. + * @throws CustomExceptions.InvalidInput If there is invalid input. + */ + protected String[] splitDeleteInput(String input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + if (!input.contains(UiConstant.ITEM_FLAG) || !input.contains(UiConstant.INDEX_FLAG)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_DELETE_PARAMETERS_ERROR); + } + + if (countForwardSlash(input) > UiConstant.NUM_OF_SLASHES_FOR_DELETE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + + String[] results = new String[UiConstant.NUM_DELETE_PARAMETERS]; + results[UiConstant.DELETE_ITEM_STRING_INDEX] = extractSubstringFromSpecificIndex(input, + UiConstant.ITEM_FLAG).trim(); + results[UiConstant.DELETE_ITEM_NUMBER_INDEX] = extractSubstringFromSpecificIndex(input, + UiConstant.INDEX_FLAG).trim(); + return results; + } + + //@@author JustinSoh + /** + * Splits user input for gym station. + * + * @param input The user input string. + * @return An array of strings containing gym station parameters. + * @throws CustomExceptions.InvalidInput If the parameters are invalid. + */ + protected String[] splitGymStationInput(String input) throws CustomExceptions.InvalidInput { + if (countForwardSlash(input) > WorkoutConstant.NUM_OF_SLASHES_FOR_GYM_STATION) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + Parser parser = new Parser(); + String exerciseName = input.split(UiConstant.SPLIT_BY_SLASH)[WorkoutConstant.STATION_NAME_INDEX].trim(); + String sets = parser.extractSubstringFromSpecificIndex(input, WorkoutConstant.SETS_FLAG).trim(); + String reps = parser.extractSubstringFromSpecificIndex(input, WorkoutConstant.REPS_FLAG).trim(); + String weights = parser.extractSubstringFromSpecificIndex(input, WorkoutConstant.WEIGHTS_FLAG).trim(); + + + String[] validatedGymStationInputs = new String[WorkoutConstant.NUMBER_OF_GYM_STATION_PARAMETERS]; + validatedGymStationInputs[WorkoutConstant.GYM_STATION_NAME_INDEX] = exerciseName; + validatedGymStationInputs[WorkoutConstant.GYM_STATION_SET_INDEX] = sets; + validatedGymStationInputs[WorkoutConstant.GYM_STATION_REPS_INDEX] = reps; + validatedGymStationInputs[WorkoutConstant.GYM_STATION_WEIGHTS_INDEX] = weights; + return validatedGymStationInputs; + } + + //@@author rouvinerh + /** + * Splits the user input for adding a run. + * + * @param input The user input string. + * @return The Run parameters split from the user input in an array of strings. + * @throws CustomExceptions.InsufficientInput If the distance and time taken for the run are missing. + * @throws CustomExceptions.InvalidInput If the parameters specified are invalid. + */ + protected String[] splitRunInput(String input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + if (!input.contains(WorkoutConstant.DISTANCE_FLAG) || + !input.contains(WorkoutConstant.RUN_TIME_FLAG)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_RUN_PARAMETERS_ERROR); + } + + if (countForwardSlash(input) != WorkoutConstant.NUM_OF_SLASHES_FOR_RUN_WITH_DATE && + countForwardSlash(input) != WorkoutConstant.NUM_OF_SLASHES_FOR_RUN_WITHOUT_DATE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + + String[] results = new String[WorkoutConstant.NUMBER_OF_RUN_PARAMETERS]; + results[WorkoutConstant.RUN_TIME_INDEX] = extractSubstringFromSpecificIndex(input, + WorkoutConstant.RUN_TIME_FLAG).trim(); + results[WorkoutConstant.RUN_DISTANCE_INDEX] = extractSubstringFromSpecificIndex(input, + WorkoutConstant.DISTANCE_FLAG).trim(); + + if (input.contains(WorkoutConstant.DATE_FLAG)) { + results[WorkoutConstant.RUN_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + WorkoutConstant.DATE_FLAG).trim(); + } + return results; + } + + //@@author JustinSoh + /** + * Splits the user input when adding a Gym. + * + * @param input The user input string. + * @return The Gym parameters split from the user input. + * @throws CustomExceptions.InsufficientInput If the number of stations is missing. + * @throws CustomExceptions.InvalidInput If the parameters specified are invalid. + */ + protected String[] splitGymInput(String input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + if (!input.contains(WorkoutConstant.NUMBER_OF_STATIONS_FLAG)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_GYM_PARAMETERS_ERROR); + } + + if (countForwardSlash(input) != WorkoutConstant.NUM_OF_SLASHES_FOR_GYM_WITH_DATE && + countForwardSlash(input) != WorkoutConstant.NUM_OF_SLASHES_FOR_GYM_WITHOUT_DATE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + + + String[] results = new String[WorkoutConstant.NUMBER_OF_GYM_PARAMETERS]; + results[WorkoutConstant.GYM_NUMBER_OF_STATIONS_INDEX] = extractSubstringFromSpecificIndex(input, + WorkoutConstant.NUMBER_OF_STATIONS_FLAG).trim(); + + if (input.contains(WorkoutConstant.DATE_FLAG)) { + results[WorkoutConstant.GYM_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + WorkoutConstant.DATE_FLAG).trim(); + } + return results; + } + + //@@author syj02 + /** + * Split user input into Appointment command, date, time and description. + * + * @param input The user input string. + * @return An array of strings containing the extracted Appointment parameters. + * @throws CustomExceptions.InsufficientInput If the user input is invalid or blank. + * @throws CustomExceptions.InvalidInput If the user input is invalid. + */ + protected String[] splitAppointmentDetails(String input) + throws CustomExceptions.InsufficientInput, CustomExceptions.InvalidInput { + String[] results = new String[HealthConstant.NUM_APPOINTMENT_PARAMETERS]; + if (!input.contains(HealthConstant.DATE_FLAG) + || !input.contains(HealthConstant.TIME_FLAG) + || !input.contains(HealthConstant.DESCRIPTION_FLAG)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_APPOINTMENT_PARAMETERS_ERROR); + } + + if (countForwardSlash(input) > HealthConstant.NUM_OF_SLASHES_FOR_APPOINTMENT) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + + results[HealthConstant.APPOINTMENT_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.DATE_FLAG).trim(); + results[HealthConstant.APPOINTMENT_TIME_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.TIME_FLAG).trim(); + results[HealthConstant.APPOINTMENT_DESCRIPTION_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.DESCRIPTION_FLAG).trim(); + return results; + } + + /** + * Split user input for Bmi command, height, weight and date. + * + * @param input The user input string. + * @return An array of strings containing the extracted Bmi parameters. + * @throws CustomExceptions.InsufficientInput If the height, weight or date parameters are missing. + * @throws CustomExceptions.InvalidInput If the input is invalid. + */ + protected String[] splitBmiInput(String input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + + if (!input.contains(HealthConstant.HEIGHT_FLAG) + || !input.contains(HealthConstant.WEIGHT_FLAG) + || !input.contains(HealthConstant.DATE_FLAG)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_BMI_PARAMETERS_ERROR); + } + + if (countForwardSlash(input) > HealthConstant.NUM_OF_SLASHES_FOR_BMI) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + + String[] results = new String[HealthConstant.NUM_BMI_PARAMETERS]; + results[HealthConstant.BMI_HEIGHT_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.HEIGHT_FLAG).trim(); + results[HealthConstant.BMI_WEIGHT_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.WEIGHT_FLAG).trim(); + results[HealthConstant.BMI_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.DATE_FLAG).trim(); + return results; + } + + //@@author j013n3 + /** + * Split user input into Period command, start date and end date. + * + * @param input The user input string. + * @return An array of strings containing the extracted Period parameters. + * @throws CustomExceptions.InsufficientInput If the user input is invalid or blank. + * @throws CustomExceptions.InvalidInput If the user input is invalid. + */ + protected String[] splitPeriodInput(String input) throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + if (!input.contains(HealthConstant.START_FLAG) + || (!input.contains(HealthConstant.START_FLAG) && !input.contains(HealthConstant.END_FLAG))) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_PERIOD_PARAMETERS_ERROR); + } + + if (countForwardSlash(input) > HealthConstant.NUM_OF_SLASHES_FOR_PERIOD) { + throw new CustomExceptions.InvalidInput(ErrorConstant.TOO_MANY_SLASHES_ERROR); + } + + String[] results = new String[HealthConstant.NUM_PERIOD_PARAMETERS]; + + if (input.contains(HealthConstant.START_FLAG) && input.contains(HealthConstant.END_FLAG)) { + results[HealthConstant.PERIOD_START_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.START_FLAG).trim(); + results[HealthConstant.PERIOD_END_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.END_FLAG).trim(); + } else if (input.contains(HealthConstant.START_FLAG) && !input.contains(HealthConstant.END_FLAG)) { + results[HealthConstant.PERIOD_START_DATE_INDEX] = extractSubstringFromSpecificIndex(input, + HealthConstant.START_FLAG).trim(); + } + + return results; + } + + //@@author L5-Z + /** + * Splits the Gym input that comes from the data file. + * Validates the numberOfStation and date input. + * + * @param input The file input string. + * @return String[] containing the gym details + * @throws CustomExceptions.FileReadError If the file data is invalid or cannot be read. + */ + private String[] splitGymFileInput(String input) throws CustomExceptions.FileReadError { + + String[] gymDetails = input.split(UiConstant.SPLIT_BY_COLON); + String gymType; + String numOfStationStr; + int numOfStation; + String date; + + // checks if there are enough parameters in the gym file + if numOfStation is a digit + try { + gymType = gymDetails[WorkoutConstant.GYM_FILE_INDEX].toLowerCase(); + numOfStationStr = gymDetails[WorkoutConstant.NUM_OF_STATIONS_FILE_INDEX]; + numOfStation = Integer.parseInt(numOfStationStr); + date = gymDetails[WorkoutConstant.DATE_FILE_INDEX]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new CustomExceptions.FileReadError(ErrorConstant.LOAD_GYM_FORMAT_ERROR); + } catch (NumberFormatException e) { + throw new CustomExceptions.FileReadError(ErrorConstant.LOAD_NUMBER_OF_STATION_ERROR); + } + + // Check if the gym type is correct (e.g. storage starts with gym| ...) + if (!gymType.equals(WorkoutConstant.GYM)) { + throw new CustomExceptions.FileReadError(ErrorConstant.LOAD_GYM_TYPE_ERROR); + } + + // Check if the number of station is blank + if (numOfStationStr.isBlank()) { + throw new CustomExceptions.FileReadError(ErrorConstant.LOAD_NUMBER_OF_STATION_ERROR); + } + + // Check if the date is correct + if (date.isBlank()) { + throw new CustomExceptions.FileReadError(ErrorConstant.INVALID_DATE_ERROR); + } + + // if the date is not NA, then validate and make sure it is correct + try { + if (!date.equals(ErrorConstant.NO_DATE_SPECIFIED_ERROR)) { + validation.validateDateInput(date); + } + } catch (CustomExceptions.InvalidInput e) { + throw new CustomExceptions.FileReadError(ErrorConstant.INVALID_DATE_ERROR); + } + + return gymDetails; + } + + //@@author JustinSoh + /** + * Adds a station to the gym object based of the file input. + * This method is used in the parseGymFileInput method. + * How the method works is that it will check if the station details are valid + * and then add the station to the gym. + * + * @param gym The gym object that the station will be added to. + * @param gymDetails The array of strings containing the gym details. + * @param baseCounter The base counter to start adding the station. + * @return The new base counter after adding the station. + * @throws CustomExceptions.InvalidInput If the input is invalid. + */ + private int addStationFromFile(Gym gym, String[] gymDetails, int baseCounter) + throws CustomExceptions.InvalidInput, CustomExceptions.InsufficientInput { + String currentStationName; + String numberOfSetsStr; + String repsStr; + String weightStrings; + + try { + currentStationName = gymDetails[baseCounter]; + numberOfSetsStr = gymDetails[baseCounter + WorkoutConstant.SETS_OFFSET]; + repsStr = gymDetails[baseCounter + WorkoutConstant.REPS_OFFSET]; + weightStrings = gymDetails[baseCounter + WorkoutConstant.WEIGHTS_OFFSET]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new CustomExceptions.InvalidInput(ErrorConstant.LOAD_GYM_FORMAT_ERROR); + } + gym.addStation(currentStationName, numberOfSetsStr, repsStr, weightStrings); + baseCounter += WorkoutConstant.INCREMENT_OFFSET; + return baseCounter; + } + + //@@author rouvinerh + + /** + * Counts the number of '/' characters there are in a given string. + * + * @param input The user input string. + * @return An integer representing the number of '/' characters there are. + */ + private int countForwardSlash(String input) { + int count = 0; + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) == '/') { + count++; + } + } + return count; + } + +} diff --git a/src/main/java/utility/Validation.java b/src/main/java/utility/Validation.java new file mode 100644 index 0000000000..e208583393 --- /dev/null +++ b/src/main/java/utility/Validation.java @@ -0,0 +1,459 @@ +package utility; + +import constants.ErrorConstant; +import constants.HealthConstant; +import constants.UiConstant; +import constants.WorkoutConstant; +import health.Bmi; +import health.HealthList; +import ui.Output; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.util.Objects; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Represents the validation class used to validate all inputs for PulsePilot. + */ +public class Validation { + + //@@author JustinSoh + public Validation(){ + + } + + //@@author rouvinerh + /** + * Validates that the input date string is correctly formatted in DD-MM-YYYY and is a valid date. + * + * @param date The string date from user input. + * @throws CustomExceptions.InvalidInput If the date is invalid. + */ + public void validateDateInput(String date) throws CustomExceptions.InvalidInput { + if (!date.matches(UiConstant.VALID_DATE_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_DATE_ERROR); + } + String[] parts = date.split(UiConstant.DASH); + int day = Integer.parseInt(parts[0]); + int month = Integer.parseInt(parts[1]); + int year = Integer.parseInt(parts[2]); + + boolean isLeapYear = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + if (month == 2 && day == 29 && !isLeapYear) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_LEAP_YEAR_ERROR); + } + + if (year < 1967) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_YEAR_ERROR); + } + + try { + LocalDate check = LocalDate.of(year, month, day); + } catch (DateTimeException e) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_DATE_ERROR); + } + } + + /** + * Validates the delete input details. + * + * @param deleteDetails An array containing the details for the delete command. + * @throws CustomExceptions.InvalidInput If the details specified are invalid. + * @throws CustomExceptions.InsufficientInput If empty strings are found. + */ + public void validateDeleteInput(String[] deleteDetails) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + if (isEmptyParameterPresent(deleteDetails)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_DELETE_PARAMETERS_ERROR); + } + validateDeleteAndLatestFilter(deleteDetails[UiConstant.DELETE_ITEM_STRING_INDEX].toLowerCase()); + if (!validateIntegerIsPositive(deleteDetails[UiConstant.DELETE_ITEM_NUMBER_INDEX])) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_INDEX_ERROR); + } + } + + //@@author L5-Z + /** + * Validates whether the filter string is either 'run', 'gym', 'workouts', 'bmi', 'period' or 'appointment'. + * + * @param filter The filter string to be checked. + * @throws CustomExceptions.InvalidInput If the filter string is none of them. + */ + public void validateHistoryFilter(String filter) throws CustomExceptions.InvalidInput { + if (filter.equals(WorkoutConstant.RUN) + || filter.equals(WorkoutConstant.GYM) + || filter.equals(HealthConstant.BMI) + || filter.equals(HealthConstant.PERIOD) + || filter.equals(HealthConstant.APPOINTMENT) + || filter.equals(WorkoutConstant.ALL)) { + return; + } + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_HISTORY_FILTER_ERROR); + } + + //@@author L5-Z + /** + * Validates whether the filter string is either 'run', 'gym', 'bmi', 'period' or 'appointment'. + * + * @param filter The filter string to be checked. + * @throws CustomExceptions.InvalidInput If the filter string is none of them. + */ + public void validateDeleteAndLatestFilter(String filter) throws CustomExceptions.InvalidInput { + if (filter.equals(WorkoutConstant.RUN) + || filter.equals(WorkoutConstant.GYM) + || filter.equals(HealthConstant.BMI) + || filter.equals(HealthConstant.PERIOD) + || filter.equals(HealthConstant.APPOINTMENT)) { + return; + } + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER); + } + + //@@author j013n3 + /** + * Validates the BMI details entered. + * + * @param bmiDetails An array of strings with split BMI details. + * @throws CustomExceptions.InvalidInput If there are any errors in the details entered. + * @throws CustomExceptions.InsufficientInput If there are empty parameters specified. + */ + public void validateBmiInput(String[] bmiDetails) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + if (isEmptyParameterPresent(bmiDetails)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_BMI_PARAMETERS_ERROR); + } + + if (!bmiDetails[HealthConstant.BMI_HEIGHT_INDEX].matches(UiConstant.VALID_TWO_DP_NUMBER_REGEX) + || !bmiDetails[HealthConstant.BMI_WEIGHT_INDEX].matches(UiConstant.VALID_TWO_DP_NUMBER_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR); + } + + double height = Double.parseDouble(bmiDetails[HealthConstant.BMI_HEIGHT_INDEX]); + double weight = Double.parseDouble(bmiDetails[HealthConstant.BMI_WEIGHT_INDEX]); + if (height <= HealthConstant.MIN_HEIGHT || weight <= HealthConstant.MIN_WEIGHT) { + throw new CustomExceptions.InvalidInput(ErrorConstant.ZERO_HEIGHT_AND_WEIGHT_ERROR); + } + if (height > HealthConstant.MAX_HEIGHT) { + throw new CustomExceptions.InvalidInput(ErrorConstant.MAX_HEIGHT_ERROR); + } + if (weight > HealthConstant.MAX_WEIGHT) { + throw new CustomExceptions.InvalidInput(ErrorConstant.MAX_WEIGHT_ERROR); + } + + validateDateInput(bmiDetails[HealthConstant.BMI_DATE_INDEX]); + validateDateNotAfterToday(bmiDetails[HealthConstant.BMI_DATE_INDEX]); + validateDateNotPresent(bmiDetails[HealthConstant.BMI_DATE_INDEX]); + } + + /** + * Validates the period details entered. + * + * @param periodDetails An array of strings with split period details. + * @param isParser A boolean indicating whether the input comes from Parser. + * @throws CustomExceptions.InvalidInput If there are any errors in the details entered. + * @throws CustomExceptions.InsufficientInput If there are empty parameters specified. + */ + public void validatePeriodInput(String[] periodDetails, boolean isParser) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + if (isEmptyParameterPresent(periodDetails)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_PERIOD_PARAMETERS_ERROR); + } + try { + validateDateInput(periodDetails[HealthConstant.PERIOD_START_DATE_INDEX]); + } catch (CustomExceptions.InvalidInput e) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_START_DATE_ERROR + e.getMessage()); + } + try { + if (validateDateNotEmpty(periodDetails[HealthConstant.PERIOD_END_DATE_INDEX])) { + validateDateInput(periodDetails[HealthConstant.PERIOD_END_DATE_INDEX]); + } + } catch (CustomExceptions.InvalidInput e) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_END_DATE_ERROR + e.getMessage()); + } + + validateIfOnlyFromParser(isParser, periodDetails); + validateDateNotAfterToday(periodDetails[HealthConstant.PERIOD_START_DATE_INDEX]); + Parser parser = new Parser(); + LocalDate startDate = parser.parseDate(periodDetails[HealthConstant.PERIOD_START_DATE_INDEX]); + if (validateDateNotEmpty(periodDetails[HealthConstant.PERIOD_END_DATE_INDEX])) { + validateDateNotAfterToday(periodDetails[HealthConstant.PERIOD_END_DATE_INDEX]); + LocalDate endDate = parser.parseDate(periodDetails[HealthConstant.PERIOD_END_DATE_INDEX]); + if (startDate.isAfter(endDate)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.PERIOD_END_BEFORE_START_ERROR); + } + } + } + + //@@author rouvinerh + /** + * Validates the run details entered. + * + * @param runDetails An array of strings with split run details. + * @throws CustomExceptions.InvalidInput If the details are wrongly formatted, or if date is in future or invalid. + * @throws CustomExceptions.InsufficientInput If empty strings are used. + */ + public void validateRunInput(String[] runDetails) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + if (isEmptyParameterPresent(runDetails)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_RUN_PARAMETERS_ERROR); + } + + if (!runDetails[WorkoutConstant.RUN_TIME_INDEX].matches(UiConstant.VALID_TIME_REGEX) && + !runDetails[WorkoutConstant.RUN_TIME_INDEX].matches(UiConstant.VALID_TIME_WITH_HOURS_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_RUN_TIME_ERROR); + } + + if (!runDetails[WorkoutConstant.RUN_DISTANCE_INDEX].matches(UiConstant.VALID_TWO_DP_NUMBER_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_RUN_DISTANCE_ERROR); + } + + if (validateDateNotEmpty(runDetails[WorkoutConstant.RUN_DATE_INDEX])) { + validateDateInput(runDetails[WorkoutConstant.RUN_DATE_INDEX]); + validateDateNotAfterToday(runDetails[WorkoutConstant.RUN_DATE_INDEX]); + } + } + + //@@author JustinSoh + /** + * Validates the gym details entered. + * + * @param gymDetails An array of strings with split Gym details. + * @throws CustomExceptions.InvalidInput If the details specified are invalid. + * @throws CustomExceptions.InsufficientInput If empty strings are used. + */ + public void validateGymInput(String[] gymDetails) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + if (isEmptyParameterPresent(gymDetails)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INSUFFICIENT_GYM_PARAMETERS_ERROR); + } + if (!validateIntegerIsPositive(gymDetails[WorkoutConstant.GYM_NUMBER_OF_STATIONS_INDEX])) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_NUMBER_OF_STATIONS_ERROR); + } + + int numberOfStations = Integer.parseInt(gymDetails[WorkoutConstant.GYM_NUMBER_OF_STATIONS_INDEX]); + if (numberOfStations > WorkoutConstant.MAX_GYM_STATION_NUMBER) { + throw new CustomExceptions.InvalidInput(ErrorConstant.MAX_STATIONS_ERROR); + } + + if (validateDateNotEmpty(gymDetails[WorkoutConstant.GYM_DATE_INDEX])) { + validateDateInput(gymDetails[WorkoutConstant.GYM_DATE_INDEX]); + validateDateNotAfterToday(gymDetails[WorkoutConstant.GYM_DATE_INDEX]); + } + } + + //@@author syj02 + /** + * Validates the appointment details entered. + * + * @param appointmentDetails An array of strings with split appointment details. + * @throws CustomExceptions.InvalidInput If there are any errors in the details entered. + * @throws CustomExceptions.InsufficientInput If date, time, or description parameters are empty or invalid. + */ + public void validateAppointmentDetails(String[] appointmentDetails) + throws CustomExceptions.InvalidInput, CustomExceptions.InsufficientInput { + if (isEmptyParameterPresent(appointmentDetails)) { + throw new CustomExceptions.InsufficientInput(ErrorConstant + .INSUFFICIENT_APPOINTMENT_PARAMETERS_ERROR); + } + validateDateInput(appointmentDetails[HealthConstant.APPOINTMENT_DATE_INDEX]); + validateTimeInput(appointmentDetails[HealthConstant.APPOINTMENT_TIME_INDEX]); + + if (appointmentDetails[HealthConstant.APPOINTMENT_DESCRIPTION_INDEX].length() + > HealthConstant.MAX_DESCRIPTION_LENGTH) { + throw new CustomExceptions.InvalidInput(ErrorConstant.DESCRIPTION_LENGTH_ERROR); + } + if (!appointmentDetails[HealthConstant.APPOINTMENT_DESCRIPTION_INDEX] + .matches(UiConstant.VALID_APPOINTMENT_DESCRIPTION_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_DESCRIPTION_ERROR); + } + } + + //@@author rouvinerh + /** + * Checks whether the username has only alphanumeric characters and spaces. + * + * @param name The input name from the user + * @return Returns true if it only has alphanumeric characters, otherwise returns false. + */ + public boolean validateIfUsernameIsValid(String name) { + return !name.matches(UiConstant.VALID_USERNAME_REGEX); + } + + /** + * Checks whether date is set to null or 'NA'. Both cases mean date is not specified. + * + * @param date The date string to check. + * @return Returns true if date is specified, otherwise returns false. + */ + public boolean validateDateNotEmpty (String date) { + return date != null && !date.equals("NA"); + } + + //@@author j013n3 + /** + * Validates whether the start date is before or equal to the end date of the latest period in the HealthList. + * Throws an error if it is. + * + * @param dateString The string representation of the date to be validated. + * @param latestPeriodEndDate The end date of the latest period in the HealthList. + * @throws CustomExceptions.InvalidInput If the date specified is not after the end date of the latest period. + */ + public void validateDateAfterLatestPeriodInput(String dateString, LocalDate latestPeriodEndDate) + throws CustomExceptions.InvalidInput { + Parser parser = new Parser(); + LocalDate date = parser.parseDate(dateString); + + if (latestPeriodEndDate != null && (date.isBefore(latestPeriodEndDate) || date.isEqual(latestPeriodEndDate))) { + throw new CustomExceptions.InvalidInput(ErrorConstant.CURRENT_START_BEFORE_PREVIOUS_END); + } + } + + /** + * Validates whether the specified start date matches the start date of the latest period in the HealthList + * and checks if end date exists. + * + * @param latestPeriodEndDate The end date of the latest period in the HealthList. + * @param periodDetails An array containing details of the current period input. + * @throws CustomExceptions.InvalidInput If the start date does not match the start date of the latest period + * or if insufficient parameters are provided. + */ + public void validateStartDatesTally(LocalDate latestPeriodEndDate, String[] periodDetails) + throws CustomExceptions.InvalidInput { + Parser parser = new Parser(); + LocalDate startDate = parser.parseDate(periodDetails[HealthConstant.PERIOD_START_DATE_INDEX]); + LocalDate latestPeriodStartDate = + Objects.requireNonNull(HealthList.getPeriod(HealthConstant.FIRST_ITEM)).getStartDate(); + + if (latestPeriodEndDate == null) { + if (!startDate.equals(latestPeriodStartDate)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_START_DATE_INPUT_ERROR); + } + if (periodDetails[HealthConstant.PERIOD_END_DATE_INDEX] == null) { + throw new CustomExceptions.InvalidInput(ErrorConstant.END_DATE_NOT_FOUND_ERROR ); + } + } + } + + /** + * Validates input data if it comes from Parser and validates the input using two other methods from Validation. + * + * @param isParser a boolean indicating whether the input comes from Parser + * @param periodDetails an array of strings containing period details + * @throws CustomExceptions.InvalidInput if the input data is invalid + */ + public void validateIfOnlyFromParser(boolean isParser, String[] periodDetails) + throws CustomExceptions.InvalidInput { + int sizeOfPeriodList = HealthList.getPeriodsSize(); + if (isParser && sizeOfPeriodList >= UiConstant.MINIMUM_PERIOD_COUNT) { + LocalDate latestPeriodEndDate = + Objects.requireNonNull(HealthList.getPeriod(HealthConstant.FIRST_ITEM)).getEndDate(); + validateStartDatesTally(latestPeriodEndDate, periodDetails); + validateDateAfterLatestPeriodInput(periodDetails[HealthConstant.PERIOD_START_DATE_INDEX], + latestPeriodEndDate); + } + } + + //@@author rouvinerh + /** + * Checks whether current directory is readable and writable. If no, print exception and exit bot. + * If yes, do nothing. + */ + public void validateDirectoryPermissions() { + Path currentDirectory = Path.of(""); + boolean isValidPermissions = Files.isReadable(currentDirectory) && Files.isWritable(currentDirectory); + if (!isValidPermissions) { + Output output = new Output(); + output.printException(ErrorConstant.NO_PERMISSIONS_ERROR); + System.exit(1); + } + } + + //@@author j013n3 + /** + * Validates whether the specified date can be found in HealthList and throws error if it is. + * + * @param dateString The date of the Bmi input to be added. + * @throws CustomExceptions.InvalidInput If the same date is found. + */ + public void validateDateNotPresent(String dateString) throws CustomExceptions.InvalidInput { + Parser parser = new Parser(); + LocalDate dateToVerify = parser.parseDate(dateString); + for (Bmi bmi : HealthList.getBmis()) { + if (bmi.getDate().isEqual(dateToVerify)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.DATE_ALREADY_EXISTS_ERROR); + } + } + } + + //@@author JustinSoh + /** + * Validates whether the current index provided is within the start and end. + * + * @param index The index to be validated. + * @param start The starting bound. + * @param end the ending bound (exclusive - e.g. end = 5 means index must be less than 5). + * @return true if the index is within the bounds, false otherwise. + */ + public static boolean validateIndexWithinBounds(int index, int start, int end) { + return index >= start && index < end; + } + + public static boolean validateIntegerIsPositive(String value) { + return value.matches(UiConstant.VALID_POSITIVE_INTEGER_REGEX); + } + + //@@author rouvinerh + /** + * Validates that time is in HH:MM 24 hours format, and if it is a valid time. + * + * @param time The String time to check. + * @throws CustomExceptions.InvalidInput If time is formatted wrongly or is not valid. + */ + protected void validateTimeInput(String time) throws CustomExceptions.InvalidInput { + if (!time.matches(UiConstant.VALID_TIME_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_ACTUAL_TIME_ERROR); + } + String [] parts = time.split(UiConstant.SPLIT_BY_COLON); + int hours = Integer.parseInt(parts[UiConstant.SPLIT_TIME_HOUR_INDEX]); + int minutes = Integer.parseInt(parts[UiConstant.SPLIT_TIME_MINUTES_INDEX]); + + if (hours < UiConstant.MIN_HOURS || hours > UiConstant.MAX_HOURS) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_ACTUAL_TIME_HOUR_ERROR); + } + if (minutes < UiConstant.MIN_MINUTES || minutes > UiConstant.MAX_MINUTES) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_ACTUAL_TIME_MINUTE_ERROR); + } + } + + //@@author rouvinerh + /** + * Checks whether the list of input details contains any empty strings. + * + * @param input A list of strings representing command inputs. + * @return False if it contains empty strings. Otherwise, returns true. + */ + protected boolean isEmptyParameterPresent(String[] input) { + for (String s : input) { + if (s != null && s.isEmpty()) { + return true; + } + } + return false; + } + + /** + * Validates whether the date specified is after today. Throws an error if it is. + * + * @param dateString A string representing the date. + * @throws CustomExceptions.InvalidInput If the date specified is after today. + */ + protected void validateDateNotAfterToday(String dateString) throws CustomExceptions.InvalidInput { + Parser parser = new Parser(); + LocalDate date = parser.parseDate(dateString); + if (date.isAfter(LocalDate.now())) { + throw new CustomExceptions.InvalidInput(ErrorConstant.DATE_IN_FUTURE_ERROR); + } + } +} diff --git a/src/main/java/workouts/Gym.java b/src/main/java/workouts/Gym.java new file mode 100644 index 0000000000..cf05b76f97 --- /dev/null +++ b/src/main/java/workouts/Gym.java @@ -0,0 +1,148 @@ +package workouts; + +import constants.ErrorConstant; +import storage.LogFile; +import utility.CustomExceptions; +import constants.UiConstant; +import constants.WorkoutConstant; +import utility.Validation; + +import java.util.ArrayList; + +/** + * Represents a Gym object that extends the Workout class. + * A gym object can have multiple GymStation objects. + * + */ +public class Gym extends Workout { + //@@author JustinSoh + + private final ArrayList stations = new ArrayList<>(); + + /** + * Constructs a new Gym. + * When a new Gym object is created, it is automatically added to a list of workouts. + */ + public Gym() { + super.addIntoWorkoutList(this); + } + + /** + * Overloaded constructor that takes the optional date parameter. + * + * @param stringDate String representing the date parameter specified. + */ + public Gym(String stringDate) { + super(stringDate); + super.addIntoWorkoutList(this); + } + + /** + * Adds a new GymStation object into the Gym object. + * + * @param name String containing the name of the gym station. + * @param numberOfSet String of the number of sets done. + * @param numberOfRepetitions String of the number of repetitions done. + * @param weights String of weights separated by commas. (e.g. "10,20,30,40") + * @throws CustomExceptions.InsufficientInput If any of the input fields are empty. + * @throws CustomExceptions.InvalidInput If the input fields are invalid. + */ + public void addStation(String name, + String numberOfSet, + String numberOfRepetitions, + String weights) + throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + + GymStation newStation = new GymStation(name, numberOfSet, numberOfRepetitions, weights); + appendIntoStations(newStation); + LogFile.writeLog("Added Gym Station: " + name, false); + } + + /** + * Gets the list of GymStation objects. + * + * @return An ArrayList of GymStation objects. + */ + public ArrayList getStations() { + return stations; + } + + /** + * Retrieves the GymStation object by index. + * + * @param index Index of the GymStation object. + * @return GymStation object. + * @throws CustomExceptions.OutOfBounds If the index is out of bounds. + */ + public GymStation getStationByIndex(int index) throws CustomExceptions.OutOfBounds { + boolean isIndexValid = Validation.validateIndexWithinBounds(index, 0, stations.size()); + if (!isIndexValid) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.INVALID_INDEX_SEARCH_ERROR); + } + return stations.get(index); + } + + /** + * Retrieves the string representation of a Gym object. + * + * @return A formatted string representing the Gym object, inclusive of the date and gym stations done. + */ + + @Override + public String toString() { + return String.format(" (Date: %s)", super.getDate()); + } + + /** + * Converts the Gym object into a string format suitable for writing into a file. + * For more examples, refer to the GymTest method toFileString_correctInput_expectedCorrectString(). + * + * @return A string representing the Gym object and its GymStation objects unsuitable for writing into a file. + */ + public String toFileString(){ + StringBuilder formattedString = new StringBuilder(); + + // Append the type, number of stations, and date (GYM:NUM_STATIONS:DATE:) + formattedString.append(WorkoutConstant.GYM.toUpperCase()) + .append(UiConstant.SPLIT_BY_COLON) + .append(stations.size()) + .append(UiConstant.SPLIT_BY_COLON) + .append(super.getDateForFile()) + .append(UiConstant.SPLIT_BY_COLON); + + int lastIndex = stations.size() - 1; + for (int i = 0; i < stations.size(); i++) { + formattedString.append(stations.get(i).toFileString()); + if (i != lastIndex) { + formattedString.append(UiConstant.SPLIT_BY_COLON); + } + } + return formattedString.toString(); + } + + /** + * Used when printing all the workouts. This method takes in parameters index. + * + * @param index indicates which particular gymStation is being queried. + * @return A string representing the history format for gym. + */ + public String getHistoryFormatForSpecificGymStation(int index) { + + // Get the string format for a specific gym station + GymStation station = getStations().get(index); + String gymStationString = station.getStationName(); + String gymSetString = String.valueOf(station.getNumberOfSets()); + + // If it is first iteration, includes dashes for irrelevant field + String prefix = index == 0 ? WorkoutConstant.GYM : UiConstant.EMPTY_STRING; + String date = index == 0 ? super.getDate() : UiConstant.EMPTY_STRING; + + return String.format(WorkoutConstant.HISTORY_WORKOUTS_DATA_FORMAT, + prefix, date, gymStationString, gymSetString, UiConstant.DASH); + } + + private void appendIntoStations(GymStation station) { + stations.add(station); + } +} diff --git a/src/main/java/workouts/GymSet.java b/src/main/java/workouts/GymSet.java new file mode 100644 index 0000000000..2e951b6848 --- /dev/null +++ b/src/main/java/workouts/GymSet.java @@ -0,0 +1,42 @@ +package workouts; + +import constants.WorkoutConstant; + +/** + * Represents a GymSet object. + */ +public class GymSet { + //@@author JustinSoh + private final double weight; + private final int numberOfRepetitions; + + /** + * Constructs a new GymSet object using the weight and reps. + * + * @param weight The weight done for the set. + * @param numberOfRepetitions The number of reps done for the set. + */ + public GymSet(Double weight, int numberOfRepetitions){ + this.weight = weight; + this.numberOfRepetitions = numberOfRepetitions; + } + + public double getWeight() { + return weight; + } + + public int getNumberOfRepetitions() { + return numberOfRepetitions; + } + + /** + * Retrieves a string representation of a GymSet object. + * + * @return A formatted string representing a GymSet, inclusive of the number of repetitions and weight done. + */ + @Override + public String toString() { + return String.format(WorkoutConstant.GYM_SET_FORMAT, this.numberOfRepetitions, this.weight); + } +} + diff --git a/src/main/java/workouts/GymStation.java b/src/main/java/workouts/GymStation.java new file mode 100644 index 0000000000..d3f2fb8fad --- /dev/null +++ b/src/main/java/workouts/GymStation.java @@ -0,0 +1,355 @@ +package workouts; + +import constants.ErrorConstant; +import constants.UiConstant; +import constants.WorkoutConstant; +import utility.CustomExceptions; +import utility.Validation; + +import java.util.ArrayList; + +/** + * Represents a GymStation object. + */ +public class GymStation { + //@@author JustinSoh + private final String stationName; + private final ArrayList sets = new ArrayList<>(); + private final int numberOfSets; + + /** + * Constructs a new GymStation object that contains the name, weight, number of repetitions and number of sets done + * in one station. + * + * @param exerciseName The name of the gym station. + * The name should not be empty and should not exceed the maximum length. + * The name should also follow the pattern (UiConstant.VALID_GYM_STATION_NAME_REGEX) + * @param numberOfSetsStr The number of sets done. + * The number of sets should be a positive integer. + * The number of sets should not be empty. + * The number of sets should match the number of weights provided in the weights string + * @param numberOfRepetitions The number of repetitions done for each set. + * @param weightsString The weights done for each set. + * The weights should be in the format "weight1,weight2,weight3..." + * @throws CustomExceptions.InvalidInput If an invalid input is passed in. + * @throws CustomExceptions.InsufficientInput If input is empty. + */ + public GymStation(String exerciseName, String numberOfSetsStr, String numberOfRepetitions, String weightsString) + throws CustomExceptions.InsufficientInput, CustomExceptions.InvalidInput { + + // Check input validity + this.stationName = validateGymStationName(exerciseName); + this.numberOfSets = validateNumberOfSets(numberOfSetsStr); + int validNumberOfReps = validateNumberOfRepetitions(numberOfRepetitions); + ArrayList validWeights = processWeightsArray(weightsString); + + // Verify if the number of weights matches the number of sets + checkIfNumberOfWeightsMatchesSets(validWeights, this.numberOfSets); + + // Process the sets and weights and add them into the gym set object + processSets(validWeights, validNumberOfReps); + } + + /** + * Retrieves the station name for the GymStation object. + * + * @return String representing the name for the station. + */ + public String getStationName() { + return stationName; + } + + /** + * Retrieves an ArrayList of gym sets for the GymStation object. + * + * @return The ArrayList of GymSet objects. + */ + public ArrayList getSets() { + return sets; + } + + /** + * Retrieves the number sets within the GymStation. + * + * @return The number of sets done. + */ + public int getNumberOfSets() { + return numberOfSets; + } + + /** + * Retrieves the string representation of a GymStation object. + * + * @return A formatted string representing a GymStation object. + */ + @Override + public String toString() { + StringBuilder returnString = new StringBuilder(String.format(WorkoutConstant.GYM_STATION_FORMAT, + this.getStationName()) + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, + this.getNumberOfSets())); + + for (int i = 0; i < this.getNumberOfSets(); i++) { + returnString.append(System.lineSeparator()); + returnString.append(String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, i+1 , + this.getSets().get(i).toString())); + } + return returnString.toString(); + } + + // Protected Functions + /** + * Retrieves the string representation of a GymStation object for writing into a file. + * Formats the string in the following format + * "[Exercise Name]:[Number of Sets]:[Repetitions]:[Weights1, Weight2,Weight3 ...]" + * + * @return A formatted string representing a GymStation object with the format above. + */ + protected String toFileString(){ + StringBuilder fileString = new StringBuilder(); + String stationName = getStationName(); + String numOfSets = String.valueOf(getNumberOfSets()); + String gymRepString = toRepString().split(UiConstant.SPLIT_BY_COMMAS)[0]; + String gymWeightString = toWeightString(); + fileString.append(stationName); + fileString.append(UiConstant.SPLIT_BY_COLON); + fileString.append(numOfSets); + fileString.append(UiConstant.SPLIT_BY_COLON); + fileString.append(gymRepString); + fileString.append(UiConstant.SPLIT_BY_COLON); + fileString.append(gymWeightString); + return fileString.toString(); + } + + /** + * Validates the weight string such that it only has numbers. + * + * @param weightsString The string representing the weights in the format "weight1,weight2,weight3..." + * @return ArrayList of integers representing the weights in the format [weight1, weight2, weight3 ...] + * @throws CustomExceptions.InvalidInput If an invalid weights string is passed in. + */ + protected ArrayList processWeightsArray(String weightsString) + throws CustomExceptions.InvalidInput { + validateWeightString(weightsString); + String[] weightsArray = weightsString.split(UiConstant.SPLIT_BY_COMMAS); + ArrayList validatedWeightsArray = new ArrayList<>(); + + for (String weight: weightsArray){ + boolean isValidWeight = validateWeight(weight); + if (isValidWeight){ + validatedWeightsArray.add(Double.parseDouble(weight)); + } + } + return validatedWeightsArray; + } + + /** + * Validates the gym station name ensuring that + * - it is not empty + * - follows the correct pattern (UiConstant.VALID_GYM_STATION_NAME_REGEX) + * - does not exceed the maximum length. (WorkoutConstant.MAX_GYM_STATION_NAME_LENGTH) + * + * @param exerciseName The string representing the gym station name + * @return String representing the gym station name + * @throws CustomExceptions.InvalidInput if an invalid gym station name is passed in + * @throws CustomExceptions.InsufficientInput if an empty gym station name is passed in + */ + protected String validateGymStationName(String exerciseName) throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + validateExerciseNameNotEmpty(exerciseName); + validateExerciseNamePattern(exerciseName); + validateExerciseNameLength(exerciseName); + return exerciseName; + } + + /** + * Validates the number of sets ensuring that it is a positive integer. + * + * @param numberOfSets The string representing the number of sets + * @return int representing the number of sets + * @throws CustomExceptions.InvalidInput if an invalid number of sets is passed in + */ + protected int validateNumberOfSets(String numberOfSets) throws CustomExceptions.InvalidInput { + boolean isSetsValid = Validation.validateIntegerIsPositive(numberOfSets); + if (!isSetsValid) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_SETS_POSITIVE_DIGIT_ERROR); + } + + return Integer.parseInt(numberOfSets); + } + + // Private Methods + /** + * Retrieves the string representation of a GymStation object with commas. + * E.g. toRepString(",") returns "1,2,3" + * + * @return A formatted string representing a GymStation object with the specified delimiter. + */ + private String toRepString() { + StringBuilder repString = new StringBuilder(); + for (int i = 0; i < sets.size(); i++) { + String currentRep = String.valueOf(sets.get(i).getNumberOfRepetitions()); + repString.append(currentRep); + if (i != sets.size() - 1) { + repString.append(UiConstant.SPLIT_BY_COMMAS); + } + } + return repString.toString(); + } + + /** + * Retrieves the string representation of a GymStation object with commas. + * E.g. toWeightString(",") returns "10,20,30" + * + * @return A formatted string representing a GymStation object with the specified delimiter. + */ + private String toWeightString(){ + StringBuilder weightString = new StringBuilder(); + for (int i = 0; i < sets.size(); i++) { + String currentRep = String.valueOf(sets.get(i).getWeight()); + weightString.append(currentRep); + if (i != sets.size() - 1) { + weightString.append(UiConstant.SPLIT_BY_COMMAS); + } + } + return weightString.toString(); + } + + /** + * Validates the number of repetitions ensuring that it is a positive integer. + * + * @param numberOfRepetitions The string representing the number of repetitions + * @return int representing the number of repetitions + * @throws CustomExceptions.InvalidInput if an invalid number of repetitions is passed in + */ + private int validateNumberOfRepetitions(String numberOfRepetitions) throws CustomExceptions.InvalidInput { + boolean isRepsValid = Validation.validateIntegerIsPositive(numberOfRepetitions); + if (!isRepsValid) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_REPS_POSITIVE_DIGIT_ERROR); + } + return Integer.parseInt(numberOfRepetitions); + } + + /** + * Function which adds a GymSet object to GymStation. + * + * @param weightsList The weight done for the particular set. + * @param numberOfRepetitions The number of repetitions done for the particular set. + */ + private void processSets(ArrayList weightsList, int numberOfRepetitions) { + for (int i = 0; i < numberOfSets; i++) { + GymSet newSet = new GymSet(weightsList.get(i), numberOfRepetitions); + sets.add(newSet); + } + } + + /** + * Validates the weight string ensuring that + * - The weight does not exceed the maximum weight (@code WorkoutConstant.MAX_GYM_WEIGHT) + * - The weight is a multiple of 0.125 (as that is the increment of weights in a gym) + * + * @param weight The string representing the weight + * @return boolean true if the weight is valid + * @throws CustomExceptions.InvalidInput if an invalid weight is passed in + */ + private boolean validateWeight(String weight) throws CustomExceptions.InvalidInput { + double weightDouble = Double.parseDouble(weight); + validateWeightDoesNotExceedMax(weightDouble); + validateWeightIsMultiple(weightDouble); + return true; + } + + + /** + * Validates the exercise name ensuring that it is not empty. + * + * @param exerciseName The name of the exercise. + * @throws CustomExceptions.InsufficientInput if the exercise name is empty. + */ + private void validateExerciseNameNotEmpty(String exerciseName) throws CustomExceptions.InsufficientInput { + if (exerciseName.isEmpty()) { + throw new CustomExceptions.InsufficientInput(ErrorConstant.INVALID_GYM_STATION_EMPTY_NAME_ERROR); + } + } + + /** + * Validates the exercise name pattern ensuring that it does not contain any special characters. + * + * @param exerciseName The name of the exercise. + * @throws CustomExceptions.InvalidInput if the exercise name does not match the pattern. + */ + private void validateExerciseNamePattern(String exerciseName) throws CustomExceptions.InvalidInput { + if (!exerciseName.matches(UiConstant.VALID_GYM_STATION_NAME_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_GYM_STATION_NAME_ERROR); + } + } + + /** + * Validates the length of the exercise name. + * + * @param exerciseName The name of the exercise. + * @throws CustomExceptions.InvalidInput if the exercise name exceeds the maximum length. + */ + private void validateExerciseNameLength(String exerciseName) throws CustomExceptions.InvalidInput { + if (exerciseName.length() > WorkoutConstant.MAX_GYM_STATION_NAME_LENGTH) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_GYM_STATION_NAME_ERROR); + } + } + + /** + * Validates that the weight does not exceed the maximum weight. + * + * @param weight The weight to be validated. + * @throws CustomExceptions.InvalidInput if the weight exceeds the maximum weight. + */ + private void validateWeightDoesNotExceedMax(double weight) throws CustomExceptions.InvalidInput { + if (weight > WorkoutConstant.MAX_GYM_WEIGHT) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_WEIGHT_MAX_ERROR); + } + } + + /** + * Validates that the weight is a multiple of 0.125. + * + * @param weight The weight to be validated. + * @throws CustomExceptions.InvalidInput if the weight is not a multiple of 0.125. + */ + private void validateWeightIsMultiple(double weight) throws CustomExceptions.InvalidInput { + if (weight % WorkoutConstant.WEIGHT_MULTIPLE != 0 ){ + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_WEIGHTS_VALUE_ERROR); + } + } + + /** + * Checks if the number of weights matches the number of sets. + * + * @param weights The list of weights. + * @param numberOfSets The number of sets. + * @throws CustomExceptions.InvalidInput if the number of weights does not match the number of sets. + */ + private void checkIfNumberOfWeightsMatchesSets(ArrayList weights, int numberOfSets) + throws CustomExceptions.InvalidInput { + if (weights.size() != numberOfSets){ + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_WEIGHTS_NUMBER_ERROR); + } + } + + /** + * Validates the weight string ensuring that + * - The weight string is not empty + * - The weight string follows the correct format (UiConstant.VALID_WEIGHTS_ARRAY_REGEX) + * + * @param weightsString The string representing the weights + * @throws CustomExceptions.InvalidInput if an invalid weight string is passed in + */ + private void validateWeightString(String weightsString) throws CustomExceptions.InvalidInput { + if (weightsString.isBlank()) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_WEIGHTS_EMPTY_ERROR); + } + + if (!weightsString.matches(UiConstant.VALID_WEIGHTS_ARRAY_REGEX)) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR); + } + } +} + + diff --git a/src/main/java/workouts/Run.java b/src/main/java/workouts/Run.java new file mode 100644 index 0000000000..25a7981c48 --- /dev/null +++ b/src/main/java/workouts/Run.java @@ -0,0 +1,246 @@ +package workouts; + + +import utility.CustomExceptions; +import constants.ErrorConstant; +import constants.UiConstant; +import constants.WorkoutConstant; + +/** + * Represents a Run object that extends the Workout class. + * It takes in the time and distance of the run as input, calculates the pace of the run based on the time and distance, + * and formats the time and distance into a readable String format when printed. + */ +public class Run extends Workout { + //@@author rouvinerh + private final Integer[] times; + private final double distance; + private final String pace; + + /** + * Constructs a new Run object with the time and distance from user input. + * + * @param stringTime The time taken for the run. + * @param stringDistance The distance of the run. + * @throws CustomExceptions.InvalidInput If there is invalid input in any parameters found. + */ + public Run(String stringTime, String stringDistance) throws CustomExceptions.InvalidInput { + times = processRunTime(stringTime); + distance = checkDistance(stringDistance); + pace = calculatePace(); + super.addIntoWorkoutList(this); + } + + /** + * Overloaded constructor that takes in time, distance and the optional date parameter from user input. + * + * @param stringTime The time taken for the run. + * @param stringDistance The distance of the run. + * @param stringDate The date of the run. + * @throws CustomExceptions.InvalidInput If there is invalid input in any parameters found. + */ + public Run(String stringTime, String stringDistance, String stringDate) throws CustomExceptions.InvalidInput { + super(stringDate); + times = processRunTime(stringTime); + distance = checkDistance(stringDistance); + pace = calculatePace(); + Workout workout = new Workout(); + workout.addIntoWorkoutList(this); + } + + /** + * Returns string format of time taken for run. + * If there isn't an hour present, returns only mm:ss. + * If not returns hh:mm:ss. + * + * @return Formatted string of the time for the run. + */ + public String getTimes() { + if (times[WorkoutConstant.RUN_TIME_HOUR_INDEX] > UiConstant.MIN_HOURS) { + int hours = times[WorkoutConstant.RUN_TIME_HOUR_INDEX]; + int minutes = times[WorkoutConstant.RUN_TIME_MINUTE_INDEX]; + int seconds = times[WorkoutConstant.RUN_TIME_SECOND_INDEX]; + return String.format(WorkoutConstant.TIME_WITH_HOURS_FORMAT, hours, minutes, seconds); + + } else { + int minutes = times[WorkoutConstant.RUN_TIME_MINUTE_INDEX]; + int seconds = times[WorkoutConstant.RUN_TIME_SECOND_INDEX]; + return String.format(WorkoutConstant.TIME_WITHOUT_HOURS_FORMAT, minutes, seconds); + } + } + + /** + * Retrieves the run distance in two decimal place format. + * + * @return Run distance as String. + */ + public String getDistance() { + return String.format(WorkoutConstant.TWO_DECIMAL_PLACE_FORMAT, distance); + } + + /** + * Retrieves run pace. + * + * @return Run pace as String. + */ + public String getPace() { + return pace; + } + + //@@author JustinSoh + + /** + * Retrieves the string representation of a Run object. + * + * @return A formatted string representing a Run object. + */ + @Override + public String toString() { + String printedDate = super.getDate(); + return String.format(WorkoutConstant.RUN_DATA_FORMAT, WorkoutConstant.RUN, + getTimes(), getDistance(), getPace(), printedDate); + } + + /** + * Retrieves the string representation of a Run object when printing all history. + * Uses WorkoutConstant.HISTORY_WORKOUTS_DATA_FORMAT to format the string. + * Ensures that the format of the string is consistent when printing gym and run objects. + * + * @return a formatted string representing a Run object. + */ + public String getFormatForAllHistory() { + String printedDate = super.getDate(); + return String.format(WorkoutConstant.HISTORY_WORKOUTS_DATA_FORMAT, + WorkoutConstant.RUN, + printedDate, + getDistance(), + getTimes(), + getPace() + ); + } + + //@@author rouvinerh + /** + * Method splits and validates run time input. + * + * @param inputTime String variable representing time taken in either hh:mm:ss or mm:ss format. + * @return A list of integers representing the hours (if present), minutes and seconds. + * @throws CustomExceptions.InvalidInput if the input time is not in the correct format. + */ + protected Integer[] processRunTime(String inputTime) throws CustomExceptions.InvalidInput { + String [] parts = inputTime.split(UiConstant.SPLIT_BY_COLON); + int hours = WorkoutConstant.NO_HOURS_PRESENT; + int minutes = UiConstant.MIN_MINUTES; + int seconds = UiConstant.MIN_SECONDS; + + if (parts.length == WorkoutConstant.NUMBER_OF_PARTS_FOR_RUN_TIME) { + minutes = Integer.parseInt(parts[WorkoutConstant.RUN_TIME_NO_HOURS_MINUTE_INDEX]); + seconds = Integer.parseInt(parts[WorkoutConstant.RUN_TIME_NO_HOURS_SECOND_INDEX]); + } else if (parts.length == WorkoutConstant.NUMBER_OF_PARTS_FOR_RUN_TIME_WITH_HOURS) { + hours = Integer.parseInt(parts[WorkoutConstant.RUN_TIME_HOUR_INDEX]); + minutes = Integer.parseInt(parts[WorkoutConstant.RUN_TIME_MINUTE_INDEX]); + seconds = Integer.parseInt(parts[WorkoutConstant.RUN_TIME_SECOND_INDEX]); + } + + Integer[] runTimeParts = new Integer[]{hours, minutes, seconds}; + checkRunTimeValues(runTimeParts); + return runTimeParts; + } + + /** + * Checks the validity of distance value specified for the run. Returns the distance as a double if valid. + * + * @param stringDistance The string representation of the distance. + * @return The run distance as a Double. + * @throws CustomExceptions.InvalidInput If the distance is outside the valid range. + */ + protected Double checkDistance(String stringDistance) throws CustomExceptions.InvalidInput { + double runDistance = Double.parseDouble(stringDistance); + if (runDistance > WorkoutConstant.MAX_RUN_DISTANCE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.DISTANCE_TOO_LONG_ERROR); + } + + if (runDistance <= WorkoutConstant.MIN_RUN_DISTANCE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.ZERO_DISTANCE_ERROR); + } + assert runDistance > 0: ErrorConstant.ZERO_DISTANCE_ERROR; + return runDistance; + } + + /** + * Method calculates the pace of the run, and formats it into minutes per km. + * + * @return The pace of the run as a String. + * @throws CustomExceptions.InvalidInput If the pace calculated is too large or small. + */ + protected String calculatePace() throws CustomExceptions.InvalidInput { + int totalSeconds = calculateTotalSeconds(); + double paceInDecimal = ((double) totalSeconds / this.distance) / UiConstant.NUM_SECONDS_IN_MINUTE; + + if (paceInDecimal > WorkoutConstant.MAX_PACE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.MAX_PACE_ERROR); + } + if (paceInDecimal < WorkoutConstant.MIN_PACE) { + throw new CustomExceptions.InvalidInput(ErrorConstant.MIN_PACE_ERROR); + } + + int minutes = (int) paceInDecimal; + double remainingSeconds = paceInDecimal - minutes; + int seconds = (int) Math.round(remainingSeconds * UiConstant.NUM_SECONDS_IN_MINUTE); + assert paceInDecimal >= 1: ErrorConstant.MIN_PACE_ERROR; + + return String.format(WorkoutConstant.RUN_PACE_FORMAT, minutes, seconds); + } + + /** + * Returns the total seconds based on the times taken for the run. + * + * @return The total number of seconds in the run. + */ + private int calculateTotalSeconds() { + int totalSeconds; + + if (times[0] > 0) { + totalSeconds = this.times[WorkoutConstant.RUN_TIME_HOUR_INDEX] * UiConstant.NUM_SECONDS_IN_HOUR + + this.times[WorkoutConstant.RUN_TIME_MINUTE_INDEX] * UiConstant.NUM_SECONDS_IN_MINUTE + + this.times[WorkoutConstant.RUN_TIME_SECOND_INDEX]; + } else { + totalSeconds = this.times[WorkoutConstant.RUN_TIME_MINUTE_INDEX] * UiConstant.NUM_SECONDS_IN_MINUTE + + this.times[WorkoutConstant.RUN_TIME_SECOND_INDEX]; + } + assert totalSeconds > 0: ErrorConstant.ZERO_TIME_ERROR; + return totalSeconds; + } + + /** + * Checks the hour, minute and second values for run time. + * + * @param runTimeParts The run time values. + * @throws CustomExceptions.InvalidInput If the run time specified is not invalid. + */ + private void checkRunTimeValues(Integer[] runTimeParts) throws CustomExceptions.InvalidInput { + int hours = runTimeParts[WorkoutConstant.RUN_TIME_HOUR_INDEX]; + int minutes = runTimeParts[WorkoutConstant.RUN_TIME_MINUTE_INDEX]; + int seconds = runTimeParts[WorkoutConstant.RUN_TIME_SECOND_INDEX]; + + if (hours == UiConstant.MIN_HOURS) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_HOUR_ERROR); + } + + // minutes can always be 00 + if (minutes > UiConstant.MAX_MINUTES) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_MINUTE_ERROR); + } + + // seconds can never be > 59 + if (seconds > UiConstant.MAX_SECONDS) { + throw new CustomExceptions.InvalidInput(ErrorConstant.INVALID_SECOND_ERROR); + } + if (hours == WorkoutConstant.NO_HOURS_PRESENT) { + // if hours not present, minutes and seconds cannot be 00 + if (minutes == UiConstant.MIN_MINUTES && seconds == UiConstant.MIN_SECONDS) { + throw new CustomExceptions.InvalidInput(ErrorConstant.ZERO_TIME_ERROR); + } + } + } +} diff --git a/src/main/java/workouts/Workout.java b/src/main/java/workouts/Workout.java new file mode 100644 index 0000000000..53531c35cf --- /dev/null +++ b/src/main/java/workouts/Workout.java @@ -0,0 +1,81 @@ +package workouts; +import java.time.LocalDate; + +import constants.ErrorConstant; +import utility.Parser; + +/** + * Workout class is a parent class that is used in Gym and Run classes. + * It contains the date of the workout and a parser object to parse the date. + */ +public class Workout { + //@@author JustinSoh + private LocalDate date = null; + + /** + * Overloaded constructor that uses the optional date parameter from user input. + * + * @param stringDate String representing the date of the workout. + */ + public Workout(String stringDate) { + Parser parser = new Parser(); + this.date = parser.parseDate(stringDate); + } + + /** + * Constructor that builds a new Workout object. + */ + public Workout() { + } + + + /** + * Returns the date of the workout. If the date is not specified, returns "NA". + * + * @return validatedDate as a string representing the date of the workout. + */ + public String getDate() { + String validatedDate = ""; + if(this.date == null){ + validatedDate = ErrorConstant.NO_DATE_SPECIFIED_ERROR; + } else { + validatedDate = this.date.toString(); + } + + return validatedDate; + } + + /** + * Formats date read from file. + * + * @return Formatted date in dd-MM-yyyy format. + */ + public String getDateForFile(){ + Parser parser = new Parser(); + return parser.parseFormattedDate(this.date); + } + + /** + * Retrieves the string representation of a Workout object. + * + * @return A formatted string representing a Workout object. + */ + @Override + public String toString(){ + return getDate().toString(); + } + + /** + * Adds the workout object into the workout list. + * + * @param workout The workout object to be added. + */ + protected void addIntoWorkoutList(Workout workout) { + WorkoutLists workoutLists = new WorkoutLists(); + if (workout instanceof Run) { + workoutLists.addRun((Run) workout); + } else if (workout instanceof Gym) { + workoutLists.addGym((Gym) workout); + } + } +} diff --git a/src/main/java/workouts/WorkoutLists.java b/src/main/java/workouts/WorkoutLists.java new file mode 100644 index 0000000000..fda1e67262 --- /dev/null +++ b/src/main/java/workouts/WorkoutLists.java @@ -0,0 +1,185 @@ +package workouts; + +import storage.LogFile; +import ui.Output; +import utility.CustomExceptions; +import constants.ErrorConstant; +import utility.Validation; + +import java.util.ArrayList; + +/** + * WorkoutLists class contains a static list of workouts, runs and gyms. + * You cannot add a new object to the list directly. + * It will automatically be added when you create a new Run/Gym object. + * To retrieve the list of workouts/gym/run, you can use the static 'get' methods provided. + */ +public class WorkoutLists { + //@@author JustinSoh + private static final ArrayList WORKOUTS = new ArrayList<>(); + private static final ArrayList RUNS = new ArrayList<>(); + private static final ArrayList GYMS = new ArrayList<>(); + + public WorkoutLists() { + + } + + /** + * Returns the static list of workouts objects which contains both runs and gyms. + * It is important to note that the list is not sorted by date (as it is optional) + * Rather, it is ordered by when it has been created. + * + * @return The list of workouts. + */ + public static ArrayList getWorkouts() { + return WORKOUTS; + } + + /** + * Returns the static list of runs objects. + * + * @return The list of runs. + */ + public static ArrayList getRuns() { + return RUNS; + } + + /** + * Returns the static list of gyms objects. + * + * @return The list of gyms. + */ + public static ArrayList getGyms() { + return GYMS; + } + + /** + * Returns latest run. + * + * @return The latest Run object added. + * @throws CustomExceptions.OutOfBounds If no runs are found in the list. + */ + public static Run getLatestRun() throws CustomExceptions.OutOfBounds { + if (RUNS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.RUN_EMPTY_ERROR); + } + return RUNS.get(RUNS.size() - 1); + } + + /** + * Returns latest gym. + * + * @return The latest Gym object added. + * @throws CustomExceptions.OutOfBounds If no gyms are found in the list. + */ + public static Gym getLatestGym() throws CustomExceptions.OutOfBounds { + if (GYMS.isEmpty()) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.GYM_EMPTY_ERROR); + } + return GYMS.get(GYMS.size() - 1); + } + + /** + * Returns the number of runs in the list. + * + * @return The number of runs. + */ + public static int getRunSize() { + return RUNS.size(); + } + + /** + * Returns the number of gyms in the list. + * + * @return The number of gyms. + */ + public static int getGymSize() { + return GYMS.size(); + } + + /** + * Deletes Gym object based on the index that will be validated. + * + * @param index Index of the Gym object to be deleted. + * @throws CustomExceptions.OutOfBounds If the index is invalid. + */ + public static void deleteGym(int index) throws CustomExceptions.OutOfBounds { + boolean indexIsValid = Validation.validateIndexWithinBounds(index, 0, GYMS.size()); + + if (!indexIsValid) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.INVALID_INDEX_DELETE_ERROR); + } + + Gym deletedGym = GYMS.get(index); + Output.printDeleteGymMessage(deletedGym); + WORKOUTS.remove(deletedGym); + GYMS.remove(index); + LogFile.writeLog("Removed gym with index: " + index, false); + } + + /** + * Deletes Run object based on the index that will be validated. + * + * @param index Index of the Run object to be deleted. + * @throws CustomExceptions.OutOfBounds If the index is invalid. + */ + public static void deleteRun(int index) throws CustomExceptions.OutOfBounds { + assert !RUNS.isEmpty() : "Run list is empty."; + boolean indexIsValid = Validation.validateIndexWithinBounds(index, 0, RUNS.size()); + if (!indexIsValid) { + throw new CustomExceptions.OutOfBounds(ErrorConstant.INVALID_INDEX_DELETE_ERROR); + } + Run deletedRun = RUNS.get(index); + Output.printDeleteRunMessage(deletedRun); + WORKOUTS.remove(deletedRun); + RUNS.remove(index); + LogFile.writeLog("Removed run with index: " + index, false); + } + + /** + * Clears the workouts, runs and gyms ArrayLists. + * Used mainly for JUnit testing to clear the list after each test. + */ + public static void clearWorkoutsRunGym() { + WORKOUTS.clear(); + RUNS.clear(); + GYMS.clear(); + } + + // Protected Methods + + /** + * Only classes within the workouts package can add a new run to the list of runs. + * This is called automatically when a new run object is created in the Run class. + * It will also automatically add the run to the workouts list by calling addWorkout. + * + * @param run the Run object to be added + */ + protected void addRun(Run run) { + RUNS.add(run); + addWorkout(run); + } + + /** + * Only classes within the workouts package can add a new gym to the list of gyms. + * This is called automatically when a new gym object is created in the Gym class. + * It will also automatically add the gym to the workouts list by calling addWorkout. + * + * @param gym the Gym object to be added. + */ + protected void addGym(Gym gym) { + GYMS.add(gym); + addWorkout(gym); + } + + // Private Methods + + /** + * Automatically adds a workout to the list of workouts. + * + * @param workout Workout object to be added to the WORKOUTS lists. + */ + private void addWorkout(Workout workout) { + WORKOUTS.add(workout); + } +} diff --git a/src/test/java/health/AppointmentTest.java b/src/test/java/health/AppointmentTest.java new file mode 100644 index 0000000000..e9f64ade64 --- /dev/null +++ b/src/test/java/health/AppointmentTest.java @@ -0,0 +1,125 @@ +package health; + +import constants.ErrorConstant; +import constants.UiConstant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import utility.CustomExceptions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + + +public class AppointmentTest { + private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private static final PrintStream originalOut = System.out; + + @BeforeEach + void setUpStreams() { + HealthList.clearHealthLists(); + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + void cleanup() { + System.setOut(originalOut); + HealthList.clearHealthLists(); + outContent.reset(); + } + + @Test + void printAppointmentHistory_printCorrectAppointmentHistory() throws CustomExceptions.OutOfBounds { + Appointment firstAppointment = new Appointment("25-03-2024", "16:30", "Physiotherapy session"); + Appointment secondAppointment = new Appointment("22-03-2024", "16:00", "Wound dressing change"); + Appointment thirdAppointment = new Appointment("22-03-2024", "11:00", "Doctor consultation"); + + String expected = "Your Appointment history:" + + System.lineSeparator() + + "1. On " + + thirdAppointment.getDate() + + " at " + + thirdAppointment.getTime() + + ": " + + thirdAppointment.getDescription() + + System.lineSeparator() + + "2. On " + + secondAppointment.getDate() + + " at " + + secondAppointment.getTime() + + ": " + + secondAppointment.getDescription() + + System.lineSeparator() + + "3. On " + + firstAppointment.getDate() + + " at " + + firstAppointment.getTime() + + ": " + + firstAppointment.getDescription() + + System.lineSeparator(); + + HealthList.printAppointmentHistory(); + assertEquals(expected, outContent.toString()); + } + + @Test + void deleteAppointment_deleteCorrectAppointment_printCorrectList() throws CustomExceptions.OutOfBounds { + Appointment firstAppointment = new Appointment("25-03-2024", "16:30", "Physiotherapy session"); + Appointment secondAppointment = new Appointment("22-03-2024", "16:00", "Wound dressing change"); + Appointment thirdAppointment = new Appointment("22-03-2024", "11:00", "Doctor consultation"); + + + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Removed appointment on " + + firstAppointment.getDate() + + " at " + + firstAppointment.getTime() + + ": " + + firstAppointment.getDescription() + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your Appointment history:" + + System.lineSeparator() + + "1. On " + + thirdAppointment.getDate() + + " at " + + thirdAppointment.getTime() + + ": " + + thirdAppointment.getDescription() + + System.lineSeparator() + + "2. On " + + secondAppointment.getDate() + + " at " + + secondAppointment.getTime() + + ": " + + secondAppointment.getDescription() + + System.lineSeparator(); + + HealthList.deleteAppointment(2); + + assertEquals(expected, outContent.toString()); + } + + /** + * Test deleting of appointment with negative invalid index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deleteAppointment_negativeIndex_throwOutOfBoundsForAppointment() { + int invalidIndex = -1; + CustomExceptions.OutOfBounds exception = assertThrows( + CustomExceptions.OutOfBounds.class, + () -> HealthList.deleteAppointment(invalidIndex) + ); + String expected = "\u001b[31mOut of Bounds Error: " + + ErrorConstant.APPOINTMENT_EMPTY_ERROR + + "\u001b[0m"; + assertEquals(expected, exception.getMessage()); + } +} diff --git a/src/test/java/health/BmiTest.java b/src/test/java/health/BmiTest.java new file mode 100644 index 0000000000..8af9e16415 --- /dev/null +++ b/src/test/java/health/BmiTest.java @@ -0,0 +1,206 @@ +package health; + +import constants.ErrorConstant; +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 static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import utility.CustomExceptions; + +class BmiTest { + private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private static final PrintStream originalOut = System.out; + + @BeforeEach + void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + void cleanup() { + System.setOut(originalOut); + HealthList.clearHealthLists(); + outContent.reset(); + } + + /** + * Tests the behaviour of toString in Bmi class. + */ + @Test + void toString_heightWeight_printsCorrectBMIAndCategory() { + Bmi bmi = new Bmi("1.75", "70.0", "19-03-2024"); + String expected = "2024-03-19" + + System.lineSeparator() + + "Your BMI is 22.86" + + System.lineSeparator() + + "Great! You're within normal range." + + System.lineSeparator(); + + System.out.println(bmi); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of a BMI within underweight range being passed into printsCorrectCategory. + */ + @Test + void printBMICategory_underweight_printsCorrectCategory() { + String expected = "You're underweight." + System.lineSeparator(); + System.out.println(Bmi.getBmiCategory(17.5)); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of a BMI within normal range being passed into printsCorrectCategory. + */ + @Test + void printBMICategory_normal_printsCorrectCategory() { + String expected = "Great! You're within normal range." + System.lineSeparator(); + System.out.println(Bmi.getBmiCategory(22.0)); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of a BMI within overweight range being passed into printsCorrectCategory. + */ + @Test + void printBMICategory_overweight_printsCorrectCategory() { + String expected = "You're overweight." + System.lineSeparator(); + System.out.println(Bmi.getBmiCategory(27.0)); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of a BMI within obese range being passed into printsCorrectCategory. + */ + @Test + void printBMICategory_obese_printsCorrectCategory() { + String expected = "You're obese." + System.lineSeparator(); + System.out.println(Bmi.getBmiCategory(32.0)); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of a BMI within severely obese range being passed into printsCorrectCategory. + */ + @Test + void printBMICategory_severelyObese_printsCorrectCategory() { + String expected = "You're severely obese." + System.lineSeparator(); + System.out.println(Bmi.getBmiCategory(40.0)); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of showCurrentBmi. + */ + @Test + void printLatestBmi_bmiObject_printsCorrectLatestBmi() throws CustomExceptions.OutOfBounds { + Bmi bmi = new Bmi("1.75", "70.00", "19-03-2024"); + HealthList healthList = new HealthList(); + healthList.addBmi(bmi); + + String expected = "2024-03-19" + + System.lineSeparator() + + "Your BMI is 22.86" + + System.lineSeparator() + + "Great! You're within normal range." + + System.lineSeparator(); + HealthList.printLatestBmi(); + assertEquals(expected, outContent.toString()); + } + + + /** + * Test the behaviour of printing Bmi history. + */ + @Test + void printBmiHistory_twoBmiObjects_printsCorrectBmiHistory() throws CustomExceptions.OutOfBounds { + new Bmi("1.75", "80.0", "20-03-2024"); + new Bmi("1.80", "74.0", "21-03-2024"); + + + String expected = "Your BMI history:" + + System.lineSeparator() + + "1. " + + "2024-03-21" + + System.lineSeparator() + + "Your BMI is 22.84" + + System.lineSeparator() + + "Great! You're within normal range." + + System.lineSeparator() + + "2. " + + "2024-03-20" + + System.lineSeparator() + + "Your BMI is 26.12" + + System.lineSeparator() + + "You're overweight." + + System.lineSeparator(); + + + HealthList.printBmiHistory(); + assertEquals(expected, outContent.toString()); + } + + /** + * Test deleting of bmi with valid list and valid index. + * Expected behaviour is to have one bmi entry left in the list. + * + * @throws CustomExceptions.OutOfBounds If the index is invalid. + */ + @Test + void deleteBmi_properList_listOfSizeOne() throws CustomExceptions.OutOfBounds { + new Bmi("1.75", "80.0", "20-03-2024"); + new Bmi("1.80", "74.0", "21-03-2024"); + + + int index = 1; + HealthList.deleteBmi(index); + assertEquals(1, HealthList.getBmisSize()); + } + + /** + * Test deleting of bmi with empty list. + * Expected behaviour is for an AssertionError to be thrown. + */ + @Test + void deleteBmi_emptyList_throwsCustomExceptions() { + assertThrows(CustomExceptions.OutOfBounds.class, () -> + HealthList.deleteBmi(0)); + } + + /** + * Test deleting of bmi with invalid index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deleteBmi_properListInvalidIndex_throwOutOfBoundsForBmi() { + Bmi firstBmi = new Bmi("1.75", "80.0", "20-03-2024"); + HealthList healthList = new HealthList(); + healthList.addBmi(firstBmi); + int invalidIndex = 5; + assertThrows (CustomExceptions.OutOfBounds.class, () -> + HealthList.deleteBmi(invalidIndex)); + } + + /** + * Test deleting of bmi with invalid negative index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deleteBmi_negativeIndex_throwOutOfBoundsForBmi() { + int invalidIndex = -1; + CustomExceptions.OutOfBounds exception = assertThrows( + CustomExceptions.OutOfBounds.class, + () -> HealthList.deleteBmi(invalidIndex) + ); + String expected = "\u001b[31mOut of Bounds Error: " + + ErrorConstant.BMI_EMPTY_ERROR + + "\u001b[0m"; + assertEquals(expected, exception.getMessage()); + } +} diff --git a/src/test/java/health/PeriodTest.java b/src/test/java/health/PeriodTest.java new file mode 100644 index 0000000000..2d9ef1195e --- /dev/null +++ b/src/test/java/health/PeriodTest.java @@ -0,0 +1,384 @@ +package health; + +import constants.ErrorConstant; +import constants.UiConstant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import utility.CustomExceptions; +import constants.HealthConstant; +import utility.Parser; + + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PeriodTest { + private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private static final PrintStream originalOut = System.out; + + @BeforeEach + void setUpStreams() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + void cleanup() { + System.setOut(originalOut); + HealthList.clearHealthLists(); + outContent.reset(); + } + + /** + * Tests the behaviour of toString in Period class. + */ + @Test + void calculatePeriodLength_printsCorrectPeriod() { + Period period = new Period("09-03-2022", "16-03-2022"); + String expected = "Period Start: " + + period.getStartDate() + + " Period End: " + + period.getEndDate() + + System.lineSeparator() + + "Period Length: " + + period.getPeriodLength() + + " day(s)" + + System.lineSeparator(); + + System.out.println(period); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the showLatestPeriod method and whether it prints + * the last Period object added. + */ + @Test + void printLatestPeriod_twoPeriodInputs_printCorrectPeriod() throws CustomExceptions.OutOfBounds { + Period firstPeriod = new Period("09-02-2023", "16-02-2023"); + Period secondPeriod = new Period("09-03-2023", "16-03-2023"); + + String expected = "Period Start: " + + secondPeriod.getStartDate() + + " Period End: " + + secondPeriod.getEndDate() + + System.lineSeparator() + + "Period Length: " + + secondPeriod.getPeriodLength() + + " day(s)" + + System.lineSeparator(); + + HealthList.printLatestPeriod(); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the showPeriodHistory method and whether it prints + * the period history correctly. + */ + @Test + void showPeriodHistory_twoInputs_printCorrectPeriodHistory() throws CustomExceptions.OutOfBounds { + Period firstPeriod = new Period("10-04-2023", "16-04-2023"); + Period secondPeriod = new Period("09-05-2023", "16-05-2023"); + + String expected = "Your Period history:" + + System.lineSeparator() + + "1. Period Start: " + + secondPeriod.getStartDate() + + " Period End: " + + secondPeriod.getEndDate() + + System.lineSeparator() + + "Period Length: " + + secondPeriod.getPeriodLength() + + " day(s)" + + System.lineSeparator() + + "2. Period Start: " + + firstPeriod.getStartDate() + + " Period End: " + + firstPeriod.getEndDate() + + System.lineSeparator() + + "Period Length: " + + firstPeriod.getPeriodLength() + + " day(s)" + + System.lineSeparator() + + "Cycle Length: " + + firstPeriod.getCycleLength() + + " day(s)" + + System.lineSeparator(); + + HealthList.printPeriodHistory(); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests deleting of periods with valid list and valid index. + * Expected behaviour is to have one periods entry left in the list. + * + * @throws CustomExceptions.OutOfBounds If the index is invalid. + */ + @Test + void deletePeriod_properList_listOfSizeOne() throws CustomExceptions.OutOfBounds { + new Period("10-04-2024", "16-04-2024"); + new Period("09-05-2024", "16-05-2024"); + + int index = 1; + HealthList.deletePeriod(index); + assertEquals(1, HealthList.getPeriodsSize()); + } + + /** + * Tests deleting of period with empty list. + * Expected behaviour is for an AssertionError to be thrown. + */ + @Test + void deletePeriod_emptyList_throwsCustomExceptions() { + assertThrows(CustomExceptions.OutOfBounds.class, () -> + HealthList.deletePeriod(0)); + } + + /** + * Tests deleting of period with invalid index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deletePeriod_properListInvalidIndex_throwOutOfBoundsForBmi() { + Period firstPeriod = new Period("10-04-2024", "16-04-2024"); + + int invalidIndex = 5; + assertThrows(CustomExceptions.OutOfBounds.class, () -> + HealthList.deletePeriod(invalidIndex)); + } + /** + * Tests the behaviour of the predictNextPeriodStartDate function and whether it prints + * correct predicted start date. + */ + @Test + void predictNextPeriodStartDate_sufficientInputs_printCorrectPredictedDate() throws CustomExceptions.OutOfBounds{ + Period firstPeriod = new Period("09-12-2023", "16-12-2023"); + Period secondPeriod = new Period("09-01-2024", "16-01-2024"); + Period thirdPeriod = new Period("10-02-2024", "16-02-2024"); + Period fourthPeriod = new Period("09-03-2024", "14-03-2024"); + + long expectedCycleLength = (31+ 32 + 28) / HealthConstant.LATEST_THREE_CYCLE_LENGTHS; + LocalDate expected = fourthPeriod.getStartDate().plusDays(expectedCycleLength); + LocalDate result = HealthList.predictNextPeriodStartDate(); + assertEquals(expected, result); + } + + /** + * Tests the behaviour of the printNextCyclePrediction function and whether it prints + * the predicted date with period is late message. + */ + @Test + void printNextCyclePrediction_afterToday_printPeriodIsLate() { + LocalDate today = LocalDate.now(); + LocalDate predictedDate = today.minusDays(5); + + String expected = HealthConstant.PREDICTED_START_DATE_MESSAGE + + predictedDate + + HealthConstant.PERIOD_IS_LATE + + "5 " + + HealthConstant.DAYS_MESSAGE + + UiConstant.FULL_STOP + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + + Period.printNextCyclePrediction(predictedDate); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printNextCyclePrediction function and whether it prints + * the predicted date and the number of days to predicted date. + */ + @Test + void printNextCyclePrediction_beforeToday_printNumberOfDaysToPredictedDate() { + LocalDate today = LocalDate.now(); + LocalDate predictedDate = today.plusDays(10); + + String expected = HealthConstant.PREDICTED_START_DATE_MESSAGE + + predictedDate + + ", in 10 " + + HealthConstant.DAYS_MESSAGE + + UiConstant.FULL_STOP + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + + Period.printNextCyclePrediction(predictedDate); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printLatestThreeCycles method and whether it prints + * the latest three period objects only. + */ + @Test + void printLatestThreeCycles_fourInputs_printsThreePeriodObjectsOnly() { + Period firstPeriod = new Period("09-01-2024", "16-01-2024"); + Period secondPeriod = new Period("10-02-2024", "16-02-2024"); + Period thirdPeriod = new Period("09-03-2024", "14-03-2024"); + Period fourthPeriod = new Period("09-04-2024", "16-04-2024"); + + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Period Start: " + + fourthPeriod.getStartDate() + + " Period End: " + + fourthPeriod.getEndDate() + + System.lineSeparator() + + "Period Length: " + + fourthPeriod.getPeriodLength() + + " day(s)" + + System.lineSeparator() + + "Period Start: " + + thirdPeriod.getStartDate() + + " Period End: " + + thirdPeriod.getEndDate() + + System.lineSeparator() + + "Period Length: " + + thirdPeriod.getPeriodLength() + + " day(s)" + + System.lineSeparator() + + "Cycle Length: " + + thirdPeriod.getCycleLength() + + " day(s)" + + System.lineSeparator() + + "Period Start: " + + secondPeriod.getStartDate() + + " Period End: " + + secondPeriod.getEndDate() + + System.lineSeparator() + + "Period Length: " + + secondPeriod.getPeriodLength() + + " day(s)" + + System.lineSeparator() + + "Cycle Length: " + + secondPeriod.getCycleLength() + + " day(s)" + + System.lineSeparator(); + + HealthList.printLatestThreeCycles(); + assertEquals(expected, outContent.toString()); + } + + /** + * Test getPeriod without of bounds index. + * Expected behaviour is for null return. + */ + @Test + void getPeriod_emptyPeriodList_expectNull() { + Period period = new Period("09-01-2024", "16-01-2024"); + Period result = HealthList.getPeriod(1); + + assertNull(result); + } + + /** + * Test deleting of period with invalid negative index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deletePeriod_negativeIndex_throwOutOfBoundsForPeriod() { + int invalidIndex = -1; + CustomExceptions.OutOfBounds exception = assertThrows( + CustomExceptions.OutOfBounds.class, + () -> HealthList.deletePeriod(invalidIndex) + ); + String expected = "\u001b[31mOut of Bounds Error: " + + ErrorConstant.PERIOD_EMPTY_ERROR + + "\u001b[0m"; + assertEquals(expected, exception.getMessage()); + } + + /** + * Test prediction of period with empty period list. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void predictNextPeriodStartDate_emptyPeriodList_throwOutOfBoundsForPeriod() { + CustomExceptions.OutOfBounds exception = assertThrows( + CustomExceptions.OutOfBounds.class, + HealthList::predictNextPeriodStartDate + ); + String expected = "\u001b[31mOut of Bounds Error: " + + ErrorConstant.PERIOD_EMPTY_ERROR + + "\u001b[0m"; + assertEquals(expected, exception.getMessage()); + } + + /** + * Test Period constructor without end date. + * Expected behaviour is to add a Period object without end date. + */ + @Test + public void periodConstructor_expectCreatePeriodWithoutEndDate() { + Period period = new Period("03-04-2024"); + + Parser parser = new Parser(); + assertEquals(parser.parseDate("03-04-2024"), period.getStartDate()); + assertNull(period.getEndDate()); + assertEquals(1, period.getPeriodLength()); + + String expected = "Period Start: " + + period.getStartDate() + + " Period End: NA" + + System.lineSeparator() + + "Period Length: " + + period.getPeriodLength() + + " day(s)" + + System.lineSeparator(); + + System.out.println(period); + assertEquals(expected, outContent.toString()); + } + + /** + * Test update end date of Period object without an end date. + * Expected behaviour is to update the end date of the Period object. + */ + @Test + public void updateEndDate_expectUpdatePeriodWithEndDate () { + Period period = new Period("03-04-2024"); + + Parser parser = new Parser(); + assertEquals(parser.parseDate("03-04-2024"), period.getStartDate()); + assertNull(period.getEndDate()); + assertEquals(1, period.getPeriodLength()); + + period.updateEndDate("05-04-2024"); + assertEquals(parser.parseDate("05-04-2024"), period.getEndDate()); + + String expected = "Period Start: " + + period.getStartDate() + + " Period End: " + + period.getEndDate() + + System.lineSeparator() + + "Period Length: " + + period.getPeriodLength() + + " day(s)" + + System.lineSeparator(); + + System.out.println(period); + assertEquals(expected, outContent.toString()); + } + + /** + * Test calculation of length of the period in days when end date is null. + * Expected behaviour is 0 return. + */ + @Test + void calculatePeriodLength_nullEndDate_expectZeroReturn() { + Period period = new Period("03-04-2024"); + assertEquals(0, period.calculatePeriodLength()); + } + + +} diff --git a/src/test/java/helper/TestHelper.java b/src/test/java/helper/TestHelper.java new file mode 100644 index 0000000000..8ecee33857 --- /dev/null +++ b/src/test/java/helper/TestHelper.java @@ -0,0 +1,273 @@ +package helper; + +import constants.ErrorConstant; +import constants.HealthConstant; +import constants.UiConstant; + +/** + * Helper class to help with testing. + * Contains methods to help create the output string for testing. + * Methods that starts with add are used when objects of that type is created. + * Methods that starts with latest are used when the latest object is printed. + * Methods that starts with error are used when an error is thrown. + */ +public class TestHelper { + //@@author JustinSoh + /** + * Helper method to help create the output string when BMI is added. + * + * @param weight weight of the user. + * @param height height of the user. + * @param date date of the user in the format yyyy-mm-dd. + * @param bmiValue bmi value of the user in 2 decimal places. + * @param bmiCategory the category (use the constants in HealthConstant). + * @return add BMI output string. + */ + public static String addBmiOutputString (String weight, + String height, + String date, + double bmiValue, + String bmiCategory){ + + return UiConstant.PARTITION_LINE + + System.lineSeparator() + + HealthConstant.BMI_ADDED_MESSAGE_PREFIX + + height + + UiConstant.LINE + + weight + + UiConstant.LINE + + date + + System.lineSeparator() + + String.format(HealthConstant.PRINT_BMI_FORMAT, + date, bmiValue, bmiCategory) + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + } + + /** + * Helper method to help create the latestString for Period. + * + * @param date date of the bmi in the format yyyy-mm-dd. + * @param bmiValue bmi value of the user in 2 decimal places. + * @param bmiMessage the category (use the constants in HealthConstant). + * @return latest BMI output string. + */ + public static String latestBmiOutputString(String date, double bmiValue, String bmiMessage ) { + return UiConstant.PARTITION_LINE + + System.lineSeparator() + + date + + System.lineSeparator() + + "Your BMI is " + + String.format("%.2f" , bmiValue) + + System.lineSeparator() + + bmiMessage + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + } + + /** + * Helper method to help create the output string when Period is added + * + * @param startDate start date of the period in the format yyyy-mm-dd. + * @param endDate end date of the period in the format yyyy-mm-dd. + * @param periodLength length of the period in days. + * @return add Period output string. + */ + public static String addPeriodOutputString (String startDate, + String endDate, + int periodLength){ + + StringBuilder outputString = new StringBuilder(); + outputString.append(UiConstant.PARTITION_LINE); + outputString.append(System.lineSeparator()); + outputString.append(HealthConstant.PERIOD_ADDED_MESSAGE_PREFIX); + outputString.append(startDate); + outputString.append(UiConstant.LINE); + outputString.append(endDate); + outputString.append(System.lineSeparator()); + outputString.append(String.format(HealthConstant.PRINT_PERIOD_FORMAT, + startDate, + endDate , + periodLength, + HealthConstant.DAYS_MESSAGE)); + outputString.append(System.lineSeparator()); + if (!HealthConstant.PERIOD_TOO_LONG_MESSAGE.isBlank()) { + outputString.append(ErrorConstant.COLOR_HEADING + + HealthConstant.PERIOD_TOO_LONG_MESSAGE + + ErrorConstant.COLOR_ENDING); + outputString.append(System.lineSeparator()); + } + outputString.append(UiConstant.PARTITION_LINE); + outputString.append(System.lineSeparator()); + + + return outputString.toString(); + } + + /** + * Helper method to help create the latestString for Period. + * + * @param startDate start date of the period in the format yyyy-mm-dd. + * @param endDate end date of the period in the format yyyy-mm-dd. + * @param periodLength length of the period in days. + * @param period the "days" message (use the constants in HealthConstant). + * @return latest Period Output string. + */ + public static String latestPeriodOutputString(String startDate, String endDate, int periodLength, String period) { + return UiConstant.PARTITION_LINE + + System.lineSeparator() + + String.format(HealthConstant.PRINT_PERIOD_FORMAT, + startDate, + endDate, + periodLength, + period) + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + + } + + /** + * Helper method to help create the output string when Appointment is added. + * + * @param date date of the appointment in the format yyyy-mm-dd. + * @param time time of the appointment in the format hh:mm. + * @param description description of the appointment. + * @return add Appointment output string. + */ + public static String addAppointmentString(String date, String time, String description) { + return UiConstant.PARTITION_LINE + + System.lineSeparator() + + HealthConstant.APPOINTMENT_ADDED_MESSAGE_PREFIX + + date + + UiConstant.LINE + + time + + UiConstant.LINE + + description + + System.lineSeparator() + + String.format(HealthConstant.PRINT_APPOINTMENT_FORMAT, date, time, description) + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + } + + /** + * Helper method to help create the latestString for Appointment. + * + * @param date date of the appointment in the format yyyy-mm-dd. + * @param time time of the appointment in the format hh:mm. + * @param description description of the appointment. + * @return latest Appointment output string. + */ + public static String latestAppointmentOutputString(String date, String time, String description) { + return UiConstant.PARTITION_LINE + + System.lineSeparator() + + String.format(HealthConstant.PRINT_APPOINTMENT_FORMAT, date, time, description) + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + } + + /** + * Helper method for CustomException.InvalidInput() to help create the output string when an invalid input is given. + * + * @param errorString the error message to be printed. + * @return Error InvalidInput Output String. + */ + public static String errorInvalidInputString(String errorString) { + return ErrorConstant.COLOR_HEADING + + "Exception Caught!" + + System.lineSeparator() + + ErrorConstant.COLOR_HEADING + + ErrorConstant.INVALID_INPUT_HEADER + + errorString + + ErrorConstant.COLOR_ENDING + + ErrorConstant.COLOR_ENDING + + System.lineSeparator(); + } + + /** + * Helper method for CustomException.OutOfBounds(). + * Used to get the out-of-bounds exception string for invalid index. + * + * @param errorString the error message to be printed. + * @return Error OutOfBounds Output String. + */ + public static String errorOutOfBoundsString(String errorString) { + return ErrorConstant.COLOR_HEADING + + "Exception Caught!" + + System.lineSeparator() + + ErrorConstant.COLOR_HEADING + + ErrorConstant.OUT_OF_BOUND_HEADER + + errorString + + ErrorConstant.COLOR_ENDING + + ErrorConstant.COLOR_ENDING + + System.lineSeparator(); + } + + /** + * Helper method for Invalid Command String. + * Used for methods that prints an error rather than use Output.printException(). + * + * @param errorString to be printed. + * @return Error Invalid Command Output String. + */ + public static String errorInvalidCommandString(String errorString) { + return ErrorConstant.COLOR_HEADING + + "Exception Caught!" + + System.lineSeparator() + + errorString + + ErrorConstant.COLOR_ENDING + + System.lineSeparator(); + } + + + /** + * Helper method for CustomException.InsufficientInput(). + * Used to get the insufficient input exception string for insufficient input. + * + * @param errorString the error message to be printed. + * @return Error InsufficientInput Output String. + */ + public static String errorInsufficientInput(String errorString){ + return ErrorConstant.COLOR_HEADING + + "Exception Caught!" + + System.lineSeparator() + + ErrorConstant.COLOR_HEADING + + ErrorConstant.INSUFFICIENT_INPUT_HEADER + + errorString + + ErrorConstant.COLOR_ENDING + + ErrorConstant.COLOR_ENDING + + System.lineSeparator(); + } + + /** + * Helper method to help create the greeting string when file is found. + * + * @param name name of the person. + * @return greeting string with name. + */ + public static String printGreetingsFoundString(String name){ + return UiConstant.FILE_FOUND_MESSAGE + + name + + System.lineSeparator() + + UiConstant.SUCCESSFUL_LOAD + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + } + + /** + * Helper method to help create the greeting string when file is not found. + * + * @return greeting string when file is not found. + */ + public static String printGreetingNotFoundString(){ + return UiConstant.FILE_MISSING_MESSAGE + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/pulsepilot/PulsePilotTest.java similarity index 77% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/seedu/pulsepilot/PulsePilotTest.java index 2dda5fd651..5b51c7f6a6 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/pulsepilot/PulsePilotTest.java @@ -1,10 +1,10 @@ -package seedu.duke; +package seedu.pulsepilot; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -class DukeTest { +class PulsePilotTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/storage/DataFileTest.java b/src/test/java/storage/DataFileTest.java new file mode 100644 index 0000000000..4c239d4892 --- /dev/null +++ b/src/test/java/storage/DataFileTest.java @@ -0,0 +1,491 @@ +//@@author L5-Z +package storage; +import constants.UiConstant; +import health.Appointment; +import health.Bmi; +import health.HealthList; +import health.Period; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import utility.CustomExceptions; +import workouts.Gym; +import workouts.Run; +import workouts.Workout; +import workouts.WorkoutLists; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +public class DataFileTest { + private final String testDataFilePath = "./test_data.txt"; + private final String testHashFilePath = "./test_hash.txt"; + private final String originalDataFilePath = "./pulsepilot_data.txt"; + private final String originalHashFilePath = "./pulsepilot_hash.txt"; + + /** + * Sets up the test environment by setting the file paths to the test files. + */ + @BeforeEach + void setUp() { + // Set the file paths to the test files + UiConstant.dataFilePath = testDataFilePath; + UiConstant.saveFile = new File(testDataFilePath); + UiConstant.hashFilePath = testHashFilePath; + } + + /** + * Tears down the test environment by deleting the test files and resetting the file paths. + */ + @AfterEach + void tearDown() { + // Delete the test files after each test + new File(testDataFilePath).delete(); + new File(testHashFilePath).delete(); + + // Reset the file paths + UiConstant.dataFilePath = originalDataFilePath; + UiConstant.saveFile = new File(originalDataFilePath); + UiConstant.hashFilePath = originalHashFilePath; + } + + /** + * Cleans up the WorkoutList and HealthList before each test. + */ + private void cleanup(){ + WorkoutLists.clearWorkoutsRunGym(); + HealthList.clearHealthLists(); + } + + /** + * Asserts that the contents of the data file match the expected values. + * + * @param name the user's name + * @param bmiArrayList the list of BMI entries + * @param appointmentArrayList the list of appointments + * @param periodArrayList the list of periods + * @param workoutArrayList the list of workouts + */ + private void assertDataFileContents(String name, ArrayList bmiArrayList, + ArrayList appointmentArrayList, + ArrayList periodArrayList, + ArrayList workoutArrayList) { + try { + List lines = Files.readAllLines(Path.of(testDataFilePath)); + assertEquals("NAME:" + name, lines.get(0)); + + int index = 1; + for (Bmi bmi : bmiArrayList) { + assertEquals("BMI:" + bmi.getHeight() + ":" + bmi.getWeight() + ":" + bmi.getBmiValueString() + ":" + + bmi.getDate(), lines.get(index++)); + } + + for (Appointment appointment : appointmentArrayList) { + assertEquals("APPOINTMENT:" + appointment.getDate() + ":" + appointment.getTime() + ":" + + appointment.getDescription(), lines.get(index++)); + } + + for (Period period : periodArrayList) { + assertEquals("PERIOD:" + period.getStartDate() + ":" + period.getEndDate() + ":" + + period.getPeriodLength(), lines.get(index++)); + } + + for (Workout workout : workoutArrayList) { + if (workout instanceof Run) { + Run run = (Run) workout; + assertEquals("RUN:" + run.getDistance() + ":" + run.getTimes() + ":" + run.getDate(), + lines.get(index++)); + } else if (workout instanceof Gym) { + Gym gym = (Gym) workout; + assertEquals(gym.toFileString(), lines.get(index++)); + } + } + } catch (IOException e) { + fail("Error reading data file: " + e.getMessage()); + } + } + + + /** + * Tests the saveDataFile method with valid data. + * Verifies that the data file is written correctly. + */ + @Test + void saveDataFile_validData_writesCorrectly() throws IOException, CustomExceptions.FileWriteError, + CustomExceptions.InvalidInput { + // Arrange + String name = "John Doe"; + ArrayList bmiArrayList = new ArrayList<>(Arrays.asList( + new Bmi("1.70", "70.00", "01-04-2023"), + new Bmi("1.80", "80.00", "15-04-2023") + )); + ArrayList appointmentArrayList = new ArrayList<>(Arrays.asList( + new Appointment("01-05-2023", "10:00", "Dentist Appointment"), + new Appointment("15-05-2023", "14:30", "Doctor's Checkup") + )); + ArrayList periodArrayList = new ArrayList<>(Arrays.asList( + new Period("01-03-2023", "05-03-2023"), + new Period("01-04-2023", "04-04-2023") + )); + + + Gym newGym = new Gym("11-11-1997"); + Gym newGym2 = new Gym(); + + try { + newGym.addStation("bench press", "4", "4", "10.0,20.0,30.0,40.0"); + newGym.addStation("squats", "4", "3", "20.0,30.0,40.0,50.0"); + newGym2.addStation("bench press", "4", "4", "10.0,20.0,30.0,40.0"); + newGym2.addStation("squats", "4", "3", "20.0,30.0,40.0,50.0"); + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception"); + } + + + ArrayList workoutArrayList = new ArrayList<>(List.of( + new Run("30:00", "5.00", "01-04-2023"), + newGym, + newGym2 + )); + + // Act + DataFile dataFile = new DataFile(); + int status = dataFile.loadDataFile(); + dataFile.saveDataFile(name, bmiArrayList, appointmentArrayList, periodArrayList, workoutArrayList); + + // Assert + List lines = Files.readAllLines(Path.of(testDataFilePath)); + + if (!lines.isEmpty()) { + assertEquals("NAME:John Doe", lines.get(0)); + assertEquals("BMI:1.70:70.00:24.22:01-04-2023", lines.get(1)); + assertEquals("BMI:1.80:80.00:24.69:15-04-2023", lines.get(2)); + assertEquals("APPOINTMENT:01-05-2023:10.00:Dentist Appointment", lines.get(3)); + assertEquals("APPOINTMENT:15-05-2023:14.30:Doctor's Checkup", lines.get(4)); + assertEquals("PERIOD:01-03-2023:05-03-2023:5", lines.get(5)); + assertEquals("PERIOD:01-04-2023:04-04-2023:4", lines.get(6)); + assertEquals("RUN:5.00:30.00:01-04-2023", lines.get(7)); + assertEquals("GYM:2:11-11-1997:bench press:4:4:10.0,20.0,30.0," + + "40.0:squats:4:3:20.0,30.0,40.0,50.0", lines.get(8)); + assertEquals("GYM:2:NA:bench press:4:4:10.0,20.0,30.0,40.0:squats:4:3:20.0,30.0,40.0,50.0", + lines.get(9)); + } else { + fail("Data file is empty"); + } + } + + /** + * Tests the saveDataFile method with empty data. + * Verifies that the data file is written correctly. + */ + @Test + void saveDataFile_emptyData_writesCorrectly() throws CustomExceptions.FileWriteError { + // Arrange + String name = "Jane Doe"; + ArrayList bmiArrayList = new ArrayList<>(); + ArrayList appointmentArrayList = new ArrayList<>(); + ArrayList periodArrayList = new ArrayList<>(); + ArrayList workoutArrayList = new ArrayList<>(); + + // Act + DataFile dataFile = new DataFile(); + int status = dataFile.loadDataFile(); + dataFile.saveDataFile(name, bmiArrayList, appointmentArrayList, periodArrayList, workoutArrayList); + + // Assert + assertTrue(new File(testDataFilePath).exists()); + assertTrue(new File(testHashFilePath).exists()); + assertTrue(new File(testHashFilePath).length() != 0); + assertDataFileContents(name, bmiArrayList, appointmentArrayList, periodArrayList, workoutArrayList); + } + + /** + * Tests the loadDataFile method when the data file does not exist. + * Verifies that a new file is created. + */ + @Test + void loadDataFile_nonExistentFile_createsNew() throws + CustomExceptions.InvalidInput, CustomExceptions.FileWriteError { + // Arrange + String name = "John Doe"; + ArrayList bmiArrayList = new ArrayList<>(Arrays.asList( + new Bmi("1.70", "70.00", "01-04-2023"), + new Bmi("1.80", "80.00", "15-04-2023") + )); + ArrayList appointmentArrayList = new ArrayList<>(Arrays.asList( + new Appointment("01-05-2023", "10:00", "Dentist Appointment"), + new Appointment("15-05-2023", "14:30", "Doctor's Checkup") + )); + ArrayList periodArrayList = new ArrayList<>(Arrays.asList( + new Period("01-03-2023", "05-03-2023"), + new Period("01-04-2023", "04-04-2023") + )); + ArrayList workoutArrayList = new ArrayList<>(Arrays.asList( + new Run("30:00", "5.00", "01-04-2023") + )); + + // Act + DataFile dataFile = new DataFile(); + int status = dataFile.loadDataFile(); + dataFile.saveDataFile(name, bmiArrayList, appointmentArrayList, periodArrayList, workoutArrayList); + + // Assert + assertEquals(UiConstant.FILE_NOT_FOUND, status); + assertTrue(new File(testDataFilePath).exists()); + assertTrue(new File(testHashFilePath).exists()); + } + + /** + * Tests the generateFileHash method with a valid file. + * Verifies that the correct hash is generated. + */ + @Test + void generateFileHash_validFile_returnsCorrectHash() throws NoSuchAlgorithmException, IOException, + CustomExceptions.InvalidInput, CustomExceptions.FileWriteError { + // Arrange + String name = "John Doe"; + ArrayList bmiArrayList = new ArrayList<>(Arrays.asList( + new Bmi("1.70", "70.00", "01-04-2023"), + new Bmi("1.80", "80.00", "15-04-2023") + )); + ArrayList appointmentArrayList = new ArrayList<>(Arrays.asList( + new Appointment("01-05-2023", "10:00", "Dentist Appointment"), + new Appointment("15-05-2023", "14:30", "Doctor's Checkup") + )); + ArrayList periodArrayList = new ArrayList<>(Arrays.asList( + new Period("01-03-2023", "05-03-2023"), + new Period("01-04-2023", "04-04-2023") + )); + ArrayList workoutArrayList = new ArrayList<>(Arrays.asList( + new Run("30:00", "5.00", "01-04-2023") + )); + + File dataFileName = new File(testDataFilePath); + DataFile dataFile = new DataFile(); + dataFile.saveDataFile(name, bmiArrayList, appointmentArrayList, periodArrayList, workoutArrayList); + + // Act + String hash = dataFile.generateFileHash(dataFileName); + + // Assert + assertNotNull(hash); + assertFalse(hash.isEmpty()); + } + + /** + * Tests the loadDataFile method with an existing file. + * Verifies that the data is loaded correctly. + */ + @Test + void loadDataFile_existingFile_readsCorrectly() throws CustomExceptions.FileReadError, + CustomExceptions.FileWriteError, CustomExceptions.InvalidInput { + // Arrange + String name = "John Doe"; + ArrayList bmiArrayList = new ArrayList<>(Arrays.asList( + new Bmi("1.80", "80.00", "15-04-2023"), + new Bmi("1.70", "70.00", "01-04-2023") + )); + ArrayList appointmentArrayList = new ArrayList<>(Arrays.asList( + new Appointment("01-05-2025", "10:00", "Dentist Appointment"), + new Appointment("15-05-2025", "14:30", "Doctor's Checkup") + )); + + // Has additional elements added to ArrayList and will thus be skipped + ArrayList periodArrayList = new ArrayList<>(Arrays.asList( + new Period ("08-05-2023"), + new Period("01-04-2023", "07-04-2023") + + + )); + Gym gym1 = new Gym(); + try { + gym1.addStation("Squat Press", "1", "50", "1.0"); + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception"); + } + ArrayList workoutArrayList = new ArrayList<>(Arrays.asList( + new Run("40:10", "10.32", "15-03-2024"), + new Run("40:10", "10.32"), + gym1 + )); + + DataFile dataFile = new DataFile(); + int status = dataFile.loadDataFile(); + dataFile.saveDataFile(name, bmiArrayList, appointmentArrayList, periodArrayList, workoutArrayList); + + // Act + cleanup(); + dataFile.readDataFile(); + + + // Assert + assertEquals(name, DataFile.userName); + assertEquals(Arrays.toString(bmiArrayList.toArray()), Arrays.toString(HealthList.getBmis().toArray())); + + assertEquals(Arrays.toString(appointmentArrayList.toArray()), + Arrays.toString(HealthList.getAppointments().toArray())); + assertEquals(Arrays.toString(periodArrayList.toArray()), + Arrays.toString(HealthList.getPeriods().toArray())); + assertEquals(Arrays.toString(workoutArrayList.toArray()), + Arrays.toString(WorkoutLists.getWorkouts().toArray())); + } + + /** + * Tests the verifyIntegrity method with an invalid file. + * Expects a FileCreateError exception to be thrown. + */ + @Test + void verifyIntegrity_invalidFileName_expectsFileCreateErrorException() { + File testFile = new File(""); + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.FileCreateError.class, () -> dataFile.verifyIntegrity(testFile)); + } + + /** + * Tests the readHashFromFile method with a valid hash file. + * Verifies that the correct hash is read. + */ + @Test + void readHashFromFile_validHashFile_returnsCorrectHash() throws IOException { + // Arrange + String expectedHash = "abc123def456"; + File hashFile = new File(testHashFilePath); + try (FileWriter writer = new FileWriter(hashFile)) { + writer.write(expectedHash); + } + + // Act + DataFile dataFile = new DataFile(); + String actualHash = dataFile.readHashFromFile(hashFile); + + // Assert + assertEquals(expectedHash, actualHash); + } + + /** + * Tests the processName method with an invalid username. + * Expects an InvalidInput exception to be thrown. + */ + @Test + void processName_invalidUsername_throwsInvalidInputException() { + // Arrange + String invalidUsername = "John~Doe123"; + + // Act and Assert + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.InvalidInput.class, () -> dataFile.processName(invalidUsername)); + } + + /** + * Tests the processAppointment method with missing appointment details. + * Expects an InvalidInput exception to be thrown. + */ + @Test + void processAppointment_missingAppointmentDetails_throwsInvalidInputException() { + // Arrange + String[] input = {"APPOINTMENT", "01-05-2025", "10:00", "invalid_description~"}; + + // Act and Assert + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.InvalidInput.class, () -> dataFile.processAppointment(input)); + } + + /** + * Tests the processPeriod method with invalid period input. + * Expects an InvalidInput exception to be thrown. + */ + @Test + void processPeriod_invalidPeriodInput_throwsInvalidInputException() { + // Arrange + String[] input = {"PERIOD", "01-04-2023", "invalid_date"}; + + // Act and Assert + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.InvalidInput.class, () -> dataFile.processPeriod(input)); + } + + /** + * Tests the processBmi method with invalid BMI input. + * Expects an InvalidInput exception to be thrown. + */ + @Test + void processBmi_invalidBmiInput_throwsInvalidInputException() { + // Arrange + String[] input = {"BMI", "invalid_height", "70.00", "24.22", "01-04-2023"}; + + // Act and Assert + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.InvalidInput.class, () -> dataFile.processBmi(input)); + } + + /** + * Tests the processRun method with invalid run input. + * Expects an InvalidInput exception to be thrown. + */ + @Test + void processRun_invalidRunInput_throwsInvalidInputException() { + // Arrange + String[] input = {"RUN", "invalid_distance", "00:30:00", "01-04-2023"}; + + // Act and Assert + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.InvalidInput.class, () -> dataFile.processRun(input)); + } + + /** + * Tests the processGym method with invalid gym input. + * Expects an InvalidInput exception to be thrown. + */ + @Test + void processGym_invalidGymInput_throwsInvalidInputException() { + // Arrange + String rawInput = "GYM:2:11-11-1997:bench press:" + + "4:4:10.0,20.0,30.0,40.0:invalid_station:4:3:20.0,30.0,40.0,50.0"; + + // Act and Assert + DataFile dataFile = new DataFile(); + assertThrows(CustomExceptions.InvalidInput.class, () -> dataFile.processGym(rawInput)); + } + + /** + * Tests the processFail method. + * Verifies that the error is logged and the files are deleted. + */ + @Test + void processFail_logsErrorAndDeletesFiles() { + // Arrange + DataFile dataFile = new DataFile(); + String errorString = "Test error string"; + String dataFilePath = testDataFilePath; + String hashFilePath = testHashFilePath; + + // Act + dataFile.processFail(errorString); + + // Assert + // Check if the error was logged + String logContent = LogFile.readLogContent(); + assertTrue(logContent.contains(errorString)); + + // Check if the data file was deleted + assertFalse(new File(dataFilePath).exists()); + + // Check if the hash file was deleted + assertFalse(new File(hashFilePath).exists()); + } +} diff --git a/src/test/java/storage/LogFileTest.java b/src/test/java/storage/LogFileTest.java new file mode 100644 index 0000000000..42e8acf499 --- /dev/null +++ b/src/test/java/storage/LogFileTest.java @@ -0,0 +1,18 @@ +package storage; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class LogFileTest { + static LogFile logTest = LogFile.getInstance(); + + /** + * Tests the behaviour of the getInstance function in the LogFile class, and whether + * it returns a non-null instance. + */ + @Test + void initializeLogFile_noInput_logFileHandlerNotNull() { + assertNotNull(LogFile.logFileHandler); + } +} diff --git a/src/test/java/ui/HandlerTest.java b/src/test/java/ui/HandlerTest.java new file mode 100644 index 0000000000..607afc7918 --- /dev/null +++ b/src/test/java/ui/HandlerTest.java @@ -0,0 +1,433 @@ +package ui; + +import constants.UiConstant; +import health.HealthList; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import constants.ErrorConstant; +import workouts.WorkoutLists; + +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the functionality of the Handler class. + * It includes tests for processing various user inputs and verifying the expected output. + */ +class HandlerTest { + private final ByteArrayInputStream inContent = new ByteArrayInputStream("".getBytes()); + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final InputStream originalIn = System.in; + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + /** + * Sets up the test environment by redirecting the standard input, output, and error streams. + */ + @BeforeEach + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + System.setIn(inContent); + System.setErr(new PrintStream(errContent)); + } + + /** + * Restores the original standard input, output, and error streams, and cleans up the + * WorkoutLists and HealthList after each test. + */ + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + System.setIn(originalIn); + System.setErr(originalErr); + WorkoutLists.clearWorkoutsRunGym(); + HealthList.clearHealthLists(); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'EXIT' command. + * Verifies that the program terminates. + */ + @Test + void processInput_exitCommand_terminatesProgram() { + String input = "EXIT"; + Handler myHandler = new Handler(input); + myHandler.processInput(); + String output = outContent.toString(); + assertTrue(output.contains("Initiating PulsePilot landing sequence...")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'WORKOUT' command + * to add a new run exercise. + * Verifies that the run is added successfully. + */ + @Test + void processInput_workoutCommand_addRunExercise() { + String input = "WORKOUT /e:run /d:10.30 /t:40:10 /date:15-03-2024"; + Handler myHandler = new Handler(input); + myHandler.processInput(); + String output = outContent.toString(); + System.out.println(output); + assertTrue(output.contains("Successfully added a new run session")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'HEALTH' command + * to add a new BMI data point. + * Verifies that the BMI data is added successfully. + */ + @Test + void processInput_healthCommand_addBMIHealthData() { + String input = "HEALTH /h:bmi /height:1.70 /weight:65.00 /date:15-03-2024"; + Handler myHandler = new Handler(input); + myHandler.processInput(); + String output = outContent.toString(); + assertTrue(output.contains("Added: bmi | 1.70 | 65.00 | 2024-03-15")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'HEALTH' command + * to add a new appointment. + * Verifies that the appointment is added successfully. + */ + @Test + void processInput_healthCommand_addAppointment() { + String input = "health /h:appointment /date:30-03-2024 /time:19:30 /description:test"; + Handler myHandler = new Handler(input); + myHandler.processInput(); + String output = outContent.toString(); + assertTrue(output.contains("Added: appointment | 2024-03-30 | 19:30 | test")); + } + + + /** + * Tests the processInput function's behaviour when the user enters the 'HISTORY' command + * to print the history of runs. + * Verifies that the run history is printed correctly. + */ + @Test + void processInput_historyCommand_printsHistoryRun() { + String inputRun = "WORKOUT /e:run /d:10.30 /t:40:10" + + System.lineSeparator() + + "HISTORY /item:run"; + Handler myHandler = new Handler(inputRun); + myHandler.processInput(); + String output = outContent.toString(); + assertTrue(output.contains("Your run history")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'LATEST' command + * to print the latest run. + * Verifies that the latest run is printed correctly. + */ + @Test + void processInput_latestCommand_printsLatestRun() { + String inputRun = "WORKOUT /e:run /d:10.30 /t:40:10 /date:15-03-2024" + + System.lineSeparator() + + "LATEST /item:run"; + Handler myHandler = new Handler(inputRun); + myHandler.processInput(); + String output = outContent.toString(); + assertTrue(output.contains("Your latest run:")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'HELP' command. + * Verifies that the help message is printed correctly. + */ + @Test + void processInput_helpCommand_printsHelp() { + String input = "HELP"; + Handler myHandler = new Handler(input); + myHandler.processInput(); + String output = outContent.toString(); + assertTrue(output.contains("Commands List")); + } + + + /** + * Tests the processInput function's behaviour when the user enters an invalid command. + * Verifies that an error message is printed. + */ + @Test + void processInput_invalidCommand_printsInvalidCommandException() { + String input = "INVALID"; + Handler handler = new Handler(input); + handler.processInput(); + String expected = "\u001b[31mException Caught!" + + System.lineSeparator() + + ErrorConstant.INVALID_COMMAND_ERROR + + "\u001b[0m" + + System.lineSeparator(); + assertEquals(expected, errContent.toString()); + } + + /** + * Tests the processInput function's behaviour when the user enters an invalid run command + * with an invalid distance. + * Verifies that an error message is printed. + */ + @Test + void processInput_invalidRunCommand_printsInvalidDistanceError() { + String input = "workout /e:run /t:22:11 /d:5"; + Handler handler = new Handler(input); + handler.processInput(); + String expected = "\u001b[31mException Caught!" + + System.lineSeparator() + + "\u001b[31m" + + "Invalid Input Exception: " + + ErrorConstant.INVALID_RUN_DISTANCE_ERROR + + "\u001b[0m\u001b[0m" + + System.lineSeparator(); + assertEquals(expected, errContent.toString()); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'HEALTH' command + * with insufficient parameters. + * Verifies that an error message is printed. + */ + @Test + void processInput_healthCommand_insufficientParameters() { + String input = "HEALTH /h:bmi /height:1.70"; + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(errContent.toString().contains(ErrorConstant.INSUFFICIENT_BMI_PARAMETERS_ERROR)); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with a valid BMI entry. + * Verifies that the BMI entry is deleted successfully. + */ + @Test + void processInput_deleteCommandWithValidBmi_validDeleteMessage() { + String input = "HEALTH /h:bmi /height:1.70 /weight:70.00 /date:09-04-2024" + + System.lineSeparator() + + "DELETE /item:bmi /index:1" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(outContent.toString().contains("Removed BMI entry")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with no BMI objects added. + * Verifies that an error message is printed. + */ + @Test + void processInput_deleteCommandWithInvalidBmiIndex_expectErrorMessage() { + String input = "HEALTH /h:bmi /height:1.70 /weight:70.00 /date:09-04-2024" + + System.lineSeparator() + + "DELETE /item:bmi /index:99" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with a valid appointment. + * Verifies that the appointment is deleted successfully. + */ + @Test + void processInput_deleteCommandWithValidAppointment_validDeleteMessage() { + String input = "health /h:appointment /date:03-04-2024 /time:14:15 /description:review checkup with surgeon" + + System.lineSeparator() + + "DELETE /item:appointment /index:1" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(outContent.toString().contains("Removed appointment")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with an invalid appointment index. + * Verifies that an error message is printed. + */ + @Test + void processInput_deleteCommandWithInvalidAppointmentIndex_validDeleteMessage() { + String input = "health /h:appointment /date:03-04-2024 /time:14:15 /description:review checkup with surgeon" + + System.lineSeparator() + + "DELETE /item:appointment /index:99" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with a valid run. + * Verifies that the run is deleted successfully. + */ + @Test + void processInput_deleteCommandWithValidRun_validDeleteMessage() { + String input = "workout /e:run /d:5.15 /t:25:03 /date:25-03-2023" + + System.lineSeparator() + + "DELETE /item:run /index:1" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(outContent.toString().contains("Removed Run")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with an invalid run index. + * Verifies that an error message is printed. + */ + @Test + void processInput_deleteCommandWithInvalidRunIndex_validDeleteMessage() { + String input = "workout /e:run /d:5.15 /t:25:03 /date:25-03-2023" + + System.lineSeparator() + + "DELETE /item:run /index:99" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with a valid period. + * Verifies that the period is deleted successfully. + */ + @Test + void processInput_deleteCommandWithValidPeriod_validDeleteMessage() { + String input = "health /h:period /start:09-03-2022 /end:16-03-2022" + + System.lineSeparator() + + "DELETE /item:period /index:1" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(outContent.toString().contains("Removed period")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with an invalid period index. + * Verifies that an error message is printed. + */ + @Test + void processInput_deleteCommandWithInvalidPeriodIndex_validDeleteMessage() { + String input = "workout /e:run /d:5.15 /t:25:03 /date:25-03-2023" + + System.lineSeparator() + + "DELETE /item:run /index:99" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with a valid gym. + * Verifies that the gym is deleted successfully. + */ + @Test + void processInput_deleteCommandWithValidGym_validDeleteMessage() { + String input = "workout /e:gym /n:1 /date:25-03-2023" + + System.lineSeparator() + + "bench press /s:2 /r:4 /w:10,20" + + System.lineSeparator() + + "DELETE /item:gym /index:1" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(outContent.toString().contains("Removed Gym")); + } + + /** + * Tests the processInput function's behaviour when the user enters the 'DELETE' command + * with an invalid gym index. + * Verifies that an error message is printed. + */ + @Test + void processInput_deleteCommandWithInvalidGymIndex_validDeleteMessage() { + String input = "workout /e:gym /n:1 /date:25-03-2023" + + System.lineSeparator() + + "bench press /s:2 /r:4 /w:10,20" + + System.lineSeparator() + + "DELETE /item:gym /index:2" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } + + /** + * Tests the processInput function's behaviour when the user enters a gym command, adds one + * station, and then types 'back' to exit. + * Verifies that the gym is not added and a delete message is printed. + */ + @Test + void processInput_workoutCommandWithGymStationExit_expectsNoGymAddedAndDeleteMessage() { + String input = "workout /e:gym /n:2 /date:25-03-2023" + + System.lineSeparator() + + "bench press /s:2 /r:4 /w:10,20" + + System.lineSeparator() + + "back" + + System.lineSeparator(); + Handler myHandler = new Handler(input); + myHandler.processInput(); + assertTrue(outContent.toString().contains("Removed Gym entry with 1 station(s).")); + } + + /** + * Tests the userInduction function's behaviour when the user enters a valid username. + * Verifies that the welcome greeting is printed. + */ + @Test + void userInduction_validUsername_printGreeting() { + String input = "john"; + Handler myHandler = new Handler(input); + myHandler.userInduction(); + String expected = "Welcome aboard, Captain john" + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Tips: Enter 'help' to view the pilot manual!" + + System.lineSeparator() + + "Initiating FTL jump sequence..." + + System.lineSeparator() + + "FTL jump completed." + + System.lineSeparator(); + assertEquals(outContent.toString(), expected); + } + + /** + * Tests the handleWorkout function's behaviour when an invalid string is passed. + * Verifies that an error message is printed. + */ + @Test + void handleWorkout_invalidInput_expectsErrorMessagePrinted() { + Handler myHandler = new Handler(); + myHandler.handleWorkout("boo"); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } + + /** + * Tests the handleHealth function's behaviour when an invalid string is passed. + * Verifies that an error message is printed. + */ + @Test + void handleHealth_invalidInput_expectsErrorMessagePrinted() { + Handler myHandler = new Handler(); + myHandler.handleWorkout("boo"); + assertTrue(errContent.toString().contains("\u001b[31mException Caught!")); + } +} diff --git a/src/test/java/ui/IntegrationTest.java b/src/test/java/ui/IntegrationTest.java new file mode 100644 index 0000000000..1761ad1246 --- /dev/null +++ b/src/test/java/ui/IntegrationTest.java @@ -0,0 +1,512 @@ +package ui; + +import constants.ErrorConstant; +import constants.HealthConstant; +import constants.WorkoutConstant; +import health.HealthList; +import health.Period; +import utility.CustomExceptions; +import utility.Parser; +import workouts.Gym; +import workouts.Run; +import workouts.WorkoutLists; +import helper.TestHelper; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class IntegrationTest { + + + private static final ByteArrayInputStream inContent = new ByteArrayInputStream("".getBytes()); + private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private static final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private static final InputStream originalIn = System.in; + private static final PrintStream originalOut = System.out; + private static final PrintStream originalErr = System.err; + + @BeforeAll + public static void setUpStreams() { + System.setIn(inContent); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + @AfterEach + public void tearDown(){ + WorkoutLists.clearWorkoutsRunGym(); + HealthList.clearHealthLists(); + outContent.reset(); + errContent.reset(); + } + + @AfterAll + public static void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + System.setIn(originalIn); + } + + /** + * Integration testing for adding a Bmi and Show latest when error is present + */ + @Test + void addBmiAndShowLatest_incorrectInput_expectErrors(){ + // Craft the input String to be passed to handler + StringBuilder inputString = new StringBuilder(); + StringBuilder expectedString = new StringBuilder(); + + // test to see if incorrect health input can be captured + inputString.append("health /h:bmiiii /height:1.75 /weight:70.00000 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_HEALTH_INPUT_ERROR)); + + inputString.append("health /h: /height:1.75 /weight:70.00 /date:22.11.2001"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_HEALTH_INPUT_ERROR)); + + inputString.append("health /h:123123 /height:1.75 /weight:70.00 /date:22.11.2001"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_HEALTH_INPUT_ERROR)); + + // test to see if incorrect command can be captured + inputString.append("healthsssss /h:bmiiii /height:1.75 /weight:70.00000 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_COMMAND_ERROR)); + + + + + // test to see if incorrect slashes can be captured + inputString.append("health /h:bmi /height:1.750000 /////weight:70.00 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.TOO_MANY_SLASHES_ERROR)); + + // test to see if incorrect height can be captured + inputString.append("health /h:bmi /height:1.750000 /weight:70.00 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR)); + + // test to see if incorrect weight can be captured + inputString.append("health /h:bmi /height:1.75 /weight:70.00000 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR)); + + // test to see if date error can be captured (invalid date) + inputString.append("health /h:bmi /height:1.75 /weight:70.00 /date:00-00-0000"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_YEAR_ERROR)); + + // test to see if date error can be captured (invalid date) + inputString.append("health /h:bmi /height:1.75 /weight:70.00 /date:30-00-0000"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_YEAR_ERROR)); + + // test to see if date error can be captured (invalid date) + inputString.append("health /h:bmi /height:1.75 /weight:70.00 /date:30-13-2000"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_DATE_ERROR)); + + // test to see if date error can be captured (invalid date) + inputString.append("health /h:bmi /height:1.75 /weight:70.00 /date:33-11-2001"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_DATE_ERROR)); + + // test to see if incorrect date can be captured + inputString.append("health /h:bmi /height:1.75 /weight:70.00 /date:22.11.2001"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_DATE_ERROR)); + + + + inputString.append("health /h:bmi /height:1.75000 /weight:70.00 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR)); + + inputString.append("health /h:bmi /height:1.75 /weight:70.00000 /date:18-03-2024"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR)); + + + inputString.append("latest /item:invalidFlag"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER)); + + inputString.append("latest /////item:"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInvalidInputString(ErrorConstant.TOO_MANY_SLASHES_ERROR)); + + inputString.append("latest /item:"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInsufficientInput(ErrorConstant.INSUFFICIENT_LATEST_FILTER_ERROR)); + + inputString.append("latest"); + inputString.append(System.lineSeparator()); + expectedString.append(TestHelper.errorInsufficientInput(ErrorConstant.INSUFFICIENT_LATEST_FILTER_ERROR)); + + // Run the process to test the output + Handler handler= new Handler(inputString.toString()); + handler.processInput(); + assertEquals(expectedString.toString(), errContent.toString()); + outContent.reset(); + } + + + /** + * Test the behaviour of print latest when given the filter period/bmi/appointment + * This is the flow of sequence + * 1. Add a bmi (bmi1) + * 2. Print latest bmi (prints bmi1) + * 3. Add a bmi (bmi2) + * 4. Print latest bmi (prints bmi2) - latest bmi should be bmi2 + * 5. Add a period (period1) + * 6. Print latest period (prints period1) + * 7. Add a period (period2) + * 8. Print latest period (prints period2) - latest period should be period2 + * 9. Add an appointment (appointment1) + * 10. Print latest appointment (prints appointment1) + * 11. Add an appointment (appointment2) that is earlier than appointment1 + * 12. Print latest appointment (prints appointment2) - latest appointment should be appointment1 still + * 13. Add an appointment (appointment3) that is the latest (2026) + * 14. Print latest appointment (prints appointment2) - latest appointment should be appointment 3 + */ + @Test + void addHealthAndShowLatest_correctInput_expectCorrectLatestOutput() { + + // Craft the input String to be passed to handler + String inputString = "health /h:bmi /height:1.75 /weight:70.00 /date:18-03-2024" + + System.lineSeparator() + + "latest /item:bmi" + + System.lineSeparator() + + "health /h:bmi /height: 2.00 /weight:40.00 /date:20-03-2024" + + System.lineSeparator() + + "latest /item:bmi" + + System.lineSeparator() + + "health /h:period /start:18-12-2023 /end:26-12-2023" + + System.lineSeparator() + + "latest /item:period" + + System.lineSeparator() + + "health /h:period /start:27-01-2024 /end:03-03-2024" + + System.lineSeparator() + + "latest /item:period" + + System.lineSeparator() + + "health /h:appointment /date:29-04-2025 /time:12:00 /description:knee surgery" + + System.lineSeparator() + + "latest /item:appointment" + + System.lineSeparator() + + "health /h:appointment /date:25-03-2024 /time:23:01 /description:knee surgery 2" + + System.lineSeparator() + + "latest /item:appointment" + + System.lineSeparator() + + "health /h:appointment /date:25-03-2026 /time:10:01 /description:plastic surgery" + + System.lineSeparator() + + "latest /item:appointment"; + + + // Craft the expected String to be printed + StringBuilder expectedString = new StringBuilder(); + + String addBmiString = TestHelper.addBmiOutputString("70.00", + "1.75", + "2024-03-18", + 22.86, + HealthConstant.NORMAL_WEIGHT_MESSAGE); + + String latestBmiString = TestHelper.latestBmiOutputString("2024-03-18", + 22.86, + HealthConstant.NORMAL_WEIGHT_MESSAGE); + + + String addBmiString2 = TestHelper.addBmiOutputString("40.00", + "2.00", + "2024-03-20", + 10.00, + HealthConstant.UNDERWEIGHT_MESSAGE); + + String latestBmiString2 = TestHelper.latestBmiOutputString("2024-03-20", + 10.00, + HealthConstant.UNDERWEIGHT_MESSAGE); + + String addPeriodString = TestHelper.addPeriodOutputString("2023-12-18", + "2023-12-26", + 9 + ); + + String latestPeriodString = TestHelper.latestPeriodOutputString("2023-12-18", + "2023-12-26", + 9, + HealthConstant.DAYS_MESSAGE); + + String addPeriodString2 = TestHelper.addPeriodOutputString("2024-01-27", + "2024-03-03", + 37 + ); + + String latestPeriodString2 = TestHelper.latestPeriodOutputString("2024-01-27", + "2024-03-03", + 37, + HealthConstant.DAYS_MESSAGE); + + String addAppointmentString = TestHelper.addAppointmentString("2025-04-29", + "12:00", + "knee surgery"); + + String latestAppointmentString = TestHelper.latestAppointmentOutputString("2025-04-29", + "12:00", + "knee surgery"); + + String addAppointmentString2 = TestHelper.addAppointmentString("2024-03-25", + "23:01", + "knee surgery 2"); + + TestHelper.latestAppointmentOutputString("2024-03-25", + "23:01", + "knee surgery 2"); + + String addAppointmentString3 = TestHelper.addAppointmentString("2026-03-25", + "10:01", + "plastic surgery"); + + String latestAppointmentString3 = TestHelper.latestAppointmentOutputString("2026-03-25", + "10:01", + "plastic surgery"); + + + expectedString.append(addBmiString); + expectedString.append(latestBmiString); + expectedString.append(addBmiString2); + expectedString.append(latestBmiString2); + expectedString.append(addPeriodString); + expectedString.append(latestPeriodString); + expectedString.append(addPeriodString2); + expectedString.append(latestPeriodString2); + expectedString.append(addAppointmentString); + expectedString.append(latestAppointmentString); + expectedString.append(addAppointmentString2); + expectedString.append(latestAppointmentString); // the latest appointment is earlier so it should print first + expectedString.append(addAppointmentString3); + expectedString.append(latestAppointmentString3); // the latest appointment is earlier then appointment3 + + // Run the process to test the output + Handler handler= new Handler(inputString); + handler.processInput(); + assertEquals(expectedString.toString(), outContent.toString()); + outContent.reset(); + + } + + + /** + * Tests the behaviour of having the same expected output when saving and loading a Gym object. + * This is tested by ensuring the print history of the Gym object is the same before and after saving and loading. + */ + @Test + void testSaveAndLoadGym_gymObjectInput_expectSamePrintHistory(){ + Gym newGym = new Gym(); + try { + + newGym.addStation("ExerciseA", "1", "10", "1.0"); + newGym.addStation("ExerciseB", "2", "20" , "1.0,2.0"); + + // Save the expected output + Output output = new Output(); + output.printAddGym(newGym); + String expectedString = outContent.toString(); + + // Save the string, clear the static list, and then simulate load + String saveString = newGym.toFileString(); + tearDown(); + Parser parser = new Parser(); + Gym loadedGym = parser.parseGymFileInput(saveString); + output.printAddGym(loadedGym); + String outputContent = outContent.toString(); + + // Expect the same value + assertEquals(expectedString, outputContent); + + + } catch (CustomExceptions.InvalidInput | CustomExceptions.FileReadError | CustomExceptions.InsufficientInput e){ + fail("Should not throw an exception"); + } + + } + + /** + * Tests if the output of the bot when adding runs and gyms, using history and latest commands is correct. + * Two gyms and runs are added, followed by the history and latest commands to view it. + */ + @Test + void testLatestDisplay_userInputsTwoGymAndRuns_expectsLatestGymAndRun(){ + String run1 = "WORKOUT /e:run /d:10.30 /t:40:10 /date:15-03-2024"; + String run2 = "WORKOUT /e:run /d:11.59 /t:30:10 /date:17-03-2024"; + String gym1 = "WORKOUT /e:gym /n:2 /date:18-03-2024"; + String gym1Station1 = "benchpress /s:2 /r:4 /w:40,60"; + String gym1Station2 = "squats /s:3 /r:4 /w:10,20,30"; + String gym2 = "WORKOUT /e:gym /n:1 /date:22-03-2024"; + String gym2Station1 = "deadlift /s:4 /r:4 /w:120,130,140,160"; + String showLatestGym = "LATEST /item:gym"; + String showLatestRun = "LATEST /item:run"; + String showHistoryGym = "HISTORY /item:gym"; + String showHistoryRun = "HISTORY /item:run"; + String showHistoryAll = "HISTORY /item:workouts"; + + + String inputString = run1 +System.lineSeparator() + + run2 +System.lineSeparator() + + gym1 + System.lineSeparator() + + gym1Station1 + System.lineSeparator() + + gym1Station2 + System.lineSeparator() + + gym2 + System.lineSeparator() + + gym2Station1 + System.lineSeparator() + + showLatestGym + System.lineSeparator() + + showLatestRun + System.lineSeparator() + + showHistoryGym + System.lineSeparator() + + showHistoryRun + System.lineSeparator() + + showHistoryAll + System.lineSeparator(); + + Handler newHandler = new Handler(inputString); + newHandler.processInput(); + String result = outContent.toString(); + tearDown(); + + // Craft expected output + try{ + Run run1Expected = new Run("40:10", "10.30", "15-03-2024"); + Run run2Expected = new Run("30:10", "11.59", "17-03-2024"); + + Gym gym1expected = new Gym("18-03-2024"); + gym1expected.addStation("benchpress", "2", "4", "40.0,60.0"); + gym1expected.addStation("squats", "3", "4", "10.0,20.0,30.0"); + + Gym gym2expected = new Gym("22-03-2024"); + gym2expected.addStation("deadlift", "4", + "4", "120.0,130.0,140.0,160.0"); + + Output output = new Output(); + output.printAddRun(run1Expected); + output.printAddRun(run2Expected); + output.printGymStationPrompt(1); + output.printGymStationPrompt(2); + output.printAddGym(gym1expected); + output.printGymStationPrompt(1); + output.printAddGym(gym2expected); + output.printLatestGym(); + output.printLatestRun(); + output.printHistory(WorkoutConstant.GYM); + output.printHistory(WorkoutConstant.RUN); + output.printHistory(WorkoutConstant.ALL); + + String expected = outContent.toString(); + assertEquals(expected, result); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e){ + fail("Shouldn't have failed"); + } + + tearDown(); + } + + /** + * Tests the behaviour of the bot when 4 Period objects are added, expects the four periods to be reflected. + * accordingly with a valid prediction on when the next cycle is. + */ + @Test + void testPrediction_userInputsFourPeriods_expectPrediction() throws CustomExceptions.InsufficientInput + , CustomExceptions.OutOfBounds { + String period1 = "health /h:period /start:18-12-2023 /end:26-12-2023"; + String period2 = "health /h:period /start:18-01-2024 /end:26-01-2024"; + String period3 = "health /h:period /start:21-02-2024 /end:28-02-2024"; + String period4 = "health /h:period /start:22-03-2024 /end:29-03-2024"; + String prediction = "health /h:prediction"; + + String inputString = period1 + System.lineSeparator() + + period2 +System.lineSeparator() + + period3 + System.lineSeparator() + + period4 + System.lineSeparator() + + prediction + System.lineSeparator(); + + + Handler newHandler = new Handler(inputString); + newHandler.processInput(); + String result = outContent.toString(); + tearDown(); + + Output output = new Output(); + + Period expectedPeriod1 = new Period("18-12-2023" , "26-12-2023"); + output.printAddPeriod(expectedPeriod1); + + Period expectedPeriod2 = new Period("18-01-2024" , "26-01-2024"); + output.printAddPeriod(expectedPeriod2); + + Period expectedPeriod3 = new Period("21-02-2024", "28-02-2024"); + output.printAddPeriod(expectedPeriod3); + + Period expectedPeriod4 = new Period("22-03-2024", "29-03-2024"); + output.printAddPeriod(expectedPeriod4); + + Parser parser = new Parser(); + parser.parsePredictionInput(); + + String expected = outContent.toString(); + assertEquals(expected, result); + + } + + /** + * Tests the behaviour of the bot when 3 Period objects are added and a prediction is attempted. + * Expects an exception thrown for prediction since there are insufficient Period objects added. + */ + @Test + void testPrediction_userInputsThreePeriods_expectNoPredictionPrintedAndErrorMessagePrinted() { + String period1 = "health /h:period /start:18-12-2023 /end:26-12-2023"; + String period2 = "health /h:period /start:18-01-2024 /end:26-01-2024"; + String period3 = "health /h:period /start:21-02-2024 /end:28-02-2024"; + String prediction = "health /h:prediction"; + + String inputString = period1 +System.lineSeparator() + + period2 + System.lineSeparator() + + period3 + System.lineSeparator() + + prediction + System.lineSeparator(); + + Handler newHandler = new Handler(inputString); + Output output = new Output(); + newHandler.processInput(); + String result = outContent.toString(); + String resultErr = errContent.toString(); + tearDown(); + + Period expectedPeriod1 = new Period("18-12-2023" , "26-12-2023"); + output.printAddPeriod(expectedPeriod1); + Period expectedPeriod2 = new Period("18-01-2024" , "26-01-2024"); + output.printAddPeriod(expectedPeriod2); + Period expectedPeriod3 = new Period("21-02-2024", "28-02-2024"); + output.printAddPeriod(expectedPeriod3); + + String expected = outContent.toString(); + assertEquals(expected, result); + + // expect error message + try { + Parser parser = new Parser(); + parser.parsePredictionInput(); + } catch (CustomExceptions.InsufficientInput | CustomExceptions.OutOfBounds e) { + output.printException(e.getMessage()); + } + String expectedErr = errContent.toString(); + assertEquals(expectedErr, resultErr); + } + + + +} diff --git a/src/test/java/ui/OutputTest.java b/src/test/java/ui/OutputTest.java new file mode 100644 index 0000000000..0411d50ad6 --- /dev/null +++ b/src/test/java/ui/OutputTest.java @@ -0,0 +1,576 @@ +package ui; + +import constants.HealthConstant; +import health.Appointment; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; + +import constants.ErrorConstant; +import constants.UiConstant; +import helper.TestHelper; +import utility.CustomExceptions; +import constants.WorkoutConstant; +import workouts.Gym; +import workouts.Run; +import workouts.WorkoutLists; +import health.Bmi; +import health.Period; +import health.HealthList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterAll; + +class OutputTest { + + + private static final ByteArrayInputStream inContent = new ByteArrayInputStream("".getBytes()); + private static final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private static final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private static final InputStream originalIn = System.in; + private static final PrintStream originalOut = System.out; + private static final PrintStream originalErr = System.err; + + @BeforeAll + public static void setUpStreams() { + System.setIn(inContent); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + } + + + @AfterEach + public void cleanup() { + WorkoutLists.clearWorkoutsRunGym(); + HealthList.clearHealthLists(); + outContent.reset(); + errContent.reset(); + } + + @AfterAll + public static void restoreStreams() { + System.setOut(originalOut); + System.setErr(originalErr); + System.setIn(originalIn); + } + + /** + * Tests the behaviour of the printHistory function for Run objects. + * + * @throws CustomExceptions.InvalidInput If there are invalid parameters specified. + */ + @Test + void printHistory_runsOnly_expectAllRunsPrinted() throws CustomExceptions.InvalidInput { + Run run1 = new Run("40:10", "10.3", "15-03-2024"); + Run run2 = new Run("01:59:10", "15.3"); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your run history:" + + System.lineSeparator() + + String.format(WorkoutConstant.RUN_HEADER_INDEX_FORMAT) + + System.lineSeparator() + + String.format(WorkoutConstant.RUN_DATA_INDEX_FORMAT, 1, run1) + + System.lineSeparator() + + String.format(WorkoutConstant.RUN_DATA_INDEX_FORMAT, 2, run2) + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + Output output = new Output(); + output.printHistory(WorkoutConstant.RUN); + assertEquals(expected, outContent.toString()); + } + + /** + * + */ + @Test + void printGreeting_correctInput_expectGreetingPrinted() { + Output output = new Output(); + String expected; + + output.printGreeting(UiConstant.FILE_FOUND, "Captain Voyager"); + expected = TestHelper.printGreetingsFoundString("Captain Voyager"); + assertEquals(expected, outContent.toString()); + cleanup(); + + output.printGreeting(UiConstant.FILE_FOUND, "Captain 123"); + expected = TestHelper.printGreetingsFoundString("Captain 123"); + assertEquals(expected, outContent.toString()); + cleanup(); + + output.printGreeting(UiConstant.FILE_NOT_FOUND, "Captain Voyager"); + expected = TestHelper.printGreetingNotFoundString(); + assertEquals(expected, outContent.toString()); + cleanup(); + } + + + /** + * Tests the behaviour of the printLatest for incorrect Input which would result in an error + * Behaviour Tested + * - invalid filter + * - empty run/gym/workouts/bmi/appointment/period list + * - empty input + */ + @Test + void printHistory_incorrectInput_throwError() { + + Output output = new Output(); + String expectedString; + + output.printHistory("invalidFilter"); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_HISTORY_FILTER_ERROR); + assertEquals(expectedString, errContent.toString()); + cleanup(); + + // printing latest of an empty run list + output.printHistory(WorkoutConstant.RUN); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.RUN_EMPTY_ERROR); + assertEquals(errContent.toString(), expectedString); + cleanup(); + + // printing latest of an empty gym list + output.printHistory(WorkoutConstant.GYM); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.GYM_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + // printing latest of an empty workout list + output.printHistory(WorkoutConstant.ALL); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.WORKOUTS_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + + // printing latest of an empty BMI list + output.printHistory(HealthConstant.BMI); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.BMI_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + // printing latest of an empty PERIOD list + output.printHistory(HealthConstant.PERIOD); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.PERIOD_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + // printing latest of an empty APPOINTMENT list + output.printHistory(HealthConstant.APPOINTMENT); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.APPOINTMENT_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + output.printHistory(""); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_HISTORY_FILTER_ERROR); + assertEquals(expectedString, errContent.toString()); + + cleanup(); + } + + + /** + * Tests the behaviour of the printLatest for incorrect Input which would result in an error + * Behaviour Tested + * - invalid filter + * - empty run/gym/bmi/appointment/period list + * - empty input + */ + @Test + void printLatest_incorrectInput_throwError() { + Output output = new Output(); + String expectedString; + + + output.printLatest("invalidFilter"); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER); + assertEquals(expectedString, errContent.toString()); + cleanup(); + + // printing latest of an empty run list + output.printLatest(WorkoutConstant.RUN); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.RUN_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + // printing latest of an empty gym list + output.printLatest(WorkoutConstant.GYM); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.GYM_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + + // printing latest of an empty BMI list + output.printLatest(HealthConstant.BMI); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.BMI_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + // printing latest of an empty PERIOD list + output.printLatest(HealthConstant.PERIOD); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.PERIOD_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + // printing latest of an empty APPOINTMENT list + output.printLatest(HealthConstant.APPOINTMENT); + expectedString = TestHelper.errorOutOfBoundsString(ErrorConstant.APPOINTMENT_EMPTY_ERROR); + assertTrue(errContent.toString().contains(expectedString)); + cleanup(); + + output.printLatest(""); + expectedString = TestHelper.errorInvalidCommandString(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER); + assertEquals(expectedString, errContent.toString()); + + cleanup(); + + } + + /** + * Tests the behaviour of the printLatestRun function after a Run object is added. + * + * @throws CustomExceptions.InvalidInput If there are invalid parameters specified. + */ + @Test + void printLatestRun_oneRun_expectOneRunPrinted() throws CustomExceptions.InvalidInput { + Run newRun = new Run("40:10", "10.3"); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your latest run:" + + System.lineSeparator() + + String.format(WorkoutConstant.RUN_HEADER_INDEX_FORMAT) + + System.lineSeparator() + + String.format(WorkoutConstant.RUN_DATA_INDEX_FORMAT, 1, newRun) + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + + Output output = new Output(); + output.printLatestRun(); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printLatestRun function when no Runs are added. + */ + @Test + void printLatestRun_noRun_expectNoRunMessage() { + String expected = "\u001b[31mException Caught!" + + System.lineSeparator() + + "\u001b[31mOut of Bounds Error: " + + ErrorConstant.RUN_EMPTY_ERROR + + "\u001b[0m\u001b[0m" + + System.lineSeparator(); + Output output = new Output(); + output.printLatestRun(); + assertTrue(errContent.toString().contains(expected)); + } + + /** + * Tests the behaviour of the printLatestGym function when two Gyms are added. + */ + @Test + void printLatestGym_twoGyms_expectOneGymPrinted() { + + try { + Gym gym1 = new Gym(); + gym1.addStation("Bench Press", "1", "10", "1.0"); + gym1.addStation("Shoulder Press", "2", "10", "1.0,2.0"); + + Gym gym2 = new Gym(); + gym2.addStation("Squat Press", "1", "50", "1.0"); + gym2.addStation("Lat Press", "2", "10", "1.0,2.0"); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Shouldn't have failed"); + } + + + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your latest gym:" + + System.lineSeparator() + + "Gym Session 2 (Date: NA)" + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_STATION_FORMAT, "Station 1 Squat Press") + + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, 1) + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 1, "50 reps at 1.000 KG") + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_STATION_FORMAT, "Station 2 Lat Press") + + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, 2) + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 1, "10 reps at 1.000 KG") + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 2, "10 reps at 2.000 KG") + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + Output output = new Output(); + output.printLatestGym(); + + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printLatestGym function when no Gyms are added. + */ + @Test + void printLatestGym_noGym_expectNoGymMessage() { + String expected = "\u001b[31mException Caught!" + + System.lineSeparator() + + "\u001b[31mOut of Bounds Error: " + + ErrorConstant.GYM_EMPTY_ERROR + + "\u001b[0m\u001b[0m" + + System.lineSeparator(); + Output output = new Output(); + output.printLatestGym(); + assertTrue(errContent.toString().contains(expected)); + } + + /** + * Tests the behaviour of the printLatestBmi function when two Bmi objects are added. + */ + @Test + void printLatestBmi_twoBmis_expectOneBmiPrinted() { + new Bmi("1.75", "70.0", "18-03-2024"); + new Bmi("1.55", "55.0", "20-03-2024"); + + Output output = new Output(); + output.printLatestBmi(); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "2024-03-20" + + System.lineSeparator() + + "Your BMI is 22.89" + + System.lineSeparator() + + "Great! You're within normal range." + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printLatestBmi function when two Period objects are added. + */ + @Test + void printLatestPeriod_twoPeriods_expectOnePeriodPrinted() { + new Period("09-02-2023", "16-02-2023"); + new Period("09-03-2023", "16-03-2023"); + + + Output output = new Output(); + output.printLatestPeriod(); + + + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Period Start: 2023-03-09 Period End: 2023-03-16" + + System.lineSeparator() + + "Period Length: 8 day(s)" + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printLatestAppointment function when two Appointment objects are added. + */ + @Test + void printLatestAppointment_twoAppointments_expectOneAppointmentPrinted() { + new Appointment("29-03-2025", "17:00", "test"); + new Appointment("24-01-2026", "12:00", "test2"); + + + Output output = new Output(); + output.printLatestAppointment(); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "On 2026-01-24 at 12:00: test2" + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of printAppointmentHistory when two Appointment objects are added. + * Expects two Appointment objects to be printed. + * + * @throws CustomExceptions.OutOfBounds If there is out of bounds access. + */ + @Test + void printAppointmentHistory_twoAppointments_expectTwoAppointmentsPrinted() throws + CustomExceptions.OutOfBounds { + new Appointment("29-03-2024", "17:00", "test"); + new Appointment("24-01-2026", "12:00", "test2"); + + + Output output = new Output(); + output.printAppointmentHistory(); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your Appointment history:" + + System.lineSeparator() + + "1. On 2024-03-29 at 17:00: test" + + System.lineSeparator() + + "2. On 2026-01-24 at 12:00: test2" + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printPeriodHistory function when two Period objects are added. + */ + @Test + void printPeriodHistory_twoPeriods_expectTwoPeriodsPrinted() throws + CustomExceptions.OutOfBounds { + new Period("09-02-2023", "16-02-2023"); + new Period("09-03-2023", "16-03-2023"); + + + Output output = new Output(); + output.printPeriodHistory(); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your Period history:" + + System.lineSeparator() + + "1. Period Start: 2023-03-09 Period End: 2023-03-16" + + System.lineSeparator() + + "Period Length: 8 day(s)" + + System.lineSeparator() + + "2. Period Start: 2023-02-09 Period End: 2023-02-16" + + System.lineSeparator() + + "Period Length: 8 day(s)" + + System.lineSeparator() + + "Cycle Length: 28 day(s)" + + System.lineSeparator() + + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + + assertEquals(expected, outContent.toString()); + } + + /** + * Tests the behaviour of the printLatestBmi function when two Bmi objects are added. + */ + @Test + void printBmiHistory_twoBmis_expectTwoBmisPrinted() throws CustomExceptions.OutOfBounds { + new Bmi("1.75", "70.0", "18-03-2024"); + new Bmi("1.55", "55.0", "20-03-2024"); + + Output output = new Output(); + output.printBmiHistory(); + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your BMI history:" + + System.lineSeparator() + + "1. 2024-03-20" + + System.lineSeparator() + + "Your BMI is 22.89" + + System.lineSeparator() + + "Great! You're within normal range." + + System.lineSeparator() + + "2. 2024-03-18" + + System.lineSeparator() + + "Your BMI is 22.86" + + System.lineSeparator() + + "Great! You're within normal range." + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + assertEquals(expected, outContent.toString()); + } + + + /** + * Tests the behaviour of the printGymHistory function, which should print both Gyms added. + */ + @Test + void printGymHistory_correctInput_expectPrintGymHistory() { + + try { + Gym gym1 = new Gym(); + gym1.addStation("Bench Press", "1", "50", "1.0"); + gym1.addStation("Shoulder Press", "2", "10", "1.0,2.0"); + + Gym gym2 = new Gym(); + gym2.addStation("Squat Press", "1", "50", "1.0"); + gym2.addStation("Lat Press", "2", "10", "1.0,2.0"); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Shouldn't have failed"); + } + + String expected = UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Your gym history:" + + System.lineSeparator() + + "Gym Session 1 (Date: NA)" + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_STATION_FORMAT, "Station 1 Bench Press") + + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, 1) + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 1, "50 reps at 1.000 KG") + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_STATION_FORMAT, "Station 2 Shoulder Press") + + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, 2) + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 1, "10 reps at 1.000 KG") + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 2, "10 reps at 2.000 KG") + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator() + + "Gym Session 2 (Date: NA)" + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_STATION_FORMAT, "Station 1 Squat Press") + + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, 1) + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 1, "50 reps at 1.000 KG") + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_STATION_FORMAT, "Station 2 Lat Press") + + String.format(WorkoutConstant.INDIVIDUAL_GYM_STATION_FORMAT, 2) + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 1, "10 reps at 1.000 KG") + + System.lineSeparator() + + String.format(WorkoutConstant.GYM_SET_INDEX_FORMAT, 2, "10 reps at 2.000 KG") + + System.lineSeparator() + + UiConstant.PARTITION_LINE + + System.lineSeparator(); + Output output = new Output(); + output.printHistory(WorkoutConstant.GYM); + assertEquals(expected, outContent.toString()); + + } + + /** + * Test the behaviour of the printRunHistory function, which should print both Runs and Gyms. + */ + @Test + void printWorkoutHistory() { + try { + new Run("01:11:12", "10.24", "19-12-1999"); + Gym gym1 = new Gym("11-11-1992"); + gym1.addStation("Bench Press", "2", "4", "10.0,20.0"); + gym1.addStation("Squat Press", "2", "4", "100.0,200.0"); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Shouldn't have failed"); + } + } + +} diff --git a/src/test/java/utility/ParserTest.java b/src/test/java/utility/ParserTest.java new file mode 100644 index 0000000000..24315b0cc0 --- /dev/null +++ b/src/test/java/utility/ParserTest.java @@ -0,0 +1,457 @@ +package utility; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import workouts.Gym; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.LocalTime; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ParserTest { + private static final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private static final PrintStream originalErr = System.err; + private Parser parser; + @BeforeAll + public static void setUpStreams() { + System.setErr(new PrintStream(errContent)); + } + + @AfterAll + public static void restoreStreams() { + System.setErr(originalErr); + } + @BeforeEach + void setUp() { + parser = new Parser(); + } + + /** + * Tests the behaviour of the parseDate function with a correctly formatted string. + */ + @Test + void parseDate_correctDateInput_returnDate() { + LocalDate result = parser.parseDate("08-03-2024"); + LocalDate expected = LocalDate.of(2024, 3, 8); + assertEquals(expected, result); + } + + /** + * Tests the behaviour of the parseDate function with an incorrectly formatted string. + * Expects null to be returned. + */ + @Test + void parseDate_incorrectDateInput_returnNull () { + String input = "2024-03-08"; + LocalDate result = parser.parseDate(input); + assertNull(result); + } + + /** + * Tests the behaviour of parseFormattedDate when valid LocalDate variable is passed. + * Expects correct string date returned. + */ + @Test + void parseFormattedDate_correctDate_returnStringDate() { + LocalDate date = LocalDate.of(2024, 4, 10); + String result = parser.parseFormattedDate(date); + String expected = "10-04-2024"; + assertEquals(expected, result); + } + + /** + * Tests the behaviour of parseFormattedDate when null LocalDate variable is passed. + * Expects "NA" returned + */ + @Test + void parseFormattedDate_nullDate_returnNoDateString() { + String result = parser.parseFormattedDate(null); + String expected = "NA"; + assertEquals(expected, result); + } + + /** + * Tests the behaviour of parseTime when a valid time string is passed. + * Expects correct LocalTime returned. + */ + @Test + void parseTime_validTime_returnCorrectTime() { + LocalTime result = parser.parseTime("23:34"); + LocalTime expected = LocalTime.of(23, 34); + assertEquals(expected, result); + } + + /** + * Tests the behaviour of parseTime when a valid time string is passed. + * Expects correct LocalTime returned. + */ + @Test + void parseTime_invalidTime_returnCorrectTime() { + parser.parseTime("60:34"); + String expected = "\u001b[31mException Caught!"; + assertTrue(errContent.toString().contains(expected)); + } + + + /** + * Tests the behaviour of correct parameters being passed to validateDate. + * Expects the correct details to be returned as a list of strings. + */ + @Test + public void splitDeleteInput_correctInput_returnsCorrectDeleteValues() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/item:appointment /index:1"; + String[] expected = {"appointment", "1"}; + String[] result = parser.splitDeleteInput(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of insufficient parameters being passed to validateDate. + * Expects InvalidInput exception to be thrown. + */ + @Test + public void splitDeleteInput_missingParameter_throwsInsufficientParameterException() { + String input = "/item:appointment"; + assertThrows(CustomExceptions.InsufficientInput.class, () -> parser.splitDeleteInput(input)); + } + + /** + * Tests the behaviour of splitDeleteInput when too many forward slashes have been specified. + * Expects InvalidInput exception to be thrown. + */ + @Test + public void splitDeleteInput_tooManyForwardSlashes_throwsInvalidInputException() { + String input = "/item:appointment /index:1/"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.splitDeleteInput(input)); + } + + /** + * Tests the behaviour of parseDeleteInput when correct parameters are passed. + * Expects correct parameters returned. + */ + @Test + public void parseDeleteInput_validParameters_expectValidDetailsReturned() { + String input = "/item:appointment /index:1"; + String[] expected = {"appointment", "1"}; + String[] result = parser.parseDeleteInput(input); + assertArrayEquals(expected, result); + } + + + //@@author j013n3 + /** + * Tests the behaviour of a correctly formatted user input being passed into splitBmiInput. + * Expects no exception to be thrown. + */ + @Test + void splitBmiInput_correctInput_returnsCorrectBmiValues() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/h:bmi /height:1.71 /weight:60.50 /date:19-03-2024"; + String[] expected = {"1.71", "60.50", "19-03-2024"}; + String[] result = parser.splitBmiInput(input); + assertArrayEquals(expected, result); + } + + + /** + * Tests the behaviour of a string with missing parameter being passed into splitBmiInput. + * Expects InsufficientInput exception to be thrown. + */ + @Test + void splitBmiInput_missingParameter_throwsInsufficientInputException() { + String input = "/h:bmi /height:1.71 /date:19-03-2024"; + assertThrows(CustomExceptions.InsufficientInput.class, () -> parser.splitBmiInput(input)); + } + + /** + * Tests the behaviour of too many forward slashes being passed into splitBmiInput. + * Expects InvalidInput exception to be thrown. + */ + @Test + void splitBmiInput_tooManyForwardSlashes_throwsInvalidInputException() { + String input = "/h:bmi /height:1.71 /weight:80.00 /date:19-03-2024 /"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.splitBmiInput(input)); + } + + + /** + * Tests the behaviour of a correctly formatted string being passed into splitPeriodInput. + * Expects no exception to be thrown. + */ + @Test + void splitPeriodInput_correctInput_noExceptionThrown() throws CustomExceptions.InvalidInput, + CustomExceptions.InsufficientInput { + String input = "/h:period /start:29-04-2023 /end:30-04-2023"; + String[] expected = {"29-04-2023", "30-04-2023"}; + String[] result = parser.splitPeriodInput(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of a string with a missing parameter being passed into splitPeriodInput. + * Expects InsufficientInput exception to be thrown. + */ + @Test + void splitPeriodInput_missingParameter_throwsInsufficientInputException() { + String input = "/h:period /end:29-04-2023"; + assertThrows(CustomExceptions.InsufficientInput.class, () -> parser.splitPeriodInput(input)); + } + + //@@author syj02 + /** + * Tests the behaviour of a correctly formatted string being passed into splitAppointmentInput. + * Expects no exception to be thrown. + */ + @Test + void splitAppointmentInput_correctInput_noExceptionThrown() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/h:appointment /date:30-03-2024 /time:19:30 /description:test"; + String[] expected = {"30-03-2024", "19:30", "test"}; + String[] result = parser.splitAppointmentDetails(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of a correctly formatted string being passed into splitAppointmentDetails. + * Expects InsufficientInput exception to be thrown. + */ + @Test + void splitAppointmentInput_missingParameter_throwsInsufficientInputException() { + String input = "/h:appointment /date:30-03-2024 /description:test"; + assertThrows(CustomExceptions.InsufficientInput.class, () -> parser.splitAppointmentDetails(input)); + } + + /** + * Tests the behaviour of too many forward slashes being passed into splitAppointmentDetails. + * Expects InvalidInput exception to be thrown. + */ + @Test + void splitAppointmentInput_tooManyForwardSlashes_throwsInvalidInputException() { + String input = "/h:appointment /date:30-03-2024 /time:19:30 /description:test/"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.splitAppointmentDetails(input)); + } + + //@@author rouvinerh + /** + * Tests the behaviour of a correctly formatted string being + * passed into parseHistoryAndLatestInput. + * Expects no error thrown, and correct filter string returned. + */ + @Test + void parseHistoryAndDeleteInput_correctInput_noExceptionThrown() { + String input = "/item:appointment"; + String result = parser.parseHistory(input); + String expected = "appointment"; + assertEquals(expected, result); + } + + /** + * Tests the behaviour of an empty string being passed into parseHistoryAndLatestInput. + * Expects null to be returned. + */ + @Test + void parseHistoryAndDeleteInput_emptyString_expectsNullReturned() { + String input = "/item:"; + assertNull(parser.parseDeleteInput(input)); + } + + //@@author JustinSoh + /** + * Tests the behaviour of a correctly formatted string without + * dates being passed to splitGymInput. + * Expects the list of strings to contain the correct parameters. + * + * @throws CustomExceptions.InsufficientInput If there is insufficient input. + */ + @Test + void splitGymInput_correctInputWithoutDate_noExceptionThrown() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/e:gym /n:3"; + String[] expected = {"3", null}; + String[] result = parser.splitGymInput(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of a correctly formatted string being passed to splitGymInput. + * Expects the list of strings to contain the correct parameters. + * + * @throws CustomExceptions.InsufficientInput If there is insufficient input. + */ + @Test + void splitGymInput_correctInputWithDate_noExceptionThrown() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/e:gym /n:3 /date:29-03-2024"; + String[] expected = {"3", "29-03-2024"}; + String[] result = parser.splitGymInput(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of an incorrectly formatted string with insufficient parameters + * being passed to splitGymInput. + * Expects an InsufficientInput exception to be thrown. + */ + @Test + void splitGymInput_incorrectInput_expectInsufficientInputExceptionThrown() { + String input = "/e:gym"; + assertThrows(CustomExceptions.InsufficientInput.class, () -> + parser.splitGymInput(input)); + } + + //@@author rouvinerh + /** + * Tests the behaviour of a correctly formatted string without dates being passed to splitGymInput. + * Expects the list of strings to contain the correct parameters. + * + * @throws CustomExceptions.InsufficientInput If there is insufficient input. + */ + @Test + void splitRunInput_correctInputWithoutDate_noExceptionThrown() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/e:run /t:25:24 /d:5.15"; + String[] expected = {"25:24", "5.15", null}; + String[] result = parser.splitRunInput(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of a correctly formatted string being passed to splitGymInput. + * Expects the list of strings to contain the correct parameters. + * + * @throws CustomExceptions.InsufficientInput If there is insufficient input. + */ + @Test + void splitRunInput_correctInputWithDate_noExceptionThrown() throws CustomExceptions.InsufficientInput, + CustomExceptions.InvalidInput { + String input = "/e:run /d:5.15 /t:25:24 /date:29-04-2024"; + String[] expected = {"25:24", "5.15", "29-04-2024"}; + String[] result = parser.splitRunInput(input); + assertArrayEquals(expected, result); + } + + /** + * Tests the behaviour of an incorrectly formatted string with insufficient parameters + * being passed to splitGymInput. + * Expects an InsufficientInput exception to be thrown. + */ + @Test + void splitRunInput_incorrectInput_expectInsufficientInputExceptionThrown() { + String input = "/e:run /d:5.10"; + assertThrows(CustomExceptions.InsufficientInput.class, () -> + parser.splitRunInput(input)); + } + + /** + * Tests the behaviour of the extractSubstringFromSpecificIndex with a correct flag. + * Expects the 'bmi' string to be extracted. + */ + @Test + void extractSubstringFromSpecificIndex_correctFlag_returnsCorrectSubstring() { + String test = "/h:bmi"; + String testDelimiter = "/h:"; + String result = parser.extractSubstringFromSpecificIndex(test, testDelimiter); + String expected = "bmi"; + assertEquals(expected, result); + } + + //@@author JustinSoh + /** + * Tests the behaviour of parseGymFileInput with the correct input. + * Ensures a Gym object is added with the correct attributes. + */ + @Test + void parseGymFileInput_correctInput_returnsGymObject() { + String input = "gym:2:11-11-1997:bench press:4:10:10,20,30,40:squats:2:5:20,30"; + String input2 = "gym:2:NA:bench press:4:10:10,20,30,40:squats:2:5:20,30"; + + try{ + Gym gymOutput = parser.parseGymFileInput(input); + Gym gymOutput2 = parser.parseGymFileInput(input2); + // make sure that there is two gym station created + assertEquals(2, gymOutput.getStations().size()); + // make sure that the date is correct + assertEquals("1997-11-11", gymOutput.getDate()); + assertEquals(gymOutput2.getDate(), "NA"); + // make sure the gym exercise names are correct + assertEquals("bench press", gymOutput.getStationByIndex(0).getStationName()); + assertEquals("squats", gymOutput.getStationByIndex(1).getStationName()); + // make sure the number of sets are correct + assertEquals(4, gymOutput.getStationByIndex(0).getNumberOfSets()); + assertEquals(2, gymOutput.getStationByIndex(1).getNumberOfSets()); + // make sure the reps of each station are correct + assertEquals(10, gymOutput.getStationByIndex(0).getSets().get(0).getNumberOfRepetitions()); + assertEquals(5, gymOutput.getStationByIndex(1).getSets().get(0).getNumberOfRepetitions()); + // make sure the weights of each station are correct + assertEquals(10, gymOutput.getStationByIndex(0).getSets().get(0).getWeight()); + assertEquals(20, gymOutput.getStationByIndex(0).getSets().get(1).getWeight()); + assertEquals(30, gymOutput.getStationByIndex(0).getSets().get(2).getWeight()); + assertEquals(40, gymOutput.getStationByIndex(0).getSets().get(3).getWeight()); + assertEquals(20, gymOutput.getStationByIndex(1).getSets().get(0).getWeight()); + assertEquals(30, gymOutput.getStationByIndex(1).getSets().get(1).getWeight()); + } catch (Exception e) { + fail("Should not throw an exception"); + } + } + + /** + * Tests the behaviour of parseGymFileInput when invalid input strings are given. + * Expects InvalidInput exception to be thrown. + */ + @Test + void parseGymFileInput_incorrectInput_throwsInvalidInputException() { + // not enough parameters + String input1 = "gym:2:11-11-1997:bench press:4:10:10,20,30,40:squats:2:5"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.parseGymFileInput(input1)); + + // blank parameters + String input2 = "gym:2:11-11-1997:bench press:4:10:10,20,30,40:squats:2:5:"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.parseGymFileInput(input2)); + + // station name too long + String input3 = "gym:2:11-11-1997:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:4:10:10,20,30,40:squats:2:5:10,20"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.parseGymFileInput(input3)); + + // station name does not follow regex + String input4 = "gym:2:11-11-1997:aa;:4:10:10,20,30,40:squats:2:5:10,20"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.parseGymFileInput(input4)); + + // non-numerical sets + String input5 = "gym:2:11-11-1997:bench press:a:10:10,20,30,40:squats:2:5:10,20"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.parseGymFileInput(input5)); + + // weights size more than number of sets + String input6 = "gym:2:11-11-1997:bench press:a:10:10,20,30,40:squats:2:5:10"; + assertThrows(CustomExceptions.InvalidInput.class, () -> parser.parseGymFileInput(input6)); + } + + // @@author rouvinerh + /** + * Tests the behaviour of correct inputs being passed to splitAndValidateGymStationInput + * Expects no exceptions thrown. + * + * @throws CustomExceptions.InvalidInput If there are invalid parameters specified. + */ + @Test + void splitAndValidateGymStationInput_validInput_correctParametersReturned() throws + CustomExceptions.InvalidInput { + String input = "Bench Press /s:2 /r:4 /w:10,20"; + String[] expected = {"Bench Press", "2", "4", "10,20"}; + String[] result = parser.splitGymStationInput(input); + assertArrayEquals(expected, result); + } + + + +} diff --git a/src/test/java/utility/ValidationTest.java b/src/test/java/utility/ValidationTest.java new file mode 100644 index 0000000000..1e0fad5d24 --- /dev/null +++ b/src/test/java/utility/ValidationTest.java @@ -0,0 +1,674 @@ +package utility; + +import constants.ErrorConstant; +import health.Bmi; +import health.Period; +import health.HealthList; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import workouts.WorkoutLists; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; + + +public class ValidationTest { + private Validation validation; + + private final ByteArrayInputStream inContent = new ByteArrayInputStream("".getBytes()); + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + private final InputStream originalIn = System.in; + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + + @BeforeEach + public void setUpStreams() { + validation = new Validation(); + System.setOut(new PrintStream(outContent)); + System.setIn(inContent); + System.setErr(new PrintStream(errContent)); + } + + @AfterEach + public void restoreStreams() { + System.setOut(originalOut); + System.setIn(originalIn); + System.setErr(originalErr); + WorkoutLists.clearWorkoutsRunGym(); + HealthList.clearHealthLists(); + } + + /** + * Tests the behaviour of isEmptyParameterPresent when an array of Strings is passed that has no empty strings. + * Expects it to return false. + */ + @Test + void isEmptyParameterPresent_nonEmptyStrings_returnsFalse() { + String[] input = {"1", "2", "3", "4"}; + assertFalse(validation.isEmptyParameterPresent(input)); + } + + /** + * Tests the behaviour of isEmptyParameterPresent when an array of Strings is passed that has empty strings. + * Expects it to return true. + */ + @Test + void isEmptyParameterPresent_nonEmptyStrings_returnsTrue() { + String[] input = {"1", "2", "3", ""}; + assertTrue(validation.isEmptyParameterPresent(input)); + } + + + /** + * Tests the behaviour of the validateDateInput function with a correctly formatted string. + * Expects no exception to be thrown. + */ + @Test + void validateDateInput_validDate_noExceptionThrown() { + String validDate = "09-11-2024"; + assertDoesNotThrow(() -> validation.validateDateInput(validDate)); + } + + /** + * Tests the behaviour of the validateDateInput function when invalid inputs + * are passed to it. + * Expects InvalidInput exception to be thrown with the correct error message printed. + */ + @Test + void validateDateInput_invalidDateInput_expectsInvalidInputExceptionWithCorrectErrorMessage() { + // invalid day format + String input1 = "9-11-2024"; + Exception exceptionThrown; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // invalid month format + String input2 = "09-1-2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // invalid year format + String input3 = "09-01-24"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // illegal day number + String input4 = "32-01-24"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input4)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // day zero + String input5 = "00-11-2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input5)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // illegal month number + String input6 = "09-13-2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input6)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // invalid delimiter + String input7 = "09/12/2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input7)); + + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + // missing year + String input8 = "09-12"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input8)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DATE_ERROR)); + + // leap year + String input9 = "29-02-2023"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input9)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_LEAP_YEAR_ERROR)); + + // year before 1967 + String input10 = "29-02-0000"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateInput(input10)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_YEAR_ERROR)); + } + + /** + * Tests the behaviour of correct parameters being passed to validateDate. + * Expects no exception to be thrown. + */ + @Test + void validateDeleteInput_correctInput_noExceptionThrown() { + String[] input = {"appointment", "2"}; + assertDoesNotThrow(() -> validation.validateDeleteInput(input)); + } + + /** + * Tests the behaviour of the validateDeleteInput function when invalid inputs are passed to it. + * Expects either InvalidInput or InsufficientInput exception to be thrown with the correct error message printed. + */ + @Test + void validateDeleteInput_incorrectInput_expectsExceptionThrownWithCorrectErrorMessage() { + // invalid item + Exception exceptionThrown; + String[] input1 = {"free!", "2"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDeleteInput(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER)); + + // invalid index + String[] input2 = {"gym", "-a"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDeleteInput(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_INDEX_ERROR)); + + // empty strings + String[] input3 = {"gym", ""}; + exceptionThrown = assertThrows(CustomExceptions.InsufficientInput.class, () -> + validation.validateDeleteInput(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INSUFFICIENT_DELETE_PARAMETERS_ERROR)); + } + + /** + * Tests the behaviour of correct filter strings being passed to validateHistoryFilter. + * Expects no exception to be thrown. + */ + @Test + void validateHistoryFilter_correctFilter_expectsNoExceptionThrown() { + assertDoesNotThrow(() -> validation.validateHistoryFilter("gym")); + assertDoesNotThrow(() -> validation.validateHistoryFilter("run")); + assertDoesNotThrow(() -> validation.validateHistoryFilter("bmi")); + assertDoesNotThrow(() -> validation.validateHistoryFilter("period")); + assertDoesNotThrow(() -> validation.validateHistoryFilter("appointment")); + assertDoesNotThrow(() -> validation.validateHistoryFilter("workouts")); + } + + /** + * Tests the behaviour of incorrect filter strings being passed to validateHistoryFilter. + * Expects InvalidInput exception to be thrown with correct error message. + */ + @Test + void validateHistoryFilter_incorrectFilter_expectsInvalidInputExceptionWithCorrectErrorMessage() { + Exception exceptionThrown; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateHistoryFilter("foo")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_HISTORY_FILTER_ERROR)); + } + + /** + * Tests the behaviour of correct filter strings being passed to validateDeleteAndLatestFilter. + * Expects no exception to be thrown. + */ + @Test + void validateDeleteAndLatestFilter_correctFilter_expectsNoExceptionThrown() { + assertDoesNotThrow(() -> validation.validateDeleteAndLatestFilter("gym")); + assertDoesNotThrow(() -> validation.validateDeleteAndLatestFilter("run")); + assertDoesNotThrow(() -> validation.validateDeleteAndLatestFilter("bmi")); + assertDoesNotThrow(() -> validation.validateDeleteAndLatestFilter("period")); + assertDoesNotThrow(() -> validation.validateDeleteAndLatestFilter("appointment")); + } + + /** + * Tests the behaviour of incorrect filter strings being passed to validateDeleteAndLatestFilter. + * Expects InvalidInput exception to be thrown with correct error message. + */ + @Test + void validateDeleteAndLatestFilter_incorrectFilter_expectsInvalidInputExceptionWithCorrectErrorMessage() { + Exception exceptionThrown; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDeleteAndLatestFilter("foo")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_LATEST_OR_DELETE_FILTER)); + } + + /** + * Tests the behaviour of correct parameters being passed into validateBmi. + * Expects no exceptions to be thrown. + */ + @Test + void validateBmiInput_correctParameters_noExceptionThrown() { + String[] input = {"1.71", "70.00", "22-02-2024"}; + assertDoesNotThrow(() -> validation.validateBmiInput(input)); + } + + /** + * Tests the behaviour of the validateBmiInput function when invalid inputs are passed to it. + * Expects either InsufficientInput or InvalidInput exception to be thrown with the correct error message + * printed. + */ + @Test + void validateBmiInput_incorrectInputs_expectsExceptionThrownWithCorrectErrorMessage() { + Exception exceptionThrown; + // 1 decimal point weight + String[] input1 = {"1.71", "70.0", "29-04-2023"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> validation.validateBmiInput(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR)); + + // 1 decimal point height + String[] input2 = {"1.7", "70.03", "29-04-2023"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateBmiInput(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_HEIGHT_WEIGHT_INPUT_ERROR)); + + // height = 0 + String[] input3 = {"0.00", "70.03", "29-04-2023"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateBmiInput(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.ZERO_HEIGHT_AND_WEIGHT_ERROR)); + + // height > 2.75 + String[] input4 = {"3.00", "70.03", "29-04-2023"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateBmiInput(input4)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.MAX_HEIGHT_ERROR)); + + // weight > 650 + String[] input5 = {"2.00", "1000.00", "29-04-2023"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateBmiInput(input5)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.MAX_WEIGHT_ERROR)); + + // specified date already added + new Bmi("1.70", "70.00", "14-04-2024"); + String[] input6 = {"1.70", "70.03", "14-04-2024"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateBmiInput(input6)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.DATE_ALREADY_EXISTS_ERROR)); + + // empty strings + String[] input7 = {"", "70.0", "29-04-2023"}; + exceptionThrown = assertThrows(CustomExceptions.InsufficientInput.class, () -> + validation.validateBmiInput(input7)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INSUFFICIENT_BMI_PARAMETERS_ERROR)); + + } + + /** + * Tests the behaviour of validateDateNotAfterToday when a date string before 2024 is given. + * Expects no exceptions to be thrown. + */ + @Test + void validateDateNotAfterToday_dateBeforeToday_noExceptionThrown() { + String input = "14-04-2023"; + assertDoesNotThrow(() -> validation.validateDateNotAfterToday(input)); + } + + /** + * Tests the behaviour of validateDateNotAfterToday when a date string after 2024 is given. + * Expects InvalidInput exception to be thrown with correct error message printed. + */ + @Test + void validateDateNotAfterToday_dateAfterToday_expectsExceptionThrownWithCorrectErrorMessage() { + String input = "14-04-2025"; + Exception exceptionThrown; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateNotAfterToday(input)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.DATE_IN_FUTURE_ERROR)); + } + + //@@author j013n3 + /** + * Tests the behaviour of correct parameters being passed into validatePeriodInput. + * Expects no exception thrown. + */ + @Test + void validatePeriodInput_correctParameters_expectsExceptionThrownWithCorrectErrorMessage() { + boolean isParser = true; + String[] input = {"22-03-2024", "28-03-2024"}; + assertDoesNotThrow(() -> validation.validatePeriodInput(input, isParser)); + } + + /** + * Tests the behaviour of incorrect parameters being passed into validatePeriodInput. + * Expects either InvalidInput or InsufficientInput exception to be thrown with correct error message printed. + */ + @Test + void validatePeriodInput_incorrectParameters_expectsExceptionThrownWithCorrectErrorMessage() { + boolean isParser = true; + Exception exceptionThrown; + // empty strings + String[] input1 = {"", "29-03-2024"}; + + exceptionThrown = assertThrows(CustomExceptions.InsufficientInput.class, () -> + validation.validatePeriodInput(input1, isParser)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INSUFFICIENT_PERIOD_PARAMETERS_ERROR)); + + // end date before start date + String[] input2 = {"28-03-2024", "22-03-2024"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validatePeriodInput(input2, isParser)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.PERIOD_END_BEFORE_START_ERROR)); + + // invalid start date + // end date before start date + String[] input3 = {"28-13-2024", "22-03-2024"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validatePeriodInput(input3, isParser)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_START_DATE_ERROR)); + } + + + //@@author rouvinerh + /** + * Tests the behaviour of correct parameters being passed into validateAppointmentDetails. + * Expects no exception to be thrown. + */ + @Test + void validateAppointmentInput_correctParameters_noExceptionThrown() { + String[] input = {"29-04-2024", "19:30", "test description"}; + assertDoesNotThrow(() -> validation.validateAppointmentDetails(input)); + } + + /** + * Tests the behaviour of incorrect parameters being passed into validateAppointmentDetails. + * Expects either InvalidInput or InsufficientInput exception to be thrown with correct error message printed. + */ + @Test + void validateAppointmentInput_incorrectParameters_expectsInvalidInputException() { + // description too long + Exception exceptionThrown; + String[] input1 = {"28-04-2024", "22:30", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateAppointmentDetails(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.DESCRIPTION_LENGTH_ERROR)); + + // description contains non-alphanumeric characters + String[] input2 = {"28-04-2024", "22:30", "doctor | ;"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateAppointmentDetails(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_DESCRIPTION_ERROR)); + + // empty strings + String[] input3 = {"", "22:30", "doctor"}; + exceptionThrown = assertThrows(CustomExceptions.InsufficientInput.class, () -> + validation.validateAppointmentDetails(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INSUFFICIENT_APPOINTMENT_PARAMETERS_ERROR)); + } + + /** + * Tests the behaviour of a correctly formatted time string being passed into validateTimeInput. + * Expects no exception to be thrown. + */ + @Test + void validateTimeInput_correctInput_noExceptionThrown() { + String input = "23:50"; + assertDoesNotThrow(() -> validation.validateTimeInput(input)); + } + + /** + * Tests the behaviour of incorrect parameters being passed into validateTimeInput. + * Expects InvalidInput exception to be thrown with correct error message printed. + */ + @Test + void validateTimeInput_invalidInput_expectsInvalidInputExceptionWithCorrectErrorMessage() { + Exception exceptionThrown; + // invalid delimiter + String input1 = "23-50"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateTimeInput(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_ACTUAL_TIME_ERROR)); + + // illegal hours + String input2 = "24:50"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateTimeInput(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_ACTUAL_TIME_HOUR_ERROR)); + + // illegal minutes + String input3 = "21:60"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateTimeInput(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_ACTUAL_TIME_MINUTE_ERROR)); + + // time contains letters + String input4 = "12:2a"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateTimeInput(input4)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_ACTUAL_TIME_ERROR)); + + // invalid format + String input5 = "21:55:44"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateTimeInput(input5)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_ACTUAL_TIME_ERROR)); + } + + /** + * Tests the behaviour of correct parameters being passed to validateRunInput. + * Expects no exceptions thrown. + */ + @Test + void validateRunInput_correctInput_expectsNoExceptionsThrown() { + // with dates + String[] input1 = {"20:25", "5.15", "29-03-2024"}; + assertDoesNotThrow(() -> validation.validateRunInput(input1)); + + // without dates + String[] input2 = {"20:25", "5.15", null}; + assertDoesNotThrow(() -> validation.validateRunInput(input2)); + } + + /** + * Tests the behaviour of incorrect parameters being passed into validateRunInput. + * Expects either InvalidInput or InsufficientInput exception to be thrown with correct error message printed. + */ + @Test + void validateRunInput_incorrectParameters_expectsExceptionThrownWithCorrectErrorMessage() { + Exception exceptionThrown; + // invalid distance + String[] input1 = {"20:25", "5"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_RUN_DISTANCE_ERROR)); + + // date in future + String[] input2 = {"20:25", "5.25", "31-03-2025"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.DATE_IN_FUTURE_ERROR)); + + // has non integer values in time + String[] input3 = {"2a:03", "5.00"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_RUN_TIME_ERROR)); + + // invalid delimiter + String[] input4 = {"25-03", "5.00"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input4)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_RUN_TIME_ERROR)); + + // too many parts + String[] input5 = {"25:03:04:22", "5.00"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input5)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_RUN_TIME_ERROR)); + + // invalid format test 1 + String[] input6 = {"1:2:3", "5.00"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input6)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_RUN_TIME_ERROR)); + + // invalid format + String[] input7 = {"100:00:00", "5.00"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateRunInput(input7)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_RUN_TIME_ERROR)); + + // empty strings + String[] input8 = {"20:25", ""}; + exceptionThrown = assertThrows(CustomExceptions.InsufficientInput.class, () -> + validation.validateRunInput(input8)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INSUFFICIENT_RUN_PARAMETERS_ERROR)); + } + + /** + * Tests the behaviour of valid input being passed to validateGymInput. + * Expects no exceptions to be thrown. + */ + @Test + void validateGymInput_correctInput_noExceptionThrown() { + String[] input1 = {"4", "29-04-2023"}; + assertDoesNotThrow(() -> validation.validateGymInput(input1)); + + String[] input2 = {"4", null}; + assertDoesNotThrow(() -> validation.validateGymInput(input2)); + } + + /** + * Tests the behaviour of incorrect parameters being passed into validateRunInput. + * Expects either InvalidInput or InsufficientInput exception to be thrown with correct error message printed. + */ + @Test + void validateGymInput_invalidInput_expectsExceptionThrownWithCorrectErrorMessage() { + Exception exceptionThrown; + // non integer number of sets + String[] input1 = {"a", null}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateGymInput(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_NUMBER_OF_STATIONS_ERROR)); + + // number of sets exceeds maximum allowed + String[] input2 = {"51", null}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateGymInput(input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.MAX_STATIONS_ERROR)); + + // number of sets below minimum allowed + String[] input3 = {"-1", null}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateGymInput(input3)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_NUMBER_OF_STATIONS_ERROR)); + + // empty strings + String[] input4 = {"", null}; + exceptionThrown = assertThrows(CustomExceptions.InsufficientInput.class, () -> + validation.validateGymInput(input4)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INSUFFICIENT_GYM_PARAMETERS_ERROR)); + } + + /** + * Tests the behaviour of a valid start date being passed to validateDateAfterLatestPeriod. + * Expects no exception to be thrown. + */ + @Test + void validateDateAfterLatestPeriodInput_validInput_noExceptionThrown() { + String input1 = "10-04-2024"; + assertDoesNotThrow(() -> + validation.validateDateAfterLatestPeriodInput(input1, null)); + + LocalDate latestPeriodEndDate2 = LocalDate.of(2024, 3, 9); + String input2 = "11-04-2024"; + assertDoesNotThrow(() -> + validation.validateDateAfterLatestPeriodInput(input2, latestPeriodEndDate2)); + } + + /** + * Tests the behaviour of invalid start dates being passed to validateDateAfterLatestPeriod. + * Expects InvalidInput exception to be thrown with correct error message printed. + */ + @Test + void validateDateAfterLatestPeriodInput_invalidDateInput_expectsInvalidInputExceptionWithCorrectMessage() { + LocalDate latestPeriodEndDate = LocalDate.of(2024, 3, 9); + Exception exceptionThrown; + //date is before latestPeriodEndDate + String input1 = "09-02-2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateAfterLatestPeriodInput(input1, latestPeriodEndDate)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.CURRENT_START_BEFORE_PREVIOUS_END)); + + //date same as latestPeriodEndDate + String input2 = "09-03-2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateAfterLatestPeriodInput(input2, latestPeriodEndDate)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.CURRENT_START_BEFORE_PREVIOUS_END)); + } + + /** + * Tests the behaviour of start dates being passed to validateStartDatesTally. + * Expects no exception to be thrown. + */ + @Test + void validateStartDatesTally_validInput_noExceptionThrown() { + new Period("01-01-2024"); + String[] input1 = {"01-01-2024", "05-01-2024"}; + assertDoesNotThrow(() -> + validation.validateStartDatesTally(null, input1)); + + LocalDate latestPeriodEndDate2 = LocalDate.of(2024,1,1); + String[] input2 = {"01-01-2024", "05-02-2024"}; + assertDoesNotThrow(() -> + validation.validateStartDatesTally(latestPeriodEndDate2, input2)); + } + + /** + * Tests the behaviour of incorrect parameters being passed into validateRunInput. + * Expects InvalidInput exception to be thrown with correct error message printed. + */ + @Test + void validateStartDatesTally_invalidInput_expectsInvalidInputExceptionWithCorrectMessage() { + new Period("01-01-2024"); + Exception exceptionThrown; + + //start dates do not tally + String[] input1 = {"01-01-2023", "05-01-2024"}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateStartDatesTally(null, input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_START_DATE_INPUT_ERROR)); + + //end date is missing from user input + String[] input2 = {"01-01-2024", null}; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateStartDatesTally(null, input2)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.END_DATE_NOT_FOUND_ERROR)); + } + + /** + * Tests the behaviour of a date that is not found the list being passed to validateDateNotPresent. + * Expects no exception to be thrown. + */ + @Test + void validateDateNotPresent_validInput_noExceptionThrown() { + new Bmi("1.75", "70.00", "02-02-2024"); + new Bmi("1.75", "71.00", "02-03-2024"); + + //date not found in list + String input1 = "03-03-2024"; + assertDoesNotThrow(() -> + validation.validateDateNotPresent(input1)); + } + + /** + * Tests the behaviour of a date that is found the list being passed to validateDateNotPresent. + * Expects InvalidException to be thrown with correct error message printed. + */ + @Test + void validateDateNotPresent_invalidInput_expectsInvalidInputExceptionWithCorrectMessage() { + new Bmi("1.75", "70.00", "02-02-2024"); + new Bmi("1.75", "71.00", "02-03-2024"); + Exception exceptionThrown; + //date found in list + String input1 = "02-02-2024"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + validation.validateDateNotPresent(input1)); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.DATE_ALREADY_EXISTS_ERROR)); + } +} diff --git a/src/test/java/workouts/GymStationTest.java b/src/test/java/workouts/GymStationTest.java new file mode 100644 index 0000000000..79e6a6014f --- /dev/null +++ b/src/test/java/workouts/GymStationTest.java @@ -0,0 +1,148 @@ +package workouts; + +import constants.ErrorConstant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import utility.CustomExceptions; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class GymStationTest { + + private GymStation gymStation; + + @BeforeEach + void setUp() { + // initialise a gymStation object to test the methods + try { + gymStation = new GymStation("Bench Press", + "1", + "10", + "1.0"); + } catch (CustomExceptions.InsufficientInput | CustomExceptions.InvalidInput e) { + fail("Should not have thrown error here"); + } + } + + @AfterEach + void cleanup() { + + } + + /** + * Tests the behaviour of valid exercise names being passed to validateExerciseName. + * Expects no exceptions to be thrown. + */ + @Test + void validateExerciseName_correctName_noExceptionThrown() { + String input1 = "Bench Press"; + assertDoesNotThrow(() -> gymStation.validateGymStationName(input1)); + + String input2 = "squat"; + assertDoesNotThrow(() -> gymStation.validateGymStationName(input2)); + + String input3 = "testing exercise"; + assertDoesNotThrow(() -> gymStation.validateGymStationName(input3)); + } + + /** + * Tests the behaviour of invalid exercise names being passed to validateExerciseName. + * Expects InvalidInput exception to be thrown. + */ + @Test + void validateExerciseName_invalidNames_expectsInvalidInputException() { + + // numbers in name + String input1 = "bench1"; + Exception exception = assertThrows(CustomExceptions.InvalidInput.class, () + -> gymStation.validateGymStationName(input1)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_GYM_STATION_NAME_ERROR)); + + // special characters in name + String input2 = "bench-;"; + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> gymStation.validateGymStationName(input2)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_GYM_STATION_NAME_ERROR)); + + // name length > 25 chars + String input4 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA "; + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> gymStation.validateGymStationName(input4)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_GYM_STATION_NAME_ERROR)); + } + + /** + * Tests the behaviour of empty exercise names being passed to validateExerciseName. + * Expects InsufficientInput exception to be thrown. + */ + @Test + void validateExerciseName_emptyNames_expectsInsufficientInputException() { + Exception exception = assertThrows(CustomExceptions.InsufficientInput.class, () + -> gymStation.validateGymStationName("")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_GYM_STATION_EMPTY_NAME_ERROR)); + } + + /** + * Tests the behaviour of a correct weights array being passed to validateWeightsArray. + * Expects no exception to be thrown, and the correct ArrayList of integers to be returned. + * + * @throws CustomExceptions.InvalidInput If the input string does not have the right format. + */ + @Test + void processWeightsArray_correctInput_returnCorrectArrayList() throws CustomExceptions.InvalidInput { + String input = "1.0,2.25,50.5,60.75,0.0"; + ArrayList expected = new ArrayList<>(); + expected.add(1.0); + expected.add(2.25); + expected.add(50.5); + expected.add(60.75); + expected.add(0.0); + + ArrayList result = gymStation.processWeightsArray(input); + assertArrayEquals(expected.toArray(), result.toArray()); + } + + /** + * Tests the behaviour of incorrect weights array being passed to validateWeightsArray. + * Expects InvalidInput exception to be thrown. + */ + @Test + void processWeightsArray_invalidInput_expectInvalidInputException() { + // negative weights + String input1 = "-1,2"; + Exception exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gymStation.processWeightsArray(input1)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR)); + + // blanks + String input = ""; + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gymStation.processWeightsArray(input)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_EMPTY_ERROR)); + + // non integer weights + String input2 = "1,a"; + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gymStation.processWeightsArray(input2)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR)); + + // incorrect multiple of weights + String input3 = "1.333,1.444,0.998"; + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gymStation.processWeightsArray(input3)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_VALUE_ERROR)); + + // exceed max weights + String input4 = "3000"; + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gymStation.processWeightsArray(input4)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHT_MAX_ERROR)); + + } + +} diff --git a/src/test/java/workouts/GymTest.java b/src/test/java/workouts/GymTest.java new file mode 100644 index 0000000000..14213d843b --- /dev/null +++ b/src/test/java/workouts/GymTest.java @@ -0,0 +1,216 @@ +package workouts; + +import constants.ErrorConstant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import utility.CustomExceptions; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class GymTest { + + @BeforeEach + void setUp() { + + } + + @AfterEach + void cleanup() { + WorkoutLists.clearWorkoutsRunGym(); + } + + /** + * Tests the behavior of adding a new station to the gym. + * Verifies whether the newly added station is correctly reflected in the gym class. + * Expected Behaviour is to add stations and sets to the gym. + */ + @Test + void addStation_validInput_expectAddedStation() { + Gym newGym = new Gym(); + try{ + + newGym.addStation("ExerciseOne", "1", "10", "1.0"); + newGym.addStation("ExerciseTwo", "2", "20", "1.0,2.0"); + assertEquals(2, newGym.getStations().size()); + + newGym.addStation("ExerciseThree", "3", "30", "1.0,2.0,3.0"); + ArrayList stations = newGym.getStations(); + assertEquals(3, stations.size()); + + for(int i = 0; i < stations.size(); i++){ + String stationName = stations.get(i).getStationName(); + ArrayList sets = stations.get(i).getSets(); + int numberOfSets = sets.size(); + + if (i == 0){ + assertEquals("ExerciseOne", stationName); + assertEquals(1, numberOfSets ); + assertEquals(1.0, sets.get(0).getWeight()); + assertEquals(10, sets.get(0).getNumberOfRepetitions()); + + } else if (i == 1){ + assertEquals("ExerciseTwo", stationName); + assertEquals(2, numberOfSets ); + assertEquals(1.0, sets.get(0).getWeight()); + assertEquals(20, sets.get(0).getNumberOfRepetitions()); + assertEquals(2.0, sets.get(1).getWeight()); + assertEquals(20, sets.get(1).getNumberOfRepetitions()); + + } else if (i == 2){ + assertEquals("ExerciseThree", stationName); + assertEquals(3, numberOfSets ); + assertEquals(1.0, sets.get(0).getWeight()); + assertEquals(30, sets.get(0).getNumberOfRepetitions()); + assertEquals(2.0, sets.get(1).getWeight()); + assertEquals(30, sets.get(1).getNumberOfRepetitions()); + assertEquals(3.0, sets.get(2).getWeight()); + assertEquals(30, sets.get(2).getNumberOfRepetitions()); + + } + } + + } catch (Exception e) { + fail("Should not throw an exception"); + } + } + + /** + * Tests the behaviour of incorrect inputs being passed to the addStation method in Gym. + * Expects InvalidInput exception to be thrown. + */ + @Test + void addStation_invalidInput_expectsInvalidInputException() { + Gym gym = new Gym(); + Exception exception; + + // number of sets is not a positive integer + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "a", "4", "1020")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_SETS_POSITIVE_DIGIT_ERROR)); + + // number of reps is not a positive integer + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "a", "1020")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_REPS_POSITIVE_DIGIT_ERROR)); + + // weights does not have comma + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "1020")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_NUMBER_ERROR)); + + // weights array > maximum + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "10000,20")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHT_MAX_ERROR)); + + // weights array > minimum + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "-10,-20")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR)); + + // weights array has letters + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "10,a")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR)); + + // weights array has spaces + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "10, 20")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR)); + + // weights array regex fail + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "a")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_ARRAY_FORMAT_ERROR)); + + // no weights specified + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "2", "3", "")); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_WEIGHTS_EMPTY_ERROR)); + + // weights and sets do not match + exception = assertThrows(CustomExceptions.InvalidInput.class, () -> + gym.addStation("Bench Press", "1", "3", "10,20")); + assertTrue(exception.getMessage().contains((ErrorConstant.INVALID_WEIGHTS_NUMBER_ERROR))); + } + + /** + * Test to see if getStationByIndex handles invalid index correctly by throwing an OutOfBounds exception. + */ + @Test + void getStationByIndex_invalidIndex_throwOutOfBoundsError(){ + Gym newGym = new Gym(); + Exception exception; + try { + newGym.addStation("ExerciseOne", "1", "10", "1.0"); + newGym.addStation("ExerciseTwo", "2", "20", "1.0,2.0"); + newGym.addStation("ExerciseThree", "3", "30", "1.0,2.0,3.0"); + + exception = assertThrows(CustomExceptions.OutOfBounds.class, () -> newGym.getStationByIndex(-1)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_INDEX_SEARCH_ERROR)); + + exception = assertThrows(CustomExceptions.OutOfBounds.class, () -> newGym.getStationByIndex(3)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_INDEX_SEARCH_ERROR)); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception"); + } + } + + /** + * Test to see if getStationByIndex returns the correct station when given a valid index. + */ + @Test + void getStationByIndex_correctIndex_expectCorrectStation(){ + Gym newGym = new Gym(); + try { + newGym.addStation("ExerciseOne", "1", "10", "1.0"); + newGym.addStation("ExerciseTwo", "2", "20", "1.0,2.0"); + newGym.addStation("ExerciseThree", "3", "30", "1.0,2.0,3.0"); + + GymStation station1 = newGym.getStationByIndex(1); + assertEquals("ExerciseTwo", station1.getStationName()); + assertEquals(2, station1.getSets().size()); + assertEquals(1.0, station1.getSets().get(0).getWeight()); + assertEquals(2.0, station1.getSets().get(1).getWeight()); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput | CustomExceptions.OutOfBounds e) { + fail("Should not throw an exception"); + } + } + + + /** + * Test to see if toFileString method in Gym works with correct input. + * Expects correct string to be returned. + */ + @Test + void toFileString_correctInput_expectedCorrectString(){ + String expected1 = "GYM:2:11-11-1997:bench press:4:4:10.0,20.0,30.0,40.0:squats:4:3:20.0,30.0,40.0,50.0"; + String expected2WithNoDate = "GYM:2:NA:bench press:4:4:10.0,20.0,30.0,40.0:squats:4:3:20.0,30.0,40.0,50.0"; + + try { + Gym newGym = new Gym("11-11-1997"); + Gym newGym2 = new Gym(); + + newGym.addStation("bench press", "4", "4", "10.0,20.0,30.0,40.0"); + newGym.addStation("squats", "4", "3", "20.0,30.0,40.0,50.0"); + newGym2.addStation("bench press", "4", "4", "10.0,20.0,30.0,40.0"); + newGym2.addStation("squats", "4", "3", "20.0,30.0,40.0,50.0"); + + String output = newGym.toFileString(); + String output2 = newGym2.toFileString(); + assertEquals(expected1, output); + assertEquals(expected2WithNoDate, output2); + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception"); + } + } +} diff --git a/src/test/java/workouts/RunTest.java b/src/test/java/workouts/RunTest.java new file mode 100644 index 0000000000..eea10e40a5 --- /dev/null +++ b/src/test/java/workouts/RunTest.java @@ -0,0 +1,163 @@ +package workouts; + +import constants.ErrorConstant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import utility.CustomExceptions; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RunTest { + + @AfterEach + void cleanup() { + WorkoutLists.clearWorkoutsRunGym(); + } + /** + * Tests the behaviour of checkRunTime when valid inputs are passed. + * Expects no exceptions to be thrown. + */ + @Test + void checkRunTime_correctInput_returnListOfTimes() throws CustomExceptions.InvalidInput { + + // with hours + String testTime = "01:59:10"; + Run runTest = new Run(testTime, "15.3"); + Integer[] result = runTest.processRunTime(testTime); + Integer[] expected = {1, 59, 10}; + assertArrayEquals(expected, result); + + // without hours + Integer[] result2 = runTest.processRunTime("50:52"); + Integer[] expected2 = {-1, 50, 52}; + assertArrayEquals(expected2, result2); + + // with hours, zero minutes zero seconds + Integer[] result3 = runTest.processRunTime("01:00:00"); + Integer[] expected3 = {1, 0, 0}; + assertArrayEquals(expected3, result3); + + // max time + Integer[] result4 = runTest.processRunTime("99:59:59"); + Integer[] expected4 = {99, 59, 59}; + assertArrayEquals(expected4, result4); + + // min time + Integer[] result5 = runTest.processRunTime("00:01"); + Integer[] expected5 = {-1, 0, 1}; + assertArrayEquals(expected5, result5); + + // max minute max second + Integer[] result6 = runTest.processRunTime("59:59"); + Integer[] expected6 = {-1, 59, 59}; + assertArrayEquals(expected6, result6); + } + + /** + * Tests the behaviour of checkRunTime when invalid inputs are passed. + * Expects InvalidInput exception to be thrown with the correct error message. + */ + @Test + void processRunTime_invalidInputs_expectInvalidInputExceptionWithCorrectErrorMessage() { + Exception exceptionThrown; + // hours set to 00 + String input1 = "00:30:00"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> new Run(input1, "15.3")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_HOUR_ERROR)); + + // invalid minutes + String input2 = "60:00"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> new Run(input2, "15.3")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_MINUTE_ERROR)); + + // invalid seconds + String input3 = "05:60"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> new Run(input3, "15.3")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.INVALID_SECOND_ERROR)); + + // 00:00 as time + String input4 = "00:00"; + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> new Run(input4, "15.3")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.ZERO_TIME_ERROR)); + + } + + /** + * Test the behaviour of checkDistance when valid distance is passed. + * Expects the 2 decimal place string distance to be returned. + * + * @throws CustomExceptions.InvalidInput If distance is outside valid range. + */ + @Test + void checkDistance_validDistance_returnTwoDecimalPlaceDistance() throws CustomExceptions.InvalidInput { + Run run1 = new Run("25:00", "5.00"); + assertEquals("5.00", run1.getDistance()); + + // min distance + Run run2 = new Run("00:02", "0.01"); + assertEquals("0.01", run2.getDistance()); + + // max distance + Run run3 = new Run("99:59:00", "5000.00"); + assertEquals("5000.00", run3.getDistance()); + } + + /** + * Tests the behaviour of checkDistance when invalid distances are passed. + * Expects InvalidInput exception to be thrown. + */ + @Test + void checkDistance_invalidDistance_throwsInvalidInputException() { + Exception exceptionThrown; + // more than max of 5000 + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + new Run("03:30:00", "5001.00")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.DISTANCE_TOO_LONG_ERROR)); + + // less than min of 0 + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + new Run("03:30:00", "0.00")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.ZERO_DISTANCE_ERROR)); + } + + /** + * Tests the behaviour of calculatedPace when a valid run is added. + * Expects no exception to be thrown and correct pace returned. + */ + @Test + void calculatePace_validTimeAndDistance_returnCorrectPace() throws CustomExceptions.InvalidInput { + Run run1 = new Run("25:00", "5.00"); + assertEquals("5:00/km", run1.getPace()); + + Run run2 = new Run("01:25:00", "5.00"); + assertEquals("17:00/km", run2.getPace()); + + // min pace + Run run3 = new Run("5:00", "5.00"); + assertEquals("1:00/km", run3.getPace()); + + // max pace + Run run4 = new Run("02:30:00", "5.00"); + assertEquals("30:00/km", run4.getPace()); + } + + /** + * Tests the behaviour of calculatedPace when a run with an invalid pace is added. + * Expects InvalidInput exception to be thrown. + */ + @Test + void calculatePace_invalidTimeAndDistance_throwInvalidInputException() { + Exception exceptionThrown; + // exceed max pace of 30:00/km + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + new Run("03:30:00", "5.00")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.MAX_PACE_ERROR)); + + // below min pace of 1:00/km + exceptionThrown = assertThrows(CustomExceptions.InvalidInput.class, () -> + new Run("02:00", "10.00")); + assertTrue(exceptionThrown.toString().contains(ErrorConstant.MIN_PACE_ERROR)); + } +} diff --git a/src/test/java/workouts/WorkoutListsTest.java b/src/test/java/workouts/WorkoutListsTest.java new file mode 100644 index 0000000000..96af974db9 --- /dev/null +++ b/src/test/java/workouts/WorkoutListsTest.java @@ -0,0 +1,260 @@ +package workouts; + +import constants.ErrorConstant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import utility.CustomExceptions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class WorkoutListsTest { + @BeforeEach + void setUp() { + + } + + /** + * Clears the list of workouts/runs/gyms after each test. + */ + @AfterEach + void cleanup() { + WorkoutLists.clearWorkoutsRunGym(); + } + + + /** + * Tests the behavior of adding a new run to the run list. + * Verifies whether the newly added run is correctly reflected in the run and WorkoutList. + */ + @Test + void addRun_normalInput_expectAppend() { + try { + Run inputRun = new Run("40:10", "10.3", "15-03-2024"); + + WorkoutLists workoutListsInstance = new WorkoutLists(); + workoutListsInstance.addRun(inputRun); + + ArrayList runList = WorkoutLists.getRuns(); + ArrayList workoutList = WorkoutLists.getWorkouts(); + + Workout expectedRun = runList.get(runList.size() - 1); + Workout expectedWorkout = workoutList.get(runList.size() - 1); + + assertEquals(inputRun, expectedRun); + assertEquals(inputRun, expectedWorkout); + + } catch (CustomExceptions.InvalidInput e) { + fail("Should not throw an exception."); + } + } + + + /** + * Tests the behavior of getting the workout list with RUN , GYM, and ALL. + * Verifies whether the method is able to correct retrieve the list of workouts. + */ + @Test + void getWorkouts_properInput_expectRetrieval() { + try { + + // Setup + ArrayList inputGymList = new ArrayList<>(); + ArrayList inputRunList = new ArrayList<>(); + + Gym gym1 = new Gym("15-11-2023"); + gym1.addStation("Bench Press", "1", "50", "1.0"); + gym1.addStation("Shoulder Press", "2", "10", "1.0,2.0"); + + Gym gym2 = new Gym("16-11-2023"); + gym2.addStation("Squat Press", "1", "50", "1.0"); + gym2.addStation("Lat Press", "2", "10", "1.0,2.0"); + gym2.addStation("Bicep curls", "1", "10", "1.0"); + + Run run1 = new Run("40:10", "10.3", "15-03-2024"); + Run run2 = new Run("30:10", "20.3", "30-03-2023"); + + + inputGymList.add(gym1); + inputGymList.add(gym2); + inputRunList.add(run1); + inputRunList.add(run2); + + + + ArrayList runList = WorkoutLists.getRuns(); + for(int i = 0; i < inputRunList.size(); i++) { + Run expected = inputRunList.get(i); + Run actual = runList.get(i); + assertEquals(expected, actual); + } + + ArrayList gymList = WorkoutLists.getGyms(); + for(int i = 0; i < inputGymList.size(); i++) { + Gym expected = inputGymList.get(i); + Gym actual = gymList.get(i); + assertEquals(expected, actual); + } + + ArrayList allList = WorkoutLists.getWorkouts(); + assertEquals(gym1, allList.get(0)); + assertEquals(gym2, allList.get(1)); + assertEquals(run1, allList.get(2)); + assertEquals(run2, allList.get(3)); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception."); + } + } + + + /** + * Tests the behavior of getting the latest run from the run list. + * Expected behavior is for actual to equal to the secondRun. + */ + @Test + void getLatestRun_properList_correctRetrieval() { + try { + new Run("20:10", "10.3", "15-03-2024"); + Run secondRun = new Run("20:10", "10.3", "15-03-2024"); + + Run actual = WorkoutLists.getLatestRun(); + assertEquals(secondRun, actual); + } catch (CustomExceptions.OutOfBounds | CustomExceptions.InvalidInput e) { + fail("Should not throw an exception"); + } + } + + /** + * Test the behaviour when you try to get the latest run from an empty list. + * Expected behaviour is to raise OutOfBounds exception. + */ + @Test + void getLatestRun_emptyList_throwOutOfBound() { + // Call the method or code that should throw the exception + assertThrows(CustomExceptions.OutOfBounds.class, WorkoutLists::getLatestRun); + } + + + /** + * Test deleting of runs with valid list and valid index. + * Expected behaviour is to have one run left in the list. + */ + @Test + void deleteRun_properList_listOfSizeOne() { + try { + new Run("20:10", "10.3", "15-03-2024"); + new Run("20:11", "10.3", "15-03-2023"); + int index = 1; + WorkoutLists.deleteRun(index); + assertEquals(1, WorkoutLists.getRunSize()); + } catch (CustomExceptions.InvalidInput | CustomExceptions.OutOfBounds e) { + fail("Should not throw an exception"); + } + } + + /** + * Test deleting of runs with empty list. + * Expected behaviour is for an AssertionError to be thrown. + */ + @Test + void deleteRun_emptyList_throwsAssertionError() { + assertThrows (AssertionError.class, () -> + WorkoutLists.deleteRun(0)); + } + + /** + * Test deleting of runs with invalid index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deleteRun_properListInvalidIndex_throwOutOfBoundsForRun(){ + try { + new Run("20:10", "10.3", "15-03-2024"); + } catch (CustomExceptions.InvalidInput e) { + fail("Should not throw an exception"); + } + int invalidIndex = 5; + assertThrows (CustomExceptions.OutOfBounds.class, () -> + WorkoutLists.deleteRun(invalidIndex)); + } + + /** + * Test deleting of gyms with valid list and valid index. + * Expected behaviour is to delete the first gym and be left with one in the list. + * The gym left should be the second gym in the list. + */ + @Test + void deleteGym_validIndex_listOfSizeOne(){ + Gym gym1 = new Gym(); + new ArrayList<>(List.of(1.0)); + new ArrayList<>(Arrays.asList(1.0,2.0)); + try { + gym1.addStation("Bench Press", "1", "50", "1.0"); + gym1.addStation("Shoulder Press", "2", "10", "1.0,2.0"); + + Gym gym2 = new Gym(); + gym2.addStation("Squat Press", "1", "50", "1.0"); + gym2.addStation("Lat Press", "2", "10", "1.0,2.0"); + gym2.addStation("Bicep curls", "1", "10", "1.0"); + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception"); + } + + int index = 0; + try { + WorkoutLists.deleteGym(index); + assertEquals(1, WorkoutLists.getGymSize()); + // check to make sure that after deleting the first gym, the second gym becomes first + assertEquals("Squat Press" , WorkoutLists.getGyms().get(0).getStationByIndex(0).getStationName()); + } catch (CustomExceptions.OutOfBounds outOfBounds) { + fail("Should not throw an exception"); + } + } + + /** + * Test deleting of gym with empty list. + * Expected behaviour is for an Out of Bound error to be thrown. + */ + @Test + void deleteGym_emptyList_throwsOutOfBoundsError() { + Exception exception = assertThrows (CustomExceptions.OutOfBounds.class, () -> + WorkoutLists.deleteGym(0)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_INDEX_DELETE_ERROR)); + } + + /** + * Test deleting of gym with invalid index. + * Expected behaviour is for an OutOfBounds error to be thrown. + */ + @Test + void deleteGym_invalidIndex_throwOutOfBoundsForGym() { + Gym gym1 = new Gym(); + try { + gym1.addStation("Bench Press", "1", "50", "1.0"); + gym1.addStation("Shoulder Press", "2", "10", "2.0,3.0"); + + } catch (CustomExceptions.InvalidInput | CustomExceptions.InsufficientInput e) { + fail("Should not throw an exception"); + } + + // test for invalid index + int invalidIndex = 5; + Exception exception = assertThrows (CustomExceptions.OutOfBounds.class, () -> + WorkoutLists.deleteGym(invalidIndex)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_INDEX_DELETE_ERROR)); + + // test for below 0 index + int invalidIndex2 = -1; + exception = assertThrows (CustomExceptions.OutOfBounds.class, () -> + WorkoutLists.deleteGym(invalidIndex2)); + assertTrue(exception.getMessage().contains(ErrorConstant.INVALID_INDEX_DELETE_ERROR)); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..d994a92350 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,9 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +____________________________________________________________ + _ _ +|_) | _ _ |_) o | _ _|_ +| |_| | _> (/_| | | (_) |_ +Engaging orbital thrusters... +PulsePilot on standby +____________________________________________________________ +What is your name, voyager? +____________________________________________________________ diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..e69de29bb2 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +0,0 @@ -James Gosling \ No newline at end of file