diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e6471cd9c4 --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: BuffBuddy + diff --git a/README.md b/README.md index e243ece764..24986caa75 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Duke project template +# BuffBuddy -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. +BuffyBuddy is your go to workout tracker to help you stay on track with your goals. ## Setting up in Intellij diff --git a/build.gradle b/build.gradle index ea82051fab..6d51358f5e 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,8 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0' + implementation 'com.google.code.gson:gson:2.8.9' + testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' } test { @@ -43,4 +45,11 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } diff --git a/data/data.json b/data/data.json new file mode 100644 index 0000000000..70509b723a --- /dev/null +++ b/data/data.json @@ -0,0 +1,80 @@ +{ + "programmeList": { + "currentActiveProgramme": 0, + "programmeList": [ + { + "programmeName": "Advanced Starter", + "dayList": [ + { + "name": "Monday", + "exercises": [ + { + "sets": 3, + "reps": 15, + "weight": 30, + "calories": 200, + "name": "Bench Press" + }, + { + "sets": 3, + "reps": 15, + "weight": 50, + "calories": 200, + "name": "Squat" + } + ] + }, + { + "name": "Wednesday", + "exercises": [ + { + "sets": 3, + "reps": 10, + "weight": 10, + "calories": 100, + "name": "Bicep Curl" + } + ] + } + ] + }, + { + "programmeName": "Advanced Starter", + "dayList": [ + { + "name": "Monday", + "exercises": [ + { + "sets": 3, + "reps": 15, + "weight": 30, + "calories": 200, + "name": "Bench Press" + }, + { + "sets": 3, + "reps": 15, + "weight": 50, + "calories": 200, + "name": "Squat" + } + ] + }, + { + "name": "Wednesday", + "exercises": [ + { + "sets": 3, + "reps": 10, + "weight": 10, + "calories": 100, + "name": "Bicep Curl" + } + ] + } + ] + } + ] + }, + "history": {} +} \ No newline at end of file diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..6e1bcc52e0 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Display | Name | Github Profile | Portfolio | +|--------------------------------------------------------|:--------------:|:----------------------------------------------:|:-------------------------------------:| +| ![](https://via.placeholder.com/100.png?text=Atul) | Atul Teja | [Github](https://github.com/atulteja) | [Portfolio](team/atulteja) | +| ![](https://via.placeholder.com/100.png?text=Thiru) | Thiru Vageesan | [Github](https://github.com/tvageesan) | [Portfolio](team/tvageesan.md) | +| ![](https://via.placeholder.com/100.png?text=Beverly) | Low Beverly | [Github](https://github.com/Bev-low) | [Portfolio](team/bev-low.md) | +| ![](https://via.placeholder.com/100.png?text=Tanishka) | Tanishka | [Github Profile](https://github.com/nirala-ts) | [Portfolio](team/nirala-ts) | +| ![](https://via.placeholder.com/100.png?text=Andreus) | Andreus | [Github Profile](https://github.com/Andreus) | [Portfolio](team/andreusxcarvalho.md) | diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..2897d0c19a 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,997 @@ # Developer Guide ## Acknowledgements + -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +BuffBuddy uses the following libraries +1. [Gson](https://google.github.io/gson/) - For saving and loading user data to a JSON file +2. [JUnit](https://junit.org/junit5/) - For automated testing +3. [Mockito](https://site.mockito.org/) - Supplements JUnit testing by creating mocks +4. [Gradle](https://gradle.org/) - For build automation -## Design & implementation +## Design -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} + +### UI Component +![Class_Diagram_for_Ui](images/uiComponent.png) + +The `UI` component manages the input and output interface between the user and the system, allowing interaction through command input and message displays. It enables seamless communication of user requests and system feedback in an organized and formatted manner. + +- **Handles user inputs and outputs**: The `UI` component relies on `Scanner` for capturing user input and `PrintStream` for outputting messages to the console. The `readCommand()` method reads a line of text, typically representing a user command, and returns it for processing. +- **Displays feedback messages**: The component provides `showMessage(String msg)`, `showMessage(Exception e)`, and `showMessage(CommandResult result)` methods to present different types of feedback to users, including general messages, error messages, and results of command executions. These methods ensure messages are formatted and include consistent visual separators. +- **Shows program start and end messages**: The component features `showWelcome()` and `showFarewell()` methods to display welcome and farewell messages, respectively, creating a friendly user experience from start to finish. +- **Keeps input and output streams flexible for testing**: The `UI` component is constructed with a `Scanner` and `PrintStream`, which can be replaced or redirected as needed, allowing easy adaptability for testing and debugging purposes. + + +
+### Programme Component + +![Programme Component Classes Diagram](./images/programmeComponentClassDiagram.png) + +The `ProgrammeList` component, + +- **Manages a collection of programmes**: The `ProgrammeList` class is designed to manage a list of `Programme` objects, supporting easy addition, retrieval, and deletion of programmes. It also allows tracking and setting an active programme. +- **Provides collection-based functionality**: The class includes essential methods for operations such as retrieving the current list size, adding a new programme, and deleting or retrieving a programme by index. All key actions are logged for better traceability and debugging. +- **Supports programme activation and management**: The `startProgramme()` method sets a specific `Programme` as the active programme, enabling users to track which programme is currently in use. This ensures a seamless way to handle active programme operations. +- **Handles edge cases and maintains data integrity**: The `ProgrammeList` class checks for out-of-bounds access and throws appropriate exceptions when invalid indexes are provided. This helps maintain data consistency and prevents runtime errors. +- **Detailed representation**: The `toString()` method returns a comprehensive representation of the programme list, indicating the active programme for better user interface display and reporting. + +The `Programme` component, + +- **Manages a collection of days**: The `Programme` class consists of multiple day objects, supporting easy addition, retrieval and deletion of days. +- **Ensures data consistency and error handling**: The `Programme` class includes validations to handle cases such as null programme names or invalid indexes when accessing days. These validations help maintain data integrity and prevent unexpected runtime issues. Methods that attempt to access invalid indexes throw appropriate exceptions, maintaining robust error handling. +- **Detailed representation**: The `toString()` method returns a formatted string that includes the programme name and all the days in the programme, making it suitable for displaying programme information in user interfaces or summaries. +- **Maintains programme organization**: The class structure, with methods for inserting, retrieving, and deleting days, supports seamless organization and updates within a `Programme`, ensuring that users can easily manage the content and structure of their training schedules. + +The `Day` component, + +- **Represents a single day of exercises**: The `Day` class models a day that can contain multiple exercises. It serves as a building block for a structured workout or training programme. +- **Manages exercises within a day**: The class supports adding, retrieving, and deleting exercises through methods like `insertExercise()`, `getExercise()`, and `deleteExercise()`. This makes it easy to modify the list of exercises for a particular day. +- **Ensures data consistency and error handling**: The `Day` class checks for edge cases, such as invalid indexes when accessing or deleting exercises, and throws appropriate exceptions. Assertions are used to enforce that the day name and exercises are not null or empty upon initialization. +- **Calculates total calories burnt**: The `getTotalCaloriesBurnt()` method sums up the calories from all exercises in the day's list, providing a quick overview of the total effort for that day. +- **Detailed representation**: The `toString()` method returns a formatted string listing the day's name and each exercise, making it convenient for displaying in user interfaces or summaries. +- **Enables object comparison and usage in collections**: The `equals()` and `hashCode()` methods are overridden to facilitate comparisons between `Day` objects and to support their use in collections, ensuring that days can be managed accurately. + +The `Exercise` component, + +- **Models an individual exercise**: The `Exercise` class represents a specific exercise, detailing its sets, reps, weight, calories burned, and name. It provides a structured way to encapsulate exercise data within a day. +- **Allows for detailed updates**: The `updateExercise()` method accepts an `ExerciseUpdate` object and selectively updates fields of the `Exercise` based on the non-null values in the `ExerciseUpdate`. This ensures flexibility in modifying only the required fields without affecting the others. +- **Includes validation**: Each update method validates inputs, ensuring that null values do not update existing fields. +- **Facilitates data retrieval**: The class provides getters such as `getCalories()`, `getWeight()`, and `getName()` for accessing specific details of the exercise. +- **Detailed representation**: The `toString()` method returns a formatted string summarizing the exercise, including its name, number of sets, reps, weight used, and calories burned. This makes it easy to display exercise details in user interfaces or reports. +- **Enables object comparison and consistent storage**: The `equals()` and `hashCode()` methods are overridden to allow for the comparison of `Exercise` objects and ensure consistency when storing them in collections. This helps in managing and tracking unique exercises within larger structures like days or programmes. + +The `ExerciseUpdate` component, + +- **Facilitates partial updates to Exercise objects**: The `ExerciseUpdate` class is designed to enable the modification of specific fields in an `Exercise` object. Each field in the `ExerciseUpdate` can be null, indicating that the corresponding attribute in the target `Exercise` should not be updated. +- **Holds update data for exercises**: The class includes fields such as `sets`, `reps`, `weight`, `calories`, and `name`, which can be used to selectively update an `Exercise`. This enables targeted updates without altering other unchanged fields. +- **Ensures flexibility in exercise management**: By accepting nulls for unmodified fields, the `ExerciseUpdate` class provides a flexible way to update only the required attributes of an `Exercise`, streamlining the process of making changes to specific exercise details. +- **Supports integration with update methods**: The `ExerciseUpdate` class can be passed as a parameter to methods in the `Exercise` class (e.g., `updateExercise()`), facilitating a seamless process for applying partial updates based on provided non-null values. +- **Simplifies exercise modification logic**: With this class, the logic for updating exercises is consolidated, simplifying the code and ensuring consistency when modifying `Exercise` objects in various contexts. + + +
+### Meal Component + +![Meal and MealList Class Diagram](./images/mealAndMealListClassDiagram.png) + +The `Meal` component, + +- **Represents individual meals with nutritional information:** The `Meal` class encapsulates details about a meal, specifically its name and calorie count. This allows easy tracking of individual meals within a day. +- **Attributes:** Each `Meal` object has two main attributes: `name` (the name of the meal) and `calories` (the calorie content of the meal). +- **Validation:** The class enforces constraints during instantiation, ensuring that the `name` cannot be null or empty and that the calorie count is non-negative, reducing errors in meal tracking. +- **Supports equality checks and hashing:** The `Meal` class overrides `equals()` and `hashCode()` methods to ensure that meals with identical names and calorie counts are considered equal, which is useful for meal comparison and for storing in collections like sets. +- **Detailed representation:** The `toString()` method of `Meal` provides a concise, readable summary of the meal’s details, including the name and calorie count in the format `"[Meal Name] | [Calories] kcal"`. This makes it easy to display meal information in logs, summaries, or user interfaces. + +The `MealList` component, + +- **Manages a collection of meals:** The `MealList` class provides functionality for managing a list of `Meal` objects, allowing for easy addition, deletion, and retrieval of meals throughout the day. +- **Provides collection-based functionality:** The class includes methods for common operations, such as checking if the list is empty, getting the total number of meals, and adding or deleting meals from the list. Each action is logged for traceability. +- **Supports detailed retrieval and representation:** The `getMeals()` method returns the list of all meals, and the `toString()` method generates a formatted string that lists each meal with its index, making it convenient for displaying meal information in a user interface or summary. +- **Ensures data consistency:** The `MealList` class handles edge cases, such as attempts to delete meals at invalid indexes, by throwing appropriate exceptions. This helps maintain data integrity within the list. +- **Facilitates efficient comparisons and storage:** The class overrides `equals()` and `hashCode()` methods, which enables comparison of two `MealList` objects and allows it to be used in collections, ensuring that meal tracking remains accurate and consistent. + +### Water Component + +![Water Class Diagram](./images/waterClassDiagram.png) + +The `Water` component, + +- **Tracks daily water intake:** The `Water` class allows for recording individual water consumption entries throughout the day, stored in liters. Each entry is logged, providing a detailed trace of daily water consumption. +- **Attributes:** The main attribute of the `Water` class is waterList, a list of Float values representing individual water intake entries in liters. +- **Validation and error handling:** When adding water entries, the `Water` class enforces that the water amount is positive. Deletion attempts with invalid indexes are handled with exceptions, ensuring safe and predictable usage. +- **Supports collection-based functionality:** The `Water` class includes methods for adding and deleting water entries, checking if the list is empty, and retrieving the entire list of entries. Each action is logged, allowing developers to track and troubleshoot any changes to the water intake log. +- **User-friendly representation:** The toString() method formats and returns a string representation of all water entries. Each entry is listed with an index, making it easy to display in user interfaces and summaries. +- **Efficient storage and retrieval:** The getWaterList() method returns the full list of water intake entries, while the class’s clear and consistent data structure facilitates straightforward water consumption tracking and data retrieval. + +### History Component + +![Class diagram of History Component](./images/historyComponent.png) + +- **Chronologically stores workout records**: The `History` class uses a `LinkedHashMap` to store workout records, mapping each `LocalDate` to a `DailyRecord` for that day. This data structure preserves insertion order, enabling sequential, date-based record management. + +- **Key Attributes**: + - **History Map**: The main attribute `history` is a `LinkedHashMap` storing each workout log, meal, and water intake record by date. + - **Logging**: A `Logger` instance for tracking actions, such as record creation, deletion, and retrieval, ensuring traceability within the class. + +- **Efficient data retrieval and management**: + - **Accessing Records by Date**: The `History` class allows retrieval of `DailyRecord` entries by specific dates, supporting users who wish to review or edit their fitness data for a particular day. + - **Adding and Deleting Records**: Users can log new workout records or delete existing entries, providing flexibility in managing their fitness history. + +- **Detailed representation for user interaction**: + - **Formatted Summaries**: Methods such as `getFormattedPersonalBests()` and `getWeeklyWorkoutSummary()` provide clear, formatted outputs, making it easy for users to read and interpret their progress. + - **String Representation**: The `toString()` method generates a readable output of all records, enhancing user experience in viewing historical data within the application interface. + +- **Ensures data integrity and consistency**: + - **Validation**: Before operations like deletion or retrieval, checks ensure records exist for specified dates, preventing errors in data handling. + - **Safe Updates and Comparisons**: By providing methods like `isBetter()` for comparing exercises, the `History` component ensures users’ records accurately reflect their achievements without duplication or inconsistencies. + +This structured and user-focused design of the `History` component empowers BuffBuddy users to track, manage, and assess their fitness journey effectively. + + + +The `DailyRecord` component, + +- **Tracks daily workout, meals, and water intake:** The `DailyRecord` class maintains a log of the day’s activities, meals consumed, + and water intake. It provides methods to add, update, and retrieve each of these records. +- **Encapsulates multiple data types:** `DailyRecord` works with various objects such as `Day` (for recording the workout programme), + `MealList` (for managing a list of meals consumed), and `Water` (for tracking daily water intake). These components are stored and managed + together within a single daily record. +- **Enables modification and deletion:** The class provides methods for adding meals and water to the record, updating the workout for the + day, and removing items such as meals or water entries. Each modification is logged for traceability. +- **Calculates key daily statistics:** `DailyRecord` is capable of calculating the total calories burned from the recorded `Day` and the + calories gained from the `MealList`. It can also sum the total water intake for the day. +- **Provides a comprehensive summary:** The class’s `toString()` method generates a detailed summary of the day’s activities, including + calories burned, meals eaten, water consumed, and the caloric balance, making it easy to retrieve and display all relevant information in a readable format. + +In summary, the `History` component manages a comprehensive log of workout records, enabling users to view, update, and delete daily entries, track personal bests, and generate weekly summaries. Its methods and attributes work together to provide a structured, accessible history of the user's fitness activities. + +### Storage Component + +![Diagram for Storage. FileManager Component](./images/storageAndFileManager.png) + +The `Storage` component, + +- **Handles the saving and loading of both `ProgrammeList` and `History` data in JSON format:** The `Storage` component is responsible + for serializing `ProgrammeList` and `History` objects into JSON format and passing them on to `FileManager`, as well as getting the + data in Json format from `FileManager` and deserializing it into the appropriate objects when needed. +- **Serves as an adapter between `FileManager` and `BuffBuddy` classes:** `Storage` acts as an intermediary, translating between the JSON + data handled by `FileManager` and the objects in the `BuffBuddy` application, ensuring seamless conversion between formats. +- **Relies on `ProgrammeList` and `History` from the Model component:** Since the `Storage` component is tasked with saving and retrieving + the `ProgrammeList` and `History` objects, it ensures the data is accurately represented and stored. +- **Utilizes custom serializers:** To properly handle date formats and other specific needs, Storage makes use of custom serializers for + objects like LocalDate from the `DateSerializer` class, ensuring that these objects are correctly serialized to and deserialized from JSON. + +The `FileManager` component, + +- **Manages the saving and loading of data:** The `FileManager` class is responsible for reading data from and writing data to the file + specified by the user. It ensures that both the `ProgrammeList` and `History` data are stored in JSON format, and retrieves them when needed. +- **Handles file creation and directory management:** Before saving data, `FileManager` checks whether the necessary directories and + files exist. If they do not, it creates them to ensure data can be stored correctly. +- **Leverages JSON for data structure:** `FileManager` uses `Gson` to serialize and deserialize JSON data, making it easy to work with + structured data. It also ensures the data is formatted in a readable way using pretty printing for clarity. +- **Performs error handling and logging:** `FileManager` employs detailed logging to track the progress of saving and loading operations. + If any issues arise during file operations (e.g., missing files, failed directory creation), they are logged, and exceptions are thrown to handle errors gracefully. + +The `DateSerializer` component, + +- **Custom serialization and deserialization for `LocalDate`**: The `DateSerializer` class provides a way to serialize and deserialize `LocalDate` objects to and from JSON strings formatted as `dd-MM-yyyy`. This ensures that date data in JSON format remains consistent and human-readable. +- **Implements `JsonSerializer` and `JsonDeserializer` interfaces**: The class implements both `JsonSerializer` and `JsonDeserializer` from the Gson library, allowing it to handle JSON conversion for `LocalDate` objects. +- **Uses a standardized date format**: The `DateTimeFormatter` is configured with the pattern `dd-MM-yyyy`, which ensures that all serialized and deserialized dates conform to this format. + + +### Parser Component + +![Class_Diagram_for_Factory_Component](images/parserComponent.png) + +The `Parser` component, + +- **Acts as the main entry point for interpreting user input**: The `Parser` class breaks down the command string into a + main command and arguments, identifies the relevant factory, and delegates command creation to the appropriate subcomponent. +- **Delegates command creation to `CommandFactory`**: By leveraging `CommandFactory`, `Parser` hands off the creation of `Command` + objects based on parsed command types and arguments, supporting extensibility for different command types. + + +The `CommandFactory` component, + +- **Centralizes command production**: The `CommandFactory` class is responsible for creating `Command` objects based on the command type in user input, providing a single access point for command creation. +- **Manages subcommand factories**: It delegates specific command creation tasks to sub-factories, including `ProgrammeCommandFactory`, `MealCommandFactory`, `WaterCommandFactory`, and `HistoryCommandFactory`, based on the parsed command. If the command is unsupported, it returns an `InvalidCommand`. + + +The `ProgrammeCommandFactory` component, + +![Class_Diagram_for_ProgrammeCommandFactory_Component](images/progFactoryComponent.png) + +- **Processes program-related commands**: This factory handles commands related to creating, viewing, editing, starting, deleting, + and logging programs within the application. +- **Parses and prepares complex program structures**: It includes helper methods to interpret hierarchical program structures, + allowing users to create and modify workout programs with days and exercises. It also supports commands with complex flags, ensuring flexibility in program management. +- _Note:_ Since `ProgrammeCommandFactory` is responsible for creating a wide variety of commands, the class diagram has been simplified by using the superclass `Command` class to + represent all sub-command classes that are actually created. + + + +The `MealCommandFactory` component, + +![Class_Diagram_for_MealCommandFactory_Component](images/mealFactoryComponent.png) + + +- **Parses meal-related commands**: This factory handles commands for adding, deleting, and viewing meals, providing a structured + way to manage dietary information within the application. +- **Validates and processes flagged arguments**: It uses `FlagParser` to interpret and validate command flags for meal-related attributes, + such as name, calories, and date. This ensures that inputs are correctly structured and validated before creating meal commands. + + +The `WaterCommandFactory` component, + +![Class_Diagram_for_WaterCommandFactory_Component](images/waterFactoryComponent.png) + +- **Handles water tracking commands**: This factory parses commands related to adding, deleting, and viewing water entries, allowing + users to track their daily water intake. +- **Ensures valid water-related input**: It uses `FlagParser` to validate command flags, ensuring that water volume and date inputs are + correctly provided. + + +The `HistoryCommandFactory` component, + +![Class_Diagram_for_HistoryCommandFactory_Component](images/historyFactoryComponent.png) + +- **Generates history-related commands**: This factory handles commands for viewing, listing, deleting history entries, and managing + personal bests and weekly summaries. +- **Interprets user commands and arguments for history management**: It uses helper methods (e.g., `prepareViewHistoryCommand`) to + parse user commands and arguments, constructing the corresponding `Command` objects for various history-related operations. + + +The `FlagParser` component, + +![Class_Diagram_for_FlagParser_Component](images/flagParserComponent.png) + +- **Interprets flagged arguments in command strings**: This class provides advanced parsing of flagged arguments, supporting flexible + parsing and retrieval of values by flags, aliases, and data types (integer, float, date). +- **Validates flags for correct command structure**: `FlagParser` ensures that required and unique flags are present, allowing flexible + command input through aliases while enforcing structure. + + +The `ParserUtils` component, + +![Class_Diagram_for_ParserUtils_Component](images/parserUtilsComponent.png) + +- **Provides utility methods for parsing tasks**: This class offers helper methods for argument splitting, number parsing, index validation, + and date formatting, simplifying common parsing tasks. +- **Handles date and number validation**: It includes specialized methods for parsing dates and numbers, ensuring valid input for commands + requiring these data types. + + +The `FlagDefinitions` component, + +![Class_Diagram_for_FlagDefinitions_Component](images/flagDefinitionsComponent.png) + +- **Defines standard command flags**: This class contains constants representing command flags, establishing a standard set of flags used + across the application. +- **Validates flags quickly**: By storing valid flags in a set (`VALID_FLAGS`), `FlagDefinitions` allows for efficient validation during + command parsing. + + + + +### Command Component + +#### Overview + +To interact with BuffBuddy, the user's input commands are parsed into discrete `Command` objects that have the sole responsibility of accomplishing that task. + +As BuffBuddy contains many commands and thus many types of `Command` subclasses, the following diagram presents a simplified representation of the various `Command` classes: + +![Summary of Command classes](images/commandSummary.png) + +Each abstract subclass of `Command` represents a generalization of the various commands available to BuffBuddy. In the following sections, each abstract class and their respective purposes will be elaborated on. + +#### Programme Commands + +`ProgrammeCommand` is an abstract class for all `Command` classes that interact with `ProgrammeList` and its encapsulated data. +The following diagram documents all `ProgrammeCommand` subclasses. + +![Summary of Programme classes](images/programmeCommandSummary.png) + +`EditProgrammeCommand` classes are a subset of `ProgrammeCommand` classes that focus specifically on editing the internal `ProgrammeList` data. As this data is concerned only with `ProgrammeList`, `EditCommand#execute()` has been narrowed through method overloading to only take in `ProgrammeList` as a parameter. + +![Summary of Edit classes](images/editCommandSummary.png) + +#### Meal Commands + +`MealCommand` is an abstract class for all `Command` classes that interact with meal-related data within the application. These commands allow users to log, edit, and manage their meal entries, ensuring that their dietary information is accurately tracked and updated. The following diagram documents all `MealCommand` subclasses. + +![Summary of Meal classes](images/mealCommandSummary.png) + +#### Water Commands + +`WaterCommand` is an abstract class for all `Command` classes that interact with water-related data within the application. These commands allow users to log, edit, and manage their water intake entries, ensuring that their hydration information is accurately tracked and updated. The following diagram documents all `WaterCommand` subclasses. + +![Summary of Water classes](images/waterCommandSummary.png) + +#### History Commands + +`HistoryCommand` is an abstract class for all `Command` classes that interact with `History` data within the application. These involve viewing weekly summaries, viewing their recorded data and getting their personal bests for each exercise. The following diagram documents all `HistoryCommand` subclasses. + +![Summary of History classes](images/historyCommandSummary.png) + +### Common Component + +![Class_Diagram_for_Common_Component](images/commonUtilsComponent.png) + +`common` package contains `Utils` class that is used across the multiple packages for validation and formatting. + +--- + + +## Implementation + +### Create Programme + +#### Overview + +#### Overview +The **Create Programme** feature allows users to create a new workout programme. Users can either create a simple programme +with just a name or design a multi-day schedule containing various exercises with details such as sets, reps, weight, and calories. +This feature enables users to personalize their workout plans according to their fitness goals. + +These operations include: +- Parsing the programme name and optional day/exercise details. +- Creating and organizing Day and Exercise objects within the programme. +- Adding the completed programme to ProgrammeList. + +#### Example Usage +Given below is an example usage scenario for 'create programme' and how the create programme command functions at each step. + +**Step 1**: The user executes the command `programme create Starter /d 1 /e Push-Ups /e Squats` to create a new programme named "Starter" with one day containing two exercises: "Push-Ups" and "Squats". + +**Step 2**: After parsing this input, a `CreateProgrammeCommand` is created. + +**Step 3**: The command then calls `ProgrammeCommandFactory#prepareCreateCommand()` to parse the details of the programme. + +**Step 4**: Inside `prepareCreateCommand`, the programme name and day details are parsed. For each day specified: +- `ProgrammeCommandFactory#parseDay()` is called to create a new `Day` object. +- For each exercise in the day, `ProgrammeCommandFactory#parseExercise()` is called to create an `Exercise` object with the specified details. +- Each created `Exercise` is added to the `Day` object. + +**Step 5**: The `CreateProgrammeCommand` then calls `ProgrammeList#insertProgramme()` with the parsed programme name and list of days to add the new programme to `ProgrammeList`. + +**Step 6**: The created `Programme` object is returned to `CreateProgrammeCommand`. + +**Step 7**: The `CreateProgrammeCommand` formats a message indicating successful creation of the programme. + +**Step 8**: The formatted message is included in a `CommandResult`, which is returned to the user interface. + +**Step 9**: The user interface displays the result message to the user, confirming the successful creation of the programme. + +#### Sequence Diagram + +![Sequence Diagram for createProgramme feature](./images/createProgramme.png) + + +_Note_: Happy path is assumed in the sequences diagram. Error handling has been simplified to keep the diagram brief. +Generally, if a conditional check fails (i.e. if the Programme Name is missing), a ProgrammeException will be thrown and +interrupt the command execution. BuffBuddy will print the appropriate error message based on the Exception and then wait for the next command. + + + +### Start Programme + +#### Overview + +The **Start Programme** feature allows users to start a specific workout programme. This sets the programme as the active programme, which other commands will default to if no programme is explicitly specified. + +#### Example Usage + +Given below is an example usage scenario for 'start programme' and how the start programme command functions at each step. + +**Step 1:** The user has a list of workout programmes stored in `ProgrammeList`. Each programme may contain multiple days and exercises. + +**Step 2:** The user executes the command `programme start 1` to start the first programme in the list. + +**Step 3:** After parsing this input, a `StartProgrammeCommand` is created and executed. + +**Step 4:** The command then calls `ProgrammeList#startProgramme()` with the given programme index to set the programme as active. + +**Step 5:** The `Programme` object that was started is returned to the `StartProgrammeCommand`. + +**Step 6:** The `StartProgrammeCommand` formats the details of the started programme into a message. + +**Step 7:** The formatted message is included in a `CommandResult`, which is returned to the user interface. + +**Step 8:** The user interface displays the result message to the user, confirming the successful activation of the programme. + +#### Sequence diagram + +![](images/startProgramme.png) + +### View Programme + +#### Overview + +The **View Programme** feature allows users to view the details of a specific programme. + +#### Example Usage + +Given below is an example usage scenario for 'view programme' and how the view programme command functions at each step. + +**Step 1:** The user has a list of workout programmes stored in `ProgrammeList`. Each programme may contain multiple days and exercises. + +**Step 2:** The user executes the command `programme view 1` to view the first programme in the list. + +**Step 3:** After parsing this input, a `ViewProgrammeCommand` is created and executed. + +**Step 4:** The command then calls `ProgrammeList#getProgramme()` with the given programme index to retrieve the programme from the list. + +**Step 5:** The retrieved `Programme` object is returned to the `ViewProgrammeCommand`. + +**Step 6:** The `ViewProgrammeCommand` formats the details of the retrieved programme into a message. + +**Step 7:** The formatted message is included in a `CommandResult`, which is returned to the user interface. + +**Step 8:** The user interface displays the result message to the user, showing the details of the selected programme. + +#### Sequence Diagram + +![](images/viewProgramme.png) + +### Delete Programme + +#### Overview + +The **Delete Programme** feature allows users to delete created programmes from the programme list. + +#### Example Usage + +Given below is an example usage scenario for 'delete programme' and how the delete programme command functions at each step. + +**Step 1:** The user has a list of workout programmes stored in `ProgrammeList`. Each programme may contain multiple days and exercises. + +**Step 2:** The user executes the command `programme delete 1` to delete the first programme in the list. + +**Step 3:** After parsing this input, a `DeleteProgrammeCommand` is created and executed. + +**Step 4:** The command then calls `ProgrammeList#deleteProgram()` with the given programme index to remove the programme from the list. + +**Step 5:** The deleted `Programme` object is returned to the `DeleteProgrammeCommand`. + +**Step 6:** The `DeleteProgrammeCommand` formats the details of the deleted programme into a message. + +**Step 7:** The formatted message is included in a `CommandResult`, which is returned to the user interface. + +**Step 8:** The user interface displays the result message to the user, confirming the successful deletion of the programme. + +#### Sequence Diagram + +![Delete Programme Sequence Diagram](images/deleteProgramme.png) + +### Edit Programme + +#### Overview + +The **Edit Programme** feature allows for in-depth management of programme structures, supporting operations to add, remove, and update days and exercises within each programme. + +To perform an edit to any aspect of this data, the EditCommand will traverse the ProgrammeList and its nested data structures until it reaches the necessary depth to perform its edit operation. + +These operations include: + +- Adding or removing Days to the Programme +- Adding or removing Exercises to Days in the Programme +- Updating the details of Exercises in Days in the Programme + +##### Example Usage + +Given below is an example usage scenario for 'delete exercise' and how the edit programme functions at each step. + +Step 1. The user creates a programme with a given number of Days with their respective Exercises. ProgrammeList will contain a reference to this programme after its creation. + +Step 2. The user executes `programme edit /p 1 /d 1 /xe 1` to delete the first exercise in the first day of the first programme. + +Step 3. After parsing this input, a `DeleteExerciseCommand` (inheriting from the generic `EditProgrammeCommand`) is created and executed. + +Step 4. The command first retrieves the chosen Programme with `ProgrammeList#getProgramme()`. + +Step 5. The command then retrieves the chosen Day with `Programme#getDay()`. + +Step 6. With the Day object, it performs the `Day#deleteExercise()` with the given exercise ID + +Step 7. The deleted Exercise object is then returned to the `DeleteExerciseCommand` to display as part of the returned `CommandResult`. + +#### Sequence Diagram + +The overall design that enables this functionality is described generically by the following sequence diagram. + +![Edit Command generic sequence](images/editCommand.png) + +The `Model` class in the above diagram is a generalization of the various data models that are being interacted with +to perform each specific edit command. For each edit command, the following sequence diagrams +further break down how this interaction works. + +In each diagram, error handling has been simplified to keep the diagram brief. +Generally, if a conditional check fails (i.e. if the selected `Programme` does not exist), a `ProgrammeException` will be thrown and interrupt the command execution. `BuffBuddy` will print the appropriate error message based on the Exception and then wait for the next command. + +##### Add day + +![Add/Remove Day](images/addDayCommand.png) + +##### Add exercise + +![Add/Remove Exercise](images/addExerciseCommand.png) + +##### Update exercise + +![Edit Exercise](images/editExerciseCommand.png) + +#### Activity Diagram + +To summarize, the following activity diagram describes how the overall operation occurs. + +![Edit Command Diagram](images/editCommandActivityDiagram.png) + + + + +### Add Meal + +#### Overview + +The **Add Meal** feature manages the functionality related to adding meals to a daily record. It interacts with various components such as `History`, `DailyRecord`, and `MealList` to ensure meals are added correctly. + +The **Add Meal** command navigates through the following hierarchy: + +- **History** → **DailyRecord** → **MealList** +- If a `DailyRecord` does not exist for a given date, it is created before adding the meal. +- Similarly, a new `MealList` object is created and added to the `DailyRecord` if it doesn't already exist. The meal is then added to the `MealList` object. + +These operations include: + +- Adding meals to a `MealList` in the `DailyRecord` of a particular date in the `History`. + +Given below is an example usage scenario for adding a meal and how the add meal command functions at each step. + +#### Example Usage + +**Step 1**: The user starts by adding a meal using the command: + +meal add /n [mealName] /c [calories] + +- The command is parsed and translated into an `AddMealCommand` object, which contains the meal object that is created as a wrapper for the name and calories. + +**Step 2**: The command retrieves the `DailyRecord` for the specified date from the `History` using `getRecordByDate()`. If no record exists, a new one is created. + +**Step 3**: The `AddMealCommand` adds the meal to the `MealList` of the `DailyRecord`. The `MealList` is then updated with the new list. + +**Step 4**: The newly added `Meal` object is displayed as part of the `CommandResult`. + +The overall design that enables this functionality is described generically by the following sequence diagram. + +#### Sequence Diagram for "Add Meal" Command + +![Add Meal Sequence Diagram](images/addMealSequenceDiagram.png) + +The diagram shows the interactions among different classes and objects during the execution of the "Add Meal" command. + +The following sequence diagrams shows the interactions between the necessary classes during the execution of the "Delete Meal" and "View meal" commands. All the parser classes interactions has been encapsulated into the 'parser' class for readability and simplification. The parser interactions remain the same for all the 3 features. + +#### Sequence Diagram for "Delete Meal" Command + +![Delete Meal Sequence Diagram](images/deleteMealSequenceDiagram.png) + +#### Sequence Diagram for "View Meal" Command + +![View Meal Sequence Diagram](images/viewMealSequenceDiagram.png) + +#### Activity Diagram for "Add Meal" Feature + +![Add Meal Activity Diagram](images/addMealActivityDiagram.png) + +#### Summary of Feature + +The **Add Meal** feature uses a **hierarchical command pattern** to manage meal additions while maintaining good encapsulation and separation of concerns. The chosen design allows easy extensibility and maintainability. + + + +### Add Water + +The **Add Water** feature manages the functionality related to adding water to a daily record. It interacts with various components such as `History`, `DailyRecord`, and `Water` to ensure water are added correctly. + +The Add Water command navigates through the following hierarchy: +- **History** → **DailyRecord** → **Water** +- If a `DailyRecord` does not exist for a given date, it is created before adding the water. + +These operations include: +- Adding a water log to `Water` in the `DailyRecord` of a particular date in `History`. +- +Given below is an example usage scenario for adding a water log and how to add water command functions at each step. + +#### Example Usage + +**Step 1**: The user starts by adding a water log using the command: + +water add /v WATER_VOLUME [/t Date] + +- The command is parsed and translated into an `AddWaterCommand` object. Water contains an arrayList of floats, representing ml of water. + +**Step 2**: The command retrieves the `DailyRecord` for the specified date from the `History` using `getRecordByDate()`. If no record exists, a new one is created. + +**Step 3**: The `AddWaterCommand` adds the water log to the `Water` of the `DailyRecord`. The `Water` is then updated with the new water log. + +**Step 4**: The newly added water log object is displayed as part of the `CommandResult`. + +The overall design that enables this functionality is described generically by the following sequence diagram. + +![Add Water Sequence Diagram](images/addWaterSequenceDiagram.png) + +The diagram shows the interactions among different classes and objects during the execution of the "Add Water" command. + + +#### Sequence Diagram for "Delete Water" Command + +The following sequence diagrams (Delete Water, View Water) follow the same structure as the Add Water sequence diagram. In these diagrams, the section where `addWaterToRecord(waterToAdd)` is called is replaced with the respective method for each action. + +#### Sequence Diagram for "Delete Water" Command + + +![Delete Water Sequence Diagram](images/deleteWaterSequenceDiagram.png) + +#### Sequence Diagram for "View Water" Command + +![View Water Sequence Diagram](images/viewWaterSequenceDiagram.png) + +#### Activity Diagram for "Add Water" Feature + +![Add Water Activity Diagram](images/addWaterActivityDiagram.png) + +#### Summary of Feature + +The **Add Water** feature uses a **hierarchical command pattern** to manage water additions while maintaining good encapsulation and separation of concerns. The chosen design allows easy extensibility and maintainability. + + + +### WeeklySummary Feature + +The Weekly Summary feature allows users to view a summary of their workouts for the current week. This functionality is achieved through a combination of several interconnected components, including `WeeklySummaryCommand`, `Parser`, `HistoryCommandFactory`, and `History`. Users can access this feature through the `history wk` command in the UI. The implementation follows a command pattern, combined with the factory pattern for command creation. + +### Overview + +The following components are crucial to the Weekly Summary feature: + +1. **Parser Component** + The `Parser` interprets the initial command and directs the flow as follows: + + - **`Parser#parse(String)`**: Accepts the raw input string, splits it into the main command and arguments. + - **`CommandFactory`**: Generates the appropriate command object based on the parsed input. + - **`HistoryCommandFactory`**: Handles the creation of history-related commands, including `WeeklySummaryCommand`. + +2. **WeeklySummaryCommand Component** + The `WeeklySummaryCommand` implements the `Command` interface and performs the following: + + - Extends the abstract `Command` class. + - Uses the command word `"wk"`. + - Executes by retrieving the weekly summary from the `History` object. + - Returns a `CommandResult` that contains the formatted summary for display. + +3. **History Component** + The `History` class manages workout data and provides: + + - **`getWeeklyWorkoutSummary()`**: Retrieves and formats the workout data for the current week. + +### Example Usage + +The following example illustrates the usage scenario and behavior of the Weekly Summary feature: + +1. **Step 1**: The user enters the `"history wk"` command in the UI. The UI reads this command and passes it to the `Parser`. +2. **Step 2**: The `Parser` breaks down the command `"history wk"` into: + - Main command: `"history"` + - Subcommand: `"wk"` +3. **Step 3**: The `Parser` uses `CommandFactory`, which recognizes this as a history command and delegates to `HistoryCommandFactory`. +4. **Step 4**: `HistoryCommandFactory` identifies `"wk"` as the `WeeklySummaryCommand` trigger and creates a new `WeeklySummaryCommand` instance. +5. **Step 5**: The `WeeklySummaryCommand` is passed back through the chain to the UI, which then calls its `execute` method. +6. **Step 6**: During execution: + - `WeeklySummaryCommand` calls `History`'s `getWeeklyWorkoutSummary()`. + - The summary is formatted and wrapped in a `CommandResult`. + - The UI displays the result to the user. + +### Sequence Diagram + +![Sequence Diagram for WeeklySummary feature](./images/weeklySummarySequenceDiagram.png) + + + +### Log Programme Feature + +The Log Programme feature allows users to log a specific day of a workout programme into the history on a specified date. This feature is implemented using components like `LogProgrammeCommand`, `Parser`, `ProgrammeCommandFactory`, and `History`. Users can activate this feature by entering the `prog log` command in the UI. The implementation follows the command pattern, alongside a factory pattern for creating commands. + +### Overview + +The following component is crucial to the Log Programme feature: + +1. **LogProgrammeCommand Component** + The `LogProgrammeCommand` is responsible for logging a workout day from a programme into the history and provides: + + - Extends the abstract `Command` class. + - Uses the command word `"log"`. + - Executes by retrieving the specified day from the `ProgrammeList` and logging it to the `History`. + - Returns a `CommandResult` containing a success message or relevant feedback. + +### Example Usage + +The following example illustrates the usage scenario and behavior of the Log Programme feature: + +1. **Step 1**: The user enters the `prog log /p [PROG_INDEX] /d [DAY_INDEX] /t [DATE]` command in the UI. The UI reads this command and passes it to the `Parser`. +2. **Step 2**: The `Parser` breaks down the command `prog log` into: + - Main command: `prog` + - Subcommand: `log` +3. **Step 3**: The `Parser` uses `CommandFactory`, which recognizes this as a programme command and delegates to `ProgrammeCommandFactory`. +4. **Step 4**: `ProgrammeCommandFactory` identifies `log` as the `LogProgrammeCommand` trigger and creates a new `LogProgrammeCommand` instance with the specified parameters. +5. **Step 5**: The `LogProgrammeCommand` is passed back through the chain to the UI, which then calls its `execute` method. +6. **Step 6**: During execution: + - `LogProgrammeCommand` retrieves the programme and day specified by the user from `ProgrammeList`. + - It then logs the day to the `History` object using `History`'s `getRecordByDate()` and `logRecord()` methods. + - The result is formatted in a `CommandResult`. + - The UI displays the result to the user. + +### Sequence Diagram + +![Sequence Diagram for Log Programme feature](./images/logProgrammeSequenceDiagram.png) + + + +### Save/Load Feature + +The save/load mechanism is handled by three main components: `Storage`, `FileManager`, and `DateSerializer`. `FileManager` manages file interactions, including reading from and writing to JSON data files, while `Storage` handles the conversion between JSON objects and `ProgrammeList`/`History` objects. The `DateSerializer` is used for converting `LocalDate` to/from JSON format. + +### Example Usage + +Given below is an example usage scenario and how the save/load mechanism behaves at each step. + +**Step 1.** The user launches the application for the first time. A `Storage` object is initialized by `BuffyBuddy`, and it attempts to load data from +the file using `FileManager`. If no data file exists, `Storage` initializes an empty `ProgrammeList` and `History`. + +**Step 2.** The user interacts with the application by adding programmes or logging workout activities and meals, modifying both the +`ProgrammeList` and `History`. After each command is carried out and when the user chooses to exit the application, `Storage#saveData()` is called. + +**Step 3.** At this point, `Storage` converts the current `ProgrammeList` and `History` into JSON format using the `createJSON()` method and passes +the `JsonObject` to `FileManager#save()`. + +**Step 4.** The `FileManager` saves the updated `JsonObject` to the data file, ensuring that the user's changes are preserved for the +next command or session. If necessary, `FileManager#createDirIfNotExist()` and `FileManager#createFileIfNotExist()` ensure that the correct directories +and files are in place before saving. + +**Step 5.** The next time the user launches the application, `Storage#loadProgrammeList()` and `Storage#loadHistory()` are called, which +load the data from the file via `FileManager#load()`. The loaded data is then converted from JSON back into `ProgrammeList` and `History` +objects, restoring the user's previous session. + +The following sequence diagram shows how a load operation for ProgrammeList goes through the Storage component: + +![Sequence Diagram for Load operation](./images/loadProgrammeListSequenceDiagram.png) + +The following sequence diagram shows how a save operation goes through the Storage component: + +![Sequence Diagram for Save operation](./images/saveSequenceDiagram.png) + +--- + +## Documentation, logging, testing, configuration, dev-ops + +* [Logging Guide](LoggingGuide.md) +* [Testing Guide](TestingGuide.md) + +## Appendix + +### Product scope + +BuffBuddy is a fitness tracking app that help you track workout, meals, water to aid you in achieving your body goals. -## Product scope ### Target user profile -{Describe the target user profile} +Gym goers who need a quick way to create, manage and track their workout plans and progress. ### Value proposition -{Describe the value proposition: what problem does it solve?} +- Users will be able to quickly create, update and view their workout programmes +- Users will be able to track their progress as they progress on their fitness journey +- Users will be able to track water and calorie intake to better track their nutrition + + ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|------------------------|----------------------------------------------------|-------------------------------------------------------| +| v1.0 | fitness enthusiast | create a new workout programme/routine | tailor my workout to fit my needs | +| v1.0 | fitness enthusiast | set a programme as active | default to this programme when logging workouts | +| v1.0 | fitness enthusiast | add a workout day to my programme | structure my programme with specific workout days | +| v1.0 | fitness enthusiast | add exercises to a workout day | define the exercises and goals for that day | +| v1.0 | fitness enthusiast | edit my existing fitness routine | further customize my routines after making them | +| v2.0 | fitness enthusiast | update exercise details like weight, sets, or reps | adjust my routine based on progress or goals | +| v1.0 | fitness enthusiast | delete a workout entry | remove mistakenly created logs | +| v1.0 | fitness enthusiast | delete a fitness routine if I no longer use it | ensure my routines remain relevant and organized | +| v2.0 | fitness enthusiast | delete a workout day or exercise from a programme | keep my programme up to date with relevant exercises | +| v1.0 | fitness enthusiast | log my workout for a specific day | keep track of my progress and activities | +| v1.0 | fitness enthusiast | view my routine when I begin my workout | follow my plan more effectively | +| v2.0 | fitness enthusiast | view a specific workout record | review my activities and progress on a particular day | +| v2.0 | fitness enthusiast | view all my workout programmes | have a quick overview of all available programmes | +| v2.0 | progress tracking user | view a summary of my weekly workout activity | measure my overall progress | +| v2.0 | progress tracking user | track my personal bests for each exercise | see improvements over time | +| v2.0 | nutrition-focused user | track calories burned during my workout | align my fitness routine with my dietary goals | +| v2.0 | nutrition-focused user | add a meal I ate | track my meals and caloric intake | +| v2.0 | nutrition-focused user | delete a meal I logged | remove incorrect meal entries | +| v2.0 | nutrition-focused user | view my meals on a certain date | see how many calories I consumed | +| v2.0 | nutrition-focused user | view a caloric balance in the history view | understand my net calorie intake and expenditure | +| v2.0 | hydration-focused user | add my water intake | track my water intake for each day | +| v2.0 | hydration-focused user | view my water intake | see how much water I have consumed across days/week | +| v2.0 | hydration-focused user | delete a water intake | remove any mistakes made when inputting water intake | +| v2.0 | user | exit BuffBuddy | close the program after completing my activities | + ## Non-Functional Requirements -{Give non-functional requirements} +- Ensure that you have Java 17 or above installed. +- Program is built to support single user only ## Glossary -* *glossary item* - Definition +- **Exercise**: An exercise defined by a name, number of reps and sets, weight and average calories burned. +- **Day**: A ‘workout day’ is a collection of exercises to be done together. +- **Programme**: A programme is a collection of workout days. +- **Daily Record**: A daily record contains a user's workout activity, food intake and water intake for any given day. + + ## Instructions for manual testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +Here’s a structured manual testing guide for BuffBuddy based on the app's user guide and aligned with the reference format you provided. + +--- + +### **Manual Testing Guide for BuffBuddy** + + + +#### **Initial Launch** + +- Download the BuffBuddy JAR file and place it in an empty folder. +- Launch the application by using `java -jar BuffBuddy.jar` in the terminal. + +#### **Adding and Managing Programmes** + +1. **Adding a New Programme** + - **Command**: `prog create PROG_NAME` + - **Example**: `prog create Starter` + - **Expected Outcome**: + - A confirmation message with the programme name appears, e.g., "New programme created: Starter". + +2. **Setting an Active Programme** + - **Command**: `prog start [PROG_INDEX]` + - **Example**: `prog start 1` + - **Expected Outcome**: + - The specified programme is marked as "Active". + +3. **Listing All Programmes** + - **Command**: `prog list` + - **Expected Outcome**: + - A list of all programmes is shown with an indication of which is currently active. + +4. **Deleting a Programme** + - **Command**: `prog delete [PROG_INDEX]` + - **Example**: `prog delete 1` + - **Expected Outcome**: + - The specified programme is deleted, and if it was the active one, another becomes active. + +--- + +#### **Adding and Managing Workout Days** + +1. **Adding a New Day to a Programme** + - **Command**: `prog edit /p PROG_INDEX /ad DAY_NAME` + - **Example**: `prog edit /p 1 /ad Cardio` + - **Expected Outcome**: + - A new day, e.g., "Cardio," is added to the specified programme. + +2. **Deleting a Day from a Programme** + - **Command**: `prog edit /p PROG_INDEX /xd DAY_INDEX` + - **Example**: `prog edit /p 1 /xd 1` + - **Expected Outcome**: + - The specified day is removed from the programme. + +--- + +#### **Adding, Updating, and Deleting Exercises** + +1. **Adding an Exercise to a Day** + - **Command**: `prog edit /p PROG_INDEX /d DAY_INDEX /ae /n EXERCISE_NAME /s SETS /r REPS /w WEIGHT /c CALORIES` + - **Example**: `prog edit /p 1 /d 1 /ae /n Push Up /s 3 /r 15 /w 0 /c 50` + - **Expected Outcome**: + - The exercise details are added to the specified day. + +2. **Updating an Exercise** + - **Command**: `prog edit /p PROG_INDEX /d DAY_INDEX /ue EXERCISE_INDEX [args]` + - **Example**: `prog edit /p 1 /d 1 /ue 1 /r 12` + - **Expected Outcome**: + - The updated exercise details are shown. + +3. **Deleting an Exercise** + - **Command**: `prog edit /p PROG_INDEX /d DAY_INDEX /xe EXERCISE_INDEX` + - **Example**: `prog edit /p 1 /d 1 /xe 1` + - **Expected Outcome**: + - The specified exercise is removed from the list for that day. + +--- + +#### **Recording and Viewing Workouts** + +1. **Logging a Workout** + - **Command**: `prog log /p PROG_INDEX /d DAY_INDEX [/t DATE]` + - **Example**: `prog log /p 1 /d 1 /t 01-01-2024` + - **Expected Outcome**: + - A confirmation message displays, showing the exercises completed and calories burned. + +2. **Viewing Workout History** + - **Command**: `history list` + - **Expected Outcome**: + - A list of all recorded workout sessions, with dates and summaries, is displayed. + +--- + +#### **Tracking and Viewing Meals** + +1. **Adding a New Meal** + - **Command**: `meal add /n MEAL_NAME /c CALORIES [/t DATE]` + - **Example**: `meal add /n Chicken Breast /c 300 /t 01-01-2024` + - **Expected Outcome**: + - Confirmation that the meal has been added, with calories shown. + +2. **Viewing Meals** + - **Command**: `meal view [DATE]` + - **Example**: `meal view 01-01-2024` + - **Expected Outcome**: + - List of meals for the specified date, showing names and calories. + +--- + +#### **Managing Water Logs** + +1. **Adding a Water Log** + - **Command**: `water add /v WATER_VOLUME [/t DATE]` + - **Example**: `water add /v 500 /t 01-01-2024` + - **Expected Outcome**: + - Confirmation that the water log has been added, showing volume. + +2. **Viewing Water Logs** + - **Command**: `water view [DATE]` + - **Expected Outcome**: + - List of water logs for the date, showing volumes in liters. + +--- + +#### **Personal Best and Summary Views** + +1. **View Personal Best for an Exercise** + - **Command**: `history pb EXERCISE_NAME` + - **Example**: `history pb Bench Press` + - **Expected Outcome**: + - Display of the user's best record for the specified exercise. + +2. **View Weekly Summary** + - **Command**: `history wk` + - **Expected Outcome**: + - A summary of workouts, meals, and water logs for the past week. + +--- + +#### **Data Management and Error Handling** + +1. **Corrupted Data File Simulation** + - **Steps**: + - Edit or corrupt the data file (e.g., remove keys). + - Re-launch BuffBuddy. + - **Expected Outcome**: + - BuffBuddy should initialize with an empty data file, treating the user as a new entry. + +2. **Exiting the Application** + - **Command**: `bye` + - **Expected Outcome**: + - BuffBuddy exits gracefully with a confirmation message. + diff --git a/docs/LoggingGuide.md b/docs/LoggingGuide.md new file mode 100644 index 0000000000..bf1115a953 --- /dev/null +++ b/docs/LoggingGuide.md @@ -0,0 +1,31 @@ +# Logging Guide + +We are using the `java.util.logging` package for logging within this project. + +## Obtaining Loggers +Each class can obtain its `Logger` using `LogsCenter.getLogger(Class)`, which configures log messages according to the specified logging level. + +## Log Output +Log messages are sent to both the console and a designated `.log` file, ensuring that logging data is accessible in real-time and for later review. + +## Logging Levels +This project uses multiple log levels to categorize the types of messages logged: + +- **INFO**: Used for general messages that indicate the normal flow of the application, like the creation of objects (e.g., "MealList created with an empty list"). +- **WARNING**: Employed when handling errors or potentially problematic situations, such as invalid input indices (e.g., logging a warning when an invalid index is passed to delete a meal). +- **SEVERE**: Not yet observed in the provided code, but typically reserved for serious issues that might prevent the application from continuing. +- **DEBUG/FINER**: While not explicitly used in the examples provided, can be added for more granular traceability, especially in methods with complex logic or decision paths. + +## Logging Conventions and Examples + +- **Action Logging**: Each action in critical methods, such as adding or deleting entries (e.g., meals or exercises), is logged to track user interactions and changes in data. +- **Validation Logging**: Log messages are used when inputs are validated, ensuring that any issues with input values are easily traceable. +- **Return Value Logging**: Methods that perform key actions often log the outcomes, such as the final list of entries after additions or deletions. This practice makes it easier to verify that operations are completing as expected. + +## Best Practices for Logging + +- When logging at **INFO** level, focus on messages that reflect major steps or state changes within the application, helping to provide a high-level overview. +- Use **WARNING** level logging to capture recoverable issues or cases where an action does not proceed as expected. +- Add **SEVERE** logging if you introduce error-handling for critical failures. + +For guidance on log message formatting and additional logging practices, refer to [Java: Logging conventions](https://github.com/se-edu/guides). \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..eebccaf527 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,9 @@ -# Duke +# BuffBuddy + +BuffBuddy is your all-in-one fitness tracking companion, designed to help you streamline and organize your workout routines. +Whether you’re planning custom programmes, tracking daily exercises, or logging your workout history, BuffBuddy keeps everything in one place. +Build personalized workout plans, log progress, and stay motivated with an intuitive interface that’s perfect for fitness enthusiasts of all levels. -{Give product intro here} Useful links: * [User Guide](UserGuide.md) diff --git a/docs/TestingGuide.md b/docs/TestingGuide.md new file mode 100644 index 0000000000..d239f59dc1 --- /dev/null +++ b/docs/TestingGuide.md @@ -0,0 +1,105 @@ +# Testing Guide + +This guide provides instructions on how to run tests for the project, as well as details on the types of tests included and best practices for writing additional tests. + +## Running Tests + +There are two main ways to run tests in this project. + +### Method 1: Using IntelliJ JUnit Test Runner +- To run all tests, right-click on the `src/test/java` folder and choose **Run 'All Tests'**. +- To run a specific test or subset of tests, right-click on a test package, test class, or individual test, and select **Run 'TestName'**. + +### Method 2: Using Gradle +- Open a terminal and navigate to the project root directory. +- Run the following command to clean and execute all tests: + + ```bash + ./gradlew clean test + ``` + + > Note: On Windows, use `gradlew.bat clean test` instead. + +- **:link: Link**: For more details on using Gradle for testing, refer to the [Gradle Tutorial](https://github.com/se-edu/guides) from `se-edu/guides`. + +## Types of Tests + +This project uses several types of tests to ensure comprehensive coverage of functionality and integration. + +### 1. Unit Tests +- **Description**: Tests the smallest units of code, typically individual methods or classes. +- **Purpose**: Verifies that each method or class works as expected in isolation. +- **Examples**: + - `AddMealCommandTest` to test the `AddMealCommand` functionality. + - `DeleteMealCommandTest` to validate the `DeleteMealCommand` execution and error handling. + +### 2. Integration Tests +- **Description**: Tests the interaction between multiple classes or modules. +- **Purpose**: Ensures that different components work together correctly. +- **Examples**: + - `MealCommandTest` to validate the execution of meal-related commands with dependencies on `History` and `ProgrammeList`. + - `CommandFactoryTest` to check that different command factories produce the correct commands based on input strings. + +### 3. Hybrid Tests (Unit and Integration) +- **Description**: Tests multiple units together and verifies their connection and interaction. +- **Purpose**: Combines unit and integration testing to validate functionality at a higher level. +- **Examples**: + - `ViewMealCommandTest` to test `ViewMealCommand` interactions with `DailyRecord` and `MealList`. + +## Testing Conventions and Examples + +The following conventions are recommended for writing and organizing tests: + +### Test Naming Conventions +- Use descriptive names to clearly state what each test is validating. For example: + - `testExecuteHappyPath` to indicate a test that checks successful execution. + - `testExecuteEdgeCaseNullDailyRecord` to indicate a test for a specific edge case. + +### Structure and Assertions +- **Arrange, Act, Assert**: Organize tests using this structure: + 1. **Arrange**: Set up the necessary objects and mocks. + 2. **Act**: Call the method under test. + 3. **Assert**: Verify the expected outcome using assertions like `assertEquals` and `assertThrows`. +- **Assertions**: Ensure that you use meaningful assertions to validate expected outcomes. Examples include: + - `assertEquals(expected, actual)` for comparing results. + - `assertThrows(Exception.class, () -> { ... })` for checking exceptions. + +### Mocking and Verification +- **Mockito** is used for creating mock objects and verifying interactions. Examples include: + - **Mock Initialization**: Use `@Mock` annotations and initialize with `MockitoAnnotations.openMocks(this)`. + - **Behavior Setup**: Define behavior for mocks, such as `when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord)`. + - **Verification**: Confirm that certain methods were called, such as `verify(mockDailyRecord).addMealToRecord(sampleMeal)`. + +## Common Test Scenarios + +- **Happy Path**: Verify that commands and methods execute as expected in normal conditions. +- **Edge Cases**: Handle cases that may cause errors, such as `null` values, invalid indices, or out-of-bound IDs. +- **Constructor Tests**: Ensure constructors handle both valid and invalid inputs properly. +- **Command Execution**: For each command, ensure that: + - Expected interactions with dependencies (e.g., `History`, `DailyRecord`) occur. + - Correct responses or `CommandResult` messages are returned. + +## Example Tests Breakdown + +### AddMealCommandTest +- **Happy Path**: `testExecuteHappyPath` verifies that `AddMealCommand` adds a meal and returns the correct success message. +- **Edge Case**: `testExecuteEdgeCaseNullDailyRecord` checks that an `AssertionError` is thrown if `DailyRecord` is `null`. + +### DeleteMealCommandTest +- **Happy Path**: `testExecuteHappyPath` confirms that `DeleteMealCommand` removes the meal and returns the correct success message. +- **Edge Case**: `testExecuteEdgeCaseInvalidIndex` checks for `IndexOutOfBoundsException` when an invalid index is used. + +### CommandFactoryTest +- **Factory Pattern**: Tests that `CommandFactory` returns the correct `Command` type based on the input command string. +- **Happy Path**: Validates each command creation, such as `createExitCommand` returning an `ExitCommand`. +- **Invalid Input**: `testCreateInvalidCommand` ensures that an unknown command returns an `InvalidCommand`. + +## Best Practices for Writing Tests + +1. **Write Clear and Isolated Tests**: Each test should focus on a single functionality or scenario. +2. **Test Edge Cases**: Include tests for common edge cases to ensure robustness, such as invalid inputs or `null` values. +3. **Use Descriptive Assertions**: Ensure that assertions provide clear, readable results and messages. +4. **Keep Tests Independent**: Avoid dependencies between tests to allow each test to run independently without side effects. +5. **Run Tests Regularly**: Run all tests frequently to catch potential issues early. + +This testing guide will help you structure, organize, and expand your test cases effectively for comprehensive project coverage. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d6cf4c3b3a..95e9792030 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,812 @@ + + # User Guide +## Table of Contents + +1. [Introduction](#introduction) +2. [Quick Start](#quick-start) +3. [Features](#features) + - [1. Add New Programme](#1-add-new-programme) + - [2. Set Programme as Active](#2-set-programme-as-active) + - [3. List All Programmes](#3-list-all-programmes) + - [4. View Programme](#4-view-programme) + - [5. Delete Programme](#5-delete-programme) + - [6. Add Day to Programme](#6-add-day-to-programme) + - [7. Delete Day from Programme](#7-delete-day-from-programme) + - [8. Add Exercise in Programme](#8-add-exercise-in-programme) + - [9. Delete Exercise from Programme](#9-delete-exercise-from-programme) + - [10. Update Exercise in Programme](#10-update-exercise-in-programme) + - [11. Log Workout](#11-log-workout) + - [12. Add New Meal](#12-add-new-meal) + - [13. View Meals](#13-view-meals) + - [14. Delete Meal](#14-delete-meal) + - [15. Add New Water Log](#15-add-new-water-log) + - [16. View Water Logs](#16-view-water-logs) + - [17. Delete Water Log](#17-delete-water-log) + - [18. View History](#18-view-history) + - [19. View Specific Record](#19-view-specific-record) + - [20. View Weekly Summary](#20-view-weekly-summary) + - [21. View PB for an Exercise](#21-view-pb-for-an-exercise) + - [22. View PBs for All Exercises](#22-view-pbs-for-all-exercises-) + - [23. Delete Record](#23-delete-record-) + - [24. Exiting BuffBuddy](#24-exit-buffbuddy) +4. [Data Storage](#data-storage) +5. [FAQ](#FAQ) +6. [Alias Table](#alias-table) +7. [Command Summary](#command-summary) + ## Introduction -{Give a product intro} +BuffBuddy is your all-in-one fitness tracking companion, designed to help you streamline and organize your workout routines. +Whether you’re planning custom programmes, tracking daily exercises, or logging your workout history, BuffBuddy keeps everything in one place. +Build personalized workout plans, log progress, and stay motivated with an intuitive interface that’s perfect for fitness enthusiasts of all levels. ## Quick Start -{Give steps to get started quickly} - 1. Ensure that you have Java 17 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `BuffBuddy` from [here](https://github.com/AY2425S1-CS2113-W10-3/tp/releases/tag/v1.0). +3. Open a command terminal, cd into the folder you put the jar file in, and use the `java -jar BuffBuddy.jar` command to run the application. + +4. Open a command terminal +5. Navigate into the folder you put the jar file in +6. Use the `java -jar BuffBuddy.jar` command to run the application. + + + +## Features + +> ### Notes on Command format +> +> Text written in `SCREAMING_SNAKE_CASE` are command parameters to be supplied by the user. +> +> Text preceded with a `/` will be read as flags. `/` is a reserved character and is not to be used as part of a parameter. e.g. `meal add /n Choc/Pie /c 200` will throw an invalid flag error. +> +> Square brackets `[...]` indicate optional parameters. e.g. `history view` and `history view 11-11-2024` are both valid. +> +> Flagged parameters can be supplied in any order. e.g. `meal add /n Pie /c 200` is equivalent to `meal add /c 200 /n Pie` +> +> Parameters can include spaces. e.g. `meal add /n Chicken Rice /c 200` is a valid command. +> +> All flags have aliases. Refer to the [alias table](#Alias-Table) to see the alternative options available for each flag. +> +> For date parameters, dates should be supplied in the `dd-MM-yyyy` format. e.g. `11-11-2024` +> +> Providing extra flags that are not specified by the User Guide will be ignored by BuffBuddy. e.g. `meal add /n Pie /c 200 /w 30` will produce the same result as `meal add /n Pie /c 200`. + + +> ### Terminology +> +> **Exercise**: A weighted exercise defined by a name, number of reps and sets, weight and average calories burned. +> +> **Day**: A ‘workout day’ is a collection of exercises to be done together. +> +> **Programme**: A programme is a collection of workout days. +> +> **Daily Record**: A daily record contains a user's workout activity, food intake and water intake for any given day. + +### 1. Add New Programme + +This feature adds a new empty workout programme with a specified name. + +**Command**: `prog create PROG_NAME` + +**Example**: `prog create Starter` + +``` +======================================== +New programme created: +Starter +======================================== +``` +_Note_: Advanced users can create a detailed programme with multiple days and exercises in one step by using the following command structure. + +This allows users to add specific exercises with sets, reps, weight, and calorie details for each day: + +**Command**: `prog create PROG_NAME /d DAY_NAME /e /n EXERCISE_NAME /s SETS /r REPS /w WEIGHT /c CALORIES /e ...` + +**Example**: `prog create Advanced Starter /d Monday /e /n Bench Press /s 3 /r 15 /w 30 /c 200 /e /n Squat /s 3 /r 15 /w 50 /c 200 /d Wednesday /e /n Bicep Curl /s 3 /r 10 /w 10 /c 100` + +``` +================================================== +New programme created: +Advanced Starter + +Day 1: Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal + +Day 2: Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal +================================================== +``` +> **Warning:** users should note that copy pasting this directly into terminal will break it into a multi-line input, which BuffBuddy does not recognize as a valid command. +> +> Please copy paste the command into a text editor such as Notepad and fix the formatting before pasting it into the terminal. + +--- + +### 2. Set Programme as Active + +This feature sets the specified programme as the "active programme". + +Once a programme is active, other commands will default to this programme if `PROG_INDEX` is not provided for those commands. + +> **Notes on Active behaviour** +> +> If the active programme is deleted, it will reset to the first programme (if exists). +> +> If there were previously no programmes and one is newly created, it will be automatically set as the active programme. + +**Command**: `prog start [PROG_INDEX]` + +**Example**: `prog start 2` + +``` +================================================== +Started programme: +Advanced Starter + +Day 1: Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal + +Day 2: Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal +================================================== +``` + +--- + +### 3. List All Programmes + +This feature displays a list of all workout programmes created by the user, showing each programme’s name and index. + +**Command**: `prog list` + +**Example**: `prog list` + +``` +================================================== +Listing programmes: +1. Starter +2. Advanced Starter -- Active +================================================== +``` + +--- + +### 4. View Programme + +This feature displays the detailed workout routine for the specified programme, organized by day. +Each exercise entry includes its name, number of sets and reps, weight, and estimated calorie burn. + +**Command**: `prog view [PROG_INDEX]` + +If `PROG_INDEX` is not specified, the command defaults to displaying the details of the currently active programme. + +**Example**: `prog view 2` + +``` +================================================== +Viewing programme: +Advanced Starter + +Day 1: Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal + +Day 2: Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal +================================================== +``` + +--- + +### 5. Delete Programme + +This feature deletes the programme at the specified index. + +_Note_: If the programme deleted was the active programme, the 1st programme will be set to active. + +**Command**: `prog delete [PROG_INDEX]` + +If `PROG_INDEX` is not specified, the command defaults to deleting the currently active programme. + +**Example**: `prog delete 2` + +``` +================================================== +Deleted: +Advanced Starter + +Day 1: Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal + +Day 2: Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal +================================================== +``` + +--- + + + +### 6. Add Day to Programme + +This feature adds a new day to the specified existing programme. + +**Command**: `prog edit [/p PROG_INDEX] /ad DAY_NAME` + +If `PROG_INDEX` is not specified, the command defaults to editing the current active programme. + +**Example**: `prog edit /p 1 /ad Cardio` + +``` +================================================== +Created new day: Cardio +================================================== +``` + +_Note_: Advanced users can directly create a day with multiple exercises and add it to an existing programme using the following command: + +**Command**: `prog edit [/p PROG_INDEX] /ad DAY_NAME /e /n EXERCISE_NAME /s SETS /r REPS /w WEIGHT /c CALORIES /e ...` + +**Example**: `prog edit /p 1 /ad Cardio /e /n Dumbbell squat /w 10 /r 15 /s 10 /c 100 /e /n Kettlebell swing /w 10 /r 15 /s 10 /c 100` + +> **Warning:** users should note that copy pasting this directly into terminal will break it into a multi-line input, which BuffBuddy does not recognize as a valid command. +> +> Please copy paste the command into a text editor such as Notepad and fix the formatting before pasting it into the terminal. + +``` +================================================== +Created new day: Cardio +1. Dumbbell squat: 10 sets of 15 at 10kg | Burnt 100 kcal +2. Kettlebell swing: 10 sets of 15 at 10kg | Burnt 100 kcal +================================================== +``` + +--- + +### 7. Delete Day from Programme + +This feature deletes the specified day from the specified existing programme. + +**Command**: `prog edit [/p PROG_INDEX] /xd DAY_INDEX` + +If `PROG_INDEX` is not specified, the command defaults to editing the current active programme. + +**Example**: `prog edit /p 1 /xd 1` + +``` +================================================== +Deleted day: +Cardio +================================================== +``` + +--- + +### 8. Add Exercise in Programme + +This feature adds a new exercise to the specified existing day in the specified existing programme. + +**Command**: `prog edit [/p PROG_INDEX] /d DAY_INDEX /ae /n EXERCISE_NAME /w WEIGHT /r REPS /s SETS /c CALORIES` + +If `PROG_INDEX` is not specified, the command defaults to editing the current active programme. + +**Example**: `prog edit /p 1 /d 2 /ae /n Lateral Pulldown /w 30 /r 15 /s 3 /c 100` + +``` +================================================== +Created new exercise: +Lateral Pulldown: 3 sets of 15 at 30kg | Burnt 100 kcal +================================================== +``` + +--- + +### 9. Delete Exercise from Programme + +This feature deletes the specified exercise from the specified existing day in the specified existing programme. + +**Command**: `prog edit [/p PROG_INDEX] /d DAY_INDEX /xe EXERCISE_INDEX` + +If `PROG_INDEX` is not specified, the command defaults to editing the current active programme. + +**Example**: `prog edit /p 1 /d 3 /xe 2` + +``` +================================================== +Deleted exercise 1: +Kettlebell swing: 10 sets of 15 at 10kg | Burnt 100 kcal +================================================== +``` + +--- + +### 10. Update Exercise in Programme + +This feature updates the specified exercise in the specified existing day of the specified existing programme. + +**Command**: `prog edit [/p PROG_INDEX] /d DAY_INDEX /ue EXERCISE_INDEX [args]` + +If `PROG_INDEX` is not specified, the command defaults to editing the current active programme. + +`[args]` must have at least 1 of the arguments below. + +- **`/w WEIGHT`**: Sets the weight for the exercise on update. +- **`/r REPS`**: Sets the repetitions for the exercise on update. +- **`/s SETS`**: Sets the number of sets for the exercise on update. +- **`/n NAME`**: Sets the name of the exercise on update. +- **`/c CALORIES`**: Sets the calories of the exercise on update. + +**Example**: `prog edit /p 1 /d 2 /ue 1 /w 8 /r 15` + +``` +================================================== +Updated exercise: Bicep Curl: 3 sets of 15 at 8kg | Burnt 100 kcal +================================================== +``` + +--- + + + +### 11. Log Workout + +This feature records the successful completion of a workout for the specified day within the chosen programme. + +**Command**: `prog log [/p PROG_INDEX] /d DAY_INDEX [/t DATE]` + +If `PROG_INDEX` is not specified, the command defaults to deleting the currently active programme. + +If `DATE` is not specified, the command defaults to the current date at the time of logging. + +**Example**: `prog log /p 2 /d 1 /t 07-11-2024` + +``` +================================================== +Congrats! You've successfully completed: +Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal +================================================== +``` + +--- + + + +### 12. Add New Meal + +This feature adds a meal to the daily record of the specific date. + +**Command**: `meal add /n MEAL_NAME /c CALORIES [/t DATE]` + +If `DATE` is not specified, the command defaults to the current date at the time of adding. + +**Example**: `meal add /n Chicken Breast /c 250 /t 30-10-2024` + +``` +================================================== +Chicken Breast | 250kcal has been added +================================================== +``` + +--- + +### 13. View Meals + +This feature displays all meals recorded for the specific date. + +**Command**: `meal view [DATE]` -## Features +If `DATE` is not specified, the command defaults to the current date at the time of viewing. -{Give detailed description of each feature} +**Example**: `meal view 30-10-2024` -### Adding a todo: `todo` -Adds a new item to the list of todo items. +``` +================================================== +Meals for 30-10-2024: -Format: `todo n/TODO_NAME d/DEADLINE` +1: Chicken Breast | 250kcal +2: Scrambled Eggs | 150kcal +================================================== +``` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +--- -Example of usage: +### 14. Delete Meal -`todo n/Write the rest of the User Guide d/next week` +This feature deletes the meal at the specified index from the daily record of the specific date. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +**Command**: `meal delete /m MEAL_INDEX [/t DATE]` +If `DATE` is not specified, the command defaults to the current date at the time of deleting. + +**Example**: `meal delete /m 1 /t 30-10-2024` + +``` +================================================== +Chicken Breast | 250kcal has been deleted +================================================== +``` + +--- + + + +### 15. Add New Water Log + +This feature adds a water log to the daily record of the specific date. + +Command: `water add /v WATER_VOLUME [/t DATE]` + +`WATER_VOLUME` is stored as a floating point number. + +If `DATE` is not specified, the command defaults to the current date at the time of adding. + +**Example**: `water add /v 200.2 /t 30-10-2024` + +``` +================================================== +200.2 liters of water has been added +================================================== +``` + +--- + +### 16. View Water Logs + +This feature displays all water logs recorded for the specific date. + +**Command**: `water view [DATE]` + +If `DATE` is not specified, the command defaults to the current date at the time of viewing. + +**Example**: `water view 30-10-2024` + +``` +================================================== +Water intake for 30-10-2024: + +1: 200.2 +================================================== +``` + +--- + +### 17. Delete Water Log + +This feature deletes the water log at the specified index from the daily record of the specific date. + +**Command**: `water delete /w WATER_INDEX [/t DATE]` + +If `DATE` is not specified, the command defaults to the current date at the time of deleting. + +**Example**: `water delete /w 1 /t 30-10-2024` + +``` +================================================== +200.2 liters of water has been deleted +================================================== +``` + +--- + +### 18. View History + +This feature displays a comprehensive record of workouts, meals, and water intake for each logged day. + +**Command**: `history list` + +**Example**: `history list` + +``` +================================================== +Completed On: 07-11-2024 + +Day: +Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal + +Total Calories burnt: 400 kcal + +Meals: +No Meals. + +Water Intake: +1: 500.3 +Total Water Intake: 500.3 liters + +Caloric Balance: -400 kcal + +============== + +Completed On: 11-11-2024 + +Day: +Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal + +Total Calories burnt: 100 kcal + +Meals: +1: Chicken Breast | 250kcal +2: Scrambled Eggs | 150kcal +Total Calories from Meals: 400 kcal + +Water Intake: +1: 200.2 +Total Water Intake: 200.2 liters + +Caloric Balance: 300 kcal +================================================== +``` + +--- + + + +### 19. View Specific Record + +This feature displays the recorded information for the specified day. + +**Command**: `history view [DATE]` + +If `DATE` is not specified, the command defaults to the current date at the time of viewing. + +**Example**: `history view 11-11-2024` + +``` +================================================== +Day: +Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal + +Total Calories burnt: 100 kcal + +Meals: +1: Chicken Breast | 250kcal +2: Scrambled Eggs | 150kcal +Total Calories from Meals: 400 kcal + +Water Intake: +1: 200.2 +Total Water Intake: 200.2 liters + +Caloric Balance: 300 kcal +================================================== +``` + +--- + +### 20. View Weekly Summary + +This feature displays a summary of workouts, meals, and water intake for the past week. + +**Command**: `history wk` + +**Example**: `history wk` + +``` +================================================== +Your weekly workout summary: +Monday +1. Bench Press: 3 sets of 15 at 30kg | Burnt 200 kcal +2. Squat: 3 sets of 15 at 50kg | Burnt 200 kcal +Completed On: 07-11-2024 + +Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal +Completed On: 11-11-2024 +================================================== +``` + +--- + +### 21. View PB for an Exercise + +This feature displays the personal best for the specified exercise. + +**Command**: `history pb EXERCISE_NAME` + +_Note_: `EXERCISE_NAME` is not case-sensitive. + +**Example**: `history pb bench press` + +``` +================================================== +Personal best for bench press: 3 sets of 15 at 30kg +================================================== +``` + +--- + +### 22. View PBs for All Exercises + +This feature displays personal bests for all the exercises. + +**Command**: `history pb` + +**Example**: `history pb` + +``` +================================================== +Personal bests for all exercises: +Bench Press: 3 sets of 15 at 30kg +Squat: 3 sets of 15 at 50kg +Bicep Curl: 3 sets of 10 at 10kg +================================================== +``` + +--- + +### 23. Delete Record + +This feature deletes the record at the specified date. + +**Command**: `history delete [DATE]` + +If `DATE` is not specified, the command defaults to the current date at the time of deleting. + +**Example**: `history delete 11-11-2024` + +``` +================================================== +Deleted record: +Day: +Wednesday +1. Bicep Curl: 3 sets of 10 at 10kg | Burnt 100 kcal + +Total Calories burnt: 100 kcal + +Meals: +1: Chicken Breast | 250kcal +2: Scrambled Eggs | 150kcal +Total Calories from Meals: 400 kcal + +Water Intake: +1: 200.2 +Total Water Intake: 200.2 liters + +Caloric Balance: 300 kcal +================================================== +``` + + + +### 24. Exit BuffBuddy + +This feature exits and closes the programme. + +**Command**: `bye` + +**Example**: `bye` + +``` +================================================== +Exiting BuffBuddy... +================================================== +Bye. Hope to see you again soon! +``` + +--- + +___ + + + +## Data Storage + +BuffBuddy uses a JSON file to store user data, ensuring persistence across sessions. + +### Saving your data + +- Saving is done automatically after each user command. It does not need to be manually triggered by a command. +- All records, including logged days, meals, and water intake, are saved in a structured format within a designated file (./data/data.json). +- The JSON format is human-readable, allowing users to view their stored data easily if needed. + +### Loading your data + +- Loading happens automatically when BuffBuddy initializes. +- If the structure of the JSON file has been tampered with (e.g., removing the "programmeList" key or using {} as the entire content), the program will handle this scenario by treating the user as a first-time user and initializing a fresh data file. +- If any data values within the JSON file are found to be invalid (e.g., negative numbers where only positive values are allowed), the specific section containing corrupted data (either the `ProgrammeList` or `History`) will be re-initialized to be empty. + +### Editing the data file + +- Users can directly edit the data file to easily change their records or import data from another file +- Users should note that they need to first exit BuffBuddy before making their changes. If the data file is edited while actively entering commands into BuffBuddy, the contents of the file will be overwritten. + + +--- + + ## FAQ -**Q**: How do I transfer my data to another computer? +1. **How can I back up my data?** + + - BuffBuddy saves data in a JSON file located at `./data/data.json`. You can create a backup by copying this file to + another location. + +2. **What happens if I accidentally delete or corrupt the data file?** + + - If the data file is deleted or corrupted, BuffBuddy will reset your program list and history to prevent data issues. + However, restoring a backup of the JSON file (if you have one) can also recover your data. + +3. **Can I add exercises that don’t involve weights?** + + - BuffBuddy currently only supports weighted exercises. Exercises like jumping jacks or other body weight exercises cannot + be added without a weight parameter. + +4. **What is the caloric balance in the history view?** -**A**: {your answer here} + - The caloric balance shows the difference between the calories burned through exercise and the calories consumed through + meals, helping you monitor your energy intake and expenditure. + +5. **What happens if I input invalid values for commands?** + - BuffBuddy performs basic validation for parameters. Negative values or missing required parameters will prompt an error, + and the command won’t be executed. Ensure all required fields are filled correctly. + +--- + +## Alias Table + +| Flag | Alias | +|------|-------------------| +| /p | /prog, /programme | +| /d | /day | +| /t | /date | +| /n | /name | +| /e | /exercise, /ex | +| /s | /set, /sets | +| /r | /rep, /reps | +| /w | /weight | +| /c | /calories | +| /ae | /addEx | +| /ue | /updateEx | +| /xe | /removeEx | +| /ad | /addDay | +| /xd | /removeDay | +| /m | /meal | +| /w | /water | +| /v | /volume, /vol | + +--- ## Command Summary -{Give a 'cheat sheet' of commands here} +| Command | Format | Example | +|-------------------------------------------|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| **Add Programme** | `prog create PROG_NAME` | `prog create Starter` | +| **Add Detailed Programme** | `prog create PROG_NAME /d DAY_NAME /e /n EXERCISE_NAME /s SETS /r REPS /w WEIGHT /c CALORIES` | `prog create Starter /d Monday /e /n Bench Press /s 3 /r 12 /w 30 /c 100` | +| **Set Active Programme** | `prog start [PROG_INDEX]` | `prog start 1` | +| **List Programmes** | `prog list` | `prog list` | +| **View Programme** | `prog view [PROG_INDEX]` | `prog view 1` | +| **Delete Programme** | `prog delete [PROG_INDEX]` | `prog delete 1` | +| **Add Day to Programme** | `prog edit [/p PROG_INDEX] /ad DAY_NAME` | `prog edit /p 1 /ad Cardio Day` | +| **Delete Day from Programme** | `prog edit [/p PROG_INDEX] /xd DAY_INDEX` | `prog edit /p 1 /xd 1` | +| **Add Exercise to Programme** | `prog edit [/p PROG_INDEX] /d DAY_INDEX /ae /n EXERCISE_NAME /w WEIGHT /r REPS /s SETS /c CALORIES` | `prog edit /p 1 /d 1 /ae /n Push Up /w 30 /r 15 /s 3 /c 100` | +| **Delete Exercise from Programme** | `prog edit [/p PROG_INDEX] /d DAY_INDEX /xe EXERCISE_INDEX` | `prog edit /p 1 /d 1 /xe 1` | +| **Update Exercise in Programme** | `prog edit [/p PROG_INDEX] /d DAY_INDEX /ue EXERCISE_INDEX [args]` | `prog edit /p 1 /d 1 /ue 1 /w 30 /r 12` | +| **Log Workout** | `prog log [/p PROG_INDEX] /d DAY_INDEX [/t DATE]` | `prog log /p 1 /d 1 /t 12-10-2024` | +| **Add Meal** | `meal add /n MEAL_NAME /c CALORIES [/t DATE]` | `meal add /n Chicken Breast /c 250 /t 30-10-2024` | +| **View Meals** | `meal view [DATE]` | `meal view 30-10-2024` | +| **Delete Meal** | `meal delete /m MEAL_INDEX [/t DATE]` | `meal delete /m 1 /t 30-10-2024` | +| **Add Water Log** | `water add /v WATER_VOLUME [/t DATE]` | `water add /v 200.2 /t 30-10-2024` | +| **View Water Logs** | `water view [DATE]` | `water view 30-10-2024` | +| **Delete Water Log** | `water delete /w WATER_INDEX [/t DATE]` | `water delete /w 1 /t 30-10-2024` | +| **View History** | `history list` | `history list` | +| **View Specific Record** | `history view [DATE]` | `history view 30-10-2024` | +| **View Weekly Summary** | `history wk` | `history wk` | +| **View Personal Best for an Exercise** | `history pb EXERCISE_NAME` | `history pb bench press` | +| **View Personal Bests for All Exercises** | `history pb` | `history pb` | +| **Delete Record** | `history delete [DATE]` | `history delete 30-10-2024` | +| **Exit BuffBuddy** | `bye` | `bye` | + +--- + -* Add todo `todo n/TODO_NAME d/DEADLINE` diff --git a/docs/diagrams/DailyRecordClass.puml b/docs/diagrams/DailyRecordClass.puml new file mode 100644 index 0000000000..e462cc77c4 --- /dev/null +++ b/docs/diagrams/DailyRecordClass.puml @@ -0,0 +1,30 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class DailyRecord { + - getCaloriesFromMeal() : int + - getTotalWaterIntake() : float + + getDayFromRecord() : Day + + getMealList() : MealList + + getWater() : Water + + logDay(day : Day) : void + + addMealToRecord(meal : Meal) : void + + deleteMealFromRecord(index : int) : Meal + + addWaterToRecord(water : Water) : void + + removeWaterFromRecord(index : int) : Water + + toString() : String +} + +class Day +class MealList +class Water +class Meal + +DailyRecord "1" --> "1" Day : day +DailyRecord "1" --> "1" MealList : mealList +DailyRecord "1" --> "1" Water : water + +DailyRecord ..> Meal + +@enduml diff --git a/docs/diagrams/addDayCommand.puml b/docs/diagrams/addDayCommand.puml new file mode 100644 index 0000000000..9b618ba027 --- /dev/null +++ b/docs/diagrams/addDayCommand.puml @@ -0,0 +1,26 @@ +@startuml +participant ":CreateDayProgrammeCommand" as CreateDayProgrammeCommand +participant ":ProgrammeList" as ProgrammeList +participant ":Programme" as Programme + +-> CreateDayProgrammeCommand : execute() +activate CreateDayProgrammeCommand + +CreateDayProgrammeCommand -> ProgrammeList : getProgramme(programmeIndex) +activate ProgrammeList + +alt Programme exists + ProgrammeList --> CreateDayProgrammeCommand : selected Programme + deactivate ProgrammeList + + CreateDayProgrammeCommand -> Programme : insertDay(createdDay) + activate Programme + + Programme --> CreateDayProgrammeCommand : inserted Day + deactivate Programme + else Programme does not exist +end + +<-- CreateDayProgrammeCommand : inserted Day +deactivate CreateDayProgrammeCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/addExerciseCommand.puml b/docs/diagrams/addExerciseCommand.puml new file mode 100644 index 0000000000..9df4367949 --- /dev/null +++ b/docs/diagrams/addExerciseCommand.puml @@ -0,0 +1,35 @@ +@startuml +participant ":CreateExerciseCommand" as AddExerciseCommand +participant ":ProgrammeList" as ProgrammeList +participant ":Programme" as Programme +participant ":Day" as Day + +-> AddExerciseCommand : execute() +activate AddExerciseCommand + +AddExerciseCommand -> ProgrammeList : getProgramme(programmeIndex) +activate ProgrammeList + +alt Programme exists + ProgrammeList --> AddExerciseCommand : selected Programme + deactivate ProgrammeList + + AddExerciseCommand -> Programme : getDay(dayIndex) + activate Programme + + alt Day exists + Programme --> AddExerciseCommand : selected Day + deactivate Programme + + AddExerciseCommand -> Day: insertExercise(createdExercise) + activate Day + Day --> AddExerciseCommand: inserted Exercise + deactivate Day + else Day does not exist + end + else Programme does not exist +end + +<-- AddExerciseCommand : insert Exercise +deactivate AddExerciseCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/addMealActivityDiagram.puml b/docs/diagrams/addMealActivityDiagram.puml new file mode 100644 index 0000000000..ac2ce36054 --- /dev/null +++ b/docs/diagrams/addMealActivityDiagram.puml @@ -0,0 +1,21 @@ +@startuml +start + +:User inputs "meal add /n [mealName] /c [calories]"; + +:Create new Meal object; +-> [Command Parsing] Parse input command; +:Create AddMealCommand object; + +-> [History Interaction] Check if DailyRecord exists for the date; +if (DailyRecord exists?) then (Yes) + :Retrieve existing DailyRecord; +else (No) + :Create new DailyRecord; + -> Add new DailyRecord to History; +endif + +-> Add Meal to MealList; +:Return success message to User; +stop +@enduml diff --git a/docs/diagrams/addMealSequenceDiagram.puml b/docs/diagrams/addMealSequenceDiagram.puml new file mode 100644 index 0000000000..221be6ecb9 --- /dev/null +++ b/docs/diagrams/addMealSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +actor User +participant ":UI" as UI +participant ":BuffBuddy" as BB +participant ":Parser" as Parser +participant ":AddMealCommand" as AddCmd +participant ":History" as History +participant ":DailyRecord" as DailyRec +participant ":MealList" as MealList +participant ":Meal" as Meal + +User -> UI: "meal add /n [mealName] /c [calories]" +activate UI +BB -> UI: readCommand() +activate BB +UI --> BB: command object +BB -> Parser: parse(command) +activate Parser + +create Meal +Parser -> Meal: new Meal(mealname, calories) +activate Meal +Meal --> Parser: meal object +deactivate Meal + +create AddCmd +Parser --> AddCmd: AddMealCommand(meal, date) +deactivate Parser +activate AddCmd + +AddCmd -> History: getRecordByDate(date) +activate History + +alt DailyRecord exists + History --> AddCmd: DailyRecord object +else No DailyRecord exists +create DailyRec + History -> DailyRec: new DailyRecord() + activate DailyRec + DailyRec --> History + deactivate DailyRec + History --> AddCmd +end +deactivate History + +AddCmd -> DailyRec: +activate DailyRec + +DailyRec -> MealList: addMealToRecord(meal) +activate MealList +MealList -> MealList: addMeal(meal) +activate MealList + +deactivate MealList +MealList --> DailyRec +deactivate MealList + + +DailyRec --> AddCmd +deactivate DailyRec + +AddCmd --> BB: CommandResult object +deactivate AddCmd + +BB -> UI: Show CommandResult +deactivate BB +destroy AddCmd +destroy Meal + +UI -> User: Display success message +deactivate UI +@enduml diff --git a/docs/diagrams/addWaterActivityDiagram.puml b/docs/diagrams/addWaterActivityDiagram.puml new file mode 100644 index 0000000000..7625515778 --- /dev/null +++ b/docs/diagrams/addWaterActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +start + +:User inputs "water add /v WATER_VOLUME /t Date"; + +-> [Command Parsing] Parse input command; +:Create AddWaterCommand object; + +-> [History Interaction] Check if DailyRecord exists for the date; +if (DailyRecord exists?) then (Yes) + :Retrieve existing DailyRecord; +else (No) + :Create new DailyRecord; + -> Add new DailyRecord to History; +endif + +-> Add Water log to Water; +:Return success message to User; +stop +@enduml \ No newline at end of file diff --git a/docs/diagrams/addWaterSequenceDiagram.puml b/docs/diagrams/addWaterSequenceDiagram.puml new file mode 100644 index 0000000000..6d97483faa --- /dev/null +++ b/docs/diagrams/addWaterSequenceDiagram.puml @@ -0,0 +1,67 @@ +@startuml +actor User +participant ":UI" as UI +participant ":BuffBuddy" as BB +participant ":Parser" as Parser +participant ":AddWaterCommand" as AddCmd +participant ":History" as History +participant ":DailyRecord" as DailyRec + +User -> UI: "water add /v WATER_VOLUME /t Date" +activate UI +BB -> UI: readCommand() +activate BB +UI --> BB: command object +BB -> Parser: parse(command) +activate Parser +Parser -> Parser : Parse inputs +note right +parser interactions are put under +the parser for simplification +end note +activate Parser +deactivate Parser +create AddCmd +Parser --> AddCmd: new AddWaterCommand(waterToAdd, date) +deactivate Parser + +activate AddCmd + +AddCmd -> History: getRecordByDate(date) +activate History + +alt DailyRecord does not exist + create DailyRec + History -> DailyRec: new DailyRecord() + activate DailyRec + DailyRec --> History + History --> AddCmd + deactivate DailyRec +else No DailyRecord exists + History --> AddCmd: DailyRecord object +end +deactivate History + +AddCmd -> DailyRec: addWaterToRecord(waterToAdd) +activate DailyRec + +DailyRec -> Water : addWater(waterVolume) +activate Water + +Water --> DailyRec +deactivate Water + + +DailyRec --> AddCmd +deactivate DailyRec + +AddCmd --> BB: CommandResult object +deactivate AddCmd +destroy AddCmd + +BB -> UI: Show CommandResult +deactivate BB + +UI -> User: Display success message +deactivate UI +@enduml \ No newline at end of file diff --git a/docs/diagrams/commandSummary.puml b/docs/diagrams/commandSummary.puml new file mode 100644 index 0000000000..3def052f42 --- /dev/null +++ b/docs/diagrams/commandSummary.puml @@ -0,0 +1,54 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class Command <>{ + + execute(programmes: ProgrammeList, history: History): CommandResult + + equals(other: Object): boolean +} + +class ExitCommand { +} + +class InvalidCommand { +} + +class CommandResult { + - message: String + + CommandResult(message: String) + + getMessage(): String + + equals(other: Object): boolean +} + +class ProgrammeCommand <>{ + # programmeIndex: int + # dayIndex: int + + equals(other: Object): boolean +} + +class EditProgrammeCommand <>{ + # exerciseIndex: int + + execute(programmes: ProgrammeList): CommandResult +} + +class WaterCommand <>{ + # date: LocalDate +} + + class MealCommand <>{ + # date: LocalDate +} + + class HistoryCommand <>{ +} + +Command <|-- ProgrammeCommand +Command <|-- ExitCommand +Command <|-- InvalidCommand +Command <|-- WaterCommand +Command <|-- MealCommand +Command <|-- HistoryCommand +ProgrammeCommand <|-- EditProgrammeCommand +Command ..> CommandResult + +@enduml \ No newline at end of file diff --git a/docs/diagrams/commonUtilsComponent.puml b/docs/diagrams/commonUtilsComponent.puml new file mode 100644 index 0000000000..926ee6e845 --- /dev/null +++ b/docs/diagrams/commonUtilsComponent.puml @@ -0,0 +1,14 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class Utils { + + validate(int integer): void + + validate(float number): void + + validate(String string): void + + formatDate(LocalDate date): String +} + +Utils ..> ParserException + +@enduml diff --git a/docs/diagrams/createProgramme.puml b/docs/diagrams/createProgramme.puml new file mode 100644 index 0000000000..c15031a570 --- /dev/null +++ b/docs/diagrams/createProgramme.puml @@ -0,0 +1,176 @@ +@startuml + +hide footbox + +actor User as user +participant ":BuffBuddy" as BuffBuddy +participant ":Ui" as Ui +participant ":Parser" as Parser +participant ":CommandFactory" as CommandFactory +participant ":ProgrammeCommandFactory" as ProgCommandFactory +participant ":CreateProgrammeCommand" as CreateProgrammeCommand +participant ":ProgrammeList" as ProgrammeList +participant ":CommandResult" as CommandResult + + +user -> BuffBuddy: start BuffBuddy +activate BuffBuddy + +create Ui +BuffBuddy -> Ui: Ui() +activate Ui + +Ui --> BuffBuddy +deactivate Ui + +create Parser +BuffBuddy -> Parser: Parser() +activate Parser + +create CommandFactory +Parser -> CommandFactory: CommandResult() +activate CommandFactory + +create ProgCommandFactory +CommandFactory -> ProgCommandFactory: ProgrammeCommandFactory() +activate ProgCommandFactory + +ProgCommandFactory --> CommandFactory +deactivate ProgCommandFactory + +CommandFactory --> Parser +deactivate CommandFactory + +Parser --> BuffBuddy +deactivate Parser + +create ProgrammeList +BuffBuddy -> ProgrammeList: loadProgrammeList() +activate ProgrammeList + +ProgrammeList --> BuffBuddy +deactivate ProgrammeList + +BuffBuddy -> Ui: readCommand() +activate Ui + +Ui --> BuffBuddy: fullCommand + + +BuffBuddy -> Parser: parse(fullCommand) +activate Parser + +Parser -> CommandFactory: createCommand(commandString, argumentString) +activate CommandFactory + +alt commandString is prog command + CommandFactory -> ProgCommandFactory: parse(argumentString) + activate ProgCommandFactory + + alt subcommand is create command + ProgCommandFactory -> ProgCommandFactory: prepareCreateCommand(arguments) + activate ProgCommandFactory + + + loop for each day + ProgCommandFactory -> ProgCommandFactory: parseDay(dayString) + activate ProgCommandFactory + + create ":Day" as Day + ProgCommandFactory -> Day: Day() + activate Day + + Day --> ProgCommandFactory + deactivate Day + + + loop for each exercise + ProgCommandFactory -> ProgCommandFactory: parseExercise(exerciseString) + activate ProgCommandFactory + end + + create ":Exercise" as Exercise + ProgCommandFactory -> Exercise: Exercise() + activate Exercise + + Exercise --> ProgCommandFactory + deactivate Exercise + + return return Exercise + + ProgCommandFactory -> Day: insertExercise() + activate Day + + Day --> ProgCommandFactory + deactivate Day + + end + return return Day + + else subcommand is other commands + end + + create CreateProgrammeCommand + ProgCommandFactory -> CreateProgrammeCommand: CreateProgrammeCommand() + activate CreateProgrammeCommand + + CreateProgrammeCommand --> ProgCommandFactory + deactivate CreateProgrammeCommand + + return return CreateProgrammeCommand + + ProgCommandFactory --> CommandFactory: Return CreateProgrammeCommand + deactivate ProgCommandFactory + destroy ProgCommandFactory + + +else commandString is other commands +end +CommandFactory --> Parser: Return CreateProgrammeCommand +deactivate CommandFactory + + +Parser --> BuffBuddy: Return CreateProgrammeCommand +deactivate Parser + +BuffBuddy -> CreateProgrammeCommand: execute() +activate CreateProgrammeCommand + +CreateProgrammeCommand -> ProgrammeList: insertProgramme(programmeName, programmeContents) +activate ProgrammeList + +create ":Programme" as Programme +ProgrammeList -> Programme: Programme() +activate Programme + +Programme --> ProgrammeList: +deactivate Programme + +ProgrammeList --> CreateProgrammeCommand: Return Programme +deactivate ProgrammeList + +create CommandResult +CreateProgrammeCommand -> CommandResult: CommandResult() + +activate CommandResult + +CommandResult --> CreateProgrammeCommand +deactivate CommandResult + +CreateProgrammeCommand --> BuffBuddy: Return CommandResult +deactivate CreateProgrammeCommand +destroy CreateProgrammeCommand + + +BuffBuddy -> Ui: showMessage(result) + + +Ui --> user: Display result +destroy CommandResult + +Ui --> BuffBuddy +deactivate BuffBuddy + +deactivate Ui + +@enduml diff --git a/docs/diagrams/deleteDayCommand.puml b/docs/diagrams/deleteDayCommand.puml new file mode 100644 index 0000000000..18b9d0cf28 --- /dev/null +++ b/docs/diagrams/deleteDayCommand.puml @@ -0,0 +1,26 @@ +@startuml +participant ":DeleteDayProgrammeCommand" as Command +participant ":ProgrammeList" as ProgrammeList +participant ":Programme" as Programme + +-> Command : execute() +activate Command + +Command -> ProgrammeList : getProgramme(programmeIndex) +activate ProgrammeList + +alt Programme exists + ProgrammeList --> Command : selected Programme + deactivate ProgrammeList + + Command -> Programme : deleteDay(dayIndex) + activate Programme + + Programme --> Command : deleted Day + deactivate Programme + else Programme does not exist +end + +<-- Command : deleted Day +deactivate Command +@enduml \ No newline at end of file diff --git a/docs/diagrams/deleteExerciseCommand.puml b/docs/diagrams/deleteExerciseCommand.puml new file mode 100644 index 0000000000..e6caf06bae --- /dev/null +++ b/docs/diagrams/deleteExerciseCommand.puml @@ -0,0 +1,35 @@ +@startuml +participant ":DeleteExerciseCommand" as Command +participant ":ProgrammeList" as ProgrammeList +participant ":Programme" as Programme +participant ":Day" as Day + +-> Command : execute() +activate Command + +Command -> ProgrammeList : getProgramme(programmeIndex) +activate ProgrammeList + +alt Programme exists + ProgrammeList --> Command : selected Programme + deactivate ProgrammeList + + Command -> Programme : getDay(dayIndex) + activate Programme + + alt Day exists + Programme --> Command : selected Day + deactivate Programme + + Command -> Day: deleteExercise(exerciseIndex) + activate Day + Day --> Command: deleted Exercise + deactivate Day + else Day does not exist + end + else Programme does not exist +end + +<-- Command : deleted Exercise +deactivate Command +@enduml \ No newline at end of file diff --git a/docs/diagrams/deleteMealSequenceDiagram.puml b/docs/diagrams/deleteMealSequenceDiagram.puml new file mode 100644 index 0000000000..b49388f340 --- /dev/null +++ b/docs/diagrams/deleteMealSequenceDiagram.puml @@ -0,0 +1,72 @@ +@startuml +actor User +participant ":UI" as UI +participant ":BuffBuddy" as BB +participant ":Parser" as Parser +participant ":DeleteMealCommand" as DeleteCmd +participant ":History" as History +participant ":DailyRecord" as DailyRec +participant ":MealList" as MealList +participant ":Meal" as Meal + +User -> UI: "meal add /n [mealName] /c [calories]" +activate UI +BB -> UI: readCommand() +activate BB +UI --> BB: Command object +BB -> Parser: parse(command) +activate Parser + +create DeleteCmd +Parser --> DeleteCmd: DeleteMealCommand(date) +deactivate Parser +activate DeleteCmd + +DeleteCmd -> History: getRecordByDate(date) +activate History + +alt DailyRecord exists + History --> DeleteCmd: DailyRecord object +else No DailyRecord exists +create DailyRec + History -> DailyRec: new DailyRecord() + activate DailyRec + DailyRec --> History : DailyRecord object + deactivate DailyRec + History --> DeleteCmd : DailyRecord object +end +deactivate History + +DeleteCmd -> DailyRec: deleteMealFromRecord() +activate DailyRec +DailyRec -> MealList: deleteMeal(index) + +create Meal +activate MealList +MealList -> Meal: get(index) +activate Meal +Meal --> MealList: Meal object +deactivate Meal + +MealList -> MealList: remove(index) +activate MealList + + +deactivate MealList +MealList --> DailyRec: Meal object +deactivate MealList +DailyRec --> DeleteCmd : Meal object + +deactivate DailyRec + + +DeleteCmd --> BB: CommandResult object +deactivate DeleteCmd + +BB -> UI: Show CommandResult +deactivate BB +destroy DeleteCmd + +UI -> User: Display success message +deactivate UI +@enduml diff --git a/docs/diagrams/deleteProgramme.puml b/docs/diagrams/deleteProgramme.puml new file mode 100644 index 0000000000..4cd718953c --- /dev/null +++ b/docs/diagrams/deleteProgramme.puml @@ -0,0 +1,86 @@ +@startuml +actor User +participant ":Ui" as Ui +participant ":BuffBuddy" as BuffBuddy +participant ":Parser" as Parser +participant ":CommandFactory" as CommandFactory +participant ":ProgCommandFactory" as ProgCommandFactory +participant ":DeleteProgrammeCommand" as Command +participant ":ProgrammeList" as Model +participant ":CommandResult" as CommandResult + + +User -> Ui : "prog delete..." +activate Ui +BuffBuddy -> Ui: readCommand() +activate BuffBuddy +Ui --> BuffBuddy : "prog delete..." + +BuffBuddy -> Parser : parse("prog delete...") +activate Parser + +Parser -> CommandFactory : createCommand("prog", "delete...") + +activate CommandFactory + +CommandFactory -> ProgCommandFactory : parse("delete", indexString) +activate ProgCommandFactory +ProgCommandFactory -> ProgCommandFactory : prepareDeleteCommand(indexString) +activate ProgCommandFactory + + +ProgCommandFactory -> "ParserUtils" : parseIndex(indexString) +activate "ParserUtils" +ProgCommandFactory <-- "ParserUtils" : parsed programme index +deactivate "ParserUtils" +create Command +ProgCommandFactory -> Command : DeleteProgrammeCommand(programmeIndex) + + +activate Command +ProgCommandFactory <-- Command: created Command +ProgCommandFactory --> ProgCommandFactory +deactivate ProgCommandFactory +CommandFactory <-- ProgCommandFactory: created Command +deactivate ProgCommandFactory +Parser <-- CommandFactory: created Command +deactivate CommandFactory +BuffBuddy <-- Parser: created Command +deactivate Parser + + +BuffBuddy -> Command : execute() + + +alt if programme exists + Command -> Model : deleteProgramme(index) + activate Model + opt if programme is active programme + Model -> Model: reset active programme + activate Model + Model --> Model + deactivate Model + end + activate Model + Command <-- Model: deleted Programme + deactivate Model +else Programme does not exist + +end +create CommandResult +Command -> CommandResult: CommandResult(...) + + +deactivate Command +activate CommandResult +BuffBuddy <-- CommandResult : return CommandResult +deactivate CommandResult +BuffBuddy -> Ui : showMessage(...) + +deactivate BuffBuddy +destroy CommandResult +User <-- Ui : "Successfully deleted..." +deactivate Ui + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/deleteWaterSequenceDiagram.puml b/docs/diagrams/deleteWaterSequenceDiagram.puml new file mode 100644 index 0000000000..c23e9af6cd --- /dev/null +++ b/docs/diagrams/deleteWaterSequenceDiagram.puml @@ -0,0 +1,71 @@ +@startuml + +actor User +participant ":UI" as UI +participant ":BuffBuddy" as BB +participant ":Parser" as Parser +participant ":DeleteWaterCommand" as Cmd +participant ":History" as History +participant ":DailyRecord" as DailyRec + +User -> UI: "water delete /w WATER_INDEX /t Date" +activate UI +BB -> UI: readCommand() +activate BB +UI --> BB: command object +BB -> Parser: parse(command) +activate Parser +Parser -> Parser : Parse inputs +note right +parser interactions are put under +the parser for simplification +end note +activate Parser +deactivate Parser +create Cmd +Parser --> Cmd: new DeleteWaterCommand(index, date) +deactivate Parser + +activate Cmd + +Cmd -> History: getRecordByDate(date) +activate History + +alt DailyRecord does not exist + create DailyRec + History -> DailyRec: new DailyRecord() + activate DailyRec + DailyRec --> History + History --> Cmd + deactivate DailyRec +else No DailyRecord exists + History --> Cmd: DailyRecord object +end +deactivate History + +activate Cmd +Cmd -> DailyRecord: removeWaterFromRecord(index) +activate DailyRecord + +DailyRecord -> Water: deleteWater(index) +activate Water +Water --> DailyRecord +deactivate Water + +DailyRecord --> Cmd: water log +deactivate DailyRecord + +deactivate Cmd + + +Cmd --> BB: CommandResult object +deactivate Cmd +destroy Cmd + +BB -> UI: Show CommandResult +deactivate BB + +UI -> User: Display success message +deactivate UI + +@enduml \ No newline at end of file diff --git a/docs/diagrams/editCommand.puml b/docs/diagrams/editCommand.puml new file mode 100644 index 0000000000..eace05b5a0 --- /dev/null +++ b/docs/diagrams/editCommand.puml @@ -0,0 +1,137 @@ +@startuml +actor User +participant ":Ui" as Ui +participant ":BuffBuddy" as BuffBuddy +participant ":Parser" as Parser +participant ":CommandFactory" as CommandFactory +participant ":ProgCommandFactory" as ProgCommandFactory +participant ":EditProgrammeCommand" as Command +participant ":Model" as Model +participant ":CommandResult" as CommandResult +participant ":Exercise" as Exercise +participant ":ExerciseUpdate" as ExerciseUpdate +participant ":Day" as Day + +User -> Ui : "prog edit..." +activate Ui +BuffBuddy -> Ui: readCommand() +activate BuffBuddy +Ui --> BuffBuddy : "prog edit..." + +BuffBuddy -> Parser : parse("prog edit...") +activate Parser + +Parser -> CommandFactory : createCommand("prog", "edit...") + +activate CommandFactory + +CommandFactory -> ProgCommandFactory : parse("edit",arguments) + +activate ProgCommandFactory + +ProgCommandFactory -> ProgCommandFactory : prepareEditCommand(arguments) +activate ProgCommandFactory + +create FlagParser + +ProgCommandFactory -> FlagParser: FlagParser(arguments) +activate FlagParser +FlagParser -> FlagParser : parse(arguments) +activate FlagParser +FlagParser --> FlagParser +deactivate FlagParser +ProgCommandFactory <-- FlagParser: parsed arguments +deactivate FlagParser + +alt edit type is create day + ProgCommandFactory -> ProgCommandFactory: parseDay(dayString) + activate ProgCommandFactory + create Day + ProgCommandFactory -> Day : Day(...) + activate Day + ProgCommandFactory <-- Day: created Day + + + loop for each exercise in the day + create Exercise + ProgCommandFactory -> Exercise : Exercise(...) + activate Exercise + ProgCommandFactory <-- Exercise: created Exercise + deactivate Exercise + ProgCommandFactory -> Day : insertExercise(...) + ProgCommandFactory <-- Day + end + + deactivate Day + ProgCommandFactory --> ProgCommandFactory: created Day + deactivate ProgCommandFactory +else edit type is create exercise + ProgCommandFactory -> ProgCommandFactory: parseExercise(exerciseString) + activate ProgCommandFactory + create Exercise + ProgCommandFactory -> Exercise: Exercise(...) + activate Exercise + ProgCommandFactory <-- Exercise: created Exercise + deactivate Exercise + ProgCommandFactory --> ProgCommandFactory: created Exercise + deactivate ProgCommandFactory +else edit type is update exercise + ProgCommandFactory -> ProgCommandFactory: parseExerciseUpdate(updateString) + activate ProgCommandFactory + create ExerciseUpdate + ProgCommandFactory -> ExerciseUpdate: ExerciseUpdate(...) + activate ExerciseUpdate + ProgCommandFactory <-- ExerciseUpdate: created ExerciseUpdate + deactivate ExerciseUpdate + ProgCommandFactory --> ProgCommandFactory: created ExerciseUpdate + + deactivate ProgCommandFactory +else other edit type +end +create Command +ProgCommandFactory -> Command : create appropiate Edit Command + +deactivate ProgCommandFactory + +activate Command +ProgCommandFactory <-- Command : created EditProgrammeCommand +CommandFactory <-- ProgCommandFactory : created EditProgrammeCommand +deactivate ProgCommandFactory + +Parser <-- CommandFactory : created EditProgrammeCommand +destroy FlagParser +deactivate CommandFactory + + +BuffBuddy <-- Parser : created EditProgrammeCommand +deactivate Parser + +BuffBuddy -> Command : execute() + + +Command -> Model : interact with Model +activate Model +Command <-- Model: returned Data +deactivate Model + +create CommandResult +Command -> CommandResult: create Command Result +destroy ExerciseUpdate + + + +activate CommandResult +Command <-- CommandResult : created CommandResult +deactivate CommandResult +BuffBuddy <-- Command: created CommandResult +deactivate Command + +BuffBuddy -> Ui : showMessage() + +deactivate BuffBuddy + +User <-- Ui : "Successfully edited..." +deactivate Ui +destroy Command +destroy CommandResult +@enduml \ No newline at end of file diff --git a/docs/diagrams/editCommandActivityDiagram.puml b/docs/diagrams/editCommandActivityDiagram.puml new file mode 100644 index 0000000000..b72c592773 --- /dev/null +++ b/docs/diagrams/editCommandActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +start + +:User executes command; + +if (command type) then (prog edit) + if (subcommand type) then (edit day) + :Execute day edit; + else (edit exercise) + :Execute exercise edit; + endif + :Retrieve updated model; + + :Create CommandResult; +endif + +:Return result to User; + +stop +@enduml \ No newline at end of file diff --git a/docs/diagrams/editCommandSummary.puml b/docs/diagrams/editCommandSummary.puml new file mode 100644 index 0000000000..893e14bce4 --- /dev/null +++ b/docs/diagrams/editCommandSummary.puml @@ -0,0 +1,43 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + class EditProgrammeCommand <>{ + + #exerciseIndex: int + +EditProgrammeCommand(programmeIndex: int, dayIndex: int, exerciseIndex: int) + +EditProgrammeCommand(programmeIndex: int, dayIndex: int) + +EditProgrammeCommand(programmeIndex: int) + +execute(programmes: ProgrammeList): CommandResult + } + + class CreateDayCommand { + # createdDay: Day + + CreateDayCommand(programmeIndex: int, createdDay: Day) + +execute(programmes: ProgrammeList): CommandResult + } + class CreateExerciseCommand { + # createdExercise: Exercise + + CreateExerciseCommand(programmeIndex: int, dayIndex: int, createdExercise: Exercise) + + execute(programmes: ProgrammeList): CommandResult + } + class DeleteDayCommand { + + DeleteDayCommand(programmeIndex: int, dayIndex: int) + + execute(programmes: ProgrammeList): CommandResult + } + class DeleteExerciseCommand { + + DeleteExerciseCommand(programmeIndex: int, dayIndex: int, exerciseIndex: int) + + execute(programmes: ProgrammeList): CommandResult + } + class EditExerciseCommand { + # update: ExerciseUpdate + + EditExerciseCommand(programmeIndex: int, dayIndex: int, exerciseIndex: int, update: ExerciseUpdate) + + execute(programmes: ProgrammeList): CommandResult + } + + EditProgrammeCommand <|--- CreateDayCommand + EditProgrammeCommand <|--- CreateExerciseCommand + EditProgrammeCommand <|--- DeleteDayCommand + EditProgrammeCommand <|--- DeleteExerciseCommand + EditProgrammeCommand <|--- EditExerciseCommand + +@enduml \ No newline at end of file diff --git a/docs/diagrams/editExerciseCommand.puml b/docs/diagrams/editExerciseCommand.puml new file mode 100644 index 0000000000..9d5bd862e1 --- /dev/null +++ b/docs/diagrams/editExerciseCommand.puml @@ -0,0 +1,46 @@ +@startuml +participant ":EditExerciseCommand" as EditExerciseCommand +participant ":ProgrammeList" as ProgrammeList +participant ":Programme" as Programme +participant ":Day" as Day +participant ":Exercise" as Exercise + +-> EditExerciseCommand : execute() +activate EditExerciseCommand + +EditExerciseCommand -> ProgrammeList : getProgramme(programmeIndex) +activate ProgrammeList + +alt Programme exists + ProgrammeList --> EditExerciseCommand : selected Programme + deactivate ProgrammeList + + EditExerciseCommand -> Programme : getDay(dayIndex) + activate Programme + + alt Day exists + Programme --> EditExerciseCommand : selected Day + deactivate Programme + + EditExerciseCommand -> Day: getExercise(exerciseIndex) + activate Day + + alt Exercise exists + Day --> EditExerciseCommand: selected Exercise + deactivate Day + + EditExerciseCommand -> Exercise: updateExercise(update) + activate Exercise + Exercise --> EditExerciseCommand: updated Exercise + deactivate Exercise + else Exercise does not exist + end + else Day does not exist + end + else Programme does not exist +end + +<-- EditExerciseCommand +deactivate EditExerciseCommand + +@enduml \ No newline at end of file diff --git a/docs/diagrams/flagDefinitionsComponent.puml b/docs/diagrams/flagDefinitionsComponent.puml new file mode 100644 index 0000000000..8b51fb4294 --- /dev/null +++ b/docs/diagrams/flagDefinitionsComponent.puml @@ -0,0 +1,26 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class FlagDefinitions { + + DATE_FLAG: String + + PROGRAMME_FLAG: String + + DAY_FLAG: String + + EXERCISE_FLAG: String + + NAME_FLAG: String + + SETS_FLAG: String + + REPS_FLAG: String + + WEIGHT_FLAG: String + + CALORIES_FLAG: String + + REMOVE_EXERCISE_FLAG: String + + ADD_EXERCISE_FLAG: String + + UPDATE_EXERCISE_FLAG: String + + ADD_DAY_FLAG: String + + REMOVE_DAY_FLAG: String + + MEAL_INDEX: String + + WATER_INDEX: String + + VOLUME_FLAG: String + + VALID_FLAGS: HashSet +} + +@enduml \ No newline at end of file diff --git a/docs/diagrams/flagParserComponent.puml b/docs/diagrams/flagParserComponent.puml new file mode 100644 index 0000000000..98fc87b728 --- /dev/null +++ b/docs/diagrams/flagParserComponent.puml @@ -0,0 +1,27 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class FlagParser { + - parsedFlags: HashMap + - aliasMap: HashMap + + - generateSplitBy(String... ignoredFlags): String + - initializeAliasMap(): void + - parse(String argumentString, String splitBy): void + - resolveAlias(String flag): String + + hasFlag(String flag): boolean + + validateRequiredFlags(String... requiredFlags): void + + getStringByFlag(String flag): String + + getIndexByFlag(String flag): int + + getIntegerByFlag(String flag): int + + getFloatByFlag(String flag): float + + getDateByFlag(String flag): LocalDate +} + +FlagParser ..> ParserUtils +FlagParser ..> FlagDefinitions +FlagParser ..> FlagException +FlagParser ..> Utils + +@enduml diff --git a/docs/diagrams/historyCommandSummary.puml b/docs/diagrams/historyCommandSummary.puml new file mode 100644 index 0000000000..5fc81c8caf --- /dev/null +++ b/docs/diagrams/historyCommandSummary.puml @@ -0,0 +1,47 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class HistoryCommand <> { + - date : LocalDate + + HistoryCommand() + + HistoryCommand(date : LocalDate) + + execute(history : History) : CommandResult +} + +class DeleteHistoryCommand { + + DeleteHistoryCommand(date : LocalDate) + + execute(history : History) : CommandResult +} + +class ListHistoryCommand { + + execute(history : History) : CommandResult +} + +class ListPersonalBestsCommand { + + execute(history : History) : CommandResult +} + +class ViewHistoryCommand { + + ViewHistoryCommand(date : LocalDate) + + execute(history : History) : CommandResult +} + +class ViewPersonalBestCommand { + - exerciseName : String + + ViewPersonalBestCommand(exerciseName : String) + + execute(history : History) : CommandResult +} + +class WeeklySummaryCommand { + + execute(history : History) : CommandResult +} + +HistoryCommand <|-- DeleteHistoryCommand +HistoryCommand <|-- ListHistoryCommand +HistoryCommand <|-- ListPersonalBestsCommand +HistoryCommand <|-- ViewHistoryCommand +HistoryCommand <|-- ViewPersonalBestCommand +HistoryCommand <|-- WeeklySummaryCommand +@enduml + diff --git a/docs/diagrams/historyFactoryComponent.puml b/docs/diagrams/historyFactoryComponent.puml new file mode 100644 index 0000000000..f2ae305e56 --- /dev/null +++ b/docs/diagrams/historyFactoryComponent.puml @@ -0,0 +1,24 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class HistoryCommandFactory { + - COMMAND_WORD: String + + + parse(String argumentString): Command + + prepareViewHistoryCommand(String argumentString): Command + + preparePersonalBestCommand(String argumentString): Command + + prepareDeleteHistoryCommand(String argumentString): Command +} + +HistoryCommandFactory "1" --> "1" InvalidCommand : " creates" +HistoryCommandFactory "1" --> "1" ListPersonalBestsCommand : " creates" +HistoryCommandFactory "1" --> "1" ViewHistoryCommand : " creates" +HistoryCommandFactory "1 " --> "1" DeleteHistoryCommand : " creates" +HistoryCommandFactory "1" --> "1" ListHistoryCommand : " creates" +HistoryCommandFactory "1" --> "1" ViewPersonalBestCommand : " creates" +HistoryCommandFactory "1" --> "1" WeeklySummaryCommand : " creates" + +HistoryCommandFactory ..> ParserUtils + +@enduml diff --git a/docs/diagrams/historycomponent.puml b/docs/diagrams/historycomponent.puml new file mode 100644 index 0000000000..014a1142c8 --- /dev/null +++ b/docs/diagrams/historycomponent.puml @@ -0,0 +1,73 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class History { + - logger : Logger + - history : LinkedHashMap + + History() + + getRecordByDate(date: LocalDate) : DailyRecord + + getHistory() : LinkedHashMap + + getWeeklyWorkoutSummary() : String + + logRecord(date: LocalDate, record: DailyRecord) : void + + deleteRecord(date: LocalDate) : DailyRecord + + hasRecord(date: LocalDate) : boolean + + getHistorySize() : int + + getFormattedPersonalBests() : String + + getPersonalBestForExercise(exerciseName: String) : String +} + +class DailyRecord { + - logger : Logger + + getDayFromRecord() : Day + + getMealList() : MealList + + getWater() : Water + + logDay(day: Day) : void + + addMealToRecord(meal: Meal) : void + + deleteMealFromRecord(index: int) : Meal + + addWaterToRecord(water: Water) : void + + removeWaterFromRecord(index: int) : Water + + toString() : String +} + +class Day { + + insertExercise(exercise: Exercise) : void + + getExercisesCount() : int + + getExercise(index: int) : Exercise +} + +class Exercise { + + getName() : String + + getWeight() : int + + toStringPb() : String +} + +class MealList { + + addMeal(meal: Meal) : void + + deleteMeal(index: int) : Meal + + getMeals() : List + + isEmpty() : boolean + + getMealCount() : int + + toString() : String +} + +class Meal { + + getCalories() : int + + getName() : String + + toString() : String +} + +class Water { + + getIntakeAmount() : float + + toString() : String +} + +History "1" *-- "0.*" DailyRecord : contains +DailyRecord "1" *-- "1" Day : day +DailyRecord "1" *-- "1" MealList : mealList +DailyRecord "1" *-- "1" Water : water +Day "1" *-- "0.*" Exercise : exercises +MealList "1" *-- "0.*" Meal : meals + +@enduml + diff --git a/docs/diagrams/loadProgrammeListSeqenceDiagram.puml b/docs/diagrams/loadProgrammeListSeqenceDiagram.puml new file mode 100644 index 0000000000..c87594949e --- /dev/null +++ b/docs/diagrams/loadProgrammeListSeqenceDiagram.puml @@ -0,0 +1,92 @@ +@startuml + +actor Client +participant ":Storage" as Storage +participant ":FileManager" as FileManager +participant ":JsonObject" as JsonObject + +Client -> Storage : loadProgrammeList() +activate Storage + +Storage -> FileManager : loadProgrammeList() +activate FileManager + +FileManager -> FileManager : load() +activate FileManager +alt element == null or element.isJsonNull() + create JsonObject as emptyJsonObject + FileManager -> emptyJsonObject : new JsonObject() + activate emptyJsonObject + note right + The header is meant to be :JsonObject + end note + emptyJsonObject --> FileManager : Empty JsonObject + deactivate emptyJsonObject +else + create JsonObject as allDataJsonObject + FileManager -> allDataJsonObject : new JsonObject() + activate allDataJsonObject + note right + The header is meant to be :JsonObject + end note + allDataJsonObject --> FileManager : Empty JsonObject + deactivate allDataJsonObject + FileManager -> allDataJsonObject : getAsJsonObject() + activate allDataJsonObject + allDataJsonObject --> FileManager : JsonObject containing all data + deactivate allDataJsonObject + +end + +FileManager --> FileManager : JsonObject (entire data file) +deactivate FileManager + +alt jsonObject == null or !jsonObject.has("programmeList") + create JsonObject as emptyProgrammeListJsonObject + FileManager -> emptyProgrammeListJsonObject : new JsonObject() + note right + The header is meant to be :JsonObject + end note + activate emptyProgrammeListJsonObject + destroy allDataJsonObject + destroy emptyJsonObject + emptyProgrammeListJsonObject --> FileManager : Empty JsonObject + deactivate emptyProgrammeListJsonObject + FileManager --> Storage : Empty JsonObject + deactivate JsonObject +else + create JsonObject + FileManager -> JsonObject : new JsonObject() + activate JsonObject + JsonObject --> FileManager : Empty JsonObject + deactivate JsonObject + FileManager -> JsonObject : getAsJsonObject("programmeList") + activate JsonObject + JsonObject --> FileManager : JsonObject containing programmeList + deactivate JsonObject + FileManager --> Storage : JsonObject containing programmeList +end +deactivate FileManager + +alt programmeListJson == null or programmeListJson.size() == 0 + Storage --> Client : new ProgrammeList() +else + Storage -> Storage : validateProgrammeList(programmeListJson) + activate Storage + deactivate Storage + alt programmeListJson not valid + Storage --> Client : new ProgrammeList() + else + Storage -> Storage : programmeListFromJson(programmeListJson) + activate Storage + Storage --> Storage : loadedProgrammeList + deactivate Storage + destroy emptyProgrammeListJsonObject + destroy JsonObject + Storage --> Client : ProgrammeList object + deactivate Storage + end +end +deactivate Storage + +@enduml diff --git a/docs/diagrams/logProgrammeSequenceDiagram.puml b/docs/diagrams/logProgrammeSequenceDiagram.puml new file mode 100644 index 0000000000..936ea31347 --- /dev/null +++ b/docs/diagrams/logProgrammeSequenceDiagram.puml @@ -0,0 +1,48 @@ +@startuml +actor User + +User -> Ui : "prog log /p [PROG_INDEX] /d [DAY_INDEX] /t [DATE]" +activate Ui +Ui -> ProgCommandFactory : parse("log", arguments) +activate ProgCommandFactory +ProgCommandFactory -> LogProgrammeCommand : new LogProgrammeCommand([PROG_INDEX], [DAY_INDEX], [DATE]) +deactivate ProgCommandFactory + +activate LogProgrammeCommand +LogProgrammeCommand -> ProgrammeList : getProgramme([PROG_INDEX]) +activate ProgrammeList +ProgrammeList -> Programme : getDay([DAY_INDEX]) +activate Programme +Programme --> ProgrammeList : Day instance +deactivate Programme +ProgrammeList --> LogProgrammeCommand : Programme instance +deactivate ProgrammeList + +LogProgrammeCommand -> History : getRecordByDate([DATE]) +activate History +alt DailyRecord does not exist + History -> DailyRecord : new DailyRecord + activate DailyRecord + deactivate DailyRecord +end +History --> LogProgrammeCommand : DailyRecord instance +deactivate History + +LogProgrammeCommand -> DailyRecord : logDayToRecord(Day) +activate DailyRecord +DailyRecord --> LogProgrammeCommand : void +deactivate DailyRecord + +LogProgrammeCommand -> History : logRecord([DATE], DailyRecord) +activate History +History --> LogProgrammeCommand : void +deactivate History + +LogProgrammeCommand --> Ui : CommandResult("Congrats! You've successfully completed: [Day details]") +deactivate LogProgrammeCommand +destroy LogProgrammeCommand + +Ui -> User : Display success message +deactivate Ui +@enduml + diff --git a/docs/diagrams/mealAndMealListClassDiagram.puml b/docs/diagrams/mealAndMealListClassDiagram.puml new file mode 100644 index 0000000000..eaea228d0b --- /dev/null +++ b/docs/diagrams/mealAndMealListClassDiagram.puml @@ -0,0 +1,26 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + +class Meal { + - calories : int + - name : String + + Meal(String name, int calories) + + getCalories() : int + + getName() : String +} + +class MealList { + - meals : ArrayList + + MealList() : void + + isEmpty() : boolean + + getSize() : int + + addMeal(Meal meal) : void + + deleteMeal(int index) : Meal + + getMeals() : ArrayList +} + +MealList "1" *-- "0..*" Meal : contains + +@enduml diff --git a/docs/diagrams/mealCommandSummary.puml b/docs/diagrams/mealCommandSummary.puml new file mode 100644 index 0000000000..f8f3fb9908 --- /dev/null +++ b/docs/diagrams/mealCommandSummary.puml @@ -0,0 +1,30 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + + class MealCommand <>{ + # date: LocalDate + + execute(History): CommandResult +} + +class AddMealCommand { + - mealName: String + - calories: int + + execute(History): CommandResult +} + +class DeleteMealCommand { + - indexMealToDelete: int + + execute(History): CommandResult +} + +class ViewMealCommand { + + execute(History): CommandResult +} + +MealCommand <|-- AddMealCommand +MealCommand <|-- DeleteMealCommand +MealCommand <|-- ViewMealCommand + +@enduml diff --git a/docs/diagrams/mealFactoryComponent.puml b/docs/diagrams/mealFactoryComponent.puml new file mode 100644 index 0000000000..f6c9951372 --- /dev/null +++ b/docs/diagrams/mealFactoryComponent.puml @@ -0,0 +1,25 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class MealCommandFactory { + - COMMAND_WORD: String + + + parse(String argumentString): Command + + prepareAddCommand(String argumentString): Command + + prepareDeleteCommand(String argumentString): Command + + prepareViewCommand(String argumentString): Command +} + +MealCommandFactory "1" --> "1" InvalidCommand : " creates" +MealCommandFactory "1" --> "1" DeleteMealCommand : " creates" +MealCommandFactory "1" --> "1" AddMealCommand : " creates" +MealCommandFactory "1 " --> "1" ViewMealCommand : " creates" +MealCommandFactory "1 " --> "1" Meal : " creates" +MealCommandFactory "1" --> "1" FlagParser : " creates" + +MealCommandFactory ..> ParserUtils +MealCommandFactory ..> FlagDefinitions +MealCommandFactory ..> MealException + +@enduml \ No newline at end of file diff --git a/docs/diagrams/parserComponent.puml b/docs/diagrams/parserComponent.puml new file mode 100644 index 0000000000..5012b734dd --- /dev/null +++ b/docs/diagrams/parserComponent.puml @@ -0,0 +1,25 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class CommandFactory { + - ProgCommandFactory progFactory + - MealCommandFactory mealFactory + - WaterCommandFactory waterFactory + - HistoryCommandFactory historyFactory + + + createCommand(String commandString, String argumentString): Command +} + +class Parser { + + parse(String fullCommand): Command +} + +Parser "1" --> "1" CommandFactory : uses +Parser ..> ParserException +Parser ..> ParserUtils + +CommandFactory ..> ExitCommand +CommandFactory ..> InvalidCommand + +@enduml diff --git a/docs/diagrams/parserUtilsComponent.puml b/docs/diagrams/parserUtilsComponent.puml new file mode 100644 index 0000000000..2d269fed62 --- /dev/null +++ b/docs/diagrams/parserUtilsComponent.puml @@ -0,0 +1,16 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class ParserUtils { + + splitArguments(String argumentString): String[] + + parseInteger(String intString): int + + parseFloat(String floatString): float + + parseIndex(String indexString): int + + parseDate(String dateString): LocalDate +} + +ParserUtils ..> ParserException +ParserUtils ..> Utils + +@enduml diff --git a/docs/diagrams/progFactoryComponent.puml b/docs/diagrams/progFactoryComponent.puml new file mode 100644 index 0000000000..14d212a773 --- /dev/null +++ b/docs/diagrams/progFactoryComponent.puml @@ -0,0 +1,39 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class ProgrammeCommandFactory { + - COMMAND_WORD: String + + + parse(String argumentString): Command + - prepareCreateCommand(String argumentString): CreateProgrammeCommand + - prepareViewCommand(String argumentString): ViewProgrammeCommand + - prepareStartCommand(String argumentString): StartProgrammeCommand + - prepareDeleteCommand(String argumentString): DeleteProgrammeCommand + - prepareLogCommand(String argumentString): LogProgrammeCommand + - prepareEditCommand(String argumentString): EditProgrammeCommand + - prepareEditExerciseCommand(FlagParser flagParser): EditExerciseProgrammeCommand + - prepareCreateExerciseCommand(FlagParser flagParser): CreateExerciseProgrammeCommand + - prepareDeleteExerciseCommand(FlagParser flagParser): DeleteExerciseProgrammeCommand + - prepareCreateDayCommand(FlagParser flagParser): CreateDayProgrammeCommand + - prepareDeleteDayCommand(FlagParser flagParser): DeleteDayProgrammeCommand + - parseDay(String dayString): Day + - parseExercise(String argumentString): Exercise + - parseExerciseUpdate(String argumentString): ExerciseUpdate +} + +ProgrammeCommandFactory "1 " --> "1" Command : "creates" + +ProgrammeCommandFactory "1 " --> "1" FlagParser : "creates" + +ProgrammeCommandFactory ..> ParserUtils +ProgrammeCommandFactory ..> FlagDefinitions +ProgrammeCommandFactory ..> ProgrammeException +ProgrammeCommandFactory ..> FlagException + +ProgrammeCommandFactory ..> Day +ProgrammeCommandFactory ..> Exercise +ProgrammeCommandFactory ..> ExerciseUpdate + + +@enduml diff --git a/docs/diagrams/programmeCommandSummary.puml b/docs/diagrams/programmeCommandSummary.puml new file mode 100644 index 0000000000..48a3208d69 --- /dev/null +++ b/docs/diagrams/programmeCommandSummary.puml @@ -0,0 +1,46 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + + class ProgrammeCommand <> { + # programmeIndex: int + # dayIndex: int + + equals(other: Object): boolean +} + +class CreateProgrammeCommand { + + programmeName: String + + programmeContents: ArrayList + + execute(programmes: ProgrammeList, history: History): CommandResult +} + +class DeleteProgrammeCommand { + + execute(programmes: ProgrammeList, history: History): CommandResult +} + +class ViewProgrammeCommand { + + execute(programmes: ProgrammeList, history: History): CommandResult +} + +class StartProgrammeCommand { + + execute(programmes: ProgrammeList, history: History): CommandResult +} + +class LogProgrammeCommand { + # date: LocalDate + + execute(programmes: ProgrammeList, history: History): CommandResult +} + +class ListProgrammeCommand { + + execute(programmes: ProgrammeList, history: History): CommandResult +} + +ProgrammeCommand <|-- CreateProgrammeCommand +ProgrammeCommand <|-- DeleteProgrammeCommand +ProgrammeCommand <|-- ViewProgrammeCommand +ProgrammeCommand <|-- StartProgrammeCommand +ProgrammeCommand <|-- LogProgrammeCommand +ProgrammeCommand <|-- ListProgrammeCommand + +@enduml \ No newline at end of file diff --git a/docs/diagrams/programmeComponentClassDiagram.puml b/docs/diagrams/programmeComponentClassDiagram.puml new file mode 100644 index 0000000000..ae2b84f0e5 --- /dev/null +++ b/docs/diagrams/programmeComponentClassDiagram.puml @@ -0,0 +1,64 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + +class ProgrammeList { + + currentActiveProgramme : int + + getProgrammeList() : ArrayList + + getProgrammeListSize() : int + + insertProgramme(String programmeName, ArrayList days) : Programme + + deleteProgram(int index) : Programme + + getProgramme(int index) : Programme + + startProgramme(int index) : Programme +} + +class Programme { + - programmeName : String + + getProgrammeName() : String + + getDay(int index) : Day + + insertDay(Day day) : void + + getDayCount() : int + + deleteDay(int index) : Day +} + +class Day { + - name : String + + getName() : String + + getExercise(int index) : Exercise + + insertExercise(Exercise exercise) : void + + deleteExercise(int index) : Exercise + + getTotalCaloriesBurnt() : int +} + +class Exercise { + - sets : int + - reps : int + - weight : int + - calories : int + - name : String + - updateSets(Integer newSets) : void + - updateReps(Integer newReps) : void + - updateWeight(Integer newWeight) : void + - updateName(String newName) : void + - updateCalories(Integer newCalories) : void + + updateExercise(ExerciseUpdate update) : void + + getCalories() : int + + getWeight() : int + + getName() : String +} + +class ExerciseUpdate { + - sets : Integer + - reps : Integer + - weight : Integer + - calories : Integer + - name : String +} + +ProgrammeList "1" --> "*" Programme : programmeList +Programme "1" --> "*" Day : dayList +Day "1" --> "*" Exercise : exerciseList +Exercise ..> ExerciseUpdate + +@enduml diff --git a/docs/diagrams/saveSeqeunceDiagram.puml b/docs/diagrams/saveSeqeunceDiagram.puml new file mode 100644 index 0000000000..c938ab0081 --- /dev/null +++ b/docs/diagrams/saveSeqeunceDiagram.puml @@ -0,0 +1,56 @@ +@startuml + +actor Client +participant ":Storage" as Storage +participant ":JsonObject" as JsonObject +participant ":FileManager" as FileManager + +Client -> Storage : saveData(programmeList, history) +activate Storage + +Storage -> Storage : createJSON (programmeList, history) +activate Storage +create JsonObject +Storage -> JsonObject : new JsonObject() +activate JsonObject +JsonObject --> Storage : empty JsonObject +deactivate JsonObject + +Storage -> Storage : programmeListToJson(programmeList) +activate Storage +Storage --> Storage : JsonObject containing ProgrammeList +deactivate Storage + +Storage -> Storage : historyToJson(history) +activate Storage +Storage --> Storage : JsonObject containing History +deactivate Storage + +Storage -> JsonObject : add(programmeList) +activate JsonObject +deactivate JsonObject +Storage -> JsonObject : add(history) +activate JsonObject +deactivate JsonObject + +Storage --> Storage : JsonObject containing all data +deactivate Storage + + +Storage -> FileManager : save(JsonObject) +activate FileManager +destroy JsonObject +FileManager -> FileManager : createDirIfNotExists() +activate FileManager +deactivate FileManager +FileManager -> FileManager : createFileIfNotExists() +activate FileManager +deactivate FileManager + +FileManager --> Storage : Data written to file +deactivate FileManager + +Storage --> Client : Data is saved +deactivate Storage + +@enduml diff --git a/docs/diagrams/startProgramme.puml b/docs/diagrams/startProgramme.puml new file mode 100644 index 0000000000..569ddcc2df --- /dev/null +++ b/docs/diagrams/startProgramme.puml @@ -0,0 +1,79 @@ +@startuml +actor User +participant ":Ui" as Ui +participant ":BuffBuddy" as BuffBuddy +participant ":Parser" as Parser +participant ":CommandFactory" as CommandFactory +participant ":ProgCommandFactory" as ProgCommandFactory +participant ":StartProgrammeCommand" as Command +participant ":ProgrammeList" as Model +participant ":CommandResult" as CommandResult + + +User -> Ui : "prog start..." +activate Ui +BuffBuddy -> Ui: readCommand() +activate BuffBuddy +Ui --> BuffBuddy : "prog start..." + +BuffBuddy -> Parser : parse("prog start...") +activate Parser + +Parser -> CommandFactory : createCommand("prog", "start...") + +activate CommandFactory + +CommandFactory -> ProgCommandFactory : parse("start", indexString) +activate ProgCommandFactory +ProgCommandFactory -> ProgCommandFactory : preparestartCommand(indexString) +activate ProgCommandFactory + + +ProgCommandFactory -> "ParserUtils" : parseIndex(indexString) +activate "ParserUtils" +ProgCommandFactory <-- "ParserUtils" : parsed programme index +deactivate "ParserUtils" +create Command +ProgCommandFactory -> Command : startProgrammeCommand(programmeIndex) + + +activate Command +ProgCommandFactory <-- Command: created Command +ProgCommandFactory --> ProgCommandFactory +deactivate ProgCommandFactory +CommandFactory <-- ProgCommandFactory: created Command +deactivate ProgCommandFactory +Parser <-- CommandFactory: created Command +deactivate CommandFactory +BuffBuddy <-- Parser: created Command +deactivate Parser + + +BuffBuddy -> Command : execute() + + +alt if programme exists + Command -> Model : startProgramme(index) + activate Model + Command <-- Model: started Programme + deactivate Model +else programme does not exist + +end +create CommandResult +Command -> CommandResult: CommandResult(...) + + +deactivate Command +activate CommandResult +BuffBuddy <-- CommandResult : return CommandResult +deactivate CommandResult +BuffBuddy -> Ui : showMessage(...) + +deactivate BuffBuddy +destroy CommandResult +User <-- Ui : "Started programme..." +deactivate Ui + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/storageAndFileManager.puml b/docs/diagrams/storageAndFileManager.puml new file mode 100644 index 0000000000..bc8382f391 --- /dev/null +++ b/docs/diagrams/storageAndFileManager.puml @@ -0,0 +1,53 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + +class Storage { + - message : String + - isProgrammeListEmpty : boolean + - isProgrammelistCorrupted : boolean + - isHistoryEmpty : boolean + - isHistoryCorrupted : boolean + - createJson(programmeList : ProgrammeList, history : History) : JsonObject + - programmeListToJson(programmeList : ProgrammeList) : JsonObject + - historyToJson(history : History) : JsonObject + - programmeListFromJson(JsonObject : jsonObject) : ProgrammeList + - historyFromJson(JsonObject : jsonObject) : History + - validateProgrammeList(JsonObject : programmeList) : void + - validateHistory(JsonObject : history) : void + - validateDate(String : dateString) : void + - validateProgramme(JsonArray : programmeList) : void + - validateDay(JsonArray : dayList) : void + - validateExercise(JsonArray : exercises) : void + - validateMeal(JsonArray : meals) : void + - validateWater(JsonArray : waterList) : void + + loadProgrammeList() : ProgrammeList + + loadHistory() : History + + saveData(programmeList : ProgrammeList, history : History) : void + + getMessage() : String +} + +class FileManager { + - path : String + - load() : JsonObject + - createDirIfNotExists() : void + - createFileIfNotExists() : void + + loadProgrammeList() : JsonObject + + loadHistory() : JsonObject + + save(data : JsonObject) : void +} + +class DateSerializer { + - formatter : DateTimeFormatter + + serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) : JsonElement + + deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) : LocalDate +} + +Storage "1" --> "1" FileManager : contains + +Storage ..> History +Storage ..> ProgrammeList +Storage ..> DateSerializer + +@enduml diff --git a/docs/diagrams/uiComponent.puml b/docs/diagrams/uiComponent.puml new file mode 100644 index 0000000000..4bf40aeb7d --- /dev/null +++ b/docs/diagrams/uiComponent.puml @@ -0,0 +1,27 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class Ui { + - in: Scanner + - out: PrintStream + + + readCommand(): String + + showLine(): void + + showMessage(String msg): void + + showMessage(Exception e): void + + showMessage(CommandResult result): void + + showWelcome(): void + + showFarewell(): void + + showFirstTime(): void +} + +class CommandResult { + - message: String + + + getMessage(): String +} + +Ui ..> CommandResult + +@enduml \ No newline at end of file diff --git a/docs/diagrams/viewMealSequenceDiagram.puml b/docs/diagrams/viewMealSequenceDiagram.puml new file mode 100644 index 0000000000..795276d9db --- /dev/null +++ b/docs/diagrams/viewMealSequenceDiagram.puml @@ -0,0 +1,53 @@ +@startuml +actor User +participant "UI" as UI +participant "BuffBuddy" as BB +participant "Parser" as Parser +participant "ViewMealCommand" as ViewCmd +participant "History" as History +participant "DailyRecord" as DailyRec + +User -> UI: "meal add /n [mealName] /c [calories]" +activate UI +BB -> UI: readCommand() +activate BB +UI --> BB: command object +BB -> Parser: parse(command) +activate Parser + +create ViewCmd +Parser --> ViewCmd: ViewMealCommand(date) +deactivate Parser +activate ViewCmd + +ViewCmd -> History: getRecordByDate(date) +activate History + +alt DailyRecord exists + History --> ViewCmd: DailyRecord object +else No DailyRecord exists +create DailyRec + History -> DailyRec: new DailyRecord() + activate DailyRec + DailyRec --> History + deactivate DailyRec + History --> ViewCmd +end +deactivate History + +ViewCmd -> DailyRec: getMealListFromRecord() +activate DailyRec +DailyRec --> ViewCmd: MealList object +deactivate DailyRec + +ViewCmd --> BB: CommandResult object +deactivate ViewCmd + +BB -> UI: Show CommandResult +deactivate BB +destroy ViewCmd + +UI -> User: Display success message +deactivate UI + +@enduml diff --git a/docs/diagrams/viewProgramme.puml b/docs/diagrams/viewProgramme.puml new file mode 100644 index 0000000000..dbf9df3aff --- /dev/null +++ b/docs/diagrams/viewProgramme.puml @@ -0,0 +1,79 @@ +@startuml +actor User +participant ":Ui" as Ui +participant ":BuffBuddy" as BuffBuddy +participant ":Parser" as Parser +participant ":CommandFactory" as CommandFactory +participant ":ProgCommandFactory" as ProgCommandFactory +participant ":ViewProgrammeCommand" as Command +participant ":ProgrammeList" as Model +participant ":CommandResult" as CommandResult + + +User -> Ui : "prog view..." +activate Ui +BuffBuddy -> Ui: readCommand() +activate BuffBuddy +Ui --> BuffBuddy : "prog view..." + +BuffBuddy -> Parser : parse("prog view...") +activate Parser + +Parser -> CommandFactory : createCommand("prog", "view...") + +activate CommandFactory + +CommandFactory -> ProgCommandFactory : parse("view", indexString) +activate ProgCommandFactory +ProgCommandFactory -> ProgCommandFactory : prepareViewCommand(indexString) +activate ProgCommandFactory + + +ProgCommandFactory -> "ParserUtils" : parseIndex(indexString) +activate "ParserUtils" +ProgCommandFactory <-- "ParserUtils" : parsed programme index +deactivate "ParserUtils" +create Command +ProgCommandFactory -> Command : ViewProgrammeCommand(programmeIndex) + + +activate Command +ProgCommandFactory <-- Command: created Command +ProgCommandFactory --> ProgCommandFactory +deactivate ProgCommandFactory +CommandFactory <-- ProgCommandFactory: created Command +deactivate ProgCommandFactory +Parser <-- CommandFactory: created Command +deactivate CommandFactory +BuffBuddy <-- Parser: created Command +deactivate Parser + + +BuffBuddy -> Command : execute() + + +alt if programme exists + Command -> Model : getProgramme(index) + activate Model + Command <-- Model: selected Programme + deactivate Model +else programme does not exist + +end +create CommandResult +Command -> CommandResult: CommandResult(...) + + +deactivate Command +activate CommandResult +BuffBuddy <-- CommandResult : return CommandResult +deactivate CommandResult +BuffBuddy -> Ui : showMessage(...) + +deactivate BuffBuddy +destroy CommandResult +User <-- Ui : "Viewing programme..." +deactivate Ui + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/viewWaterSequenceDiagram.puml b/docs/diagrams/viewWaterSequenceDiagram.puml new file mode 100644 index 0000000000..4a4cddc0f0 --- /dev/null +++ b/docs/diagrams/viewWaterSequenceDiagram.puml @@ -0,0 +1,71 @@ +@startuml + +actor User +participant ":UI" as UI +participant ":BuffBuddy" as BB +participant ":Parser" as Parser +participant ":ViewWaterCommand" as Cmd +participant ":History" as History +participant ":DailyRecord" as DailyRec + +User -> UI: "water view /t Date" +activate UI +BB -> UI: readCommand() +activate BB +UI --> BB: command object +BB -> Parser: parse(command) +activate Parser +Parser -> Parser : Parse inputs +note right +parser interactions are put under +the parser for simplification +end note +activate Parser +deactivate Parser +create Cmd +Parser --> Cmd: new ViewWaterCommand(date) +deactivate Parser + +activate Cmd + +Cmd -> History: getRecordByDate(date) +activate History + +alt DailyRecord does not exist + create DailyRec + History -> DailyRec: new DailyRecord() + activate DailyRec + DailyRec --> History + History --> Cmd + deactivate DailyRec +else No DailyRecord exists + History --> Cmd: DailyRecord object +end +deactivate History + +activate Cmd +Cmd -> DailyRecord: getWaterFromRecord(date) +activate DailyRecord + +DailyRecord -> Water: getWaterList() +activate Water +Water --> DailyRecord +deactivate Water + +DailyRecord --> Cmd: Water list +deactivate DailyRecord + +deactivate Cmd + + +Cmd --> BB: CommandResult object +deactivate Cmd +destroy Cmd + +BB -> UI: Show CommandResult +deactivate BB + +UI -> User: Display success message +deactivate UI + +@enduml \ No newline at end of file diff --git a/docs/diagrams/waterClassDiagram.puml b/docs/diagrams/waterClassDiagram.puml new file mode 100644 index 0000000000..b4dc7393a3 --- /dev/null +++ b/docs/diagrams/waterClassDiagram.puml @@ -0,0 +1,15 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + +class Water { + - waterList : ArrayList + + Water(): void + + isEmpty() : boolean + + addWater(float water) : void + + deleteWater(int index) : float + + getWaterList() : ArrayList +} + +@enduml \ No newline at end of file diff --git a/docs/diagrams/waterCommandSummary.puml b/docs/diagrams/waterCommandSummary.puml new file mode 100644 index 0000000000..4750d26d01 --- /dev/null +++ b/docs/diagrams/waterCommandSummary.puml @@ -0,0 +1,29 @@ +@startuml + +skinparam classAttributeIconSize 0 +hide circle + +class WaterCommand <> { + # date: LocalDate + + execute(History): CommandResult +} + +class AddWaterCommand { + - amount: int + + execute(History): CommandResult +} + +class DeleteWaterCommand { + - waterIndex: int + + execute(History): CommandResult +} + +class ViewWaterCommand { + + execute(History): CommandResult +} + +WaterCommand <|-- AddWaterCommand +WaterCommand <|-- DeleteWaterCommand +WaterCommand <|-- ViewWaterCommand + +@enduml diff --git a/docs/diagrams/waterFactoryComponent.puml b/docs/diagrams/waterFactoryComponent.puml new file mode 100644 index 0000000000..ddec94dd5d --- /dev/null +++ b/docs/diagrams/waterFactoryComponent.puml @@ -0,0 +1,24 @@ +@startuml +skinparam classAttributeIconSize 0 +hide circle + +class WaterCommandFactory { + - COMMAND_WORD: String + + + parse(String argumentString): Command + - prepareAddCommand(String argumentString): AddWaterCommand + - prepareDeleteCommand(String argumentString): DeleteWaterCommand + - prepareViewCommand(String argumentString): ViewWaterCommand +} + +WaterCommandFactory "1" --> "1" InvalidCommand : " creates" +WaterCommandFactory "1" --> "1" DeleteWaterCommand : " creates" +WaterCommandFactory "1" --> "1" AddWaterCommand : " creates" +WaterCommandFactory "1 " --> "1" ViewWaterCommand : " creates" +WaterCommandFactory "1" --> "1" FlagParser : " creates" + +WaterCommandFactory ..> ParserUtils +WaterCommandFactory ..> FlagDefinitions +WaterCommandFactory ..> WaterException + +@enduml diff --git a/docs/diagrams/weeklysummarySequenceDiagram.puml b/docs/diagrams/weeklysummarySequenceDiagram.puml new file mode 100644 index 0000000000..b7be077a8a --- /dev/null +++ b/docs/diagrams/weeklysummarySequenceDiagram.puml @@ -0,0 +1,60 @@ +@startuml +actor User +participant UI +participant BuffBuddy +participant Parser +participant HistoryCommandFactory +participant WeeklySummaryCommand +participant History +participant DailyRecord + +User -> UI: "history wk" +activate UI +UI -> BuffBuddy: readCommand("history wk") +activate BuffBuddy +BuffBuddy -> Parser: parse("history wk") +activate Parser +Parser -> HistoryCommandFactory: create command("wk") +activate HistoryCommandFactory +HistoryCommandFactory -> WeeklySummaryCommand: new WeeklySummaryCommand() +deactivate HistoryCommandFactory +deactivate Parser + +WeeklySummaryCommand -> History: getWeeklyWorkoutSummary() +activate WeeklySummaryCommand +activate History + +alt No DailyRecords in past week + History -> WeeklySummaryCommand: "No workout history available for the past week." +else DailyRecords with workout data + loop Each DailyRecord in past week + History -> DailyRecord: getDayFromRecord() + activate DailyRecord + alt DailyRecord has Day + DailyRecord -> DailyRecord: getExercisesCount() + loop Each exercise + DailyRecord -> DailyRecord: getExercise(i) + DailyRecord -> WeeklySummaryCommand: Add exercise to summary + end + else DailyRecord has no Day + History -> WeeklySummaryCommand: Ignore DailyRecord + end + deactivate DailyRecord + destroy DailyRecord + end + WeeklySummaryCommand -> History: formatted weekly summary string +end +deactivate History +destroy History + +WeeklySummaryCommand -> BuffBuddy: CommandResult with weekly summary +deactivate WeeklySummaryCommand +destroy WeeklySummaryCommand + +BuffBuddy -> UI: Show CommandResult (weekly summary or no history message) +deactivate BuffBuddy +UI -> User: Display weekly summary or "No workout history available for the past week." +deactivate UI + +@enduml + diff --git a/docs/images/DailyRecordClass.png b/docs/images/DailyRecordClass.png new file mode 100644 index 0000000000..6a4ab4740a Binary files /dev/null and b/docs/images/DailyRecordClass.png differ diff --git a/docs/images/addDayCommand.png b/docs/images/addDayCommand.png new file mode 100644 index 0000000000..a2328fb7f6 Binary files /dev/null and b/docs/images/addDayCommand.png differ diff --git a/docs/images/addExerciseCommand.png b/docs/images/addExerciseCommand.png new file mode 100644 index 0000000000..4021ed7b96 Binary files /dev/null and b/docs/images/addExerciseCommand.png differ diff --git a/docs/images/addMealActivityDiagram.png b/docs/images/addMealActivityDiagram.png new file mode 100644 index 0000000000..9d5546156f Binary files /dev/null and b/docs/images/addMealActivityDiagram.png differ diff --git a/docs/images/addMealSequenceDiagram.png b/docs/images/addMealSequenceDiagram.png new file mode 100644 index 0000000000..c6c770cf16 Binary files /dev/null and b/docs/images/addMealSequenceDiagram.png differ diff --git a/docs/images/addWaterActivityDiagram.png b/docs/images/addWaterActivityDiagram.png new file mode 100644 index 0000000000..d9c2c42bcb Binary files /dev/null and b/docs/images/addWaterActivityDiagram.png differ diff --git a/docs/images/addWaterSequenceDiagram.png b/docs/images/addWaterSequenceDiagram.png new file mode 100644 index 0000000000..44e5e7329e Binary files /dev/null and b/docs/images/addWaterSequenceDiagram.png differ diff --git a/docs/images/commandSummary.png b/docs/images/commandSummary.png new file mode 100644 index 0000000000..204d7be9d0 Binary files /dev/null and b/docs/images/commandSummary.png differ diff --git a/docs/images/commonUtilsComponent.png b/docs/images/commonUtilsComponent.png new file mode 100644 index 0000000000..698c75a50e Binary files /dev/null and b/docs/images/commonUtilsComponent.png differ diff --git a/docs/images/createCommand.jpeg b/docs/images/createCommand.jpeg new file mode 100644 index 0000000000..145ccc971c Binary files /dev/null and b/docs/images/createCommand.jpeg differ diff --git a/docs/images/createProgramme.png b/docs/images/createProgramme.png new file mode 100644 index 0000000000..8866c0c629 Binary files /dev/null and b/docs/images/createProgramme.png differ diff --git a/docs/images/deleteDayCommand.png b/docs/images/deleteDayCommand.png new file mode 100644 index 0000000000..473da49f2b Binary files /dev/null and b/docs/images/deleteDayCommand.png differ diff --git a/docs/images/deleteExerciseCommand.png b/docs/images/deleteExerciseCommand.png new file mode 100644 index 0000000000..94c626d596 Binary files /dev/null and b/docs/images/deleteExerciseCommand.png differ diff --git a/docs/images/deleteMealSequenceDiagram.png b/docs/images/deleteMealSequenceDiagram.png new file mode 100644 index 0000000000..1825f78a7c Binary files /dev/null and b/docs/images/deleteMealSequenceDiagram.png differ diff --git a/docs/images/deleteProgramme.png b/docs/images/deleteProgramme.png new file mode 100644 index 0000000000..6733d95dec Binary files /dev/null and b/docs/images/deleteProgramme.png differ diff --git a/docs/images/deleteWaterSequenceDiagram.png b/docs/images/deleteWaterSequenceDiagram.png new file mode 100644 index 0000000000..1da5cf67df Binary files /dev/null and b/docs/images/deleteWaterSequenceDiagram.png differ diff --git a/docs/images/editCommand.png b/docs/images/editCommand.png new file mode 100644 index 0000000000..e3bd723f52 Binary files /dev/null and b/docs/images/editCommand.png differ diff --git a/docs/images/editCommandActivityDiagram.png b/docs/images/editCommandActivityDiagram.png new file mode 100644 index 0000000000..d41eace417 Binary files /dev/null and b/docs/images/editCommandActivityDiagram.png differ diff --git a/docs/images/editCommandSummary.png b/docs/images/editCommandSummary.png new file mode 100644 index 0000000000..c2bcae5a93 Binary files /dev/null and b/docs/images/editCommandSummary.png differ diff --git a/docs/images/editExerciseCommand.png b/docs/images/editExerciseCommand.png new file mode 100644 index 0000000000..825f3cc231 Binary files /dev/null and b/docs/images/editExerciseCommand.png differ diff --git a/docs/images/flagDefinitionsComponent-0.png b/docs/images/flagDefinitionsComponent-0.png new file mode 100644 index 0000000000..510fd437eb Binary files /dev/null and b/docs/images/flagDefinitionsComponent-0.png differ diff --git a/docs/images/flagDefinitionsComponent.png b/docs/images/flagDefinitionsComponent.png new file mode 100644 index 0000000000..3766634e99 Binary files /dev/null and b/docs/images/flagDefinitionsComponent.png differ diff --git a/docs/images/flagParserComponent.png b/docs/images/flagParserComponent.png new file mode 100644 index 0000000000..26b153fefa Binary files /dev/null and b/docs/images/flagParserComponent.png differ diff --git a/docs/images/historyCommandSummary.png b/docs/images/historyCommandSummary.png new file mode 100644 index 0000000000..dac4c3eb8e Binary files /dev/null and b/docs/images/historyCommandSummary.png differ diff --git a/docs/images/historyComponent.png b/docs/images/historyComponent.png new file mode 100644 index 0000000000..f063f63d8f Binary files /dev/null and b/docs/images/historyComponent.png differ diff --git a/docs/images/historyFactoryComponent.png b/docs/images/historyFactoryComponent.png new file mode 100644 index 0000000000..80a72f9c79 Binary files /dev/null and b/docs/images/historyFactoryComponent.png differ diff --git a/docs/images/loadProgrammeListSeqenceDiagram.png b/docs/images/loadProgrammeListSeqenceDiagram.png new file mode 100644 index 0000000000..39b86a2871 Binary files /dev/null and b/docs/images/loadProgrammeListSeqenceDiagram.png differ diff --git a/docs/images/loadProgrammeListSequenceDiagram.png b/docs/images/loadProgrammeListSequenceDiagram.png new file mode 100644 index 0000000000..5d898b146c Binary files /dev/null and b/docs/images/loadProgrammeListSequenceDiagram.png differ diff --git a/docs/images/logProgrammeSequenceDiagram.png b/docs/images/logProgrammeSequenceDiagram.png new file mode 100644 index 0000000000..4e59e51d5d Binary files /dev/null and b/docs/images/logProgrammeSequenceDiagram.png differ diff --git a/docs/images/mealAndMealListClassDiagram.png b/docs/images/mealAndMealListClassDiagram.png new file mode 100644 index 0000000000..3e844ac759 Binary files /dev/null and b/docs/images/mealAndMealListClassDiagram.png differ diff --git a/docs/images/mealCommandSummary.png b/docs/images/mealCommandSummary.png new file mode 100644 index 0000000000..1877e58896 Binary files /dev/null and b/docs/images/mealCommandSummary.png differ diff --git a/docs/images/mealFactoryComponent.png b/docs/images/mealFactoryComponent.png new file mode 100644 index 0000000000..0d0867dac4 Binary files /dev/null and b/docs/images/mealFactoryComponent.png differ diff --git a/docs/images/parserComponent.png b/docs/images/parserComponent.png new file mode 100644 index 0000000000..f141337c5b Binary files /dev/null and b/docs/images/parserComponent.png differ diff --git a/docs/images/parserUtilsComponent.png b/docs/images/parserUtilsComponent.png new file mode 100644 index 0000000000..ef83e5020b Binary files /dev/null and b/docs/images/parserUtilsComponent.png differ diff --git a/docs/images/progFactoryComponent.png b/docs/images/progFactoryComponent.png new file mode 100644 index 0000000000..b5114c59a9 Binary files /dev/null and b/docs/images/progFactoryComponent.png differ diff --git a/docs/images/programmeCommandSummary.png b/docs/images/programmeCommandSummary.png new file mode 100644 index 0000000000..b4ae939e09 Binary files /dev/null and b/docs/images/programmeCommandSummary.png differ diff --git a/docs/images/programmeComponentClassDiagram.png b/docs/images/programmeComponentClassDiagram.png new file mode 100644 index 0000000000..71a00b2ae6 Binary files /dev/null and b/docs/images/programmeComponentClassDiagram.png differ diff --git a/docs/images/saveSeqeunceDiagram.png b/docs/images/saveSeqeunceDiagram.png new file mode 100644 index 0000000000..ee6c7a668d Binary files /dev/null and b/docs/images/saveSeqeunceDiagram.png differ diff --git a/docs/images/saveSequenceDiagram.png b/docs/images/saveSequenceDiagram.png new file mode 100644 index 0000000000..71b98021a0 Binary files /dev/null and b/docs/images/saveSequenceDiagram.png differ diff --git a/docs/images/startProgramme.png b/docs/images/startProgramme.png new file mode 100644 index 0000000000..9c72a580f2 Binary files /dev/null and b/docs/images/startProgramme.png differ diff --git a/docs/images/storageAndFileManager.png b/docs/images/storageAndFileManager.png new file mode 100644 index 0000000000..0f843e3879 Binary files /dev/null and b/docs/images/storageAndFileManager.png differ diff --git a/docs/images/uiComponent.png b/docs/images/uiComponent.png new file mode 100644 index 0000000000..403d997c2e Binary files /dev/null and b/docs/images/uiComponent.png differ diff --git a/docs/images/viewMealSequenceDiagram.png b/docs/images/viewMealSequenceDiagram.png new file mode 100644 index 0000000000..90c93dde90 Binary files /dev/null and b/docs/images/viewMealSequenceDiagram.png differ diff --git a/docs/images/viewProgramme.png b/docs/images/viewProgramme.png new file mode 100644 index 0000000000..95447b73ac Binary files /dev/null and b/docs/images/viewProgramme.png differ diff --git a/docs/images/viewWaterSequenceDiagram.png b/docs/images/viewWaterSequenceDiagram.png new file mode 100644 index 0000000000..9bd87f2e7a Binary files /dev/null and b/docs/images/viewWaterSequenceDiagram.png differ diff --git a/docs/images/waterClassDiagram.png b/docs/images/waterClassDiagram.png new file mode 100644 index 0000000000..ad98aefb69 Binary files /dev/null and b/docs/images/waterClassDiagram.png differ diff --git a/docs/images/waterCommandSummary.png b/docs/images/waterCommandSummary.png new file mode 100644 index 0000000000..310104880c Binary files /dev/null and b/docs/images/waterCommandSummary.png differ diff --git a/docs/images/waterFactoryComponent.png b/docs/images/waterFactoryComponent.png new file mode 100644 index 0000000000..6f7572dedc Binary files /dev/null and b/docs/images/waterFactoryComponent.png differ diff --git a/docs/images/weeklySummarySequenceDiagram.png b/docs/images/weeklySummarySequenceDiagram.png new file mode 100644 index 0000000000..71c43d9dd7 Binary files /dev/null and b/docs/images/weeklySummarySequenceDiagram.png differ diff --git a/docs/team/andreusxcarvalho.md b/docs/team/andreusxcarvalho.md new file mode 100644 index 0000000000..dcdf76e3a7 --- /dev/null +++ b/docs/team/andreusxcarvalho.md @@ -0,0 +1,43 @@ +# Project Portfolio: Carvalho Andreus Roby + +## Project: BuffBuddy +BuffBuddy is a CLI-based fitness tracker that helps users manage workout routines, meals, water intake, and personal bests. The History feature provides detailed tracking and analysis of workout progress over time. + +## Summary of Contributions + +### Code Contributed +- **Code Link**: [Click here to view my code on the tP Code Dashboard](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=carvalho&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-09-20&tabOpen=true&tabType=authorship&tabAuthor=andreusxcarvalho&tabRepo=AY2425S1-CS2113-W10-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented + +- **History Feature Commands**: Developed commands to manage and track workout history and personal bests. + + - **ListPersonalBestsCommand** (`history pb`): Lists personal bests across all exercises. + - **ViewPersonalBestCommand** (`history pb `): Displays personal best for a specified exercise. + - **WeeklySummaryCommand**: Provides a summary of weekly workout activity. + - **ListHistoryCommand**: Lists workout history records. + - **ViewHistoryCommand**: Displays specific details of a selected history entry. + + - **Justification**: These commands provide users with flexible options to review, track, and manage workout progress, enhancing motivation and tracking accuracy. + - **Highlights**: Commands support error handling and validation, ensuring smooth user interaction and accurate data tracking. + +### Contributions to the User Guide (UG) +- Documented sections for each history-related command, explaining usage and expected outcomes. + +### Contributions to the Developer Guide (DG) + +- **Sections Contributed**: Documented the entire History feature, covering all commands and their interactions. +- **History Component Overview**: Provided an overview of the History component and its role within BuffBuddy. + +- **Class Diagrams**: + - **Comprehensive History Class Diagram**: Created a detailed UML class diagram illustrating the structure of the History component and its interactions with other components. + - **History Commands Class Diagram**: Designed a class diagram that details the structure and relationships of all the History-related command classes. + +- **Sequence Diagrams**: + - **WeeklySummaryCommand Sequence Diagram**: Designed a complete sequence diagram for the `WeeklySummaryCommand` feature, representing its flow and interactions. + - **LogProgrammeCommand Sequence Diagram**: Developed a sequence diagram for the `LogProgrammeCommand` feature, illustrating the interactions between components during the logging process. + +### Contributions to Team-Based Tasks +- Participated in team meetings and code reviews. +- Assisted with integrating commands into the main BuffBuddy structure. + diff --git a/docs/team/atulteja.md b/docs/team/atulteja.md new file mode 100644 index 0000000000..e4f75c2416 --- /dev/null +++ b/docs/team/atulteja.md @@ -0,0 +1,56 @@ +# Project Portfolio: Atul Teja Vellampalli + +## Project: BuffBuddy +BuffBuddy is a workout and meal tracker that tracks your programmes, workouts, meals and water intake alongside tracking your calories and personal bests. The user interacts with it using a CLI. + +## Summary of Contributions + +### Code Contributed +- **Code Link**: [Click here to view my code on the tP Code Dashboard](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=Atulteja&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-09-20&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=Atulteja&tabRepo=AY2425S1-CS2113-W10-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented + +- **Meals Intake features**: Designed and wrote the code to add, delete and view meals. + - **What it does**: This feature allows the user to add, delete or view meals to their daily record for that day. They are also able to add, delete and view meals from previous days. + - **Justification**: The meals features is essential in tracking ones calorie intake to visualise the amount of food so as to make changes to their diet to better reach their goals. + +- **Program Feature classes**: Designed and wrote the Exercise, Day, Programme, Programme List classes handling the programme features. + - **What it does**: These classes form the foundational components for managing and supporting the creation, modification, and tracking of exercise programs. + - **Justification**: These classes are essential building blocks for a structured approach to managing exercise programs. This way, users can handle workout data modularly. + +### Contributions to the User Guide (UG) +- Added/edited the following sections: + - **Added documentation for meal related features** + - Provided comprehensive explanations and examples on how users can add, delete and view their meals. + - **Added documentation for the water related features**: + - Provided comprehensive explanations and examples on how users can add, delete and view their water intake. +- Added all these functions to the summary table at the end if the UG for ease of reference. + +### Contributions to the Developer Guide (DG) +- **Sections Contributed**: Addmeal feature, Meal, Meallist and water components +- **UML Diagrams**: + - **Meal, MealList and water class diagrams** + - Illustrates the structure of the meal, meallist and water classes within the system, listing out all its methods and parameters whilst depicting their accessibility. + - **Addmeal, Deletemeal and Viewmeal sequence diagram** + - Created a sequence diagram for the 3 commands, illustrating the step-by-step interaction between various classes and components. + - **Addmeal activity diagram** + - Created an activity diagram for the "Add Meal" command, detailing the workflow from user input to returning a success message. + +### Contributions to Team-Based Tasks +- Actively participated in team meetings and discussions +- Helped in designing the class and data structures for the programme meal and water components +- Participated in a collaborative debugging session to identify and resolve issues before the V2.0 release + +### Review/Mentoring Contributions +- **Pull Request Reviews**: + - [PR #31 - Add Create and Edit Command](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/31) + - [PR #151 - Added WaterCommandFactory and ViewWaterCommand classes ](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/151) + - [PR #159 - Fix History and Logging Issue](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/159) + - [PR #163 - Shift building of string from PBCommands to History class](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/163) + - [PR #214 - Java Docs for Storage, Water and FileManager](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/214) + - [PR #218 - Polish History features ](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/218) + - [PR #224 - Update Edit Programme User Guide](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/224) + +### Contributions Beyond the Project Team +- Bug testing for other teams doing peer reviews + diff --git a/docs/team/bev-low.md b/docs/team/bev-low.md new file mode 100644 index 0000000000..88efbd19ef --- /dev/null +++ b/docs/team/bev-low.md @@ -0,0 +1,62 @@ +# Project Portfolio: Low Beverly + +## Project: BuffBuddy +BuffBuddy is a fitness tracker that tracks your programmes, workouts, meals and water intake. The user interacts with +it using a CLI, and it has a GUI created with JavaFX. It is written in Java. + +## Summary of Contributions + +### Code Contributed +- **Code Link**: [Click here to view my code on the tP Code Dashboard](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=bev-low&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-09-20&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=Bev-low&tabRepo=AY2425S1-CS2113-W10-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented + +- **Save/Load Feature**: Enabled data persistence with JSON using Gson, ensuring data continuity across sessions. + - **What it does**: Saves user data in JSON format, maintaining it across application usage. + - **Justification**: Prevents data loss, improving user experience. + - **Highlights**: Integrated with complex data structures and includes error handling with `StorageException` and file validation for data integrity. + - **Credits**: Gson library used for JSON handling. + +- **Water Intake Features**: Added features to log, view, and manage daily water intake. + - **What it does**: Tracks hydration goals by allowing users to add, view, and delete intake entries. + - **Justification**: Supports health-conscious users in monitoring hydration. + - **Highlights**: Commands adhere to object-oriented principles, with error handling to ensure accurate entries. + +- **Custom Exceptions**: Created tailored exceptions for specific errors. + - **What it does**: Provides clear error messages for user issues. + - **Justification**: Improves usability with context-specific guidance. + - **Highlights**: Manages invalid input, out-of-bounds errors, and file corruption, enhancing robustness and data safety. + +### Contributions to the User Guide (UG) +- Added/edited the following sections: + - **Water Features**: Documented usage for adding, viewing, and deleting water entries. + - **Programme Edit Features**: Enhanced readability by splitting programme edit commands into smaller sections. + +### Contributions to the Developer Guide (DG) +- **Sections Contributed**: Storage component, DailyRecord Class, Programme component, Add Water Feature, Save/Load Feature +- **UML Diagrams**: + - **Storage Component**: Shows the structure and interactions of the Storage class, highlighting its methods and connections to FileManager, ProgrammeList, and History, ensuring smooth data handling. + - **DailyRecord Class**: Illustrates the attributes and methods of DailyRecord, emphasizing its role in managing daily workout, meal, and water data. + - **Programme component**: Displays relationships within ProgrammeList, Programme, Day, and Exercise, emphasizing workout programme organization and data flow. + - **Add Water Feature**: Outlines the sequence of operations for adding a water entry, from user action to data storage in DailyRecord. + - **Save/Load Feature**: Shows the sequence for saving/loading data, detailing Storage and FileManager interactions to ensure data persistence and integrity. + + +### Contributions to Team-Based Tasks +- Attended team meetings. +- Set up Gson and Mockito in Gradle. +- Maintained issue tracker and milestones. +- Refactored meal-related functionality to align with `History` and `DailyRecord`, ensuring proper data management. +- Integrated custom exceptions into main and test code for V2.0. +- Collaborated on debugging session to resolve issues pre-V2.0 release. + +### Review/Mentoring Contributions +- **Pull Request Reviews**: + - [PR #151 - Added WaterCommandFactory and ViewWaterCommand classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/151) + - [PR #90 - Add assertions and logging details for files in parser package](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/90) + - [PR #81 - Add assertions & logging to Command classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/81) + - [PR #45 - Add delete & create day functionality to edit command](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/45) +- Mentored team members by providing code feedback and debugging assistance. + +### Contributions Beyond the Project Team +- Reported 15 bugs in Mock PE for [AY2425S1-CS2113-T10-4](https://github.com/AY2425S1-CS2113-T10-4/tp/issues/184) \ No newline at end of file diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/nirala-ts.md b/docs/team/nirala-ts.md new file mode 100644 index 0000000000..a12b170ae6 --- /dev/null +++ b/docs/team/nirala-ts.md @@ -0,0 +1,101 @@ +# Project Portfolio: Nirala Tanishka Singh + +## Project: BuffBuddy +BuffBuddy is a CLI workout and meal tracker that tracks your programmes, workouts, meals and water intake alongside tracking your calories and personal bests. + +## Summary of Contributions + +### Code Contributed +- **Code Link**: [Click here to view my code on the tP Code Dashboard](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-09-20&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=nirala-ts&tabRepo=AY2425S1-CS2113-W10-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented + +1.**Add New Programme**: Feature to create workout programs. +- **What it does**: Allows users to add a new workout program by specifying a name or creating a more detailed program with multiple days and exercises in one command. This enables advanced users to efficiently set up complex routines. +- **Justification**: The flexibility in creating simple or detailed programs caters to both beginner and advanced users, enhancing user experience and saving time when setting up intricate workout plans. +- **Highlights**: This feature’s enhancement for advanced users—allowing multi-day, multi-exercise entries in a single command—significantly reduces setup time for comprehensive programs. + +2.**Set Programme as Active**: Feature to designate an active workout program. +- **What it does**: Lets users set a workout program as "active," allowing other commands to automatically apply to the active program without needing to specify it repeatedly. +- **Justification**: This reduces redundant input, streamlining interactions and improving ease of use for users who frequently access the same workout program. +- **Highlights**: Simplifies user experience by providing a default program, making it more intuitive and efficient to execute common commands. + +3.**List All Programmes**: Feature to display all created workout programs. +- **What it does**: Shows a list of all workout programs along with the active program indicator, offering users an organized view of their routines. +- **Justification**: The list feature helps users maintain an overview of available workout options, facilitating easy selection or modification of programs. +- **Highlights**: Clearly marks the active program in the list, enhancing quick identification and management of routines. + +4.**View Programme**: Feature to show details of a specific workout program. +- **What it does**: Displays the breakdown of a selected program, organized by day and exercise, showing details such as sets, reps, weight, and calories burned. +- **Justification**: This feature enables users to review the structure and specifics of each workout, helping them plan effectively and track their progress. +- **Highlights**: Organizes data in an accessible way for users, providing a comprehensive snapshot of each program’s details. + +5.**Delete Programme**: Feature to remove a workout program. +- **What it does**: Allows users to delete a program by specifying its index or by defaulting to the active program. +- **Justification**: This helps users keep their program list manageable by allowing the removal of unused or outdated workouts. +- **Highlights**: Provides flexibility by supporting both specified and default deletions, enhancing usability for users who frequently update their routines. + + +Each feature in BuffBuddy was designed with advanced input flexibility, allowing parameters to be entered in any order and making certain parameters optional to accommodate varied user preferences. +Additionally, all flags associated with these features have aliases, providing users with alternative command syntax to access the same functionality. This design not only improves usability but also enhances the overall user experience by enabling more intuitive and flexible interactions. +Implementing this flexibility was challenging and required sophisticated parsing logic to ensure consistent and accurate processing of commands. The parsing mechanism was carefully developed to handle alias recognition, optional parameters, and various parameter sequences without compromising functionality. + + +### Contributions to the User Guide (UG) + +In my contributions to the User Guide (UG), I aimed to provide comprehensive documentation for users to effectively utilize the various commands available to personalize and develop their workout programs. +Here’s an overview of the features I documented: + +1. **Add New Programme**: I detailed how users can create a new workout program, from simply naming it to creating complex multi-day schedules with exercises specifying sets, reps, weight, and calories. This allows users to tailor their workout structure to meet their specific fitness goals. +2. **Set Programme as Active**: I explained the functionality of setting a program as "active," which streamlines user interaction by defaulting other commands to this active program. This feature enhances the user experience by making it easier to manage workouts without constantly re-specifying program indices. +3. **List All Programmes**: I outlined the command to view a complete list of user-created workout programs, which provides an overview of all available programs and highlights the active program for easy reference. +4. **View Programme**: I included a breakdown of how to display the details of a particular program, such as exercises organized by day, with full exercise specifications, allowing users to review their progress or plan workouts accordingly. +5. **Delete Programme**: I provided guidance on deleting a program, either by specifying the program index or defaulting to the active program, ensuring users can manage and clear their list of workout programs efficiently. +6. **Exit BuffBuddy**: I illustrated how users can safely exit the application. + +Each feature description includes clear command syntax, examples, and sample outputs to illustrate expected results, helping users to navigate and personalize their fitness routines with confidence. + +In addition to the features I also +- standardized the formatting and descriptions across all features in the User Guide, ensuring consistency and clarity for a smoother user experience +- fixed bugs raised related to UG during to PE-D +- added Command Summary Table +- added Alias Table +- added FAQ section +- added To Note section + +### Contributions to the Developer Guide (DG) + +**Sections Contributed**: Ui Component, Parser Component, Common Component, Create Programme Implementation, User Stories Table + +**UML Diagrams**: +- **UI Component**: Showcased the structure and interactions within the `Ui` component, detailing its methods and relationships with other components. +- **Common Component**: Provided UML diagram for the `Utils` class in `common` package. +- **Parser Component**: Provided a visual overview of the Parser component, which includes the following classes: + - Parser: The main class responsible for handling the parsing of user commands. + - FlagParser: Manages the interpretation and validation of flags in commands, enabling flexibility and alias support. + - ParserUtils: A utility class that supports parsing processes, handling repetitive parsing tasks. + - CommandFactory and specific factories like `HistoryCommandFactory`, `MealCommandFactory`, `ProgCommandFactory`, and `WaterCommandFactory`. + These factories create specific command objects based on the parsed input, enabling modular handling of different command types (e.g., history, meals, programs, and water intake). +- **Create Programme Feature**: Depicted the sequence of operations for the `Create Programme` feature, showing interactions between various classes for example `Ui`, `Parser`, `ProgCommandFactory`, and `ProgrammeList`. + +--- + +### Contributions to Team-Based Tasks +- Actively participated in team meetings. +- Assisted with collaborative tasks such as code integration and discussions on project workflow, project design and implementations. +- Assisted team members in debugging and fixing bugs for version 2.1 + - [PR #287 - standardise UG format, add missing features](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/287) + - [PR #290 - add aliases, update flag definitions ](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/290) + - [PR #291 - add FAQ, update tables](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/291) + - [PR #303 - Update UG](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/303) + +### Review/Mentoring Contributions +- **Pull Request Reviews**: + - [PR #137 - Added defensive coding standards to exercise, day, programme and programmeList classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/137) + - [PR #152 - Add DailyRecord Class](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/152) + - [PR #108 - Storage assert and log](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/108) + - [PR #151 - Added WaterCommandFactory and ViewWaterCommand classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/151) + +### Contributions Beyond the Project Team + +Reported 13 bugs for Y2425S1-CS2113-W14-3 team during PE-D: [Y2425S1-CS2113-W14-3-PED](https://github.com/nirala-ts/ped/issues) diff --git a/docs/team/tvageesan.md b/docs/team/tvageesan.md new file mode 100644 index 0000000000..6d97417965 --- /dev/null +++ b/docs/team/tvageesan.md @@ -0,0 +1,71 @@ +# Project Portfolio: Thiru Vageesan + +## Project: BuffBuddy + +BuffBuddy is a workout and meal tracker that tracks your programmes, workouts, meals and water intake alongside tracking your calories and personal bests. The user interacts with it using a CLI. + +## Summary of Contributions + +### Code Contributed + +- **Code Link**: [Click here to view my code on the tP Code Dashboard](https://nus-cs2113-ay2425s1.github.io/tp-dashboard/?search=tvageesan&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2024-09-20&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=TVageesan&tabRepo=AY2425S1-CS2113-W10-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +### Enhancements Implemented + +- Implemented the `BuffBuddy` class which serves as the main entry point for the application. This class initializes the UI, parser, storage, programme list, and history, and contains the main application loop. +- Implemented the `Edit Programme` epic feature as well as its associated features `Edit Exercise`, `Create Exercise`, `Delete Exercise`, `Create Day`, `Delete Day` which allows the user to edit the details of a programme. +- Designed and implemented initial version of `ProgrammeCommand` classes, which allows the user to add, delete and view programmes. +- Implemented the `Delete Workout Log` feature, which allows the user to delete a logged workout from `History`. + +### Contributions to the User Guide (UG) + +- Added/edited the following sections: + - **Added documentation for edit programme related features**: Description of the section or feature. + - **Standardized format for all commands**: Made sure that all command descriptions were kept consistent and followed the same format. + - **Added important notes on command format**: Added notes on the format of the arguments for each command to avoid user confusion. + +### Contributions to the Developer Guide (DG) + +- **Sections Contributed**: Command component, feature documentation for `Edit Programme` +- **UML Diagrams**: + - **Command Class** as well as all subclasses + - **Edit Command** sequence diagram, activity diagram as well as sequence diagrams for all five of its associated subcommands. + - **Start Programme Command** section and sequence diagram + - **View Programme Command** section and sequence diagram + - **Delete Programme Command** section and sequence diagram +### Contributions to Team-Based Tasks + +- Participated in team meetings. +- Assisted with planning of milestone features and adjusting issues according to time constraints. +- Helped design overall class structure and maintain a consistent vision for the project as the project developed + +### Review/Mentoring Contributions + +- **Pull Request Reviews**: + + - [PR #71 - HistoryTest](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/71) + - [PR #150 - Added MealCommandFactory and ViewMealCommand classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/150) + - [PR #23 - Update Storage Class, toJson/fromJson Methods](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/23) + - [PR #139 - Weeklysummary and personalbests features](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/139) + - [PR #159 - Fix History and Logging Issue](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/159) + - [PR #25 - Add parsing functionalities on user input ](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/25) + - [PR #28 - Add parsing functionalities on user input, Fix bugs related to checkstyle](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/28) + - [PR #29 - Complete History class](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/29) + - [PR #50 - Improve History class](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/50) + - [PR #143 - Add Meal, MealList, skeleton Record, mealcommand and addmealcommand classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/143) + - [PR #144 - Refactor Storage and DataAdapter classes](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/144) + - [PR #179 - Update Developer Guide](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/179) + - [PR #199 - Updated javaDocs for the meals features ](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/199) + - [PR #204 - Edits in Parser Package](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/204) + - [PR #219 - Enhance Test Coverage for Programme Commands](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/219) + - [PR #286 - V2.0 bug fix](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/286) + - [PR #231 - Fix the string in print PB commands](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/231) + - [PR #292 - Fix DG Order ](https://github.com/AY2425S1-CS2113-W10-3/tp/pull/292) + +- Mentored team members by providing code feedback and debugging assistance. +- Reviewed code consistently such that the team adhered to good design principles and coding standards. +- Suggested & helped plan larger refactors such as the creation of `FlagParser` or `FileManager` classes + +### Contributions Beyond the Project Team + +Reported 11 bugs for the WheresMyMoney team during PE Dry run: https://github.com/TVageesan/ped/issues diff --git a/src/main/java/BuffBuddy.java b/src/main/java/BuffBuddy.java new file mode 100644 index 0000000000..3d98aebf2f --- /dev/null +++ b/src/main/java/BuffBuddy.java @@ -0,0 +1,86 @@ +// @@author TVageesan +import command.Command; +import command.CommandResult; +import command.ExitCommand; +import storage.Storage; +import history.History; +import parser.Parser; +import ui.Ui; +import programme.ProgrammeList; +import java.util.NoSuchElementException; + +/** +* Represents the main class of the BuffBuddy application, a fitness tracking +* program designed to manage user commands, interact with storage, display UI messages, +* and maintain a history of commands and user programs. +*/ + +public class BuffBuddy { + private static final String DEFAULT_FILE_PATH = "./data/data.json"; + + private final Ui ui; + private final History history; + private final ProgrammeList programmes; + private final Storage storage; + private final Parser parser; + + public BuffBuddy(String filePath) { + ui = new Ui(); + parser = new Parser(); + storage = new Storage(filePath); + programmes = storage.loadProgrammeList(); + history = storage.loadHistory(); + ui.showMessage(storage.getMessage()); + } + + /** + * Main entry point for the BuffBuddy application. + * Initializes a BuffBuddy instance with the default file path and + * starts the main command handling loop. + * + * @param args Command-line arguments (unused). + */ + + public static void main(String[] args) { + new BuffBuddy(DEFAULT_FILE_PATH).run(); + } + + /** + * Runs the main program loop for BuffBuddy, displaying a welcome message, + * handling user commands, and displaying a farewell message upon exit. + */ + + public void run() { + ui.showWelcome(); + handleCommands(); + ui.showFarewell(); + } + + /** + * Handles the command processing loop, reading commands from the user, + * parsing them, executing the corresponding actions, and saving data. + * Exits the loop when an ExitCommand is issued. + */ + + private void handleCommands() { + while(true) { + try { + String fullCommand = ui.readCommand(); + Command command = parser.parse(fullCommand); + CommandResult result = command.execute(programmes, history); + ui.showMessage(result); + if (command instanceof ExitCommand) { + return; + } + storage.saveData(programmes,history); + } catch (Exception e) { + // NoSuchElementException occurs on CTRL + C exit of BuffBuddy, and thus should not be printed + if (e instanceof NoSuchElementException) { + continue; + } + ui.showMessage(e); + } + } + } +} + diff --git a/src/main/java/command/Command.java b/src/main/java/command/Command.java new file mode 100644 index 0000000000..512dfd01bb --- /dev/null +++ b/src/main/java/command/Command.java @@ -0,0 +1,36 @@ +//@@author TVageesan +package command; +import programme.ProgrammeList; +import history.History; + +/** + * Represents an abstract command. + */ +public abstract class Command { + /** + * Constructs a Command. + */ + public Command(){} + + /** + * Executes the command. + * + * @param programmes The list of programmes. + * @param history The history of commands executed. + * @return The result of the command execution. + */ + public abstract CommandResult execute(ProgrammeList programmes, History history); + + /** + * Checks if this Command is equal to another object. + * + * @param other The object to compare with. + * @return true if this Command is equal to the other object, false otherwise. + */ + @Override + public boolean equals(Object other) { + boolean isSameObject = (this == other); + boolean isSameClass = (getClass() != other.getClass()); + return (isSameObject || isSameClass); + } +} diff --git a/src/main/java/command/CommandResult.java b/src/main/java/command/CommandResult.java new file mode 100644 index 0000000000..5c528a9f65 --- /dev/null +++ b/src/main/java/command/CommandResult.java @@ -0,0 +1,53 @@ +//@@author andreusxcarvalho +package command; + +import java.util.Objects; + +/** + * Represents the result of a command execution. + */ + +public class CommandResult { + private final String message; + + /** + * Constructs a CommandResult with the specified message. + * + * @param message The message to be included in the command result. + */ + public CommandResult(String message) { + this.message = message; + } + + /** + * Returns the message of the command result. + * + * @return The message of the command result. + */ + public String getMessage() { + return message; + } + + /** + * Checks if this CommandResult is equal to another object. + * + * @param other The object to compare with. + * @return true if this CommandResult is equal to the other object, false otherwise. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + CommandResult that = (CommandResult) other; + return message.equals(that.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } +} diff --git a/src/main/java/command/ExitCommand.java b/src/main/java/command/ExitCommand.java new file mode 100644 index 0000000000..dc3135aca6 --- /dev/null +++ b/src/main/java/command/ExitCommand.java @@ -0,0 +1,28 @@ +//@@author andreusxcarvalho +package command; +import programme.ProgrammeList; +import history.History; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command to exit the application. + */ +public class ExitCommand extends Command { + public static final String COMMAND_WORD = "bye"; + private final Logger logger = Logger.getLogger(this.getClass().getName()); + + /** + * Executes the exit command. + * + * @param programmes The list of programmes. + * @param history The history of commands executed. + * @return The result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + logger.log(Level.INFO, "ExitCommand executed."); + return new CommandResult("Exiting BuffBuddy..."); + } +} diff --git a/src/main/java/command/InvalidCommand.java b/src/main/java/command/InvalidCommand.java new file mode 100644 index 0000000000..7e5b25dc8b --- /dev/null +++ b/src/main/java/command/InvalidCommand.java @@ -0,0 +1,27 @@ +//@@author andreusxcarvalho +package command; +import programme.ProgrammeList; +import history.History; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command that is invalid. + */ +public class InvalidCommand extends Command { + private final Logger logger = Logger.getLogger(this.getClass().getName()); + + /** + * Executes the invalid command. + * + * @param programmes The list of programmes. + * @param history The history of commands executed. + * @return The result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + logger.log(Level.INFO, "InvalidCommand executed successfully."); + return new CommandResult("Invalid command."); + } +} diff --git a/src/main/java/command/history/DeleteHistoryCommand.java b/src/main/java/command/history/DeleteHistoryCommand.java new file mode 100644 index 0000000000..9403113554 --- /dev/null +++ b/src/main/java/command/history/DeleteHistoryCommand.java @@ -0,0 +1,54 @@ +// @@author andreusxcarvalho +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; + +import java.time.LocalDate; + +import static common.Utils.formatDate; + +/** + * Represents a command to delete a specific history record by date. + *

+ * The {@code DeleteHistoryCommand} checks for the existence of a history record on + * the given date and removes it if present. If the record exists, it returns a message + * confirming deletion and displaying the record. If no record is found, it returns a + * message indicating that no record exists on the specified date. + *

+ */ +public class DeleteHistoryCommand extends HistoryCommand { + public static final String COMMAND_WORD = "delete"; + + /** + * Constructs a {@code DeleteHistoryCommand} with the specified date. + * + * @param date the {@link LocalDate} of the record to be deleted + */ + public DeleteHistoryCommand(LocalDate date) { + super(date); + } + + /** + * Executes the delete command on the given history and returns the result. + *

+ * Attempts to delete the {@link DailyRecord} for the specified date from the {@link History}. + * If the record exists, it is deleted, and a success message with the record details is returned. + * If the record does not exist, a message is returned indicating that no record was found. + *

+ * + * @param history the {@link History} object from which the record is to be deleted + * @return a {@link CommandResult} indicating success if the record is deleted, or an error message if not + */ + @Override + public CommandResult execute(History history) { + DailyRecord record = history.deleteRecord(date); + if (record == null) { + return new CommandResult("No record found at " + formatDate(date)); + } + String result = record.toString(); + return new CommandResult("Deleted record: \n" + result); + } +} + diff --git a/src/main/java/command/history/HistoryCommand.java b/src/main/java/command/history/HistoryCommand.java new file mode 100644 index 0000000000..67697d8967 --- /dev/null +++ b/src/main/java/command/history/HistoryCommand.java @@ -0,0 +1,65 @@ +// @@author andreusxcarvalho +package command.history; + +import command.Command; +import command.CommandResult; +import history.History; +import programme.ProgrammeList; + +import java.time.LocalDate; + +/** + * Represents an abstract command to handle history-related operations. + *

+ * The {@code HistoryCommand} class serves as a base for all commands that operate + * on workout history data, including viewing, listing, and deleting history records. + * Subclasses must implement the {@link #execute(History)} method to define specific + * history-related actions. + *

+ */ +public abstract class HistoryCommand extends Command { + protected LocalDate date; + + /** + * Constructs a {@code HistoryCommand} with a specified date. + * + * @param date the {@link LocalDate} associated with the command's operation + */ + public HistoryCommand(LocalDate date) { + this.date = date; + } + + /** + * Constructs a {@code HistoryCommand} without a specified date. + * This constructor can be used for commands that do not require a specific date. + */ + public HistoryCommand() {} + + /** + * Executes the command with both a {@link ProgrammeList} and {@link History} context. + *

+ * This method delegates to the {@link #execute(History)} method, which subclasses + * must implement to define specific behavior. + *

+ * + * @param programmes the {@link ProgrammeList} (not used in this base class) + * @param history the {@link History} object on which the command operates + * @return the {@link CommandResult} indicating the outcome of the command + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history) { + return execute(history); + } + + /** + * Executes the command on the specified {@link History} object. + *

+ * Subclasses must implement this method to perform specific operations on the history. + *

+ * + * @param history the {@link History} object on which the command operates + * @return the {@link CommandResult} indicating the result of the command execution + */ + public abstract CommandResult execute(History history); +} + diff --git a/src/main/java/command/history/ListHistoryCommand.java b/src/main/java/command/history/ListHistoryCommand.java new file mode 100644 index 0000000000..7217a8ddbd --- /dev/null +++ b/src/main/java/command/history/ListHistoryCommand.java @@ -0,0 +1,29 @@ +// @@author andreusxcarvalho +package command.history; + +import command.CommandResult; +import history.History; + +/** + * Represents a command to list the full history of workout records. + *

+ * The {@code ListHistoryCommand} retrieves all entries in the workout history and + * formats them as a string for display. If the history is empty, a message indicating + * that no history is available will be returned. + *

+ */ +public class ListHistoryCommand extends HistoryCommand { + public static final String COMMAND_WORD = "list"; + + /** + * Executes the command to retrieve and format the entire workout history. + * + * @param history the {@link History} object containing workout records + * @return a {@link CommandResult} with the formatted history or a message if no history is available + */ + @Override + public CommandResult execute(History history) { + return new CommandResult(history.toString()); + } +} + diff --git a/src/main/java/command/history/ListPersonalBestsCommand.java b/src/main/java/command/history/ListPersonalBestsCommand.java new file mode 100644 index 0000000000..05c7bf3c36 --- /dev/null +++ b/src/main/java/command/history/ListPersonalBestsCommand.java @@ -0,0 +1,30 @@ +// @@author andreusxcarvalho +package command.history; + +import command.CommandResult; +import history.History; + +/** + * Command to list personal bests for all exercises. + *

+ * The {@code ListPersonalBestsCommand} retrieves and displays the personal best + * records for each exercise in the workout history. If there are no personal bests + * available, an appropriate message is returned. + *

+ */ +public class ListPersonalBestsCommand extends HistoryCommand { + public static final String COMMAND_WORD = "pb"; + + /** + * Executes the command to retrieve and format the personal bests for all exercises. + * + * @param history the {@link History} object containing workout records + * @return a {@link CommandResult} with the formatted personal bests or a message if no records are found + */ + @Override + public CommandResult execute(History history) { + String result = history.getFormattedPersonalBests(); + return new CommandResult(result); + } +} + diff --git a/src/main/java/command/history/ViewHistoryCommand.java b/src/main/java/command/history/ViewHistoryCommand.java new file mode 100644 index 0000000000..41e520a790 --- /dev/null +++ b/src/main/java/command/history/ViewHistoryCommand.java @@ -0,0 +1,47 @@ +// @@author andreusxcarvalho +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; + +import java.time.LocalDate; + +import static common.Utils.formatDate; + +/** + * Command to view the history for a specific date. + *

+ * The {@code ViewHistoryCommand} retrieves and displays the workout record for a specified date. + * If no record exists for the given date, it returns a message indicating that no record was found. + *

+ */ +public class ViewHistoryCommand extends HistoryCommand { + public static final String COMMAND_WORD = "view"; + + /** + * Constructs a {@code ViewHistoryCommand} for a specific date. + * + * @param date the {@link LocalDate} for which the history record is to be viewed + */ + public ViewHistoryCommand(LocalDate date) { + super(date); + } + + /** + * Executes the command to retrieve and display the history record for the specified date. + * + * @param history the {@link History} object containing workout records + * @return a {@link CommandResult} containing the formatted daily record or a message if no record is found + */ + @Override + public CommandResult execute(History history) { + if (!history.hasRecord(date)) { + return new CommandResult("No record found for " + formatDate(date)); + } + DailyRecord record = history.getRecordByDate(date); + String result = record.toString(); + return new CommandResult(result); + } +} + diff --git a/src/main/java/command/history/ViewPersonalBestCommand.java b/src/main/java/command/history/ViewPersonalBestCommand.java new file mode 100644 index 0000000000..e230d96b63 --- /dev/null +++ b/src/main/java/command/history/ViewPersonalBestCommand.java @@ -0,0 +1,44 @@ +// @@author andreusxcarvalho +package command.history; + +import command.CommandResult; +import history.History; + +/** + * Command to view the personal best for a specific exercise. + *

+ * The {@code ViewPersonalBestCommand} retrieves and displays the personal best record for a specified exercise. + * If no record exists for the exercise, a message indicating this is returned. + *

+ */ +public class ViewPersonalBestCommand extends HistoryCommand { + public static final String COMMAND_WORD = "pb"; + + private final String exerciseName; + + /** + * Constructs a {@code ViewPersonalBestCommand} with a specified exercise name. + * + * @param exerciseName the name of the exercise to view the personal best for + * @throws AssertionError if {@code exerciseName} is null or empty + */ + public ViewPersonalBestCommand(String exerciseName) { + assert exerciseName != null : "Exercise name must not be null"; + assert !exerciseName.isEmpty() : "Exercise name must not be empty"; + this.exerciseName = exerciseName; + } + + /** + * Executes the command to retrieve and display the personal best for the specified exercise. + * + * @param history the {@link History} object containing workout records + * @return a {@link CommandResult} containing the personal best for the specified exercise or a + * message indicating that no record was found + */ + @Override + public CommandResult execute(History history) { + String result = history.getPersonalBestForExercise(exerciseName); + return new CommandResult(result); + } +} + diff --git a/src/main/java/command/history/WeeklySummaryCommand.java b/src/main/java/command/history/WeeklySummaryCommand.java new file mode 100644 index 0000000000..394b0c1084 --- /dev/null +++ b/src/main/java/command/history/WeeklySummaryCommand.java @@ -0,0 +1,29 @@ +// @@author andreusxcarvalho +package command.history; + +import command.CommandResult; +import history.History; + +/** + * Command to view a summary of the weekly workout history. + *

+ * The {@code WeeklySummaryCommand} retrieves a summary of the workouts completed in the past week from the + * {@link History} object and formats it for display. + *

+ */ +public class WeeklySummaryCommand extends HistoryCommand { + public static final String COMMAND_WORD = "wk"; + + /** + * Executes the command to retrieve and display the weekly workout summary. + * + * @param history the {@link History} object containing workout records + * @return a {@link CommandResult} containing the weekly workout summary or a message if no records are available + */ + @Override + public CommandResult execute(History history) { + String weeklySummary = history.getWeeklyWorkoutSummary(); + return new CommandResult("Your weekly workout summary: \n" + weeklySummary); + } +} + diff --git a/src/main/java/command/meals/AddMealCommand.java b/src/main/java/command/meals/AddMealCommand.java new file mode 100644 index 0000000000..2fe2bdd762 --- /dev/null +++ b/src/main/java/command/meals/AddMealCommand.java @@ -0,0 +1,83 @@ +// @@author Atulteja +package command.meals; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import meal.Meal; +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to add a meal to a specific date's daily record. + */ +public class AddMealCommand extends MealCommand { + public static final String COMMAND_WORD = "add"; + private static final Logger logger = Logger.getLogger(AddMealCommand.class.getName()); + + private final Meal mealToAdd; + + /** + * Constructs an AddMealCommand with the specified meal and date. + * + * @param meal the meal to add to the daily record + * @param date the date on which the meal should be added + * @throws AssertionError if the meal is null + */ + public AddMealCommand(Meal meal, LocalDate date) { + super(date); + assert meal != null : "Meal cannot be null"; + this.mealToAdd = meal; + + logger.log(Level.INFO, "AddMealCommand created with meal: {0} for date: {1}", + new Object[]{meal, date}); + } + + /** + * Executes the AddMealCommand, adding the specified meal to the daily record for the given date. + * + * @param history the history containing daily records where the meal will be added + * @return a CommandResult indicating the success of the operation + * @throws AssertionError if the daily record for the specified date is not found + */ + public CommandResult execute(History history) { + DailyRecord dailyRecord = history.getRecordByDate(date); + assert dailyRecord != null : "Daily record not found"; + dailyRecord.addMealToRecord(mealToAdd); + logger.log(Level.INFO, "Meal added: {0}", mealToAdd); + + return new CommandResult(mealToAdd.toString() + " has been added"); + } + + /** + * Checks if this AddMealCommand is equal to another object. + * Two AddMealCommand objects are considered equal if they have the same meal and date. + * + * @param o the object to compare with + * @return true if the specified object is equal to this AddMealCommand, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AddMealCommand that)) { + return false; + } + return Objects.equals(mealToAdd, that.mealToAdd) && + Objects.equals(date, that.date); + } + + /** + * Returns the hash code for AddMealCommand. + * The hash code is based on the meal and date fields. + * + * @return the hash code value for this AddMealCommand + */ + @Override + public int hashCode() { + return Objects.hash(mealToAdd, date); + } +} diff --git a/src/main/java/command/meals/DeleteMealCommand.java b/src/main/java/command/meals/DeleteMealCommand.java new file mode 100644 index 0000000000..32e7717967 --- /dev/null +++ b/src/main/java/command/meals/DeleteMealCommand.java @@ -0,0 +1,73 @@ +// @@author Atulteja +package command.meals; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import meal.Meal; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to delete a meal from a specific date's daily record. + */ +public class DeleteMealCommand extends MealCommand { + public static final String COMMAND_WORD = "delete"; + private static final Logger logger = Logger.getLogger(DeleteMealCommand.class.getName()); + + private final int indexMealToDelete; + + /** + * Constructs a DeleteMealCommand with the specified meal index and date. + * + * @param index the index of the meal to delete from the daily record + * @param date the date from which the meal should be deleted + * @throws AssertionError if the index is negative + */ + public DeleteMealCommand(int index, LocalDate date) { + super(date); + + assert index >= 0 : "Index to delete cannot be negative"; + + this.indexMealToDelete = index; + + logger.log(Level.INFO, "DeleteMealCommand created for index: {0} on date: {1}", + new Object[]{index, date}); + } + + /** + * Executes the DeleteMealCommand, deleting the specified meal from the daily record for the given date. + * + * @param history the history containing daily records where the meal will be deleted + * @return a CommandResult indicating the success of the operation + * @throws AssertionError if the daily record for the specified date is not found + */ + public CommandResult execute(History history) { + DailyRecord dailyRecord = history.getRecordByDate(date); + assert dailyRecord != null : "Daily record not found"; + Meal deletedMeal = dailyRecord.deleteMealFromRecord(indexMealToDelete); + + return new CommandResult(deletedMeal + " has been deleted"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DeleteMealCommand)) { + return false; + } + DeleteMealCommand that = (DeleteMealCommand) o; + return indexMealToDelete == that.indexMealToDelete && + Objects.equals(date, that.date); + } + + @Override + public int hashCode() { + return Objects.hash(indexMealToDelete, date); + } +} diff --git a/src/main/java/command/meals/MealCommand.java b/src/main/java/command/meals/MealCommand.java new file mode 100644 index 0000000000..760f3c8e7f --- /dev/null +++ b/src/main/java/command/meals/MealCommand.java @@ -0,0 +1,57 @@ +// @@author Atulteja +package command.meals; + +import command.Command; +import command.CommandResult; +import history.History; +import programme.ProgrammeList; + +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents an abstract command related to meals, allowing various meal-related operations. + * This class serves as a base for specific meal commands like adding or deleting a meal. + */ +public abstract class MealCommand extends Command { + + private static final Logger logger = Logger.getLogger(MealCommand.class.getName()); + + protected LocalDate date; + + /** + * Constructs a MealCommand with the specified date. + * + * @param date the date associated with this command + * @throws AssertionError if the date is null + */ + public MealCommand(LocalDate date) { + assert date != null : "Date cannot be null"; + this.date = date; + logger.log(Level.INFO, "MealCommand initialized with date: {0}", date); + } + + /** + * Executes the meal-related command using the provided history. + * Subclasses should implement this method to perform their specific command logic. + * + * @param history the history containing daily records where the command will be executed + * @return a CommandResult indicating the success or outcome of the operation + */ + public abstract CommandResult execute(History history); + + /** + * Executes the meal-related command using both the provided programme list and history. + * This implementation only uses the history for meal commands. + * + * @param programmes the programme list, currently unused in this implementation + * @param history the history containing daily records where the command will be executed + * @return a CommandResult indicating the success or outcome of the operation + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history) { + logger.log(Level.INFO, "Executing MealCommand with ProgrammeList and History."); + return execute(history); + } +} diff --git a/src/main/java/command/meals/ViewMealCommand.java b/src/main/java/command/meals/ViewMealCommand.java new file mode 100644 index 0000000000..2a155ee0d3 --- /dev/null +++ b/src/main/java/command/meals/ViewMealCommand.java @@ -0,0 +1,67 @@ +// @@author Atulteja +package command.meals; + +import command.CommandResult; +import common.Utils; +import history.DailyRecord; +import history.History; +import meal.MealList; +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to view meals for a specific date. + */ +public class ViewMealCommand extends MealCommand { + public static final String COMMAND_WORD = "view"; + private static final Logger logger = Logger.getLogger(ViewMealCommand.class.getName()); + + /** + * Constructs a ViewMealCommand for the specified date. + * + * @param date the date for which meals should be viewed + */ + public ViewMealCommand(LocalDate date) { + super(date); + + logger.log(Level.INFO, "ViewMealCommand created for date: {0}", date); + } + + /** + * Executes the ViewMealCommand, retrieving the meals from the daily record for the specified date. + * + * @param history the history containing daily records where the meal list will be retrieved + * @return a CommandResult containing a string representation of the meals for the given date + * @throws AssertionError if the daily record for the specified date is not found + */ + public CommandResult execute(History history) { + logger.log(Level.INFO, "Executing ViewMealCommand for date: {0}", date); + String formattedDate = Utils.formatDate(date); + + DailyRecord dailyRecord = history.getRecordByDate(date); + assert dailyRecord != null : "Daily record not found"; + MealList meals = dailyRecord.getMealListFromRecord(); + + logger.log(Level.INFO, "Retrieved MealList for date {0}: {1}", new Object[]{date, meals}); + + return new CommandResult("Meals for " + formattedDate + ": \n\n" + meals.toString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ViewMealCommand that)) { + return false; + } + return Objects.equals(date, that.date); + } + + @Override + public int hashCode() { + return Objects.hash(date); + } +} diff --git a/src/main/java/command/programme/CreateProgrammeCommand.java b/src/main/java/command/programme/CreateProgrammeCommand.java new file mode 100644 index 0000000000..82e3e1fd77 --- /dev/null +++ b/src/main/java/command/programme/CreateProgrammeCommand.java @@ -0,0 +1,54 @@ +// @@author TVageesan +package command.programme; +import java.util.ArrayList; +import java.util.logging.Level; + +import command.CommandResult; +import programme.Day; +import programme.ProgrammeList; +import programme.Programme; +import history.History; + +/** + * Represents a command to create a new programme in ProgrammeList. + */ +public class CreateProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "create"; + public static final String SUCCESS_MESSAGE_FORMAT = "New programme created: %n%s"; + + private final String programmeName; + private final ArrayList programmeContents; + + /** + * Constructs a CreateProgrammeCommand with the specified programme name and contents. + * + * @param programmeName The name of the programme to be created. + * @param programmeContents The list of days that make up the programme. + */ + public CreateProgrammeCommand(String programmeName, ArrayList programmeContents) { + assert programmeName != null && !programmeName.isEmpty() : "Programme name must not be null"; + assert programmeContents != null : "Programme contents must not be null"; + this.programmeName = programmeName; + this.programmeContents = programmeContents; + } + + /** + * Executes the command to create a new programme and adds it to the programme list. + * + * @param programmes The list of programmes to which the new programme will be added. + * @param history The history object to record the command execution. + * @return A CommandResult object containing the result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + assert programmes != null : "Programme list must not be null"; + + Programme created = programmes.insertProgramme(programmeName, programmeContents); + assert created != null : "programme must be created"; + + logger.log(Level.INFO, "CreateCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, created); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/DeleteProgrammeCommand.java b/src/main/java/command/programme/DeleteProgrammeCommand.java new file mode 100644 index 0000000000..63be5be9f7 --- /dev/null +++ b/src/main/java/command/programme/DeleteProgrammeCommand.java @@ -0,0 +1,46 @@ +// @@author TVageesan +package command.programme; + +import command.CommandResult; +import history.History; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.logging.Level; + +/** + * Represents a command to delete a programme from the ProgrammeList. + */ +public class DeleteProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "delete"; + public static final String SUCCESS_MESSAGE_FORMAT = "Deleted:%n%s"; + + /** + * Constructs a DeleteProgrammeCommand with the specified programme index. + * + * @param programmeIndex The index of the programme to be deleted. + */ + public DeleteProgrammeCommand(int programmeIndex) { + super(programmeIndex); + logger.log(Level.INFO, "DeleteCommand created with programme index: {0}", programmeIndex); + } + + /** + * Executes the command to delete a programme from the programme list. + * + * @param programmes The list of programmes from which the programme will be deleted. + * @param history The history object to record the command execution. + * @return A CommandResult object containing the result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + assert programmes != null : "Programme list must not be null"; + + Programme programme = programmes.deleteProgram(programmeIndex); + + logger.log(Level.INFO, "DeleteCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, programme); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/ListProgrammeCommand.java b/src/main/java/command/programme/ListProgrammeCommand.java new file mode 100644 index 0000000000..0b01725bc3 --- /dev/null +++ b/src/main/java/command/programme/ListProgrammeCommand.java @@ -0,0 +1,26 @@ +package command.programme; + +import command.CommandResult; +import programme.ProgrammeList; +import history.History; + +import java.util.logging.Level; + +public class ListProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "list"; + public static final String SUCCESS_MESSAGE_FORMAT = "Listing programmes: %n%s"; + + public ListProgrammeCommand() { + logger.log(Level.INFO, "ListCommand created."); + } + + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + assert programmes != null : "Programme list must not be null"; + + logger.log(Level.INFO, "ListCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, programmes); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/LogProgrammeCommand.java b/src/main/java/command/programme/LogProgrammeCommand.java new file mode 100644 index 0000000000..6d640b53d8 --- /dev/null +++ b/src/main/java/command/programme/LogProgrammeCommand.java @@ -0,0 +1,105 @@ +// @@author andreusxcarvalho + +package command.programme; +import command.CommandResult; +import history.DailyRecord; +import programme.Programme; +import programme.ProgrammeList; +import programme.Day; +import history.History; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; + +/** + * Represents a command to log a specific day of a programme into the history. + */ +public class LogProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "log"; + + private final LocalDate date; + + /** + * Constructs a LogProgrammeCommand with the specified programme index, day index, and date. + * + * @param programmeIndex The index of the programme. + * @param dayIndex The index of the day. + * @param date The date to log the day into the history. + */ + public LogProgrammeCommand(int programmeIndex, int dayIndex, LocalDate date){ + super(programmeIndex, dayIndex); + + assert dayIndex >= 0 : "Day index must be non-negative"; + assert date != null : "Date must not be null"; + + this.date = date; + logger.log(Level.INFO, + "LogCommand created with progIndex: {0}, dayIndex: {1}, date: {2}", + new Object[]{programmeIndex, dayIndex, date} + ); + } + + /** + * Executes the command to log a specific day of a programme into the history. + * + * @param programmes The list of programmes. + * @param history The history object to record the command execution. + * @return A CommandResult object containing the result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + logger.log( + Level.INFO, + "Executing LogCommand with progIndex: {0}, dayIndex: {1}, date: {2}", + new Object[]{programmeIndex, dayIndex, date} + ); + + assert programmes != null : "ProgrammeList must not be null"; + assert history != null : "History must not be null"; + + Programme selectedProgramme = programmes.getProgramme(programmeIndex); + Day completed = selectedProgramme.getDay(dayIndex); + StringBuilder result = new StringBuilder(); + + DailyRecord dailyRecord = history.getRecordByDate(date); + if (dailyRecord.getDayFromRecord() != null) { + result.append("You are replacing a previously logged day.\n"); + } + dailyRecord.logDayToRecord(completed); + history.logRecord(date, dailyRecord); + + logger.log(Level.INFO, "LogCommand executed successfully for day: {0}", completed); + + result.append(String.format("Congrats! You've successfully completed:%n%s", completed)); + return new CommandResult(result.toString()); + } + + /** + * Checks if this command is equal to another object. + * + * @param o The other object to compare to. + * @return true if the other object is equal to this command, false otherwise. + */ + @Override + public boolean equals(Object o){ + if (this == o){ + return true; + } + + if (!(o instanceof LogProgrammeCommand that)){ + return false; + } + + logger.log( + Level.INFO,"Comparing LogCommand with this: {0}, that: {1}", + new Object[]{this.programmeIndex, that.programmeIndex} + ); + + boolean isProgrammeIndexEqual = (programmeIndex == that.programmeIndex); + boolean isDayIndexEqual = (dayIndex == that.dayIndex); + boolean isDateEqual = Objects.equals(date, that.date); + + return (isProgrammeIndexEqual && isDayIndexEqual && isDateEqual); + } +} diff --git a/src/main/java/command/programme/ProgrammeCommand.java b/src/main/java/command/programme/ProgrammeCommand.java new file mode 100644 index 0000000000..b2d5ca6b1b --- /dev/null +++ b/src/main/java/command/programme/ProgrammeCommand.java @@ -0,0 +1,65 @@ +// @@author TVageesan +package command.programme; +import command.Command; + +import java.util.logging.Logger; + +import static common.Utils.NULL_INTEGER; + +/** + * Represents an abstract command related to programmes. + */ +public abstract class ProgrammeCommand extends Command { + protected final Logger logger = Logger.getLogger(this.getClass().getName()); + protected int programmeIndex; + protected int dayIndex; + + /** + * Constructs a ProgrammeCommand with the specified programme index and day index. + * + * @param programmeIndex The index of the programme. + * @param dayIndex The index of the day. + */ + public ProgrammeCommand(int programmeIndex, int dayIndex) { + this(programmeIndex); + assert dayIndex >= 0 : "dayIndex must not be negative"; + this.dayIndex = dayIndex; + } + + /** + * Constructs a ProgrammeCommand with the specified programme index. + * + * @param programmeIndex The index of the programme. + */ + public ProgrammeCommand(int programmeIndex) { + // We accept NULL_INTEGER as a valid programmeIndex as it is an optional argument + assert programmeIndex == NULL_INTEGER || programmeIndex >= 0 : "Program index must be valid"; + this.programmeIndex = programmeIndex; + } + + /** + * Constructs a ProgrammeCommand with no specified indices. + */ + public ProgrammeCommand(){} + + /** + * Checks if this command is equal to another object. + * + * @param other The other object to compare to. + * @return true if the other object is equal to this command, false otherwise. + */ + @Override + public boolean equals(Object other){ + if (this == other){ + return true; + } + + if (!(other instanceof ProgrammeCommand that)){ + return false; + } + + boolean isProgIndexEqual = this.programmeIndex == that.programmeIndex; + boolean isDayIndexEqual = this.dayIndex == that.dayIndex; + return isProgIndexEqual && isDayIndexEqual; + } +} diff --git a/src/main/java/command/programme/StartProgrammeCommand.java b/src/main/java/command/programme/StartProgrammeCommand.java new file mode 100644 index 0000000000..cc25566f56 --- /dev/null +++ b/src/main/java/command/programme/StartProgrammeCommand.java @@ -0,0 +1,51 @@ +package command.programme; + +import command.CommandResult; +import programme.ProgrammeList; +import programme.Programme; +import history.History; + +import java.util.logging.Level; + +import static common.Utils.NULL_INTEGER; + +/** + * Represents a command to start a specific programme. + */ +public class StartProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "start"; + public static final String SUCCESS_MESSAGE_FORMAT = "Started programme: %n%s"; + + /** + * Constructs a StartProgrammeCommand with the specified programme index. + * + * @param programmeIndex The index of the programme to start. + */ + public StartProgrammeCommand(int programmeIndex) { + super(programmeIndex); + logger.log(Level.INFO, "StartCommand created with programme index: {0}", programmeIndex); + } + + /** + * Executes the command to start a specific programme. + * + * @param programmes The list of programmes. + * @param history The history object to record the command execution. + * @return A CommandResult object containing the result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + if (programmeIndex == NULL_INTEGER){ + programmeIndex = programmes.getCurrentActiveProgramme(); + } + assert programmes != null : "Programme list must not be null"; + + Programme started = programmes.startProgramme(programmeIndex); + assert started != null : "Programme must not be null"; + + logger.log(Level.INFO, "StartCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, started); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/ViewProgrammeCommand.java b/src/main/java/command/programme/ViewProgrammeCommand.java new file mode 100644 index 0000000000..f49b0b2aa8 --- /dev/null +++ b/src/main/java/command/programme/ViewProgrammeCommand.java @@ -0,0 +1,47 @@ +package command.programme; + +import command.CommandResult; + +import programme.ProgrammeList; +import programme.Programme; +import history.History; + +import java.util.logging.Level; + +/** + * Represents a command to view a specific programme. + */ +public class ViewProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "view"; + public static final String SUCCESS_MESSAGE_FORMAT = "Viewing programme: %n%s"; + + /** + * Constructs a ViewProgrammeCommand with the specified programme index. + * + * @param programmeIndex The index of the programme to view. + */ + public ViewProgrammeCommand(int programmeIndex) { + super(programmeIndex); + logger.log(Level.INFO, "ViewCommand created with programme index: {0}", programmeIndex); + } + + /** + * Executes the command to view a specific programme. + * + * @param programmes The list of programmes. + * @param history The history object to record the command execution. + * @return A CommandResult object containing the result of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history){ + assert programmes != null : "ProgrammeList must not be null"; + + Programme programme = programmes.getProgramme(programmeIndex); + assert programme != null : "Programme must not be null"; + + logger.log(Level.INFO, "ViewCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, programme); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/edit/CreateDayProgrammeCommand.java b/src/main/java/command/programme/edit/CreateDayProgrammeCommand.java new file mode 100644 index 0000000000..279c16b25d --- /dev/null +++ b/src/main/java/command/programme/edit/CreateDayProgrammeCommand.java @@ -0,0 +1,49 @@ +// @@author TVageesan +package command.programme.edit; + +import command.CommandResult; +import programme.Day; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.logging.Level; + +/** + * Command to create a new day in a programme. + * This command adds a new Day object to an existing programme identified by the programme ID. + */ +public class CreateDayProgrammeCommand extends EditProgrammeCommand { + public static final String SUCCESS_MESSAGE_FORMAT = "Created new day: %s%n"; + private final Day createdDay; + + /** + * Constructs a new CreateDayCommand. + * @param programmeIndex The ID of the programme to add the day to + * @param createdDay The Day object to be added to the programme + */ + public CreateDayProgrammeCommand(int programmeIndex, Day createdDay) { + super(programmeIndex); + assert createdDay != null : "created day cannot be null"; + this.createdDay = createdDay; + logger.log(Level.INFO, "CreateDayCommand created with day: {0}", createdDay); + } + + /** + * Executes the command to insert the created day into the specified programme. + * @author TVageesan + * @param programmes the ProgrammeList that contains the programmes where the day will be added + * @return a CommandResult containing a success message indicating the created day + */ + @Override + public CommandResult execute(ProgrammeList programmes) { + assert programmes != null : "programmes cannot be null"; + + Programme selectedProgramme = programmes.getProgramme(programmeIndex); + selectedProgramme.insertDay(createdDay); + + logger.log(Level.INFO, "CreateDayCommand executed successfully."); + + String resultMessage = String.format(SUCCESS_MESSAGE_FORMAT, createdDay); + return new CommandResult(resultMessage); + } +} diff --git a/src/main/java/command/programme/edit/CreateExerciseProgrammeCommand.java b/src/main/java/command/programme/edit/CreateExerciseProgrammeCommand.java new file mode 100644 index 0000000000..fedbd1269d --- /dev/null +++ b/src/main/java/command/programme/edit/CreateExerciseProgrammeCommand.java @@ -0,0 +1,54 @@ +// @@author TVageesan +package command.programme.edit; + +import command.CommandResult; +import programme.Day; +import programme.Exercise; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.logging.Level; + +/** + * Command to create a new exercise within a specified day of a programme. + *

+ * This class is responsible for creating an exercise and adding it to a specific day + * within a specified programme. + *

+ */ +public class CreateExerciseProgrammeCommand extends EditProgrammeCommand { + public static final String SUCCESS_MESSAGE_FORMAT = "Created new exercise: %n%s%n"; + private final Exercise createdExercise; + + /** + * Constructs a CreateExerciseCommand with the specified programme index, day ID, and exercise. + * + * @param programmeIndex the index of the programme to which the exercise will be added + * @param dayIndex the ID of the day within the programme where the exercise will be inserted + * @param createdExercise the exercise to be created and added to the day + */ + public CreateExerciseProgrammeCommand(int programmeIndex, int dayIndex, Exercise createdExercise) { + super(programmeIndex,dayIndex); + assert createdExercise != null : "created exercise must not be null"; + this.createdExercise = createdExercise; + logger.log(Level.INFO, "CreateExerciseCommand created with exercise: {0}", createdExercise); + } + + /** + * Executes the command to insert the created exercise into the specified day of the programme. + * @param programmes the ProgrammeList that contains the programmes where the exercise will be added + * @return a CommandResult containing a success message indicating the created exercise + */ + public CommandResult execute(ProgrammeList programmes) { + assert programmes != null : "programmes cannot be null"; + + Programme selectedProgramme = programmes.getProgramme(programmeIndex); + Day selectedDay = selectedProgramme.getDay(dayIndex); + selectedDay.insertExercise(createdExercise); + + logger.log(Level.INFO, "CreateExerciseCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, createdExercise); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/edit/DeleteDayProgrammeCommand.java b/src/main/java/command/programme/edit/DeleteDayProgrammeCommand.java new file mode 100644 index 0000000000..64ef2e0ab2 --- /dev/null +++ b/src/main/java/command/programme/edit/DeleteDayProgrammeCommand.java @@ -0,0 +1,53 @@ +// @@author TVageesan +package command.programme.edit; + +import command.CommandResult; +import programme.Day; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.logging.Level; + +/** + * Command to delete a specific day from a programme. + *

+ * This class encapsulates the functionality to remove a day from a + * specified programme, identified by its index. + *

+ */ +public class DeleteDayProgrammeCommand extends EditProgrammeCommand { + public static final String SUCCESS_MESSAGE_FORMAT = "Deleted day: %n%s%n"; + + /** + * Constructs a DeleteDayCommand with the specified programme index and day ID. + * + * @param programmeIndex the index of the programme from which the day will be deleted + * @param dayIndex the ID of the day to be deleted from the programme + */ + public DeleteDayProgrammeCommand(int programmeIndex, int dayIndex) { + super(programmeIndex, dayIndex); + logger.log( + Level.INFO, + "DeleteDayCommand created with programme index: {0} and day index: {1}", + new Object[]{programmeIndex, dayIndex} + ); + } + + /** + * Executes the command to delete the specified day from the programme. + * + * @param programmes the ProgrammeList containing the programmes where the day will be deleted + * @return a CommandResult containing a success message indicating the deleted day + */ + public CommandResult execute(ProgrammeList programmes) { + assert programmes != null : "programmes cannot be null"; + + Programme selectedProgramme = programmes.getProgramme(programmeIndex); + Day deletedDay = selectedProgramme.deleteDay(dayIndex); + + logger.log(Level.INFO, "DeleteDayCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, deletedDay); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/edit/DeleteExerciseProgrammeCommand.java b/src/main/java/command/programme/edit/DeleteExerciseProgrammeCommand.java new file mode 100644 index 0000000000..346f3438f3 --- /dev/null +++ b/src/main/java/command/programme/edit/DeleteExerciseProgrammeCommand.java @@ -0,0 +1,57 @@ +// @@author TVageesan +package command.programme.edit; + +import command.CommandResult; +import programme.Day; +import programme.Exercise; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.logging.Level; + +/** + * Command to delete a specific exercise from a day within a programme. + *

+ * This class encapsulates the functionality to remove an exercise identified by its + * ID from a specified day of a programme, which is also identified by its index. + *

+ */ +public class DeleteExerciseProgrammeCommand extends EditProgrammeCommand { + public static final String SUCCESS_MESSAGE_FORMAT = "Deleted exercise %d: %n%s%n"; + + /** + * Constructs a DeleteExerciseCommand with the specified programme index, day ID, and exercise ID. + * + * @param programmeIndex the index of the programme from which the exercise will be deleted + * @param dayIndex the ID of the day from which the exercise will be deleted + * @param exerciseIndex the ID of the exercise to be deleted + */ + public DeleteExerciseProgrammeCommand(int programmeIndex, int dayIndex, int exerciseIndex) { + super(programmeIndex, dayIndex, exerciseIndex); + logger.log( + Level.INFO, + "DeleteExerciseCommand created with programme index: {0}, day index: {1}, and exercise index: {2}", + new Object[]{programmeIndex, dayIndex, exerciseIndex} + ); + } + + /** + * Executes the command to delete the specified exercise from the given day of the programme. + * + * @param programmes the ProgrammeList containing the programmes where the exercise will be deleted + * @return a CommandResult containing a success message indicating the deleted exercise + */ + @Override + public CommandResult execute(ProgrammeList programmes) { + assert programmes != null : "programmes cannot be null"; + + Programme selectedProgramme = programmes.getProgramme(programmeIndex); + Day selectedDay = selectedProgramme.getDay(dayIndex); + Exercise deletedExercise = selectedDay.deleteExercise(exerciseIndex); + + logger.log(Level.INFO, "DeleteExerciseCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, exerciseIndex + 1, deletedExercise); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/edit/EditExerciseProgrammeCommand.java b/src/main/java/command/programme/edit/EditExerciseProgrammeCommand.java new file mode 100644 index 0000000000..a41f8fbacc --- /dev/null +++ b/src/main/java/command/programme/edit/EditExerciseProgrammeCommand.java @@ -0,0 +1,68 @@ +// @@author TVageesan +package command.programme.edit; + +import command.CommandResult; + +import programme.Day; +import programme.Exercise; +import programme.ExerciseUpdate; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.logging.Level; + +/** + * Command to edit a specific exercise's fields within a day of a programme. + *

+ * This class encapsulates the functionality to update the details of an + * existing exercise identified by its ID within a specified day of a programme. + *

+ */ +public class EditExerciseProgrammeCommand extends EditProgrammeCommand { + + public static final String SUCCESS_MESSAGE_FORMAT = "Updated exercise: %s%n"; + + private final ExerciseUpdate update; + + /** + * Constructs an EditExerciseCommand with the specified programme index, day ID, + * exercise ID, and updated exercise details. + * + * @param programmeIndex the index of the programme containing the exercise to be updated + * @param dayIndex the ID of the day containing the exercise to be updated + * @param exerciseIndex the ID of the exercise to be updated + * @param update the ExerciseUpdate object containing the fields that need to be updated in the target Exercise + */ + public EditExerciseProgrammeCommand(int programmeIndex, int dayIndex, int exerciseIndex, ExerciseUpdate update) { + super(programmeIndex, dayIndex, exerciseIndex); + assert update != null : "update object must not be null"; + this.update = update; + logger.log( + Level.INFO, + "EditExerciseCommand created with programme index: {0}, day index: {1}, and exercise index: {2}", + new Object[]{programmeIndex, dayIndex, exerciseIndex} + ); + } + + /** + * Executes the command to update the specified exercise in the given day of the programme. + * + * @param programmes the ProgrammeList containing the programmes where the exercise will be updated + * @return a CommandResult containing a success message indicating the updated exercise + */ + @Override + public CommandResult execute(ProgrammeList programmes) { + assert programmes != null : "programmes cannot be null"; + + Programme selectedProgramme = programmes.getProgramme(programmeIndex); + Day selectedDay = selectedProgramme.getDay(dayIndex); + Exercise selectedExercise = selectedDay.getExercise(exerciseIndex); + + selectedExercise.updateExercise(update); + + logger.log(Level.INFO, "EditExerciseCommand executed successfully."); + + String result = String.format(SUCCESS_MESSAGE_FORMAT, selectedExercise); + return new CommandResult(result); + } +} diff --git a/src/main/java/command/programme/edit/EditProgrammeCommand.java b/src/main/java/command/programme/edit/EditProgrammeCommand.java new file mode 100644 index 0000000000..3bb00b7476 --- /dev/null +++ b/src/main/java/command/programme/edit/EditProgrammeCommand.java @@ -0,0 +1,73 @@ +// @@author TVageesan +package command.programme.edit; + +import command.CommandResult; +import command.programme.ProgrammeCommand; +import history.History; +import programme.ProgrammeList; + +import java.util.logging.Logger; + +/** + * Abstract command class for all editing operations on a programme. + *

+ * This class serves as a base for all editing commands that operate on + * a programme, including operations to modify exercises or days within + * a programme. + *

+ */ +public abstract class EditProgrammeCommand extends ProgrammeCommand { + public static final String COMMAND_WORD = "edit"; + protected final Logger logger = Logger.getLogger(this.getClass().getName()); + + protected int exerciseIndex; + + /** + * Constructs an EditCommand with the specified programme index, day ID, and exercise ID. + * + * @param programmeIndex the index of the programme being edited + * @param dayIndex the ID of the day being edited within the programme + * @param exerciseIndex the ID of the exercise being edited + */ + public EditProgrammeCommand(int programmeIndex, int dayIndex, int exerciseIndex) { + super(programmeIndex, dayIndex); + assert exerciseIndex >= 0 : "exercise id must be non-negative"; + this.exerciseIndex = exerciseIndex; + } + + /** + * Constructs an EditCommand with the specified programme index and day ID. + * + * @param programmeIndex the index of the programme being edited + * @param dayIndex the ID of the day being edited within the programme + */ + public EditProgrammeCommand(int programmeIndex, int dayIndex) { + super(programmeIndex, dayIndex); + } + + /** + * Constructs an EditCommand with the specified programme index. + * + * @param programmeIndex the index of the programme being edited + */ + public EditProgrammeCommand(int programmeIndex) { + super(programmeIndex); + } + + /** + * Executes the edit command on the given ProgrammeList. + *

+ * This method must be implemented by subclasses to define specific + * editing behavior. + *

+ * + * @param programmes the ProgrammeList containing the programmes to edit + * @return a CommandResult indicating the outcome of the edit operation + */ + public abstract CommandResult execute(ProgrammeList programmes); + + @Override + public CommandResult execute(ProgrammeList programmes, History history) { + return execute(programmes); + } +} diff --git a/src/main/java/command/water/AddWaterCommand.java b/src/main/java/command/water/AddWaterCommand.java new file mode 100644 index 0000000000..d62acf396a --- /dev/null +++ b/src/main/java/command/water/AddWaterCommand.java @@ -0,0 +1,72 @@ +//@@author Bev-low +package command.water; + +import command.CommandResult; +import history.DailyRecord; +import history.History; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to add a specified amount of water to the daily record for a given date. + */ +public class AddWaterCommand extends WaterCommand { + public static final String COMMAND_WORD = "add"; + private static final Logger logger = Logger.getLogger(AddWaterCommand.class.getName()); + + protected float waterToAdd; + + /** + * Constructs an AddWaterCommand with the specified amount of water to add and the date. + * + * @param waterToAdd The amount of water to add in liters. Must be positive. + * @param date The date for which the water is to be added. Must not be null. + * @throws AssertionError if waterToAdd is not positive or date is null. + */ + public AddWaterCommand(float waterToAdd, LocalDate date) { + super(date); + + assert waterToAdd > 0 : "Water to add must be positive"; + + this.waterToAdd = waterToAdd; + logger.log(Level.INFO, "AddWaterCommand created to add {0} liters for date: {1}", + new Object[]{waterToAdd, date}); + } + + /** + * Executes the command to add water to the daily record in the specified history. + * + * @param history The {@code History} object that contains daily records. + * @return A {@code CommandResult} containing a message indicating the success of the operation. + * @throws AssertionError if the daily record for the specified date is not found. + */ + public CommandResult execute(History history) { + DailyRecord dailyRecord = history.getRecordByDate(date); + assert dailyRecord != null : "Daily record not found"; + dailyRecord.addWaterToRecord(waterToAdd); + logger.log(Level.INFO, "{0} liters of water added for date: {1}", + new Object[]{waterToAdd, date}); + + return new CommandResult(waterToAdd + " liters of water has been added"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AddWaterCommand that)) { + return false; + } + return Objects.equals(waterToAdd, that.waterToAdd) && + Objects.equals(date, that.date); + } + + @Override + public int hashCode() { + return Objects.hash(waterToAdd, date); + } +} diff --git a/src/main/java/command/water/DeleteWaterCommand.java b/src/main/java/command/water/DeleteWaterCommand.java new file mode 100644 index 0000000000..46dc27efd4 --- /dev/null +++ b/src/main/java/command/water/DeleteWaterCommand.java @@ -0,0 +1,71 @@ +//@@author Bev-low +package command.water; + +import command.CommandResult; +import history.DailyRecord; +import history.History; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to delete a specified entry of water intake from the daily record for a given date. + */ +public class DeleteWaterCommand extends WaterCommand { + public static final String COMMAND_WORD = "delete"; + private static final Logger logger = Logger.getLogger(DeleteWaterCommand.class.getName()); + + protected int indexWaterToDelete; + + /** + * Constructs a {@code DeleteWaterCommand} with the specified index of the water entry to delete and the date. + * + * @param indexOfWaterToDelete The index of the water entry to delete. Must be zero or greater. + * @param date The date for which the water entry is to be deleted. Must not be {@code null}. + * @throws AssertionError if {@code indexOfWaterToDelete} is negative or {@code date} is {@code null}. + */ + public DeleteWaterCommand(int indexOfWaterToDelete, LocalDate date) { + super(date); + + assert indexOfWaterToDelete >= 0 : "Index to delete cannot be negative"; + + this.indexWaterToDelete = indexOfWaterToDelete; + logger.log(Level.INFO, "DeleteWaterCommand created for index: {0} on date: {1}", + new Object[]{indexWaterToDelete, date}); + } + + /** + * Executes the command to delete a water entry from the daily record in the specified history. + * + * @param history The {@code History} object that contains daily records. + * @return A {@code CommandResult} containing a message indicating the success of the deletion. + * @throws AssertionError if the daily record for the specified date is not found. + */ + public CommandResult execute(History history) { + DailyRecord dailyRecord = history.getRecordByDate(date); + assert dailyRecord != null : "Daily record not found"; + float deletedWater = dailyRecord.removeWaterFromRecord(indexWaterToDelete); + + return new CommandResult(deletedWater + " liters of water has been deleted"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DeleteWaterCommand)) { + return false; + } + DeleteWaterCommand that = (DeleteWaterCommand) o; + return indexWaterToDelete == that.indexWaterToDelete && + Objects.equals(date, that.date); + } + + @Override + public int hashCode() { + return Objects.hash(indexWaterToDelete, date); + } +} diff --git a/src/main/java/command/water/ViewWaterCommand.java b/src/main/java/command/water/ViewWaterCommand.java new file mode 100644 index 0000000000..6081768040 --- /dev/null +++ b/src/main/java/command/water/ViewWaterCommand.java @@ -0,0 +1,67 @@ +//@@author Bev-low +package command.water; + +import command.CommandResult; +import common.Utils; +import history.DailyRecord; +import history.History; +import water.Water; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a command to view the water intake for a specific date. + */ +public class ViewWaterCommand extends WaterCommand { + public static final String COMMAND_WORD = "view"; + private static final Logger logger = Logger.getLogger(ViewWaterCommand.class.getName()); + + /** + * Constructs a ViewWaterCommand for the specified date. + * + * @param date The date for which the water intake is to be viewed. Must not be null. + * @throws AssertionError if date is null. + */ + public ViewWaterCommand(LocalDate date) { + super(date); + + logger.log(Level.INFO, "ViewWaterCommand created for date: {0}", date); + } + + /** + * Executes the command to view the water intake for the specified date in the provided history. + * + * @param history The {@code History} object that contains daily records. + * @return A {@code CommandResult} containing a message displaying the water intake for the date. + * @throws AssertionError if the daily record for the specified date is not found. + */ + public CommandResult execute(History history) { + String formattedDate = Utils.formatDate(date); + + DailyRecord dailyRecord = history.getRecordByDate(date); + assert dailyRecord != null : "Daily record not found"; + Water water = dailyRecord.getWaterFromRecord(); + + logger.log(Level.INFO, "Retrieved Water record for date: {0}, Water: {1}", new Object[]{date, water}); + return new CommandResult("Water intake for " + formattedDate + ": \n\n" + water.toString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ViewWaterCommand that)) { + return false; + } + return Objects.equals(date, that.date); + } + + @Override + public int hashCode() { + return Objects.hash(date); + } +} diff --git a/src/main/java/command/water/WaterCommand.java b/src/main/java/command/water/WaterCommand.java new file mode 100644 index 0000000000..aedfd33dc1 --- /dev/null +++ b/src/main/java/command/water/WaterCommand.java @@ -0,0 +1,57 @@ +//@@author Bev-low +package command.water; + +import command.Command; +import command.CommandResult; +import history.History; +import programme.ProgrammeList; +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents an abstract command related to water tracking. + * This class serves as a base for specific water-related commands that operate on a given date. + */ +public abstract class WaterCommand extends Command { + + private static final Logger logger = Logger.getLogger(WaterCommand.class.getName()); + + protected LocalDate date; + + /** + * Constructs a water command with the specified date. + * + * @param date The date associated with the command. Must not be {@code null}. + * @throws AssertionError if {@code date} is {@code null}. + */ + public WaterCommand(LocalDate date) { + assert date != null : "Date cannot be null"; + + this.date = date; + + logger.log(Level.INFO, "WaterCommand initialized with date: {0}", date); + } + + /** + * Executes the water-related command using the specified history. + * This method is intended to be implemented by subclasses to provide specific command behavior. + * + * @param history The {@code History} object containing records to operate on. + * @return A {@code CommandResult} with the outcome of the command execution. + */ + public abstract CommandResult execute(History history); + + /** + * Executes the water-related command using both a {@code ProgrammeList} and {@code History}. + * + * @param programmes The {@code ProgrammeList} object, representing a list of programmes. + * @param history The {@code History} object containing records to operate on. + * @return A {@code CommandResult} with the outcome of the command execution. + */ + @Override + public CommandResult execute(ProgrammeList programmes, History history) { + logger.log(Level.INFO, "Executing WaterCommand with ProgrammeList and History."); + return execute(history); + } +} diff --git a/src/main/java/common/Utils.java b/src/main/java/common/Utils.java new file mode 100644 index 0000000000..01b583930c --- /dev/null +++ b/src/main/java/common/Utils.java @@ -0,0 +1,56 @@ +// @@author andreusxcarvalho + +package common; + +import exceptions.ParserException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class Utils { + + public static final int NULL_INTEGER = -1; + + public static final float NULL_FLOAT = -1.0f; + + public static final String DATE_FORMAT = "dd-MM-yyyy"; + + public static boolean isNull(int val) { + return val == -1; + } + + public static boolean isNull(String val) { + return val == null || val.isEmpty(); + } + + public static String formatDate(LocalDate date){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + return date.format(formatter); + } + + public static void validate(int integer) { + if (integer <= 0){ + throw ParserException.invalidInt(integer); + } + } + + public static void validate(float number) { + if (number == Double.POSITIVE_INFINITY) { + throw ParserException.infinityFloat(number); + } else if (number < 0){ + throw ParserException.invalidFloat(number); + } + } + + public static void validate(String string) { + if (string == null || string.trim().isEmpty()) { + throw ParserException.invalidString(string); + } + } + + public static void validate(LocalDate localDate) { + if(localDate == null){ + throw ParserException.invalidDate(); + } + } +} diff --git a/src/main/java/exceptions/BuffBuddyException.java b/src/main/java/exceptions/BuffBuddyException.java new file mode 100644 index 0000000000..36def0ed3f --- /dev/null +++ b/src/main/java/exceptions/BuffBuddyException.java @@ -0,0 +1,40 @@ +// @@author andreusxcarvalho + +package exceptions; + +/** + * BuffBuddyException serves as the base exception class for BuffBuddy. + * All custom exceptions in BuffBuddy should inherit from this class to + * allow unified handling of exceptions. + */ +public class BuffBuddyException extends RuntimeException { + + /** + * Default constructor with a generic error message. + */ + public BuffBuddyException() { + super("An error occurred in BuffBuddy."); + } + + /** + * Constructor that accepts a custom error message. + * @param message Custom error message describing the exception. + */ + public BuffBuddyException(String message) { + super(message); + } + + + + /** + * Constructor that accepts a custom error message and a throwable cause. + * This is useful for wrapping other exceptions in BuffBuddy-specific exceptions. + * @param message Custom error message describing the exception. + * @param cause The cause of the exception. + */ + public BuffBuddyException(String message, Throwable cause) { + super(message, cause); + } +} + + diff --git a/src/main/java/exceptions/FlagException.java b/src/main/java/exceptions/FlagException.java new file mode 100644 index 0000000000..47de25c00e --- /dev/null +++ b/src/main/java/exceptions/FlagException.java @@ -0,0 +1,77 @@ +//@@author Bev-low + +package exceptions; + +/** + * Represents exceptions related to flags in the application. + */ +public class FlagException extends BuffBuddyException { + + /** + * Constructs a new FlagExceptions with the specified detail message. + * + * @param message The detail message for this exception. + */ + public FlagException(String message) { + super(message); + } + + /** + * Returns a FlagExceptions indicating that a specific flag is missing. + * + * @param flag The name of the missing flag. + * @return A {@code FlagExceptions} with a message indicating that the specified flag is not present. + */ + public static FlagException missingFlag(String flag) { + return new FlagException("Flag " + flag + " not present"); + } + + /** + * Returns a FlagException indicating that arguments are missing + * + * @return A {@code FlagException} with a message indicating that arguments are missing + * after the flag. + */ + public static FlagException missingArguments() { + return new FlagException("Missing arguments."); + } + + /** + * Returns a FlagException indicating that arguments are missing after a flag. + * + * @return A {@code FlagException} with a message indicating that arguments are missing + * after the flag. + */ + public static FlagException missingRequiredArguments(String flag) { + return new FlagException("Missing required arguments after flag: " + flag + "."); + } + + /** + * Returns a FlagException indicating that a specific flag has been provided more than once + * @param flag The name of the duplicate flag. + * @return A {@code FlagException} with a message indicating that the specified flag has been duplicated. + */ + + public static FlagException duplicateFlag(String flag) { + return new FlagException("Flag " + flag + " was provided more than once."); + } + + /** + * Returns a FlagException indicating that a specific flag has been provided more than once + * @param flag The name of the duplicate flag. + * @return A {@code FlagException} with a message indicating that the specified flag has been duplicated. + */ + + public static FlagException invalidFlag(String flag) { + return new FlagException("Flag " + flag + " is not recognized."); + } + + /** + * Returns a FlagException indicating that more than one unique flag was provided + * @param flags The list of clashing unique flags. + * @return A {@code FlagException} with a message indicating that more than one unique flag was provided. + */ + public static FlagException nonUniqueFlag(String flags) { + return new FlagException("Flags " + flags + "cannot be provided in the same command."); + } +} diff --git a/src/main/java/exceptions/HistoryException.java b/src/main/java/exceptions/HistoryException.java new file mode 100644 index 0000000000..7a43d62538 --- /dev/null +++ b/src/main/java/exceptions/HistoryException.java @@ -0,0 +1,47 @@ +//@@author Bev-low + +package exceptions; + +import programme.Day; + +/** + * Represents exceptions related to history operations in the application. + */ +public class HistoryException extends BuffBuddyException { + + /** + * Constructs a new HistoryExceptions with the specified detail message. + * + * @param message The detail message for this exception. + */ + public HistoryException(String message) { + super(message); + } + + /** + * Returns a HistoryExceptions indicating that a specified day does not exist. + * + * @return A {@code HistoryExceptions} with a message indicating that the specified day + * cannot be deleted because it does not exist. + */ + public static HistoryException dayNotFound() { + return new HistoryException("Day does not exist, cannot be deleted"); + } + + /** + * Returns a HistoryExceptions indicating that an exercise name was not provided. + * + * @return A {@code HistoryExceptions} with a message indicating that the exercise name + * is missing and prompting the user to specify it. + */ + public static HistoryException exerciseNameNotFound() { + return new HistoryException("Exercise name not provided. Please specify the exercise to " + + "view your personal best."); + } + + public static HistoryException existingDay(Day existingDay) { + return new HistoryException("A record already exists for this date. Please delete the current day entry if " + + "you wish to make any changes."); + } +} + diff --git a/src/main/java/exceptions/MealException.java b/src/main/java/exceptions/MealException.java new file mode 100644 index 0000000000..0a6b3e0774 --- /dev/null +++ b/src/main/java/exceptions/MealException.java @@ -0,0 +1,38 @@ +//@@author Bev-low + +package exceptions; + +/** + * Represents exceptions related to meal operations in the application. + */ +public class MealException extends BuffBuddyException { + + /** + * Constructs a new MealException with the specified detail message. + * + * @param message The detail message for this exception. + */ + public MealException(String message) { + super(message); + } + + /** + * Returns a MealException indicating that a specified meal does not exist. + * + * @return A {@code MealException} with a message indicating that the specified meal + * does not exist. + */ + public static MealException doesNotExist() { + return new MealException("Meal does not exist"); + } + + /** + * Returns a MealException indicating that the number of calories are less than 0. + * + * @return A {@code MealException} with a message indicating that the number of calories + * is less than 0. + */ + public static MealException caloriesOutOfBounds() { + return new MealException("Calories cannot be negative"); + } +} diff --git a/src/main/java/exceptions/ParserException.java b/src/main/java/exceptions/ParserException.java new file mode 100644 index 0000000000..0d05992340 --- /dev/null +++ b/src/main/java/exceptions/ParserException.java @@ -0,0 +1,87 @@ +//@@author Bev-low + +package exceptions; + +/** + * Represents exceptions related to parsing operations in the application. + */ +public class ParserException extends BuffBuddyException { + + /** + * Constructs a new ParserExceptions with the specified detail message. + * + * @param message The detail message for this exception. + */ + public ParserException(String message) { + super(message); + } + + /** + * Returns a ParserExceptions indicating that an invalid float value was provided. + * + * @param no The invalid float value. + * @return A {@code ParserExceptions} with a message indicating that the provided float is invalid. + */ + public static ParserException invalidFloat(float no) { + return new ParserException("Number " + no + " is not a valid, it should be more than 0"); + } + + /** + * Returns a ParserExceptions indicating that an invalid integer value was provided. + * + * @param no The invalid integer value. + * @return A {@code ParserExceptions} with a message indicating that the provided integer is invalid. + */ + public static ParserException invalidInt(int no) { + return new ParserException("Number " + no + " is not a valid, it should be more than 0"); + } + + /** + * Returns a ParserExceptions indicating that an invalid date format was provided. + * + * @param date The invalid date. + * @return A {@code ParserExceptions} with a message indicating that the provided date is invalid. + */ + public static ParserException invalidDate(String date) { + return new ParserException("Invalid Date: " + date + ", Provide Date in format dd-MM-yyyy."); + } + + /** + * Returns a ParserExceptions indicating that an invalid date was previously recorded. + * + * @return A {@code ParserExceptions} with a message indicating that the provided date is invalid. + */ + public static ParserException invalidDate() { + return new ParserException("Invalid Date recorded"); + } + + /** + * Returns a ParserExceptions indicating that a command is missing. + * + * @return A {@code ParserExceptions} with a message indicating that a command is missing. + */ + public static ParserException missingCommand() { + return new ParserException("Missing Command, please refer to User Guide"); + } + + /** + * Returns a ParserExceptions indicating that arguments are missing after a command. + * + * @return A {@code ParserExceptions} with a message indicating that arguments are missing. + */ + public static ParserException missingArguments() { + return new ParserException("Missing Arguments, please refer to User Guide"); + } + + public static ParserException infinityFloat(float number) { + return new ParserException("Number " + number + " is too large, please key in a smaller number."); + } + + public static ParserException infinityInt(String trimmedIntString) { + return new ParserException("Number too large, please key in a smaller number."); + } + + public static ParserException invalidString(String string) { + return new ParserException("String: " + string + " is invalid"); + } +} diff --git a/src/main/java/exceptions/ProgrammeException.java b/src/main/java/exceptions/ProgrammeException.java new file mode 100644 index 0000000000..097dc56bce --- /dev/null +++ b/src/main/java/exceptions/ProgrammeException.java @@ -0,0 +1,70 @@ +//@@author Bev-low + +package exceptions; + +/** + * Represents exceptions related to programme operations in the application. + */ +public class ProgrammeException extends BuffBuddyException { + + /** + * Constructs a new ProgrammeExceptions with the specified detail message. + * + * @param message The detail message for this exception. + */ + public ProgrammeException(String message) { + super(message); + } + + /** + * Returns a ProgrammeExceptions indicating that a programme name is missing. + * + * @return A {@code ProgrammeExceptions} with a message indicating a missing programme name. + */ + public static ProgrammeException programmeMissingName() { + return new ProgrammeException("Programme is missing a name."); + } + + /** + * Returns a ProgrammeExceptions indicating that a programme list is empty. + * + * @return A {@code ProgrammeExceptions} with a message indicating an empty programme list + */ + public static ProgrammeException programmeListEmpty() { + return new ProgrammeException("Programme list is empty"); + } + + /** + * Returns a ProgrammeExceptions indicating that tha programme has already been set to active. + * + * @return A {@code ProgrammeExceptions} with a message indicating the programme has already started. + */ + public static ProgrammeException programmeAlreadyActive(int index) { + return new ProgrammeException("Program " + (index + 1) + " has already been started"); + } + + /** + * Returns a ProgrammeExceptions indicating that a day name is missing. + * + * @return A {@code ProgrammeExceptions} with a message indicating a missing day name. + */ + public static ProgrammeException missingDayName() { + return new ProgrammeException("Missing Day Name, please provide one."); + } + + /** + * Returns a {@code ProgrammeExceptions} indicating that a specified object + * does not exist. + * + * @param name The name of the object that does not exist. + * @return A {@code ProgrammeExceptions} with a message indicating that the specified + * object does not exist. + */ + public static ProgrammeException doesNotExist(String name) { + return new ProgrammeException(name + " does not exist."); + } + + public static ProgrammeException indexOutOfBounds() { + return new ProgrammeException("Index should be more than 0"); + } +} diff --git a/src/main/java/exceptions/StorageException.java b/src/main/java/exceptions/StorageException.java new file mode 100644 index 0000000000..14dcb15279 --- /dev/null +++ b/src/main/java/exceptions/StorageException.java @@ -0,0 +1,54 @@ +//@@author Bev-low + +package exceptions; + +import java.io.IOException; + +/** + * Represents exceptions related to storage operations in the application. + */ +public class StorageException extends IOException { + + /** + * Constructs a new {@code StorageExceptions} with the specified detail message. + * + * @param message The detail message for this exception. + */ + public StorageException(String message) { + super(message); + } + + /** + * Returns a StorageExceptions indicating that the storage file could not be saved. + * + * @return A {@code StorageExceptions} with a message indicating that the storage file + * could not be saved. + */ + public static StorageException unableToSave() { + return new StorageException("Could not save storage file"); + } + + /** + * Returns a StorageExceptions}indicating that a directory could not be created. + * + * @return A {@code StorageExceptions} with a message indicating that the directory + * could not be created. + */ + public static StorageException unableToCreateDirectory() { + return new StorageException("Could not create directory"); + } + + /** + * Returns a StorageExceptions indicating that a file could not be created. + * + * @return A {@code StorageExceptions} with a message indicating that the file + * could not be created. + */ + public static StorageException unableToCreateFile() { + return new StorageException("Could not create file"); + } + + public static StorageException corruptedFile(String type) { + return new StorageException("Corrupted file, initialising new " + type); + } +} diff --git a/src/main/java/exceptions/WaterException.java b/src/main/java/exceptions/WaterException.java new file mode 100644 index 0000000000..22731554b5 --- /dev/null +++ b/src/main/java/exceptions/WaterException.java @@ -0,0 +1,38 @@ +//@@author Bev-low + +package exceptions; + +/** + * Represents exceptions related to water log operations in the application. + */ +public class WaterException extends BuffBuddyException { + + /** + * Constructs a new {@code WaterExceptions} with the specified detail message. + * + * @param message The detail message for this exception. + */ + public WaterException(String message) { + super(message); + } + + /** + * Returns a WaterExceptions indicating that a specified water log does not exist. + * + * @return A {@code WaterExceptions} with a message indicating that the specified water log + * does not exist. + */ + public static WaterException doesNotExist() { + return new WaterException("Water log does not exist"); + } + + /** + * Returns a WaterExceptions indicating that the water volume is less than 0. + * + * @return A {@code WaterExceptions} with a message indicating that the water volume \ + * is less than 0. + */ + public static WaterException volumeOutOfBounds() { + return new WaterException("Water amount cannot be negative"); + } +} diff --git a/src/main/java/history/DailyRecord.java b/src/main/java/history/DailyRecord.java new file mode 100644 index 0000000000..4712659a78 --- /dev/null +++ b/src/main/java/history/DailyRecord.java @@ -0,0 +1,255 @@ +//@@author Bev-low +package history; + +import exceptions.HistoryException; +import exceptions.MealException; +import meal.Meal; +import meal.MealList; +import programme.Day; +import water.Water; + +import java.util.logging.Logger; + +/** + * Represents a daily record containing a {@link Day} for workouts, a {@link MealList} for meals, + * and a {@link Water} record for water intake. + *

+ * Each {@code DailyRecord} logs a day's activities, meals, and water intake, supporting methods + * for managing these entries and calculating totals. Instances are stored in a {@code history} + * map that associates each record with a specific date. + *

+ */ +public class DailyRecord { + private static final Logger logger = Logger.getLogger(DailyRecord.class.getName()); + private Day day; + private final MealList mealList; + private final Water water; + + /** + * Constructs a daily record with an empty meal list and water intake record. + */ + public DailyRecord() { + this.mealList = new MealList(); + this.water = new Water(); + } + + /** + * Retrieves the day object representing the day's workout or activities. + * + * @return the {@code Day} from the record, or {@code null} if no day is recorded + */ + public Day getDayFromRecord() { + return day; + } + + /** + * Deletes the current day record from the daily record. + *

+ * If no day has been logged, this method throws an {@link IllegalStateException}. + *

+ * + * @return the deleted {@code Day} object + * @throws IllegalStateException if there is no logged workout for the day + */ + public Day deleteDayFromRecord() { + if (this.day == null) { + throw HistoryException.dayNotFound(); + } + + Day deleted = this.day; + this.day = null; + return deleted; + } + + /** + * Retrieves the mealList object containing all meals recorded for the day. + * + * @return the {@code MealList} for the daily record + */ + public MealList getMealListFromRecord() { + return mealList; + } + + /** + * Retrieves the water object containing a water list. + * + * @return the {@code Water} intake record for the day + */ + public Water getWaterFromRecord() { + return water; + } + + /** + * Logs a new {@code Day} to the daily record, replacing any existing record for that day. + *

+ * This method updates the day's activities with the given {@code Day} object, + * ensuring it is non-null, and logs the update. + *

+ * + * @param newDay the {@code Day} object to log, which must not be {@code null} + */ + public void logDayToRecord(Day newDay) { //this replaces any current day recorded + assert newDay != null : "day must not be null"; + + this.day = new Day(newDay); + logger.info("Day updated: " + day); + } + + /** + * Adds a meal to the daily meal record. + *

+ * This method appends the given meal to the {@code mealList} and logs the addition. + *

+ * + * @param meal the {@code Meal} to add to the record, which must not be {@code null} + */ + public void addMealToRecord(Meal meal) { + assert meal != null; + + mealList.addMeal(meal); + logger.info("meal added: " + meal); + } + + /** + * Deletes a meal from the mealList at the specified index. + *

+ * This method removes the meal at the given index from the {@code mealList}. + *

+ * + * @param index the index of the meal to delete, which must be non-negative + * @return the {@link Meal} that was deleted from the record + * @throws IndexOutOfBoundsException if the index is out of range + */ + public Meal deleteMealFromRecord(int index) { + if (index < 0 || index >= mealList.getSize()) { + throw MealException.doesNotExist(); + } + logger.info("meal deleted, index: " + index); + return mealList.deleteMeal(index); + } + + /** + * Adds a specified amount of water to the daily water intake record. + *

+ * This method appends the given amount of water to the water record, ensuring the + * amount is non-negative. + *

+ * + * @param toAddWater the amount of water to add, which must be non-negative + */ + public void addWaterToRecord(float toAddWater) { + assert toAddWater >= 0; + + water.addWater(toAddWater); + logger.info("Water added: " + toAddWater); + } + + /** + * Removes a water entry from the water intake record at the specified index. + *

+ * This method deletes the water entry at the given index from the water intake record + * and returns the removed water amount. + *

+ * + * @param index the index of the water entry to delete, which must be non-negative + * @return the amount of water that was removed + * @throws IndexOutOfBoundsException if the index is out of range + */ + public float removeWaterFromRecord(int index) { + logger.info("water deleted, index: " + index); + return water.deleteWater(index); + } + + /** + * Calculates the total calories from all meals in the {@code mealList}. + *

+ * This method iterates over each {@link Meal} in the {@code mealList}, ensuring each meal is + * non-null, and sums up the calories from each meal to get the total caloric intake from meals. + *

+ * + * @return the total calories gained from meals + */ + private int getCaloriesFromMeals() { + int caloriesMeal = 0; + for (Meal meal : mealList.getMeals()) { + assert meal != null : "meal must not be null"; + caloriesMeal += meal.getCalories(); + } + logger.info("Calories from meals calculated: " + caloriesMeal); + return caloriesMeal; + } + + /** + * Calculates the total water intake from all recorded water entries in {@code water}. + *

+ * This method iterates over each water entry in {@code water.getWaterList()}, ensuring each entry + * is non-null, and accumulates the total amount of water consumed. + *

+ * + * @return the total water intake in liters + */ + private float getTotalWaterIntake() { + float totalWater = 0; + for (Float waterAmount : water.getWaterList()) { + assert waterAmount != null : "water must not be null"; + totalWater += waterAmount; + } + logger.info("total water: " + totalWater); + return totalWater; + } + + /** + * Provides a detailed string representation of the daily record, including the day's + * activities, meals consumed, water intake, and caloric balance. + *

+ * This method compiles information from the {@code day}, {@code mealList}, and {@code water} + * components to create a summary of the daily record. It includes: + *

+ *
    + *
  • The day’s activities and total calories burned, if available.
  • + *
  • A list of meals and the total calories gained from meals, if any meals are recorded.
  • + *
  • The total water intake, if any water entries are recorded.
  • + *
+ *

+ * If any of the components (day, meals, or water) are absent, it indicates this in the output. + * Additionally, it calculates and displays the caloric balance (calories gained minus calories burned). + *

+ * + * @return a formatted string representation of the daily record, detailing calories burned, calories + * gained from meals, water intake, and overall caloric balance. + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + int caloriesBurnt = 0; + int caloriesGained = getCaloriesFromMeals(); + + result.append("Day: \n"); + if (day != null) { + caloriesBurnt = day.getTotalCaloriesBurnt(); + result.append(day.toString()).append("\n"); + result.append("Total Calories burnt: ").append(caloriesBurnt).append(" kcal\n\n"); + } else { + result.append("No Day.\n\n"); + } + + result.append("Meals: \n"); + if (!mealList.getMeals().isEmpty()) { + result.append(mealList).append("\n"); + result.append("Total Calories from Meals: ").append(caloriesGained).append(" kcal\n\n"); + } else { + result.append("No Meals.\n\n"); + } + + result.append("Water Intake: \n"); + if (!water.getWaterList().isEmpty()) { + result.append(water).append("\n"); + result.append("Total Water Intake: ").append(getTotalWaterIntake()).append(" liters \n\n"); + } else { + result.append("No Water.\n\n"); + } + + result.append("Caloric Balance: ").append(caloriesGained - caloriesBurnt).append(" kcal"); + return result.toString(); + } +} diff --git a/src/main/java/history/History.java b/src/main/java/history/History.java new file mode 100644 index 0000000000..0baaf934ca --- /dev/null +++ b/src/main/java/history/History.java @@ -0,0 +1,305 @@ +// @@author andreusxcarvalho +package history; + +import programme.Exercise; + +import java.util.logging.Logger; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Manages and tracks the workout history, including daily workout records, weekly summaries, + * and personal best exercises. + *

+ * This class provides methods to add, retrieve, delete, and summarize workout records, as well as + * manage personal bests for exercises. Workout records are stored in a chronological order + * using a {@link LinkedHashMap} to preserve the insertion order by date. + *

+ */ +public class History { + private static final Logger logger = Logger.getLogger(History.class.getName()); + private final LinkedHashMap history; + + /** + * Initializes a new empty workout history. + */ + public History() { + history = new LinkedHashMap<>(); + } + + //@@author Bev-Low + /** + * Retrieves the daily record for a specific date. + *

+ * Checks if a record exists in the {@code history} map for the specified date. + * If a record is found, it is returned. If no record exists, a new {@link DailyRecord} is + * created, added to the {@code history} map, and then returned. + *

+ * + * @param date the {@link LocalDate} of the record to retrieve + * @return the {@link DailyRecord} for the specified date + */ + public DailyRecord getRecordByDate(LocalDate date) { + DailyRecord record = history.get(date); + if (record == null) { + record = new DailyRecord(); + logRecord(date, record); + } + return record; + } + + /** + * checks if history is empty. + * + * @return true if it is empty + */ + public boolean isEmpty() { + return history.isEmpty(); + } + // @@author + + /** + * Returns the entire workout history. + * + * @return the {@code LinkedHashMap} containing dates and corresponding daily records + */ + public LinkedHashMap getHistory() { + return history; + } + + /** + * Generates a summary of the workout history for the past week. + * + *

This method retrieves workout data from the {@code history} map, which + * contains records of daily activities. It filters out any {@code DailyRecord} + * entries that do not contain a workout {@code Day}, so only records with + * workout data are included in the weekly summary.

+ * + * @return A formatted string summarizing the workout history for the past + * week, or a message indicating no workout history is available if + * no relevant records are found. + */ + public String getWeeklyWorkoutSummary() { + if (history.isEmpty()) { + return "No workout history available."; + } + + StringBuilder weeklySummary = new StringBuilder(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate today = LocalDate.now(); + LocalDate oneWeekAgo = today.minusDays(7); + int totalExercises = 0; + + for (Map.Entry entry : history.entrySet()) { + LocalDate date = entry.getKey(); + DailyRecord dailyRecord = entry.getValue(); + + // Only include records that have a workout (Day) + if (!date.isBefore(oneWeekAgo) && !date.isAfter(today) && dailyRecord.getDayFromRecord() != null) { + weeklySummary.append(dailyRecord.getDayFromRecord().toString()); + weeklySummary.append(String.format("Completed On: %s%n%n", date.format(formatter))); + totalExercises += dailyRecord.getDayFromRecord().getExercisesCount(); + } + } + + if (totalExercises == 0) { + return "No workout history available for the past week."; + } + + return weeklySummary.toString(); + } + + /** + * Logs a daily record on a specific date into the history. + * + * @param date the date of the workout record + * @param record the daily record to add to the history + */ + public void logRecord(LocalDate date, DailyRecord record) { + history.put(date, record); + } + + // @@author TVageesan + /** + * Deletes the daily record for a specified date. + *

+ * Checks if a record exists in the {@code history} map for the given date. + * If present, removes the record and returns it. If not present, returns {@code null}. + *

+ * + * @param date the date of the record to delete + * @return the deleted {@code DailyRecord} if it existed, or {@code null} if no record exists for the specified date + */ + public DailyRecord deleteRecord(LocalDate date) { + assert date != null : "Date must not be null"; + if (!history.containsKey(date)) { + return null; + } + return history.remove(date); + } + // @@author + + /** + * Checks if a workout record exists for a specific date. + * + * @param date the date to check for a workout record + * @return {@code true} if a record exists for the specified date, {@code false} otherwise + */ + public boolean hasRecord(LocalDate date) { + return history.containsKey(date); + } + + /** + * Returns the number of records in the workout history. + * + * @return the size of the workout history + */ + public int getHistorySize() { + return history.size(); + } + + /** + * Returns a formatted string of personal bests for all exercises. + * + * @return a string listing personal bests for each exercise, or a message indicating no personal bests found + */ + public String getFormattedPersonalBests() { + Map personalBests = getPersonalBestsMap(); + + if (personalBests.isEmpty()) { + return "No personal bests found."; + } + + StringBuilder bestsMessage = new StringBuilder("Personal bests for all exercises:\n"); + for (Map.Entry entry : personalBests.entrySet()) { + bestsMessage.append(entry.getKey()) + .append(": ") + .append(entry.getValue().toStringPb()) + .append("\n"); + } + + return bestsMessage.toString(); + } + + /** + * Builds a map of the personal best exercise for each type based on weight. + * + *

Filters out any {@code DailyRecord} entries without a valid workout {@code Day} to avoid null + * pointer exceptions.

+ * + * @return a map of exercise names and their corresponding best {@link Exercise} entries + */ + private Map getPersonalBestsMap() { + Map personalBests = new LinkedHashMap<>(); + + for (DailyRecord dailyRecord : history.values()) { + // Skip this record if it does not have a Day (workout data) + if (dailyRecord.getDayFromRecord() == null) { + continue; + } + + int exercisesCount = dailyRecord.getDayFromRecord().getExercisesCount(); + for (int i = 0; i < exercisesCount; i++) { + Exercise exercise = dailyRecord.getDayFromRecord().getExercise(i); + String exerciseName = exercise.getName(); + + if (!personalBests.containsKey(exerciseName) || isBetter(exercise, personalBests.get(exerciseName))) { + personalBests.put(exerciseName, exercise); + } + } + } + return personalBests; + } + + /** + * Compares two exercises to determine if the current exercise is better based on weight. + * + * @param current the current exercise to evaluate + * @param best the existing best exercise to compare against + * @return {@code true} if the current exercise is better, {@code false} otherwise + */ + private boolean isBetter(Exercise current, Exercise best) { + return current.getWeight() > best.getWeight(); + } + + /** + * Retrieves a formatted personal best entry for a specific exercise. + * + *

Filters out any {@code DailyRecord} entries without a valid workout {@code Day} to avoid + * null pointer exceptions.

+ * + * @param exerciseName the name of the exercise to look up + * @return a formatted string showing the personal best for the specified exercise, or a message if not found + */ + public String getPersonalBestForExercise(String exerciseName) { + Exercise personalBest = null; + + for (DailyRecord dailyRecord : history.values()) { + // Skip this record if it does not have a Day (workout data) + if (dailyRecord.getDayFromRecord() == null) { + continue; + } + + int exercisesCount = dailyRecord.getDayFromRecord().getExercisesCount(); + for (int i = 0; i < exercisesCount; i++) { + Exercise exercise = dailyRecord.getDayFromRecord().getExercise(i); + + if (exercise.getName().equalsIgnoreCase(exerciseName)) { + if (personalBest == null || isBetter(exercise, personalBest)) { + personalBest = exercise; + } + } + } + } + + if (personalBest != null) { + return String.format("Personal best for %s: %s", exerciseName, personalBest.toStringPb()); + } else { + return String.format("No personal best found for %s", exerciseName); + } + } + + //@@author Bev-Low + /** + * Converts the {@code history} map, which stores {@link DailyRecord} objects associated with their + * completion dates, into a formatted string representation for printing. + *

+ * This method iterates over each entry in the {@code history} map and formats each + * {@code LocalDate} key and corresponding {@code DailyRecord} value into a readable string. + * Each record is separated by a line of equals signs to improve readability. + *

+ * If the history is empty, this method returns a message indicating that no history is available. + * + * @return a formatted string representation of the history, displaying each {@code DailyRecord} + * with its completion date. If the history is empty, returns "No history available." + */ + @Override + public String toString() { + StringBuilder historyString = new StringBuilder(); + int count = 0; + int size = history.size(); + + if (history.isEmpty()) { + return "No history available."; + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + for (LocalDate date : history.keySet()) { + historyString.append("\n"); + historyString.append(String.format("Completed On: %s%n%n", date.format(formatter))); + DailyRecord dailyRecord = history.get(date); + historyString.append(dailyRecord.toString()); + count++; + if (count < size) { + historyString.append("\n\n==============\n"); + } + } + + return historyString.toString(); + } + //@@author +} + diff --git a/src/main/java/meal/Meal.java b/src/main/java/meal/Meal.java new file mode 100644 index 0000000000..e72bdf8dce --- /dev/null +++ b/src/main/java/meal/Meal.java @@ -0,0 +1,71 @@ +// @@author Atulteja + +package meal; + +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a meal with a name and calorie count. + */ +public class Meal { + + private static final Logger logger = Logger.getLogger(Meal.class.getName()); + + private int calories; + private String name; + + /** + * Constructs a Meal with the specified name and calorie count. + * + * @param name the name of the meal + * @param calories the calorie count of the meal + * @throws AssertionError if the name is null, empty, or if the calories are negative + */ + public Meal(String name, int calories) { + assert name != null && !name.isEmpty() : "Meal name cannot be null or empty"; + assert calories >= 0 : "Calories cannot be negative"; + + this.name = name; + this.calories = calories; + + logger.log(Level.INFO, "Meal created: {0} with {1} kcal", new Object[]{name, calories}); + } + + public int getCalories() { + return calories; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + return Objects.hash(calories, name); + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Meal meal = (Meal) o; + return calories == meal.calories && + Objects.equals(name, meal.name); + } + + /** + * Overridden to return a string representation of the meal, including its name and calorie count. + * + * @return a string representation of the meal + */ + @Override + public String toString() { + return name + " | " + calories + "kcal"; + } +} diff --git a/src/main/java/meal/MealList.java b/src/main/java/meal/MealList.java new file mode 100644 index 0000000000..0db1cdd93a --- /dev/null +++ b/src/main/java/meal/MealList.java @@ -0,0 +1,107 @@ +// @@author Atulteja + +package meal; + +import exceptions.MealException; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Represents a list of meals, providing functionality to add, delete, and retrieve meals. + */ +public class MealList { + private static final Logger logger = Logger.getLogger(MealList.class.getName()); + private ArrayList meals; + + public MealList() { + meals = new ArrayList<>(); + logger.log(Level.INFO, "MealList created with an empty list."); + } + + public boolean isEmpty() { + return meals.isEmpty(); + } + + public int getSize() { + return meals.size(); + } + + /** + * Adds a meal to the list. + * + * @param meal the meal to add to the list + * @throws AssertionError if the meal is null + */ + public void addMeal(Meal meal) { + assert meal != null : "Meal cannot be null"; + + + meals.add(meal); + logger.log(Level.INFO, "Added meal: {0}. Current list: {1}", new Object[]{meal, meals}); + } + + /** + * Deletes a meal from the list at the specified index. + * + * @param index the index of the meal to delete + * @return the meal that was deleted + * @throws IndexOutOfBoundsException if the index is out of bounds + */ + public Meal deleteMeal(int index) { + if (index < 0 || index >= meals.size()) { + logger.log(Level.WARNING, "Invalid index for deletion: {0}", index); + throw MealException.doesNotExist(); + } + + Meal mealToBeDeleted = meals.get(index); + meals.remove(index); + logger.log(Level.INFO, "Deleted meal: {0} at index {1}. Current list: {2}", + new Object[]{mealToBeDeleted, index, meals}); + return mealToBeDeleted; + } + + public ArrayList getMeals() { + logger.log(Level.INFO, "Retrieved meal list: {0}", meals); + return meals; + } + + /** + * Overrides the toString to returns a string representation of the meal list. + * Each meal is represented by its index and details on a new line. + * + * @return a string representation of the meal list + */ + @Override + public String toString() { + StringBuilder output = new StringBuilder(); + int count = 1; + + for (Meal meal : meals) { + output.append(count).append(": ").append(meal).append("\n"); + count++; + } + + return output.toString().trim(); // Trim to remove trailing newline + } + + @Override + public int hashCode() { + return Objects.hash(meals); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MealList meallist = (MealList) o; + return meals == meallist.meals; + } +} diff --git a/src/main/java/parser/FlagDefinitions.java b/src/main/java/parser/FlagDefinitions.java new file mode 100644 index 0000000000..0c28560ae3 --- /dev/null +++ b/src/main/java/parser/FlagDefinitions.java @@ -0,0 +1,49 @@ +//@@author nirala-ts +package parser; +import java.util.HashSet; + +public class FlagDefinitions { + public static final String DATE_FLAG = "/t"; + + public static final String PROGRAMME_FLAG = "/p"; + public static final String DAY_FLAG = "/d"; + public static final String EXERCISE_FLAG = "/e"; + + public static final String NAME_FLAG = "/n"; + public static final String SETS_FLAG = "/s"; + public static final String REPS_FLAG = "/r"; + public static final String WEIGHT_FLAG = "/w"; + public static final String CALORIES_FLAG = "/c"; + + public static final String REMOVE_EXERCISE_FLAG = "/xe"; + public static final String ADD_EXERCISE_FLAG = "/ae"; + public static final String UPDATE_EXERCISE_FLAG = "/ue"; + + public static final String ADD_DAY_FLAG = "/ad"; + public static final String REMOVE_DAY_FLAG = "/xd"; + + public static final String MEAL_INDEX = "/m"; + public static final String WATER_INDEX = "/w"; + public static final String VOLUME_FLAG = "/v"; + + public static final HashSet VALID_FLAGS = new HashSet<>(); + + static { + VALID_FLAGS.add(DATE_FLAG); + VALID_FLAGS.add(PROGRAMME_FLAG); + VALID_FLAGS.add(DAY_FLAG); + VALID_FLAGS.add(EXERCISE_FLAG); + VALID_FLAGS.add(NAME_FLAG); + VALID_FLAGS.add(SETS_FLAG); + VALID_FLAGS.add(REPS_FLAG); + VALID_FLAGS.add(WEIGHT_FLAG); + VALID_FLAGS.add(CALORIES_FLAG); + VALID_FLAGS.add(REMOVE_EXERCISE_FLAG); + VALID_FLAGS.add(ADD_EXERCISE_FLAG); + VALID_FLAGS.add(UPDATE_EXERCISE_FLAG); + VALID_FLAGS.add(ADD_DAY_FLAG); + VALID_FLAGS.add(REMOVE_DAY_FLAG); + VALID_FLAGS.add(MEAL_INDEX); + VALID_FLAGS.add(VOLUME_FLAG); + } +} diff --git a/src/main/java/parser/FlagParser.java b/src/main/java/parser/FlagParser.java new file mode 100644 index 0000000000..839870ad81 --- /dev/null +++ b/src/main/java/parser/FlagParser.java @@ -0,0 +1,309 @@ +//@@author nirala-ts + +package parser; + +import exceptions.FlagException; + +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static common.Utils.isNull; + +import static parser.FlagDefinitions.DAY_FLAG; +import static parser.FlagDefinitions.DATE_FLAG; +import static parser.FlagDefinitions.EXERCISE_FLAG; +import static parser.FlagDefinitions.NAME_FLAG; +import static parser.FlagDefinitions.CALORIES_FLAG; +import static parser.FlagDefinitions.REPS_FLAG; +import static parser.FlagDefinitions.SETS_FLAG; +import static parser.FlagDefinitions.WEIGHT_FLAG; +import static parser.FlagDefinitions.PROGRAMME_FLAG; +import static parser.FlagDefinitions.UPDATE_EXERCISE_FLAG; +import static parser.FlagDefinitions.ADD_DAY_FLAG; +import static parser.FlagDefinitions.ADD_EXERCISE_FLAG; +import static parser.FlagDefinitions.REMOVE_DAY_FLAG; +import static parser.FlagDefinitions.REMOVE_EXERCISE_FLAG; +import static parser.FlagDefinitions.MEAL_INDEX; +import static parser.FlagDefinitions.WATER_INDEX; +import static parser.FlagDefinitions.VOLUME_FLAG; + +import static parser.ParserUtils.parseInteger; +import static parser.ParserUtils.parseIndex; +import static parser.ParserUtils.parseFloat; +import static parser.ParserUtils.parseDate; +import static parser.ParserUtils.splitArguments; + +/** + * The {@code FlagParser} class simplifies the parsing of flagged argument strings. The values can be retrieved in + * various formats, such as Integer, Date, String, or Index. + * This class also supports aliasing for easy retrieval of data in different formats. + * + * @author nirala-ts + */ +public class FlagParser { + private static final String DEFAULT_SPLIT_BY = "(?=/)"; + private static final String SPLIT_BY_START = "(?=/(?!("; + private static final String SPLIT_BY_DELIMITER = "|"; + private static final String SPLIT_BY_END = ")\\b))"; + + private final Logger logger = Logger.getLogger(FlagParser.class.getName()); + + private final Map parsedFlags = new HashMap<>(); + private final Map aliasMap = new HashMap<>(); + + /** + * Constructs a {@code FlagParser} with the given argument string, setting up aliases and ignored flags. + * This parser is essential for processing commands with multiple flags. + * + * @param argumentString The argument string to parse. + * @param ignoredFlags Flags that are optional for this instance. + * @throws FlagException if {@code argumentString} is null. + */ + public FlagParser(String argumentString, String... ignoredFlags) { + if (isNull(argumentString)){ + throw FlagException.missingArguments(); + } + + initializeAliasMap(); + parse(argumentString, generateSplitBy(ignoredFlags)); + } + + /** + * Generates a regex for splitting the argument string by specified flags and aliases. + * + * @param ignoredFlags Flags to ignore while generating the regex split pattern. + * @return A regex string to split the argument string by flags. + */ + private String generateSplitBy(String... ignoredFlags){ + if (ignoredFlags.length == 0){ + return DEFAULT_SPLIT_BY; + } + + // Starts building the regex pattern, initializing with specific syntax for ignored flags + StringBuilder splitBy = new StringBuilder(SPLIT_BY_START); + for (String ignoredFlag: ignoredFlags) { + splitBy.append(ignoredFlag.substring(1)).append(SPLIT_BY_DELIMITER); + + // Checks for aliases and adds them to the pattern if they map to the ignored flag + for (Map.Entry entry: aliasMap.entrySet()){ + String canonicalFlag = entry.getValue(); + String aliasFlag = entry.getKey(); + if (canonicalFlag.equals(ignoredFlag)){ + splitBy.append(aliasFlag.substring(1)).append(SPLIT_BY_DELIMITER); + } + } + } + + // Removes the trailing delimiter added after the last flag to prevent invalid regex syntax + splitBy.setLength(splitBy.length() - 1); + + return splitBy.append(SPLIT_BY_END).toString(); + } + + /** + * Sets up flag aliases to allow flexible parsing by recognizing alternative names for flags. + */ + private void initializeAliasMap() { + aliasMap.put("/programme", PROGRAMME_FLAG); + aliasMap.put("/prog", PROGRAMME_FLAG); + + aliasMap.put("/day", DAY_FLAG); + aliasMap.put("/date", DATE_FLAG); + + aliasMap.put("/name", NAME_FLAG); + + aliasMap.put("/exercise", EXERCISE_FLAG); + aliasMap.put("/ex", EXERCISE_FLAG); + aliasMap.put("/set", SETS_FLAG); + aliasMap.put("/sets", SETS_FLAG); + aliasMap.put("/rep", REPS_FLAG); + aliasMap.put("/reps", REPS_FLAG); + aliasMap.put("/weight", WEIGHT_FLAG); + aliasMap.put("/calories", CALORIES_FLAG); + + aliasMap.put("/addEx", ADD_EXERCISE_FLAG); + aliasMap.put("/updateEx", UPDATE_EXERCISE_FLAG); + aliasMap.put("/removeEx", REMOVE_EXERCISE_FLAG); + + aliasMap.put("/addDay", ADD_DAY_FLAG); + aliasMap.put("/removeDay", REMOVE_DAY_FLAG); + + aliasMap.put("/meal", MEAL_INDEX); + + aliasMap.put("/water", WATER_INDEX); + aliasMap.put("/volume", VOLUME_FLAG); + aliasMap.put("/vol", VOLUME_FLAG); + } + + /** + * Parses the argument string by splitting it based on the given regex and populates + * the {@code parsedFlags} map with flag-value pairs. + * + * @param argumentString The string to parse. + * @param splitBy The regex used to split the argument string by flags. + */ + private void parse(String argumentString, String splitBy) { + assert argumentString != null : "Argument string must not be null"; + + String[] args = argumentString.split(splitBy); + for (String arg : args) { + + String[] argParts = splitArguments(arg); + String flag = argParts[0].trim(); + String value = argParts[1].trim(); + flag = resolveAlias(flag); + + if (hasFlag(flag)) { + throw FlagException.duplicateFlag(flag); + } + + if (!FlagDefinitions.VALID_FLAGS.contains(flag)) { + throw FlagException.invalidFlag(flag); + } + + logger.log(Level.INFO, "Successfully parsed flag: {0} with value: {1}", new Object[]{flag, value}); + parsedFlags.put(flag, value); + } + } + + /** + * Resolves the alias for a given flag, returning the canonical flag if an alias is found. + * + * @param flag The flag or alias to resolve. + * @return The canonical flag, or the original flag if no alias is found. + */ + private String resolveAlias(String flag) { + if (aliasMap.containsKey(flag)) { + return aliasMap.get(flag); + } + return flag; + } + + /** + * Checks if a flag is present in the parsed flags. + * + * @param flag The flag to check. + * @return {@code true} if the flag is present, {@code false} otherwise. + */ + public boolean hasFlag(String flag) { + assert flag != null && !flag.isEmpty() : "Flag must not be null or empty"; + + flag = resolveAlias(flag); + boolean hasFlag = parsedFlags.containsKey(flag); + + logger.log(Level.INFO, "Flag {0} presence: {1}", new Object[]{flag, hasFlag}); + return hasFlag; + } + + /** + * Validates that all required flags are present in the parsed flags and contains a non-null value. + * + * @param requiredFlags The required flags to validate. + * @throws FlagException if any required flag is missing. + */ + public void validateRequiredFlags(String... requiredFlags) { + assert requiredFlags != null : "Required flags string must not be null"; + + for (String flag : requiredFlags) { + + flag = resolveAlias(flag); + + if (!hasFlag(flag)) { + logger.log(Level.WARNING, "Missing required flag: {0}", flag); + throw FlagException.missingFlag(flag); + } + + String value = getStringByFlag(flag); + + if (isNull(value)) { + logger.log(Level.WARNING, "Required flag has null value: {0}", flag); + throw FlagException.missingRequiredArguments(flag); + } + } + } + + public void validateUniqueFlag(String... uniqueFlags){ + int count = 0; + StringBuilder seenFlags = new StringBuilder(); + + for (String flag : uniqueFlags) { + if (hasFlag(flag)) { + count++; + seenFlags.append(flag).append(" "); + } + } + + if (count > 1) { + throw FlagException.nonUniqueFlag(seenFlags.toString()); + } + } + + /** + * Retrieves the string value associated with a flag. + * + * @param flag The flag whose value to retrieve. + * @return The value associated with the flag, or {@code null} if the flag is absent. + */ + public String getStringByFlag(String flag) { + assert flag != null && !flag.isEmpty() : "Flag must not be null or empty"; + + flag = resolveAlias(flag); + + if (!parsedFlags.containsKey(flag)) { + logger.log(Level.INFO, "Flag {0} not found; returning null", flag); + return null; + } + + String value = parsedFlags.get(flag); + logger.log(Level.INFO, "Successfully retrieved value for flag {0}: {1}", new Object[]{flag, value}); + return value.trim(); + } + + /** + * Retrieves the zero-based index associated with a flag. + * + * @param flag The flag whose index to retrieve. + * @return The zero-based index parsed from the flag's value. + */ + public int getIndexByFlag(String flag) { + String indexString = getStringByFlag(flag); + return parseIndex(indexString); + } + + /** + * Retrieves the integer value associated with a flag. + * + * @param flag The flag whose integer value to retrieve. + * @return The integer parsed from the flag's value. + */ + public int getIntegerByFlag(String flag){ + String intString = getStringByFlag(flag); + return parseInteger(intString); + } + + /** + * Retrieves the float value associated with a flag. + * + * @param flag The flag whose float value to retrieve. + * @return The float parsed from the flag's value. + */ + public float getFloatByFlag(String flag) { + String floatString = getStringByFlag(flag); + return parseFloat(floatString); + } + + /** + * Retrieves the date value associated with a flag. + * + * @param flag The flag whose date value to retrieve. + * @return The {@code LocalDate} parsed from the flag's value. + */ + public LocalDate getDateByFlag(String flag){ + String dateString = getStringByFlag(flag); + return parseDate(dateString); + } +} diff --git a/src/main/java/parser/Parser.java b/src/main/java/parser/Parser.java new file mode 100644 index 0000000000..5f0d18d054 --- /dev/null +++ b/src/main/java/parser/Parser.java @@ -0,0 +1,65 @@ +//@@author nirala-ts + +package parser; + +import command.Command; +import exceptions.ParserException; +import parser.command.factory.CommandFactory; + +import java.util.logging.Logger; +import java.util.logging.Level; + +import static parser.ParserUtils.splitArguments; + +/** + * The {@code Parser} class serves as an intermediary between user input and command execution + * within the BuffBuddy application. It interprets user-provided strings and generates appropriate + * {@code Command} objects for execution. + * + */ +public class Parser { + private final Logger logger = Logger.getLogger(this.getClass().getName()); + private final CommandFactory commandFactory; + + /** + * Constructs a new {@code Parser} instance, initializing its associated {@code CommandFactory}. + */ + public Parser() { + this.commandFactory = new CommandFactory(); + } + + /** + * Constructs a new {@code Parser} instance, using the provided {@code CommandFactory}. + * This constructor is primarily for testing purposes. + */ + public Parser(CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + /** + * Parses the given command string and returns the corresponding {@code Command} object. + * + * @param fullCommand The complete user input, containing the command and any arguments. + * @return A {@code Command} object that represents the parsed command. + * @throws ParserException if the input is null or empty. + */ + public Command parse(String fullCommand) { + if (fullCommand == null || fullCommand.trim().isEmpty()) { + logger.log(Level.WARNING, "Command is empty"); + throw ParserException.missingCommand(); + } + + /* + * Splits the full command input into the primary command and its associated arguments, + * enabling identification of the command category. + */ + String[] inputArguments = splitArguments(fullCommand); + String commandString = inputArguments[0]; + String argumentString = inputArguments[1]; + + logger.log(Level.INFO, "Successfully parsed command: {0}, with arguments: {1}", + new Object[]{commandString, argumentString}); + + return commandFactory.createCommand(commandString, argumentString); + } +} diff --git a/src/main/java/parser/ParserUtils.java b/src/main/java/parser/ParserUtils.java new file mode 100644 index 0000000000..8d23f649e9 --- /dev/null +++ b/src/main/java/parser/ParserUtils.java @@ -0,0 +1,188 @@ +// @@author nirala-ts + +package parser; + +import exceptions.ParserException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static common.Utils.DATE_FORMAT; +import static common.Utils.NULL_INTEGER; +import static common.Utils.NULL_FLOAT; +import static common.Utils.validate; + + +/** + * The {@code ParserUtils} class is a utility class containing common methods used across all parsing functions. + * These methods handle tasks such as splitting arguments, parsing integers and floats, validating indices, + * and formatting dates. + */ +public class ParserUtils { + private static final Logger logger = Logger.getLogger(ParserUtils.class.getName()); + + /** + * Splits the argument string into the primary command and its arguments. + * + * @param argumentString The full argument string provided by the user. + * @return A string array containing the command as the first element and the remaining arguments as the second. + * @throws AssertionError if {@code argumentString} is null. + */ + public static String[] splitArguments(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + String[] inputArguments = argumentString.split(" ", 2); + String command = inputArguments[0].trim(); + String args = (inputArguments.length > 1) ? inputArguments[1].trim() : ""; + + logger.log(Level.INFO, "Successfully split arguments. Command: {0}, Arguments: {1}", + new Object[]{command, args}); + return new String[]{command, args}; + } + + /** + * Trims the input string to remove leading and trailing whitespace. + * + * @param argumentString The string to trim. + * @return The trimmed version of {@code argumentString}. + * @throws ParserException if {@code argumentString} is empty after trimming. + */ + static String trimInput(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + String trimmedString = argumentString.trim(); + + if (trimmedString.isEmpty()){ + logger.log(Level.WARNING, "Trimmed input is empty"); + throw ParserException.missingArguments(); + } + + logger.log(Level.INFO, "Successfully trimmed input: {0}", trimmedString); + return trimmedString; + } + + /** + * Parses a string as an integer, returning a default value if the string is null. + * + * @param intString The string to parse as an integer. + * @return The parsed integer, or {@code NULL_INTEGER} if {@code intString} is null. + * @throws ParserException if {@code intString} cannot be parsed as an integer. + */ + public static int parseInteger(String intString){ + if (intString == null) { + logger.log(Level.INFO, "Integer string is null. Returning default value: {0}", NULL_INTEGER); + return NULL_INTEGER; + } + + String trimmedIntString = trimInput(intString); + int result = -1; + + if (trimmedIntString.length() > 10) { // 10 digits is the maximum for Integer.MAX_VALUE (2,147,483,647) + throw ParserException.infinityInt(trimmedIntString); + } + + try{ + result = Integer.parseInt(trimmedIntString); + logger.log(Level.INFO, "Successfully parsed integer: {0}", result); + } catch (NumberFormatException e){ + logger.log(Level.WARNING, "Failed to parse integer from string: {0}", intString); + throw ParserException.invalidInt(result); + } + + validate(result); + + return result; + } + + /** + * Parses a string as a float, returning a default value if the string is null. + * + * @param floatString The string to parse as a float. + * @return The parsed float, or {@code NULL_FLOAT} if {@code floatString} is null. + * @throws ParserException if {@code floatString} cannot be parsed as a float. + */ + public static float parseFloat(String floatString) { + if (floatString == null) { + logger.log(Level.INFO, "Float string is null. Returning default value: {0}", NULL_FLOAT); + return NULL_FLOAT; + } + + String trimmedFloatString = trimInput(floatString); + float result = -1; + + try { + result = Float.parseFloat(trimmedFloatString); + logger.log(Level.INFO, "Successfully parsed float: {0}", result); + } catch (NumberFormatException e) { + logger.log(Level.WARNING, "Failed to parse float from string: {0}", floatString); + throw ParserException.invalidFloat(result); + } + + validate(result); + + return result; + } + + /** + * Parses a string as an index, adjusting it to zero-based and returning a default if null. + * + * @param indexString The string to parse as an index. + * @return The zero-based index, or {@code NULL_INTEGER} if {@code indexString} is null. + * @throws ParserException if the index is less than zero. + */ + public static int parseIndex(String indexString) { + if (indexString == null) { + logger.log(Level.INFO, "Index string is null. Returning default value: {0}", NULL_INTEGER); + return NULL_INTEGER; + } + + int index = parseInteger(indexString); + validate(index); + index--; + + logger.log(Level.INFO, "Successfully parsed index: {0}", index); + return index; + + } + + /** + * Parses a string as a date using the specified date format. If null, returns the current date. + * + * @param dateString The string to parse as a date. + * @return The parsed {@code LocalDate} object, or today's date if {@code dateString} is null. + * @throws ParserException if the date format is invalid. + */ + public static LocalDate parseDate(String dateString) { + if (dateString == null || dateString.trim().isEmpty()) { + LocalDate today = LocalDate.now(); + logger.log(Level.INFO, "Date string is null/empty. Returning current date: {0}", today); + return today; + } + + String trimmedDateString = trimInput(dateString); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + + try { + LocalDate date = LocalDate.parse(trimmedDateString, formatter); + + String[] parts = trimmedDateString.split("-"); + int inputDay = Integer.parseInt(parts[0]); + int inputMonth = Integer.parseInt(parts[1]); + + // Check if the parsed date matches the input values as LocalDate.parse + // automatically adjusts invalid dates to the nearest valid one + if (date.getDayOfMonth() != inputDay || date.getMonthValue() != inputMonth) { + throw new DateTimeParseException("Invalid date: " + dateString, dateString, 0); + } + + logger.log(Level.INFO, "Successfully parsed date: {0}", date); + return date; + } catch (DateTimeParseException e) { + logger.log(Level.WARNING, "Invalid date format: {0}. Expected format: {1}", + new Object[]{dateString, DATE_FORMAT}); + throw ParserException.invalidDate(trimmedDateString); + } + } +} diff --git a/src/main/java/parser/command/factory/CommandFactory.java b/src/main/java/parser/command/factory/CommandFactory.java new file mode 100644 index 0000000000..0f646f1696 --- /dev/null +++ b/src/main/java/parser/command/factory/CommandFactory.java @@ -0,0 +1,82 @@ +// @@author nirala-ts + +package parser.command.factory; + +import command.Command; +import command.ExitCommand; +import command.InvalidCommand; + +/** + * The {@code CommandFactory} class is responsible for creating instances of various {@code Command} objects + * based on the specified command string. It serves as a central factory that distributes command creation + * requests to specific factories for subcommands like "prog," "history," and others. + * For unsupported commands, it returns an {@code InvalidCommand} to handle erroneous input. + * + *

Supported Commands:

+ *
    + *
  • Program Commands - handled by {@code ProgCommandFactory}
  • + *
  • Meal Commands - handled by {@code MealCommandFactory}
  • + *
  • Water Commands - handled by {@code WaterCommandFactory}
  • + *
  • History Commands - handled by {@code HistoryCommandFactory}
  • + *
  • Exit Command - creates an {@code ExitCommand} directly
  • + *
  • Invalid Command - returns an {@code InvalidCommand} for unknown inputs
  • + *
+ * + * @author nirala-ts + */ +public class CommandFactory { + private final ProgrammeCommandFactory progFactory; + private final MealCommandFactory mealFactory; + private final WaterCommandFactory waterFactory; + private final HistoryCommandFactory historyFactory; + + /** + * Constructs a {@code CommandFactory} and initializes subcommand factories for handling + * specific types of commands. + */ + public CommandFactory() { + this.progFactory = new ProgrammeCommandFactory(); + this.mealFactory = new MealCommandFactory(); + this.waterFactory = new WaterCommandFactory(); + this.historyFactory = new HistoryCommandFactory(); + } + + /** + * Constructor for dependency injection, allowing custom instances of subcommand factories + * for testing with mock objects. + * + * @param progFactory the {@code ProgCommandFactory} instance to handle "prog" commands + * @param mealFactory the {@code MealCommandFactory} instance to handle "meal" commands + * @param waterFactory the {@code WaterCommandFactory} instance to handle "water" commands + * @param historyFactory the {@code HistoryCommandFactory} instance to handle "history" commands + */ + public CommandFactory(ProgrammeCommandFactory progFactory, MealCommandFactory mealFactory, + WaterCommandFactory waterFactory, HistoryCommandFactory historyFactory) { + this.progFactory = progFactory; + this.mealFactory = mealFactory; + this.waterFactory = waterFactory; + this.historyFactory = historyFactory; + } + + /** + * Creates and returns the appropriate {@code Command} object based on the provided command string. + * Delegates command parsing to the relevant subcommand factory when available. Returns an + * {@code ExitCommand} for exit requests and an {@code InvalidCommand} for unsupported commands. + * + * @param commandString The main command word indicating the type of command. + * @param argumentString The arguments provided for the command, if any. + * @return A {@code Command} object corresponding to the parsed command. + */ + public Command createCommand(String commandString, String argumentString) { + assert commandString != null; + + return switch (commandString) { + case ProgrammeCommandFactory.COMMAND_WORD -> progFactory.parse(argumentString); + case MealCommandFactory.COMMAND_WORD -> mealFactory.parse(argumentString); + case WaterCommandFactory.COMMAND_WORD -> waterFactory.parse(argumentString); + case HistoryCommandFactory.COMMAND_WORD -> historyFactory.parse(argumentString); + case ExitCommand.COMMAND_WORD -> new ExitCommand(); + default -> new InvalidCommand(); + }; + } +} diff --git a/src/main/java/parser/command/factory/HistoryCommandFactory.java b/src/main/java/parser/command/factory/HistoryCommandFactory.java new file mode 100644 index 0000000000..92555cedcb --- /dev/null +++ b/src/main/java/parser/command/factory/HistoryCommandFactory.java @@ -0,0 +1,104 @@ +// @@author andreusxcarvalho +package parser.command.factory; + +import command.Command; +import command.history.ListPersonalBestsCommand; +import command.history.ViewHistoryCommand; +import command.history.DeleteHistoryCommand; +import command.history.ListHistoryCommand; +import command.history.ViewPersonalBestCommand; +import command.history.WeeklySummaryCommand; +import command.InvalidCommand; + +import java.time.LocalDate; + +import static parser.ParserUtils.parseDate; +import static parser.ParserUtils.splitArguments; + +/** + * Factory class responsible for creating instances of history-related commands. + *

+ * The {@code HistoryCommandFactory} parses the input arguments and generates + * specific command objects based on the type of history command requested. + * Supported commands include viewing, listing, and deleting history, as well as + * managing personal bests. + *

+ */ +public class HistoryCommandFactory { + public static final String COMMAND_WORD = "history"; + + /** + * Parses the given argument string and creates the corresponding {@link Command} object. + *

+ * Based on the subcommand specified in the argument string, this method determines + * the appropriate command type and returns an instance of that command. + *

+ * + * @param argumentString the argument string containing the subcommand and any additional arguments + * @return the created {@link Command} object, or an {@link InvalidCommand} if the subcommand is unrecognized + */ + public Command parse(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + String[] inputArguments = splitArguments(argumentString); + String subCommandString = inputArguments[0]; + String arguments = inputArguments[1]; + + return switch (subCommandString) { + case ViewHistoryCommand.COMMAND_WORD -> prepareViewHistoryCommand(arguments); + case ListHistoryCommand.COMMAND_WORD -> new ListHistoryCommand(); + case ListPersonalBestsCommand.COMMAND_WORD -> preparePersonalBestCommand(arguments); + case WeeklySummaryCommand.COMMAND_WORD -> new WeeklySummaryCommand(); + case DeleteHistoryCommand.COMMAND_WORD -> prepareDeleteHistoryCommand(arguments); + default -> new InvalidCommand(); + }; + } + + /** + * Prepares a {@link ViewHistoryCommand} for a specific date. + *

+ * This method parses the argument string to extract the date and creates a {@link ViewHistoryCommand} + * for viewing the history record on that date. + *

+ * + * @param argumentString the argument string containing the date + * @return a {@link ViewHistoryCommand} for the specified date + */ + private Command prepareViewHistoryCommand(String argumentString) { + LocalDate date = parseDate(argumentString); + return new ViewHistoryCommand(date); + } + + /** + * Prepares a command to handle personal bests, either listing all or viewing a specific exercise. + *

+ * If the argument string is empty, a {@link ListPersonalBestsCommand} is created. + * Otherwise, a {@link ViewPersonalBestCommand} is created for the specified exercise. + *

+ * + * @param argumentString the argument string specifying an exercise name, or empty to list all personal bests + * @return a {@link ListPersonalBestsCommand} or {@link ViewPersonalBestCommand}, depending on the argument + */ + private Command preparePersonalBestCommand(String argumentString) { + if (argumentString.isEmpty()) { + return new ListPersonalBestsCommand(); + } + return new ViewPersonalBestCommand(argumentString); + } + + /** + * Prepares a {@link DeleteHistoryCommand} to delete a record for a specific date. + *

+ * This method parses the argument string to extract the date and creates a {@link DeleteHistoryCommand} + * to delete the history record on that date. + *

+ * + * @param argumentString the argument string containing the date to delete + * @return a {@link DeleteHistoryCommand} for the specified date + */ + private Command prepareDeleteHistoryCommand(String argumentString) { + LocalDate date = parseDate(argumentString); + return new DeleteHistoryCommand(date); + } +} + diff --git a/src/main/java/parser/command/factory/MealCommandFactory.java b/src/main/java/parser/command/factory/MealCommandFactory.java new file mode 100644 index 0000000000..194992e47d --- /dev/null +++ b/src/main/java/parser/command/factory/MealCommandFactory.java @@ -0,0 +1,103 @@ +// @@author Atulteja +package parser.command.factory; + +import command.Command; +import command.InvalidCommand; +import command.meals.AddMealCommand; +import command.meals.DeleteMealCommand; +import command.meals.ViewMealCommand; +import exceptions.MealException; +import meal.Meal; +import parser.FlagParser; + +import java.time.LocalDate; + +import static parser.FlagDefinitions.MEAL_INDEX; +import static parser.ParserUtils.parseDate; +import static parser.ParserUtils.splitArguments; + + +/** + * Factory class for parsing meal-related commands. + * This class processes command strings and creates appropriate meal command instances. + */ +public class MealCommandFactory { + public static final String COMMAND_WORD = "meal"; + + /** + * Parses the argument string and returns the appropriate meal command based on the subcommand. + * + * @param argumentString the argument string containing the subcommand and flags + * @return the corresponding Command object based on the subcommand; + * InvalidCommand if the subcommand is unrecognized + * @throws AssertionError if the argument string is null + */ + public Command parse(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + String[] inputArguments = splitArguments(argumentString); + String subCommandString = inputArguments[0]; + String arguments = inputArguments[1]; + + return switch (subCommandString) { + case AddMealCommand.COMMAND_WORD -> prepareAddCommand(arguments); + case DeleteMealCommand.COMMAND_WORD -> prepareDeleteCommand(arguments); + case ViewMealCommand.COMMAND_WORD -> prepareViewCommand(arguments); + default -> new InvalidCommand(); + }; + } + + /** + * Prepares an AddMealCommand by parsing the argument string for the meal name, calories, and date. + * + * @param argumentString the argument string containing flags for name, calories, and date + * @return an AddMealCommand containing the parsed meal details + * @throws AssertionError if required flags are missing or invalid + */ + public Command prepareAddCommand(String argumentString) { + FlagParser flagParser = new FlagParser(argumentString); + + flagParser.validateRequiredFlags("/n", "/c"); + + String mealName = flagParser.getStringByFlag("/n"); + int mealCalories = flagParser.getIntegerByFlag("/c"); + + if (mealCalories < 0){ + throw MealException.caloriesOutOfBounds(); + } + + LocalDate date = flagParser.getDateByFlag("/t"); + + Meal mealToAdd = new Meal(mealName, mealCalories); + + return new AddMealCommand(mealToAdd, date); + } + + /** + * Prepares a DeleteMealCommand by parsing the argument string for the meal index and date. + * + * @param argumentString the argument string containing flags for meal index and date + * @return a DeleteMealCommand containing the parsed meal index and date + * @throws AssertionError if the required flag is missing or invalid + */ + public Command prepareDeleteCommand(String argumentString) { + FlagParser flagParser = new FlagParser(argumentString); + + flagParser.validateRequiredFlags(MEAL_INDEX); + + int mealIndexToDelete = flagParser.getIndexByFlag(MEAL_INDEX); + LocalDate date = flagParser.getDateByFlag("/t"); + + return new DeleteMealCommand(mealIndexToDelete, date); + } + + /** + * Prepares a ViewMealCommand by parsing the argument string for the date. + * @param argumentString the argument string containing the date + * @return a ViewMealCommand containing the parsed date + */ + public Command prepareViewCommand(String argumentString) { + LocalDate date = parseDate(argumentString); + return new ViewMealCommand(date); + } +} diff --git a/src/main/java/parser/command/factory/ProgrammeCommandFactory.java b/src/main/java/parser/command/factory/ProgrammeCommandFactory.java new file mode 100644 index 0000000000..e319479795 --- /dev/null +++ b/src/main/java/parser/command/factory/ProgrammeCommandFactory.java @@ -0,0 +1,434 @@ +// @@author nirala-ts + +package parser.command.factory; + +import command.Command; +import command.InvalidCommand; +import command.programme.CreateProgrammeCommand; +import command.programme.DeleteProgrammeCommand; +import command.programme.ViewProgrammeCommand; +import command.programme.ListProgrammeCommand; +import command.programme.StartProgrammeCommand; +import command.programme.LogProgrammeCommand; +import command.programme.edit.EditProgrammeCommand; +import command.programme.edit.EditExerciseProgrammeCommand; +import command.programme.edit.CreateExerciseProgrammeCommand; +import command.programme.edit.DeleteExerciseProgrammeCommand; +import command.programme.edit.CreateDayProgrammeCommand; +import command.programme.edit.DeleteDayProgrammeCommand; + +import exceptions.FlagException; +import exceptions.ProgrammeException; +import parser.FlagParser; +import programme.Day; +import programme.Exercise; +import programme.ExerciseUpdate; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static parser.FlagDefinitions.DAY_FLAG; +import static parser.FlagDefinitions.DATE_FLAG; +import static parser.FlagDefinitions.EXERCISE_FLAG; +import static parser.FlagDefinitions.NAME_FLAG; +import static parser.FlagDefinitions.CALORIES_FLAG; +import static parser.FlagDefinitions.REPS_FLAG; +import static parser.FlagDefinitions.SETS_FLAG; +import static parser.FlagDefinitions.WEIGHT_FLAG; +import static parser.FlagDefinitions.PROGRAMME_FLAG; +import static parser.FlagDefinitions.UPDATE_EXERCISE_FLAG; +import static parser.FlagDefinitions.ADD_DAY_FLAG; +import static parser.FlagDefinitions.ADD_EXERCISE_FLAG; +import static parser.FlagDefinitions.REMOVE_DAY_FLAG; +import static parser.FlagDefinitions.REMOVE_EXERCISE_FLAG; + +import static parser.ParserUtils.parseIndex; +import static parser.ParserUtils.splitArguments; + +/** + * The {@code ProgCommandFactory} class is a factory responsible for creating all program-related commands. + * This class includes helper methods such as {@code parseDay} and {@code parseExercise} to handle + * common parsing tasks across program-related commands, centralizing command creation and + * simplifying command handling. + * + *

Supported Commands:

+ *
    + *
  • Create program command - handled by {@link #prepareCreateCommand}
  • + *
  • View program command - handled by {@link #prepareViewCommand}
  • + *
  • Edit program and exercises command - handled by {@link #prepareEditCommand}
  • + *
  • Start program command - handled by {@link #prepareStartCommand}
  • + *
  • Delete program command - handled by {@link #prepareDeleteCommand}
  • + *
  • Log program activity command - handled by {@link #prepareLogCommand}
  • + *
+ * + */ + +public class ProgrammeCommandFactory { + public static final String COMMAND_WORD = "prog"; + private final Logger logger = Logger.getLogger(this.getClass().getName()); + + /** + * Parses the provided argument string to identify and create the appropriate command. + * + * @param argumentString The command's argument string, which contains the subcommand and arguments. + * @return The created {@code Command} object based on the subcommand specified in the argument string. + */ + public Command parse(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + /* + * Splits the full command input into the primary command and its associated arguments, + * enabling identification of the command category. + */ + String[] inputArguments = splitArguments(argumentString); + String subCommandString = inputArguments[0]; + String arguments = inputArguments[1]; + + logger.log(Level.INFO, "Successfully parsed sub-command: {0}, with arguments: {1}", + new Object[]{subCommandString, arguments}); + + return switch (subCommandString) { + case CreateProgrammeCommand.COMMAND_WORD -> prepareCreateCommand(arguments); + case ViewProgrammeCommand.COMMAND_WORD -> prepareViewCommand(arguments); + case ListProgrammeCommand.COMMAND_WORD -> new ListProgrammeCommand(); + case EditProgrammeCommand.COMMAND_WORD -> prepareEditCommand(arguments); + case StartProgrammeCommand.COMMAND_WORD -> prepareStartCommand(arguments); + case DeleteProgrammeCommand.COMMAND_WORD -> prepareDeleteCommand(arguments); + case LogProgrammeCommand.COMMAND_WORD -> prepareLogCommand(arguments); + default -> new InvalidCommand(); + }; + } + + /** + * Prepares and returns a {@link CreateProgrammeCommand} to create a new program with a specified name and days. + * + * @param argumentString The argument string containing program details, including days and exercises. + * @return A {@link CreateProgrammeCommand} object that represents the request to create a new program. + * @throws ProgrammeException when programme is missing name. + */ + private Command prepareCreateCommand(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + ArrayList days = new ArrayList<>(); + + // Splits the argument by DAY_FLAG to separate each day. + // The first element represents the program name. + String[] progParts = argumentString.split(DAY_FLAG); + String progName = progParts[0].trim(); + if (progName.isEmpty()) { + logger.log(Level.WARNING, "Programme name is empty"); + throw ProgrammeException.programmeMissingName(); + } + + for (int i = 1; i < progParts.length; i++) { + String dayString = progParts[i].trim(); + Day day = parseDay(dayString); + days.add(day); + } + + logger.log(Level.INFO, "Successfully prepared CreateCommand with programme: {0}", progName); + return new CreateProgrammeCommand(progName, days); + } + + /** + * Prepares and returns a {@link ViewProgrammeCommand} to view a specific program by its index. + * + * @param argumentString The string containing the program index to view. + * @return A {@link ViewProgrammeCommand} initialized with the specified program index. + */ + private Command prepareViewCommand(String argumentString) { + if (argumentString.isEmpty()) { + argumentString = null; + } + + int progIndex = parseIndex(argumentString); + + logger.log(Level.INFO, "Successfully prepared ViewCommand"); + return new ViewProgrammeCommand(progIndex); + } + + /** + * Prepares and returns a {@link StartProgrammeCommand} to activate a specific program by its index. + * + * @param argumentString The string containing the program index to start. + * @return A {@link StartProgrammeCommand} initialized with the specified program index. + */ + private Command prepareStartCommand(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + int progIndex = parseIndex(argumentString); + + logger.log(Level.INFO, "Successfully prepared StartCommand"); + return new StartProgrammeCommand(progIndex); + } + + /** + * Prepares and returns a {@link DeleteProgrammeCommand} to remove a specific program by its index. + * + * @param argumentString The string containing the program index to delete. + * @return A {@link DeleteProgrammeCommand} initialized with the specified program index. + */ + private Command prepareDeleteCommand(String argumentString){ + if (argumentString.isEmpty()) { + argumentString = null; + } + + int progIndex = parseIndex(argumentString); + + logger.log(Level.INFO, "Successfully prepared DeleteCommand"); + return new DeleteProgrammeCommand(progIndex); + } + + //@@author andreusxcarvalho + /** + * Prepares and returns a {@link LogProgrammeCommand} to log activity in a specific program on a particular date. + * + * @param argumentString The string containing flags for the date, program index, and day index. + * @return A {@link LogProgrammeCommand} initialized with the specified date and indices. + * @throws FlagException If required flags are missing. + */ + public Command prepareLogCommand(String argumentString) { + FlagParser flagParser = new FlagParser(argumentString); + + flagParser.validateRequiredFlags(DAY_FLAG); + + LocalDate date = flagParser.getDateByFlag(DATE_FLAG); + int progIndex = flagParser.getIndexByFlag(PROGRAMME_FLAG); + int dayIndex = flagParser.getIndexByFlag(DAY_FLAG); + + logger.log(Level.INFO, "Successfully prepared LogCommand with Date: {0}, " + + "Programme index: {1}, Day index: {2}", new Object[]{progIndex, dayIndex, date}); + + return new LogProgrammeCommand(progIndex, dayIndex, date); + } + + // @@author TVageesan + /** + * Prepares and returns an appropriate {@link EditProgrammeCommand} based on the flags parsed + * from the provided argument string. + * + * @param argumentString A {@link String} containing arguments to parse. + * @return The specific {@link EditProgrammeCommand} object that corresponds to the flag detected. + * @throws FlagException If required flags are missing or unique flags are supplied more than once + */ + private EditProgrammeCommand prepareEditCommand(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + /* + * Parse by flags except for all exercise related flags, + * ignoring exercise related flags for later parsing if needed + */ + FlagParser flagParser = new FlagParser ( + argumentString, + NAME_FLAG, REPS_FLAG,SETS_FLAG,WEIGHT_FLAG,EXERCISE_FLAG,CALORIES_FLAG + ); + + // Validate that the command flags appear at most once in the argument string + flagParser.validateUniqueFlag( + UPDATE_EXERCISE_FLAG, + ADD_EXERCISE_FLAG, + REMOVE_EXERCISE_FLAG, + ADD_DAY_FLAG, + REMOVE_DAY_FLAG + ); + + if (flagParser.hasFlag(UPDATE_EXERCISE_FLAG)) { + return prepareEditExerciseCommand(flagParser); + } + + if (flagParser.hasFlag(ADD_EXERCISE_FLAG)) { + return prepareCreateExerciseCommand(flagParser); + } + + if (flagParser.hasFlag(REMOVE_EXERCISE_FLAG)) { + return prepareDeleteExerciseCommand(flagParser); + } + + if (flagParser.hasFlag(ADD_DAY_FLAG)) { + return prepareCreateDayCommand(flagParser); + } + + if (flagParser.hasFlag(REMOVE_DAY_FLAG)) { + return prepareDeleteDayCommand(flagParser); + } + + // If none of the command flags are present, throw exception + throw FlagException.missingArguments(); + } + + /** + * Creates and returns an {@link EditExerciseProgrammeCommand} for updating an exercise in a program. + * + * @author TVageesan + * @param flagParser A {@link FlagParser} containing parsed flags and arguments. + * @return An {@link EditExerciseProgrammeCommand} object to edit an existing exercise. + * @throws FlagException If required flags are missing. + */ + private EditExerciseProgrammeCommand prepareEditExerciseCommand(FlagParser flagParser) { + assert flagParser != null: "flagParser must not be null"; + + flagParser.validateRequiredFlags(DAY_FLAG); + String editString = flagParser.getStringByFlag(UPDATE_EXERCISE_FLAG); + + String[] editParts = splitArguments(editString); + int exerciseIndex = parseIndex(editParts[0]); + String exerciseString = editParts[1]; + + return new EditExerciseProgrammeCommand( + flagParser.getIndexByFlag(PROGRAMME_FLAG), + flagParser.getIndexByFlag(DAY_FLAG), + exerciseIndex, + parseExerciseUpdate(exerciseString) + ); + } + + /** + * Creates and returns a {@link CreateExerciseProgrammeCommand} for adding a new exercise to a day in a program. + * + * @param flagParser A {@link FlagParser} containing parsed flags and arguments. + * @return A {@link CreateExerciseProgrammeCommand} object to create a new exercise. + * @throws FlagException If required flags are missing. + */ + private CreateExerciseProgrammeCommand prepareCreateExerciseCommand(FlagParser flagParser) { + assert flagParser != null: "flagParser must not be null"; + + flagParser.validateRequiredFlags(DAY_FLAG); + String exerciseString = flagParser.getStringByFlag(ADD_EXERCISE_FLAG); + return new CreateExerciseProgrammeCommand( + flagParser.getIndexByFlag(PROGRAMME_FLAG), + flagParser.getIndexByFlag(DAY_FLAG), + parseExercise(exerciseString) + ); + } + + /** + * Creates and returns a {@link DeleteExerciseProgrammeCommand} for removing an exercise from a day in a program. + * + * @param flagParser A {@link FlagParser} containing parsed flags and arguments. + * @return A {@link DeleteExerciseProgrammeCommand} object to delete an existing exercise. + * @throws FlagException If required flags are missing. + */ + private DeleteExerciseProgrammeCommand prepareDeleteExerciseCommand(FlagParser flagParser) { + assert flagParser != null: "flagParser must not be null"; + + flagParser.validateRequiredFlags(DAY_FLAG, REMOVE_EXERCISE_FLAG); + return new DeleteExerciseProgrammeCommand( + flagParser.getIndexByFlag(PROGRAMME_FLAG), + flagParser.getIndexByFlag(DAY_FLAG), + flagParser.getIndexByFlag(REMOVE_EXERCISE_FLAG) + ); + } + + /** + * Creates and returns a {@link CreateDayProgrammeCommand} for adding a new day to a program. + * + * @param flagParser A {@link FlagParser} containing parsed flags and arguments. + * @return A {@link CreateDayProgrammeCommand} object to create a new day. + * @throws FlagException If required flags are missing. + */ + private CreateDayProgrammeCommand prepareCreateDayCommand(FlagParser flagParser) { + assert flagParser != null: "flagParser must not be null"; + + String dayString = flagParser.getStringByFlag(ADD_DAY_FLAG); + return new CreateDayProgrammeCommand( + flagParser.getIndexByFlag(PROGRAMME_FLAG), + parseDay(dayString) + ); + } + + /** + * Creates and returns a {@link DeleteDayProgrammeCommand} for removing a day from a program. + * + * @param flagParser A {@link FlagParser} containing parsed flags and arguments. + * @return A {@link DeleteDayProgrammeCommand} object to delete an existing day. + * @throws FlagException If required flags are missing. + */ + private DeleteDayProgrammeCommand prepareDeleteDayCommand(FlagParser flagParser) { + assert flagParser != null: "flagParser must not be null"; + + flagParser.validateRequiredFlags(REMOVE_DAY_FLAG); + return new DeleteDayProgrammeCommand( + flagParser.getIndexByFlag(PROGRAMME_FLAG), + flagParser.getIndexByFlag(REMOVE_DAY_FLAG) + ); + } + + // @@author nirala-ts + /** + * Parses a string of day related arguments and returns a Day object. + * + * @param dayString the input string representing a day and its exercises, not null. + * @return a Day object representing the parsed day and its exercises. + * @throws ProgrammeException if there are missing arguments to create a day. + */ + private Day parseDay(String dayString) { + assert dayString != null : "Day string must not be null"; + + String[] dayParts = dayString.split(EXERCISE_FLAG); + String dayName = dayParts[0].trim(); + if (dayName.isEmpty()) { + throw ProgrammeException.missingDayName(); + } + + Day day = new Day(dayName); + + for (int j = 1; j < dayParts.length; j++) { + String exerciseString = dayParts[j].trim(); + Exercise exercise = parseExercise(exerciseString); + day.insertExercise(exercise); + } + + logger.log(Level.INFO, "Parsed day successfully: {0}", dayName); + return day; + } + + // @@author nirala-ts + /** + * Parses an exercise string to create an {@link Exercise} object with required attributes. + * + * @param argumentString The string containing exercise details and flags. + * @return An {@link Exercise} object initialized with the specified attributes. + * @throws FlagException If required flags are missing. + * */ + private Exercise parseExercise(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + FlagParser flagParser = new FlagParser(argumentString); + + // Ensures the exercise contains all necessary information before creating a new Exercise + flagParser.validateRequiredFlags(SETS_FLAG, REPS_FLAG, WEIGHT_FLAG, CALORIES_FLAG, NAME_FLAG); + + return new Exercise( + flagParser.getIntegerByFlag(SETS_FLAG), + flagParser.getIntegerByFlag(REPS_FLAG), + flagParser.getIntegerByFlag(WEIGHT_FLAG), + flagParser.getIntegerByFlag(CALORIES_FLAG), + flagParser.getStringByFlag(NAME_FLAG) + ); + } + + // @@author TVageesan + /** + * Parses an exercise string to create an {@link ExerciseUpdate} object with required attributes. + * + * @param argumentString The string containing exercise details and flags. + * @return An {@link ExerciseUpdate} object initialized with the specified attributes. + * @throws FlagException If required flags are missing. + * */ + private ExerciseUpdate parseExerciseUpdate(String argumentString){ + assert argumentString != null : "Argument string must not be null"; + + FlagParser flagParser = new FlagParser(argumentString); + + return new ExerciseUpdate( + flagParser.getIntegerByFlag(SETS_FLAG), + flagParser.getIntegerByFlag(REPS_FLAG), + flagParser.getIntegerByFlag(WEIGHT_FLAG), + flagParser.getIntegerByFlag(CALORIES_FLAG), + flagParser.getStringByFlag(NAME_FLAG) + ); + } +} + diff --git a/src/main/java/parser/command/factory/WaterCommandFactory.java b/src/main/java/parser/command/factory/WaterCommandFactory.java new file mode 100644 index 0000000000..ecec32b81e --- /dev/null +++ b/src/main/java/parser/command/factory/WaterCommandFactory.java @@ -0,0 +1,100 @@ +//@@author Bev-low +package parser.command.factory; + +import command.Command; +import command.InvalidCommand; +import command.water.AddWaterCommand; +import command.water.DeleteWaterCommand; +import command.water.ViewWaterCommand; +import exceptions.WaterException; +import parser.FlagParser; + +import java.time.LocalDate; + +import static parser.FlagDefinitions.VOLUME_FLAG; +import static parser.FlagDefinitions.WATER_INDEX; +import static parser.FlagDefinitions.DATE_FLAG; +import static parser.ParserUtils.parseDate; +import static parser.ParserUtils.splitArguments; + +/** + * A factory class to create and return specific water-related commands based on input arguments. + */ +public class WaterCommandFactory { + public static final String COMMAND_WORD = "water"; + + /** + * Parses the argument string to determine which water-related command to create. + * The first word of the argument string is used to identify the specific command (e.g., add, delete, view). + * + * @param argumentString the input arguments provided by the user + * @return a {@code Command} object representing the water command to be executed + * @throws AssertionError if {@code argumentString} is null + */ + public Command parse(String argumentString) { + assert argumentString != null : "Argument string must not be null"; + + String[] inputArguments = splitArguments(argumentString); + String subCommandString = inputArguments[0]; + String arguments = inputArguments[1]; + + return switch (subCommandString) { + case AddWaterCommand.COMMAND_WORD -> prepareAddCommand(arguments); + case DeleteWaterCommand.COMMAND_WORD -> prepareDeleteCommand(arguments); + case ViewWaterCommand.COMMAND_WORD -> prepareViewCommand(arguments); + default -> new InvalidCommand(); + }; + } + + /** + * Prepares the AddWaterCommand by parsing the volume of water to be added and date. + * + * @param argumentString the argument string containing the flags and values + * @return an {@code AddWaterCommand} with the parsed water volume and date + * @throws IllegalArgumentException if required flags are missing + */ + public Command prepareAddCommand(String argumentString) { + FlagParser flagParser = new FlagParser(argumentString); + + flagParser.validateRequiredFlags(VOLUME_FLAG); + + float water = flagParser.getFloatByFlag(VOLUME_FLAG); + + if (water < 0){ + throw WaterException.volumeOutOfBounds(); + } + + LocalDate date = flagParser.getDateByFlag(DATE_FLAG); + + return new AddWaterCommand(water, date); + } + + /** + * Prepares the DeleteWaterCommand by parsing the index of water to be deleted and the date. + * + * @param argumentString the argument string containing the flags and values + * @return a {@code DeleteWaterCommand} with the parsed index and date + * @throws IllegalArgumentException if required flags are missing + */ + public Command prepareDeleteCommand(String argumentString) { + FlagParser flagParser = new FlagParser(argumentString); + + flagParser.validateRequiredFlags(WATER_INDEX); + + int waterIndexToDelete = flagParser.getIndexByFlag(WATER_INDEX); + LocalDate date = flagParser.getDateByFlag(DATE_FLAG); + + return new DeleteWaterCommand(waterIndexToDelete, date); + } + + /** + * Prepares the ViewWaterCommand to view water logs for a specific date. + * + * @param argumentString the argument string containing the date + * @return a {@code ViewWaterCommand} with the parsed date + */ + public Command prepareViewCommand(String argumentString) { + LocalDate date = parseDate(argumentString); + return new ViewWaterCommand(date); + } +} diff --git a/src/main/java/programme/Day.java b/src/main/java/programme/Day.java new file mode 100644 index 0000000000..397dc3eb1b --- /dev/null +++ b/src/main/java/programme/Day.java @@ -0,0 +1,162 @@ +// @@author Atulteja +package programme; + +import exceptions.ProgrammeException; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Represents a day in a workout programme, containing a list of exercises. + * Provides methods to add, delete, and retrieve exercises, as well as calculate the total calories burnt. + */ +public class Day { + private static final Logger logger = Logger.getLogger(Day.class.getName()); + + private final String name; + private final ArrayList exercises; + + /** + * Deep copies a new Day by copying the name and exercises from an existing Day. + * + * @param day the Day to copy + */ + public Day(Day day) { + this.name = day.name; + this.exercises = new ArrayList<>(day.exercises); + } + + /** + * Constructs a Day with a specified name and an empty list of exercises. + * + * @param name the name of the day + * @throws AssertionError if the name is null or empty + */ + public Day(String name){ + assert name != null && !name.isEmpty() : "Name cannot be null or empty"; + + this.name = name; + this.exercises = new ArrayList<>(); + + logger.log(Level.INFO, "Day created with empty exercise list: {0}", this); + } + + /** + * Constructs a Day with a specified name and a list of exercises. + * + * @param name the name of the day + * @param exercises the list of exercises for the day + * @throws AssertionError if the name is null or empty, or if exercises is null + */ + public Day(String name, ArrayList exercises) { + assert name != null && !name.isEmpty() : "Name cannot be null or empty"; + assert exercises != null : "Exercises list cannot be null"; + + this.exercises = exercises; + this.name = name; + + logger.log(Level.INFO, "Day created: {0}", this); + } + + public String getName() { + return name; + } + + public int getExercisesCount() { + logger.log(Level.INFO, "Number of exercises: {0}", exercises.size()); + return exercises.size(); + } + + /** + * Returns the exercise at the specified index. + * + * @param index the index of the exercise to retrieve + * @return the exercise at the specified index + * @throws ProgrammeException if the index is out of bounds + */ + public Exercise getExercise(int index){ + if (index < 0 || index >= exercises.size()) { + throw ProgrammeException.doesNotExist("exercise"); + } + + logger.log(Level.INFO, "Retrieving exercise at index {0}: {1}", new Object[]{index, exercises.get(index)}); + return exercises.get(index); + } + + /** + * Inserts an exercise into the day's exercise list. + * + * @param exercise the exercise to insert + * @throws AssertionError if the exercise is null + */ + public void insertExercise(Exercise exercise) { + assert exercise != null : "Exercise to insert cannot be null"; + exercises.add(exercise); + logger.log(Level.INFO, "Inserted exercise {0}", exercise); + } + + /** + * Deletes the exercise at the specified index. + * + * @param index the index of the exercise to delete + * @return the deleted exercise + * @throws ProgrammeException if the index is out of bounds + */ + public Exercise deleteExercise(int index) { + if (index < 0 || index >= exercises.size()) { + throw ProgrammeException.doesNotExist("exercise"); + } + Exercise toBeDeleted = exercises.get(index); + exercises.remove(index); + logger.log(Level.INFO, "Deleted exercise at index {0}: {1}", new Object[]{index, toBeDeleted}); + return toBeDeleted; + } + + /** + * Calculates the total calories burnt from all exercises in the day. + * + * @return the total calories burnt + */ + public int getTotalCaloriesBurnt(){ + int totalCalories = 0; + for (Exercise exercise : exercises) { + totalCalories += exercise.getCalories(); + } + return totalCalories; + } + + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(name).append("\n"); + + for (int i = 0; i < exercises.size(); i++) { + Exercise exercise = exercises.get(i); + result.append(String.format("%d. %s%n", i + 1, exercise)); + } + + return result.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(name, exercises); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Day day = (Day) o; + return Objects.equals(name, day.name) && + Objects.equals(exercises, day.exercises); + } +} diff --git a/src/main/java/programme/Exercise.java b/src/main/java/programme/Exercise.java new file mode 100644 index 0000000000..614f634339 --- /dev/null +++ b/src/main/java/programme/Exercise.java @@ -0,0 +1,182 @@ +// @@author Atulteja +package programme; + +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static common.Utils.isNull; + +/** + * Represents an exercise with sets, reps, weight, calories burned, and a name. + * Provides methods to update individual fields and retrieve key details. + */ +public class Exercise { + private static final Logger logger = Logger.getLogger(Exercise.class.getName()); + + private int sets; + private int reps; + private int weight; + private int calories; + private String name; + + + /** + * Constructs an Exercise with the specified details. + * + * @param sets the number of sets for the exercise + * @param reps the number of reps for each set + * @param weight the weight used for the exercise + * @param calories the calories burned by the exercise + * @param name the name of the exercise + */ + public Exercise(int sets, int reps, int weight, int calories, String name) { + this.sets = sets; + this.reps = reps; + this.weight = weight; + this.calories = calories; + this.name = name; + + logger.log(Level.INFO, "Exercise created: {0}", this); + } + + // @@author TVageesan + + /** + * Updates the current Exercise's fields based on the non-null values in the provided ExerciseUpdate object. + *

+ * For each non-null field in the UpdateExercise object, the corresponding field in this Exercise is updated. + *

+ * + * @param update the ExerciseUpdate containing fields to be updated in this Exercise. + */ + public void updateExercise(ExerciseUpdate update) { + assert update != null : "ExerciseUpdate object must be provided"; + + updateSets(update.sets); + updateReps(update.reps); + updateWeight(update.weight); + updateName(update.name); + updateCalories(update.calories); + } + + /** + * Updates the number of sets for this exercise. + * + * @param newSets the new number of sets; if null, the sets are not updated + */ + private void updateSets(Integer newSets) { + if (isNull(newSets)) { + return; + } + logger.log(Level.INFO, "Updating sets from {0} to {1}", new Object[]{sets, newSets}); + sets = newSets; + } + + /** + * Updates the number of reps for this exercise. + * + * @param newReps the new number of reps; if null, the reps are not updated + */ + private void updateReps(Integer newReps) { + if (isNull(newReps)) { + return; + } + logger.log(Level.INFO, "Updating reps from {0} to {1}", new Object[]{reps, newReps}); + reps = newReps; + } + + /** + * Updates the weight for this exercise. + * + * @param newWeight the new weight; if null, the weight is not updated + */ + private void updateWeight(Integer newWeight) { + if (isNull(newWeight)) { + return; + } + logger.log(Level.INFO, "Updating weight from {0} to {1}", new Object[]{weight, newWeight}); + weight = newWeight; + } + + /** + * Updates the name of this exercise. + * + * @param newName the new name; if null, the name is not updated + */ + private void updateName(String newName) { + if (isNull(newName)) { + return; + } + logger.log(Level.INFO, "Updating name from {0} to {1}", new Object[]{name, newName}); + name = newName; + } + + /** + * Updates the calories burned for this exercise. + * + * @param newCalories the new calorie count for the exercise; if null, the calories are not updated + */ + private void updateCalories(Integer newCalories) { + if (isNull(newCalories)) { + return; + } + logger.log(Level.INFO, "Updating calories from {0} to {1}", new Object[]{calories, newCalories}); + calories = newCalories; + } + + // @@author Atulteja + + public int getCalories() { + return calories; + } + + public int getWeight(){ + return weight; + } + + public String getName() { + return name; + } + + /** + * Returns a string representation of the exercise, including name, sets, reps, weight, and calories burned. + * + * @return a string representation of the exercise + */ + @Override + public String toString() { + return String.format("%s: %d sets of %d at %dkg | Burnt %d kcal", name, sets, reps, weight, calories); + } + + /** + * Returns a string representation of the exercise, sets, reps, weight. + * + * @return a string representation of the exercise for printing personal best + */ + public String toStringPb() { + return String.format("%d sets of %d at %dkg", sets, reps, weight); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Exercise exercise = (Exercise) o; + return sets == exercise.sets && + reps == exercise.reps && + weight == exercise.weight && + calories == exercise.calories && + Objects.equals(name, exercise.name); + } + + @Override + public int hashCode() { + return Objects.hash(sets, reps, weight, calories, name); + } +} diff --git a/src/main/java/programme/ExerciseUpdate.java b/src/main/java/programme/ExerciseUpdate.java new file mode 100644 index 0000000000..74bae4e24e --- /dev/null +++ b/src/main/java/programme/ExerciseUpdate.java @@ -0,0 +1,37 @@ +// @@author TVageesan + +package programme; + +/** + * Represents an immutable set of updates for an Exercise object. + *

+ * This class encapsulates the data fields to update specific attributes of an Exercise. + * Any field with a non-null value indicates a field to be updated in the target Exercise. + *

+ */ +public class ExerciseUpdate { + + protected final int sets; + protected final int reps; + protected final int weight; + protected final int calories; + protected final String name; + + /** + * Constructs an ExerciseUpdate with specified fields. Each non-null parameter + * represents a field intended to be updated in the target Exercise. + * + * @param sets the updated number of sets, or NULL_INTEGER if not updating + * @param reps the updated number of reps, or NULL_INTEGER if not updating + * @param weight the updated weight, or NULL_INTEGER if not updating + * @param calories the updated calorie count, or NULL_INTEGER if not updating + * @param name the updated name, or NULL_STRING if not updating + */ + public ExerciseUpdate(int sets, int reps, int weight, int calories, String name) { + this.sets = sets; + this.reps = reps; + this.weight = weight; + this.calories = calories; + this.name = name; + } +} diff --git a/src/main/java/programme/Programme.java b/src/main/java/programme/Programme.java new file mode 100644 index 0000000000..8cd3989650 --- /dev/null +++ b/src/main/java/programme/Programme.java @@ -0,0 +1,113 @@ +// @@author Atulteja + +package programme; + +import exceptions.ProgrammeException; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Represents a Programme consisting of multiple days, each containing exercises. + * Provides functionality to add, retrieve, and delete days, as well as access programme information. + */ +public class Programme { + private static final Logger logger = Logger.getLogger(Programme.class.getName()); + + private final String programmeName; + private final ArrayList dayList; + + /** + * Constructs a Programme with the specified name and a list of days. + * + * @param programmeName the name of the programme + * @param dayList the list of days in the programme + * @throws AssertionError if the programme name is null/empty or dayList is null + */ + public Programme(String programmeName, ArrayList dayList) { + assert programmeName != null && !programmeName.isEmpty() : "Programme name cannot be null or empty"; + assert dayList != null : "Day list cannot be null"; + + this.programmeName = programmeName; + this.dayList = dayList; + + logger.log(Level.INFO, "Programme created: {0}", this); + } + + public String getProgrammeName() { + return programmeName; + } + + /** + * Retrieves the day at the specified index. + * + * @param index the index of the day to retrieve + * @return the Day at the specified index + * @throws ProgrammeException if the index is out of bounds for the day list + */ + public Day getDay(int index){ + if (index < 0 || index >= dayList.size()) { + logger.log(Level.WARNING, "Invalid index: {0} for getDay()", index); + throw ProgrammeException.doesNotExist("day"); + } + logger.log(Level.INFO, "Retrieving day at index {0}: {1}", new Object[]{index, dayList.get(index)}); + return dayList.get(index); + } + + /** + * Inserts a new day into the programme. + * + * @param day the day to insert + * @throws AssertionError if the day is null + */ + public void insertDay(Day day) { + assert day != null : "Day to insert cannot be null"; + logger.log(Level.INFO, "Inserting day: {0}", day); + + dayList.add(day); + } + + public int getDayCount() { + logger.log(Level.INFO, "Getting day count: {0}", dayList.size()); + return dayList.size(); + } + + + /** + * Deletes the day at the specified index. + * + * @param index the index of the day to delete + * @return the Day that was deleted + * @throws ProgrammeException if the index is out of bounds for the day list + */ + public Day deleteDay(int index){ + if (index < 0 || index >= dayList.size()) { + logger.log(Level.WARNING, "Invalid index: {0} for deleteDay()", index); + throw ProgrammeException.doesNotExist("day"); + } + Day toBeDeleted = dayList.get(index); + dayList.remove(index); + logger.log(Level.INFO, "Deleted day at index {0}: {1}", new Object[]{index, toBeDeleted}); + return toBeDeleted; + } + + + /** + * Returns a string representation of the programme, including its name and all days. + * + * @return a string representation of the programme + */ + @Override + public String toString() { + StringBuilder str = new StringBuilder(); + str.append(programmeName).append("\n\n"); + + for (int i = 0; i < dayList.size(); i++) { + str.append("Day ").append(i+1).append(": ").append(dayList.get(i)).append("\n"); + } + + return str.toString(); + } +} diff --git a/src/main/java/programme/ProgrammeList.java b/src/main/java/programme/ProgrammeList.java new file mode 100644 index 0000000000..15d53db5a0 --- /dev/null +++ b/src/main/java/programme/ProgrammeList.java @@ -0,0 +1,208 @@ +// @@author Atulteja + +package programme; + +import exceptions.ProgrammeException; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static common.Utils.NULL_INTEGER; + + +/** + * Represents a list of Programmes, providing functionality to add, retrieve, delete, and start a Programme. + * Maintains the current active Programme and supports basic Programme management operations. + */ +public class ProgrammeList { + + private static final Logger logger = Logger.getLogger(ProgrammeList.class.getName()); + + int currentActiveProgramme; + private final ArrayList programmeList; + + /** + * Constructs an empty ProgrammeList. + */ + public ProgrammeList() { + programmeList = new ArrayList<>(); + currentActiveProgramme = NULL_INTEGER; + logger.log(Level.INFO, "ProgrammeList created with an empty list."); + } + + public ArrayList getProgrammeList() { + return programmeList; + } + + public int getProgrammeListSize(){ + logger.log(Level.INFO, "Getting programme list size: {0}", programmeList.size()); + return programmeList.size(); + } + + /** + * Checks if the ProgrammeList is empty. + * + * @return true if it is empty + */ + public boolean isEmpty() { + return programmeList.isEmpty(); + } + + /** + * Deactivates the current active programme by setting the active programme index to NULL_INTEGER. + * If there are no programmes in the list, it logs a warning and ensures the index remains as NULL_INTEGER. + */ + public void deactivateCurrentProgramme() { + if (programmeList.isEmpty()) { + logger.log(Level.WARNING, "Attempted to deactivate programme, but no programmes exist in the list."); + currentActiveProgramme = NULL_INTEGER; // Defensive: ensure it's set correctly even if empty + } else { + logger.log(Level.INFO, "Deactivating current active programme."); + currentActiveProgramme = NULL_INTEGER; + } + } + + /** + * Inserts a new Programme into the Programme list with the specified name and days. + * + * @param programmeName the name of the Programme + * @param days the list of days associated with the Programme + * @return the Programme that was added + */ + public Programme insertProgramme(String programmeName, ArrayList days) { + Programme programmeToAdd = new Programme(programmeName, days); + programmeList.add(programmeToAdd); + + if (programmeList.size() == 1) { + currentActiveProgramme = 0; + } + + return programmeToAdd; + } + + /** + * Deletes a Programme at the specified index or at the current active Programme if index is NULL_INTEGER. + * + * @param index the index of the Programme to delete, or NULL_INTEGER to delete the active Programme + * @return the Programme that was deleted + * @throws IndexOutOfBoundsException if the index is out of bounds for the Programme list + */ + public Programme deleteProgram(int index){ + if (index == NULL_INTEGER){ + index = currentActiveProgramme; + } + + if (index < 0 || index >= programmeList.size()) { + logger.log(Level.WARNING, "Invalid index: {0} for deleteProgram()", index); + throw ProgrammeException.doesNotExist("programme"); + } + + Programme programmeToDelete = programmeList.get(index); + programmeList.remove(index); + + if (programmeList.isEmpty()) { + deactivateCurrentProgramme(); + // currentActiveProgramme = NULL_INTEGER; + } else if (index == currentActiveProgramme) { + // Reset `currentActiveProgramme` to 0 if the deleted programme was the active one + currentActiveProgramme = 0; + } + + logger.log(Level.INFO, "Deleted programme at index {0}: {1}", new Object[]{index, programmeToDelete}); + + return programmeToDelete; + } + + /** + * Retrieves a Programme at the specified index or at the current active Programme if index is NULL_INTEGER. + * + * @param index the index of the Programme to retrieve, or NULL_INTEGER to retrieve the active Programme + * @return the Programme at the specified index + * @throws IndexOutOfBoundsException if the index is out of bounds for the Programme list + */ + public Programme getProgramme(int index){ + if (index == NULL_INTEGER){ + index = currentActiveProgramme; + } + + if(index < 0) { + throw ProgrammeException.indexOutOfBounds(); + } + + if (index >= programmeList.size()) { + logger.log(Level.WARNING, "Invalid index: {0} for getProgramme()", index); + throw ProgrammeException.doesNotExist("programme"); + } + + logger.log(Level.INFO, "Retrieving programme at index {0}: {1}", new Object[]{index, programmeList.get(index)}); + return programmeList.get(index); + } + + /** + * Retrieves the current active Programme index. + * + * @return the current active Programme index, or {@code NULL_INTEGER} if no Programme is active. + */ + public int getCurrentActiveProgramme(){ + logger.log(Level.INFO, "Retrieving index of current program: {1}", new Object[]{currentActiveProgramme}); + return currentActiveProgramme; + } + + /** + * Sets a Programme at the specified index as the current active Programme. + * + * @param startIndex the index of the Programme to start + * @return the Programme that was started + * @throws IndexOutOfBoundsException if the startIndex is out of bounds for the Programme list + */ + public Programme startProgramme(int startIndex) { + if (programmeList.isEmpty()){ + deactivateCurrentProgramme(); + //currentActiveProgramme = NULL_INTEGER; + logger.log(Level.WARNING, "Attempted to start a programme but the list is empty"); + throw ProgrammeException.programmeListEmpty(); + } + + if (startIndex < 0) { + throw ProgrammeException.indexOutOfBounds(); + } + if (startIndex >= programmeList.size()) { + logger.log(Level.WARNING, "Invalid index: {0} for startProgramme()", startIndex); + throw ProgrammeException.doesNotExist("programme"); + } + + if (currentActiveProgramme == startIndex) { + throw ProgrammeException.programmeAlreadyActive(startIndex); + } + + currentActiveProgramme = startIndex; + Programme activeProgramme = programmeList.get(currentActiveProgramme); + logger.log(Level.INFO, "Started programme at index {0}: {1}", new Object[]{startIndex, activeProgramme}); + return activeProgramme; + } + + /** + * Returns a string representation of the Programme list, indicating the active Programme. + * + * @return a string representation of the Programme list + */ + @Override + public String toString() { + if (programmeList.isEmpty()){ + return "No programmes found."; + } + + StringBuilder str = new StringBuilder(); + for (int i = 0; i < programmeList.size(); i++) { + Programme programme = programmeList.get(i); + str.append(i + 1).append(". ").append(programme.getProgrammeName()); + if (i == currentActiveProgramme) { + str.append(" -- Active"); + } + str.append("\n"); + } + return str.toString(); + } + +} 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/storage/DateSerializer.java b/src/main/java/storage/DateSerializer.java new file mode 100644 index 0000000000..38dfa6bbda --- /dev/null +++ b/src/main/java/storage/DateSerializer.java @@ -0,0 +1,51 @@ +//@@author Bev-low + +package storage; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonDeserializationContext; +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * A custom serializer and deserializer for {@link LocalDate} objects, formatting dates as + * {@code dd-MM-yyyy} for JSON serialization and deserialization. + *

+ * This class implements {@link JsonSerializer} and {@link JsonDeserializer} to convert + * {@code LocalDate} objects to and from JSON strings in a specific date format. + *

+ */ +public class DateSerializer implements JsonSerializer, JsonDeserializer { + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + /** + * Serializes a {@code LocalDate} object into a JSON string using the {@code dd-MM-yyyy} format. + * + * @param src the {@code LocalDate} to serialize, must not be {@code null} + * @param typeOfSrc the type of the source object + * @param context the serialization context + * @return a {@link JsonElement} containing the formatted date string + */ + @Override + public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) { + assert src != null; + return new JsonPrimitive(src.format(formatter)); + } + + /** + * Deserializes a JSON string into a {@code LocalDate} object using the {@code dd-MM-yyyy} format. + * + * @param json the JSON element containing the date string + * @param typeOfT the type of the object to deserialize + * @param context the deserialization context + * @return a {@code LocalDate} parsed from the JSON string + */ + @Override + public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return LocalDate.parse(json.getAsString(), formatter); + } +} diff --git a/src/main/java/storage/FileManager.java b/src/main/java/storage/FileManager.java new file mode 100644 index 0000000000..2e37fe6f5c --- /dev/null +++ b/src/main/java/storage/FileManager.java @@ -0,0 +1,176 @@ +//@@author Bev-low + +package storage; + +import com.google.gson.JsonObject; +import com.google.gson.JsonElement; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; +import exceptions.StorageException; + +import java.util.logging.Logger; +import java.util.logging.Level; + +import java.io.File; +import java.io.FileWriter; +import java.io.FileReader; +import java.io.IOException; + +/** + * Represents the FileManager system for saving and loading tasks. + * The FileManager class handles reading from and writing to the file specified by the user. + */ +public class FileManager { + private final String path; + private final Logger logger = Logger.getLogger(this.getClass().getName()); + + /** + * Constructs a FileManager with a specified file path. + * + * @param path the path to the file for storing data + */ + public FileManager(String path) { + this.path = path; + } + + /** + * Loads the JSON object containing the programme list from the data loaded by the {@link #load()} method. + *

+ * This method retrieves the "programmeList" section from the JSON data. If the "programmeList" section + * is not found, returns an empty JSON object. + * + * @return the JSON object containing the programme list if available, or an empty JSON object if not found + */ + public JsonObject loadProgrammeList() { + try { + JsonObject jsonObject = load(); + if (jsonObject == null || !jsonObject.has("programmeList")) { + logger.log(Level.INFO, "No programme list found."); + return new JsonObject(); + } + logger.log(Level.INFO, "Programme list Loaded"); + return jsonObject.getAsJsonObject("programmeList"); + } catch (RuntimeException e) { + logger.log(Level.WARNING, "Failed to load programme list: " + e.getMessage()); + return new JsonObject(); + } + } + + /** + * Loads the JSON object containing the history from the data loaded by the {@link #load()} method. + *

+ * This method retrieves the "history" section from the JSON data. If the "history" section + * is not found, returns an empty JSON object. + * + * @return the JSON object containing the history if available, or an empty JSON object if not found + */ + public JsonObject loadHistory() { + try { + JsonObject jsonObject = load(); + if (jsonObject == null || !jsonObject.has("history")) { + logger.log(Level.INFO, "No history found."); + return new JsonObject(); + } + logger.log(Level.INFO, "History Loaded"); + return jsonObject.getAsJsonObject("history"); + } catch (RuntimeException e) { + logger.log(Level.WARNING, "Failed to load history: " + e.getMessage()); + return new JsonObject(); + } + } + + /** + * Loads data from the file specified by the path and converts it to a JSON object. + *

+ * This method attempts to read the file at the specified path and parse its contents as a JSON object. + * If the file is empty or contains no data, an empty JSON object is returned. Any I/O errors encountered + * during file reading will result in a {@link StorageException}. + * + * @return a JSON object containing the data loaded from the file, or an empty JSON object if the file is empty + * @throws StorageException if an I/O error occurs during loading + */ + private JsonObject load() { + logger.info("Attempting to load data from file: " + path); + try (FileReader reader = new FileReader(path)){ + JsonElement element = JsonParser.parseReader(reader); + if(element == null || element.isJsonNull()) { + logger.info("No data found"); + return new JsonObject(); + } + logger.info("Data successfully loaded from file"); + return element.getAsJsonObject(); + } catch(Exception e){ + logger.log(Level.WARNING, "Failed to load data from file: " + path, e); + return new JsonObject(); + } + } + + /** + * Saves the specified JSON object to the file. + *

+ * This method writes the contents of the provided JSON object to the file specified by the path. + * If the directory or file does not exist, it attempts to create them before writing. The data + * is saved in a pretty-printed JSON format for readability. + * + * @param data the JSON object containing the data to be saved + * @throws IOException if an I/O error occurs during saving + */ + public void save(JsonObject data) throws IOException { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + createDirIfNotExist(); + createFileIfNotExist(); + + try (FileWriter writer = new FileWriter(path)) { + gson.toJson(data, writer); + logger.info("Data successfully saved to file."); + } catch (StorageException e) { + logger.log(Level.WARNING, "Failed to save data to file: " + path, e); + throw StorageException.unableToSave(); + } + } + + /** + * Creates the directory for the file if it does not exist. + * + * @throws IOException if the directory creation fails + */ + private void createDirIfNotExist() throws IOException { + File dir = new File(path).getParentFile(); + + if (dir == null || dir.exists()){ + logger.log(Level.INFO, "Directory exists"); + return; + } + + boolean isSuccess = dir.mkdirs(); + + if (!isSuccess){ + logger.log(Level.WARNING, "Failed to create directory."); + throw StorageException.unableToCreateDirectory(); + } + logger.log(Level.INFO, "Directory created"); + } + + /** + * Creates the file if it does not exist. + * + * @throws IOException if the file creation fails + */ + private void createFileIfNotExist() throws IOException { + File file = new File(path); + if (file.exists()) { + logger.log(Level.INFO, "File exists"); + return; + } + + boolean isSuccess = file.createNewFile(); + + if (!isSuccess) { + logger.log(Level.WARNING, "Failed to create file."); + throw StorageException.unableToCreateFile(); + } + logger.log(Level.INFO, "File created"); + } +} diff --git a/src/main/java/storage/Storage.java b/src/main/java/storage/Storage.java new file mode 100644 index 0000000000..0a43e3e8be --- /dev/null +++ b/src/main/java/storage/Storage.java @@ -0,0 +1,394 @@ +//@@author Bev-low + +package storage; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonElement; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import exceptions.ParserException; +import exceptions.StorageException; +import history.DailyRecord; +import history.History; +import programme.ProgrammeList; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static common.Utils.validate; + +/* + Storage acts as an adapter layer between the FileManager and BuffBuddy classes, + translating between JSON and programmeList or History objects +*/ +public class Storage { + + private static final Logger logger = Logger.getLogger(Storage.class.getName()); + private FileManager fileManager; + private String message; + private boolean isProgrammeListEmpty = false; + private boolean isProgrammeListCorrupted = false; + private boolean isHistoryEmpty = false; + private boolean isHistoryCorrupted = false; + /** + * Constructs a Storage object with a specified file path. + * + * @param path the path to the file used for storing data + */ + public Storage(String path) { + this.fileManager = new FileManager(path); + message = null; + } + + /** + * Loads the programme list from the JSON object obtained via the FileManager. + *

+ * This method retrieves the JSON data containing the programme list from the FileManager. If no + * programme list data is found, it initializes an empty ProgrammeList. + * + * @return the ProgrammeList object containing programme data, or an empty ProgrammeList if not found + */ + public ProgrammeList loadProgrammeList() { + try { + JsonObject programmeListJson = fileManager.loadProgrammeList(); + + if(programmeListJson == null || programmeListJson.size() == 0) { + isProgrammeListEmpty = true; + return new ProgrammeList(); + } + + try { + validateProgrammeList(programmeListJson); + } catch (ParserException e) { + throw StorageException.corruptedFile("Programme List."); + } + + logger.info("Loading programmeList"); + message = "Welcome back!"; + return programmeListFromJson(programmeListJson); + } catch (StorageException e ) { + logger.info("Programme list corrupted, empty list initialised"); + isProgrammeListCorrupted = true; + return new ProgrammeList(); + } + } + + /** + * Converts json Object containing history to from the JSON object obtained via the FileManager. + *

+ * This method retrieves the JSON data containing the history from the FileManager. If no + * history data is found, it initializes an empty History. + * + * @return the history object containing programme data, or an empty history if not found + */ + public History loadHistory() { + try { + JsonObject historyJson = fileManager.loadHistory(); + if (historyJson == null || historyJson.size() == 0) { + isHistoryEmpty = true; + return new History(); + } + + try { + validateHistory(historyJson); + } catch (ParserException e) { + throw StorageException.corruptedFile("History."); + } + + logger.info("Loading history"); + return historyFromJson(historyJson); + } catch (StorageException e) { + logger.info("history corrupted, empty history initialised"); + isHistoryCorrupted = true; + return new History(); + } + } + + /** + * Saves the programme list and history data to the file. + * + * @param programmeList the ProgrammeList object to be saved + * @param history the History object to be saved + */ + public void saveData(ProgrammeList programmeList, History history) { + assert programmeList != null : "programmeList must not be null"; + assert history != null : "history must not be null"; + + JsonObject jsonObject = createJSON(programmeList, history); + logger.info("JsonObject containing programme list and history created."); + + try{ + fileManager.save(jsonObject); + } catch (Exception e) { + logger.info("Failed to save data"); + } + } + + /** + * To set message about status of data + * + * @return String value that to tell user what is the state of the file. + */ + public String getMessage() { + if(isProgrammeListEmpty && isHistoryEmpty) { + message = "First time here, welcome to BuffBuddy!"; + } else if (isHistoryCorrupted && isProgrammeListCorrupted) { + message = "data is corrupted, initialise new ProgrammeList and History"; + } else if (isHistoryCorrupted) { + message = "History is corrupted, initialise new History, loaded ProgrammeList"; + } else if (isProgrammeListCorrupted) { + message = "ProgrammeList is corrupted, initialise new ProgrammeList, loaded History"; + } else { + message = "Welcome back!"; + } + return message; + } + + /** + * Creates a JSON object containing the programme list and history data. + * + * @param programmeList the ProgrammeList object to be added to JSON + * @param history the History object to be added to JSON + * @return a JSON object containing the programme list and history data + */ + private JsonObject createJSON(ProgrammeList programmeList, History history) { + JsonObject jsonObject = new JsonObject(); + + assert programmeList != null : "programmeList must not be null"; + assert history != null : "history must not be null"; + + jsonObject.add("programmeList", programmeListToJson(programmeList)); + logger.info("Programme list converted to JsonObject."); + jsonObject.add("history", historyToJson(history)); + logger.info("History converted to JsonObject."); + return jsonObject; + } + + /** + * Converts a ProgrammeList object to a JSON object. + * + * @param programmeList the ProgrammeList object to convert + * @return a JSON object representing the programme list + */ + private JsonObject programmeListToJson(ProgrammeList programmeList) { + Gson gson = new Gson(); + logger.log(Level.INFO, "Programme list converted to Json for saving."); + return gson.toJsonTree(programmeList).getAsJsonObject(); + } + + /** + * Converts a JSON object to a ProgrammeList object. + * + * @param jsonObject the JSON object representing the programme list + * @return the ProgrammeList object created from the JSON data + */ + private ProgrammeList programmeListFromJson(JsonObject jsonObject) { + Gson gson = new Gson(); + logger.log(Level.INFO, "Programme list converted from Json for loading."); + return gson.fromJson(jsonObject, ProgrammeList.class); + } + + /** + * Converts a History object to a JSON object. + * + * @param history the History object to convert + * @return a JSON object representing the history + */ + private JsonObject historyToJson(History history) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new DateSerializer()) // Custom serializer for LocalDate + .setPrettyPrinting() + .create(); + + JsonObject historyJson = new JsonObject(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LinkedHashMap historyMap = history.getHistory(); //To access the Hashmap + + for (LocalDate date : historyMap.keySet()) { + DailyRecord dailyRecord = historyMap.get(date); + historyJson.add(date.format(formatter), gson.toJsonTree(dailyRecord)); + } + logger.log(Level.INFO, "History converted to Json for saving."); + return historyJson; + } + + /** + * Converts a JSON object to a History object. + * + * @param jsonObject the JSON object representing the history + * @return the History object created from the JSON data + */ + private History historyFromJson(JsonObject jsonObject) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new DateSerializer()) // Custom deserializer for LocalDate + .create(); + History history = new History(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LinkedHashMap historyMap = history.getHistory(); //To access the Hashmap + + + for (Map.Entry entry : jsonObject.entrySet()) { + LocalDate date = LocalDate.parse(entry.getKey(), formatter); + DailyRecord dailyRecord = gson.fromJson(entry.getValue(), DailyRecord.class); + historyMap.put(date, dailyRecord); + } + logger.log(Level.INFO, "historyJson converted from Json for loading."); + return history; + } + + /** + * Sets the FileManager instance for testing purposes. + * + * @param mockFileManager the mocked FileManager to be used for testing. + */ + public void setFileManager(FileManager mockFileManager) { + this.fileManager = mockFileManager; + } + + /** + * Validates the given JSON object representing a programme list. + * + * @param programmeList the JSON object containing the programme data to be validated + * @throws ParserException if any validation fails for the integer, float, or string fields + */ + private void validateProgrammeList(JsonObject programmeList) { + JsonArray programmeArray = programmeList.getAsJsonArray("programmeList"); + validateProgramme(programmeArray); + } + + /** + * Validates the given JSON object representing a history. + * + * @param history the JSON object containing the history data to be validated + * @throws ParserException if any validation fails for the date, day, exercise, meal, or water fields + */ + private void validateHistory(JsonObject history) { + for (Map.Entry entry : history.entrySet()) { + String date = entry.getKey(); + JsonObject record = entry.getValue().getAsJsonObject(); + + validateDate(date); + + JsonObject day = record.getAsJsonObject("day"); + String dayName = day.get("name").getAsString(); + validate(dayName); + + JsonArray exercises = day.getAsJsonArray("exercises"); + validateExercise(exercises); + + JsonObject mealList = record.getAsJsonObject("mealList"); + JsonArray meals = mealList.getAsJsonArray("meals"); + validateMeal(meals); + + JsonObject water = record.getAsJsonObject("water"); + JsonArray waterList = water.getAsJsonArray("waterList"); + validateWater(waterList); + } + } + + /** + * Validates a date string. + * + * @param dateString the date string to be validated in the format "dd-MM-yyyy" + * @throws ParserException if the date string is invalid or cannot be parsed + */ + private void validateDate(String dateString) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + LocalDate date = LocalDate.parse(dateString, formatter); + validate(date); + } + + /** + * Validates an array of programmes. + * + * @param programmeList the JSON array of programme objects to be validated + * @throws ParserException if any validation fails for the programme or its nested day data + */ + private void validateProgramme(JsonArray programmeList) { + for (JsonElement programmeElement : programmeList) { + JsonObject programme = programmeElement.getAsJsonObject(); + String programmeName = programme.get("programmeName").getAsString(); + validate(programmeName); + + JsonArray dayList = programme.getAsJsonArray("dayList"); + validateDay(dayList); + } + + } + + /** + * Validates an array of days. + * + * @param dayList the JSON array of day objects to be validated + * @throws ParserException if any validation fails for the day or its nested exercise data + */ + private void validateDay(JsonArray dayList) { + for (JsonElement dayElement : dayList) { + JsonObject day = dayElement.getAsJsonObject(); + String dayName = day.get("name").getAsString(); + validate(dayName); + + JsonArray exercises = day.getAsJsonArray("exercises"); + validateExercise(exercises); + } + } + + + /** + * Validates an array of exercises. + * + * @param exercises the JSON array of exercise objects to be validated + * @throws ParserException if any validation fails for the sets, reps, weight, calories, or name of exercises + */ + private void validateExercise(JsonArray exercises) { + for (JsonElement exerciseElement : exercises) { + JsonObject exercise = exerciseElement.getAsJsonObject(); + int sets = exercise.get("sets").getAsInt(); + int reps = exercise.get("reps").getAsInt(); + float weight = exercise.get("weight").getAsInt(); + int calories = exercise.get("calories").getAsInt(); + String exerciseName = exercise.get("name").getAsString(); + + validate(sets); + validate(reps); + validate(weight); + validate(calories); + validate(exerciseName); + } + } + + /** + * Validates an array of meals. + * + * @param meals the JSON array of meal strings to be validated + * @throws ParserException if any validation fails for the meal name or calories + */ + private void validateMeal(JsonArray meals) { + for (JsonElement mealElement : meals) { + JsonObject mealObject = mealElement.getAsJsonObject(); + String mealName = mealObject.get("name").getAsString(); + int calories = mealObject.get("calories").getAsInt(); + + validate(mealName); + validate(calories); + } + } + + /** + * Validates an array of water amounts. + * + * @param waterList the JSON array of water amounts to be validated + * @throws ParserException if any validation fails for the water amount + */ + private void validateWater(JsonArray waterList) { + for (JsonElement waterElement : waterList) { + float waterAmount = waterElement.getAsFloat(); + validate(waterAmount); + } + } +} diff --git a/src/main/java/ui/Ui.java b/src/main/java/ui/Ui.java new file mode 100644 index 0000000000..460d580cf9 --- /dev/null +++ b/src/main/java/ui/Ui.java @@ -0,0 +1,109 @@ +// @@author nirala-ts + +package ui; + +import command.CommandResult; + +import java.io.PrintStream; +import java.util.Scanner; + +/** + * Represents the user interface for the task management system. + * This class handles user input and output, providing methods to read commands, + * display messages, and show welcome or farewell messages. + */ +public class Ui { + private static final String ERROR_HEADER = "Error: "; + private static final String LINE_CHAR = "="; + private static final int LINE_LENGTH = 50; + + private static final String PROMPT = "What can I do for you?"; + private static final String FAREWELL ="Bye. Hope to see you again soon!"; + private static final String GREETING = "Hello! I'm..."; + private static final String LOGO = """ + ____ __ __ ____ _ _ \s + | _ \\ / _|/ _| _ \\ | | | | \s + | |_) |_ _| |_| |_| |_) |_ _ __| | __| |_ _\s + | _ <| | | | _| _| _ <| | | |/ _` |/ _` | | | | + | |_) | |_| | | | | | |_) | |_| | (_| | (_| | |_| | + |____/ \\__,_|_| |_| |____/ \\__,_|\\__,_|\\__,_|\\__, | + __/ | + |___/\s + """; + private final Scanner in; + private final PrintStream out; + + /** + * Constructs an Ui object, initializing the input and output streams. + */ + public Ui() { + in = new Scanner(System.in); + out = new PrintStream(System.out); + } + + /** + * Reads a command input from the user. + * + * @return the input command as a string + */ + public String readCommand() { + return in.nextLine(); + } + + /** + * Displays a line for visual separation in the output. + */ + public void showLine() { + out.println(LINE_CHAR.repeat(LINE_LENGTH)); + } + + /** + * Displays a message to the user. + * + * @param msg the message to be displayed + */ + public void showMessage(String msg) { + showLine(); + String strippedMsg = msg.trim(); + out.println(strippedMsg); + showLine(); + } + + /** + * Displays an error message to the user. + * + * @param e the exception to be displayed + */ + public void showMessage(Exception e) { + showMessage(ERROR_HEADER + e.getMessage()); + } + + /** + * Displays the success/failure message of a Command to the user. + * + * @param result the CommandResult to be displayed + */ + public void showMessage(CommandResult result){ + showMessage(result.getMessage()); + } + + /** + * Displays a welcome message to the user. + */ + public void showWelcome() { + out.println(GREETING); + out.println(LOGO); + out.println(PROMPT); + } + + /** + * Displays a farewell message to the user. + */ + public void showFarewell() { + out.println(FAREWELL); + } + + public void showFirstTime(){ + out.println("First time here, lets get started!"); + } +} diff --git a/src/main/java/water/Water.java b/src/main/java/water/Water.java new file mode 100644 index 0000000000..c526552aca --- /dev/null +++ b/src/main/java/water/Water.java @@ -0,0 +1,96 @@ +// @@author Atulteja +package water; + +import exceptions.WaterException; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents a list of water intake records, allowing addition, deletion, and retrieval of records. + */ +public class Water { + + private static final Logger logger = Logger.getLogger(Water.class.getName()); + private final ArrayList waterList; + + /** + * Constructs a new Water instance with an empty list of water entries. + */ + public Water() { + waterList = new ArrayList<>(); + logger.log(Level.INFO, "Water instance created with an empty list."); + } + + /** + * Adds a specified amount of water to the list. + * + * @param water the amount of water in liters to add; must be positive + * @throws AssertionError if the amount of water is not positive + */ + public void addWater(float water) { + assert water > 0 : "Water amount must be positive"; + + waterList.add(water); + logger.log(Level.INFO, "{0} liters of water added. Current list: {1}", new Object[]{water, waterList}); + } + + /** + * Checks if the water list is empty. + * + * @return true if the water list contains no entries; false otherwise + */ + public boolean isEmpty() { + return waterList.isEmpty(); + } + + /** + * Deletes a water entry at the specified index. + * + * @param index the index of the water entry to delete + * @return the amount of water deleted + * @throws IndexOutOfBoundsException if the index is out of bounds for the water list + */ + public float deleteWater(int index) { + if (index < 0 || index >= waterList.size()) { + logger.log(Level.WARNING, "Invalid index for deletion: {0}", index); + throw WaterException.doesNotExist(); + } + + float waterToBeDeleted = waterList.get(index); + waterList.remove(index); + logger.log(Level.INFO, "Deleted {0} liters of water at index {1}. Current list: {2}", + new Object[]{waterToBeDeleted, index, waterList}); + return waterToBeDeleted; + } + + /** + * Retrieves the list of water entries. + * + * @return an ArrayList containing all water entries in liters + */ + public ArrayList getWaterList() { + logger.log(Level.INFO, "Retrieved water list: {0}", waterList); + return waterList; + } + + /** + * Returns a string representation of the water entries. + * + * @return a string listing all water entries or "No record" if the list is empty + */ + @Override + public String toString() { + StringBuilder output = new StringBuilder(); + + if(waterList.isEmpty()) { + return "No record."; + } + + for (int i = 0; i < waterList.size(); i++) { + output.append(i + 1).append(": ").append(waterList.get(i)).append("\n"); + } + + return output.toString().trim(); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/BuffBuddyTest.java similarity index 81% rename from src/test/java/seedu/duke/DukeTest.java rename to src/test/java/BuffBuddyTest.java index 2dda5fd651..17c984b807 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/BuffBuddyTest.java @@ -1,10 +1,7 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; -class DukeTest { +public class BuffBuddyTest { @Test public void sampleTest() { assertTrue(true); diff --git a/src/test/java/command/CommandResultTest.java b/src/test/java/command/CommandResultTest.java new file mode 100644 index 0000000000..00f0d352d1 --- /dev/null +++ b/src/test/java/command/CommandResultTest.java @@ -0,0 +1,33 @@ +package command; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class CommandResultTest { + + @Test + public void testCommandResult_initialization() { + String message = "Operation successful"; + CommandResult commandResult = new CommandResult(message); + + assertNotNull(commandResult, "CommandResult should be initialized"); + assertEquals(message, commandResult.getMessage(), "The message should match the initialized value"); + } + + @Test + public void testCommandResult_emptyMessage() { + CommandResult commandResult = new CommandResult(""); + + assertNotNull(commandResult, "CommandResult should be initialized"); + assertEquals("", commandResult.getMessage(), "The message should be an empty string"); + } + + @Test + public void testCommandResult_nullMessage() { + CommandResult commandResult = new CommandResult(null); + + assertNotNull(commandResult, "CommandResult should be initialized"); + assertEquals(null, commandResult.getMessage(), "The message should be null"); + } +} diff --git a/src/test/java/command/CommandTest.java b/src/test/java/command/CommandTest.java new file mode 100644 index 0000000000..c56e83a0b3 --- /dev/null +++ b/src/test/java/command/CommandTest.java @@ -0,0 +1,29 @@ +package command; + +import history.History; +import org.junit.jupiter.api.Test; +import programme.ProgrammeList; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + +public class CommandTest { + + private static class TestCommand extends Command { + @Override + public CommandResult execute(ProgrammeList programmes, History history) { + return new CommandResult("Test executed"); + } + } + + @Test + public void testExecute_returnsCommandResult() { + ProgrammeList mockProgrammes = mock(ProgrammeList.class); + History mockHistory = mock(History.class); + + TestCommand testCommand = new TestCommand(); + CommandResult result = testCommand.execute(mockProgrammes, mockHistory); + + assertNotNull(result, "The execute method should return a non-null CommandResult"); + } +} diff --git a/src/test/java/command/ExitCommandTest.java b/src/test/java/command/ExitCommandTest.java new file mode 100644 index 0000000000..e76d608947 --- /dev/null +++ b/src/test/java/command/ExitCommandTest.java @@ -0,0 +1,39 @@ +package command; + +import history.History; +import org.junit.jupiter.api.Test; +import programme.ProgrammeList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mock; + +public class ExitCommandTest { + + @Test + public void testExecute_doesNotThrowException() { + ProgrammeList mockProgrammes = mock(ProgrammeList.class); + History mockHistory = mock(History.class); + + ExitCommand exitCommand = new ExitCommand(); + + assertDoesNotThrow(() -> exitCommand.execute(mockProgrammes, mockHistory)); + } + + @Test + public void testExecute_withNullProgrammeList() { + History mockHistory = mock(History.class); + + ExitCommand exitCommand = new ExitCommand(); + + assertDoesNotThrow(() -> exitCommand.execute(null, mockHistory)); + } + + @Test + public void testExecute_withNullHistory() { + ProgrammeList mockProgrammes = mock(ProgrammeList.class); + + ExitCommand exitCommand = new ExitCommand(); + + assertDoesNotThrow( () -> exitCommand.execute(mockProgrammes, null)); + } +} diff --git a/src/test/java/command/InvalidCommandTest.java b/src/test/java/command/InvalidCommandTest.java new file mode 100644 index 0000000000..1b8859142a --- /dev/null +++ b/src/test/java/command/InvalidCommandTest.java @@ -0,0 +1,22 @@ +package command; + +import history.History; +import org.junit.jupiter.api.Test; +import programme.ProgrammeList; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + + +public class InvalidCommandTest { + + @Test + public void testExecute_showsInvalidCommandMessage() { + ProgrammeList mockprogrammes = mock(ProgrammeList.class); + History mockHistory = mock(History.class); + + InvalidCommand invalidCommand = new InvalidCommand(); + CommandResult result = invalidCommand.execute(mockprogrammes, mockHistory); + assertNotNull(result); + } +} diff --git a/src/test/java/command/history/DeleteHistoryCommandTest.java b/src/test/java/command/history/DeleteHistoryCommandTest.java new file mode 100644 index 0000000000..220ed4e850 --- /dev/null +++ b/src/test/java/command/history/DeleteHistoryCommandTest.java @@ -0,0 +1,72 @@ +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static common.Utils.formatDate; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DeleteHistoryCommandTest { + + private DeleteHistoryCommand deleteHistoryCommand; + private History history; + private LocalDate testDate; + + @BeforeEach + public void setUp() { + history = new History(); + testDate = LocalDate.now(); + deleteHistoryCommand = new DeleteHistoryCommand(testDate); + } + + @Test + public void testExecuteHappyPathDeleteExistingRecord() { + // Happy Path: Record exists and is deleted successfully + DailyRecord dailyRecord = new DailyRecord(); + history.logRecord(testDate, dailyRecord); + + CommandResult expectedResult = new CommandResult("Deleted record: \n" + dailyRecord.toString()); + + // Act + CommandResult result = deleteHistoryCommand.execute(history); + + // Assert + assertEquals(expectedResult, result, "Execution should return the deleted record for the specified date."); + } + + @Test + public void testExecuteEdgeCaseNoRecordForDate() { + // Edge Case: No record exists for the specified date + CommandResult expectedResult = new CommandResult("No record found at " + formatDate(testDate)); + + // Act + CommandResult result = deleteHistoryCommand.execute(history); + + // Assert + assertEquals(expectedResult, result, + "Execution should return 'No record found' when no record exists for the specified date."); + } + + @Test + public void testExecuteEdgeCaseRecordExistsForDifferentDate() { + // Edge Case: Record exists, but for a different date + LocalDate differentDate = testDate.minusDays(1); + DailyRecord dailyRecord = new DailyRecord(); + history.logRecord(differentDate, dailyRecord); + + CommandResult expectedResult = new CommandResult("No record found at " + formatDate(testDate)); + + // Act + CommandResult result = deleteHistoryCommand.execute(history); + + // Assert + assertEquals(expectedResult, result, + "Execution should return 'No record found' if the record exists only for a different date."); + } +} + diff --git a/src/test/java/command/history/ListHistoryCommandTest.java b/src/test/java/command/history/ListHistoryCommandTest.java new file mode 100644 index 0000000000..cfc60bd81e --- /dev/null +++ b/src/test/java/command/history/ListHistoryCommandTest.java @@ -0,0 +1,76 @@ +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.ProgrammeList; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ListHistoryCommandTest { + + private ListHistoryCommand listHistoryCommand; + private History history; + private ProgrammeList programmeList; + + @BeforeEach + public void setUp() { + listHistoryCommand = new ListHistoryCommand(); + history = new History(); + programmeList = new ProgrammeList(); // Assuming ProgrammeList is initialized this way + } + + @Test + public void testExecuteHappyPathSingleRecord() { + // Happy Path: Single Record in History + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + history.logRecord(date, dailyRecord); + + CommandResult expectedResult = new CommandResult(history.toString()); + + // Act + CommandResult result = listHistoryCommand.execute(programmeList, history); + + // Assert + assertEquals(expectedResult, result, "Execution with a single record should output the correct history."); + } + + @Test + public void testExecuteEdgeCaseEmptyHistory() { + // Edge Case: Empty History + CommandResult expectedResult = new CommandResult("No history available."); + + // Act + CommandResult result = listHistoryCommand.execute(programmeList, history); + + // Assert + assertEquals(expectedResult, result, "Execution with an empty history should return 'No history available.'"); + } + + @Test + public void testExecuteEdgeCaseMultipleRecordsInHistory() { + // Edge Case: Multiple Records in History + LocalDate date1 = LocalDate.now().minusDays(1); + LocalDate date2 = LocalDate.now(); + + DailyRecord record1 = new DailyRecord(); + DailyRecord record2 = new DailyRecord(); + + history.logRecord(date1, record1); + history.logRecord(date2, record2); + + CommandResult expectedResult = new CommandResult(history.toString()); + + // Act + CommandResult result = listHistoryCommand.execute(programmeList, history); + + // Assert + assertEquals(expectedResult, result, "Execution with multiple records should output the correct history."); + } +} + diff --git a/src/test/java/command/history/ListPersonalBestsCommandTest.java b/src/test/java/command/history/ListPersonalBestsCommandTest.java new file mode 100644 index 0000000000..3a171a4ade --- /dev/null +++ b/src/test/java/command/history/ListPersonalBestsCommandTest.java @@ -0,0 +1,90 @@ +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.ProgrammeList; +import programme.Exercise; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ListPersonalBestsCommandTest { + + private ListPersonalBestsCommand listPersonalBestsCommand; + private History history; + private ProgrammeList programmeList; + + @BeforeEach + public void setUp() { + listPersonalBestsCommand = new ListPersonalBestsCommand(); + history = new History(); + programmeList = new ProgrammeList(); // Assuming ProgrammeList can be initialized this way + } + + @Test + public void testExecuteHappyPathSingleExercise() { + // Happy Path: Single Exercise Record + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + Exercise exercise = new Exercise(3, 12, 50, 200, "Bench Press"); + dailyRecord.logDayToRecord(new programme.Day("Day 1")); + dailyRecord.getDayFromRecord().insertExercise(exercise); + history.logRecord(date, dailyRecord); + + CommandResult expectedResult = new CommandResult(history.getFormattedPersonalBests()); + + // Act + CommandResult result = listPersonalBestsCommand.execute(programmeList, history); + + // Assert + assertEquals(expectedResult, result, + "Execution with a single exercise should return the correct personal best."); + } + + @Test + public void testExecuteEdgeCaseNoExercises() { + // Edge Case: No Exercises in History + CommandResult expectedResult = new CommandResult("No personal bests found."); + + // Act + CommandResult result = listPersonalBestsCommand.execute(programmeList, history); + + // Assert + assertEquals(expectedResult, result, "Execution with no exercises should return 'No personal bests found.'"); + } + + @Test + public void testExecuteEdgeCaseMultipleExercises() { + // Edge Case: Multiple Exercises with different dates + LocalDate date1 = LocalDate.now().minusDays(1); + LocalDate date2 = LocalDate.now(); + + DailyRecord record1 = new DailyRecord(); + DailyRecord record2 = new DailyRecord(); + + Exercise exercise1 = new Exercise(3, 12, 60, 250, "Squat"); + Exercise exercise2 = new Exercise(4, 10, 70, 300, "Deadlift"); + + record1.logDayToRecord(new programme.Day("Day 1")); + record1.getDayFromRecord().insertExercise(exercise1); + record2.logDayToRecord(new programme.Day("Day 2")); + record2.getDayFromRecord().insertExercise(exercise2); + + history.logRecord(date1, record1); + history.logRecord(date2, record2); + + CommandResult expectedResult = new CommandResult(history.getFormattedPersonalBests()); + + // Act + CommandResult result = listPersonalBestsCommand.execute(programmeList, history); + + // Assert + assertEquals(expectedResult, result, + "Execution with multiple exercises should return the correct personal bests."); + } +} + diff --git a/src/test/java/command/history/ViewHistoryCommandTest.java b/src/test/java/command/history/ViewHistoryCommandTest.java new file mode 100644 index 0000000000..8e7252e975 --- /dev/null +++ b/src/test/java/command/history/ViewHistoryCommandTest.java @@ -0,0 +1,72 @@ +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static common.Utils.formatDate; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ViewHistoryCommandTest { + + private ViewHistoryCommand viewHistoryCommand; + private History history; + private LocalDate testDate; + + @BeforeEach + public void setUp() { + history = new History(); + testDate = LocalDate.now(); + viewHistoryCommand = new ViewHistoryCommand(testDate); + } + + @Test + public void testExecuteHappyPathSingleRecordForDate() { + // Happy Path: Record exists for the specified date + DailyRecord dailyRecord = new DailyRecord(); + history.logRecord(testDate, dailyRecord); + + CommandResult expectedResult = new CommandResult(dailyRecord.toString()); + + // Act + CommandResult result = viewHistoryCommand.execute(history); + + // Assert + assertEquals(expectedResult, result, "Execution should return the correct record for the specified date."); + } + + @Test + public void testExecuteEdgeCaseNoRecordForDate() { + // Edge Case: No record exists for the specified date + CommandResult expectedResult = new CommandResult("No record found for " + formatDate(testDate)); + + // Act + CommandResult result = viewHistoryCommand.execute(history); + + // Assert + assertEquals(expectedResult, result, + "Execution should return 'No record found' when no record exists for the specified date."); + } + + @Test + public void testExecuteEdgeCaseRecordExistsForDifferentDate() { + // Edge Case: Record exists, but for a different date + LocalDate differentDate = testDate.minusDays(1); + DailyRecord dailyRecord = new DailyRecord(); + history.logRecord(differentDate, dailyRecord); + + CommandResult expectedResult = new CommandResult("No record found for " + formatDate(testDate)); + + // Act + CommandResult result = viewHistoryCommand.execute(history); + + // Assert + assertEquals(expectedResult, result, + "Execution should return 'No record found' if the record exists only for a different date."); + } +} + diff --git a/src/test/java/command/history/ViewPersonalBestCommandTest.java b/src/test/java/command/history/ViewPersonalBestCommandTest.java new file mode 100644 index 0000000000..621f882a0c --- /dev/null +++ b/src/test/java/command/history/ViewPersonalBestCommandTest.java @@ -0,0 +1,81 @@ +package command.history; + +import command.CommandResult; +import history.History; +import programme.ProgrammeList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ViewPersonalBestCommandTest { + + private ProgrammeList programmeList; + private History history; + + @BeforeEach + public void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + } + + @Test + public void testExecuteHappyPathSingleExercise() { + // Arrange + String exerciseName = "Bench Press"; + history.getRecordByDate(java.time.LocalDate.now()).logDayToRecord(new programme.Day("Day 1")); + history.getRecordByDate(java.time.LocalDate.now()).getDayFromRecord().insertExercise( + new programme.Exercise(3, 12, 50, 200, exerciseName) + ); + + ViewPersonalBestCommand command = new ViewPersonalBestCommand(exerciseName); + + // Act + CommandResult result = command.execute(programmeList, history); + + // Assert + String expectedMessage = "Personal best for " + exerciseName + ": 3 sets of 12 at 50kg"; + assertEquals(expectedMessage, result.getMessage(), "Execution should return correct personal best message."); + } + + @Test + public void testExecuteEdgeCaseExerciseNotFound() { + // Arrange + String exerciseName = "Squat"; + ViewPersonalBestCommand command = new ViewPersonalBestCommand(exerciseName); + + // Act + CommandResult result = command.execute(programmeList, history); + + // Assert + String expectedMessage = "No personal best found for " + exerciseName; + assertEquals(expectedMessage, result.getMessage(), + "Execution should return message indicating no personal best found."); + } + + @Test + public void testExecuteEdgeCaseSimilarExerciseNames() { + // Arrange + String exerciseName1 = "Bench Press"; + String exerciseName2 = "Bench Press Incline"; + + history.getRecordByDate(java.time.LocalDate.now()).logDayToRecord(new programme.Day("Day 1")); + history.getRecordByDate(java.time.LocalDate.now()).getDayFromRecord().insertExercise( + new programme.Exercise(3, 12, 50, 200, exerciseName1) + ); + history.getRecordByDate(java.time.LocalDate.now()).getDayFromRecord().insertExercise( + new programme.Exercise(3, 10, 45, 180, exerciseName2) + ); + + ViewPersonalBestCommand command = new ViewPersonalBestCommand(exerciseName2); + + // Act + CommandResult result = command.execute(programmeList, history); + + // Assert + String expectedMessage = "Personal best for " + exerciseName2 + ": 3 sets of 10 at 45kg"; + assertEquals(expectedMessage, result.getMessage(), + "Execution should return the correct personal best for a similarly named exercise."); + } +} + diff --git a/src/test/java/command/history/WeeklySummaryCommandTest.java b/src/test/java/command/history/WeeklySummaryCommandTest.java new file mode 100644 index 0000000000..ff538d7ecf --- /dev/null +++ b/src/test/java/command/history/WeeklySummaryCommandTest.java @@ -0,0 +1,81 @@ +package command.history; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import programme.ProgrammeList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WeeklySummaryCommandTest { + + private ProgrammeList programmeList; + private History history; + private WeeklySummaryCommand weeklySummaryCommand; + + @BeforeEach + public void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + weeklySummaryCommand = new WeeklySummaryCommand(); + } + + @Test + public void testExecuteHappyPathWithWeeklyData() { + // Arrange + LocalDate today = LocalDate.now(); + LocalDate threeDaysAgo = today.minusDays(3); + DailyRecord todayRecord = new DailyRecord(); + DailyRecord threeDaysAgoRecord = new DailyRecord(); + + todayRecord.logDayToRecord(new programme.Day("Day 1")); + todayRecord.getDayFromRecord().insertExercise(new programme.Exercise(3, 12, 50, 200, "Bench Press")); + + threeDaysAgoRecord.logDayToRecord(new programme.Day("Day 2")); + threeDaysAgoRecord.getDayFromRecord().insertExercise(new programme.Exercise(3, 10, 45, 150, "Squat")); + + history.logRecord(today, todayRecord); + history.logRecord(threeDaysAgo, threeDaysAgoRecord); + + // Act + CommandResult result = weeklySummaryCommand.execute(programmeList, history); + + // Assert + String expectedMessage = "Your weekly workout summary: \n" + history.getWeeklyWorkoutSummary(); + assertEquals(expectedMessage, result.getMessage(), "Execution should return correct weekly workout summary."); + } + + @Test + public void testExecuteEdgeCaseNoWorkoutHistory() { + // Act + CommandResult result = weeklySummaryCommand.execute(programmeList, history); + + // Assert + String expectedMessage = "Your weekly workout summary: \nNo workout history available."; + assertEquals(expectedMessage, result.getMessage(), "Execution should indicate no workout history available."); + } + + @Test + public void testExecuteEdgeCaseOnlyOldRecords() { + // Arrange + LocalDate tenDaysAgo = LocalDate.now().minusDays(10); + DailyRecord oldRecord = new DailyRecord(); + + oldRecord.logDayToRecord(new programme.Day("Day 1")); + oldRecord.getDayFromRecord().insertExercise(new programme.Exercise(3, 10, 40, 120, "Deadlift")); + + history.logRecord(tenDaysAgo, oldRecord); + + // Act + CommandResult result = weeklySummaryCommand.execute(programmeList, history); + + // Assert + String expectedMessage = "Your weekly workout summary: \nNo workout history available for the past week."; + assertEquals(expectedMessage, result.getMessage(), + "Execution should indicate no recent workout history available."); + } +} diff --git a/src/test/java/command/meals/AddMealCommandTest.java b/src/test/java/command/meals/AddMealCommandTest.java new file mode 100644 index 0000000000..97f862463a --- /dev/null +++ b/src/test/java/command/meals/AddMealCommandTest.java @@ -0,0 +1,79 @@ +package command.meals; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import meal.Meal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +public class AddMealCommandTest { + + private AddMealCommand addMealCommand; + private Meal sampleMeal; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private DailyRecord mockDailyRecord; + + @BeforeEach + public void setUp() { + // Initialize the mocks + MockitoAnnotations.openMocks(this); + + sampleMeal = new Meal("Sample Meal", 500); + date = LocalDate.now(); + + // Set up History mock to return a DailyRecord for the specified date + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + addMealCommand = new AddMealCommand(sampleMeal, date); + } + + @Test + public void testExecuteHappyPath() { + // Arrange + CommandResult expectedResult = new CommandResult(sampleMeal.toString() + " has been added"); + + // Act + CommandResult result = addMealCommand.execute(mockHistory); + + // Assert + verify(mockDailyRecord).addMealToRecord(sampleMeal); + assertEquals(expectedResult, result, "Execution should " + + "return a CommandResult with the correct success message."); + } + + @Test + public void testExecuteEdgeCaseNullDailyRecord() { + // Set up History mock to return null for the DailyRecord + when(mockHistory.getRecordByDate(date)).thenReturn(null); + + // If assertions are enabled, an AssertionError is expected; otherwise, + // replace with a custom exception if defined + assertThrows(AssertionError.class, () -> addMealCommand.execute(mockHistory), "Executing " + + "AddMealCommand without a valid DailyRecord should throw an AssertionError."); + } + + @Test + public void testExecuteEdgeCaseNullMeal() { + // Attempting to create a command with a null meal should throw an AssertionError + // If a specific custom exception is defined for null meal, replace AssertionError with it + assertThrows(AssertionError.class, () -> new AddMealCommand(null, date), "Creating " + + "AddMealCommand with null Meal should throw AssertionError."); + } +} + + diff --git a/src/test/java/command/meals/DeleteMealCommandTest.java b/src/test/java/command/meals/DeleteMealCommandTest.java new file mode 100644 index 0000000000..3f2b9b93be --- /dev/null +++ b/src/test/java/command/meals/DeleteMealCommandTest.java @@ -0,0 +1,88 @@ +package command.meals; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import meal.Meal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +public class DeleteMealCommandTest { + + private DeleteMealCommand deleteMealCommand; + private Meal sampleMeal; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private DailyRecord mockDailyRecord; + + @BeforeEach + public void setUp() { + // Initialize the mocks + MockitoAnnotations.openMocks(this); + + sampleMeal = new Meal("Sample Meal", 500); + date = LocalDate.now(); + + // Set up History mock to return a DailyRecord for the specified date + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + // Set up DailyRecord mock to return the sample meal when deleting by index + when(mockDailyRecord.deleteMealFromRecord(0)).thenReturn(sampleMeal); + + deleteMealCommand = new DeleteMealCommand(0, date); + } + + @Test + public void testExecuteHappyPath() { + // Arrange + CommandResult expectedResult = new CommandResult(sampleMeal + " has been deleted"); + + // Act + CommandResult result = deleteMealCommand.execute(mockHistory); + + // Assert + verify(mockDailyRecord).deleteMealFromRecord(0); + assertEquals(expectedResult, result, "Execution should " + + "return a CommandResult with the correct success message."); + } + + @Test + public void testExecuteEdgeCaseNullDailyRecord() { + // Set up History mock to return null for the DailyRecord + when(mockHistory.getRecordByDate(date)).thenReturn(null); + + assertThrows(AssertionError.class, () -> deleteMealCommand.execute(mockHistory), "Executing " + + "DeleteMealCommand without a valid DailyRecord should throw an AssertionError."); + } + + @Test + public void testExecuteEdgeCaseInvalidIndex() { + // Set up DailyRecord mock to throw IndexOutOfBoundsException when an invalid index is used + when(mockDailyRecord.deleteMealFromRecord(5)).thenThrow(new IndexOutOfBoundsException("Invalid index")); + + DeleteMealCommand invalidIndexCommand = new DeleteMealCommand(5, date); + + assertThrows(IndexOutOfBoundsException.class, () -> invalidIndexCommand.execute(mockHistory), "Executing " + + "DeleteMealCommand with an invalid index should throw IndexOutOfBoundsException."); + } + + @Test + public void testConstructorEdgeCaseNegativeIndex() { + // Attempting to create a command with a negative index should throw an AssertionError + assertThrows(AssertionError.class, () -> new DeleteMealCommand(-1, date), "Creating " + + "DeleteMealCommand with negative index should throw AssertionError."); + } +} diff --git a/src/test/java/command/meals/MealCommandTest.java b/src/test/java/command/meals/MealCommandTest.java new file mode 100644 index 0000000000..be5f7b17af --- /dev/null +++ b/src/test/java/command/meals/MealCommandTest.java @@ -0,0 +1,73 @@ +package command.meals; + +import command.CommandResult; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import programme.ProgrammeList; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; + +public class MealCommandTest { + + private MealCommand mealCommand; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private ProgrammeList mockProgrammeList; + + @BeforeEach + public void setUp() { + //initializes the @Mock annotated fields in the test class + MockitoAnnotations.openMocks(this); + date = LocalDate.now(); + mealCommand = new TestMealCommand(date); // Using a concrete subclass for testing + } + + @Test + public void testConstructorHappyPath() { + assertEquals(date, mealCommand.date, "Date should be " + + "initialized correctly in MealCommand."); + } + + @Test + public void testConstructorEdgeCaseNullDate() { + assertThrows(AssertionError.class, () -> new TestMealCommand(null), "Creating " + + "MealCommand with null date should throw an AssertionError."); + } + + @Test + public void testExecuteWithProgrammeListAndHistoryHappyPath() { + CommandResult expected = new CommandResult("Executed with history"); + + CommandResult result = mealCommand.execute(mockProgrammeList, mockHistory); + + assertEquals(expected, result, "execute should return a " + + "CommandResult with the correct message."); + // Assuming some method interaction with mockHistory in the subclass + verify(mockHistory).getRecordByDate(date); + } + + // Concrete subclass of MealCommand for testing purposes + private static class TestMealCommand extends MealCommand { + public TestMealCommand(LocalDate date) { + super(date); + } + + @Override + public CommandResult execute(History history) { + // Ensure getRecordByDate is called as part of the execution + history.getRecordByDate(date); + return new CommandResult("Executed with history"); + } + } +} diff --git a/src/test/java/command/meals/ViewMealCommandTest.java b/src/test/java/command/meals/ViewMealCommandTest.java new file mode 100644 index 0000000000..10ee523d96 --- /dev/null +++ b/src/test/java/command/meals/ViewMealCommandTest.java @@ -0,0 +1,83 @@ +package command.meals; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import meal.MealList; +import java.time.format.DateTimeFormatter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +public class ViewMealCommandTest { + + private ViewMealCommand viewMealCommand; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private DailyRecord mockDailyRecord; + + @Mock + private MealList mockMealList; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + date = LocalDate.now(); + + // Set up History mock to return a DailyRecord for the specified date + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + // Set up DailyRecord mock to return a MealList + when(mockDailyRecord.getMealListFromRecord()).thenReturn(mockMealList); + + viewMealCommand = new ViewMealCommand(date); + } + + @Test + public void testExecuteHappyPath() { + // Arrange + when(mockMealList.toString()).thenReturn("Sample Meal List"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + String formattedDate = date.format(formatter); + + CommandResult expectedResult = new CommandResult("Meals for " + formattedDate + ": \n\nSample Meal List"); + + // Act + CommandResult result = viewMealCommand.execute(mockHistory); + + // Assert + verify(mockHistory).getRecordByDate(date); + verify(mockDailyRecord).getMealListFromRecord(); + assertEquals(expectedResult, result, "Execution should return a " + + "CommandResult with the correct meal list output."); + } + + @Test + public void testExecuteEdgeCaseNullDailyRecord() { + // Arrange + when(mockHistory.getRecordByDate(date)).thenReturn(null); + + // Act & Assert + assertThrows(AssertionError.class, () -> viewMealCommand.execute(mockHistory), "Executing " + + "ViewMealCommand with a null DailyRecord should throw an AssertionError."); + } + + @Test + public void testConstructorEdgeCaseNullDate() { + // Act & Assert + assertThrows(AssertionError.class, () -> new ViewMealCommand(null), "Creating " + + "ViewMealCommand with a null date should throw an AssertionError."); + } +} diff --git a/src/test/java/command/programme/CreateProgrammeCommandTest.java b/src/test/java/command/programme/CreateProgrammeCommandTest.java new file mode 100644 index 0000000000..e731b1f945 --- /dev/null +++ b/src/test/java/command/programme/CreateProgrammeCommandTest.java @@ -0,0 +1,65 @@ +// @@author nirala-ts + +package command.programme; + +import command.CommandResult; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CreateProgrammeCommandTest { + + private static final String VALID_PROGRAMME_NAME = "New Programme"; + private static final String EMPTY_PROGRAMME_NAME = ""; + private static final ArrayList VALID_PROGRAMME_CONTENTS = new ArrayList<>(); + private static final ArrayList NULL_PROGRAMME_CONTENTS = null; + private ProgrammeList programmeList; + private History history; + + @BeforeEach + void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + } + + @Test + void constructor_initializesWithValidParameters(){ + assertDoesNotThrow(() -> new CreateProgrammeCommand(VALID_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS)); + } + + @Test + void constructor_throwsAssertionErrorIfProgrammesIsNull() { + assertThrows(AssertionError.class, () -> new CreateProgrammeCommand( + VALID_PROGRAMME_NAME, NULL_PROGRAMME_CONTENTS) + ); + } + + @Test + void constructor_createsProgrammeWithEmptyName() { + assertThrows(AssertionError.class, () -> new CreateProgrammeCommand( + EMPTY_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS) + ); + } + + @Test + void execute_createsProgrammeSuccessfully_returnsSuccessMessage() { + CreateProgrammeCommand command = new CreateProgrammeCommand(VALID_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS); + String expectedMessage = String.format(CreateProgrammeCommand.SUCCESS_MESSAGE_FORMAT, + new Programme(VALID_PROGRAMME_NAME, VALID_PROGRAMME_CONTENTS) + ); + CommandResult expectedResult = new CommandResult(expectedMessage); + + CommandResult actualResult = command.execute(programmeList, history); + assertEquals(expectedResult, actualResult); + } +} + diff --git a/src/test/java/command/programme/DeleteProgrammeCommandTest.java b/src/test/java/command/programme/DeleteProgrammeCommandTest.java new file mode 100644 index 0000000000..e41790fa5b --- /dev/null +++ b/src/test/java/command/programme/DeleteProgrammeCommandTest.java @@ -0,0 +1,69 @@ +// @@author nirala-ts + +package command.programme; + +import command.CommandResult; +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.ProgrammeList; +import programme.Day; +import programme.Programme; +import history.History; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DeleteProgrammeCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int INVALID_PROGRAMME_ID = -2; + private static final int OUT_OF_RANGE_PROGRAMME_ID = 999; + + private ProgrammeList programmeList; + private Programme expectedProgramme; + private DeleteProgrammeCommand command; + private History history; + + @BeforeEach + void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + Day day = new Day("Day 1", new ArrayList<>()); + ArrayList days = new ArrayList<>(); + days.add(day); + programmeList.insertProgramme("Mock Programme", days); + + expectedProgramme = new Programme("Mock Programme", days); + command = new DeleteProgrammeCommand(VALID_PROGRAMME_ID); + } + + @Test + void constructor_initializesWithValidParameters() { + assertDoesNotThrow(() -> new DeleteProgrammeCommand(VALID_PROGRAMME_ID)); + } + + @Test + void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() { + assertThrows(AssertionError.class, () -> new DeleteProgrammeCommand(INVALID_PROGRAMME_ID)); + } + + @Test + void execute_deletesDayFromProgramme_returnsSuccessMessage() { + String expectedMessage = String.format(DeleteProgrammeCommand.SUCCESS_MESSAGE_FORMAT, expectedProgramme); + CommandResult expectedResult = new CommandResult(expectedMessage); + + CommandResult actualResult = command.execute(programmeList, history); + assertEquals(expectedResult, actualResult); + } + + @Test + void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() { + DeleteProgrammeCommand invalidCommand = new DeleteProgrammeCommand(OUT_OF_RANGE_PROGRAMME_ID); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList, history), + "Expected IndexOutOfBoundsBuffBuddyException for an out-of-range programme ID"); + } +} diff --git a/src/test/java/command/programme/ListProgrammeCommandTest.java b/src/test/java/command/programme/ListProgrammeCommandTest.java new file mode 100644 index 0000000000..9843400a99 --- /dev/null +++ b/src/test/java/command/programme/ListProgrammeCommandTest.java @@ -0,0 +1,67 @@ +// @@author nirala-ts + +package command.programme; + +import command.CommandResult; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ListProgrammeCommandTest { + + private ProgrammeList programmeList; + private History history; + private ListProgrammeCommand command; + + @BeforeEach + void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + command = new ListProgrammeCommand(); + + // Add some programmes to the list for testing + ArrayList days1 = new ArrayList<>(); + days1.add(new Day("Day 1", new ArrayList<>())); + programmeList.insertProgramme("Programme 1", days1); + + ArrayList days2 = new ArrayList<>(); + days2.add(new Day("Day 2", new ArrayList<>())); + programmeList.insertProgramme("Programme 2", days2); + } + + @Test + void execute_returnsListOfProgrammes() { + CommandResult result = command.execute(programmeList, history); + assertNotNull(result); + String expectedMessage = String.format( + ListProgrammeCommand.SUCCESS_MESSAGE_FORMAT, + programmeList + ); + assertEquals(expectedMessage, result.getMessage()); + } + + @Test + void execute_returnsEmptyListWhenNoProgrammes() { + programmeList = new ProgrammeList(); // Reset to empty list + CommandResult result = command.execute(programmeList, history); + assertNotNull(result); + String expectedMessage = String.format( + ListProgrammeCommand.SUCCESS_MESSAGE_FORMAT, + "No programmes found." + ); + assertEquals(expectedMessage, result.getMessage()); + } + + @Test + void execute_handlesNullProgrammeList() { + assertThrows(AssertionError.class, () -> command.execute(null, history)); + } +} diff --git a/src/test/java/command/programme/LogProgrammeCommandTest.java b/src/test/java/command/programme/LogProgrammeCommandTest.java new file mode 100644 index 0000000000..407a50fbd2 --- /dev/null +++ b/src/test/java/command/programme/LogProgrammeCommandTest.java @@ -0,0 +1,75 @@ +// @@ author andreusxcarvalho + +package command.programme; +import org.junit.jupiter.api.Test; + +import command.CommandResult; +import history.History; +import history.DailyRecord; +import programme.Day; +import programme.ProgrammeList; +import programme.Programme; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +public class LogProgrammeCommandTest { + + @Test + public void testExecute_logsDayIntoHistory() { + ProgrammeList mockProgrammes = mock(ProgrammeList.class); + History mockHistory = mock(History.class); + Day mockDay = mock(Day.class); + DailyRecord mockDailyRecord = mock(DailyRecord.class); + Programme mockProgramme = mock(Programme.class); + + int progIndex = 0; + int dayIndex = 0; + LocalDate date = LocalDate.of(2024, 12, 12); + + // Mock ProgrammeList behavior to return the mock Programme when getProgramme() is called + when(mockProgrammes.getProgramme(progIndex)).thenReturn(mockProgramme); + when(mockProgramme.getDay(dayIndex)).thenReturn(mockDay); + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + LogProgrammeCommand logCommand = new LogProgrammeCommand(progIndex, dayIndex, date); + + CommandResult result = logCommand.execute(mockProgrammes, mockHistory); + + verify(mockProgrammes).getProgramme(progIndex); + verify(mockProgramme).getDay(dayIndex); + verify(mockDailyRecord).logDayToRecord(mockDay); + assertNotNull(result); + } + + @Test + public void testExecute_edgeCase_invalidProgrammeIndex() { + ProgrammeList mockProgrammes = mock(ProgrammeList.class); + History mockHistory = mock(History.class); + int invalidProgIndex = -1; + int dayIndex = 0; + LocalDate date = LocalDate.of(2024, 12, 12); + + LogProgrammeCommand logCommand = new LogProgrammeCommand(invalidProgIndex, dayIndex, date); + + assertThrows(NullPointerException.class, () -> logCommand.execute(mockProgrammes, mockHistory)); + } + + @Test + public void testExecute_edgeCase_nullHistory() { + ProgrammeList mockProgrammes = mock(ProgrammeList.class); + int progIndex = 0; + int dayIndex = 0; + LocalDate date = LocalDate.of(2024, 12, 12); + + LogProgrammeCommand logCommand = new LogProgrammeCommand(progIndex, dayIndex, date); + + assertThrows(AssertionError.class, () -> logCommand.execute(mockProgrammes, null)); + } +} diff --git a/src/test/java/command/programme/StartProgrammeCommandTest.java b/src/test/java/command/programme/StartProgrammeCommandTest.java new file mode 100644 index 0000000000..7aebad4914 --- /dev/null +++ b/src/test/java/command/programme/StartProgrammeCommandTest.java @@ -0,0 +1,65 @@ +// @@author nirala-ts + +package command.programme; + +import command.CommandResult; +import exceptions.ProgrammeException; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class StartProgrammeCommandTest { + + private ProgrammeList programmeList; + private History history; + private StartProgrammeCommand command; + + @BeforeEach + void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + + ArrayList days1 = new ArrayList<>(); + days1.add(new Day("Day 1", new ArrayList<>())); + programmeList.insertProgramme("Programme 1", days1); + + ArrayList days2 = new ArrayList<>(); + days2.add(new Day("Day 2", new ArrayList<>())); + programmeList.insertProgramme("Programme 2", days2); + + programmeList.deactivateCurrentProgramme(); + } + + @Test + void execute_startsProgrammeSuccessfully() { + command = new StartProgrammeCommand(0); + CommandResult result = command.execute(programmeList, history); + assertNotNull(result); + Programme startedProgramme = programmeList.getProgramme(0); + String expectedMessage = String.format(StartProgrammeCommand.SUCCESS_MESSAGE_FORMAT, startedProgramme); + assertEquals(expectedMessage, result.getMessage()); + } + + + @Test + void execute_invalidProgrammeIndex_throwsException() { + command = new StartProgrammeCommand(5); + assertThrows(ProgrammeException.class, () -> command.execute(programmeList, history)); + } + + @Test + void execute_nullProgrammeList_throwsException() { + command = new StartProgrammeCommand(0); + assertThrows(AssertionError.class, () -> command.execute(null, history)); + } +} + diff --git a/src/test/java/command/programme/ViewProgrammeCommandTest.java b/src/test/java/command/programme/ViewProgrammeCommandTest.java new file mode 100644 index 0000000000..093b405309 --- /dev/null +++ b/src/test/java/command/programme/ViewProgrammeCommandTest.java @@ -0,0 +1,62 @@ +// @@author nirala-ts + +package command.programme; + +import command.CommandResult; +import exceptions.ProgrammeException; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Programme; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ViewProgrammeCommandTest { + + private ProgrammeList programmeList; + private History history; + private ViewProgrammeCommand command; + + @BeforeEach + void setUp() { + programmeList = new ProgrammeList(); + history = new History(); + + ArrayList days1 = new ArrayList<>(); + days1.add(new Day("Day 1", new ArrayList<>())); + programmeList.insertProgramme("Programme 1", days1); + + ArrayList days2 = new ArrayList<>(); + days2.add(new Day("Day 2", new ArrayList<>())); + programmeList.insertProgramme("Programme 2", days2); + } + + @Test + void execute_viewsProgrammeSuccessfully() { + command = new ViewProgrammeCommand(0); + CommandResult result = command.execute(programmeList, history); + assertNotNull(result); + + Programme viewedProgramme = programmeList.getProgramme(0); + String expectedMessage = String.format(ViewProgrammeCommand.SUCCESS_MESSAGE_FORMAT, viewedProgramme); + assertEquals(expectedMessage, result.getMessage()); + } + + @Test + void execute_invalidProgrammeIndex_throwsException() { + command = new ViewProgrammeCommand(5); // Invalid index + assertThrows(ProgrammeException.class, () -> command.execute(programmeList, history)); + } + + @Test + void execute_nullProgrammeList_throwsException() { + command = new ViewProgrammeCommand(0); + assertThrows(AssertionError.class, () -> command.execute(null, history)); + } +} diff --git a/src/test/java/command/programme/edit/CreateDayCommandTest.java b/src/test/java/command/programme/edit/CreateDayCommandTest.java new file mode 100644 index 0000000000..ba6ce79bcc --- /dev/null +++ b/src/test/java/command/programme/edit/CreateDayCommandTest.java @@ -0,0 +1,75 @@ +package command.programme.edit; + +import command.CommandResult; +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CreateDayCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int INVALID_PROGRAMME_ID = -2; + private static final int OUT_OF_RANGE_PROGRAMME_ID = 999; + private ProgrammeList programme; + private Day day; + private CreateDayProgrammeCommand command; + + @BeforeEach + void setUp() { + // Creates a programmeList with one programme with no days + programme = new ProgrammeList(); + programme.insertProgramme("Mock programme", new ArrayList<>()); + day = new Day("Day 1", new ArrayList<>()); + command = new CreateDayProgrammeCommand(VALID_PROGRAMME_ID, day); + } + + // Test for the constructor with a happy path case + @Test + void constructor_initializesWithValidProgrammeIdAndDay() { + assertDoesNotThrow(() -> new CreateDayProgrammeCommand(VALID_PROGRAMME_ID, day)); + } + + // Edge case for the constructor: Negative programme ID + @Test + void constructor_throwsExceptionIfProgrammeIdIsNegative() { + assertThrows(AssertionError.class, () -> new CreateDayProgrammeCommand(INVALID_PROGRAMME_ID, day)); + } + + // Edge case for the constructor: Day is null + @Test + void constructor_throwsExceptionIfDayIsNull() { + assertThrows(AssertionError.class, () -> new CreateDayProgrammeCommand(VALID_PROGRAMME_ID, null)); + } + + // Test for the "execute" method with a happy path case + @Test + void execute_addsDayToProgrammeList_returnsSuccessMessage() { + String expectedMessage = String.format(CreateDayProgrammeCommand.SUCCESS_MESSAGE_FORMAT, day); + CommandResult expectedResult = new CommandResult(expectedMessage); + + CommandResult actualResult = command.execute(programme); + assertEquals(expectedResult, actualResult); + } + + // Edge case for the "execute" method: Programmes list is null + @Test + void execute_throwsAssertionErrorIfProgrammesIsNull() { + assertThrows(AssertionError.class, () -> command.execute(null)); + } + + // Edge case for the "execute" method: Programme list does not contain programme ID + @Test + void execute_handlesNonexistentProgrammeIdGracefully() { + CreateDayProgrammeCommand invalidCommand = new CreateDayProgrammeCommand(OUT_OF_RANGE_PROGRAMME_ID, day); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programme)); + } +} + diff --git a/src/test/java/command/programme/edit/CreateExerciseCommandTest.java b/src/test/java/command/programme/edit/CreateExerciseCommandTest.java new file mode 100644 index 0000000000..f6afb3033b --- /dev/null +++ b/src/test/java/command/programme/edit/CreateExerciseCommandTest.java @@ -0,0 +1,109 @@ +package command.programme.edit; + +import command.CommandResult; +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Exercise; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CreateExerciseCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int VALID_DAY_ID = 0; + + private static final int INVALID_PROGRAMME_ID = -2; + private static final int INVALID_DAY_ID = -1; + private static final int OUT_OF_RANGE_PROGRAMME_ID = 999; + private static final int OUT_OF_RANGE_DAY_ID = 999; + + private ProgrammeList programmeList; + private Exercise exercise; + private CreateExerciseProgrammeCommand command; + + @BeforeEach + void setUp() { + // Creates a ProgrammeList with a single programme and one day + programmeList = new ProgrammeList(); + ArrayList days = new ArrayList<>(); + Day day = new Day("Day 1", new ArrayList<>()); + days.add(day); + programmeList.insertProgramme("Mock Programme", days); + + // Initialize the Exercise and CreateExerciseCommand with valid IDs + exercise = new Exercise(3, 10, 100, 200, "Deadlift"); + command = new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, exercise); + } + + // Test for the constructor with valid programme and day IDs and exercise + @Test + void constructor_initializesWithValidParameters() { + assertDoesNotThrow(() -> new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, exercise)); + } + + // Edge case for the constructor: Negative programme ID + @Test + void constructor_throwsAssertionErrorIfProgrammeIdIsInvalid() { + assertThrows(AssertionError.class, () -> + new CreateExerciseProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID, exercise) + ); + } + + // Edge case for the constructor: Negative day ID + @Test + void constructor_throwsAssertionErrorIfdayIndexIsNegative() { + assertThrows(AssertionError.class, () -> + new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID, exercise) + ); + } + + // Edge case for the constructor: Exercise is null + @Test + void constructor_throwsAssertionErrorIfExerciseIsNull() { + assertThrows(AssertionError.class, () -> + new CreateExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, null) + ); + } + + // Test for the execute method with a valid programme and day ID + @Test + void execute_addsExerciseToDay_returnsSuccessMessage() { + String expectedMessage = String.format(CreateExerciseProgrammeCommand.SUCCESS_MESSAGE_FORMAT, exercise); + CommandResult expectedResult = new CommandResult(expectedMessage); + + CommandResult actualResult = command.execute(programmeList); + assertEquals(expectedResult, actualResult); + } + + // Edge case for the execute method: Programme list is null + @Test + void execute_throwsAssertionErrorIfProgrammesIsNull() { + assertThrows(AssertionError.class, () -> command.execute(null)); + } + + // Edge case for the execute method: Nonexistent programme ID + @Test + void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() { + CreateExerciseProgrammeCommand invalidCommand = new CreateExerciseProgrammeCommand( + OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID, exercise + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } + + // Edge case for the execute method: Nonexistent day ID within an existing programme + @Test + void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() { + CreateExerciseProgrammeCommand invalidCommand = new CreateExerciseProgrammeCommand( + VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID, exercise + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } +} + diff --git a/src/test/java/command/programme/edit/DeleteDayCommandTest.java b/src/test/java/command/programme/edit/DeleteDayCommandTest.java new file mode 100644 index 0000000000..d967f9e5df --- /dev/null +++ b/src/test/java/command/programme/edit/DeleteDayCommandTest.java @@ -0,0 +1,94 @@ +package command.programme.edit; + +import command.CommandResult; +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DeleteDayCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int VALID_DAY_ID = 0; + private static final int INVALID_PROGRAMME_ID = -2; + private static final int INVALID_DAY_ID = -1; + private static final int OUT_OF_RANGE_PROGRAMME_ID = 999; + private static final int OUT_OF_RANGE_DAY_ID = 999; + + private ProgrammeList programmeList; + private Day day; + private DeleteDayProgrammeCommand command; + + @BeforeEach + void setUp() { + // Set up a ProgrammeList with one programme and one day + programmeList = new ProgrammeList(); + day = new Day("Day 1", new ArrayList<>()); + ArrayList days = new ArrayList<>(); + days.add(day); + programmeList.insertProgramme("Mock Programme", days); + + // Initialize DeleteDayCommand with valid IDs + command = new DeleteDayProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID); + } + + // Test for constructor with valid inputs + @Test + void constructor_initializesWithValidParameters() { + assertDoesNotThrow(() -> new DeleteDayProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID)); + } + + // Edge case for constructor: Negative programme ID + @Test + void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() { + assertThrows(AssertionError.class, () -> new DeleteDayProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID)); + } + + // Edge case for constructor: Negative day ID + @Test + void constructor_throwsAssertionErrorIfdayIndexIsNegative() { + assertThrows(AssertionError.class, () -> new DeleteDayProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID)); + } + + // Test for execute method: successfully deletes day and returns success message + @Test + void execute_deletesDayFromProgramme_returnsSuccessMessage() { + String expectedMessage = String.format(DeleteDayProgrammeCommand.SUCCESS_MESSAGE_FORMAT, day); + CommandResult expectedResult = new CommandResult(expectedMessage); + + CommandResult actualResult = command.execute(programmeList); + assertEquals(expectedResult, actualResult); + } + + // Edge case for execute: Programme list is null + @Test + void execute_throwsAssertionErrorIfProgrammesIsNull() { + assertThrows(AssertionError.class, () -> command.execute(null)); + } + + // Edge case for execute: Nonexistent programme ID + @Test + void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() { + DeleteDayProgrammeCommand invalidCommand = new DeleteDayProgrammeCommand( + OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } + + // Edge case for execute: Nonexistent day ID within existing programme + @Test + void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() { + DeleteDayProgrammeCommand invalidCommand = new DeleteDayProgrammeCommand( + VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } +} + diff --git a/src/test/java/command/programme/edit/DeleteExerciseCommandTest.java b/src/test/java/command/programme/edit/DeleteExerciseCommandTest.java new file mode 100644 index 0000000000..6b81304c24 --- /dev/null +++ b/src/test/java/command/programme/edit/DeleteExerciseCommandTest.java @@ -0,0 +1,126 @@ +package command.programme.edit; + +import command.CommandResult; +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Exercise; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DeleteExerciseCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int VALID_DAY_ID = 0; + private static final int VALID_EXERCISE_ID = 0; + private static final int INVALID_PROGRAMME_ID = -2; + private static final int INVALID_DAY_ID = -1; + private static final int INVALID_EXERCISE_ID = -1; + private static final int OUT_OF_RANGE_PROGRAMME_ID = 999; + private static final int OUT_OF_RANGE_DAY_ID = 999; + private static final int OUT_OF_RANGE_EXERCISE_ID = 999; + + private ProgrammeList programmeList; + private Exercise exercise; + private DeleteExerciseProgrammeCommand command; + + @BeforeEach + void setUp() { + // Set up a ProgrammeList with one programme, one day, and one exercise + programmeList = new ProgrammeList(); + exercise = new Exercise(3, 10, 100, 200, "Deadlift"); + ArrayList exercises = new ArrayList<>(); + exercises.add(exercise); + Day day = new Day("Day 1", exercises); + + ArrayList days = new ArrayList<>(); + days.add(day); + programmeList.insertProgramme("Mock Programme", days); + + // Initialize DeleteExerciseCommand with valid IDs + command = new DeleteExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID); + } + + // Test for constructor with valid inputs + @Test + void constructor_initializesWithValidParameters() { + assertDoesNotThrow(() -> new DeleteExerciseProgrammeCommand( + VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID) + ); + } + + // Edge case for constructor: Negative programme ID + @Test + void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() { + assertThrows(AssertionError.class, () -> + new DeleteExerciseProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID) + ); + } + + // Edge case for constructor: Negative day ID + @Test + void constructor_throwsAssertionErrorIfdayIndexIsNegative() { + assertThrows(AssertionError.class, () -> + new DeleteExerciseProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID, VALID_EXERCISE_ID) + ); + } + + // Edge case for constructor: Negative exercise ID + @Test + void constructor_throwsAssertionErrorIfexerciseIndexIsNegative() { + assertThrows(AssertionError.class, () -> + new DeleteExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, INVALID_EXERCISE_ID) + ); + } + + // Test for execute method: successfully deletes exercise and returns success message + @Test + void execute_deletesExerciseFromDay_returnsSuccessMessage() { + String expectedMessage = String.format( + DeleteExerciseProgrammeCommand.SUCCESS_MESSAGE_FORMAT, VALID_EXERCISE_ID + 1, exercise + ); + CommandResult expectedResult = new CommandResult(expectedMessage); + + CommandResult actualResult = command.execute(programmeList); + assertEquals(expectedResult, actualResult); + } + + // Edge case for execute: Programme list is null + @Test + void execute_throwsAssertionErrorIfProgrammesIsNull() { + assertThrows(AssertionError.class, () -> command.execute(null)); + } + + // Edge case for execute: Nonexistent programme ID + @Test + void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() { + DeleteExerciseProgrammeCommand invalidCommand = new DeleteExerciseProgrammeCommand( + OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } + + // Edge case for execute: Nonexistent day ID within existing programme + @Test + void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() { + DeleteExerciseProgrammeCommand invalidCommand = new DeleteExerciseProgrammeCommand( + VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID, VALID_EXERCISE_ID + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } + + // Edge case for execute: Nonexistent exercise ID within existing day + @Test + void execute_handlesNonexistentexerciseIndexGracefully() { + DeleteExerciseProgrammeCommand invalidCommand = new DeleteExerciseProgrammeCommand( + VALID_PROGRAMME_ID, VALID_DAY_ID, OUT_OF_RANGE_EXERCISE_ID + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } +} diff --git a/src/test/java/command/programme/edit/EditExerciseCommandTest.java b/src/test/java/command/programme/edit/EditExerciseCommandTest.java new file mode 100644 index 0000000000..a8fc6ba4f6 --- /dev/null +++ b/src/test/java/command/programme/edit/EditExerciseCommandTest.java @@ -0,0 +1,143 @@ +package command.programme.edit; + +import command.CommandResult; +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Exercise; +import programme.ExerciseUpdate; +import programme.ProgrammeList; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EditExerciseCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int VALID_DAY_ID = 0; + private static final int VALID_EXERCISE_ID = 0; + + //Since programme_id is optional, -1 (NULL_INTEGER) is a valid input. + private static final int INVALID_PROGRAMME_ID = -2; + + private static final int INVALID_DAY_ID = -1; + private static final int INVALID_EXERCISE_ID = -1; + private static final int OUT_OF_RANGE_PROGRAMME_ID = 999; + private static final int OUT_OF_RANGE_DAY_ID = 999; + private static final int OUT_OF_RANGE_EXERCISE_ID = 999; + + private ProgrammeList programmeList; + private ExerciseUpdate update; + private Exercise expectedExercise; + private EditExerciseProgrammeCommand command; + + @BeforeEach + void setUp() { + // Set up a ProgrammeList with one programme, one day, and one exercise + programmeList = new ProgrammeList(); + Exercise originalExercise = new Exercise(3, 10, 100, 200, "Deadlift"); + update = new ExerciseUpdate(3, 12, 105, 205, "Deadlift Updated"); + expectedExercise = new Exercise(3, 12, 105, 205, "Deadlift Updated"); + + ArrayList exercises = new ArrayList<>(); + exercises.add(originalExercise); + Day day = new Day("Day 1", exercises); + + ArrayList days = new ArrayList<>(); + days.add(day); + programmeList.insertProgramme("Mock Programme", days); + + // Initialize EditExerciseCommand with valid IDs and the updated exercise + command = new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update); + } + + // Test for constructor with valid inputs + @Test + void constructor_initializesWithValidParameters() { + assertDoesNotThrow(() -> + new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update) + ); + } + + // Edge case for constructor: Negative programme ID + @Test + void constructor_throwsAssertionErrorIfProgrammeIdIsNegative() { + assertThrows(AssertionError.class, () -> + new EditExerciseProgrammeCommand(INVALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update) + ); + } + + // Edge case for constructor: Negative day ID + @Test + void constructor_throwsAssertionErrorIfdayIndexIsNegative() { + assertThrows(AssertionError.class, () -> + new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID, VALID_EXERCISE_ID, update) + ); + } + + // Edge case for constructor: Negative exercise ID + @Test + void constructor_throwsAssertionErrorIfexerciseIndexIsNegative() { + assertThrows(AssertionError.class, () -> + new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, INVALID_EXERCISE_ID, update) + ); + } + + // Edge case for constructor: Updated exercise is null + @Test + void constructor_throwsAssertionErrorIfUpdatedExerciseIsNull() { + assertThrows(AssertionError.class, () -> + new EditExerciseProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, null) + ); + } + + // Test for execute method: successfully updates exercise and returns success message + @Test + void execute_updatesExerciseInDay_returnsSuccessMessage() { + String expectedMessage = String.format( + EditExerciseProgrammeCommand.SUCCESS_MESSAGE_FORMAT, expectedExercise + ); + + CommandResult expectedResult = new CommandResult(expectedMessage); + CommandResult actualResult = command.execute(programmeList); + + assertEquals(expectedResult, actualResult); + } + + // Edge case for execute: Programme list is null + @Test + void execute_throwsAssertionErrorIfProgrammesIsNull() { + assertThrows(AssertionError.class, () -> command.execute(null)); + } + + // Edge case for execute: Nonexistent programme ID + @Test + void execute_throwsIndexOutOfBoundsIfProgrammeIdDoesNotExist() { + EditExerciseProgrammeCommand invalidCommand = new EditExerciseProgrammeCommand( + OUT_OF_RANGE_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID, update + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } + + // Edge case for execute: Nonexistent day ID within existing programme + @Test + void execute_throwsIndexOutOfBoundsIfdayIndexDoesNotExist() { + EditExerciseProgrammeCommand invalidCommand = new EditExerciseProgrammeCommand( + VALID_PROGRAMME_ID, OUT_OF_RANGE_DAY_ID, VALID_EXERCISE_ID, update + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } + + // Edge case for execute: Nonexistent exercise ID within existing day + @Test + void execute_handlesNonexistentexerciseIndexGracefully() { + EditExerciseProgrammeCommand invalidCommand = new EditExerciseProgrammeCommand( + VALID_PROGRAMME_ID, VALID_DAY_ID, OUT_OF_RANGE_EXERCISE_ID, update + ); + assertThrows(ProgrammeException.class, () -> invalidCommand.execute(programmeList)); + } +} diff --git a/src/test/java/command/programme/edit/EditProgrammeCommandTest.java b/src/test/java/command/programme/edit/EditProgrammeCommandTest.java new file mode 100644 index 0000000000..bcd0c0ac34 --- /dev/null +++ b/src/test/java/command/programme/edit/EditProgrammeCommandTest.java @@ -0,0 +1,54 @@ +package command.programme.edit; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class EditProgrammeCommandTest { + + private static final int VALID_PROGRAMME_ID = 0; + private static final int INVALID_PROGRAMME_ID = -2; + private static final int VALID_DAY_ID = 0; + private static final int VALID_EXERCISE_ID = 0; + private static final int INVALID_EXERCISE_ID = -1; + private static final int INVALID_DAY_ID = -1; + + // Test for the constructor with valid parameters + @Test + void constructor_withValidParameters_initializesCorrectly() { + assertDoesNotThrow(() -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, VALID_EXERCISE_ID)); + } + + // Edge case for constructor with exerciseIndex: Negative exercise ID + @Test + void constructor_withNegativeexerciseIndex_throwsAssertionError() { + assertThrows(AssertionError.class, () -> + new TestEditProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID, INVALID_EXERCISE_ID) + ); + } + + // Happy path for constructor without exerciseIndex + @Test + void constructor_withoutexerciseIndex_initializesCorrectly() { + assertDoesNotThrow(() -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID, VALID_DAY_ID)); + } + + // Edge case for constructor without exerciseIndex: Invalid day ID + @Test + void constructor_withNegativedayIndex_throwsAssertionError() { + assertThrows(AssertionError.class, () -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID, INVALID_DAY_ID)); + } + + // Happy path for constructor with only programme index + @Test + void constructor_withProgrammeIndex_initializesCorrectly() { + assertDoesNotThrow(() -> new TestEditProgrammeCommand(VALID_PROGRAMME_ID)); + } + + // Edge case for constructor with programme index: Negative programme index + @Test + void constructor_withNegativeProgrammeIndex_throwsAssertionError() { + assertThrows(AssertionError.class, () -> new TestEditProgrammeCommand(INVALID_PROGRAMME_ID)); + } +} diff --git a/src/test/java/command/programme/edit/TestEditProgrammeCommand.java b/src/test/java/command/programme/edit/TestEditProgrammeCommand.java new file mode 100644 index 0000000000..452f8d3e51 --- /dev/null +++ b/src/test/java/command/programme/edit/TestEditProgrammeCommand.java @@ -0,0 +1,24 @@ +package command.programme.edit; + +import command.CommandResult; +import programme.ProgrammeList; + +// A concrete subclass of EditCommand to help testing abstract EditCommand class +class TestEditProgrammeCommand extends EditProgrammeCommand { + public TestEditProgrammeCommand(int programmeIndex, int dayIndex, int exerciseIndex) { + super(programmeIndex, dayIndex, exerciseIndex); + } + + public TestEditProgrammeCommand(int programmeIndex, int dayIndex) { + super(programmeIndex, dayIndex); + } + + public TestEditProgrammeCommand(int programmeIndex) { + super(programmeIndex); + } + + @Override + public CommandResult execute(ProgrammeList programmes) { + return new CommandResult("Executed"); + } +} diff --git a/src/test/java/command/water/AddWaterCommandTest.java b/src/test/java/command/water/AddWaterCommandTest.java new file mode 100644 index 0000000000..9613dc09a1 --- /dev/null +++ b/src/test/java/command/water/AddWaterCommandTest.java @@ -0,0 +1,68 @@ +package command.water; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AddWaterCommandTest { + + private AddWaterCommand addWaterCommand; + private float waterAmount; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private DailyRecord mockDailyRecord; + + @BeforeEach + public void setUp() { + // Initialize the mocks + MockitoAnnotations.openMocks(this); + + waterAmount = 1.5f; + date = LocalDate.now(); + + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + addWaterCommand = new AddWaterCommand(waterAmount, date); + } + + @Test + public void testExecuteHappyPath() { + CommandResult expectedResult = new CommandResult(waterAmount + " liters of water has been added"); + + CommandResult result = addWaterCommand.execute(mockHistory); + + verify(mockDailyRecord).addWaterToRecord(waterAmount); + assertEquals(expectedResult, result, + "Execution should return a CommandResult with the correct success message."); + } + + @Test + public void testExecuteEdgeCaseNullDailyRecord() { + // Set up History mock to return null for the DailyRecord + when(mockHistory.getRecordByDate(date)).thenReturn(null); + + assertThrows(AssertionError.class, () -> addWaterCommand.execute(mockHistory), + "Executing AddWaterCommand without a valid DailyRecord should throw an AssertionError."); + } + + @Test + public void testExecuteEdgeCaseNegativeWaterAmount() { + assertThrows(AssertionError.class, () -> new AddWaterCommand(-1.0f, date), + "Creating AddWaterCommand with a negative water amount should throw AssertionError."); + } +} diff --git a/src/test/java/command/water/DeleteWaterCommandTest.java b/src/test/java/command/water/DeleteWaterCommandTest.java new file mode 100644 index 0000000000..83ca1161c6 --- /dev/null +++ b/src/test/java/command/water/DeleteWaterCommandTest.java @@ -0,0 +1,81 @@ +package command.water; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DeleteWaterCommandTest { + + private DeleteWaterCommand deleteWaterCommand; + private float sampleWaterAmount; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private DailyRecord mockDailyRecord; + + @BeforeEach + public void setUp() { + // Initialize the mocks + MockitoAnnotations.openMocks(this); + + sampleWaterAmount = 1.5f; + date = LocalDate.now(); + + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + when(mockDailyRecord.removeWaterFromRecord(0)).thenReturn(sampleWaterAmount); + + deleteWaterCommand = new DeleteWaterCommand(0, date); + } + + @Test + public void testExecuteHappyPath() { + CommandResult expectedResult = new CommandResult(sampleWaterAmount + + " liters of water has been deleted"); + + CommandResult result = deleteWaterCommand.execute(mockHistory); + + verify(mockDailyRecord).removeWaterFromRecord(0); + assertEquals(expectedResult, result, + "Execution should return a CommandResult with the correct success message."); + } + + @Test + public void testExecuteEdgeCaseNullDailyRecord() { + when(mockHistory.getRecordByDate(date)).thenReturn(null); + + assertThrows(AssertionError.class, () -> deleteWaterCommand.execute(mockHistory), "Executing " + + "DeleteWaterCommand without a valid DailyRecord should throw an AssertionError."); + } + + @Test + public void testExecuteEdgeCaseInvalidIndex() { + // Set up DailyRecord mock to throw IndexOutOfBoundsException when an invalid index is used + when(mockDailyRecord.removeWaterFromRecord(5)).thenThrow(new IndexOutOfBoundsException("Invalid index")); + + DeleteWaterCommand invalidIndexCommand = new DeleteWaterCommand(5, date); + + assertThrows(IndexOutOfBoundsException.class, () -> invalidIndexCommand.execute(mockHistory), "Executing " + + "DeleteWaterCommand with an invalid index should throw IndexOutOfBoundsException."); + } + + @Test + public void testConstructorEdgeCaseNegativeIndex() { + assertThrows(AssertionError.class, () -> new DeleteWaterCommand(-1, date), "Creating " + + "DeleteWaterCommand with negative index should throw AssertionError."); + } +} diff --git a/src/test/java/command/water/ViewWaterCommandTest.java b/src/test/java/command/water/ViewWaterCommandTest.java new file mode 100644 index 0000000000..f65ab8e2ef --- /dev/null +++ b/src/test/java/command/water/ViewWaterCommandTest.java @@ -0,0 +1,80 @@ +package command.water; + +import command.CommandResult; +import history.DailyRecord; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import water.Water; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ViewWaterCommandTest { + + private ViewWaterCommand viewWaterCommand; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private DailyRecord mockDailyRecord; + + @Mock + private Water mockWater; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + date = LocalDate.now(); + + // Set up History mock to return a DailyRecord for the specified date + when(mockHistory.getRecordByDate(date)).thenReturn(mockDailyRecord); + + // Set up DailyRecord mock to return a Water object + when(mockDailyRecord.getWaterFromRecord()).thenReturn(mockWater); + + when(mockWater.toString()).thenReturn("Sample Water Record"); + + viewWaterCommand = new ViewWaterCommand(date); + } + + @Test + public void testExecuteHappyPath() { + String expectedWaterMessage = "Water intake for " + date.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")) + + ": \n\n" + "Sample Water Record"; + CommandResult expectedResult = new CommandResult(expectedWaterMessage); + + CommandResult result = viewWaterCommand.execute(mockHistory); + + verify(mockHistory).getRecordByDate(date); + verify(mockDailyRecord).getWaterFromRecord(); + + assertEquals(expectedResult, result, + "Execution should return a CommandResult with the correct water record output."); + } + + + + @Test + public void testExecuteEdgeCaseNullDailyRecord() { + when(mockHistory.getRecordByDate(date)).thenReturn(null); + + assertThrows(AssertionError.class, () -> viewWaterCommand.execute(mockHistory), "Executing " + + "ViewWaterCommand with a null DailyRecord should throw an AssertionError."); + } + + @Test + public void testConstructorEdgeCaseNullDate() { + assertThrows(AssertionError.class, () -> new ViewWaterCommand(null), "Creating " + + "ViewWaterCommand with a null date should throw an AssertionError."); + } +} diff --git a/src/test/java/command/water/WaterCommandTest.java b/src/test/java/command/water/WaterCommandTest.java new file mode 100644 index 0000000000..dfc3142b9d --- /dev/null +++ b/src/test/java/command/water/WaterCommandTest.java @@ -0,0 +1,68 @@ +package command.water; + +import command.CommandResult; +import history.History; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import programme.ProgrammeList; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; + +public class WaterCommandTest { + + private WaterCommand waterCommand; + private LocalDate date; + + @Mock + private History mockHistory; + + @Mock + private ProgrammeList mockProgrammeList; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + date = LocalDate.now(); + waterCommand = new TestWaterCommand(date); // Using a concrete subclass for testing + } + + @Test + public void testConstructorHappyPath() { + assertEquals(date, waterCommand.date, "Date should be initialized correctly in WaterCommand."); + } + + @Test + public void testConstructorEdgeCaseNullDate() { + assertThrows(AssertionError.class, () -> new TestWaterCommand(null), + "Creating WaterCommand with null date should throw an AssertionError."); + } + + @Test + public void testExecuteWithProgrammeListAndHistoryHappyPath() { + CommandResult expected = new CommandResult("Executed with history"); + + CommandResult result = waterCommand.execute(mockProgrammeList, mockHistory); + + assertEquals(expected, result, "Execute should return a CommandResult with the correct message."); + verify(mockHistory).getRecordByDate(date); + } + + // Concrete subclass of WaterCommand for testing purposes + private static class TestWaterCommand extends WaterCommand { + public TestWaterCommand(LocalDate date) { + super(date); + } + + @Override + public CommandResult execute(History history) { + history.getRecordByDate(date); + return new CommandResult("Executed with history"); + } + } +} diff --git a/src/test/java/history/DailyRecordTest.java b/src/test/java/history/DailyRecordTest.java new file mode 100644 index 0000000000..e30b25c342 --- /dev/null +++ b/src/test/java/history/DailyRecordTest.java @@ -0,0 +1,283 @@ +//@@author Bev-low + +package history; + +import exceptions.MealException; +import exceptions.WaterException; +import meal.Meal; +import meal.MealList; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Exercise; +import water.Water; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +public class DailyRecordTest { + private DailyRecord dailyRecord; + private Day validDay; + private Meal meal1; + private Meal meal2; + + @BeforeEach + public void setUp() { + validDay = new Day("validDay"); + validDay.insertExercise(new Exercise(3, 12, 50, 120, "Bench_Press")); + validDay.insertExercise(new Exercise(3, 12, 80, 200, "Squat")); + + meal1 = new Meal("potato", 100); + meal2 = new Meal("pasta", 900); + } + + @AfterEach + public void tearDown() { + dailyRecord = null; + validDay = null; + meal1 = null; + meal2 = null; + } + + @Test + public void testConstructor_default() { + dailyRecord = new DailyRecord(); + assertNotNull(dailyRecord.getWaterFromRecord()); + assertNotNull(dailyRecord.getMealListFromRecord()); + } + + @Test + public void logDay_validDay() { + dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(validDay); + + assertEquals(validDay, dailyRecord.getDayFromRecord()); + } + + @Test + public void logDay_emptyDay() { + dailyRecord = new DailyRecord(); + Day emptyDay = new Day("Empty Day"); + dailyRecord.logDayToRecord(emptyDay); + assertEquals(emptyDay, dailyRecord.getDayFromRecord()); + } + + @Test + public void addMealToRecord_validMeals() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + dailyRecord.addMealToRecord(meal2); + assertFalse(dailyRecord.getMealListFromRecord().isEmpty()); + assertEquals(2, dailyRecord.getMealListFromRecord().getSize()); + assertEquals("pasta", dailyRecord.getMealListFromRecord().getMeals().get(1).getName()); + } + + @Test + public void addMealToRecord_negativeCaloriesMeal() { + dailyRecord = new DailyRecord(); + assertThrows(AssertionError.class, () -> dailyRecord.addMealToRecord(new Meal("potato", -100))); + } + + @Test + public void addMealToRecord_nullMeal() { + dailyRecord = new DailyRecord(); + assertThrows(AssertionError.class, () -> dailyRecord.addMealToRecord(null)); + } + + @Test + public void deleteMealFromRecord_validIndex() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + dailyRecord.addMealToRecord(meal2); + dailyRecord.deleteMealFromRecord(0); + assertEquals(1, dailyRecord.getMealListFromRecord().getSize()); + assertEquals("pasta", dailyRecord.getMealListFromRecord().getMeals().get(0).getName()); + } + + @Test + public void deleteMealFromRecord_negativeIndex() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + assertThrows(MealException.class, () -> dailyRecord.deleteMealFromRecord(-1), + "Expected MealException for negative index in meal list."); + } + + @Test + public void deleteMealFromRecord_outOfBoundsIndex() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + assertThrows(MealException.class, () -> dailyRecord.deleteMealFromRecord(10), + "Expected MealException for out-of-bounds index in meal list."); + } + + @Test + public void addWaterToRecord_validWater() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + dailyRecord.addWaterToRecord(400.0f); + assertFalse(dailyRecord.getWaterFromRecord().isEmpty()); + assertEquals(2, dailyRecord.getWaterFromRecord().getWaterList().size()); + assertEquals(100.0f, dailyRecord.getWaterFromRecord().getWaterList().get(0)); + } + + @Test + public void addWaterToRecord_negativeWater() { + dailyRecord = new DailyRecord(); + assertThrows(AssertionError.class, () -> dailyRecord.addWaterToRecord(-500.0f)); + } + + @Test + public void removeWaterFromRecord_validIndex() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + dailyRecord.addWaterToRecord(400.0f); + dailyRecord.removeWaterFromRecord(0); + assertEquals(1, dailyRecord.getWaterFromRecord().getWaterList().size()); + assertEquals(400.0f, dailyRecord.getWaterFromRecord().getWaterList().get(0)); + } + + @Test + public void removeWaterFromRecord_negativeIndex() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + assertThrows(WaterException.class, () -> dailyRecord.removeWaterFromRecord(-1), + "Expected WaterExceptions for negative index in water list."); + } + + @Test + public void removeWaterFromRecord_outOfBoundsIndex() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + assertThrows(WaterException.class, () -> dailyRecord.removeWaterFromRecord(10), + "Expected WaterExceptions for out-of-bounds index in water list."); + } + + @Test + public void getDayFromRecord_initialDay() { + dailyRecord = new DailyRecord(); + assertNull(dailyRecord.getDayFromRecord()); + } + + @Test + public void getDayFromRecord_afterLogDay() { + dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(validDay); + assertEquals("validDay", dailyRecord.getDayFromRecord().getName()); + } + + @Test + public void getMealList_initialMealList() { + dailyRecord = new DailyRecord(); + MealList mealList = dailyRecord.getMealListFromRecord(); + assertNotNull(mealList); + assertTrue(mealList.getMeals().isEmpty()); + } + + @Test + public void getMealList_afterAddMeal() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + dailyRecord.addMealToRecord(meal2); + MealList mealList = dailyRecord.getMealListFromRecord(); + assertEquals("potato", mealList.getMeals().get(0).getName()); + assertEquals("pasta", mealList.getMeals().get(1).getName()); + } + + @Test + public void getMealList_afterDeleteMeal() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + dailyRecord.addMealToRecord(meal2); + dailyRecord.deleteMealFromRecord(0); + assertEquals(1, dailyRecord.getMealListFromRecord().getSize()); + } + + @Test + public void getWater_initialWater() { + dailyRecord = new DailyRecord(); + Water water = dailyRecord.getWaterFromRecord(); + assertNotNull(water); + assertTrue(water.getWaterList().isEmpty()); + } + + @Test + public void getWater_afterAddWater() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + dailyRecord.addWaterToRecord(400.0f); + Water water = dailyRecord.getWaterFromRecord(); + assertEquals(100.0f, water.getWaterList().get(0)); + assertEquals(400.0f, water.getWaterList().get(1)); + } + + @Test + public void getWater_afterRemoveWater() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + dailyRecord.addWaterToRecord(400.0f); + dailyRecord.removeWaterFromRecord(0); + assertEquals(1, dailyRecord.getWaterFromRecord().getWaterList().size()); + } + + @Test + public void toString_emptyRecord() { + MealList mockMealList = mock(MealList.class); + Water mockWater = mock(Water.class); + Day mockDay = mock(Day.class); + + dailyRecord = spy(new DailyRecord()); + + when(mockMealList.getMeals()).thenReturn(new ArrayList()); + when(mockWater.getWaterList()).thenReturn(new ArrayList()); + when(mockDay.getExercisesCount()).thenReturn(0); + + doReturn(mockMealList).when(dailyRecord).getMealListFromRecord(); + doReturn(mockDay).when(dailyRecord).getDayFromRecord(); + doReturn(mockWater).when(dailyRecord).getWaterFromRecord(); + + String result = dailyRecord.toString(); + + assertTrue(result.contains("No Day")); + assertTrue(result.contains("No Water")); + assertTrue(result.contains("No Meals")); + } + + @Test + public void toString_callsGetCaloriesFromMeal() { + dailyRecord = new DailyRecord(); + dailyRecord.addMealToRecord(meal1); + dailyRecord.addMealToRecord(meal2); + String result = dailyRecord.toString(); + assertTrue(result.contains("Total Calories from Meals:")); + } + + @Test + public void toString_callsGetTotalWater() { + dailyRecord = new DailyRecord(); + dailyRecord.addWaterToRecord(100.0f); + dailyRecord.addWaterToRecord(400.0f); + String result = dailyRecord.toString(); + assertTrue(result.contains("Total Water Intake:")); + } + + @Test + public void toString_testDayToString() { + dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(validDay); + String result = dailyRecord.toString(); + assertFalse(result.contains("No Day")); + } +} + diff --git a/src/test/java/history/HistoryTest.java b/src/test/java/history/HistoryTest.java new file mode 100644 index 0000000000..a012586000 --- /dev/null +++ b/src/test/java/history/HistoryTest.java @@ -0,0 +1,108 @@ +package history; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Exercise; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HistoryTest { + + private History history; + private Day sampleDay; + + @BeforeEach + public void setUp() { + history = new History(); + sampleDay = new Day("Leg Day"); + Exercise sampleExercise = new Exercise(3, 10, 100, 200, "Squat"); + sampleDay.insertExercise(sampleExercise); + } + + @Test + public void testLogRecordAndGetRecordByDate() { + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(sampleDay); + + history.logRecord(date, dailyRecord); + + DailyRecord retrievedRecord = history.getRecordByDate(date); + assertNotNull(retrievedRecord, "Retrieved record should not be null."); + assertEquals(dailyRecord, retrievedRecord, "Retrieved record should match the logged record."); + } + + @Test + public void testGetWeeklyWorkoutSummaryWithData() { + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(sampleDay); + + history.logRecord(date, dailyRecord); + + String weeklySummary = history.getWeeklyWorkoutSummary(); + assertTrue(weeklySummary.contains("Leg Day"), "Weekly summary should contain the day's name."); + assertTrue(weeklySummary.contains("Squat"), "Weekly summary should contain the exercise name."); + } + + @Test + public void testGetWeeklyWorkoutSummaryWithoutData() { + String weeklySummary = history.getWeeklyWorkoutSummary(); + assertEquals("No workout history available.", weeklySummary, + "Weekly summary should indicate no data available."); + } + + @Test + public void testGetPersonalBestForExercise() { + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(sampleDay); + + history.logRecord(date, dailyRecord); + + String personalBest = history.getPersonalBestForExercise("Squat"); + assertTrue(personalBest.contains("Personal best for Squat"), + "Personal best output should contain exercise name."); + assertTrue(personalBest.contains("100kg"), "Personal best output should contain correct weight."); + } + + @Test + public void testGetFormattedPersonalBests() { + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(sampleDay); + + history.logRecord(date, dailyRecord); + + String formattedPersonalBests = history.getFormattedPersonalBests(); + assertTrue(formattedPersonalBests.contains("Personal bests for all exercises:"), + "Output should contain header."); + assertTrue(formattedPersonalBests.contains("Squat: 3 sets of 10 at 100kg"), + "Output should contain exercise details."); + } + + @Test + public void testToStringNoHistory() { + String historyString = history.toString(); + assertEquals("No history available.", historyString, "Output should indicate no history available."); + } + + @Test + public void testToStringWithHistory() { + LocalDate date = LocalDate.now(); + DailyRecord dailyRecord = new DailyRecord(); + dailyRecord.logDayToRecord(sampleDay); + + history.logRecord(date, dailyRecord); + + String historyString = history.toString(); + assertTrue(historyString.contains("Completed On:"), "Output should contain 'Completed On' date."); + assertTrue(historyString.contains("Leg Day"), "Output should contain the day's name."); + } +} + diff --git a/src/test/java/meal/MealListTest.java b/src/test/java/meal/MealListTest.java new file mode 100644 index 0000000000..74419a5b8b --- /dev/null +++ b/src/test/java/meal/MealListTest.java @@ -0,0 +1,129 @@ +package meal; + +import exceptions.MealException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MealListTest { + + private MealList mealList; + private Meal sampleMeal; + + @BeforeEach + public void setUp() { + mealList = new MealList(); + sampleMeal = new Meal("Sample Meal", 300); + } + + @Test + public void testIsEmptyHappyPath() { + assertTrue(mealList.isEmpty(), "Meal list " + + "should be empty initially."); + } + + @Test + public void testIsEmptyEdgeCaseNonEmptyList() { + mealList.addMeal(sampleMeal); + assertFalse(mealList.isEmpty(), "Meal list should not be empty after adding a meal."); + } + + @Test + public void testIsEmptyEdgeCaseEmptyAfterDeletion() { + mealList.addMeal(sampleMeal); + mealList.deleteMeal(0); + assertTrue(mealList.isEmpty(), "Meal list " + + "should be empty after adding and then deleting the only meal."); + } + + @Test + public void testGetSizeHappyPath() { + mealList.addMeal(sampleMeal); + assertEquals(1, mealList.getSize(), "Size should be 1 after adding one meal."); + } + + @Test + public void testGetSizeEdgeCaseEmptyList() { + assertEquals(0, mealList.getSize(), "Size should be 0 for an empty meal list."); + } + + @Test + public void testGetSizeEdgeCaseMultipleMeals() { + mealList.addMeal(sampleMeal); + mealList.addMeal(new Meal("Another Meal", 500)); + assertEquals(2, mealList.getSize(), "Size should be 2 after adding two meals."); + } + + @Test + public void testAddMealHappyPath() { + mealList.addMeal(sampleMeal); + assertEquals(1, mealList.getSize(), "Size should be 1 after adding one meal."); + assertTrue(mealList.getMeals().contains(sampleMeal), "Meal list should " + + "contain the added meal."); + } + + @Test + public void testAddMealEdgeCaseNullMeal() { + assertThrows(AssertionError.class, () -> mealList.addMeal(null), "Adding a null " + + "meal should throw an AssertionError."); + } + + @Test + public void testAddMealEdgeCaseDuplicateMeals() { + mealList.addMeal(sampleMeal); + mealList.addMeal(sampleMeal); + assertEquals(2, mealList.getSize(), "Size should " + + "be 2 after adding the same meal twice."); + } + + @Test + public void testDeleteMealHappyPath() { + mealList.addMeal(sampleMeal); + Meal deletedMeal = mealList.deleteMeal(0); + assertEquals(deletedMeal, sampleMeal, "Deleted meal should be equal to the meal added."); + assertTrue(mealList.isEmpty(), "Meal list should be empty after deleting the only meal."); + } + + @Test + public void testDeleteMealEdgeCaseNegativeIndex() { + assertThrows(MealException.class, () -> mealList.deleteMeal(-1), + "Deleting with a negative index should throw IndexOutOfBoundsBuffBuddyException."); + } + + @Test + public void testDeleteMealEdgeCaseIndexOutOfBounds() { + mealList.addMeal(sampleMeal); + assertThrows(MealException.class, () -> mealList.deleteMeal(1), + "Deleting with an out-of-bounds index should throw IndexOutOfBoundsBuffBuddyException."); + } + + @Test + public void testGetMealsHappyPath() { + mealList.addMeal(sampleMeal); + ArrayList meals = mealList.getMeals(); + assertEquals(1, meals.size(), "Meal list should have one meal after adding one."); + assertEquals(meals.get(0), sampleMeal, "The meal retrieved should be equal to the added meal."); + } + + @Test + public void testGetMealsEdgeCaseEmptyList() { + ArrayList meals = mealList.getMeals(); + assertTrue(meals.isEmpty(), "Retrieved list should be empty for a new MealList instance."); + } + + @Test + public void testGetMealsEdgeCaseMultipleMeals() { + Meal anotherMeal = new Meal("Another Meal", 500); + mealList.addMeal(sampleMeal); + mealList.addMeal(anotherMeal); + ArrayList meals = mealList.getMeals(); + assertEquals(2, meals.size(), "Meal list should contain two meals after adding two."); + assertTrue(meals.get(0).equals(sampleMeal) && meals.get(1).equals(anotherMeal), + "Meals should be equal to those added."); + } +} diff --git a/src/test/java/meal/MealTest.java b/src/test/java/meal/MealTest.java new file mode 100644 index 0000000000..ff0411101f --- /dev/null +++ b/src/test/java/meal/MealTest.java @@ -0,0 +1,101 @@ +package meal; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class MealTest { + + @Test + public void testConstructorHappyPath() { + Meal meal = new Meal("Sample Meal", 300); + assertEquals("Sample Meal", meal.getName(), "Meal name should match the expected value."); + assertEquals(300, meal.getCalories(), "Meal calories should match the expected value."); + } + + @Test + public void testConstructorEdgeCaseNullName() { + assertThrows(AssertionError.class, () -> new Meal(null, 300), "Creating " + + "a Meal with a null name should throw an AssertionError."); + } + + @Test + public void testConstructorEdgeCaseEmptyName() { + assertThrows(AssertionError.class, () -> new Meal("", 300), "Creating " + + "a Meal with an empty name should throw an AssertionError."); + } + + @Test + public void testConstructorEdgeCaseNegativeCalories() { + assertThrows(AssertionError.class, () -> new Meal("Negative Calories Meal", -100), + "Create a Meal with negative calories should throw an AssertionError."); + } + + @Test + public void testGetCaloriesHappyPath() { + Meal meal = new Meal("Sample Meal", 250); + assertEquals(250, meal.getCalories(), "getCalories should " + + "return the correct calorie count."); + } + + @Test + public void testGetNameHappyPath() { + Meal meal = new Meal("Healthy Salad", 150); + assertEquals("Healthy Salad", meal.getName(), "getName " + + "should return the correct meal name."); + } + + @Test + public void testEqualsHappyPath() { + Meal meal1 = new Meal("Same Meal", 400); + Meal meal2 = new Meal("Same Meal", 400); + assertEquals(meal1, meal2, "Meals with the same name and calories should be equal."); + } + + @Test + public void testEqualsEdgeCaseDifferentCalories() { + Meal meal1 = new Meal("Same Meal", 400); + Meal meal2 = new Meal("Same Meal", 300); + assertNotEquals(meal1, meal2, "Meals with different calories should not be equal."); + } + + @Test + public void testEqualsEdgeCaseDifferentName() { + Meal meal1 = new Meal("Meal One", 400); + Meal meal2 = new Meal("Meal Two", 400); + assertNotEquals(meal1, meal2, "Meals with different names should not be equal."); + } + + @Test + public void testHashCodeHappyPath() { + Meal meal1 = new Meal("Hash Meal", 500); + Meal meal2 = new Meal("Hash Meal", 500); + assertEquals(meal1.hashCode(), meal2.hashCode(), "Meals with the " + + "same name and calories should have the same hash code."); + } + + @Test + public void testHashCodeEdgeCaseDifferentCalories() { + Meal meal1 = new Meal("Hash Meal", 500); + Meal meal2 = new Meal("Hash Meal", 300); + assertNotEquals(meal1.hashCode(), meal2.hashCode(), "Meals with " + + "different calories should have different hash codes."); + } + + @Test + public void testHashCodeEdgeCaseDifferentName() { + Meal meal1 = new Meal("Hash Meal One", 500); + Meal meal2 = new Meal("Hash Meal Two", 500); + assertNotEquals(meal1.hashCode(), meal2.hashCode(), "Meals with " + + "different names should have different hash codes."); + } + + @Test + public void testToStringHappyPath() { + Meal meal = new Meal("Sample Meal", 350); + assertEquals("Sample Meal | 350kcal", meal.toString(), "toString should " + + "return the correct formatted string."); + } +} diff --git a/src/test/java/parser/FlagParserTest.java b/src/test/java/parser/FlagParserTest.java new file mode 100644 index 0000000000..ec5ce859cf --- /dev/null +++ b/src/test/java/parser/FlagParserTest.java @@ -0,0 +1,137 @@ +// @@author nirala-ts + +package parser; + +import exceptions.FlagException; +import exceptions.ParserException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static parser.FlagDefinitions.MEAL_INDEX; +import static parser.FlagDefinitions.WATER_INDEX; + +class FlagParserTest { + + private FlagParser flagParser; + + @BeforeEach + void setUp() { + String argumentString = "/p 1 /d Day1 /date 12-12-2023 /w 2.5 /n TestExercise /s 3 /r 10"; + flagParser = new FlagParser(argumentString); + } + + @Test + void testHasFlagValidCase() { + assertTrue(flagParser.hasFlag("/p")); + assertTrue(flagParser.hasFlag("/t")); + } + + @Test + void testHasFlagMissingFlag() { + assertFalse(flagParser.hasFlag("/m")); + } + + @Test + void testHasFlagEmptyFlag() { + assertThrows(AssertionError.class, () -> flagParser.hasFlag(""), + "Expected AssertionError for empty flag"); + } + + @Test + void testValidateRequiredFlagsValidCase() { + assertDoesNotThrow(() -> flagParser.validateRequiredFlags("/p", "/d", "/t"), + "Expected no exception for valid flags"); + } + + @Test + void testValidateRequiredFlagsMissingFlag() { + FlagException exception = assertThrows(FlagException.class, + () -> flagParser.validateRequiredFlags("/p", MEAL_INDEX), + "Expected MissingFlagBuffBuddyException for missing required flag"); + assertTrue(exception.getMessage().contains(MEAL_INDEX)); + } + + @Test + void testGetStringByFlagValidCase() { + assertEquals("Day1", flagParser.getStringByFlag("/d"), + "Expected value 'Day1' for flag '/d'"); + } + + @Test + void testGetStringByFlagFlagNotPresent() { + assertNull(flagParser.getStringByFlag("/x"), + "Expected null for non-existent flag '/x'"); + } + + @Test + void testGetStringByFlagEmptyFlag() { + assertThrows(AssertionError.class, () -> flagParser.getStringByFlag(""), + "Expected AssertionError for empty flag"); + } + + @Test + void testGetIndexByFlagValidCase() { + assertEquals(0, flagParser.getIndexByFlag("/p"), + "Expected zero-based index '0' for flag '/p' with value '0'"); + } + + @Test + void testGetIndexByFlagInvalidIndex() { + FlagParser invalidParser = new FlagParser("/p abc"); + assertThrows(ParserException.class, () -> invalidParser.getIndexByFlag("/p"), + "Expected InvalidFormatBuffBuddyException for invalid index"); + } + + @Test + void testGetIntegerByFlagValidCase() { + assertEquals(3, flagParser.getIntegerByFlag("/s"), + "Expected integer value '3' for flag '/s'"); + } + + @Test + void testGetIntegerByFlagInvalidInteger() { + FlagParser invalidParser = new FlagParser("/s abc"); + assertThrows(ParserException.class, () -> invalidParser.getIntegerByFlag("/s"), + "Expected InvalidFormatBuffBuddyException for invalid integer"); + } + + @Test + void testGetFloatByFlagValidCase() { + assertEquals(2.5f, flagParser.getFloatByFlag("/w"), + "Expected float value '2.5' for flag '/w'"); + } + + @Test + void testGetFloatByFlagInvalidFloat() { + FlagParser invalidParser = new FlagParser("/w abc"); + assertThrows(ParserException.class, () -> invalidParser.getFloatByFlag(WATER_INDEX), + "Expected InvalidFormatBuffBuddyException for invalid float"); + } + + @Test + void testGetDateByFlagValidCase() { + assertEquals(LocalDate.of(2023, 12, 12), flagParser.getDateByFlag("/t"), + "Expected date '12-12-2023' for flag '/t'"); + } + + @Test + void testGetDateByFlagInvalidDate() { + FlagParser invalidParser = new FlagParser("/t 32-12-2023"); + assertThrows(ParserException.class, () -> invalidParser.getDateByFlag("/t"), + "Expected InvalidFormatBuffBuddyException for invalid date"); + } + + @Test + void testParseNullArgumentString() { + assertThrows(FlagException.class, () -> new FlagParser(null), + "Expected EmptyInputBuffBuddyException for null argument string"); + } +} diff --git a/src/test/java/parser/ParserTest.java b/src/test/java/parser/ParserTest.java new file mode 100644 index 0000000000..c3cdeaef94 --- /dev/null +++ b/src/test/java/parser/ParserTest.java @@ -0,0 +1,81 @@ +// @@author nirala-ts + +package parser; + +import command.Command; +import command.InvalidCommand; +import command.ExitCommand; +import exceptions.ParserException; +import parser.command.factory.CommandFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +class ParserTest { + + private Parser parser; + private CommandFactory commandFactoryMock; + + @BeforeEach + void setUp() { + commandFactoryMock = mock(CommandFactory.class); + parser = new Parser(commandFactoryMock); + } + + @Test + void testParseValidCommand() { + String fullCommand = "prog start /p 1"; + Command expectedCommand = mock(Command.class); + when(commandFactoryMock.createCommand("prog", "start /p 1")).thenReturn(expectedCommand); + + Command result = parser.parse(fullCommand); + + assertNotNull(result); + assertEquals(expectedCommand, result); + verify(commandFactoryMock).createCommand("prog", "start /p 1"); + } + + @Test + void testParseExitCommand() { + String fullCommand = "bye"; + when(commandFactoryMock.createCommand("bye", "")).thenReturn(new ExitCommand()); + Command result = parser.parse(fullCommand); + + assertInstanceOf(ExitCommand.class, result); + } + + @Test + void testParseInvalidCommand() { + String fullCommand = "unknownCommand"; + when(commandFactoryMock.createCommand("unknownCommand", "")). + thenReturn(new InvalidCommand()); + Command result = parser.parse(fullCommand); + + assertInstanceOf(InvalidCommand.class, result); + } + + @Test + void testParseEmptyCommand() { + assertThrows(ParserException.class, () -> parser.parse(""), + "Should throw EmptyInputBuffBuddyException on empty command"); + } + + @Test + void testParseOnlySpacesCommand() { + assertThrows(ParserException.class, () -> parser.parse(" "), + "Should throw EmptyInputBuffBuddyException on command with only spaces"); + } + + @Test + void testParseNullCommand() { + assertThrows(ParserException.class, () -> parser.parse(null), + "Should throw EmptyInputBuffBuddyException on null command"); + } +} diff --git a/src/test/java/parser/ParserUtilsTest.java b/src/test/java/parser/ParserUtilsTest.java new file mode 100644 index 0000000000..793daeb9f9 --- /dev/null +++ b/src/test/java/parser/ParserUtilsTest.java @@ -0,0 +1,201 @@ +// @@author nirala-ts + +package parser; + +import exceptions.ParserException; +import org.junit.jupiter.api.Test; +import java.time.LocalDate; +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 common.Utils.NULL_INTEGER; +import static common.Utils.NULL_FLOAT; + +class ParserUtilsTest { + + // Tests for splitArguments + @Test + void testSplitArgumentsValidInput() { + String input = "command arg1 arg2"; + String[] result = ParserUtils.splitArguments(input); + assertArrayEquals(new String[]{"command", "arg1 arg2"}, result, + "Should split command and arguments correctly."); + } + + @Test + void testSplitArgumentsEmptyArgumentString() { + String input = "command"; + String[] result = ParserUtils.splitArguments(input); + assertArrayEquals(new String[]{"command", ""}, result, "Should handle missing arguments."); + } + + // Tests for trimInput + @Test + void testTrimInputValidInput() { + String input = " trim this "; + String result = ParserUtils.trimInput(input); + assertEquals("trim this", result, "Should trim leading and trailing whitespace."); + } + + @Test + void testTrimInputEmptyString() { + assertThrows(ParserException.class, () -> ParserUtils.trimInput(" "), + "Should throw PaserExceptions on empty input."); + } + + // Tests for parseInteger + @Test + void testParseIntegerValidInteger() { + String input = "42"; + int result = ParserUtils.parseInteger(input); + assertEquals(42, result, "Should parse valid integer correctly."); + } + + @Test + void testParseIntegerLargeNumberEdgeCase() { + String input = "2147483647"; + int result = ParserUtils.parseInteger(input); // Integer.MAX_VALUE + assertEquals(2147483647, result, "Should parse valid integer correctly."); + } + + @Test + void testParseIntegerNegativeEdgeCase() { + String input = "-1"; + assertThrows(ParserException.class, () -> ParserUtils.parseInteger(input)); + } + + @Test + void testParseIntegerNullInputInvalid() { + int result = ParserUtils.parseInteger(null); + assertEquals(NULL_INTEGER, result, "Should return default NULL_INTEGER when input is null."); + } + + @Test + void testParseIntegerNonNumericInputInvalid() { + assertThrows(ParserException.class, () -> ParserUtils.parseInteger("abc"), + "Should throw PaserExceptions on invalid integer."); + } + + // Tests for parseFloat + @Test + void testParseFloatValidFloat() { + String input = "3.14"; + float result = ParserUtils.parseFloat(input); + assertEquals(3.14f, result, 0.001, "Should parse valid float correctly."); + } + + @Test + void testParseFloatLargeFloatEdgeCase() { + String input = "3.4028235E38"; + float result = ParserUtils.parseFloat(input); // Float.MAX_VALUE + assertEquals(Float.MAX_VALUE, result, 0.001, "Should parse valid float correctly."); + } + + @Test + void testParseFloatSmallNegativeEdgeCase() { + String input = "-0.0001"; + assertThrows(ParserException.class, () -> ParserUtils.parseFloat(input)); + } + + @Test + void testParseFloatNullInputInvalid() { + float result = ParserUtils.parseFloat(null); + assertEquals(NULL_FLOAT, result, "Should return default NULL_FLOAT when input is null."); + } + + @Test + void testParseFloatNonNumericInputInvalid() { + assertThrows(ParserException.class, () -> ParserUtils.parseFloat("abc"), + "Should throw PaserExceptions on invalid float."); + } + + // Tests for parseIndex + @Test + void testParseIndexValidIndex() { + String input = "3"; + int result = ParserUtils.parseIndex(input); + assertEquals(2, result, "Should parse and convert valid index to zero-based."); + } + + @Test + void testParseIndexLargeIndexEdgeCase() { + String input = "2147483647"; // Integer.MAX_VALUE + int result = ParserUtils.parseIndex(input); + assertEquals(2147483646, result, "Should handle large index correctly and convert to zero-based."); + } + + @Test + void testParseIndexZeroIndexEdgeCase() { + String input = "1"; + int result = ParserUtils.parseIndex(input); + assertEquals(0, result, "Should handle zero-based conversion for index 1."); + } + + @Test + void testParseIndexNegativeIndexInvalid() { + assertThrows(ParserException.class, () -> ParserUtils.parseIndex("-1"), + "Should throw PaserExceptions on negative index."); + } + + @Test + void testParseIndexNullInputInvalid() { + int result = ParserUtils.parseIndex(null); + assertEquals(NULL_INTEGER, result, "Should return default NULL_INTEGER when input is null."); + } + + @Test + void testParseIndexNonNumericInputInvalid() { + assertThrows(ParserException.class, () -> ParserUtils.parseIndex("abc"), + "Should throw PaserExceptions on non-numeric index."); + } + + // Tests for parseDate + @Test + void testParseDateValidDate() { + String dateString = "15-08-2023"; // assuming DATE_FORMAT = "dd-MM-yyyy" + LocalDate expectedDate = LocalDate.of(2023, 8, 15); + LocalDate actualDate = ParserUtils.parseDate(dateString); + + assertEquals(expectedDate, actualDate, "The parsed date should match the expected date."); + } + + @Test + void testParseDateCurrentDateWhenNullInput() { + LocalDate actualDate = ParserUtils.parseDate(null); + LocalDate expectedDate = LocalDate.now(); + + assertEquals(expectedDate, actualDate, "When the input is null, the parsed date should be today's date."); + } + + @Test + void testParseDateTrimmedInput() { + String dateString = " 15-08-2023 "; // Input with extra spaces + LocalDate expectedDate = LocalDate.of(2023, 8, 15); + LocalDate actualDate = ParserUtils.parseDate(dateString); + + assertEquals(expectedDate, actualDate, + "The parsed date should match the expected date, ignoring extra spaces."); + } + + @Test + void testParseDateValidLeapYearDate() { + String dateString = "29-02-2024"; // Leap year date + LocalDate expectedDate = LocalDate.of(2024, 2, 29); + LocalDate actualDate = ParserUtils.parseDate(dateString); + + assertEquals(expectedDate, actualDate, "The parsed date should match the expected leap year date."); + } + + @Test + void testParseDateInvalidDayInMonth() { + assertThrows(ParserException.class, () -> ParserUtils.parseDate("31-02-2023"), + "Should throw exception on invalid date (February 31st)."); + } + + @Test + void testParseDateInvalidMonth() { + assertThrows(ParserException.class, () -> ParserUtils.parseDate("31-13-2023"), + "Should throw exception on invalid month (13)."); + } +} + diff --git a/src/test/java/parser/command/factory/CommandFactoryTest.java b/src/test/java/parser/command/factory/CommandFactoryTest.java new file mode 100644 index 0000000000..eeb41d0a90 --- /dev/null +++ b/src/test/java/parser/command/factory/CommandFactoryTest.java @@ -0,0 +1,110 @@ +// @@author nirala-ts + +package parser.command.factory; + +import command.Command; +import command.ExitCommand; +import command.InvalidCommand; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +class CommandFactoryTest { + + private CommandFactory commandFactory; + private ProgrammeCommandFactory progFactoryMock; + private MealCommandFactory mealFactoryMock; + private WaterCommandFactory waterFactoryMock; + private HistoryCommandFactory historyFactoryMock; + + @BeforeEach + void setUp() { + progFactoryMock = mock(ProgrammeCommandFactory.class); + mealFactoryMock = mock(MealCommandFactory.class); + waterFactoryMock = mock(WaterCommandFactory.class); + historyFactoryMock = mock(HistoryCommandFactory.class); + + commandFactory = new CommandFactory(progFactoryMock, mealFactoryMock, waterFactoryMock, historyFactoryMock); + } + + @Test + void testCreateExitCommand() { + String commandString = "bye"; + String argumentString = ""; + + Command result = commandFactory.createCommand(commandString, argumentString); + + assertInstanceOf(ExitCommand.class, result); + } + + @Test + void testCreateInvalidCommand() { + String commandString = "unknownCommand"; + String argumentString = ""; + + Command result = commandFactory.createCommand(commandString, argumentString); + + assertInstanceOf(InvalidCommand.class, result); + } + + @Test + void testCreateProgCommand() { + String commandString = "prog"; + String argumentString = "start /p 1"; + Command expectedCommand = mock(Command.class); + + when(progFactoryMock.parse(argumentString)).thenReturn(expectedCommand); + + Command result = commandFactory.createCommand(commandString, argumentString); + + assertEquals(expectedCommand, result); + verify(progFactoryMock).parse(argumentString); + } + + @Test + void testCreateMealCommand() { + String commandString = "meal"; + String argumentString = "add /name Sample Meal"; + Command expectedCommand = mock(Command.class); + + when(mealFactoryMock.parse(argumentString)).thenReturn(expectedCommand); + + Command result = commandFactory.createCommand(commandString, argumentString); + + assertEquals(expectedCommand, result); + verify(mealFactoryMock).parse(argumentString); + } + + @Test + void testCreateWaterCommand() { + String commandString = "water"; + String argumentString = "log /volume 500"; + Command expectedCommand = mock(Command.class); + + when(waterFactoryMock.parse(argumentString)).thenReturn(expectedCommand); + + Command result = commandFactory.createCommand(commandString, argumentString); + + assertEquals(expectedCommand, result); + verify(waterFactoryMock).parse(argumentString); + } + + @Test + void testCreateHistoryCommand() { + String commandString = "history"; + String argumentString = "view /d 11-11-2023"; + Command expectedCommand = mock(Command.class); + + when(historyFactoryMock.parse(argumentString)).thenReturn(expectedCommand); + + Command result = commandFactory.createCommand(commandString, argumentString); + + assertEquals(expectedCommand, result); + verify(historyFactoryMock).parse(argumentString); + } +} diff --git a/src/test/java/parser/command/factory/HistoryCommandFactoryTest.java b/src/test/java/parser/command/factory/HistoryCommandFactoryTest.java new file mode 100644 index 0000000000..3a4ee1dff2 --- /dev/null +++ b/src/test/java/parser/command/factory/HistoryCommandFactoryTest.java @@ -0,0 +1,61 @@ +package parser.command.factory; + +import command.Command; +import command.InvalidCommand; +import command.history.HistoryCommand; +import command.history.ViewPersonalBestCommand; +import command.history.ListPersonalBestsCommand; +import command.history.WeeklySummaryCommand; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class HistoryCommandFactoryTest { + + private HistoryCommandFactory historyCommandFactory; + + @BeforeEach + public void setUp() { + historyCommandFactory = new HistoryCommandFactory(); + } + + @Test + public void testParseHistoryCommand() { + // Test that "history" returns a HistoryCommand instance + Command command = historyCommandFactory.parse("view"); + assertInstanceOf(HistoryCommand.class, command, "Expected HistoryCommand instance for 'history view'."); + } + + @Test + public void testParseListPersonalBestsCommand() { + // Test that "history pb" with no additional arguments returns a ListPersonalBestsCommand instance + Command command = historyCommandFactory.parse("pb"); + assertInstanceOf(ListPersonalBestsCommand.class, command, + "Expected ListPersonalBestsCommand instance for 'history pb'."); + } + + @Test + public void testParseViewPersonalBestCommand() { + // Test that "history pb [exercise]" returns a ViewPersonalBestCommand instance + Command command = historyCommandFactory.parse("pb Bench_Press"); + assertInstanceOf(ViewPersonalBestCommand.class, command, + "Expected ViewPersonalBestCommand instance for 'history pb Bench_Press'."); + } + + @Test + public void testParseWeeklySummaryCommand() { + // Test that "history wk" returns a WeeklySummaryCommand instance + Command command = historyCommandFactory.parse("wk"); + assertInstanceOf(WeeklySummaryCommand.class, command, + "Expected WeeklySummaryCommand instance for 'history wk'."); + } + + @Test + public void testParseInvalidCommand() { + // Test that an invalid subcommand returns an InvalidCommand instance + Command command = historyCommandFactory.parse("unknown"); + assertInstanceOf(InvalidCommand.class, command, "Expected InvalidCommand instance for unknown subcommand."); + } +} + diff --git a/src/test/java/parser/command/factory/MealCommandFactoryTest.java b/src/test/java/parser/command/factory/MealCommandFactoryTest.java new file mode 100644 index 0000000000..aacb62ca1f --- /dev/null +++ b/src/test/java/parser/command/factory/MealCommandFactoryTest.java @@ -0,0 +1,89 @@ +package parser.command.factory; + +import command.Command; +import command.meals.AddMealCommand; +import command.meals.DeleteMealCommand; +import command.meals.ViewMealCommand; +import exceptions.FlagException; +import exceptions.ParserException; +import meal.Meal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MealCommandFactoryTest { + + private MealCommandFactory mealCommandFactory; + + @BeforeEach + public void setUp() { + mealCommandFactory = new MealCommandFactory(); + } + + @Test + public void testParseAddMealCommand() { + String argumentString = "add /n Sample Meal /c 300 /t 31-10-2024"; + AddMealCommand expectedCommand = new AddMealCommand(new Meal("Sample Meal", 300), LocalDate.of(2024, 10, 31)); + + Command result = mealCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result, "Parsed command should be equal to the expected AddMealCommand."); + } + + @Test + public void testParseDeleteMealCommand() { + String argumentString = "delete /m 1 /t 31-10-2024"; + DeleteMealCommand expectedCommand = new DeleteMealCommand(0, LocalDate.of(2024, 10, 31)); + + Command result = mealCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result, "Parsed command should be equal to the expected DeleteMealCommand."); + } + + @Test + public void testParseViewMealCommand() { + String argumentString = "view 31-10-2024"; + ViewMealCommand expectedCommand = new ViewMealCommand(LocalDate.of(2024, 10, 31)); + + Command result = mealCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result, "Parsed command should be equal to the expected ViewMealCommand."); + } + + @Test + public void testPrepareAddCommandMissingNameFlag() { + String argumentString = "/c 300 /t 31-10-2024"; + + assertThrows(FlagException.class, () -> mealCommandFactory.prepareAddCommand(argumentString), + "Missing required flag /n should throw FlagException."); + } + + @Test + public void testPrepareAddCommandMissingCaloriesFlag() { + String argumentString = "/n Sample meal /t 31-10-2024"; + + assertThrows(FlagException.class, () -> mealCommandFactory.prepareAddCommand(argumentString), + "Missing required flag /c should throw FlagException."); + } + + @Test + public void testPrepareDeleteCommandMissingIndexFlag() { + String argumentString = "/t 31-10-2024"; + + assertThrows(FlagException.class, () -> mealCommandFactory.prepareDeleteCommand(argumentString), + "Missing required flag /m should throw FlagException."); + } + + @Test + public void testPrepareViewCommandInvalidDate() { + String argumentString = "invalid-date"; + + assertThrows(ParserException.class, () -> mealCommandFactory.prepareViewCommand(argumentString), + "Invalid date format should throw InvalidFormatBuffBuddyException."); + } +} + diff --git a/src/test/java/parser/command/factory/ProgrammeCommandFactoryTest.java b/src/test/java/parser/command/factory/ProgrammeCommandFactoryTest.java new file mode 100644 index 0000000000..67c0ec5147 --- /dev/null +++ b/src/test/java/parser/command/factory/ProgrammeCommandFactoryTest.java @@ -0,0 +1,318 @@ +// @@author nirala-ts + +package parser.command.factory; + +import command.Command; +import command.InvalidCommand; +import command.programme.CreateProgrammeCommand; +import command.programme.DeleteProgrammeCommand; +import command.programme.ListProgrammeCommand; +import command.programme.StartProgrammeCommand; +import command.programme.ViewProgrammeCommand; +import command.programme.LogProgrammeCommand; + +import exceptions.ProgrammeException; +import exceptions.ParserException; +import exceptions.FlagException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import programme.Day; +import programme.Exercise; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ProgrammeCommandFactoryTest { + private ProgrammeCommandFactory programmeCommandFactory; + + @BeforeEach + void setUp() { + programmeCommandFactory = new ProgrammeCommandFactory(); + } + + // Tests for parse + @Test + void testParseValidCreateCommand() { + String argumentString = "create /p MyProgram /d Day1"; + + Command result = programmeCommandFactory.parse(argumentString); + + assertInstanceOf(CreateProgrammeCommand.class, result); + } + + @Test + void testParseValidListCommand() { + String argumentString = "list"; + + Command result = programmeCommandFactory.parse(argumentString); + + assertInstanceOf(ListProgrammeCommand.class, result); + } + + @Test + void testParseInvalidCommand() { + String argumentString = "unknownCommand"; + + Command result = programmeCommandFactory.parse(argumentString); + + assertInstanceOf(InvalidCommand.class, result); + } + + // Tests for prepareCreateCommand + @Test + public void testPrepareCreateCommandValidInput() { + String argumentString = "MyProgram /d Day1 /e /name PushUps /set 3 /rep 15 /w 10 /c 50"; + + Day expectedDay = new Day("Day1"); + expectedDay.insertExercise(new Exercise(3, 15, 10, 50, "PushUps")); + + ArrayList expectedDays = new ArrayList<>(List.of(expectedDay)); + CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyProgram", expectedDays); + + Command result = programmeCommandFactory.parse("create " + argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareCreateCommandNoDaysNoExercises() { + String argumentString = "MyEmptyProgram"; + + CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyEmptyProgram", + new ArrayList<>()); + + Command result = programmeCommandFactory.parse("create " + argumentString); + + assertEquals(expectedCommand, result); + } + + + @Test + public void testPrepareCreateCommandMultipleDays() { + String argumentString = "MyProgram /d Day1 /e /name PushUps /set 3 /rep 15 /w 10 /c 50 /d " + + "Day2 /e /name SitUps /set 2 /rep 20 /w 10 /c 30"; + + Day day1 = new Day("Day1"); + day1.insertExercise(new Exercise(3, 15, 10, 50, "PushUps")); + + Day day2 = new Day("Day2"); + day2.insertExercise(new Exercise(2, 20, 10, 30, "SitUps")); + + ArrayList expectedDays = new ArrayList<>(Arrays.asList(day1, day2)); + CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyProgram", expectedDays); + + Command result = programmeCommandFactory.parse("create " + argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareCreateCommandMultipleDaysMultipleExercises() { + String argumentString = "MyProgram " + + "/d Day1 /e /name PushUps /set 3 /rep 15 /w 10 /c 50 " + + "/e /name SitUps /set 2 /rep 20 /w 10 /c 30 " + + "/d Day2 /e /name Squats /set 4 /rep 10 /w 20 /c 100 " + + "/e /name Lunges /set 3 /rep 12 /w 15 /c 80"; + + Day day1 = new Day("Day1"); + day1.insertExercise(new Exercise(3, 15, 10, 50, "PushUps")); + day1.insertExercise(new Exercise(2, 20, 10, 30, "SitUps")); + + Day day2 = new Day("Day2"); + day2.insertExercise(new Exercise(4, 10, 20, 100, "Squats")); + day2.insertExercise(new Exercise(3, 12, 15, 80, "Lunges")); + + ArrayList expectedDays = new ArrayList<>(Arrays.asList(day1, day2)); + CreateProgrammeCommand expectedCommand = new CreateProgrammeCommand("MyProgram", expectedDays); + + Command result = programmeCommandFactory.parse("create " + argumentString); + + assertEquals(expectedCommand, result); + } + + @Test void testPrepareCreateCommandNegativeExerciseParameter() { + String argumentString = "MyProgram /d Day1 /e /name PushUps /set -3 /rep 15 /w 5 /c 50"; + + assertThrows(ParserException.class, + () -> programmeCommandFactory.parse("create " + argumentString)); + } + + @Test + public void testPrepareCreateCommandMissingProgrammeName() { + String argumentString = "/d Day1 /e /name PushUps /set 3 /rep 15 /w 0 /c 50"; + + assertThrows(ProgrammeException.class, + () -> programmeCommandFactory.parse("create " + argumentString)); + } + + @Test + public void testPrepareCreateCommandInvalidDayFormat() { + String argumentString = "MyProgram /d /e /name PushUps /set 3 /rep 15 /w 0 /c 50"; + + assertThrows(ProgrammeException.class, + () -> programmeCommandFactory.parse("create " + argumentString)); + } + + @Test + public void testPrepareCreateCommandInvalidExerciseFormat() { + String argumentString = "MyProgram /d Day1 /e /name PushUps /set 3 /rep 15 /w invalid /c 50"; + + assertThrows(ParserException.class, + () -> programmeCommandFactory.parse("create " + argumentString)); + } + + @Test + public void testPrepareCreateCommandMissingExerciseName() { + String argumentString = "MyProgram /d Day1 /e /name /set 3 /rep 15 /w 0 /c 50"; + + assertThrows(FlagException.class, + () -> programmeCommandFactory.parse("create " + argumentString)); + } + + @Test + public void testPrepareCreateCommandMissingExerciseFlag() { + String argumentString = "MyProgram /d Day1 /e /name Lunges /rep 15 /w 0 /c 50"; + + assertThrows(FlagException.class, + () -> programmeCommandFactory.parse("create " + argumentString)); + } + + + // Tests for prepareViewCommand + @Test + public void testPrepareViewCommandValidIndex() { + String argumentString = "view 1"; + ViewProgrammeCommand expectedCommand = new ViewProgrammeCommand(0); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareViewCommandInvalidIndexFormat() { + String argumentString = "view invalidIndex"; + + assertThrows(ParserException.class, () -> programmeCommandFactory.parse(argumentString)); + } + + @Test + public void testPrepareViewCommandNoIndex() { + String argumentString = "view"; + ViewProgrammeCommand expectedCommand = new ViewProgrammeCommand(-1); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + // Tests for prepareStartCommand + @Test + public void testPrepareStartCommandValidIndex() { + String argumentString = "start 1"; + StartProgrammeCommand expectedCommand = new StartProgrammeCommand(0); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareStartCommandNoIndex() { + String argumentString = "start"; + + assertThrows(ParserException.class, () -> programmeCommandFactory.parse( argumentString)); + } + + @Test + public void testPrepareStartCommandInvalidIndexFormat() { + String argumentString = "start invalidIndex"; + + assertThrows(ParserException.class, () -> programmeCommandFactory.parse(argumentString)); + } + + // Tests for prepareDeleteCommand + @Test + public void testPrepareDeleteCommandValidIndex() { + String argumentString = "delete 1"; + DeleteProgrammeCommand expectedCommand = new DeleteProgrammeCommand(0); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareDeleteCommandInvalidIndexFormat() { + String argumentString = "delete invalidIndex"; + + assertThrows(ParserException.class, () -> programmeCommandFactory.parse(argumentString)); + } + + @Test + public void testPrepareDeleteCommandNoIndex() { + String argumentString = "delete"; + DeleteProgrammeCommand expectedCommand = new DeleteProgrammeCommand(-1); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + // Tests for prepareLogCommand + @Test + public void testPrepareLogCommandValidArgumentsAllFlags() { + String argumentString = "log /p 1 /d 1 /date 05-11-2023"; + LogProgrammeCommand expectedCommand = new LogProgrammeCommand(0, 0, + LocalDate.of(2023, 11, 5)); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareLogCommandMissingDayFlag() { + String argumentString = "log /p 1 /date 05-11-2023"; + + assertThrows(FlagException.class, () -> programmeCommandFactory.parse( argumentString)); + } + + @Test + public void testPrepareLogCommandInvalidDateFormat() { + //Expected format: dd-MM-yyyy + String argumentString = "log /p 1 /d 0 /date 2023-11-05"; + + assertThrows(ParserException.class, () -> programmeCommandFactory.parse( argumentString)); + } + + @Test + public void testPrepareLogCommandMissingDateFlag() { + String argumentString = "log /p 1 /d 1"; + LocalDate currentDate = LocalDate.now(); + LogProgrammeCommand expectedCommand = new LogProgrammeCommand(0, 0, currentDate); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } + + @Test + public void testPrepareLogCommandMissingDateAndProgIndexFlag() { + String argumentString = "log /d 4"; + + LocalDate currentDate = LocalDate.now(); + LogProgrammeCommand expectedCommand = new LogProgrammeCommand(-1, 3, currentDate); + + Command result = programmeCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result); + } +} diff --git a/src/test/java/parser/command/factory/WaterCommandFactoryTest.java b/src/test/java/parser/command/factory/WaterCommandFactoryTest.java new file mode 100644 index 0000000000..43c89dc4d3 --- /dev/null +++ b/src/test/java/parser/command/factory/WaterCommandFactoryTest.java @@ -0,0 +1,84 @@ +package parser.command.factory; + +import command.Command; +import command.water.AddWaterCommand; +import command.water.DeleteWaterCommand; +import command.water.ViewWaterCommand; +import exceptions.FlagException; +import exceptions.ParserException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class WaterCommandFactoryTest { + + private WaterCommandFactory waterCommandFactory; + + @BeforeEach + public void setUp() { + waterCommandFactory = new WaterCommandFactory(); + } + + @Test + public void testParseAddWaterCommand() { + String argumentString = "add /v 1.5 /t 31-10-2024"; + AddWaterCommand expectedCommand = new AddWaterCommand(1.5f, LocalDate.of(2024, 10, 31)); + + Command result = waterCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result, "Parsed command should be equal to the expected AddWaterCommand."); + } + + @Test + public void testParseDeleteWaterCommand() { + String argumentString = "delete /w 1 /t 31-10-2024"; + DeleteWaterCommand expectedCommand = new DeleteWaterCommand(0, LocalDate.of(2024, 10, 31)); + + Command result = waterCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result, "Parsed command should be equal to the expected DeleteWaterCommand."); + } + + @Test + public void testParseViewWaterCommand() { + String argumentString = "view 31-10-2024"; + ViewWaterCommand expectedCommand = new ViewWaterCommand(LocalDate.of(2024, 10, 31)); + + Command result = waterCommandFactory.parse(argumentString); + + assertEquals(expectedCommand, result, "Parsed command should be equal to the expected ViewWaterCommand."); + } + + @Test + public void testPrepareAddCommandMissingRequiredFlags() { + // Missing /v (volume) flag + String argumentString = "/t 31-10-2024"; + + assertThrows(FlagException.class, () -> waterCommandFactory.prepareAddCommand(argumentString), + "Missing required flag /v should throw FlagException."); + } + + @Test + public void testPrepareDeleteCommandMissingRequiredFlags() { + // Missing /w (water index) flag + String argumentString = "/t 31-10-2024"; + + assertThrows(FlagException.class, + () -> waterCommandFactory.prepareDeleteCommand(argumentString), + "Missing required flag /w should throw FlagException."); + } + + @Test + public void testPrepareViewCommandInvalidDate() { + String argumentString = "invalid-date"; + + assertThrows(ParserException.class, + () -> waterCommandFactory.prepareViewCommand(argumentString), + "Invalid date format should throw FlagException."); + } +} + diff --git a/src/test/java/programme/DayTest.java b/src/test/java/programme/DayTest.java new file mode 100644 index 0000000000..4f9b5d0f09 --- /dev/null +++ b/src/test/java/programme/DayTest.java @@ -0,0 +1,118 @@ +// @@author nirala-ts + +package programme; + +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DayTest { + + private Day day; + private Exercise exercise1; + private Exercise exercise2; + + @BeforeEach + void setUp() { + exercise1 = new Exercise(3, 10, 50, 160, "Bench_Press"); + exercise2 = new Exercise(3, 12, 20, 100, "Triceps_Extension"); + day = new Day("Push"); + } + + @Test + void testConstructorWithEmptyExerciseList() { + Day expectedDay = new Day("Push", new ArrayList<>()); + assertEquals(expectedDay, day); + } + + @Test + void testConstructorWithPredefinedExerciseList() { + ArrayList exercises = new ArrayList<>(Arrays.asList(exercise1, exercise2)); + Day predefinedDay = new Day("Push", exercises); + Day expectedDay = new Day("Push", exercises); + + assertEquals(expectedDay, predefinedDay); + } + + @Test + void testInsertExercise() { + day.insertExercise(exercise1); + day.insertExercise(exercise2); + + ArrayList expectedExercises = new ArrayList<>(Arrays.asList(exercise1, exercise2)); + Day expectedDay = new Day("Push", expectedExercises); + + assertEquals(expectedDay, day); + } + + @Test + void testGetExerciseInvalidIndex() { + assertThrows(ProgrammeException.class, () -> day.getExercise(0)); + } + + @Test + void testDeleteExercise() { + day.insertExercise(exercise1); + day.insertExercise(exercise2); + + day.deleteExercise(0); + + ArrayList expectedExercises = new ArrayList<>(Collections.singletonList(exercise2)); + Day expectedDay = new Day("Push", expectedExercises); + + assertEquals(expectedDay, day); + } + + @Test + void testDeleteExerciseInvalidIndex() { + assertThrows(ProgrammeException.class, () -> day.deleteExercise(0)); + } + + @Test + void testGetTotalCaloriesBurnt() { + day.insertExercise(exercise1); + day.insertExercise(exercise2); + + int totalCalories = day.getTotalCaloriesBurnt(); + + assertEquals(260, totalCalories); // 160 + 100 + } + + @Test + void testToString() { + day.insertExercise(exercise1); + day.insertExercise(exercise2); + + String expectedOutput = "Push\n" + + "1. Bench_Press: 3 sets of 10 at 50kg | Burnt 160 kcal\n" + + "2. Triceps_Extension: 3 sets of 12 at 20kg | Burnt 100 kcal\n"; + assertEquals( + expectedOutput.replace("\r\n", "\n").replace("\r", "\n"), + day.toString().replace("\r\n", "\n").replace("\r", "\n") + ); + } + + @Test + void testEqualsAndHashCode() { + ArrayList exercises = new ArrayList<>(Arrays.asList(exercise1, exercise2)); + Day day1 = new Day("Push", exercises); + Day day2 = new Day("Push", new ArrayList<>(Arrays.asList(exercise1, exercise2))); + + assertEquals(day1, day2); + assertEquals(day1.hashCode(), day2.hashCode()); + } + + @Test + void testNotEquals() { + Day otherDay = new Day("Pull", new ArrayList<>(Arrays.asList(exercise1, exercise2))); + assertNotEquals(day, otherDay); + } +} diff --git a/src/test/java/programme/ExerciseTest.java b/src/test/java/programme/ExerciseTest.java new file mode 100644 index 0000000000..6dcaa52337 --- /dev/null +++ b/src/test/java/programme/ExerciseTest.java @@ -0,0 +1,128 @@ +// @@author nirala-ts + +package programme; + +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.assertNotEquals; +import static common.Utils.NULL_INTEGER; + + +class ExerciseTest { + + + + private Exercise exercise; + + @BeforeEach + void setUp() { + exercise = new Exercise(1, 2, 3, 400, "Bench_Press"); + } + + @Test + void testConstructor() { + Exercise expectedExercise = new Exercise(1, 2, 3, 400, "Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testUpdateExerciseSets() { + ExerciseUpdate update = new ExerciseUpdate(5, NULL_INTEGER, + NULL_INTEGER, NULL_INTEGER, null); + exercise.updateExercise(update); + + Exercise expectedExercise = new Exercise(5, 2, + 3, 400, "Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testUpdateExerciseReps() { + ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, 12, + NULL_INTEGER, NULL_INTEGER, null); + exercise.updateExercise(update); + + Exercise expectedExercise = new Exercise(1, 12, + 3, 400, "Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testUpdateExerciseWeight() { + ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, NULL_INTEGER, + 60, NULL_INTEGER, null); + exercise.updateExercise(update); + + Exercise expectedExercise = new Exercise(1, 2, + 60, 400, "Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testUpdateExerciseCalories() { + ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, NULL_INTEGER, + NULL_INTEGER, 200, null); + exercise.updateExercise(update); + + Exercise expectedExercise = new Exercise(1, 2, + 3, 200, "Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testUpdateExerciseName() { + ExerciseUpdate update = new ExerciseUpdate(NULL_INTEGER, NULL_INTEGER, + NULL_INTEGER, NULL_INTEGER, "Incline_Bench_Press"); + exercise.updateExercise(update); + + Exercise expectedExercise = new Exercise(1, 2, + 3, 400, "Incline_Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testUpdateMultipleFields() { + ExerciseUpdate update = new ExerciseUpdate(4, 12, + 55, 180, "Incline_Bench_Press"); + exercise.updateExercise(update); + + Exercise expectedExercise = new Exercise(4, 12, + 55, 180, "Incline_Bench_Press"); + assertEquals(expectedExercise, exercise); + } + + @Test + void testEqualsAndHashCode() { + Exercise exerciseCopy = new Exercise(1, 2, 3, 400, "Bench_Press"); + assertEquals(exercise, exerciseCopy); + assertEquals(exercise.hashCode(), exerciseCopy.hashCode()); + } + + @Test + void testNotEquals() { + Exercise differentExercise = new Exercise(3, 10, 50, 160, "Squats"); + assertNotEquals(exercise, differentExercise); + } + + @Test + void testToString() { + String expectedString = "Bench_Press: 1 sets of 2 at 3kg | Burnt 400 kcal"; + assertEquals(expectedString, exercise.toString()); + } + + @Test + void testGetCalories() { + assertEquals(400, exercise.getCalories()); + } + + @Test + void testGetName() { + assertEquals("Bench_Press", exercise.getName()); + } + + @Test + void testGetWeight() { assertEquals(3, exercise.getWeight()); } +} + diff --git a/src/test/java/programme/ProgrammeListTest.java b/src/test/java/programme/ProgrammeListTest.java new file mode 100644 index 0000000000..95c1aca3f3 --- /dev/null +++ b/src/test/java/programme/ProgrammeListTest.java @@ -0,0 +1,151 @@ +// @@author nirala-ts + +package programme; + +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ProgrammeListTest { + + private ProgrammeList programmeList; + private Programme mockProgramme1; + private Programme mockProgramme2; + private Day mockDay1; + private Day mockDay2; + + @BeforeEach + public void setUp() { + programmeList = new ProgrammeList(); + + // Create mock Programme and Day objects + mockProgramme1 = mock(Programme.class); + mockProgramme2 = mock(Programme.class); + mockDay1 = mock(Day.class); + mockDay2 = mock(Day.class); + + // Stub methods for mock programmes + when(mockProgramme1.getProgrammeName()).thenReturn("Mocked Programme 1"); + when(mockProgramme2.getProgrammeName()).thenReturn("Mocked Programme 2"); + when(mockProgramme1.getDay(0)).thenReturn(mockDay1); + when(mockProgramme1.deleteDay(0)).thenReturn(mockDay1); + + // Insert mock Programmes into ProgrammeList + ArrayList days1 = new ArrayList<>(); + days1.add(mockDay1); + programmeList.insertProgramme("Mocked Programme 1", days1); + + ArrayList days2 = new ArrayList<>(); + days2.add(mockDay2); + programmeList.insertProgramme("Mocked Programme 2", days2); + + // Replace with mocked Programmes + programmeList.getProgrammeList().set(0, mockProgramme1); + programmeList.getProgrammeList().set(1, mockProgramme2); + } + + @Test + void testInsertProgramme() { + ArrayList days = new ArrayList<>(); + days.add(mockDay1); + Programme newProgramme = programmeList.insertProgramme("New Programme", days); + + assertEquals(newProgramme, programmeList.getProgramme(2)); + assertEquals(3, programmeList.getProgrammeListSize()); + } + + @Test + void testDeleteProgrammeValidIndex() { + Programme deletedProgramme = programmeList.deleteProgram(0); + + assertEquals(mockProgramme1, deletedProgramme); + assertEquals(1, programmeList.getProgrammeListSize()); + } + + @Test + void testDeleteActiveProgramme() { + if (programmeList.getCurrentActiveProgramme() != 0) { + programmeList.startProgramme(0); + } + Programme deletedProgramme = programmeList.deleteProgram(-1); + assertEquals(mockProgramme1, deletedProgramme); + assertEquals(1, programmeList.getProgrammeListSize()); + } + + + @Test + void testDeleteProgrammeInvalidIndex() { + assertThrows(ProgrammeException.class, () -> programmeList.deleteProgram(5)); + + assertEquals(2, programmeList.getProgrammeListSize()); + } + + @Test + void testGetActiveProgramme() { + if (programmeList.getCurrentActiveProgramme() != 0) { + programmeList.startProgramme(0); + } + Programme activeProgramme = programmeList.getProgramme(-1); + assertEquals(mockProgramme1, activeProgramme); + } + + @Test + void testGetProgrammeValidIndex() { + Programme programme = programmeList.getProgramme(1); + + assertEquals(mockProgramme2, programme); + } + + @Test + void testGetCurrentActiveProgrammeWithActiveProgramme() { + // Start a programme and check if the index is correctly returned + programmeList.startProgramme(1); + int currentActive = programmeList.getCurrentActiveProgramme(); + assertEquals(1, currentActive, "Should return the index of the current active programme."); + } + + @Test + void testGetProgrammeInvalidIndex() { + assertThrows(ProgrammeException.class, () -> programmeList.getProgramme(5)); + } + + @Test + void testStartProgramme() { + Programme activeProgramme = programmeList.startProgramme(1); + + assertEquals(mockProgramme2, activeProgramme); + } + + @Test + void testStartProgrammeInvalidIndex() { + assertThrows(ProgrammeException.class, () -> programmeList.startProgramme(5)); + } + + @Test + void testToString() { + if (programmeList.getCurrentActiveProgramme() != 0) { + programmeList.startProgramme(0); + } + String programmeListString = programmeList.toString(); + String expectedString = "1. Mocked Programme 1 -- Active\n2. Mocked Programme 2\n"; + + assertEquals(expectedString, programmeListString); + } + + @Test + void testToStringEmptyList() { + ProgrammeList emptyProgrammeList = new ProgrammeList(); + + String programmeListString = emptyProgrammeList.toString(); + String expectedString = "No programmes found."; + + assertEquals(expectedString, programmeListString); + } +} diff --git a/src/test/java/programme/ProgrammeTest.java b/src/test/java/programme/ProgrammeTest.java new file mode 100644 index 0000000000..33af7a2150 --- /dev/null +++ b/src/test/java/programme/ProgrammeTest.java @@ -0,0 +1,101 @@ +// @@author nirala-ts + +package programme; + +import exceptions.ProgrammeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +public class ProgrammeTest { + + private Programme programme; + private Day mockDay1; + private Day mockDay2; + + @BeforeEach + public void setUp() { + mockDay1 = mock(Day.class); + mockDay2 = mock(Day.class); + + ArrayList dayList = new ArrayList<>(); + dayList.add(mockDay1); + programme = new Programme("Mocked Programme", dayList); + } + + @Test + void testConstructor() { + ArrayList dayList = new ArrayList<>(); + dayList.add(mockDay1); + Programme newProgramme = new Programme("Test Programme", dayList); + + assertEquals("Test Programme", newProgramme.getProgrammeName()); + assertEquals(1, newProgramme.getDayCount()); + } + + @Test + void testGetProgrammeName() { + assertEquals("Mocked Programme", programme.getProgrammeName()); + } + + @Test + void testGetDayValidIndex() { + Day day = programme.getDay(0); + + assertEquals(mockDay1, day); + } + + @Test + void testGetDayInvalidIndex() { + assertThrows(ProgrammeException.class, () -> programme.getDay(5)); + } + + @Test + void testInsertDay() { + programme.insertDay(mockDay2); + + assertEquals(2, programme.getDayCount()); + assertEquals(mockDay2, programme.getDay(1)); + } + + @Test + void testDeleteDayValidIndex() { + Day deletedDay = programme.deleteDay(0); + + assertEquals(mockDay1, deletedDay); + assertEquals(0, programme.getDayCount()); + } + + @Test + void testDeleteDayInvalidIndex() { + assertThrows(ProgrammeException.class, () -> programme.deleteDay(5)); + } + + @Test + void testGetDayCount() { + assertEquals(1, programme.getDayCount()); + programme.insertDay(mockDay2); + assertEquals(2, programme.getDayCount()); + } + + @Test + void testToString() { + programme.insertDay(mockDay2); + String expectedString = "Mocked Programme\n\nDay 1: " + mockDay1 + "\nDay 2: " + mockDay2 + "\n"; + + assertEquals(expectedString, programme.toString()); + } + + @Test + void testToStringEmptyProgramme() { + Programme emptyProgramme = new Programme("Empty Programme", new ArrayList<>()); + String expectedString = "Empty Programme\n\n"; + + assertEquals(expectedString, emptyProgramme.toString()); + } +} diff --git a/src/test/java/storage/DateSerializerTest.java b/src/test/java/storage/DateSerializerTest.java new file mode 100644 index 0000000000..84a5ba676e --- /dev/null +++ b/src/test/java/storage/DateSerializerTest.java @@ -0,0 +1,70 @@ +//@@author Bev-low + +package storage; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DateSerializerTest { + private DateSerializer dateSerializer; + + @BeforeEach + public void setUp() { + dateSerializer = new DateSerializer(); + } + + @Test + public void testSerialize_validDate() { + LocalDate date = LocalDate.of(2024, 12, 12); + JsonElement jsonElement = dateSerializer.serialize(date, LocalDate.class, null); + assertEquals("12-12-2024", jsonElement.getAsString()); + } + + @Test + public void testSerialize_invalidDate() { + LocalDate invalidDate = null; + assertThrows(AssertionError.class, () -> { + dateSerializer.serialize(invalidDate, LocalDate.class, null); + }, "Serializer should throw an AssertionError for null input."); + } + + @Test + public void testSerialize_emptyDate() { + assertThrows(Exception.class, () -> { + LocalDate emptyDate = LocalDate.parse(""); + dateSerializer.serialize(emptyDate, LocalDate.class, null); + }, "Serializer should throw an exception for empty date string."); + } + + @Test + public void testDeserialize_validJson() { + JsonElement jsonElement = new JsonPrimitive("12-12-2024"); + LocalDate date = dateSerializer.deserialize(jsonElement, LocalDate.class, null); + assertEquals(LocalDate.of(2024, 12, 12), date); + } + + @Test + public void testDeserialize_invalidJson() { + JsonElement invalidJson = new JsonPrimitive("invalid-date"); + assertThrows(DateTimeParseException.class, () -> { + dateSerializer.deserialize(invalidJson, LocalDate.class, null); + }); + } + + @Test + public void testDeserialize_emptyJson() { + JsonElement emptyJson = new JsonPrimitive(""); + assertThrows(DateTimeParseException.class, () -> { + dateSerializer.deserialize(emptyJson, LocalDate.class, null); + }); + } +} + diff --git a/src/test/java/storage/FileManagerTest.java b/src/test/java/storage/FileManagerTest.java new file mode 100644 index 0000000000..dfdab458c8 --- /dev/null +++ b/src/test/java/storage/FileManagerTest.java @@ -0,0 +1,107 @@ +//@@author Bev-low + +package storage; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FileManagerTest { + private FileManager fileManager; + private final String testFilePath = "./src/test/resources/test_data.json"; + + @BeforeEach + public void setUp() { + fileManager = new FileManager(testFilePath); + File file = new File(testFilePath); + if (file.exists()) { + file.delete(); + } + } + + private void createTestFile(String content) throws IOException { + try (FileWriter writer = new FileWriter(testFilePath)) { + writer.write(content); + } + } + + @Test + public void testLoadProgrammeList_trivialCase() throws IOException { + JsonObject testData = new JsonObject(); + testData.add("programmeList", JsonParser.parseString("{ \"programme\": \"test\" }")); + createTestFile(testData.toString()); + + JsonObject result = fileManager.loadProgrammeList(); + assertTrue(result.has("programme"), "Programme list should contain the expected 'programme' key."); + } + + @Test + public void testLoadProgrammeList_fileNotFound() { + JsonObject result = fileManager.loadProgrammeList(); + assertTrue(result.isJsonNull() || result.size() == 0, "ProgrammeList should " + + "be empty when the file doesn't exist."); + } + + @Test + public void testLoadProgrammeList_noProgrammeListKey() throws IOException { + createTestFile("{}"); + JsonObject result = fileManager.loadProgrammeList(); + assertEquals(0, result.size(), "Programme list should be empty when no 'programmeList' key is present."); + } + + @Test + public void testLoadHistory_trivialCase() throws IOException { + JsonObject testData = new JsonObject(); + testData.add("history", JsonParser.parseString("{ \"date\": \"2024-10-28\" }")); + createTestFile(testData.toString()); + + JsonObject result = fileManager.loadHistory(); + assertTrue(result.has("date"), "History should contain the expected 'date' key."); + } + + @Test + public void testLoadHistory_fileNotFound() { + JsonObject result = fileManager.loadHistory(); + assertTrue(result.isJsonNull() || result.size() == 0, "History should be empty when the file doesn't exist."); + } + + @Test + public void testLoadHistory_noHistoryKey() throws IOException { + createTestFile("{}"); + JsonObject result = fileManager.loadHistory(); + assertEquals(0, result.size(), "History should be empty when no 'history' key is present."); + } + + @Test + public void testSave_trivialCase() throws IOException { + JsonObject testData = new JsonObject(); + testData.addProperty("key", "value"); + + fileManager.save(testData); + String fileContent = Files.readString(Path.of(testFilePath)); + JsonObject result = JsonParser.parseString(fileContent).getAsJsonObject(); + + assertEquals("value", result.get("key").getAsString(), "Saved data should contain the key and value."); + } + + @Test + public void testSave_emptyData() throws IOException { + JsonObject emptyData = new JsonObject(); + fileManager.save(emptyData); + + String fileContent = Files.readString(Path.of(testFilePath)); + JsonObject result = JsonParser.parseString(fileContent).getAsJsonObject(); + + assertEquals(0, result.size(), "File should contain an empty JSON object."); + } +} diff --git a/src/test/java/storage/StorageTest.java b/src/test/java/storage/StorageTest.java new file mode 100644 index 0000000000..1e08661f4b --- /dev/null +++ b/src/test/java/storage/StorageTest.java @@ -0,0 +1,145 @@ +//@@author Bev-low + +package storage; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import history.History; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + + +import java.io.FileReader; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doThrow; + +import programme.ProgrammeList; + +public class StorageTest { + private FileManager mockFileManager; + private Storage storage; + + @BeforeEach + public void setUp() throws NoSuchFieldException, IllegalAccessException { + mockFileManager = Mockito.mock(FileManager.class); + storage = new Storage("./src/test/resources/test_data.json"); + storage.setFileManager(mockFileManager); + } + + @AfterEach + public void tearDown() { + storage = null; + mockFileManager = null; + } + + private JsonObject loadJsonFromFile() throws Exception { + try (FileReader reader = new FileReader("./src/test/resources/validJson_data.json")) { + JsonElement element = JsonParser.parseReader(reader); + return element.getAsJsonObject(); + } catch(IOException e) { + throw new RuntimeException("Failed to load data due to: " + e.getMessage()); + } + } + + @Test + public void testLoadProgrammeList_validJson() throws Exception { + JsonObject jsonObject = loadJsonFromFile(); + JsonObject programmeListJsonObject = jsonObject.getAsJsonObject("programmeList"); + + when(mockFileManager.loadProgrammeList()).thenReturn(programmeListJsonObject); + + ProgrammeList programmeList = storage.loadProgrammeList(); + + assertNotNull(programmeList); + assertEquals(1, programmeList.getProgrammeList().size()); + assertEquals("Starter", programmeList.getProgrammeList().get(0).getProgrammeName()); + } + + @Test + public void testLoadProgrammeList_emptyJson() throws Exception { + JsonObject jsonObject = new JsonObject(); + when(mockFileManager.loadProgrammeList()).thenReturn(jsonObject); + ProgrammeList programmeList = storage.loadProgrammeList(); + + assertNotNull(programmeList); + assertEquals(0, programmeList.getProgrammeList().size()); + } + + @Test + public void testLoadProgrammeList_null() throws Exception { + JsonObject jsonObject = null; + when(mockFileManager.loadProgrammeList()).thenReturn(jsonObject); + ProgrammeList programmeList = storage.loadProgrammeList(); + assertNotNull(programmeList); + } + + @Test + public void testLoadHistory_validJson() throws Exception { + JsonObject jsonObject = loadJsonFromFile(); + JsonObject historyJsonObject = jsonObject.getAsJsonObject("history"); + when(mockFileManager.loadHistory()).thenReturn(historyJsonObject); + History history = storage.loadHistory(); + + assertNotNull(history); + assertEquals(1, history.getHistorySize()); + } + + @Test + public void testLoadHistory_emptyJson() throws Exception { + JsonObject jsonObject = new JsonObject(); + when(mockFileManager.loadProgrammeList()).thenReturn(jsonObject); + History history = storage.loadHistory(); + + assertNotNull(history); + assertEquals(0, history.getHistorySize()); + } + + @Test + public void testLoadHistory_null() throws Exception { + JsonObject jsonObject = null; + when(mockFileManager.loadHistory()).thenReturn(jsonObject); + History history = storage.loadHistory(); + assertNotNull(history); + } + + @Test + public void testSave_validJsonObject() throws Exception { + ProgrammeList programmeList = new ProgrammeList(); + History history = new History(); + storage.saveData(programmeList, history); + verify(mockFileManager).save(any(JsonObject.class)); + } + + @Test + public void testSaveData_nullProgrammeList() { + History history = new History(); + assertThrows(AssertionError.class, () -> storage.saveData(null, history), "programmeList must not be null"); + } + + @Test + public void testSaveData_nullHistory() { + ProgrammeList programmeList = new ProgrammeList(); + assertThrows(AssertionError.class, () -> storage.saveData(programmeList, null), "history must not be null"); + } + + @Test + public void testSave_filePathDoesNotExist() throws Exception { + doThrow(new IOException("Simulated IO error")).when(mockFileManager).save(any(JsonObject.class)); + ProgrammeList programmeList = new ProgrammeList(); + History history = new History(); + + storage.saveData(programmeList, history); + verify(mockFileManager).save(any(JsonObject.class)); + } + +} diff --git a/src/test/java/ui/UiTest.java b/src/test/java/ui/UiTest.java new file mode 100644 index 0000000000..04a1a3ad92 --- /dev/null +++ b/src/test/java/ui/UiTest.java @@ -0,0 +1,66 @@ +// @@author nirala-ts + +package ui; + +import command.CommandResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UiTest { + + private Ui ui; + private ByteArrayOutputStream outputStream; + + @BeforeEach + void setUp() { + outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + ui = new Ui(); + } + + @Test + void testShowWelcome() { + ui.showWelcome(); + String output = outputStream.toString(); + assertTrue(output.contains("Hello! I'm...")); + outputStream.reset(); + } + + @Test + void testShowFarewell() { + ui.showFarewell(); + String output = outputStream.toString(); + assertTrue(output.contains("Bye. Hope to see you again soon!")); + outputStream.reset(); + } + + @Test + void testShowMessageWithString() { + String testMessage = "This is a test message"; + ui.showMessage(testMessage); + String output = outputStream.toString(); + assertTrue(output.contains(testMessage)); + outputStream.reset(); + } + + @Test + void testShowMessageWithException() { + Exception testException = new Exception("Test exception message"); + ui.showMessage(testException); + String output = outputStream.toString(); + assertTrue(output.contains("Error: Test exception message")); // Verify it includes the ERROR_HEADER + outputStream.reset(); + } + + @Test + void testShowMessageWithCommandResult() { + CommandResult testResult = new CommandResult("Test command result message"); + ui.showMessage(testResult); + String output = outputStream.toString(); + assertTrue(output.contains("Test command result message")); + outputStream.reset(); + } +} diff --git a/src/test/java/water/WaterTest.java b/src/test/java/water/WaterTest.java new file mode 100644 index 0000000000..47e1442386 --- /dev/null +++ b/src/test/java/water/WaterTest.java @@ -0,0 +1,96 @@ +package water; + +import exceptions.WaterException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class WaterTest { + + private Water water; + + @BeforeEach + public void setUp() { + water = new Water(); + } + + @Test + public void testAddWaterHappyPath() { + water.addWater(2.5f); + + ArrayList expectedList = new ArrayList<>(); + expectedList.add(2.5f); + + assertEquals(expectedList, water.getWaterList(), "Water list should contain the added amount."); + } + + @Test + public void testAddWaterEdgeCaseNegativeWaterAmount() { + assertThrows(AssertionError.class, () -> water.addWater(-1.0f), + "Adding negative water should throw AssertionError."); + } + + @Test + public void testIsEmptyInitiallyEmpty() { + assertTrue(water.isEmpty(), "New Water instance should be empty."); + } + + @Test + public void testIsEmptyAfterAddingWater() { + water.addWater(1.0f); + assertFalse(water.isEmpty(), "Water instance should not be empty after adding water."); + } + + @Test + public void testDeleteWaterHappyPath() { + water.addWater(3.0f); + float deletedWater = water.deleteWater(0); + + assertEquals(3.0f, deletedWater, "Deleted water amount should match the amount added."); + assertTrue(water.isEmpty(), "Water list should be empty after deleting the only entry."); + } + + @Test + public void testDeleteWaterEdgeCaseInvalidIndex() { + // Update to expect IndexOutOfBoundsBuffBuddyException instead of IndexOutOfBoundsException + assertThrows(WaterException.class, () -> water.deleteWater(0), + "Deleting from an empty list should throw WaterExceptions."); + } + + @Test + public void testGetWaterList() { + water.addWater(1.5f); + water.addWater(2.5f); + + ArrayList expectedList = new ArrayList<>(); + expectedList.add(1.5f); + expectedList.add(2.5f); + + assertEquals(expectedList, water.getWaterList(), "Water list should contain all added amounts."); + } + + @Test + public void testToStringEmptyList() { + // Test string output for an empty water list + assertEquals("No record.", water.toString(), + "Empty water list should return 'No record.'"); + } + + @Test + public void testToStringWithEntries() { + // Add entries and check the string representation + water.addWater(1.0f); + water.addWater(2.0f); + + String expectedOutput = "1: 1.0\n2: 2.0"; + assertEquals(expectedOutput, water.toString(), + "String representation should match the format of indexed entries."); + } +} + diff --git a/src/test/resources/validJson_data.json b/src/test/resources/validJson_data.json new file mode 100644 index 0000000000..72fb788a27 --- /dev/null +++ b/src/test/resources/validJson_data.json @@ -0,0 +1,79 @@ +{ + "programmeList": { + "currentActiveProgramme": 0, + "programmeList": [ + { + "programmeName": "Starter", + "dayList": [ + { + "name": "ONE", + "exercises": [ + { + "sets": 3, + "reps": 12, + "weight": 30, + "calories": 200, + "name": "Bench_Press" + }, + { + "sets": 3, + "reps": 12, + "weight": 50, + "calories": 200, + "name": "Squat" + } + ] + }, + { + "name": "TWO", + "exercises": [ + { + "sets": 3, + "reps": 12, + "weight": 10, + "calories": 100, + "name": "Bicep_Curl" + } + ] + } + ] + } + ] + }, + "history": { + "12-12-2024": { + "day": { + "name": "ONE", + "exercises": [ + { + "sets": 3, + "reps": 12, + "weight": 30, + "calories": 200, + "name": "Bench_Press" + }, + { + "sets": 3, + "reps": 12, + "weight": 50, + "calories": 200, + "name": "Squat" + } + ] + }, + "mealList": { + "meals": [ + { + "calories": 560, + "name": "pasta" + } + ] + }, + "water": { + "waterList": [ + 300.0 + ] + } + } + } +} \ No newline at end of file diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..e69de29bb2 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +0,0 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling 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