diff --git a/.gitignore b/.gitignore index f69985ef1f..302ee93226 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,8 @@ bin/ /text-ui-test/ACTUAL.txt text-ui-test/EXPECTED-UNIX.TXT + +#User data file +/data/ +/text-ui-test/data/ + diff --git a/build.gradle b/build.gradle index b0c5528fb5..27a0c80ca9 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ test { } application { - mainClassName = "seedu.duke.Duke" + mainClassName = "seedu.duke.Main" } shadowJar { @@ -43,4 +43,5 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 7e1ce222a9..e8297cd72e 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -185,7 +185,7 @@ - + diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..d717776a54 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,12 @@ -# About us +--- +layout: page +title: About Us +--- Display | Name | Github Profile | Portfolio --------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + | Tay Weida | [Github](https://github.com/weidak) | [Portfolio](team/weidak.md) + | Tan Xing Jie | [Github](https://github.com/xingjie99) | [Portfolio](team/xingjie99.md) +| Tan Rui Yang | [Github](https://github.com/tryyang2001) | [Portfolio](team/tryyang2001.md) + | Toh Yi Zhi | [Github](https://github.com/tttyyzzz) | [Portfolio](team/tttyyzzz.md) +| Tan Le Yi | [Github](https://github.com/tlyi) | [Portfolio](team/tlyi.md) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..e64ca2b12e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,949 @@ -# Developer Guide +--- +layout: page +title: Developer Guide +--- -## Acknowledgements +The aim of this guide is to help the reader to understand how the system and components of Fitbot is +designed, implemented and tested. In the same time, this developer guide also serves to help developers who are interested in understanding the architecture +of Fitbot and some design considerations. +[Don't know about Fitbot? Click here to find more.](https://ay2122s1-cs2113t-f14-2.github.io/tp/) -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +## Content page +[Acknowledgements](#acknowledgements) -## Design & implementation +[Design](#design) +- [Architecture](#architecture) +- [Data Component](#data-component) + - [Data Component (Profile)](#data-component-profile) + - [Data Component (ItemBank and Item)](#data-component-item) + - [ItemBank Class Hierarchy](#itembank-class-hierarchy) + - [Item Class Hierarchy](#item-class-hierarchy) +- [Ui Component](#ui-component) +- [Logic Component](#logic-component) +- [Storage Component](#storage-component) +- [State Component](#state-component) +- [Implementation](#implementation) + - [Parsing of Commands](#parsing-of-commands) + - [Add Food Item Feature](#add-a-food-item-feature) + - [Add a Recurring Exercise Feature](#add-a-recurring-exercise-feature) + - [Loading Of Data On Startup](#loading-of-data-on-startup) + - [Create Profile If Not Exist On Startup](#create-profile-if-not-exist-on-startup) +- [Product Scope](#product-scope) + - [Target User Profile](#target-user-profile) + - [Value Proposition](#value-proposition) +- [User Stories](#user-stories) +- [Non-functional Requirements](#non-functional-requirements) +- [Glossary](#glossary) +- [Instruction for Manual Testing](#instructions-for-manual-testing) + - [Launch](#launch) + - [Setting Up Profile](#setting-up-profile) + - [Customising Profile](#customising-profile) + - [Recording Food Items](#recording-food-items) + - [Recording Exercise Items](#recording-exercise-items) + - [Scheduling Exercises](#scheduling-exercises) + - [Building Food Bank](#building-food-bank) + - [Building Exercise Bank](#building-exercise-bank) + - [Exiting Program](#exiting-program) + - [Manipulating And Saving Data](#manipulating-and-saving-data) -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} -## Product scope -### Target user profile +## **Acknowledgements** -{Describe the target user profile} +Firstly, we would like to acknowledge [AddressBook Level-2](https://github.com/se-edu/addressbook-level2/blob/master/test/java/seedu/addressbook/parser/ParserTest.java) as we adapted its utility methods for +testing in one of our classes, `ParserManager`. \ +We would also like to acknowledge [AddressBook Level-3](https://se-education.org/addressbook-level3/) for providing many useful insights on how to write and structure our code in +Object-Oriented Programming fashion. -### Value proposition +## **Design** -{Describe the value proposition: what problem does it solve?} +### **Architecture** -## User Stories +

+ Architecture Diagram +

+ +`Main` class is the component that interacts with all the necessary classes. +The `Main` class consists of the few components as shown below: +- `Ui`: Facilitates interaction between user and application +- `Logic`: Parses commands and execute them respectively +- `Data`: Allows users to perform Create, Read, Update, Delete (CRUD) operations on the data in the application +- `Storage`: Stores all data in the application. Saves a copy of data in relevant files. + Data will be retrieved from storage upon starting of application. +- `State`: Helps the user to restore or create profile data. + + +Upon launching of application: +- The application will check if there are files that are already stored in the respective folder. + If there is such files, the contents of the files will be loaded to the data section of the application. + Instances of profile, data(e.g. FoodList, ExerciseList, FutureExerciseList, ItemBank) and storage will be created. +- If there is no such files, the files will be created. + + +Upon exiting of application: +- The application will save all data into the files created. All instance of components will be cleared automatically. + +Class diagram of Main + +

+ Main Class Diagram +

+ +When _Fitbot_ is being started, the above instances (`Ui`,`State`, `LogicManager`,`DataManager` and `StorageManager`) +are being created in the main class. The main class will require 1 of each instance for the application to function. + + +- Main class start of by running the `start()` function which loads all information using StorageManager class to the +Profile, FoodList, ExerciseList,ItemBank(foodBank, exerciseBank). +-Next main class will check if user contains the profile using the `checkAndCreateProfile()`. If user does not have +a profile, the application will assist user to create a profile by prompting questions. +-After setting up/ ensuring that user have a profile, the main program will enter `enterTaskModeUntlByeCommand()` +for user to interact with the application +- Once user has keyed in the command `bye`, the main program will exit out of the `enterTaskModeUntlByeCommand()` +and run the `exit()` command to exit the application. + + + + +### **Data Component** + +

+ Data Component Diagram +

+ +The `Data` component is responsible to perform operations such as data modification and query in the code. + +In `Data` component, it consists of: +1. `DataManager` class which is responsible to help interaction between classes in `Logic` Component and classes in `Data` Component. +2. `Profile` package which is responsible to any manipulation and modification of data for `Profile`. +3. `Item` package which is responsible to any manipulation and modification of data for `Item` such as `Food` and `Exercise`. +4. `Verifiable` interface which is responsible to check data validity in storage files. + +#### **Data Component (Profile)** + +

+ Profile Diagram +

+ +A `Profile` class has various attributes such as `Name`, `Height`, `Weight`, `Gender`, `Age`, `CalorieGoal` and `ActivityFactor` + +- Using these attributes it is able to calculate an estimated Basal Metabolic Rate (BMR) using the Harris-Benedict Equation based on your activity levels. Therefore, while calculating your net calories for the day, your BMR is factored in to give you an estimated calculation of your net calorie. + +- All the attributes implement a `Verifiable` interface to enable us to check if the attributes are valid. This is important for the setting up of profile or the loading of profile from storage to ensure data integrity of the user's attributes. + +The `ProfileUtils` class is used in performing calculations (such as BMR or BMI) with the various attributes of the `Profile` class. + +#### **Data Component (Item)** + +

+ ItemBank And Item Class Diagram +

+ +Above is a high-level class diagram for all the classes in `Item` package. In `Item` package, it has +two different class hierarchy, one is `ItemBank`, and one is `Item`. + +The main purpose of having `ItemBank` and `Item` classes is to allow user to perform writing, reading, editing and deleting operations in the program. + +#### ItemBank Class Hierarchy +1. `ItemBank` is the *highest superclass* that contains one attribute called `internalItems` which is an _array list_ of `Item`. +2. `ItemList` being the *subclass* of `ItemBank` and *superclass* of `FoodList` and `ExerciseList`, which inherits all the methods available from `ItemBank`, with additional methods that form a dependency on `Item` class. +3. `FoodList` and `ExerciseList` are *subclasses* that inherit all the methods available from `ItemList`, while each of them also contains more methods that form a dependency +on `Food` class and `Exercise` class respectively. +4. `FutureExerciseList` is a *subclass* that inherits all the methods available from `ExerciseList` and contains other methods that form a dependency +on `Exercise` class. + +As shown in the diagram above, `DataManager` class has association with `ItemBank`. This also implies that it has association with +all the subclasses that inherits `ItemBank`. + +#### Item Class Hierarchy +1. An `Item` class contains two attributes, `name` which represents the name of the item, and `calories` which represents the calorie intake/burnt from the item. +2. `Food` and `Exercise` are the only two _subclasses_ that inherit the `Item` class. +3. `Food` class has two extra attributes called `dateTime` and `timePeriod`, the former stores the consumed food date and time, while the latter compute the time period +(only value such as `MORNING`, `AFTERNOON`, `EVENING` and `NIGHT` as shown in the enumeration class `TimePeriod`) of the food consumed time. Note that the `timePeriod` +value must present when a `Food` object is created. +4. `Exercise` class has one extra attribute called `date` which stores the date of the exercise taken. + +As shown in the diagram above, the `Item` class implements interface `Verifiable`. This interface contains method to check the validity for the items +in the storage files. If the data in storage file is invalid, that item will not be loaded to the program. Note that since the superclass +`Item` implements `Verifiable`, its subclasses `Food` and `Exercise` also implement the interface. + +Abstract classes of Items and ItemLists acts as an agent for meaningful subclasses of Food and Exercise to inherit its attributes and functionality for a more concise use-case. + +### **Ui Component** + +The purpose of `Ui` component is to interact with the user. It reads in input from the user and prints messages on the +console. Below shows a sequence diagram of how `Ui` component interacts with the rest of the application. + + +

+ Ui Sequence Diagram +

+ +Step 1: The main program starts off with the `run()` method (method not shown but the activation bar is present), and the +`run()` method self invokes itself by calling `enterTaskModeUntilByeCommand()`,\ +Step 2: In this method, the `Main` class will prompt for user input using `getUserInput()`.\ +Step 3: Once the `Ui` instance received user input, the `Main` class will process the data. This process will be elaborated in [Parsing of Commands](#parsing-of-commands) under [Implementation](#implementation). +Step 4: After processing the data, no matter success or fail, the `Main` class will tap on `Ui` instance again to print +message on the console using `formatMessageFramedWithDivider()`. + + + +Note: The `Main` class has 2 activation bars due to the `run()` method which will then activate +`enterTaskModeUntilByeCommand()`. In the example above, it is assumed that `bye` command is not used as example. + +### **Logic Component** + +The `Logic` component is responsible for making sense of user input. + +Below is a high level class diagram of the `Logic` component, which shows how it interacts with other components +like `Main`, `Storage` and `Data`. + + +

+ Logic Class Diagram +

+ +The general workflow of the `Logic` component is as follows: +1. When the program first starts, `Main` instantiates `LogicManager` and initialises it with `StorageManager` and `DataManager`. At the same time, `ParserManager` is also instantiated within `LogicManager`. +2. Whenever `Main` receives user input, it feeds this user input to the `LogicManager`. +3. `LogicManager` then calls on `ParserManager` to parse the user input. +4. The `ParserManager` parses the user input and creates a `Command` object. + - More specifically, it creates a `XYZCommand` object, where `XYZ` is a placeholder for the + specific command type, e.g `AddFoodCommand`, `UpdateProfileCommand`, etc. + - `XYZCommand` class inherits from the abstract class `Command`, which is used to represent all executable commands in the application. +5. `ParserManager` returns the `Command` object to `LogicManager`, which then executes the `Command`. +6. During execution, `Command` will perform data manipulation on `DataManager`. +7. After execution, all `Command` objects stores the result of the execution in a `CommandResult` object. +This `CommandResult` object is then returned to `LogicManager`. +8. Depending on the type of `Command` being executed which affects the type of data that has been manipulated, `LogicManager` will call upon `StorageManager` to save the affected data to the file system. +9. Finally, `CommandResult` is returned to `Main`. + +Here is a more detailed class diagram illustrating the classes within the `Logic` component. +

+ Parser Class Diagram +

+ +Taking a closer look into the parsing process, the `ParserManager` actually does not do most of the parsing itself. This is how the parsing process works: +1. `ParserManager` creates `XYZCommandParser`, which is then responsible for creating the specific `XYZCommand`. + - All `XYZCommandParser` classes implement the interface `Parser`, which dictates that + they are able to parse user inputs. + - They also make use of utility methods stored in `ParserUtils` to extract + all the parameters relevant to the command, and constants in `ParserMessages` to format the desired output. +2. After parsing the input, `XYZCommandParser` returns `XYZCommand` to `ParserManager`, + which then returns the same `XYZCommand` to `LogicManager`. + + +### **Storage component** + +This is a (partial) class diagram that represents the `Storage` component. + +

+ Storage Class Diagram +

+ +1. Firstly, `Storage` inherits each of the `XYZStorage` interfaces. This ensures the `Storage` API has the relevant load and save functions +of the various storages. +2. `StorageManager` then implements the `Storage` API that contains the relevant functionalities and instantiates `XYZStorage` using the respective `XYZStorageUtils`. +3. Each of the `XYZStorageUtils` utilizes the general classes of `FileChecker` to create/check for their files and `FileSaver` to write to their respective files. +4. `XYZStorageUtils` also uses `XYZDecoder` to decode files from the saved .txt file and a `XYZEncoder` to encode items into the saved file. + +The `StorageManager` component loads and saves: + +- your profile: name, height, weight, gender, age, calorie goal and activity factor +- list of exercises done: including date performed +- list of food consumed: including date and time of consumption +- upcoming exercises: recurring exercises that are scheduled in the future +- food and exercise banks: names and calories of relevant item + +This way of design ensures that each class has the correct methods to perform its capabilities. + + +### **State Component** + +#### **Create Profile (StartState)** + +

+ Start State Class Diagram +

+ +- When the `StartState` method is being called, it instantiated and execute methods in the 7 classes, which are +`NameCreator`, `HeightCreator`, `WeightCreator`, `GenderCreator`, `AgeCreator`, `CalorieGoalCreator` and +`ActivityFactorCreator`. +- The above Classes instantiated by `StartState` are inherited from `AttributeCreator` class. These profile attributes +are inherited from `AttributeCreator` to conform DRY principle, by extracting out common methods. + + + +### **Implementation** + +This section describes some noteworthy details on how certain features are implemented +and some design considerations. + +❗️ **Note**: Due to limitations of PlantUML, the lifeline in sequence diagrams does not end at the destroy marker (X) as it should, but reaches the end of the diagram instead. + +#### **Parsing of Commands** +The sequence diagram below models the interactions between the different classes within the Logic component. +This particular case illustrates how a user input add f/potato c/20 is parsed and process to execute the appropriate actions. + + +

+ Logic Sequence Diagram +

+ + +Step 1: When the program first starts, `Main` instantiates `LogicManager`, and initialises it with the objects `storageManager` and `dataManager`. This is so that `LogicManager` has access to the storage and data components. + +Step 2: `LogicManager` then instantiates a `ParserManager` object. This is the class where all the parsing of commands will occur. + +Step 3: In the case where `Main` receives the user input `add f/potato c/30`, `Main` will call the method `execute` from `LogicManager`, and provides the user input as the argument. + +Step 4: `LogicManager` then calls the `parseCommand` method from `ParserManager`. Now, `ParserManager` will start to parse the user input. + +Step 5: Firstly, `ParserManager` has to determine the type of `Command` it is trying to parse. The details of this process is shown in the sequence diagram below. + + +

+ Parse Command Ref Frame +

+ +Step 6: As seen in the above diagram, `ParserManager` first splits the input into command word and the remaining parameters. In this case, the command word is `add`, and the parameters are `f/potato c/30`. + +Step 7: Depending on the command word, `ParserManager` will then instantiate the appropriate `XYZCommandParser`. In this case, since the command word is `add`, an `AddCommandParser` object is created. +However, if the command word is not known to the program (i.e. is not equal to any `XYZ` specified), an `InvalidCommand` will be created and returned immediately. + +Step 8: After the appropriate `AddCommandParser` is created, `ParserManager` will then call the method `parse` on the `AddCommandParser`. `AddCommandParser` will then start to parse all the required parameters specific to the `AddCommand`. This process is explained in detail in the below sequence diagram. + + +

+ Parse Parameters Ref Frame +

+ +Step 9: Since the `AddCommand` can be called for different items in the program, such as `FoodList`, `ExerciseList`, etc., `AddCommandParser` will first call the `extractItemTypePrefix` method from `ParserUtils`. +Note that all methods from `ParserUtils` are static as `ParserUtils` is purely a utility class. This method will extract the item type prefix, which is the first parameter provided after the command word. In this case, `f` is extracted and returned to `AddCommandParser`. +If at this point the item type prefix extracted is not known to the program, an `InvalidCommand` will be created and returned immediately. + +Step 10: After determining that the `AddCommand` is to be performed on the `FoodList`, `AddCommandParser` will call its own method, `parseAddToFood`. + +Step 11: Inside the method `parseAddToFood`, `AddCommandParser` will first call the method `hasExtraDelimiters` from `ParserUtils` to determine if there are extra `/` characters in the input, which would make it invalid. If yes, an `InvalidCommand` will be created and returned immediately. + +Step 12: Then, `AddCommandParser` will call the relevant methods from `ParserUtils` to extract specific parameters. In this case, the methods `extractItemDescription`, `extractItemCalories` and `extractDateTime` are called. + +Step 13: If all the parameters extracted are valid, then `AddCommandParser` will instantiate and initialise an `AddFoodCommand` object with the extracted details. Else, if any of the parameters are invalid, an `InvalidCommand` would be created and returned immediately. + +Step 14: The instantiated `Command` object is then returned to `ParserManager`, which then returns it to `LogicManager`. In this case, `AddFoodCommand` is returned. + +Step 15: `LogicManager` then calls the method `setData` on `AddFoodCommand` to provide it with the program's data. + +Step 16: `LogicManager` then calls the method `execute` on `AddFoodCommand` to perform the data manipulation process. For example, in this case, a food item will be added to the `FoodList`. The details of this process is not shown here, but can be seen in [this section](#add-a-food-item-feature). + +Step 17: Finally, `AddFoodCommand` instantiates a `CommandResult` object representing the result of the execution. This `CommandResult` is then returned to `LogicManager`, which then returns it to `Main`. + +#### **Add a Food Item Feature** + +

+ Add Food Item Sequence Diagram +

+ +The purpose of this feature is to allow the user to add food item to the food list. The above diagram shown is the +sequence diagram of the process of adding the food item. + +When the user gives an input, `ParserManager` from the `Logic` component will try to read the input, and then call the correct +command. In this case we assume that the correct format of **Add Food** input is given and the `AddFoodCommand` has already been +called and created. + +Step 1: When the `execute` method in the `AddFoodCommand` is being called, it will first check that if the `calories` is equal to null. If this +condition is true, meaning that the user does not provide the calorie value of the food item, thus the item is expected to be found in the `FoodBank`. + +Step 2: Once the calorie value is determined, whether from `FoodBank` or user input, the AddFoodCommand then call the constructor +of the `Food`. When the `Food` constructor is called, it will perform a [self-invocation](#self-invocation)`setTimePeriod` to set the enum value `timePeriod` +of the Food. After that, it returns the Food object to the `AddFoodCommand`. + +Step 3: The newly created Food object `food` is checked if it has a valid calorie value by calling the method `isValid()` in `Food` class. This +method returns a boolean result `isValid` to `AddFoodCommand`. Then the program will enter an `alt` block which determines whether the `food` object +should be added to the `foodList`. + +Step 4a: If the calorie value is invalid, the boolean `isValid` will be false. When this condition is satisfied, it will enter the first +alternative path, which then creates a `CommandResult` class object that contains the message to inform the user that the calorie value +is incorrect. In this path, no food item is added to the `foodList`. This `CommandResult` object is returned to the `AddFoodCommand`. + +Step 4b: If the calorie value is valid, the boolean `isValid` is true, the `AddFoodCommand` will call the method `addItem` from the `FoodList` object, which performs the add food operation in the +`Food List`. After the new `Food` Item is added, it will perform a [self-invocation](#self-invocation) `sortList` to sort the `FoodList` according to +the date and time of all the food items inside the list. Since the `addItem` method is void type, nothing is returned to `AddFoodCommand`. + After the `addItem` method is executed without giving any error, the `AddFoodCommand` then calls a `CommandResult` object that contains the message indicating the command is executed +successfully. This `CommandResult` object is returned to the `AddFoodCommand`. + +Step 5: Once the `CommandResult` class object is returned, the `AddFoodCommand` then return this `commandResult` to the class that calls it. +At this stage, the `AddFoodCommand` execution is successfully ended. + +After all the steps are done, the objects of class `AddFoodCommand`, `Food` and `CommandResult` are no longer referenced and hence get removed +by the `Garbage Collector` in Java. However ,the lifeline of `foodBank` and `foodList` objects are still continuing because they +are created in `DataManager` class and have the potential to get referenced by other commands call such as `add`, `delete`, `view` and `edit`. + +One may also observe that the lifeline does not end even though the object is deleted and no longer be referenced. This problem +is due to the flaw of the drawing tool, *PlantUml* used. For a more accurate sequence diagram, the lifeline should end immediately +once the object is no longer referenced. + +#### **Design considerations:** + +The current data structure used in `FoodList` is [Array List](#array-list). The rationale of choosing an array list implementation is because +it supports resizability and random accessibility. However, the drawback of such an array list is that sorting requires +O(n2), which slows down the code efficiency. In the future increment, alternative data structures such as +[Priority Queue](#priority-queue) and [Min Heap](#min-heap) can be implemented to achieve O(logn) addition and they are +naturally sorted and thus no additional sorting required. + +The same reasoning for the class `ItemBank`, which is the superclass of `FoodList` and `ExerciseList`,the current implementation +data structure is also an [Array List](#array-list). In the future increment, since the `ItemBank` need to perform query +operation frequently and the items inside need to be sorted alphabetically, the data structure of the attribute will be changed +to [TreeMap](#tree-map) to achieve O(logn) query time. + + +#### **Add a Recurring Exercise Feature** + +![Add Recurring Exercise Sequence Diagram](images/AddRecurringExerciseSequenceDiagram.png) + +The purpose of this feature is to allow the user to add recurring exercises to the future exercise list. The above diagram +is the sequence diagram of adding recurring exercises to the future exercise list, assuming that the user input satisfies the restrictions +and does not cause any errors to be thrown. + +Step 1: The `ParserManager` from the `Logic` component parses the input given by the user and calls the `execute` method in +`AddRecurringExerciseCommand`. The condition `calories == null` is checked to see if the user input any calories for +the recurring exercise. If it is `true`, it means that no calories input is detected and the method +`findCalories` in class `ItemBank` is called, and it will return an int value of `calories`. + +Step 2: Within `execute` method, `addRecurringItem` method in `FutureExerciseList` is also called. This method will +iteratively call the constructor for `Exercise` class and add the exercises into `FutureExerciseList` via the self-invocation +`addItem` method. This iteration will end when all exercises on `dayOfTheWeek` between `startDate` and `endDate` are added. + +Step 3: After `addRecurringExercises` method is executed, `AddRecurringExerciseCommand` calls a `CommandResult` object. +This object outputs a message and `AddRecurringExerciseCommand` will return `commandResult`, indicating that +`AddRecurringExerciseCommand` is successfully executed and ended. + +#### **Loading of Data On StartUp** + +There are many files that are used for our current implementation. +Therefore, since they are similar in behaviour and function, we will only be looking at the loading of the Profile component on the starting up of _Fitbot_. + +

+ ProfileStorageLoadSequenceDiagram +

+ +Upon successful launch of the application, `Main` will call to initialize `StorageManager`. +This in turn initializes all the subclasses of `Storage`, including `ProfileStorage`, with their respective file paths. +Afterwhich, `Main` calls a loading function `loadAll` that calls an internal `loadProfile()` function (self-invocation). This then calls the `loadProfile()` method of `ProfileStorage`. + +`ProfileStorage` then does 2 main things: + +1. Checks and creates the file if it is missing. +2. Retrieves the data from the file with the use of the ProfileDecoder to decode. + + +

+ ProfileStorageLoadSequenceDiagram +

+

Reference Diagram: Checks for the file and create directory if it does not exist

+ +The diagram above explains how the application checks if a file exists. If it exists, it will not perform any additional functionality. Otherwise, it will generate a new file in preparation for storage. + + +

+ ProfileStorageLoadSequenceDiagram +

+

Reference Diagram: Retrieval of data from storage with the use of ProfileDecoder to decode

+ + +The diagram above explains the processes to decode the items from the file. + +Upon reaching the `decodeProfile(line)` method, the reference frame depicts a process of decoding its attributes one by one to ensure that they are able to detect each attribute's readability from storage. +If the methods are unable to read the respective attribute from storage, an invalid attribute will be initialized. This then returns an initialized profile with invalid attributes for `StartState` to catch, allowing users to change +their attributes instead of losing their entire profile data on startup. + + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode all attributes

+ + +Below represents each of the attribute's decoding process: + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode Name

+ + +

+ ProfileStorageLoadSequenceDiagram +

+

Reference Diagram: Decode Height +

+ + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode Weight

+ + + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode Gender

+ + + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode Age

+ + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode CalorieGoal

+ + +

+ ProfileStorageLoadSequenceDiagram + +

+

Reference Diagram: Decode ActivityFactor

+ + +The other storages load in a similar fashion to this, except for each decoder, they decode `Item` for `ItemBank`s, `Food` for `FoodList` and `Exercise` for `ExerciseList`. + + + +#### **Create Profile If Not Exist On Startup** + +When user first enters _Fitbot_, the profile of the user is not set up (attributes may not exist). If user were to +interact with the application, there might be incorrect output, +therefore the `checkAndCreateProfile()` is implemented to check whether the profile exist +and if it does not exist, _Fitbot_ will guide user through to fill up the profile attributes. + +

+ CreateProfileDiagram +

+ +On startup, the main function will run `checkAndCreateProfile()` (not in diagram), which will cause StartState +instance to be created by dataManager and run `new StartState.checkAndCreateProfile()`. + +Step 1: the `checkAndCreateProfile()` will check if there is a profile exist, by checking all the attributes. +If all attributes are correct, the profile will be returned. If there is all the attributes are incorrect or there is no +attribute in storage, the StartState will self invocate by calling `createProfile()`. + +Step 2: `createProfile()` will instantiate a new profile instance. Using a while loop to check whether all attributes + have been updated, the `createProfile()` will ensure that all attributes in the new profile instance are valid before +proceeding to next step. + +Step 3: Once all the profile attributes are set up, it will store the profile attributes into storage, by calling +`saveProfile()` in StorageManager class. + +Step 4: The StartState will replace the reference of old profile instance with the new profile instance +(not shown in diagram). Since the old profile instance is being dereferenced, it has reached the end of the lifeline, +shown by a cross at its lifeline. The profile in the StartState will then be returned to the dataManager. + + +#### **Design Considerations** + + + +The purpose of replacing the new profile with the old profile is to ensure that if the new user decides to close the +program without finishing creating profile, the profile attributes keyed in by the user will not be saved. It will only +save all the profile attributes when all the attributes has been inputted by the user are present and valid. + + +## **Product scope** +### **Target user profile** + +University students who are looking to keep track of their calorie consumption and calorie outputs. + +### **Value proposition** + +During these restricted COVID-19 times, we are confined to home-based learning. As a result, we tend to be less active and have fewer opportunities to stay active. This app aims to help you to gain or lose weight based on your goal of implementing a calorie deficit or calorie surplus. + +Its overview shows your progress over the weeks, indicating whether or not you have hit your daily calorie goal target for the past 7 days. + +## **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| +|v1.0|new user|want to store food records|track my food intake| +|v1.0|new user|want to store exercise records| track my exercises| +|v1.0|new user|delete exercise records| amend wrong inputs| +|v1.0|new user|delete food records| amend wrong inputs| +|v1.0|new user|to see how close or how far i am from the calorie target| so that I could manage my calorie input| +|v1.0|new user|save my profile data|reduce the hassle of typing repeatedly upon application startup| +|v1.0|new user|calculate BMI|gauge whether I am in the healthy range| +|v1.0|new user|store all records|refer to them whenever needed| +|v2.0|new user|have a profile| to keep track of all information to calculate my net calories| |v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +|v2.0|user|set up profile first before entering application|so that a more accurate analysis could be done| +|v2.0|user|have a summary|see my calorie targets +|v2.0|user|have a history|spend less time typing all the requirements to store items| +|v2.0|user|have an exercise list that update itself|have more time for exercises| +|v2.0|user|have a sorted food list|see what I have eaten on different times of the day| +|v2.0|user|have varying inputs methods|reduce unnecessary hassle| +|v2.0|user|have a delete all command|start afresh| +|v2.0|user|have sorted food list|view the meals I have eaten throughout the week| +|v2.0|user|have sorted food list by meal time|track the number of supper meals I have in a week| +|v2.0|frequent user|have the ability to edit past records|edit past wrong inputs| +|v2.0|university student|be able to store weekly recurring sports activities|reduce hassle of input entries| +|v2.0|body-conscious user|calculate net calories inclusive of BMR|have a more accurate gauge of calories burnt| + + + + + +## **Non-Functional Requirements** + +1. Should work on any OS as long as it has Java 11 or above installed on their PC. +2. Should be able to hold up to at least a year of data without a slowdown of performance in daily use. +3. Any user that is comfortable with typing of speeds >55 words per minute would be able to accomplish these tasks faster than if they used a mouse to navigate. + +## **Glossary** + +#### _self invocation_ +In UML sequence diagram, a method that does a calling to another of its own methods is called self-invocation. +#### _array list_ +A linear data structure that inherits Java `List` implementation and `Array` implementation. It behaves like a normal array, +except that it is resizable. Moreover, the amount of time taken for reallocation the elements when capacity grows is a constant +time. In Java, array list can be implemented using `ArrayList` in `Collection`. +#### _priority queue_ +An abstract data type similar to a regular queue or stack data structure in which elements in priority queue are ordered +and have "priority" associated with each element. The priority can be defined by the coder. In the case of `FoodList`, the +priority will be defined as earlier date and time will have higher priority. +#### _min heap_ +The implementation of min heap is almost the same as priority queue, in which it is sorted according to some "priority" +constraint. In addition, a min heap can be modelled as a [binary tree](https://en.wikipedia.org/wiki/Binary_tree) structure +having all the parent nodes smaller or equal to its children nodes. +#### _tree map_ +A tree map is a combination of tree structure and hash map structure. In Java, tree map is implemented using a self-balancing +[Red-Black tree](https://www.geeksforgeeks.org/red-black-tree-set-1-introduction-2/) structure and it is sorted according +to the natural order of its keys. In the case of `ItemBank`, the key should be the String type of the `Item` description, +which will be sorted lexicographically. \ +(more coming in the future...) + +## **Instructions for manual testing** + +Given below are some instructions that can be used to test the application manually. + +### **Launch** + +1. Initial launch + - Prerequisite: There is no Fitbot.jar file on your desktop. + - Test case: + 1. Download the jar file and copy into an empty folder + 2. Go to your command prompt, and go into your directory. + 3. Run the command `java -jar Fitbot.jar`. + + Expected: a data folder will be created in the file that contain Fitbot.jar. + + + +### **Setting Up Profile** + +1. Setting Up Profile I + - Prerequisite: Fitbot.jar is in a folder with or without data folder. + - Test case: + 1. Delete profile.txt from data folder if present. + 2. Run _Fitbot_ using `java -jar Fitbot.jar`. + + Expected: _Fitbot_ will prompt for your name upon start up. + +2. Setting Up Profile II + - Prerequisite: profile.txt is present. + - Test case: + 1. Change 1 of the attribute to an invalid attribute. + 2. Run _Fitbot_ using `java -jar Fitbot.jar`.\ + Expected: _Fitbot_ will prompt for the attribute missing. + +3. Setting Up Profile III + - Prerequisite: profile.txt is deleted/ not present. + - Test case: + 1. When being asked to type a name, type the command `bye`.\ + Expected: A question shows up to confirm with the user whether the user wants to exit the program or wants to set his or her + name as "bye". The user then need to type 1 to exit the program, 2 to set the name as "bye" and any other key to go back. + 2. Restart the program, checks whether profile.txt is deleted/ not present. + 3. Fill in some of the profile attributes. Do not fill up all of them. + 4. Type the command `bye`.\ + Expected: _Fitbot_ is able to exit. + + + +### **Customising Profile** + +1. Viewing current profile: + 1. Prerequisite: Have an initialized profile after the startup of the program + 2. Test case: `profile`\ + Expected: Displays your profile in a viewable format +2. Changing attributes: + 1. Prerequisite: Have an initialized profile after the startup of the program + 2. Test case: `profile n/John|`\ + Expected: Name is not changed as the use of `|` character is illegal. + 3. Test case: `profile n/John/`\ + Expected: Name is not changed as the use of `/` character is also illegal. + 4. Test case: `profile h/-20` \ + Expected: An error message shows that you can only input a number between 1 and 300, and height will not change. + 5. Test case: `profile w/-200` \ + Expected: An error message shows that you can only input a number between 1 and 300, and weight will not change. + 6. Test case: `profile a/170`\ + Expected: An error message shows that you can only input a whole number between 10 and 150. + 7. Test case: `profile s/mfe`\ + Expected: Error message displays that you should only use 'M' or 'F' only. + 8. Test case: `profile g/20000`\ + Expected: Error message that displays that you can only input from -3000 to 10000. + 9. Test case: `profile x/0`\ + Expected: Error message asking you to try a whole number from 1 to 5 instead. + 10. Test case: `profile x/2 w/90 h/190 a/22 s/m g/500 n/Johnny English`\ + Expected: Profile will change even with the parameters not being in order. This allows users flexibility to change their attributes in any order and with whichever parameters they want. (At least 1 attribute and at max 7 attributes can be changed at once) + +### **Recording Food Items:** + +1. Adding a new Food Item when the Food List is empty: + 1. Prerequisite: Checks if the food list is empty using `view f/`. An output message showing that + the current food list is empty is expected. + 2. Test case: `add f/chicken rice c/607` \ + Expected: New Food Item is added to the Food List. A message telling the user that a new food item has been added will show up. + The date and time are the date and time when the user call this command. + 3. Test case: `add f/chicken rice c/607 d/x t/1200` (where x is a date in DD-MM-YYYY format and within 7 days of today)\ + Expected: No Food Item is added to the Food List. A message will show up and tell the user that + the date must be within 7 days of today. +2. Viewing a new Food Item: + 1. Test case: `view f/` when the Food List is empty\ + Expected: No food item shown. A message will show up, telling the user that there is no food items within 7 days of today. + 2. Test case: `view f/` after calling `add f/chicken rice c/607`\ + Expected: The output will show the food list with at least one food item chicken rice, with calorie 607 cal and the date and + time is when the user called the `add` command. +3. Deleting Food Item: + 1. Prerequisite: Simply replace the date to any date that is within 7 days of today date. The time field is also optional + and can be replaced with any valid timing. + 2. Test case: `delete f/1 d/x t/1400` when the Food List is empty (where x is a date in DD-MM-YYYY format and within 7 days of today)\ + Expected: No food item is deleted. The output will show a message telling the user that the Food List is empty. + 3. Test case: `delete f/1 d/x t/1400` after calling `add f/chicken rice c/607 d/x t/1400` (where x is a date in DD-MM-YYYY format and within 7 days of today)\ + Expected: The food item with index 1 is removed. The output will show a message teling the user that the food item + is deleted. (Note that the food item will be `chicken rice` if there is only one food in the food list) + 4. Test case: `delete f/-1 d/x t/1400` (where x is a date in DD-MM-YYYY format and within 7 days of today)\ + Expected: No food item is deleted. The output will show a message telling the user that the input index should be a + number that is greater than 0. + 5. Test case: `delete f/all` \ + Expected: All Food Items in the Food List are deleted. Message will show up and inform the user that all food items + in the Food List are deleted. + +### **Recording Exercise Items** + +1. Adding Exercise Items + 1. Prerequisite: View the current Exercise List using `view e/`. + 2. Test case: `add e/running c/200` \ + Expected: A new Exercise Item is added to the Exercise List. Details of the added exercise are shown. + The date of the exercise is the date when the user calls this command. + 3. Test case: `add e/running c/200 d/x` (where x is a date in DD-MM-YYYY format and within the past 7 days of today) \ + Expected: A new Exercise Item is added to the Exercise List. Details of the added exercise are shown. + The date of the exercise is the date that the user input. + 4. Test case: `add e/running c/200 d/x` (where x is a date in DD-MM-YYYY format and more than 7 days ago) \ + Expected: No Exercise Item is added to the Exercise List. Error message will show up and inform the user that + a valid date within 7 days of today is required. +2. Viewing Exercise Items + 1. Test case: `view e/` when the Exercise List is empty\ + Expected: Message indicating that the Exercise List is empty is shown. + 2. Test case: `view e/` when the Exercise List is not empty\ + Expected: All of the Exercise Items are displayed. +3. Deleting Exercise Items + 1. Prerequisite: View the current Exercise List using `view e/`. + 2. Test case: `delete e/1 d/x` when the Exercise List is empty (where x is a date in DD-MM-YYYY format and within 7 days of today)\ + Expected: No Exercise Item is deleted from the Exercise List. Error message will show up and inform the user that + the Exercise List is empty. + 3. Test case: `delete e/1 d/x` when the Exercise List contains at least one exercise with date x (where x is a date in DD-MM-YYYY format)\ + Expected: The Exercise Item with index 1 is deleted. Details of the deleted exercise are shown. + 4. Test case: `delete e/2 d/x` when the Exercise List contains only one exercise with date x (where x is a date in DD-MM-YYYY format)\ + Expected: No Exercise Item is deleted from the Exercise List. Error message will show up and inform the user that + the Exercise Item with index 2 on date x is not found in the Exercise List. + 5. Test case: `delete e/all` \ + Expected: All Exercise Item in the Exercise List are deleted. Message will show up and inform the user that all exercises + in the Exercise List are deleted. + +### **Scheduling Exercises** + +1. Adding Upcoming Exercise Items + 1. Prerequisite: View the current Upcoming Exercise List using `view u/`. + 2. Test case: `add e/running c/200 d/x` (where x is a date in DD-MM-YYYY format and one year within the future) \ + Expected: A new Upcoming Exercise Item is added to the Upcoming Exercise List. Details of the added upcoming exercise are shown. + 3. Test case: `add e/running c/200 d/x` (where x is a date in DD-MM-YYYY format and more than one year in the future) \ + Expected: No Upcoming Exercise Item is added to the Upcoming Exercise List. Error message will show up and inform the user that + a valid future date within one year from today is required. +2. Adding Recurring Exercise Items + 1. Prerequisite: View the current Upcoming Exercise List using `view u/`. + 2. Test case: `add r/running c/200 :/x -/y @/1,3 ` when y occurs after x (where x and y are dates in DD-MM-YYYY format and both one year within the future) \ + Expected: Upcoming Exercise Items that occur on Monday and Wednesday (`@/1,3`) between date x and y are added to the Upcoming Exercise List. + A message will show up, indicating that the recurring exercises have been added. + 3. Test case: `add r/running c/200 :/x -/y @/1,3 ` when x occurs after y (where x and y are dates in DD-MM-YYYY format) \ + Expected: No Upcoming Exercise Item is added to the Upcoming Exercise List. Error message will show up and inform the user that + y should occur after x. + 4. Test case: `add r/running c/200 :/x -/y @/1,3 ` when y occurs after x (where x and y are dates in DD-MM-YYYY format and one or more of them are more than one year in the future) \ + Expected: No Upcoming Exercise Item is added to the Upcoming Exercise List. Error message will show up and inform the user that + both x and y should be within one year in the future. + 5. Test case: add r/running c/200 :/x -/y @/1,3 (where x and y are dates in DD-MM-YYYY format and one or more of them are in the past)\ + Expected: No Upcoming Exercise Item is added to the Upcoming Exercise List. Error message will show up and inform the user that + both x and y should be within one year in the future. + +3. Viewing Upcoming Exercise Items + 1. Test case: `view u/` when the Upcoming Exercise List is empty\ + Expected: Message indicating that the Upcoming Exercise List is empty is shown. + 2. Test case: `view u/` when the Upcoming Exercise List is not empty\ + Expected: All of the Upcoming Exercise Items are displayed. +4. Editing Upcoming Exercise Items + 1. Prerequisite: View the current Upcoming Exercise List using `view u/`. + 2. Test case: `edit u/` \ + Expected: No Upcoming Exercise Item is edited. Error message will show up and inform the user that there should be an input for item number. + 3. Test case: `edit u/1` \ + Expected: No Upcoming Exercise Item is edited. Error message will show up and inform the user that there should be an input for the details to be edited. + 4. Test case: `edit u/1 n/runnning` when the Upcoming Exercise List is empty \ + Expected: No Upcoming Exercise Item is edited. Error message will show up and inform the user that the Upcoming Exercise List is empty. + 5. Test case: `edit u/1 n/running` when the Upcoming Exercise List is not empty \ + Expected: The name of the Upcoming Exercise List with index 1 in the Upcoming Exercise List is updated to 'running'. Details of the newly updated Upcoming Exercise Item will be shown. +5. Deleting Upcoming Exercise Items + 1. Prerequisite: View the current Exercise List using `view u/`. + 2. Test case: `delete u/1` when the Exercise List is empty\ + Expected: No Upcoming Exercise Item is deleted from the Upcoming Exercise List. Error message will show up and inform the user that + the Upcoming Exercise List is empty. + 3. Test case: `delete u/1` when the Upcoming Exercise List contains at least one upcoming exercise\ + Expected: The Upcoming Exercise Item with index 1 is deleted. Details of the deleted upcoming exercise are shown. + 4. Test case: `delete u/1,2,3` when the Exercise List contains three or more upcoming exercises\ + Expected: The Upcoming Exercise Items with index 1, 2, 3 are deleted. Details of all the deleted upcoming exercises are shown. + 5. Test case: `delete u/all` \ + Expected: All Upcoming Exercise Items in the Upcoming Exercise List are deleted. Message will show up and inform the user that all upcoming exercises + in the Upcoming Exercise List are deleted. + +### **Building Food Bank** +1. Adding Food Bank Items + 1. Prerequisite: Food Bank does not contain a Food Item with the name "potato". + 2. Test case: `add fbank/potato` \ + Expected: Error as the item's calorie details were not provided. + 3. Test case: `add fbank/potato c/250`\ + Expected: Food Item with name 'potato' and calories '250' has been added to Food Bank. + 4. Test case: `add f/potato` \ + Expected: Food Item with name 'potato' and calories '250' has been added to Food List. User no longer has + to provide the calorie details for 'potato' as it can be retrieved from the Food Bank. + 5. Test case: `add fbank/potato c/300`\ + Expected: Error as the name "potato" already exists in the Food Bank, and all names in the Food Bank need to be unique. +2. Viewing Food Bank Items + 1. Test case: `view fbank/` \ + Expected: Shows the list of Food Items in the Food Bank. + 2. Test case: `view fbank/a` \ + Expected: Invalid format. The command word `view` must be followed by `fbank/` exactly. +3. Editing Food Bank Items + 1. Prerequisite: Food Bank contains at least one Item. To find the index of the Item you want to edit, use `view fbank/`. + 2. Test case: `edit fbank/` \ + Expected: Error as there is no input for item number. + 3. Test case: `edit fbank/1` \ + Expected: Error as you need to specify what to edit about this Item. + 4. Test case: `edit fbank/1 n/tomato` \ + Expected: Item number 1 in the Food Bank has been changed to 'tomato' (provided that you do not already have an Item named 'tomato' in the Food Bank). +4. Deleting Food Bank Items + 1. Prerequisite: Food Bank contains at least 3 Items. To find the index of the Item you want to delete, use `view fbank/`. + 2. Test case: `delete fbank/1,2` \ + Expected: Food Bank Items 1 and 2 have been deleted. + 3. Test case: `delete fbank/1` \ + Expected: Food Bank Item 1 has been deleted. + 4. Test case: `delete fbank/x`, where x is any number bigger than the size of the list. \ + Expected: Error as x is out of the range of the item list. + 5. Test case: `delete fbank/all` \ + Expected: All items in the Food Bank have been deleted. + +### **Building Exercise Bank** + +1. Adding Exercise Bank Items + 1. Prerequisite: Exercise Bank does not contain a Exercise Item with the name "30 mins jogging". + 2. Test case: `add ebank/30 mins jogging` \ + Expected: Error as the item's calorie details were not provided. + 3. Test case: `add ebank/30 mins jogging c/500`\ + Expected: Exercise Item with name '30 mins jogging' and calories '500' has been added to Exercise Bank. + 4. Test case: `add e/30 Mins Jogging` \ + Expected: Exercise Item with name '30 Mins Jogging' and calories '500' has been added to Food List. User no longer has + to provide the calorie details for '30 Mins Jogging' as it can be retrieved from the Exercise Bank, since the match for item name in the Item Banks is case-insensitive. + 5. Test case: `add ebank/30 mins jogging c/300`\ + Expected: Error as the name "30 mins jogging" already exists in the Exercise Bank, and all names in the Exercise Bank need to be unique. +2. Viewing Exercise Bank Items + 1. Test case: `view ebank/` \ + Expected: Shows the list of Exercise Items in the Exercise Bank. + 2. Test case: `view ebank/a` \ + Expected: Invalid format. The command word `view` must be followed by `ebank/` exactly. +3. Editing Exercise Bank Items + 1. Prerequisite: Exercise Bank contains at least one Item. To find the index of the Item you want to edit, use `view ebank/`. + 2. Test case: `edit ebank/` \ + Expected: Error as there is no input for item number. + 3. Test case: `edit ebank/1` \ + Expected: Error as you need to specify what to edit about this Item. + 4. Test case: `edit ebank/1 n/cycling` \ + Expected: Item number 1 in the Exercise Bank has been changed to 'cycling' (provided that you do not already have an Item named 'cycling' in the Exercise Bank). +4. Deleting Exercise Bank Items + 1. Prerequisite: Exercise Bank contains at least 3 Items. To find the index of the Item you want to delete, use `view ebank/`. + 2. Test case: `delete ebank/1,2` \ + Expected: Exercise Bank Items 1 and 2 have been deleted. + 3. Test case: `delete ebank/1` \ + Expected: Exercise Bank Item 1 has been deleted. + 4. Test case: `delete ebank/x`, where x is any number bigger than the size of the list. \ + Expected: Error as x is out of the range of the item list. + 5. Test case: `delete ebank/all` \ + Expected: All items in the Exercise Bank have been deleted. + +### **Exiting Program** +1. Exiting Program at any time + 1. Prerequisite: Ensures that Fitbot is still running. + 2. Test case: `bye` \ + Expected: A goodbye message will show up, and the program is terminated successfully. + +### **Manipulating and Saving Data** + +1. Manipulating _Profile_ data in the text files directly. + - Prerequisite: Data folder with profile.txt already present. (You have run through the profile creation at least once) +
    +
    • Procecures:
    • +
        +
      1. Navigate to the /data folder which is in the same directory as your Fitbot.jar)
      2. +
      3. Open the profile.txt with your editor of choice and view the attributes. It should look something like this: john|180.0|65.0|M|22|300|2

        +

        profile_text_file.png

        +
      4. +
      5. Edit the height attribute to reflect this: john|BUG|65.0|M|22|300|2

        +

        profile_text_file_modified.png

        +
      6. +
      7. Save the file and try to relaunch the application.
      8. +
      +
    • Expected: The application should detect the height is invalid and prompts you to change its value inside the application
    • +
    +
-## Non-Functional Requirements -{Give non-functional requirements} +2. Manipulating saved text files, leading to invalid line. + - Causes of invalid lines in storage: Invalid number of delimiters present, unrecognizable texts and empty spaces (eg. `E|run|309`, where the date field is not present) -## Glossary +
    +
      +
    • +

      Prerequisite: Data folder is present with items already added to lists. (We will be using exercise_list.txt as an example.)

      +

      Your exercise_list.txt should look something like this:

      +

      +
    • +
    • Procecures:
    • +
        +
      1. Change one of the lines generated by the application with invalid line.

        +

        exercise_list_modified

        +
      2. +
      3. Launch the application where it should be able to detect the invalid line in storage.
      4. +
      +
    • Expected: The user will be notified of the invalid line and it will be subsequently ignored. Upon the next operation that requires saving of data ("bye", "add e/" commands etc.), this invalid line will be overwritten and ignored, preserving the data integrity of the rest of the lines.
    • +
    +
-* *glossary item* - Definition + _Note: The dates used here was during the creation of the DeveloperGuide. We have set hard limits to not accept anything past 10 years prior or greater than 1 year into the future for completeness of our data. For instance, you are unable to set a exercise performed in 1991 as it will just ignore the line without notifying the user._ -## 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} diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..8495ecd5e3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,19 @@ -# Duke -{Give product intro here} +![Start Up Screen](./images/StartupLogo.png) + +_Fitbot_ is a **desktop app** that helps university students who are looking to +**keep track of their calorie consumption and calorie output** with the speed and convenience of +**command-line based** tools, especially in times of online school. + +_Fitbot_ can be used across all operating systems such as Windows, Mac OS X, Linux and Unix. It is optimised for use via +a [Command-Line Interface (CLI)](https://en.wikipedia.org/wiki/Command-line_interface), so it would be especially beneficial for fast +typers and people who enjoy a clean and simple app interface. + + +If you type fast, and you need an easy and quick way to record your calories, _Fitbot_ is the app for you! 💪💯 + +**Useful links**: +* If you are interested to know how to use _Fitbot_ : [User Guide](UserGuide.md) +* If you are interested to know how we built _Fitbot_ : [Developer Guide](DeveloperGuide.md) +* If you are interested to know more about the team behing _Fitbot_: [About Us](AboutUs.md) -Useful links: -* [User Guide](UserGuide.md) -* [Developer Guide](DeveloperGuide.md) -* [About Us](AboutUs.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..cded530347 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,1238 @@ -# User Guide +--- +layout: page +title: User Guide +--- -## Introduction +## **Introducing _Fitbot_** +_Fitbot_ is a **desktop app** that helps university students who are looking to +**keep track of their calorie consumption and calorie output** with the speed and convenience of +**command-line based** tools, especially in times of online school. -{Give a product intro} +_Fitbot_ can be used across all operating systems such as Windows, Mac OS X, Linux and Unix. It is optimised for use via +a [Command-Line Interface (CLI)](https://en.wikipedia.org/wiki/Command-line_interface), so it would be especially beneficial for fast +typers and people who enjoy a clean and simple app interface. -## Quick Start -{Give steps to get started quickly} +If you type fast, and you need an easy and quick way to record your calories, _Fitbot_ is the app for you! 💪💯 -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +## **About This User Guide** +This guide explains how you can use all the features available on _Fitbot_ and +maximise your user experience. -## Features +To preface, _Fitbot_ was developed with modern-day university students in mind as our target audience. As a result, we do expect users to have a basic level +of comfort with using computers, and it would be even better if you are familiar with CLI. However, if you do not, do not worry as we +have provided a comprehensive set of instructions to [get started](#2-quick-start)! -{Give detailed description of each feature} -### Adding a todo: `todo` -Adds a new item to the list of todo items. +Throughout this guide, we will be using some special formatting and symbols to bring your attention to certain aspects: -Format: `todo n/TODO_NAME d/DEADLINE` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +| Formatting | Meaning | +|--------|-------| +_italics_ | Text that has been _italicised_ indicates that it is a term specific to _Fitbot_. +**bold** | Text that has been **bolded** indicates that it is important. +`abc` | Text with a `highlight` indicates that it is code that can be typed by you into the command line or displayed by _Fitbot_. +ℹ️ |This symbol indicates important information. +❗| This symbol indicates important rules to follow. Make sure you **pay extra attention** to the information, else _Fitbot_ will fail to execute certain functions! +💡|This symbol indicates tips and tricks that you can use to make your _Fitbot_ experience even smoother. +⏫| This symbol indicates a shortcut to the content page. You may click it to quickly navigate back to the content page. -Example of usage: -`todo n/Write the rest of the User Guide d/next week` -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` -## FAQ +## **Content Page** -**Q**: How do I transfer my data to another computer? +[1. Terminology](#1-terminology) -**A**: {your answer here} +[2. Quick Start](#2-quick-start) -## Command Summary +[3. Set Up Profile](#3-set-up-profile) -{Give a 'cheat sheet' of commands here} +[4. Features](#4-features) +- [4.1 Customising Your Profile](#41-customising-your-profile) + - [4.1.1 Setting Up Your Profile](#411-setting-up-your-profile) + - [4.1.2 Viewing Profile `profile`](#412-viewing-profile-profile) + - [4.1.3 Updating Profile Attributes](#413-updating-profile-attributes) + - [4.1.4 Calculating BMI `bmi`](#414-calculating-bmi-bmi) +- [4.2 Recording Your Food Consumption](#42-recording-your-food-consumption) + - [4.2.1 Adding Food Items `add f/`](#421-adding-food-items-add-f) + - [4.2.2 Viewing Food List `view f/`](#422-viewing-food-list-view-f) + - [4.2.3 Deleting Food Items `delete f/`](#423-deleting-food-items-delete-f) +- [4.3 Recording Your Exercises](#43-recording-your-exercises) + - [4.3.1 Adding Exercise Items `add e/`](#431-adding-exercise-items-add-e) + - [4.3.2 Viewing Exercise Items `view e/`](#432-viewing-exercise-items-view-e) + - [4.3.3 Deleting Exercises `delete e/`](#433-deleting-exercises-delete-e) +- [4.4 Scheduling Your Exercises](#44-scheduling-your-exercises) + - [4.4.1 Adding Upcoming Exercise Items `add e/`](#441-adding-upcoming-exercise-items-add-e) + - [4.4.2 Adding Recurring Exercise Items `add r/`](#442-adding-recurring-exercise-items-add-r) + - [4.4.3 Viewing Upcoming Exercise List `view u/`](#443-viewing-upcoming-exercise-items-view-u) + - [4.4.4 Editing Upcoming Exercise Items `edit u/`](#444-editing-upcoming-exercise-items-edit-u) + - [4.4.5 Deleting Upcoming Exercise Items `delete u/`](#445-deleting-upcoming-exercise-items-delete-u) +- [4.5 Building Your Food Bank](#45-building-your-food-bank) + - [4.5.1 Adding Food Bank Items `add fbank/`](#451-adding-food-bank-items-add-fbank) + - [4.5.2 Viewing Food Bank `view fbank/`](#452-viewing-food-bank-view-fbank) + - [4.5.3 Editing Food Bank Items `edit fbank/`](#453-editing-food-bank-items-edit-fbank) + - [4.5.4 Deleting Food Bank Items `delete fbank/`](#454-deleting-food-bank-items-delete-fbank) +- [4.6 Building Your Exercise Bank](#46-building-your-exercise-bank) + - [4.6.1 Adding Exercise Bank Items `add ebank/`](#461-adding-exercise-bank-items-add-ebank) + - [4.6.2 Viewing Exercise Bank `view ebank/`](#462-viewing-exercise-bank-view-ebank) + - [4.6.3 Editing Exercise Bank Items `edit ebank/`](#463-editing-exercise-bank-items-edit-ebank) + - [4.6.4 Deleting Exercise Bank Items `delete ebank/`](#464-deleting-exercise-bank-items-delete-ebank) +- [4.7 Viewing Your Calorie Summary `overview`](#47-viewing-your-calorie-summary-overview) +- [4.8 Viewing Help `help`](#48-viewing-help-help) +- [4.9 Exiting Program `bye`](#49-exiting-program-bye) -* Add todo `todo n/TODO_NAME d/DEADLINE` +[5. Data Management](#5-data-management) +- [5.1 Saving Data](#51-saving-the-data) +- [5.2 Editing Data](#52-editing-the-data-file) + +[6. Data Limits](#6-data-limits) + +[7. FAQ](#7-faq) + +[8. Command Summary](#8-command-summary) + +## **1. Terminology** + +***Calorie*** - Calorie is a unit used to measure the energy of an item. One calorie is the amount of energy required to raise the +temperature of one gram of water by one degree Celsius. On average, a male will require approximately 2500 cal consumed +from food per day while a female will require around 2000 cal intake per day.\ +\ +***Calorie goal*** - The calorie goal is defined as the amount of calories you wish to achieve per day. It calculated by subtracting the calories you burnt naturally and physically per day from your calorie intake. +If you are planning to lose weight, it is recommended to set your daily calorie goal as a negative value and if you are planning to gain weight, it is recommended to set your daily calorie goal as +a positive value calorie goal. + +***Net Calorie*** - Net calorie is the difference between calorie goal and the total calories burnt from exercising and +[body metabolism](https://www.news-medical.net/life-sciences/What-is-Metabolism.aspx). +For your information, we use [basal metabolic rate (BMR)](https://en.wikipedia.org/wiki/Basal_metabolic_rate) to compute +the rate of calories consumed by body metabolic activity.\ +\ +***Activity factor*** - Activity factor is a measure of the level of activity. This value is used in the calculation of BMR. In our application, +we use an integer ranged from 1 to 5 to measure the activity factor: +- *Integer 1*: Little to no exercise (0-1 day of exercises a week) +- *Integer 2*: Lightly active (1-3 days of exercises a week) +- *Integer 3*: Moderately active (3-5 days of exercises a week) +- *Integer 4*: Very active (6-7 days of exercises a week) +- *Integer 5*: Extremely active (when sports is your passion and have a very physical job scope) + +***Body Mass Index (BMI)*** - A measure to evaluate if the body weight is healthy. BMI is computed by using the body +weight (in kg) divided by the square of the body height (in m). \ +\ +***Item*** - We use the term _Item_ to represent the items that can be stored in _Fitbot_. The available items +are _Food Item_ and _Exercise Item_.\ +\ +***Upcoming Exercise*** - We define the exercise with date after today's date as an Upcoming Exercise. This kind of exercise +will be handled specifically. It will not be added to the Exercise List but will be saved internally in the storage file. More +details of the process can be found [here](#442-adding-recurring-exercise-items-add-r).\ +\ +***Item Bank*** - The Item Bank is an item storage that is capable of storing the Food or Exercise Item with its respective calories. This is +to help you to store the calorie intake of a specific Food Item or the calories burnt from a specific Exercise Item for future use. More details +on how to add item into the Item Bank can be found [here](#451-adding-food-bank-items-add-fbank).\ +\ +***Parameters*** - Parameters are values in the command format that _Fitbot_ expects you to provide. For example, `height` and `weight` +are parameters used to obtain your body height and weight values.\ +\ +***Prefix*** - Prefix is used to help _Fitbot_ identifies the corresponding parameters you provided. For example, to allow _Fitbot_ +to save your body height and weight, prefixes `h/` and `w/` are used. More details about how to use the prefixes in each +command can be found [here](#4-features). + +[⏫ Back to content page](#content-page) + + +## **2. Quick Start** + +1. Ensure you have Java 11 or above installed in your computer. If not, install Java 11 [here](https://www.oracle.com/java/technologies/downloads/). +2. Download the latest version of [Fitbot.jar](https://github.com/AY2122S1-CS2113T-F14-2/tp/releases) from the cloud. +3. Copy the file to the folder you want to use as the home folder for your _Fitbot_. +4. Go to command prompt and change the directory to the file's location. [Not sure how to do this?](https://www.groovypost.com/howto/open-command-window-terminal-window-specific-folder-windows-mac-linux/) +5. Type `java -jar Fitbot.jar` into the command prompt and press enter to start the program. + +If successfully loaded, you will see this logo: + +![Start Up Screen](./images/StartupLogo.png) + +You are now all good to start using _Fitbot_! + +[⏫ Back to content page](#content-page) + + +## **3. Set Up Profile** + +_Fitbot_ will detect if there is a profile present in the application. If you have not set up a profile, +_Fitbot_ will ask you for your particulars. + + +Particulars required include name, height, weight, age, gender, calorie goal and activity factor. The application will prompt for your particulars again if your input is incorrect. + +Below are the questions that you will be prompted. + +**Name Input** + +To let _Fitbot_ know how to address you, please input your name. \ +Note that the special characters `/` and `|` are not allowed! + +```text +*========================================================================================================* + Fitbot realised that your profile has not been created. + Let's start creating a profile below! +__________________________________________________________________________________________________________ +What's your name? +__________________________________________________________________________________________________________ +Lisa +``` + +Next, _Fitbot_ will need to know some of your personal details in order to calculate your calories more accurately. + + +**Height Input** + +Please input your height (in centimetres) as positive numbers. Decimals are accepted (e.g. 159.2). + +``` +__________________________________________________________________________________________________________ +Nice name you have there! Hello Lisa. +What's your height? (in cm) +__________________________________________________________________________________________________________ +159.2 +``` + +**Weight Input** + +Please input your weight (in kilograms) as positive numbers. Similar to height, decimals are accepted (e.g. 50.0). + +```text +__________________________________________________________________________________________________________ +Your height is 159.2cm. +What's your weight? (in kg) +__________________________________________________________________________________________________________ +50.0 +``` + +**Gender Input** + +Please input the letter 'm' if you are a male and 'f' if you are a female (e.g. f). + +```text +__________________________________________________________________________________________________________ +Your weight is 50.0kg. +What is your gender? (If you are a male, type 'm', if you are a female , +type 'f') +__________________________________________________________________________________________________________ +f +``` + + +**Age Input** + +Please input your age as a positive whole number (e.g. 21). + +```text +__________________________________________________________________________________________________________ +You are a female. +How old are you? +__________________________________________________________________________________________________________ +21 +``` + + +**Calorie Goal** + +To customise your experience, let _Fitbot_ know what your [net calorie goal](#1-terminology) (in cal) is so that _Fitbot_ can tell you if you are on track. \ +Please input your calorie goal as a whole number in the range of -3000 to 10000 (e.g. 1500). + +```text +__________________________________________________________________________________________________________ +You are 21 years old. +Please input your net calorie goal. +__________________________________________________________________________________________________________ +1500 +``` + +**Activity Factor** + +Let _Fitbot_ know what your [activity factor](#1-terminology) is by choosing 1 to 5 based on the activity level description as shown below (e.g. 2). + +```text +__________________________________________________________________________________________________________ +You calorie goal is 1500 cal. +In terms of activity level, how active are you? +Based on the rubrics below, please key in 1 to 5 based on how active you are. +1 -> Sedentary - Little or no exercise +2 -> Lightly Active - Light exercise or sports, around 1-3 days a week +3 -> Moderately Active - Regular exercise or sports, around 3-5 days a week +4 -> Very Active - Frequent exercise or sports, around 6-7 days a week +5 -> If you are extra active - Sports or exercising is your passion and + a physical jobscope. +__________________________________________________________________________________________________________ +2 +``` + + +Once you are done filling up all the particulars above, you should see the following message: +```text +Your activity factor is 2. + + Profile created successfully! +*========================================================================================================* + +You can start by typing a command or view the list of available commands by +typing "help". +__________________________________________________________________________________________________________ + +``` + +After setting up the profile, you are now ready to use the rest of _Fitbot_'s features! + +❗ You can exit _Fitbot_ with the `bye` command, but if you exit before completing the profile, all attributes will +not be saved. + +[⏫ Back to content page](#content-page) + +## **4. Features** + +ℹ️ ***Command Format*** + + +- Commands are not case-sensitive (e.g. `help`, `HELP`,`hElP` are all able to execute the `help` command) + +- Words in upper case (e.g. `UPPER_CASE`) are used to signify parameters. + +- Parameters in curly brackets `{}` are optional. + + e.g. `bmi {h/HEIGHT_IN_CM w/WEIGHT_IN_KG}` can be `bmi` or `bmi h/150 w/70` + +- The order of the parameters do not matter. + + e.g. `add f/potato c/200 d/07-11-2021` or `add f/potato d/07-11-2021 c/200` both adds a Food Item called "potato" with 200 calories on 07 Nov 2021. + +❗ Please do not use the characters `/` and `|` in your input other than to specify parameters! +([more details](#6-data-limits)) + +### **4.1 Customising Your Profile** + +You can customise your profile with the following commands. + +#### 4.1.1 Setting Up Your Profile + +Refer to [3. Set Up Profile](#3-set-up-profile) to set up your profile for the first time. + +#### 4.1.2 Viewing Profile: `profile` + +This command allows you to view your name, height, weight, gender, age, calorie goal and activity factor. + +Format: `profile` + +Example: + +```text +profile +__________________________________________________________________________________________________________ +Hello Lisa! This is your profile: +*====================================== + Height 159.2cm + Weight 50.0kg + Gender F + Age 21 + Calorie goal 1500 cal + Activity factor 2 +======================================* +__________________________________________________________________________________________________________ +``` +#### 4.1.3 Updating Profile Attributes + +You can update your profile attributes using this command. + +Format: `profile {n/NAME} {h/HEIGHT} {w/WEIGHT} {s/GENDER} {a/AGE} {g/CALORIE_GOAL} {x/ACTIVITY_FACTOR}` \ +\ +where : +\ +       n/          name \ +       h/          height in cm \ +       w/         weight in kg \ +       a/          age \ +       g/          calorie goal \ +       s/          gender (M or F) \ +       x/          activity factor (1: Sedentary - 5: Extremely Active) + +❗️ Please input at least 1 of the optional parameters (in no particular order)! + +Example: +```text +profile a/22 g/1800 +__________________________________________________________________________________________________________ +Your profile has been updated! +Hello Lisa! This is your profile: +*=========================================================== + Height 159.2cm + Weight 50.0kg + Gender F + Age 22 + Calorie Goal 1800 cal + Activity Factor 2 +===========================================================* +__________________________________________________________________________________________________________ +``` + +#### 4.1.4 Calculating BMI `bmi` + +You can calculate your [BMI](#1-terminology) using this command. +_Fitbot_ will also tell you the status of your BMI *(underweight, healthy, overweight, obese)* based on the calculated value. + +Format: `bmi {h/HEIGHT_IN_CM w/WEIGHT_IN_KG}` + +💡 **Tip:** If you do not provide the two optional parameters of height and weight, the BMI will be computed based on the current +height and weight values in your profile. + +Example: +- `bmi` calculates the BMI value based on the current height and weight in your profile. +- `bmi h/180 w/65` calculates the BMI value based on height 180cm and weight 65kg. + +```text +bmi +__________________________________________________________________________________________________________ +Your BMI value according to your current profile is: + 22.5 (Healthy) +__________________________________________________________________________________________________________ +bmi h/180 w/65 +__________________________________________________________________________________________________________ +The calculated BMI value is 20.1 (Healthy) +__________________________________________________________________________________________________________ +``` + +[⏫ Back to content page](#content-page) + +### **4.2 Recording Your Food Consumption** + +You can record all the Food Items consumed within a week into the Food List. This allows you +to keep track of your food calorie consumption and gain a better understanding of your diet habits. + +#### 4.2.1 Adding Food Items `add f/` + +You can record Food Items consumed on a specific date and time with a specific calorie value into the Food List. +This command allows you to add a new Food Item to the Food List. Repeated Food Items on the same day and time is supported too. + +Format: `add f/ITEM {c/CALORIES} {d/DD-MM-YYYY} {t/HHMM}` adds a Food Item consumed with its respective calories on the given +date (`DD-MM-YYYY`) and time (`HHMM`). + +❗ The input date `DD-MM-YYYY` must be within the past 7 days (including today). For example, if today's date is `07-11-2021`, then the input +date must be within `01-11-2021` to `07-11-2021`. ([more details](#6-data-limits)) + + +Example: +```text +add f/chicken rice c/607 d/04-11-2021 t/1400 +__________________________________________________________________________________________________________ +A food item has been added: + chicken rice (607 cal) @ 14:00, 04 Nov 2021 +__________________________________________________________________________________________________________ +``` +💡 **Tip:** If you do not specify the date and time of the Food Item, it is assumed that the date and time is based on the date and time of input. + +💡 **Tip:** It is possible to add Food Item without providing the calories of it. You can do this by saving the corresponding +Food Item into the Food Bank. More details can be found [here](#45-building-your-food-bank). + + + +#### 4.2.2 Viewing Food List `view f/` + +You may want to view what Food Items you have consumed and how many calories you have taken in this week. This command will show +the list of Food Items and their calories that you have added within the past 7 days (including today) into the Food List. + +Format: `view f/` + +Example: + +```text +view f/ +__________________________________________________________________________________________________________ +Here is a summary of all the food items you have consumed in the past week: +.......................................................................................................... +You have consumed 3 food item(s) on Thursday (04 Nov 2021): +In the morning: + 1. donut x2 (607 cal) @ 10:00 +In the afternoon: + 1. chicken rice (607 cal) @ 14:00 +In the evening: + 1. yong tau foo (560 cal) @ 19:20 +Total calories consumed in the day: 1774 cal +.......................................................................................................... +You have consumed 3 food item(s) on Friday (05 Nov 2021): +In the morning: + 1. butter bread x2 (418 cal) @ 08:30 +In the afternoon: + 1. penang laksa (377 cal) @ 13:00 +In the evening: + 1. sliced fish bee hoon (349 cal) @ 18:40 +Total calories consumed in the day: 1144 cal +.......................................................................................................... +You have consumed 1 food item(s) on Saturday (06 Nov 2021): +At night: + 1. roti prata x3 (507 cal) @ 23:50 +Total calories consumed in the day: 507 cal +.......................................................................................................... +Total number of food consumed in this week: 7 +Total calories consumed in this week: 3425 cal +__________________________________________________________________________________________________________ +``` + +#### 4.2.3 Deleting Food Items `delete f/` + +If you have accidentally added a wrong Food Item inside your Food List, there is no need to worry! This command allows you to +remove a specific Food Item in the Food List. + +Format: `delete f/LIST_NO d/DD-MM-YYYY t/HHMM` deletes the *nth* Food Item in the Food List which has the date (`DD-MM-YYYY`) +and time (`HHMM`), where *n* is the index of the Food to delete. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list. + + +Example: + +```text +delete f/1 d/04-11-2021 t/1000 +__________________________________________________________________________________________________________ +A food item has been deleted: + donut x2 (607 cal) @ 10:00, 04 Nov 2021 +__________________________________________________________________________________________________________ +``` +💡 **Tip:** If you wish to remove all the Food Items from the list, there is a shortcut command: `delete f/all`. +``` +delete f/all +__________________________________________________________________________________________________________ +All food items have been removed. +__________________________________________________________________________________________________________ +``` +[⏫ Back to content page](#content-page) + +### **4.3 Recording Your Exercises** + +Besides Food Items, you may also record exercises that you have done into the Exercise List. This will allow you to keep +track of total calories burnt and check whether you have done sufficient exercises. + +#### 4.3.1 Adding Exercise Items `add e/` + +By using this command, you can add any Exercise Items done into the Exercise List. Repeated exercises on the same day is supported too. + +Format: `add e/ITEM {c/CALORIES} {d/DD-MM-YYYY}` adds an Exercise Item with its respective calories burnt on the given date +(`DD-MM-YYYY`). + +ℹ️ If the date `DD-MM-YYYY` provided is in the future, this exercise will be treated as an Upcoming Exercise Item, +and it will be added to the [Upcoming Exercise List](#441-adding-upcoming-exercise-items-add-e) instead. + +❗ The input date `DD-MM-YYYY` must be within the past 7 days (including today). For example, if today's date is `07-11-2021`, then the input +date must be within `01-11-2021` to `07-11-2021`. ([more details](#6-data-limits)) + +Example: + +```text +add e/hiit c/290 d/07-11-2021 +__________________________________________________________________________________________________________ +An exercise item has been added: + hiit (290 cal) @ 07 Nov 2021 +__________________________________________________________________________________________________________ +``` + +💡 **Tip:** Similar to [Food Item](#421-adding-food-items-add-f), if you do not specify the date and time of the Exercise Item, +it is assumed that the date and time is based on the date and time of input. + +💡 **Tip:** It is possible to add Exercise Item without providing the calories burnt from it. You can do this by saving the corresponding +Exercise Item into the Exercise Bank. More details can be found at [here](#46-building-your-exercise-bank). + +#### 4.3.2 Viewing Exercise Items `view e/` + +It is possible to view and check all the exercises you have added. This command is for you to view all the exercises that you have done +and their calories within the past 7 days (including today) in the list. + +Format: `view e/` + +Example: + +```text +view e/ +__________________________________________________________________________________________________________ +Here is a summary of all the exercises you have done in the past week: +.......................................................................................................... +You have done 1 exercise(s) on Wednesday (03 Nov 2021): + 1. 5km run (500 cal) +Total calories burnt in the day: 500 cal +.......................................................................................................... +You have done 1 exercise(s) on Friday (05 Nov 2021): + 1. biking (500 cal) +Total calories burnt in the day: 500 cal +.......................................................................................................... +You have done 2 exercise(s) on Sunday (07 Nov 2021): + 1. training (200 cal) + 2. hiit (290 cal) +Total calories burnt in the day: 490 cal +.......................................................................................................... +Total exercises done in this week: 4 +Total calories burnt in this week: 1490 cal +__________________________________________________________________________________________________________ +``` +ℹ️ It is also possible to view all the Upcoming Exercise Items from the Upcoming Exercise List that you have added before. More +details can be found [here](#443-viewing-upcoming-exercise-items-view-u). + +#### 4.3.3 Deleting Exercises `delete e/` + +In case you have added the wrong exercise, this command allows you to remove any exercise from the Exercise List. + +Format: `delete e/LIST_NO d/DD-MM-YYYY` deletes the *nth* Exercise Item in the Exercise List which contains the date (`DD-MM-YYYY`), +where *n* is the index of the exercise to delete. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list. + +Example: +``` +delete e/1 d/05-11-2021 +__________________________________________________________________________________________________________ +An exercise item has been deleted: + biking (500 cal) @ 05 Nov 2021 +__________________________________________________________________________________________________________ +``` + +💡 **Tip:** If you wish to remove all the Exercise Items from the Exercise List, there is a shortcut command: `delete e/all`. + +``` +delete e/all +__________________________________________________________________________________________________________ +All exercise items have been removed. +__________________________________________________________________________________________________________ +``` +[⏫ Back to content page](#content-page) + +### **4.4 Scheduling Your Exercises** + +Besides your current exercises, you may also record all your Upcoming Exercise Items that you plan to do into the Upcoming Exercise List. +This will allow you to plan your exercises in advance and be more aware of your calorie output from your exercises in the near future. + +ℹ️ All of your upcoming Exercise Items will be automatically loaded into the Exercise List if the dates of the upcoming Exercise Items have passed. + +#### 4.4.1 Adding Upcoming Exercise Items `add e/` + +By adding an Exercise Item with a future date, your exercise will be treated as an Upcoming Exercise Item and gets added into the Upcoming Exercise List. + +Format:`add e/ITEM {c/CALORIES} {d/DD-MM-YYYY}` adds an Upcoming Exercise Item with its respective calories burnt on the given date (`DD-MM-YYYY`}. + + +❗️ The date `DD-MM-YYYY` provided must be in the future. Otherwise, if it is within the past 7 days (including today), it will be + added to the [Exercise List](#431-adding-exercise-items-add-e) instead.\ +❗ The date `DD-MM-YYYY` can only be within a year from tomorrow as any exercises beyond a year is too far into the future. ([more details](#6-data-limits)) + +Example: +```text +add e/hiit c/290 d/01-01-2022 +__________________________________________________________________________________________________________ +An exercise item for the future has been added: + hiit (290 cal) @ 01 Jan 2022 +__________________________________________________________________________________________________________ +``` + + + +#### 4.4.2 Adding Recurring Exercise Items `add r/` + +It is possible to schedule exercises that you do on a regular basis. By using this command, you can conveniently add recurring exercises +into the Upcoming Exercise List. This can be particularly useful if you have weekly trainings to record! + +Format: `add r/ITEM c/CALORIES :/START_DATE -/END_DATE @/DAY_OF_THE_WEEK {,DAY_OF_THE_WEEK,...}` adds recurring exercises with its respective calories burnt to the Upcoming Exercise List. +You may specify which day(s) of the week by providing multiple `DAY_OF_THE_WEEK` that are separated by commas. + +❗ `START_DATE` and `END_DATE` must be in the future.\ +❗ `START_DATE` must be before `END_DATE`.\ +❗ `START_DATE` and `END_DATE` must follow `DD-MM-YYYY` format.\ +❗ `START_DATE` and `END_DATE` can only be within a year from today as any exercises beyond a year is too far into the future.\ +❗ `DAY_OF_THE_WEEK` must be an integer between 1 and 7, representing Monday to Sunday respectively. + + +Example: +```text +add r/running c/200 :/01-12-2021 -/30-12-2021 @/1,4 +__________________________________________________________________________________________________________ +Recurring exercise item for the future has been added! +You can view your upcoming exercises by typing "view u/"! +__________________________________________________________________________________________________________ +``` + +#### 4.4.3 Viewing Upcoming Exercise Items `view u/` + +In order to view all of your Upcoming Exercise Items, this command can be used and all of your scheduled exercises and their calories +will be displayed for you to see. + +Format: `view u/` + +Example: +```text +view u/ +__________________________________________________________________________________________________________ +You have 10 upcoming exercise(s): + 1. running (200 cal) (Thursday 02 Dec 2021) + 2. running (200 cal) (Monday 06 Dec 2021) + 3. running (200 cal) (Thursday 09 Dec 2021) + 4. running (200 cal) (Monday 13 Dec 2021) + 5. running (200 cal) (Thursday 16 Dec 2021) + 6. running (200 cal) (Monday 20 Dec 2021) + 7. running (200 cal) (Thursday 23 Dec 2021) + 8. running (200 cal) (Monday 27 Dec 2021) + 9. running (200 cal) (Thursday 30 Dec 2021) + 10. hiit (290 cal) (Saturday 01 Jan 2022) +__________________________________________________________________________________________________________ +``` + +#### 4.4.4 Editing Upcoming Exercise Items `edit u/` + +If you would like to make some changes to the details of your Upcoming Exercise Items, this command can be used and your Upcoming Exercise Items +in the Upcoming Exercise List will be updated. + +Format: `edit u/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES} {d/NEW_DATE}` edits the *nth* item in the Upcoming Exercise List, where *n* is the index of the Exercise to edit. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list.\ +❗ Though the parameters are optional, do provide at least one of them so that _Fitbot_ knows what you want to update about the Item! + + +Example: +```text +edit u/1 c/150 d/30-11-2021 +__________________________________________________________________________________________ +Item number 1 in upcoming exercises has been changed to: + running (150 cal) @ 30 Nov 2021 +__________________________________________________________________________________________ +``` + +#### 4.4.5 Deleting Upcoming Exercise Items `delete u/` + +In case any Upcoming Exercise Item is cancelled and you would like to remove it from the Upcoming Exercise List, you can use +this command to delete that particular Upcoming Exercise Item. Deletion of multiple Upcoming Exercise Items is supported too. + +Format: `delete u/LIST_NO {,LIST_NO,...}` deletes the *nth* Upcoming Exercise Item in the Upcoming Exercise List, where *n* is the index of the exercise to delete. +You may delete multiple Exercise Items at once by providing multiple `LIST_NO` that are separated by commas. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list. + +Example: +``` +delete u/1,2 +__________________________________________________________________________________________ +All of the following upcoming exercises have been deleted: + 1. running (150 cal) (Tuesday 30 Nov 2021) + 2. running (200 cal) (Monday 06 Dec 2021) +Number of upcoming exercise(s) left: 8 +__________________________________________________________________________________________ +``` +💡 **Tip:** If you wish to remove all the Exercise Items from the Upcoming Exercise List, there is a shortcut command: `delete u/all`. +``` +delete u/all +__________________________________________________________________________________________________________ +All upcoming exercise items have been removed. +__________________________________________________________________________________________________________ +``` +[⏫ Back to content page](#content-page) + +### **4.5. Building your Food Bank** +Do you have certain dishes or snacks that you frequently consume? Instead of having to key in their calories everytime you want to +record them into the Food List, you can add it into the Food Bank once so that the next time you want to record this item in the Food List, +you would only have to provide the name of the item! + +#### 4.5.1 Adding Food Bank Items `add fbank/` +You can add a Food Item into the Food Bank using this command. + +Format: `add fbank/ITEM c/CALORIES` + +Example: +``` +add fbank/McSpicy Meal c/1081 +__________________________________________________________________________________________________________ +A food item has been added to the food bank: + McSpicy Meal (1081 cal) +__________________________________________________________________________________________________________ +``` + +❗️ The Food Bank cannot have duplicate item names as _Fitbot_ needs to be able to uniquely identify the item calories. Note that +the match for item name is **case-insensitive**. For example, if an item named `potato` already exists in the Food Bank, an item named `POTATO` also cannot be added in. + +After adding the item to the Food Bank, you can now add the item into the Food List whenever you want without specifying the calories. +Simply provide the name of the item and _Fitbot_ will search up the corresponding calorie value for the item with the same name for you from the Food Bank! As before, note that the matching of names is +**case-insensitive**. + +Example: +``` +add f/mcspicy meal d/07-11-2021 t/1536 +__________________________________________________________________________________________________________ +A food item has been added: + mcspicy meal (1081 cal) @ 15:36, 07 Nov 2021 +__________________________________________________________________________________________________________ +``` + +#### 4.5.2 Viewing Food Bank `view fbank/` +You can view all the Items in the Food Bank using this command. + +Format: `view fbank/` + +Example: +``` +view fbank/ +__________________________________________________________________________________________________________ +You have 2 food(s) in your food bank: + 1. Baked Potato (300 cal) + 2. McSpicy Meal (1081 cal) +__________________________________________________________________________________________________________ +``` + +#### 4.5.3 Editing Food Bank Items `edit fbank/` +If you have entered any wrong information or wish to update existing Items in the Food Bank, you can use this command to do so. + +Format: `edit fbank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}` edits the *nth* Item in the Food Bank, where *n* is the index of the Food Item to edit. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list.\ +❗ Though the parameters are optional, do provide at least one of them so that _Fitbot_ knows what you want to update about the Item! + +Example: +``` +edit fbank/1 c/350 +__________________________________________________________________________________________________________ +Item number 1 in the food bank has been changed to: + Baked Potato (350 cal) +__________________________________________________________________________________________________________ +``` + +#### 4.5.4 Deleting Food Bank Items `delete fbank/` +You can delete an Item from the Food Bank using this command. + +Format: `edit fbank/LIST_NO {,LIST_NO,...}` deletes the *nth* Item in the Food Bank, where *n* is the index of the Food Item to edit. +You may delete multiple Food Items at once by providing multiple `LIST_NO` that are separated by commas. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list. + +Example: +``` +delete fbank/1,2 +__________________________________________________________________________________________________________ +All of the following food bank items have been deleted: + 1. Baked Potato (350 cal) + 2. McSpicy Meal (1001 cal) +Number of food item(s) left in the food bank: 0 +__________________________________________________________________________________________________________ +``` + +💡 **Tip:** If you wish to remove all the Food Items from the Food Bank, there is a shortcut command: `delete fbank/all`. +``` +delete fbank/all +__________________________________________________________________________________________________________ +All food items in the food bank have been removed. +__________________________________________________________________________________________________________ +``` + +[⏫ Back to content page](#content-page) + +### **4.6. Building your Exercise Bank** +Have a standard workout routine that you use? You can add it into the Exercise Bank for _Fitbot_ to remember the amount of calories you +burn from it! + + +#### 4.6.1 Adding Exercise Bank Items `add ebank/` +You can add an Exercise Item into the Exercise Bank using this command. + +Format: `add ebank/ITEM c/CALORIES` + +Example: +``` +add ebank/30 min jog c/450 +__________________________________________________________________________________________________________ +An exercise item has been added to the exercise bank: + 30 min jog (450 cal) +__________________________________________________________________________________________________________ +``` + +❗️ The Exercise Bank cannot have duplicate item names as _Fitbot_ needs to be able to uniquely identify the item calories. Note that +the match for item name is **case-insensitive**. For example, if an item named `jogging` already exists in the Exercise Bank, an item named `Jogging` also cannot be added in. + +Similar to the Food Bank, after adding the item to the Exercise Bank, the next time you want to add this item to your Exercise List, you can just provide its name +and _Fitbot_ will search up the corresponding calorie value for the item with the same name for you from the Exercise Bank! As before, note that the matching of names is +**case-insensitive**. + +Example: +``` +add e/30 Min Jog d/07-11-2021 +__________________________________________________________________________________________________________ +An exercise item has been added: + 30 Min Jog (450 cal) @ 07 Nov 2021 +__________________________________________________________________________________________________________ +``` + +#### 4.6.2 Viewing Exercise Bank `view ebank/` +You can view all the Items in the Exercise Bank using this command. + + +Format: `view ebank/` + +Example: +``` +view ebank/ +__________________________________________________________________________________________________________ +You have 1 exercise(s) in your exercise bank: + 1. 30 min jog (450 cal) +__________________________________________________________________________________________________________ +``` + + +#### 4.6.3 Editing Exercise Bank Items `edit ebank/` +If you have entered any wrong information or wish to update existing Items in the Exercise Bank, you can use this command to do so. + +Format: `edit ebank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}` edits the *nth* Item in the Exercise Bank, where *n* is the index of the Exercise Item to edit. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list.\ +❗ Though the parameters are optional, do provide at least one of them so that _Fitbot_ knows what you want to update about the Item! + + +Example: +``` +edit ebank/1 n/35 min jog +__________________________________________________________________________________________________________ +Item number 1 in the exercise bank has been changed to: + 35 min jog (450 cal) +__________________________________________________________________________________________________________ +``` + + +#### 4.6.4 Deleting Exercise Bank Items `delete ebank/` +You can delete an Item from the Exercise Bank using this command. + +Format: `delete ebank/LIST_NO {,LIST_NO,...}` deletes the *nth* Item in the Exercise Bank, where *n* is the index of the Exercise Item to edit. +You may delete multiple Exercise Items at once by providing multiple `LIST_NO` that are separated by commas. + +❗ `LIST_NO` must be a positive integer within the range of the number of Items in the list. + +Example: +``` +delete ebank/1 +__________________________________________________________________________________________________________ +An exercise item has been deleted from the exercise bank: + 35 min jog (450 cal) +Number of exercise item(s) left in the exercise bank: 0 +__________________________________________________________________________________________________________ +``` + +💡 **Tip:** If you wish to remove all the Exercise Items from the Exercise Bank, there is a shortcut command: `delete ebank/all`. +``` +delete ebank/all +__________________________________________________________________________________________________________ +All exercise items in the exercise bank have been removed. +__________________________________________________________________________________________________________ +``` + + +[⏫ Back to content page](#content-page) + +### **4.7 Viewing Your Calorie Summary** `overview` +This command is used to view the summary of the calories gained from food, calories burnt from exercises as well as +net calories after including BMR. + +Format: `overview` + +Example: +```text +overview +-*WEEKLY OVERVIEW*- +Hi Lisa, this is your calorie summary for the week. + +Food: +You have consumed 14600 cal this week from 01-Nov to 07-Nov. +Calorie gained from food (Daily) +01-Nov █████████████████ 2600 +02-Nov █████████████ 1900 +03-Nov ██████████████████████████████ 4500 +04-Nov ████████████████ 2400 +05-Nov █████████████████████ 3200 +06-Nov 0 +07-Nov 0 + +Exercise: +You have burnt 3090 cal this week from 01-Nov to 07-Nov. +Calorie burnt from exercise (Daily) +01-Nov ██████ 230 +02-Nov ██████████████████████████████ 1200 +03-Nov ████████████████████ 780 +04-Nov ██████████████ 540 +05-Nov █████████ 340 +06-Nov 0 +07-Nov 0 + +Daily net calories**: +01-Nov : -1130 +02-Nov : -2800 +03-Nov : 220 +04-Nov : -1640 +05-Nov : -640 +06-Nov : -3500 +07-Nov : -3500 + +Number of supper meals this week: 1 + +** Net calories = Food consumed - Exercise output - your basal metabolic rate, +where your basal metabolic rate is a factor of your age, gender, height and +weight retrieved from your profile. +All calculations are done in calories. +__________________________________________________________________________________________________________ +This is your calorie overview for today: +Your calorie gained from food is: 0 +Your calorie lost from exercise is: 0 +Your net calorie intake is: -3500 +Your calorie goal is: 123 +You are 3623 cal away from your goal! +__________________________________________________________________________________________________________ +``` +[⏫ Back to content page](#content-page) + +### **4.8 Viewing Help** `help` + +If you need help using _Fitbot_, you can use this command to see a list of commands recognised by _Fitbot_ and their usage. + +Format: `help` + +Example: +```text +help +__________________________________________________________________________________________________________ +Welcome to the help page. +Below are the commands to get you started. +More details could be found on: +https://tinyurl.com/fitbot-user-guide + +In the formats of the command, prefixes wrapped in curly brackets {} +means that they are optional. + +add -- Add food or exercise record to the current list. + Add Food Item + Format: add f/ITEM {c/CALORIES} {d/DD-MM-YYYY} {t/HHMM} + Prefix Input + f/ Description of the food + c/ Calories of the food + d/ Date of food in DD-MM-YYYY + t/ Time of food in HHMM + + Add Exercise Item + Format: add f/ITEM {c/CALORIES} {d/DD-MM-YYYY} {t/HHMM} + Prefix Input + e/ Description of exercise + c/ Calories burnt from exercise + d/ Date of exercise in DD-MM-YYYY + + Add Recurring Exercise to Upcoming Exercise List + Format: add r/ITEM c/CALORIES :/START_DATE -/END_DATE + @/DAY_OF_THE_WEEK {,DAY_OF_THE_WEEK,...} + Format: delete e/LIST_NO d/DD-MM-YYYY + Prefix Input + r/ Description of upcoming exercise + c/ Calories burnt from exercise + :/ Start date of exercise in DD-MM-YYYY + -/ End date of exercise in DD-MM-YYYY + @/ Workout days of the week + + Add Food Item in Food Bank + Format: add fbank/ITEM c/CALORIES + Prefix Input + fbank/ Description of food + c/ Calories of the food + + Add Exercise Item in ExerciseBank + Format: add ebank/ITEM c/CALORIES + Prefix Input + ebank/ Description of exercise + c/ Calories burnt from exercise + +bmi -- Calculate the Body-Mass-Index of user + Format: bmi {h/HEIGHT_IN_CM w/WEIGHT_IN_KG} + Prefix Input + h/ Height of user in cm + w/ Weight of user in kg + If no prefixes are given, bmi will be calculated using the + height and weight in the profile. + +bye -- Exit the program. + Format: bye + +delete -- Delete entry of food or exercise added from a list. + Deleting Food Item + Format: delete f/LIST_NO d/DD-MM-YYYY t/HHMM + Prefix Input + f/ Index of food in Food List within the + given date + d/ Date of food in DD-MM-YYYY + t/ Time of food in HHMM + + Delete Exercise Item + Format: delete e/LIST_NO d/DD-MM-YYYY + Prefix Input + e/ Index of exercise in Exercise List within the given date + d/ Date of exercise in DD-MM-YYYY + + Delete Upcoming Exercise Items from Upcoming Exercise List + Format: delete u/LIST_NO {,LIST_NO,...} + Prefix Input + u/ The index of the item within the Upcoming Exercise List + + Delete Food Item from Food Bank + Format: delete fbank/LIST_NO {,LIST_NO,...} + Prefix Input + fbank/ The index of the item within the Food Bank + + Delete Exercise Item from Exercise Bank + Format: delete ebank/LIST_NO {,LIST_NO,...} + Prefix Input + ebank/ The index of the item within the Exercise Bank + +edit -- Edit entry of food or exercise added from a list. + Edit Food Bank + Format: edit fbank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES} + Prefix Input + fbank/ The index of the item within the Food Bank + n/ New description of food name + c/ Calories of food + + Edit Exercise Bank + Format: edit ebank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES} + Prefix Input + ebank/ The index of the item within the Exercise Bank + n/ New description of exercise name + c/ Calories burnt from exercise + + Edit Upcoming Exercise List + Format: edit u/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES} + Prefix Input + u/ The index of the item within the Upcoming Exercise List + n/ New description of exercise name + c/ Calories burnt from exercise + +help -- View help for commands + Format: help + +profile -- View or modify profile details + Format: profile {n/NAME} {h/HEIGHT(CM)} {w/WEIGHT(KG)} {a/AGE} + {g/CALORIEGOAL} {s/GENDER(M/F)} {x/ACTIVITYFACTOR(1-5)} + Prefix Input + n/ Name of user + h/ Height of user in cm + w/ Weight of user in kg + s/ Gender of user, m for male, f for female + a/ Age of user + g/ Net calorie goal of user. + Net calorie is based on calorie of food consumed + - calories burnt from exercise and bmr + x/ Activity factor of user, range 1 to 5 + If no prefixes are given, user will be shown the current + profile particulars. + +overview -- View weekly and daily summary of calories + Format: overview + +view -- View all the food and/or exercises added. + + Viewing Food List + Format: view f/ + + View Exercise List + Format: view e/ + + View Upcoming Exercise List + Format: view u/ + + View Food Bank + Format: view fbank/ + + View Exercise Bank + Format: view ebank/ +__________________________________________________________________________________________________________ +``` + +[⏫ Back to content page](#content-page) + +### **4.9. Exiting Program** `bye` + +You can use this command to exit _Fitbot_. + +Format: `bye` + +Example: +```text +bye +_________________________________________________________________________________________________________ +Exiting Fitbot.... +Bye! Hope to see you again soon!! +_________________________________________________________________________________________________________ +``` + +[⏫ Back to content page](#content-page) + +### **5. Data Management** + + +#### 5.1. Saving The Data + +There is no need to save manually. Any updates made to the data will be automatically stored into the local drive and reloaded when _Fitbot_ is restarted. + +#### 5.2 Editing The Data File + +_Fitbot_ data files are saved as `.txt` files `/data/.txt`. \ +Advanced users are welcomed to update data directly by editing the data files. + +❗ If your changes to the data files format are invalid, _Fitbot_ will skip the wrongly formatted line when it loads in the data. + +❗ Ensure that you quit _Fitbot_ first if you would like to edit the files. Edits made directly to files when _Fitbot_ is running will not be saved. + +[⏫ Back to content page](#content-page) + +### **6. Data Limits** + +You can see all the data range available in order to help you better understand the input and output. Note that _all the ranges stated here are inclusive of the boundaries._ + +- Items: + (Assuming today's date is 08 Nov 2021) + - For food and exercise dates: Past 7 days inclusive of today (eg. 02 Nov 2021 - 08 Nov 2021). + - For upcoming exercise dates: Tomorrow until 1 year later (eg. 09 Nov 2021 - 08 Nov 2021). + - Names for items: No usage of `|` or `/` inside the named items. Can substitute with `-` instead. + - Calories: Whole number from 1 to 10,000. +- Profile: + - Name: No usage of `|` or `/` inside the names. Can be substituted with `-` instead. + - Height: Number from 1 to 300. + - Weight: Number from 1 to 300. + - Age: Whole number from 10 to 150. + - Gender: Only 'M' or 'F' characters are accepted (case insensitive). Any additional characters will render the input invalid. + - Calorie Goal: Whole number from -3000 to 10000. + - Activity Factor: Whole number from 1 to 5. + +[⏫ Back to content page](#content-page) + +### **7. FAQ** + +**Q:** How do I transfer my data to another computer?\ +**A:** Zip the folder with _Fitbot_ and its data files, and transfer to the new computer. Extract the zipped folder onto your new computer and follow steps 1, 4 and 5 in [Quick Start](#2-quick-start) to get your _Fitbot_ running on your new computer. + +**Q:** How many profiles can I create?\ +**A:** _Fitbot_ only supports 1 profile. If you need to make any changes to your current profile, you can refer to [Updating Profile Attributes](#413-updating-profile-attributes). + +**Q:** How can I reset _Fitbot_ and its data?\ +**A:** You can reset _Fitbot_ by deleting the data folder in the same directory as the `Fitbot.jar` file location. If you choose to remove +only selected `.txt` files, data in the remaining files will still be loaded and viewed in _Fitbot_. + +**Q:** Why does _Fitbot_ need so many personal particulars?\ +**A:** _Fitbot_ needs your height, weight, age, gender and activity factor so that we can calculate your [BMR](https://en.wikipedia.org/wiki/Basal_metabolic_rate). +Calorie goal is required to help you check how close or how far you are away from your calorie goal. +_Fitbot_ needs to know your name to address you. + +[⏫ Back to content page](#content-page) + +### **8. Command Summary** + + +| Action | Format | Examples | +|--------|-------|----------| +add|`add f/ITEM {c/CALORIE} {d/DD-MM-YYYY} {t/HHMM}`

`add e/ITEM {c/CALORIES} {d/DD-MM-YYYY}`

`add r/ITEM c/CALORIES :/START_DATE -/END_DATE @/DAY_OF_THE_WEEK {,DAY_OF_THE_WEEK,...}`

`add fbank/ITEM c/CALORIES`

`add ebank/ITEM c/CALORIES`| `add f/chicken rice c/607 d/20-10-2021`

`add e/hiit c/290 d/23-10-2021`

`add r/handball training c/432 :/24-10-2021 -/11-11-2021 @/1,3`

`add fbank/chicken rice c/667`

`add ebank/5km run c/478` +bmi|`bmi h/HEIGHT_IN_CM w/WEIGHT_IN_KG`

`bmi`|`bmi h/170 w/65` ,

`bmi` +bye|`bye`| +delete|`delete f/LIST_NO d/DD-MM-YYYY t/HHMM`

`delete e/LIST_NO d/DD-MM-YYYY`

`delete u/LIST_NO {,LIST_NO,...}`

`delete fbank/LIST_NO {,LIST_NO,...}`

`delete ebank/LIST_NO {,LIST_NO,...}`

`delete f/all`

`delete e/all`

`delete u/all`

`delete fbank/all`

`delete ebank/all` | `delete f/1 d/04-11-2021 t/1000`

`delete e/1 d/05-11-2021`

`delete u/1,2`

`delete fbank/2,3`

`delete ebank/2,5`










+edit| `edit u/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES} {d/NEW_DATE}`

`edit fbank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}`

`edit ebank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}`| `edit u/1 n/volleyball training c/560 d/24-10-2021`

`edit fbank/1 n/2.4km run c/267`

`edit ebank/1 n/chicken rice c/800` +help | `help`| +overview|`overview`| +profile|`profile {h/HEIGHT_IN_CM} {w/WEIGHT_IN_KG} {n/NAME} {s/GENDER} {a/AGE} {g/CALORIE_IN_CAL} {x/ACTIVITY_FACTOR}`

`profile`|`profile h/170 w/65 n/John a/22 s/M g/500 x/2`

`profile` +view|`view e/`

`view f/`

`view u/`

`view fbank/`

`view ebank/` + +[⏫ Back to content page](#content-page) diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000000..baf96b195b --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,13 @@ +title: "Fitbot" +theme: minima + + +header_pages: + - UserGuide.md + - DeveloperGuide.md + - AboutUs.md + +plugins: + - jemoji + + diff --git a/docs/diagrams/AddFoodItemSequenceDiagram.puml b/docs/diagrams/AddFoodItemSequenceDiagram.puml new file mode 100644 index 0000000000..901da8ee65 --- /dev/null +++ b/docs/diagrams/AddFoodItemSequenceDiagram.puml @@ -0,0 +1,59 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include Style.puml +hide footbox + +participant "command:AddFoodCommand" As AddFoodCommand LOGIC_COLOUR +participant "foodBank:ItemBank" As FoodBank DATA_COLOUR + +?->AddFoodCommand :execute() +activate AddFoodCommand LOGIC_COLOUR + +opt calories == null + AddFoodCommand -> FoodBank : findCalorie(description) + activate FoodBank DATA_COLOUR + FoodBank --> AddFoodCommand : calories + deactivate FoodBank +end + +create "food:Food" As Food DATA_COLOUR +AddFoodCommand -> Food ** : new Food(description,calories,dateTime) +activate Food DATA_COLOUR +Food -> Food ++ DATA_COLOUR : setTimePeriod (dateTime) +Food --> Food -- +Food --> AddFoodCommand : food +deactivate Food + +AddFoodCommand -> Food : isValid() +activate Food DATA_COLOUR +participant ":CommandResult" As CommandResult1 LOGIC_COLOUR +Food --> AddFoodCommand :isValid +deactivate Food +alt !isValid + AddFoodCommand -> CommandResult1 **: new CommandResult(String.format(MESSAGE_INVALID_CALORIES, food)) + activate CommandResult1 LOGIC_COLOUR + CommandResult1 --> AddFoodCommand + deactivate CommandResult1 +else else + participant "filteredFoodList:FoodList" As FoodList DATA_COLOUR + participant ":CommandResult" As CommandResult LOGIC_COLOUR + AddFoodCommand -> FoodList : \t\t\t\t\t\t\t\t\t\taddItem(food) + activate FoodList DATA_COLOUR + FoodList -> FoodList ++ DATA_COLOUR: sortList() + FoodList --> FoodList -- + FoodList --> AddFoodCommand + deactivate FoodList + AddFoodCommand -> CommandResult **: \t\t\t\t\tnew CommandResult(String.format(MESSAGE_SUCCESS,food)) + activate CommandResult LOGIC_COLOUR + CommandResult --> AddFoodCommand + deactivate CommandResult +end + +?<-- AddFoodCommand : commandResult\t\t +deactivate AddFoodCommand +destroy Food +destroy CommandResult +destroy CommandResult1 +destroy AddFoodCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/AddFutureExerciseSequenceDaigram.puml b/docs/diagrams/AddFutureExerciseSequenceDaigram.puml new file mode 100644 index 0000000000..49ca6389c1 --- /dev/null +++ b/docs/diagrams/AddFutureExerciseSequenceDaigram.puml @@ -0,0 +1,44 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include Style.puml +hide footbox + +participant "command:AddFutureExerciseCommand" As AddFutureExerciseCommand LOGIC_COLOUR +participant "exerciseBank:ItemBank" As ItemBank DATA_COLOUR + +activate AddFutureExerciseCommand LOGIC_COLOUR +?->AddFutureExerciseCommand :execute() + +opt isCalorieFromBank + AddFutureExerciseCommand -> ItemBank : getCaloriesOfItemWithMatchingName(description) + activate ItemBank DATA_COLOUR + ItemBank --> AddFutureExerciseCommand : \t\t\t\tcalories + deactivate ItemBank +end +create "Exercise" As Exercise DATA_COLOUR + AddFutureExerciseCommand -> Exercise ** : \t\t\t\tnew Exercise(description,calories,date) + activate Exercise DATA_COLOUR + Exercise --> AddFutureExerciseCommand : \t\t\t\t\t\t\texercise + deactivate Exercise + + +participant "futureExerciseItems:FutureExerciseList" As FutureExerciseItems DATA_COLOUR +participant "commandResult:CommandResult" As CommandResult LOGIC_COLOUR +AddFutureExerciseCommand -> FutureExerciseItems : \t\t\t\t\t\t\t\t\t\taddItem(exercise) +activate FutureExerciseItems DATA_COLOUR +FutureExerciseItems -> FutureExerciseItems ++ DATA_COLOUR: sortList() +FutureExerciseItems --> FutureExerciseItems -- +FutureExerciseItems --> AddFutureExerciseCommand +deactivate FutureExerciseItems +AddFutureExerciseCommand -> CommandResult **: \t\t\t\t\t\t\tnew CommandResult(String.format(MESSAGE_SUCCESS,exercise)) +activate CommandResult LOGIC_COLOUR +CommandResult --> AddFutureExerciseCommand +deactivate CommandResult + +?<-- AddFutureExerciseCommand : commandResult +deactivate AddFutureExerciseCommand +destroy Exercise +destroy CommandResult +destroy AddFutureExerciseCommand +@enduml diff --git a/docs/diagrams/AddRecurringExerciseSequenceDiagram.puml b/docs/diagrams/AddRecurringExerciseSequenceDiagram.puml new file mode 100644 index 0000000000..bc541123d8 --- /dev/null +++ b/docs/diagrams/AddRecurringExerciseSequenceDiagram.puml @@ -0,0 +1,51 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include Style.puml +hide footbox + +participant "command:AddRecurringExerciseCommand" As AddRecurringExerciseCommand LOGIC_COLOUR +participant "exerciseBank:ItemBank" As ItemBank DATA_COLOUR + +activate AddRecurringExerciseCommand LOGIC_COLOUR +?->AddRecurringExerciseCommand :execute() + +opt calories == null + AddRecurringExerciseCommand -> ItemBank : findCalorie(description) + activate ItemBank DATA_COLOUR + ItemBank --> AddRecurringExerciseCommand : calories + deactivate ItemBank + +end + +participant "futureExerciseItems:FutureExerciseList" As FutureExerciseItems DATA_COLOUR +AddRecurringExerciseCommand -> FutureExerciseItems : \taddRecurringExercises(exercise, calories, startDate, endDate, dayOfTheWeek) +activate FutureExerciseItems DATA_COLOUR +loop matchesAllDayAndDate +create ":Exercise" As Exercise DATA_COLOUR + FutureExerciseItems -> Exercise ** : \tnew Exercise(description,calories,date) + activate Exercise DATA_COLOUR + Exercise --> FutureExerciseItems : exercise + deactivate Exercise +FutureExerciseItems -> FutureExerciseItems ++ DATA_COLOUR: addItem(exercise) +FutureExerciseItems --> FutureExerciseItems -- +end +FutureExerciseItems -> FutureExerciseItems ++ DATA_COLOUR: sortList() +FutureExerciseItems --> FutureExerciseItems -- +FutureExerciseItems --> AddRecurringExerciseCommand +deactivate FutureExerciseItems + +participant "commandResult:CommandResult" As CommandResult LOGIC_COLOUR +AddRecurringExerciseCommand -> CommandResult **: \t\t\t\t\t\t\tnew CommandResult(MESSAGE_SUCCESS) +activate CommandResult LOGIC_COLOUR +CommandResult --> AddRecurringExerciseCommand +deactivate CommandResult + + + +?<-- AddRecurringExerciseCommand : commandResult +deactivate AddRecurringExerciseCommand +destroy Exercise +destroy CommandResult +destroy AddRecurringExerciseCommand +@enduml diff --git a/docs/diagrams/Architecture.puml b/docs/diagrams/Architecture.puml new file mode 100644 index 0000000000..44477be0b6 --- /dev/null +++ b/docs/diagrams/Architecture.puml @@ -0,0 +1,28 @@ +@startuml +!include style.puml +hide footbox + +participant ":Ui" as ui UI_COLOUR +participant ":Main" as main MAIN_COLOUR +main -> ui ++ UI_COLOUR: getUserInput() +main++ MAIN_COLOUR +-> ui UI_COLOUR : addFoodItem +ui --> main -- +'create ":Parser" as parser LOGIC_COLOUR +main -> parser ++ LOGIC_COLOUR : parseAddFoodItem() +participant ":Data" as itemlist DATA_COLOUR +parser -> itemlist ++ DATA_COLOUR : addFoodItem() +itemlist --> parser -- : +parser --> main -- +participant ":Storage" as storage STORAGE_COLOUR +main -> storage ++ STORAGE_COLOUR: saveFoodList() +'destroy parser +storage -> storage ++ STORAGE_COLOUR: save to file +storage --> storage -- +storage --> main -- +main --> ui ++ UI_COLOUR: formatMessageFramedWithDivider() success +<- ui : show add food item\n success message +ui --> main -- +main -- + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ChecksForFileStorage.puml b/docs/diagrams/ChecksForFileStorage.puml new file mode 100644 index 0000000000..dfbded2da4 --- /dev/null +++ b/docs/diagrams/ChecksForFileStorage.puml @@ -0,0 +1,33 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Checks for file and create directory if missing +participant "profileStorage:ProfileStorage" As ProfileStorage STORAGE_COLOUR +participant ":FileChecker" As FileChecker STORAGE_COLOUR +participant "file:File" As File STORAGE_COLOUR + + +activate ProfileStorage STORAGE_COLOUR +ProfileStorage -> FileChecker: createFileIfMissing(filePath) +activate FileChecker STORAGE_COLOUR + FileChecker -> File **: new File(filePath) + activate File STORAGE_COLOUR + File --> FileChecker: file + deactivate + FileChecker -> FileChecker ++ STORAGE_COLOUR: createDirectory() + FileChecker --> FileChecker -- + opt !file.exists() + FileChecker -> File : createNewFile() + activate File STORAGE_COLOUR + File --> FileChecker + deactivate + end + destroy File + FileChecker --> ProfileStorage +deactivate + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/CreateProfile.puml b/docs/diagrams/CreateProfile.puml new file mode 100644 index 0000000000..1a07fa87cf --- /dev/null +++ b/docs/diagrams/CreateProfile.puml @@ -0,0 +1,32 @@ +@startuml +'https://plantuml.com/sequence-diagram +hide footbox +!include Style.puml +participant ":StartState" As StartState STATE_COLOUR +-> StartState ++ STATE_COLOUR: checkAndCreateProfile() +participant "oldProfile:Profile" as oldProfile DATA_COLOUR +StartState -> oldProfile ++ DATA_COLOUR :checkProfileComplete() +oldProfile --> StartState -- +alt profile.checkProfileComplete() +else +StartState -> StartState ++ STATE_COLOUR: createProfile() +participant "newProfile:Profile" as newProfile DATA_COLOUR +create newProfile +StartState ->newProfile ++ DATA_COLOUR : new Profile() +loop !profile.checkProfileComplete +ref over StartState,newProfile +create Profile attributes +end +end +newProfile --> StartState -- +participant ":StorageManager" as StoreManager STORAGE_COLOUR +StartState -> StoreManager ++ STORAGE_COLOUR: saveProfile(profile) +StoreManager --> StartState -- +StartState --> StartState -- MAIN_COLOUR : replace profile +end +<-- StartState -- : profile +destroy oldProfile + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/DataComponent.puml b/docs/diagrams/DataComponent.puml new file mode 100644 index 0000000000..a085100628 --- /dev/null +++ b/docs/diagrams/DataComponent.puml @@ -0,0 +1,38 @@ +@startuml +'https://plantuml.com/class-diagram +!include Style.puml +hide interface methods + +package Data { + + class DataManager DATA_COLOUR { + - profile: Profile + - foodItems: FoodList + - filteredFoodItems: FoodList + - exerciseItems: ExerciseList + - filteredExerciseItems: ExerciseList + - foodBank: ItemBank + - exerciseBank: ItemBank + } + + class "<>\nVerifiable" As Verifiable DATA_COLOUR { + isValid(): boolean + } + + package Profile { + + } + + package Item { + + } + +} + +Verifiable <|.. Item +Verifiable <|.. Profile +DataManager --> Profile +DataManager --> Item + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeActivityFactor.puml b/docs/diagrams/DecodeActivityFactor.puml new file mode 100644 index 0000000000..e730e301e2 --- /dev/null +++ b/docs/diagrams/DecodeActivityFactor.puml @@ -0,0 +1,33 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode ActivityFactor +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":ActivityFactor" As ActivityFactor DATA_COLOUR + +activate ProfileDecoder STORAGE_COLOUR +alt no errors thrown +ProfileDecoder -> ActivityFactor ** DATA_COLOUR: new ActivityFactor(Integer.parse(detail)) +activate ActivityFactor DATA_COLOUR + ActivityFactor --> ProfileDecoder +deactivate +destroy ActivityFactor +else NumberFormatException +ProfileDecoder -> ActivityFactor ** DATA_COLOUR: new ActivityFactor(Integer.MIN_VALUE) +note right STORAGE_FADED_COLOUR + The (Integer.MIN_VALUE) here represents an invalid + integer of ActivityFactor that is handled later on in + the startup of the bot. +end note +activate ActivityFactor DATA_COLOUR + ActivityFactor --> ProfileDecoder +deactivate +destroy ActivityFactor + +end + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeAge.puml b/docs/diagrams/DecodeAge.puml new file mode 100644 index 0000000000..da4d9bb5c3 --- /dev/null +++ b/docs/diagrams/DecodeAge.puml @@ -0,0 +1,35 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode Age +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":Age" As Age DATA_COLOUR +participant ":Age" As Age DATA_COLOUR + +activate ProfileDecoder STORAGE_COLOUR +alt no errors thrown +ProfileDecoder -> Age ** DATA_COLOUR: new Age(Integer.parse(detail)) +activate Age DATA_COLOUR + Age --> ProfileDecoder +deactivate +destroy Age +else NumberFormatException +ProfileDecoder -> Age ** DATA_COLOUR: new Age(Integer.MIN_VALUE) +note right STORAGE_FADED_COLOUR +The (Integer.MIN_VALUE) here represents an +invalid integer of Age that is handledlater +on in the startup of the bot. + +end note +activate Age DATA_COLOUR + Age --> ProfileDecoder +deactivate +destroy Age + +end + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeAttributes.puml b/docs/diagrams/DecodeAttributes.puml new file mode 100644 index 0000000000..e96fbc479a --- /dev/null +++ b/docs/diagrams/DecodeAttributes.puml @@ -0,0 +1,41 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode all the attributes +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR + +ref over ProfileDecoder +Decode Name +end ref + +ref over ProfileDecoder +Decode Height +end ref + +ref over ProfileDecoder +Decode Weight +end ref + +ref over ProfileDecoder +Decode Gender +end ref + +ref over ProfileDecoder +Decode Age +end ref + +ref over ProfileDecoder +Decode CalorieGoal +end ref + +ref over ProfileDecoder +Decode ActivityFactor +end ref + +activate ProfileDecoder STORAGE_COLOUR + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeCalorieGoal.puml b/docs/diagrams/DecodeCalorieGoal.puml new file mode 100644 index 0000000000..a16686d0e5 --- /dev/null +++ b/docs/diagrams/DecodeCalorieGoal.puml @@ -0,0 +1,33 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode CalorieGoal +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":CalorieGoal" As CalorieGoal DATA_COLOUR + +activate ProfileDecoder STORAGE_COLOUR +alt no errors thrown +ProfileDecoder -> CalorieGoal ** DATA_COLOUR: new CalorieGoal(Integer.parse(detail)) +activate CalorieGoal DATA_COLOUR + CalorieGoal --> ProfileDecoder +deactivate +destroy CalorieGoal +else NumberFormatException +ProfileDecoder -> CalorieGoal ** DATA_COLOUR: new CalorieGoal(Integer.MIN_VALUE) +note right STORAGE_FADED_COLOUR + The (Integer.MIN_VALUE) here represents an invalid integer + of CalorieGoal that is handled later on in the startup of + the bot. +end note +activate CalorieGoal DATA_COLOUR + CalorieGoal --> ProfileDecoder +deactivate +destroy CalorieGoal + +end + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeGender.puml b/docs/diagrams/DecodeGender.puml new file mode 100644 index 0000000000..667d0e7097 --- /dev/null +++ b/docs/diagrams/DecodeGender.puml @@ -0,0 +1,27 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode Gender +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":Gender" As Gender DATA_COLOUR +activate ProfileDecoder STORAGE_COLOUR +alt detail.length() > 1 +ProfileDecoder -> Gender ** DATA_COLOUR: new Gender('X') +activate Gender DATA_COLOUR + Gender --> ProfileDecoder +deactivate +else else +destroy Gender +ProfileDecoder -> Gender ** DATA_COLOUR: new Gender(detail) +activate Gender DATA_COLOUR + Gender --> ProfileDecoder +deactivate +destroy Gender +end +note right STORAGE_FADED_COLOUR: The 'X' here represents an \ninvalid gender that is handled\nlater on in the startup of the \nbot. It only accept single\ncharacters. + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeHeight.puml b/docs/diagrams/DecodeHeight.puml new file mode 100644 index 0000000000..a2e0dc12e7 --- /dev/null +++ b/docs/diagrams/DecodeHeight.puml @@ -0,0 +1,32 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode Height +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":Height" As Height DATA_COLOUR + +activate ProfileDecoder STORAGE_COLOUR +alt no errors thrown +ProfileDecoder -> Height ** DATA_COLOUR: new Height(Double.parse(detail)) +activate Height DATA_COLOUR + Height --> ProfileDecoder +deactivate +destroy Height +else NumberFormatException +ProfileDecoder -> Height ** DATA_COLOUR: new Height(Double.MIN_VALUE) +note right STORAGE_FADED_COLOUR +The (Double.MIN_VALUE) here represents an invalid double +of Height that is handled later on in the startup of the bot. +end note +activate Height DATA_COLOUR + Height --> ProfileDecoder +deactivate +destroy Height + +end + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeName.puml b/docs/diagrams/DecodeName.puml new file mode 100644 index 0000000000..6f15293b51 --- /dev/null +++ b/docs/diagrams/DecodeName.puml @@ -0,0 +1,18 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode Name +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":Name" As Name DATA_COLOUR + +activate ProfileDecoder STORAGE_COLOUR +ProfileDecoder -> Name ** DATA_COLOUR: new Name(detail) +activate Name DATA_COLOUR + Name --> ProfileDecoder +deactivate +destroy Name +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/DecodeWeight.puml b/docs/diagrams/DecodeWeight.puml new file mode 100644 index 0000000000..b5c48f98d6 --- /dev/null +++ b/docs/diagrams/DecodeWeight.puml @@ -0,0 +1,32 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Decode Weight +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR +participant ":Weight" As Weight DATA_COLOUR + +activate ProfileDecoder STORAGE_COLOUR +alt no errors thrown +ProfileDecoder -> Weight ** DATA_COLOUR: new Weight(Double.parse(detail)) +activate Weight DATA_COLOUR + Weight --> ProfileDecoder +deactivate +destroy Weight +else NumberFormatException +ProfileDecoder -> Weight ** DATA_COLOUR: new Weight(Double.MIN_VALUE) +note right STORAGE_FADED_COLOUR +The (Double.MIN_VALUE) here represents an invalid double +of Weight that is handled later on in the startup of the bot. +end note +activate Weight DATA_COLOUR + Weight --> ProfileDecoder +deactivate +destroy Weight + +end + +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/ItemBankAndItemClassDiagram.puml b/docs/diagrams/ItemBankAndItemClassDiagram.puml new file mode 100644 index 0000000000..306b567623 --- /dev/null +++ b/docs/diagrams/ItemBankAndItemClassDiagram.puml @@ -0,0 +1,53 @@ +@startuml +'https://plantuml.com/class-diagram +!include Style.puml +hide circle +skinparam classAttributeIconSize 0 + +class DataManager DATA_COLOUR +class "<>\nVerifiable" As Verifiable DATA_COLOUR + + +package "Item" As Item1 { +class ItemBank DATA_COLOUR +class "{abstract}\nItemList" as ItemList DATA_COLOUR +class FoodList DATA_COLOUR +class ExerciseList DATA_COLOUR +class FutureExerciseList DATA_COLOUR +DataManager -left-> ItemBank +class "{abstract}\nItem" as Item DATA_COLOUR{ +#name: String +#calories: int +} + +class Food DATA_COLOUR { +#dateTime: LocalDateTime +} + +class Exercise DATA_COLOUR { +#date: LocalDate +} + +enum "<>\nTimePeriod" as TimePeriod DATA_COLOUR { +MORNING, AFTERNOON, +EVENING, NIGHT +} + +hide TimePeriod method + +ItemBank <|-- ItemList +ItemList <|-- FoodList +ItemList <|-- ExerciseList +ExerciseList <|-- FutureExerciseList + +Verifiable <|.left. Item : "\t\t" +Item <|-left- Food +Item <|-- Exercise +Item "*" <-- ItemBank: "internalItems\n\n\n" +TimePeriod "1 " <-up- Food : "\n\n\n timePeriod" + +Food <.. FoodList +Exercise <.. ExerciseList +} + +@enduml \ No newline at end of file diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml new file mode 100644 index 0000000000..51b6702a50 --- /dev/null +++ b/docs/diagrams/LogicClassDiagram.puml @@ -0,0 +1,38 @@ +@startuml +'https://plantuml.com/class-diagram +!include Style.puml +hide circle +hide empty members +skinparam minclassWidth 120 +skinparam Padding 5 + +class StorageManager STORAGE_COLOUR +class DataManager DATA_COLOUR +class LogicManager LOGIC_COLOUR +class ParserManager LOGIC_COLOUR +class CommandResult LOGIC_COLOUR +class XYZCommand LOGIC_COLOUR +abstract class "{abstract}\nCommand" AS Command LOGIC_COLOUR + + +LogicManager --> "1" ParserManager +LogicManager --> "1" StorageManager +LogicManager -> "1" DataManager + +ParserManager ..> XYZCommand : > creates +XYZCommand --|> Command +Command .right.> CommandResult : > produces + + +class Main MAIN_COLOUR + +Main --> "1" LogicManager : > instantiates and \n feeds user input +Command --> "1" DataManager : > modifies +ParserManager ..> Command : > returns +StorageManager ..> DataManager : > saves +LogicManager ..> Command : > executes +Main ..> CommandResult : > retrieves + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/LogicSequenceDiagram.puml b/docs/diagrams/LogicSequenceDiagram.puml new file mode 100644 index 0000000000..4279cd798e --- /dev/null +++ b/docs/diagrams/LogicSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include Style.puml +hide footbox +autoactivate on + +participant ":Main" AS Main MAIN_COLOUR +participant ":LogicManager" AS LogicManager LOGIC_COLOUR +participant ":ParserManager" AS ParserManager LOGIC_COLOUR +participant ":AddCommandParser" AS AddCommandParser LOGIC_COLOUR +participant "c:AddFoodCommand" AS AddFoodCommand LOGIC_COLOUR +participant "r:CommandResult" AS CommandResult LOGIC_COLOUR +activate Main MAIN_COLOUR + + +create LogicManager +Main -> LogicManager LOGIC_COLOUR : new LogicManager(storageManager, dataManager) + +create ParserManager +LogicManager-> ParserManager LOGIC_COLOUR +return +return + + +Main -> LogicManager LOGIC_COLOUR : execute("add f/potato c/30") + + + +LogicManager -> ParserManager LOGIC_COLOUR : parseCommand ("add f/potato c/30") +ref over ParserManager + determine type + of command +end ref + +create AddCommandParser +ParserManager -> AddCommandParser LOGIC_COLOUR +return +ParserManager -> AddCommandParser LOGIC_COLOUR : parse("f/...") + +ref over AddCommandParser + parse required + parameters + for the command +end ref + + +create AddFoodCommand +AddCommandParser -> AddFoodCommand LOGIC_COLOUR : new AddFoodCommand("potato", ...) +return c +return c +return c +destroy AddCommandParser + +LogicManager -> AddFoodCommand LOGIC_COLOUR : setData(dataManager) +destroy ParserManager +return +LogicManager -> AddFoodCommand LOGIC_COLOUR : execute() +note over AddFoodCommand #WHITE : The data manipulation process \n is not shown here. + + +create CommandResult +AddFoodCommand -> CommandResult LOGIC_COLOUR + +return r +return r +destroy CommandResult +return r +destroy AddFoodCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/MainClass.puml b/docs/diagrams/MainClass.puml new file mode 100644 index 0000000000..5a55f58890 --- /dev/null +++ b/docs/diagrams/MainClass.puml @@ -0,0 +1,29 @@ +@startuml +!include Style.puml +'https://plantuml.com/class-diagram +hide circle + +class " Ui " as Ui +class " State " as State STATE_COLOUR + +class DataManager DATA_COLOUR +DataManager "1" <-- Main: stores < +LogicManager "1" <-- Main : parse < +Ui "1" <-- Main : formats output < +StorageManager "1" <-- Main : retrieves < +State "1" <-- Main : checks < + + +class Main MAIN_COLOUR { +start() +checkAndCreateProfile() +enterTaskModeUntilByeCommand() +exit() +} + +class LogicManager LOGIC_COLOUR + +class StorageManager STORAGE_COLOUR + +class Ui UI_COLOUR +@enduml \ No newline at end of file diff --git a/docs/diagrams/OverviewCommand.puml b/docs/diagrams/OverviewCommand.puml new file mode 100644 index 0000000000..030badf377 --- /dev/null +++ b/docs/diagrams/OverviewCommand.puml @@ -0,0 +1,39 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include Style.puml + +hide footbox +participant Data DATA_COLOUR +participant Ui UI_COLOUR +-> Ui ++ UI_COLOUR: overview +participant Main MAIN_COLOUR +Ui -> Main ++ MAIN_COLOUR: getInput() +participant Parser LOGIC_COLOUR +create Parser LOGIC_COLOUR +Main -> Parser ++ LOGIC_COLOUR : parseInput() +participant OverviewCommand LOGIC_COLOUR +create OverviewCommand LOGIC_COLOUR +Parser -> OverviewCommand ++ LOGIC_COLOUR +OverviewCommand --> Parser -- LOGIC_COLOUR +Parser --> Main -- LOGIC_COLOUR +Main -> OverviewCommand ++ LOGIC_COLOUR: execute() +participant Statistics UI_COLOUR +create Statistics +OverviewCommand -> Statistics ++ UI_COLOUR +destroy Parser +Statistics -> Data ++ DATA_COLOUR +Data --> Statistics -- : data +Statistics --> OverviewCommand -- +participant CommandResult LOGIC_COLOUR +create CommandResult +OverviewCommand -> CommandResult ++ LOGIC_COLOUR +CommandResult -> OverviewCommand -- +OverviewCommand --> Main -- +destroy Statistics +Main -> CommandResult ++ LOGIC_COLOUR +CommandResult -> Main -- : overviewMessage +Main -> Ui -- : formatMessage() +destroy OverviewCommand +<--Ui -- +destroy CommandResult +@enduml \ No newline at end of file diff --git a/docs/diagrams/ParseCommandRefFrame.puml b/docs/diagrams/ParseCommandRefFrame.puml new file mode 100644 index 0000000000..7b6c1e6309 --- /dev/null +++ b/docs/diagrams/ParseCommandRefFrame.puml @@ -0,0 +1,39 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include Style.puml +mainframe sd determine type of command + + +participant ":ParserManager" AS ParserManager LOGIC_COLOUR +participant ":AddCommandParser" AS AddCommandParser LOGIC_COLOUR +participant ":XYZCommandParser" AS XYZCommandParser LOGIC_COLOUR +-> ParserManager : parseCommand("add f/potato c/30") +participant ":InvalidCommand" AS InvalidCommand LOGIC_COLOUR + +activate ParserManager LOGIC_COLOUR + +ParserManager -> ParserManager : splitInputIntoCommandAndParams(input) +activate ParserManager LOGIC_COLOUR +return ["add", "f/potato c/30"] +alt command word is "add" + create AddCommandParser + ParserManager -> AddCommandParser LOGIC_COLOUR : new AddCommandParser() + activate AddCommandParser LOGIC_COLOUR + return + +else command word is "xyz" + create XYZCommandParser + ParserManager -> XYZCommandParser LOGIC_COLOUR : new XYZCommandParser() + activate XYZCommandParser LOGIC_COLOUR + return + + note left #WHITE: XYZ = Delete, View, \nEdit, etc. +else command word is not known + create InvalidCommand + ParserManager -> InvalidCommand LOGIC_COLOUR : new InvalidCommand(ParserMessages.MESSAGE_ERROR_COMMAND_DOES_NOT_EXIST) + activate InvalidCommand LOGIC_COLOUR + return +end + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ParseParametersRefFrame.puml b/docs/diagrams/ParseParametersRefFrame.puml new file mode 100644 index 0000000000..5069a2cea5 --- /dev/null +++ b/docs/diagrams/ParseParametersRefFrame.puml @@ -0,0 +1,55 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include Style.puml +mainframe sd parse required parameters for the command + + +participant ":AddCommandParser" AS AddCommandParser LOGIC_COLOUR +participant "<> \n ParserUtils" AS ParserUtils LOGIC_COLOUR +participant "a:InvalidCommand" AS InvalidCommand1 LOGIC_COLOUR +participant ":AddFoodCommand" AS AddFoodCommand LOGIC_COLOUR +participant "b:InvalidCommand" AS InvalidCommand2 LOGIC_COLOUR +participant "c:InvalidCommand" AS InvalidCommand3 LOGIC_COLOUR +-> AddCommandParser : parse("f/potato c/30") +activate AddCommandParser LOGIC_COLOUR +AddCommandParser -> ParserUtils : extractItemTypePrefix("f/potato c/30") +activate ParserUtils LOGIC_COLOUR +return "f" +alt item type is food + AddCommandParser -> AddCommandParser : parseAddToFood("f/potato c/30", "f") + activate AddCommandParser LOGIC_COLOUR + AddCommandParser -> ParserUtils ++ LOGIC_COLOUR : hasExtraDelimiters("f/potato c/30", AddFoodCommand.EXPECTED_PREFIXES) + return + alt hasExtraDelimiters + create InvalidCommand1 + AddCommandParser -> InvalidCommand1 : new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS) + activate InvalidCommand1 LOGIC_COLOUR + return command + else else + AddCommandParser -> ParserUtils ++ LOGIC_COLOUR : extractItemDescription("f/potato c/30", "f") + return "potato" + AddCommandParser -> ParserUtils ++ LOGIC_COLOUR : extractItemCalories("f/potato c/30") + return "30" + AddCommandParser -> ParserUtils ++ LOGIC_COLOUR : extractDateTime("f/potato c/30") + return LocalDate.now() + alt all parameters are valid + create AddFoodCommand + AddCommandParser -> AddFoodCommand ++ LOGIC_COLOUR : new AddFoodCommand("potato", 30, LocalDate.now()) + return command + else else + create InvalidCommand2 + AddCommandParser -> InvalidCommand2 ++ LOGIC_COLOUR : new InvalidCommand(ParserException.getMessage()) + return command + end +end +else item type is X + note over AddCommandParser #WHITE: The process for extracting parameters \n differ for each X, \n where X = exercise, recurring exercise, \n exercise bank, food bank. However, the process is similar, \n hence they have been omitted for simplicity. +else item type is not known + create InvalidCommand3 + AddCommandParser -> InvalidCommand3 ++ LOGIC_COLOUR : new InvalidCommand(ParserMessages.MESSAGE_ADD_COMMAND_INVALID_FORMAT) + return command + +end +return command +return command +@enduml \ No newline at end of file diff --git a/docs/diagrams/ParserClassDiagram.puml b/docs/diagrams/ParserClassDiagram.puml new file mode 100644 index 0000000000..68b00f8738 --- /dev/null +++ b/docs/diagrams/ParserClassDiagram.puml @@ -0,0 +1,51 @@ +@startuml +'https://plantuml.com/class-diagram +!include Style.puml +hide empty members +skinparam minclassWidth 120 +skinparam Padding 2 + +class ParserMessages LOGIC_COLOUR +class ParserManager LOGIC_COLOUR +class ParserUtils LOGIC_COLOUR +class XYZCommandParser LOGIC_COLOUR +class XYZCommand LOGIC_COLOUR +abstract class "{abstract}\nCommand" AS Command LOGIC_COLOUR +class Parser <> LOGIC_COLOUR + +ParserManager ..> ParserMessages : > uses +ParserManager .down.> XYZCommandParser : > creates +XYZCommandParser ..right|> Parser +XYZCommandParser .up.> ParserMessages : > uses + + +note as N2 #white +XYZCommand = +AddCommand, +DeleteCommand, +ViewCommand, etc. +end note + +XYZCommandParser .. N2 #transparent +N2 .. XYZCommand #transparent + + +XYZCommandParser .left.> ParserUtils : > uses +XYZCommandParser ...> XYZCommand : > creates and returns +XYZCommand -right[hidden]- XYZCommandParser +ParserManager -down[hidden]-XYZCommandParser + +ParserManager ..> Command :> returns +XYZCommand -|> Command + +note as N1 #white +ParserMessages and ParserUtils +are purely utility classes +and contain static +methods/constants only. +end note + +ParserMessages.. N1 #transparent +N1 .. ParserUtils #transparent + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ProfileClassDiagram.puml b/docs/diagrams/ProfileClassDiagram.puml new file mode 100644 index 0000000000..4d01087385 --- /dev/null +++ b/docs/diagrams/ProfileClassDiagram.puml @@ -0,0 +1,53 @@ +@startuml +'https://plantuml.com/class-diagram +!include Style.puml +hide class fields +hide class methods +hide interface fields +hide interface methods + + +package Profile { + class Profile DATA_COLOUR + package Attributes { + class Name DATA_COLOUR + class Height DATA_COLOUR + class Weight DATA_COLOUR + class Age DATA_COLOUR + class Gender DATA_COLOUR + class CalorieGoal DATA_COLOUR + class ActivityFactor DATA_COLOUR + } + package Utilities { + class ProfileUtils DATA_COLOUR + class "<>\nActivityLevel" DATA_COLOUR + } +} + + class DataManager DATA_COLOUR + class "<>\nVerifiable" DATA_COLOUR + + +'Attributes ..|> "<>\nVerifiable" + +Profile "1" <-right- DataManager : profile +Name "1" --* Profile : name +Profile *-- "1" Height : height +Profile *-- "1" Weight : weight +Profile *-- "1" Age : age +Profile *-- "1" Gender : gender +Profile *-- "1" CalorieGoal : calorieGoal +Profile *-- "1" ActivityFactor : activityFactor +ActivityFactor ..> "<>\nActivityLevel" :> retrieves +ProfileUtils ..> "<>\nActivityLevel" :> calculates with +Profile <.up. ProfileUtils :< manipulates + +"<>\nVerifiable" <|.up. Name +"<>\nVerifiable" <|.up. Height +"<>\nVerifiable" <|.up. Weight +"<>\nVerifiable" <|.up. Age +"<>\nVerifiable" <|.up. Gender +"<>\nVerifiable" <|.up. CalorieGoal +"<>\nVerifiable" <|.up. ActivityFactor + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ProfileStorageClassDiagram.puml b/docs/diagrams/ProfileStorageClassDiagram.puml new file mode 100644 index 0000000000..c0b559cc9d --- /dev/null +++ b/docs/diagrams/ProfileStorageClassDiagram.puml @@ -0,0 +1,26 @@ +@startuml +'https://plantuml.com/class-diagram + +!include Style.puml +hide class fields +hide class methods +hide interface fields +hide interface methods + +class ProfileDecoder STORAGE_COLOUR +class ProfileStorage STORAGE_COLOUR +class "{abstract}\nStorage" STORAGE_COLOUR +class ProfileEncoder STORAGE_COLOUR +class FileChecker STORAGE_COLOUR +class FileSaver STORAGE_COLOUR +interface "<>\nProfileStorageInterface" STORAGE_COLOUR + +ProfileStorage <.up.. " " +"{abstract}\nStorage" <|-down- ProfileStorage +"<>\nProfileStorageInterface" <|.left. ProfileStorage +ProfileDecoder <.up. ProfileStorage :< decodes +ProfileEncoder <.up. ProfileStorage :< encodes +ProfileStorage .down.> FileChecker :> checks for file +ProfileStorage .left.> FileSaver :> saves + +@enduml \ No newline at end of file diff --git a/docs/diagrams/RetrieveDataFromStorage.puml b/docs/diagrams/RetrieveDataFromStorage.puml new file mode 100644 index 0000000000..79ec4e1843 --- /dev/null +++ b/docs/diagrams/RetrieveDataFromStorage.puml @@ -0,0 +1,47 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox + +group sd Retrieval of data from storage with the use of ProfileDecoder to decode +participant "profileStorage:ProfileStorage" As ProfileStorage STORAGE_COLOUR +participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR + +participant "file:File" As File STORAGE_COLOUR +participant "in:Scanner" As Scanner STORAGE_COLOUR +participant "profile:Profile" As Profile DATA_COLOUR + +activate ProfileStorage STORAGE_COLOUR +ProfileStorage -> ProfileDecoder: retrieveProfileFromData(filePath) +activate ProfileDecoder STORAGE_COLOUR + ProfileDecoder -> File **: new File(filePath) + activate File STORAGE_COLOUR + File --> ProfileDecoder: file + deactivate + ProfileDecoder -> Scanner ** : new Scanner(file) + activate Scanner STORAGE_COLOUR + Scanner --> ProfileDecoder: + deactivate + ProfileDecoder -> Scanner : nextLine() + activate Scanner STORAGE_COLOUR + Scanner --> ProfileDecoder: line + deactivate + ProfileDecoder -> ProfileDecoder ++ STORAGE_COLOUR: decodeProfile(line) + + ref over ProfileDecoder + Decode all the attributes + end ref + ProfileDecoder -> Profile ** : new Profile(name, height, ...) + activate Profile DATA_COLOUR + Profile --> ProfileDecoder : + deactivate + ProfileDecoder --> ProfileDecoder -- : profile + destroy Profile + + destroy Scanner + destroy File + ProfileDecoder --> ProfileStorage : profile +deactivate +end +@enduml \ No newline at end of file diff --git a/docs/diagrams/StartState.puml b/docs/diagrams/StartState.puml new file mode 100644 index 0000000000..312bfeee65 --- /dev/null +++ b/docs/diagrams/StartState.puml @@ -0,0 +1,37 @@ +@startuml + +!include Style.puml +hide circle +hide empty members +skinparam minclassWidth 120 +skinparam Padding 5 + +class AttributeCreator STATE_COLOUR +class NameCreator STATE_COLOUR +class HeightCreator STATE_COLOUR +class WeightCreator STATE_COLOUR +class GenderCreator STATE_COLOUR +class AgeCreator STATE_COLOUR +class CalorieGoalCreator STATE_COLOUR +class ActivityFactorCreator STATE_COLOUR +class StartState STATE_COLOUR + + +StartState ..> NameCreator : > executes +StartState ..> HeightCreator : > executes +StartState ..> WeightCreator : > executes +StartState ..> GenderCreator : > executes +StartState ..> AgeCreator : > executes +StartState ..> CalorieGoalCreator : > executes +StartState ..> ActivityFactorCreator : > executes +NameCreator --|> AttributeCreator +HeightCreator --|> AttributeCreator +WeightCreator --|> AttributeCreator +GenderCreator --|> AttributeCreator +AgeCreator --|> AttributeCreator +CalorieGoalCreator --|> AttributeCreator +ActivityFactorCreator --|> AttributeCreator + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml new file mode 100644 index 0000000000..a4cca617e8 --- /dev/null +++ b/docs/diagrams/StorageClassDiagram.puml @@ -0,0 +1,48 @@ +@startuml +'https://plantuml.com/class-diagram + +!include Style.puml +hide class fields +hide class methods +hide interface fields +hide interface methods + +package Storage { +class XYZDecoder STORAGE_COLOUR +class XYZEncoder STORAGE_COLOUR + +class FileChecker STORAGE_COLOUR +class FileSaver STORAGE_COLOUR +class StorageManager STORAGE_COLOUR +class XYZStorageUtils STORAGE_COLOUR + +class "<>\nXYZStorage" STORAGE_COLOUR + +class "<>\nStorage" STORAGE_COLOUR + +} + +package Data { +} + +package Logic { +} + +Data <.. "StorageManager" +Logic --> "StorageManager" +Logic -left-> Data + +"<>\nXYZStorage" <|-left- "<>\nStorage" + +StorageManager .left.|> "<>\nStorage" + +"<>\nXYZStorage" <|.. XYZStorageUtils + +StorageManager --> "1" "<>\nXYZStorage" + +XYZDecoder <.up. XYZStorageUtils :< decodes +XYZEncoder <.up. XYZStorageUtils :< encodes +XYZStorageUtils .right.> FileChecker :> checks for file +XYZStorageUtils .left.> FileSaver :> saves + +@enduml \ No newline at end of file diff --git a/docs/diagrams/StorageManagerClassDiagram.puml b/docs/diagrams/StorageManagerClassDiagram.puml new file mode 100644 index 0000000000..002ef885ea --- /dev/null +++ b/docs/diagrams/StorageManagerClassDiagram.puml @@ -0,0 +1,48 @@ +@startuml +'https://plantuml.com/class-diagram + +!include Style.puml +hide class fields +hide class methods +hide interface fields +hide interface methods + +class StorageManager STORAGE_COLOUR +class ProfileStorageUtils STORAGE_COLOUR +class FoodBankStorageUtils STORAGE_COLOUR +class FoodListStorageUtils STORAGE_COLOUR +class ExerciseListStorageUtils STORAGE_COLOUR +class FutureExerciseListStorageUtils STORAGE_COLOUR +class ExerciseBankStorageUtils STORAGE_COLOUR +class "<>\nProfileStorage" STORAGE_COLOUR +class "<>\nFoodBankStorage" STORAGE_COLOUR +class "<>\nExerciseBankStorage" STORAGE_COLOUR +class "<>\nFoodListStorage" STORAGE_COLOUR +class "<>\nExerciseListStorage" STORAGE_COLOUR +class "<>\nUpcomingStorage" STORAGE_COLOUR +class "<>\nStorage" STORAGE_COLOUR + +"<>\nProfileStorage" <|-- "<>\nStorage" +"<>\nFoodBankStorage" <|-- "<>\nStorage" +"<>\nExerciseBankStorage" <|-- "<>\nStorage" +"<>\nFoodListStorage" <|-- "<>\nStorage" +"<>\nExerciseListStorage" <|-- "<>\nStorage" +"<>\nUpcomingStorage" <|-- "<>\nStorage" + +StorageManager <|.up. "<>\nStorage" + +"<>\nProfileStorage" <|.. ProfileStorageUtils +"<>\nFoodBankStorage" <|.. FoodBankStorageUtils +"<>\nExerciseBankStorage" <|.. ExerciseBankStorageUtils +"<>\nFoodListStorage" <|.. FoodListStorageUtils +"<>\nExerciseListStorage" <|.. ExerciseListStorageUtils +"<>\nUpcomingStorage" <|.. FutureExerciseListStorageUtils + +StorageManager --> "1" "<>\nProfileStorage" +StorageManager --> "1" "<>\nFoodBankStorage" +StorageManager --> "1" "<>\nExerciseBankStorage" +StorageManager --> "1" "<>\nFoodListStorage" +StorageManager --> "1" "<>\nExerciseListStorage" +StorageManager --> "1" "<>\nUpcomingStorage" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/StorageManagerLoadSequenceDiagram.puml b/docs/diagrams/StorageManagerLoadSequenceDiagram.puml new file mode 100644 index 0000000000..3e4fa836f4 --- /dev/null +++ b/docs/diagrams/StorageManagerLoadSequenceDiagram.puml @@ -0,0 +1,58 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include style.puml +hide footbox +skinparam ParticipantPadding 1 + +participant ":Main" As Main MAIN_COLOUR +participant "storage:StorageManager" As StorageManager STORAGE_COLOUR +participant "profileStorage:ProfileStorage" As ProfileStorage STORAGE_COLOUR +participant Profile DATA_COLOUR + + +activate Main MAIN_COLOUR +create "storage:StorageManager" As StorageManager STORAGE_COLOUR +Main -> StorageManager **: new StorageManager() +activate StorageManager STORAGE_COLOUR + create "profileStorage:ProfileStorage" As ProfileStorage STORAGE_COLOUR + StorageManager -> ProfileStorage ** : new ProfileStorage(filePath) + activate ProfileStorage STORAGE_COLOUR + ProfileStorage --> StorageManager : profileStorage + deactivate + StorageManager --> Main : storage +deactivate + + +Main -> StorageManager: loadAll() + +activate StorageManager STORAGE_COLOUR + alt no errors thrown + StorageManager -> StorageManager ++ STORAGE_COLOUR : loadProfile() + StorageManager -> ProfileStorage : loadProfile() + activate ProfileStorage STORAGE_COLOUR + participant ":FileChecker" As FileChecker STORAGE_COLOUR + participant ":ProfileDecoder" As ProfileDecoder STORAGE_COLOUR + ref over ProfileStorage, FileChecker + Checks for the file and create directory if missing + end ref + ref over ProfileStorage, ProfileDecoder + Retrieval of data from storage with the use of ProfileDecoder to decode + end ref + ProfileStorage --> StorageManager:profile + deactivate + StorageManager --> StorageManager -- :profile + else UnableToReadFileException + StorageManager -> Profile **: new Profile() + activate Profile DATA_COLOUR + Profile --> StorageManager -- + destroy Profile + end + + + StorageManager --> Main: data + +deactivate + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/Style.puml b/docs/diagrams/Style.puml new file mode 100644 index 0000000000..d7fe02f5b7 --- /dev/null +++ b/docs/diagrams/Style.puml @@ -0,0 +1,42 @@ +@startuml +'COLOURS +!define BLACK #2F3030 +!define UI_COLOUR #FFCCFF +!define UI_FADED_COLOUR #FFE6F3 +!define MAIN_COLOUR #EA6B66 +!define MAIN_FADED_COLOUR #F1C3BE +!define LOGIC_COLOUR #FFFF66 +!define LOGIC_FADED_COLOUR #FFF7C4 +!define DATA_COLOUR #99FFFF +!define DATA_FADED_COLOUR #DAE8FC +!define STORAGE_COLOUR #D5E8D4 +!define STORAGE_FADED_COLOUR #E1F7E7 +!define STATE_COLOUR #40ff49 + +hide circle +hide footbox + +skinparam Shadowing false +skinparam class { + BorderColor BLACK + BorderThickness 1 + AttributeIconSize 0 +} +skinparam participant { + BorderColor BLACK + BorderThickness 1 +} +skinparam arrow { + Color BLACK +} +skinparam sequence { + TitleFontColor BLACK + LifeLineBackgroundColor WHITE + LifeLineBorderColor BLACK +} + +hide circle +hide enum methods + + +@enduml diff --git a/docs/diagrams/UiSequenceDiagram.puml b/docs/diagrams/UiSequenceDiagram.puml new file mode 100644 index 0000000000..d57ad8a9e3 --- /dev/null +++ b/docs/diagrams/UiSequenceDiagram.puml @@ -0,0 +1,21 @@ +@startuml +'https://plantuml.com/sequence-diagram +!include Style.puml +hide footbox +actor user #ffffff +participant Ui as ui UI_COLOUR +participant Main as main MAIN_COLOUR +main ++ MAIN_COLOUR +main -> main ++ MAIN_COLOUR: enterTaskModeUntilByeCommand() +main -> ui ++ UI_COLOUR : getUserInput() +user -> ui : type command +ui --> main -- : command +ref over main +parsing of commands +end +main -> ui ++ UI_COLOUR: formatMessageFramedWithDivider() +ui -> user : prints message \n on console +ui --> main -- + + +@enduml \ No newline at end of file diff --git a/docs/images/AddFoodItemSequenceDiagram.png b/docs/images/AddFoodItemSequenceDiagram.png new file mode 100644 index 0000000000..feee6dfb2c Binary files /dev/null and b/docs/images/AddFoodItemSequenceDiagram.png differ diff --git a/docs/images/AddFutureExerciseSequenceDaigram.png b/docs/images/AddFutureExerciseSequenceDaigram.png new file mode 100644 index 0000000000..2b8f89f681 Binary files /dev/null and b/docs/images/AddFutureExerciseSequenceDaigram.png differ diff --git a/docs/images/AddRecurringExerciseSequenceDiagram.png b/docs/images/AddRecurringExerciseSequenceDiagram.png new file mode 100644 index 0000000000..f8cf0144ac Binary files /dev/null and b/docs/images/AddRecurringExerciseSequenceDiagram.png differ diff --git a/docs/images/Architecture.png b/docs/images/Architecture.png new file mode 100644 index 0000000000..f317782f6d Binary files /dev/null and b/docs/images/Architecture.png differ diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png new file mode 100644 index 0000000000..d51962c6f0 Binary files /dev/null and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/ChecksForFileStorage.png b/docs/images/ChecksForFileStorage.png new file mode 100644 index 0000000000..c6f002b14e Binary files /dev/null and b/docs/images/ChecksForFileStorage.png differ diff --git a/docs/images/CreateProfile.png b/docs/images/CreateProfile.png new file mode 100644 index 0000000000..711773ac58 Binary files /dev/null and b/docs/images/CreateProfile.png differ diff --git a/docs/images/DataClassDiagram.png b/docs/images/DataClassDiagram.png new file mode 100644 index 0000000000..0ba09497a9 Binary files /dev/null and b/docs/images/DataClassDiagram.png differ diff --git a/docs/images/DataComponent.png b/docs/images/DataComponent.png new file mode 100644 index 0000000000..53a51b00dd Binary files /dev/null and b/docs/images/DataComponent.png differ diff --git a/docs/images/DataManagerClass.png b/docs/images/DataManagerClass.png new file mode 100644 index 0000000000..44cf3b344a Binary files /dev/null and b/docs/images/DataManagerClass.png differ diff --git a/docs/images/DecodeActivityFactor.png b/docs/images/DecodeActivityFactor.png new file mode 100644 index 0000000000..6f6e74718d Binary files /dev/null and b/docs/images/DecodeActivityFactor.png differ diff --git a/docs/images/DecodeAge.png b/docs/images/DecodeAge.png new file mode 100644 index 0000000000..ed4ec9588e Binary files /dev/null and b/docs/images/DecodeAge.png differ diff --git a/docs/images/DecodeAttributes.png b/docs/images/DecodeAttributes.png new file mode 100644 index 0000000000..23350afb00 Binary files /dev/null and b/docs/images/DecodeAttributes.png differ diff --git a/docs/images/DecodeCalorieGoal.png b/docs/images/DecodeCalorieGoal.png new file mode 100644 index 0000000000..73d15ccc30 Binary files /dev/null and b/docs/images/DecodeCalorieGoal.png differ diff --git a/docs/images/DecodeGender.png b/docs/images/DecodeGender.png new file mode 100644 index 0000000000..52a5a5903d Binary files /dev/null and b/docs/images/DecodeGender.png differ diff --git a/docs/images/DecodeHeight.png b/docs/images/DecodeHeight.png new file mode 100644 index 0000000000..8603af8268 Binary files /dev/null and b/docs/images/DecodeHeight.png differ diff --git a/docs/images/DecodeName.png b/docs/images/DecodeName.png new file mode 100644 index 0000000000..d20da056cf Binary files /dev/null and b/docs/images/DecodeName.png differ diff --git a/docs/images/DecodeWeight.png b/docs/images/DecodeWeight.png new file mode 100644 index 0000000000..5c30d172a5 Binary files /dev/null and b/docs/images/DecodeWeight.png differ diff --git a/docs/images/ItemBankAndItemClassDiagram.png b/docs/images/ItemBankAndItemClassDiagram.png new file mode 100644 index 0000000000..ce98dae78a Binary files /dev/null and b/docs/images/ItemBankAndItemClassDiagram.png differ diff --git a/docs/images/ItemBankCodeSnippet.png b/docs/images/ItemBankCodeSnippet.png new file mode 100644 index 0000000000..38d17ef61a Binary files /dev/null and b/docs/images/ItemBankCodeSnippet.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png new file mode 100644 index 0000000000..2cbb1ee6f4 Binary files /dev/null and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/LogicSequenceDiagram.png b/docs/images/LogicSequenceDiagram.png new file mode 100644 index 0000000000..f4c6cbb368 Binary files /dev/null and b/docs/images/LogicSequenceDiagram.png differ diff --git a/docs/images/MainClass.png b/docs/images/MainClass.png new file mode 100644 index 0000000000..da071460f2 Binary files /dev/null and b/docs/images/MainClass.png differ diff --git a/docs/images/ParseCommandRefFrame.png b/docs/images/ParseCommandRefFrame.png new file mode 100644 index 0000000000..e4fa4c8d84 Binary files /dev/null and b/docs/images/ParseCommandRefFrame.png differ diff --git a/docs/images/ParseParametersRefFrame.png b/docs/images/ParseParametersRefFrame.png new file mode 100644 index 0000000000..cfec2e0cdc Binary files /dev/null and b/docs/images/ParseParametersRefFrame.png differ diff --git a/docs/images/ParserClassDiagram.png b/docs/images/ParserClassDiagram.png new file mode 100644 index 0000000000..a5e7716e55 Binary files /dev/null and b/docs/images/ParserClassDiagram.png differ diff --git a/docs/images/ProfileClassDiagram.png b/docs/images/ProfileClassDiagram.png new file mode 100644 index 0000000000..13031af26b Binary files /dev/null and b/docs/images/ProfileClassDiagram.png differ diff --git a/docs/images/ProfileStorageClassDiagram.png b/docs/images/ProfileStorageClassDiagram.png new file mode 100644 index 0000000000..2056baf5b9 Binary files /dev/null and b/docs/images/ProfileStorageClassDiagram.png differ diff --git a/docs/images/RetrieveDataFromStorage.png b/docs/images/RetrieveDataFromStorage.png new file mode 100644 index 0000000000..688efde1b1 Binary files /dev/null and b/docs/images/RetrieveDataFromStorage.png differ diff --git a/docs/images/StartState.png b/docs/images/StartState.png new file mode 100644 index 0000000000..fc7ee0a50d Binary files /dev/null and b/docs/images/StartState.png differ diff --git a/docs/images/StartupLogo.png b/docs/images/StartupLogo.png new file mode 100644 index 0000000000..6204e01725 Binary files /dev/null and b/docs/images/StartupLogo.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png new file mode 100644 index 0000000000..a1476a7564 Binary files /dev/null and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/StorageManagerClassDiagram.png b/docs/images/StorageManagerClassDiagram.png new file mode 100644 index 0000000000..4f7d3e73e7 Binary files /dev/null and b/docs/images/StorageManagerClassDiagram.png differ diff --git a/docs/images/StorageManagerLoadSequenceDiagram.png b/docs/images/StorageManagerLoadSequenceDiagram.png new file mode 100644 index 0000000000..2d6dcb4d89 Binary files /dev/null and b/docs/images/StorageManagerLoadSequenceDiagram.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png new file mode 100644 index 0000000000..0acc66ecef Binary files /dev/null and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiSequenceDiagram.png b/docs/images/UiSequenceDiagram.png new file mode 100644 index 0000000000..3ddc004c7d Binary files /dev/null and b/docs/images/UiSequenceDiagram.png differ diff --git a/docs/images/exercise_list.png b/docs/images/exercise_list.png new file mode 100644 index 0000000000..8077836d85 Binary files /dev/null and b/docs/images/exercise_list.png differ diff --git a/docs/images/exercise_list_modified.png b/docs/images/exercise_list_modified.png new file mode 100644 index 0000000000..c04eb62f47 Binary files /dev/null and b/docs/images/exercise_list_modified.png differ diff --git a/docs/images/photos/lypic.jpg b/docs/images/photos/lypic.jpg new file mode 100644 index 0000000000..5baa8f8608 Binary files /dev/null and b/docs/images/photos/lypic.jpg differ diff --git a/docs/images/photos/ruiyang.png b/docs/images/photos/ruiyang.png new file mode 100644 index 0000000000..82fe914ea9 Binary files /dev/null and b/docs/images/photos/ruiyang.png differ diff --git a/docs/images/photos/weida.jpg b/docs/images/photos/weida.jpg new file mode 100644 index 0000000000..60db5fd654 Binary files /dev/null and b/docs/images/photos/weida.jpg differ diff --git a/docs/images/photos/xingjie.jpg b/docs/images/photos/xingjie.jpg new file mode 100644 index 0000000000..a6595d3078 Binary files /dev/null and b/docs/images/photos/xingjie.jpg differ diff --git a/docs/images/photos/yizhi.jpg b/docs/images/photos/yizhi.jpg new file mode 100644 index 0000000000..8ed79cb121 Binary files /dev/null and b/docs/images/photos/yizhi.jpg differ diff --git a/docs/images/profile_text_file.png b/docs/images/profile_text_file.png new file mode 100644 index 0000000000..ffc21ef554 Binary files /dev/null and b/docs/images/profile_text_file.png differ diff --git a/docs/images/profile_text_file_modified.png b/docs/images/profile_text_file_modified.png new file mode 100644 index 0000000000..ca2c19f6fa Binary files /dev/null and b/docs/images/profile_text_file_modified.png differ diff --git a/docs/img.png b/docs/img.png new file mode 100644 index 0000000000..06f0c40d05 Binary files /dev/null and b/docs/img.png differ 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/tlyi.md b/docs/team/tlyi.md new file mode 100644 index 0000000000..3a7c15aace --- /dev/null +++ b/docs/team/tlyi.md @@ -0,0 +1,79 @@ +--- +layout: page +title: Le Yi's Project Portfolio Page +--- + +## Overview +Fitbot is a desktop app that helps university students who are looking to keep track of their calorie consumption and calorie +output with the speed and convenience of command-line based tools, especially in times of online school. + +## Summary of Contributions +The following sections summarise what I have contributed to the project. + +#### **New Feature**: Added the ability to parse user input into commands +- What it does: Makes sense of user input and extracts the relevant type of command and parameters. +- Justification: This feature is necessary for Fitbot to understand the user inputs. +- Highlights: + - _Flexible parameter ordering_ - Parameters can be provided in any order and will still be parsed appropriately for the user's convenience. + - _Optional parameters_ - Some commands are able to take in many parameters, but not all are required at all times. Hence, the parser was implemented such that it can handle variable number of parameters for the same command. For example, to edit the profile using the `profile` command, users can input up to 7 parameters, but at any time, only one parameter is required. + This makes the commands shorter and more user-friendly. + - _Error handling_ - Specific error messages are shown for cases where user input is invalid, e.g. item type is not specified, unnecessary parameters detected, string given where numbers were expected, etc. +- Pull requests: [#36](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/36), [#42](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/42), [#96](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/96), [#121](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/121), [#169](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/169) + + +#### **New Feature**: Add, View, and Delete Food Bank/Exercise Bank Items +- What it does: The Food Bank and Exercise Bank allows users to store commonly consumed food/commonly done exercise items and their respective calories so that the next time they want to record them, they no longer have to provide the calories for these items. +- Justification: This makes it much more convenient for users to record their calories as they do not have to manually key in the calories everytime. +- Highlights: + - _Case insensitive match for item names_: For example, if the user stores the item with name "jogging" in the Exercise Bank, the next time he/she tries to record "JOGGING" as an Exercise Item, the calories for the item "jogging" will be automatically retrieved. This is so that it is more convenient for the user and he/she does not need to worry if the names are an exact match. +- Pull requests: [#96](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/96) + + + + +#### **Code Contributed**: [Reposense Link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=authorship&tabAuthor=tlyi&tabRepo=AY2122S1-CS2113T-F14-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + + +#### **Project Management**: +- Created issues and user stories for `v2.0`, `v2.0b` +- Assigned PE-D issues to team for `v2.1` +- Oversaw the code integration and reviewing process with the team for major code increments +([#37](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/37), +[#123](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/123), +[#224](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/224), + [#230](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/230), +[#234](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/234/files)) + + +#### **Enhancements To Existing Features**: + +#### **Built the skeleton code for Command classes** +To start off the project, we needed a base structure to build on for parsing and execution of commands. I set up the abstract `Command` class and the initial few `XYZCommand` classes for v1.0 to provide the structure for the rest of the team to build on. +([#36](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/36)) + +#### **Make the Logic Component more OOP** + +This was done in several increments. +1. Created `Parser` as an interface with various `Parser` classes specific to each `Command` class implementing it ([#102](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/102)) +2. Integrate command parsing and execution into a single `LogicManager` class ([#141](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/141)) +3. Integrate `DataManager` and `StorageManager` with `LogicManager` so that all operations involving command parsing, data manipulation and data saving are contained within `LogicManager`. ([#145](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/145)) + + + +#### **Documentation**: +- User Guide: + - Added more details to the introduction of Fitbot ([#128](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/128)) + - Added a section to explain the usage of the User Guide ([#128](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/128)) + - Added documentation for all Food Bank and Exercise Bank related commands, i.e. the sections [_"Building Your Food Bank"_](https://ay2122s1-cs2113t-f14-2.github.io/tp/UserGuide.html#45-building-your-food-bank) and [_"Building Your Exercise Bank"_](https://ay2122s1-cs2113t-f14-2.github.io/tp/UserGuide.html#46-building-your-exercise-bank). + - Added shortcuts back to content page after every major section for easier navigation ([#230](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/230)) +- Developer Guide: + - Added documentation for the [_`Logic` component_](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#logic-component) ([#134](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/134), [#241](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/241)) + - Added documentation for the implementation of [_"Parsing of Commands"_](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#parsing-of-commands) ([#241](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/241)) + - Added instructions of manual testing on [_"Building Food Bank"_](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#building-food-bank) and [_"Building Exercise Bank"_](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#building-exercise-bank) ([#230](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/230)) + - Standardised styling for PlantUML diagrams ([#117](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/117)) +- Standardised the styling on Github Pages for Product Website. ([#115](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/115), [#236](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/236)) + + +#### **Community**: +- PRs reviewed (with non-trivial comments): [#32](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/32), [#33](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/33), [#34](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/34), [#35](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/35). [#39](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/39) + diff --git a/docs/team/tryyang2001.md b/docs/team/tryyang2001.md new file mode 100644 index 0000000000..ebe0d1bf9f --- /dev/null +++ b/docs/team/tryyang2001.md @@ -0,0 +1,76 @@ +--- +layout: page +title: Rui Yang's Project Portfolio Page +--- + +## **Overview** + +Fitbot is a desktop app that helps university students who are looking to keep track of their calorie consumption and calorie +output with the speed and convenience of command-line based tools, especially in times of online school. + +### **Summary of Contributions** +Below are all the contributions done by the author to Fitbot. + +#### **New Feature**: Add, View and Delete Food Item +- What it does: This feature allows users to add, view and delete food item to the food list, together with the calorie of the food item, + and the date and time when consuming the food. +- Justification: This feature is one of the core feature in Fitbot application. It can help the users to keep track of the + food calorie taken. With all the food items stored in a list, this feature enables calculation of total calorie, and the number + of suppers taken. +- Highlights: This feature adds a food item to the food list, view all the food items in the food list and delete unwanted food item +- Pull requests: [#34](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/34), [#40](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/40), + [#46](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/46), [#81](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/81) + [#82](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/82), [#98](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/98) + +#### **Code Contributed**: +Here is the [link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&since=2021-09-25&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=tryyang2001&tabRepo=AY2122S1-CS2113T-F14-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) +to see the code contributed by the author. + +#### **Project Management**: +- Creates Github organization for the team and set up Github team repository +- Helps in maintaining issue tracker +- Helps to review, approve and merge Github pull requests +- Helps to publish release v1.0 and v2.0 in team repo + +#### **Enhancements To Existing Features**: +##### **Delete exercise according to date** +- Since exercises stored in the exercise list are split according to their date, users are now required to delete the exercise + by specifying the index and the date. +- Pull request: [#98](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/98) + +##### **Create Item class and ItemList class to make the code more OOP** +- Applies inheritance in Food, Exercise, FoodList and ExerciseList classes, helps to extract common methods, refactor + codes to improve efficiency, adds javaDoc to all public methods and most of the private methods, and standardizes the + code style. +- Pull request: [#81](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/81) + [#82](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/82), [#98](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/98), [#118](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/118) + +##### **Implement filtering items within 7 days of today tom improve code efficiency** +- When the program starts, filters the food list and exercise list to only take in items that are within 7 days of today (including today) + so that the users can only view the food and exercise items taken within this period. +- Pull request: [#122](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/122), [#140](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/140) + +##### **Checking for Food and Exercise Item data validity** +- Checks whether the date and input calorie from a food or exercise item are valid. +- Pull request: [#220](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/220) + +#### **Documentation**: +##### **Contribution to User Guide** +- Writes documentation for Section 3 (Terminology), 4.2 (Recording Food) and 4.3 (Recording Exercise) [#124](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/124) +- Fixes formatting issue in UG [#55](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/55), [#56](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/56), [#57](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/57), +- Ensures all the links provided in UG are valid [#161](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/161) +- Other Pull request: [#137](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/137) + +##### **Contribution to Documentation Guide** +- Writes simple introduction, creates content page draft, ensures hyperlinks provided in DG are valid [#132](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/132) +- Diagrams drawn: + - Architecture diagram [#106](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/106) + - ItemBank and Item class diagram [#106](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/106), [#183](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/183) + - Add Food Item sequence diagram +- Writes design details for Data Component (ItemBank and Item) [#133](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/133) +- Writes implementation details and design considerations for Add Food Item [#111](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/111) +- Writes documentation in glossary [#111](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/111), [#137](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/137) + +#### **Community**: +- PRs reviewed (with non-trivial comments): [#32](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/32), [#33](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/33), [#35](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/35), [#36](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/36), [#39](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/39), [#43](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/43), [#80](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/80), [#216](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/216), +- Reported bugs and suggestions for other teams in the class: [Team ConTech Developer Guide Reviews](https://github.com/nus-cs2113-AY2122S1/tp/pull/24) \ No newline at end of file diff --git a/docs/team/tttyyzzz.md b/docs/team/tttyyzzz.md new file mode 100644 index 0000000000..a21c6b9717 --- /dev/null +++ b/docs/team/tttyyzzz.md @@ -0,0 +1,71 @@ +--- +layout: page +title: Yi Zhi's Project Portfolio Page +--- + + + +## Overview +Fitbot is a desktop app that helps university students who are looking to keep track of their calorie consumption and calorie +output with the speed and convenience of command-line based tools, especially in times of online school. + +### Summary of Contributions +Summary of contributions are as listed below. + +### **New Feature** +#### **creation of create profile** + +- What it does: _Fitbot_ will prompt and guide user for profile particulars before allowing users to type in +other commands. + +- Justification: There are some commands that require profile attributes such as `BMI` and `Overview` commands. A bug might occur if the user decides to use the commands above first instead of setting up profile. +- Highlights: If the user profile is not complete, profile data will not be saved. + +### **New Feature** +#### **Overview** +- What it does: create a summary of the daily and weekly overview of the calories gained from food and +lost from exercise. +- Justification: allows users to have a better user experience and allow users to visualised data. +- Highlights: There is bar graph in the overview +- +### **New Feature** +#### **creation of `Help` Class** +- What it does: A summary of all the commands and their formats. + +### **New Feature** +Fitbot start-up page. + +#### **Code Contributed**: +[Link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=authorship&tabAuthor=tttyyzzz&tabRepo=AY2122S1-CS2113T-F14-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false) +#### **Project Management**: + +PRs reviewed: [#33](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/33#discussion_r725440121), +[#36](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/36#discussion_r725726457), +[#39](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/39#discussion_r726717261), +[#217](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/217#discussion_r742581641) +#### **Enhancements To Existing Features**: +#### Create Profile v2.0 +Will check if the attributes is valid. IF most of the attributes are invalid, it will ask user to recreate the profile. +Else, the system will pick up those attributes that are invalid and will prompt the user for a valid input upon +starting up _Fitbot_ + +Allow users to have the option to quit the application if they wish to quit while setting up the profile attributes. +#### **Documentation** + +**User guide** +- Section 3: Set Up profile +- Section 4.7: Viewing Your Calorie Summary `overview` +- Section 4.8: Viewing Help `help` + +[Link to user guide](https://ay2122s1-cs2113t-f14-2.github.io/tp/UserGuide.html) + +**Developer Guide** +- Explanation for Main Architecture Diagram. +- Explanation of _Ui component_ (includes ui sequence diagram) +- Explanation of _State component_ (includes state class diagram) +- Explanation of _Create Profile If Not Exist On Startup_ (create profile sequence diagram) +- Give test cases for _Setting Up Profile_ in _Instructions for manual testing_ + +[Link to developer guide](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html) + +#### **Community**: diff --git a/docs/team/weidak.md b/docs/team/weidak.md new file mode 100644 index 0000000000..45a3303882 --- /dev/null +++ b/docs/team/weidak.md @@ -0,0 +1,87 @@ +--- +layout: page +title: Weida's Project Portfolio Page +--- + + +## Overview + +Fitbot is a desktop app that helps university students who are looking to keep track of their calorie consumption and calorie output with the speed and convenience of command-line based tools, especially in times of online school. + +### Summary of Contributions +The following items listed below are what I have contributed to the team: + +#### **New Feature**: Profile + +- What it does: Contains the user's attributes such as Name, Height, Weight, Age, Gender, Calorie Goal, Activity Factor. +- Justification: The main idea behind the profile was to make Fitbot feel more personalized and let it know your characteristics. Most of these attributes are required in the Harris-Benedict Equation we used to calculate one's Basal Metabolic Rate, which is + important in determining an accurate calorie budget for the day. +- Highlights: This feature incorporates its individual classes of each attribute to ensure data integrity is preserved and profile is a container that + holds these attributes together. + +#### **New Feature**: Saving and loading of data from Storage + +- What it does: It allows users to load all the data upon startup of the application and save data depending on the command that is being executed. +- Justification: Having a storage utility is crucial in our app's functionality of calculating the past week of exercise/food consumed, +as well as remembering one's profile attributes that lead to the calculations. +- Highlights: This feature loads and saves all the different data structures we have come up with, including FoodList, ExerciseList, FutureExerciseList, ItemBank, Profile. It can detect an anomaly within the file and handle it in a way that it can be dealt with later on +in another state whereby users can modify their attributes again in case they have accidentally corrupted a particular attribute. + + +#### **Code Contributed**: + +[Reposense Link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=weidak&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=authorship&tabAuthor=weidak&tabRepo=AY2122S1-CS2113T-F14-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false) + +#### **Project Management**: + +- Aided in setting up the GitHub repo and initializing the project during the early stages of the project +- Created issues and user stories for `v1.0`, and generally helped to find bugs and assign new issues on a consistent basis such as Documentation or type.Task related. + + + +[comment]: <> (TODO add the PRs reviewed on github prs..) + +#### **Enhancements To Existing Features**: + +**Add verifiability to various Profile attributes** + +- As our program loads the attributes from storage (to see whether it has been modified maliciously), it detects the attribute validity. With this +implementation, we are able to utilize the StartState (implemented by Yi Zhi) to rectify the problem without discarding the entire profile data. ([#88](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/88)) + +**Creation of StorageManager Class** + +- As we grew in our list of features, our storages for the various features grew as well. As such, acting as an agent, it encapsulates all storages required to make our program work with their respective functionality (loading/saving). ([#99](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/99)) + +**Creation of DataManager Class** + +- After deliberating with the other members, a DataManager class was created to encompass all the different types of Data we have (FoodList, Profile, ExerciseBank etc.) +and ensured all the items are localised in this class. This de-clutters Main.java and primarily acts as an agent for the various items. ([#143](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/143)) + +**Storage OOP** + +- Using a `Storage.java` API, it interfaces all other Storage-related interfaces and ensures the methods for each `Storage` superinterface is implemented. As such, they are initialised by each storage's class of `XYZStorageUtils.java` and brought together in the `StorageManager.java` class. + +#### **Documentation**: + + +- User Guide: + - [Section 4.2 (Customising your profile)](https://ay2122s1-cs2113t-f14-2.github.io/tp/UserGuide.html#41-customising-your-profile) ([#126](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/126), [#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - [Section 6 (Data Limits)](https://ay2122s1-cs2113t-f14-2.github.io/tp/UserGuide.html#6-data-limits) ([#243](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) +- Developer Guide: + - Diagrams: + - [Data Component Diagram (Design)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#data-component) ([#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - [Profile Class Diagram (Design)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#data-component-profile) ([#108](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/108), [#135](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/135), [#208](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/208)[#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - [Storage Class Diagram (Design)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#storage-component) ([#108](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/108), [#135](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/135), [#208](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/208), [#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - [All the diagrams under "Loading of Data on Startup" (Implementation)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#loading-of-data-on-startup) ([#208](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/208) ,[#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - Explanations: + - [Explanation for Profile Component (Design)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#data-component-profile) ([#108](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/108), [#135](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/135), [#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - [Explanation for Storage Component (Design)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#storage-component) ([#108](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/108), [#135](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/135), [#208](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/208), [#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - [Explanation for "Loading of Data on Startup" (Implementation)](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#loading-of-data-on-startup) ([#208](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/208), [#238](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/238)) + - Manual Testing: + - [Manipulating and saving data](https://ay2122s1-cs2113t-f14-2.github.io/tp/DeveloperGuide.html#manipulating-and-saving-data) ([#229](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/229)) + +#### **Community**: + +- Helped other team review their Developer Guide ([here](https://github.com/nus-cs2113-AY2122S1/tp/pull/38/files/573949f70d1e1057b046baeb5df957ba63857559)) +- PRs reviewed (With non-trivial comments): [#34](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/34), [#35](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/35), + [#80](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/80), [#119](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/119) \ No newline at end of file diff --git a/docs/team/xingjie99.md b/docs/team/xingjie99.md new file mode 100644 index 0000000000..68667b82e9 --- /dev/null +++ b/docs/team/xingjie99.md @@ -0,0 +1,69 @@ +--- +layout: page +title: Xing Jie's Project Portfolio Page +--- + + +## Overview + +Fitbot is a desktop app that helps university students who are looking to keep track of their calorie consumption and calorie output with the speed and convenience of command-line based tools, especially in times of online school. + +### Summary of Contributions + +Given below are my contributions to the project. + +#### **New Feature**: Add, Delete and View Upcoming Exercises in Upcoming Exercise List + +- What it does: Allows user to add, delete and view exercises that occur in the future in the Upcoming Exercise List. + All of the upcoming exercises added will automatically be loaded into the exercise list if the dates of the upcoming exercises have passed. +- Justification: This feature improves the product by enabling the user to keep track of his/her upcoming exercises and plan them in advance. +- Highlights: Users are also able to add exercises that occur on a timely basis by adding recurring exercises into the Upcoming Exercise List. + + +#### **New Feature**: Edit items in Upcoming Exercise List, Food Bank and Exercise Bank. + +- What it does: Allows user to edit items with wrong information in the Upcoming Exercise List, Food Bank and Exercise Bank. +- Justification: This feature improves the product by enabling user to change the information of the item directly, instead of having to delete and add the item again. +- Highlights: Users are only required to key in the information that needs to be changed instead of having to type out all the information again. + +#### **New Feature**: Delete multiple items in Upcoming Exercise List, Food Bank and Exercise Bank. + +- What it does: Allows user to delete multiple items at once in the Upcoming Exercise List, Food Bank and Exercise Bank. +- Justification: This feature improves the product by enabling user to delete multiple items at once instead of having to delete them one by one. +- Highlights: Users are only required to key in the indexes of all the items they wish to delete. + +(Completed this feature together with Le Yi) + + +#### **Code Contributed**: + +[RepoSense Link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=xingjie99&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=authorship&tabAuthor=xingjie99&tabRepo=AY2122S1-CS2113T-F14-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false) + +#### **Project Management**: + +- Partake in maintaining issue tracker (create issues, assign labels and assignees ) +- Partake in reviewing, approving and merging PRs + +#### **Enhancements to Existing Features**: + +- Restricted upcoming exercises to be within a year to prevent addition of exercises that are too far into the future. +- Automatically loads upcoming exercises into Exercise List upon re-running the application if the dates have passed. + + +#### **Documentation**: + +- User Guide: + + - Wrote Section 4.4 Scheduling Your Exercises + - Wrote FAQ + - Looked through the whole UG and standardized the formatting and wordings. [#237](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/237) + +- Developer Guide: + + - Wrote the manual testing for Recording Exercise Items and Scheduling Exercise + - Wrote implementation details and design considerations for Add Recurring Exercises. + + +#### **Community**: +- PR reviewed: [#34](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/34), [#36](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/36), [#39](https://github.com/AY2122S1-CS2113T-F14-2/tp/pull/39) +- Reported bugs and suggestions for other teams in the class: [Team LOTS Developer Guide Review](https://github.com/nus-cs2113-AY2122S1/tp/pull/34) diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..7a39593554 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: seedu.duke.Main + diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/main/java/seedu/duke/Main.java b/src/main/java/seedu/duke/Main.java new file mode 100644 index 0000000000..f7292e3bbb --- /dev/null +++ b/src/main/java/seedu/duke/Main.java @@ -0,0 +1,88 @@ +package seedu.duke; + +import seedu.duke.data.DataManager; +import seedu.duke.logic.LogicManager; +import seedu.duke.logic.commands.CommandResult; +import seedu.duke.state.StartState; +import seedu.duke.storage.Storage; +import seedu.duke.storage.StorageManager; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.ui.Ui; + + +/** + * Main class of Fitbot. + * Initialises the application and starts interaction with user. + */ +public class Main { + private DataManager dataManager; + private Ui ui; + private StorageManager storageManager; + private LogicManager logicManager; + private StartState startState; + + + + /** + * Entry point of the application. + */ + public static void main(String[] args) { + new Main().run(args); + } + + /** + * Runs the application until command is given to exit it. + **/ + private void run(String[] args) { + start(); + checkAndCreateProfile(); + enterTaskModeUntilByeCommand(); + exit(); + } + + //@@author tttyyzzz + /** + * Checks the attributes of profile in dataManager. + */ + private void checkAndCreateProfile() { + dataManager.setProfile(startState.checkAndCreateProfile()); + } + //@@author + + /** + * Initialises the application by creating the required objects and loading data from the + * storage file, then showing the welcome message. + */ + private void start() { + this.storageManager = new StorageManager(); + this.ui = new Ui(); + this.dataManager = storageManager.loadAll(); + this.logicManager = new LogicManager(storageManager, dataManager); + this.startState = new StartState(dataManager.getProfile(), storageManager, ui); + ui.printStartMessage( + dataManager.getProfile().checkProfileComplete(), + dataManager.getProfile().checkProfilePresent()); + } + + /** + * Reads the user input and executes appropriate command. + * Runs indefinitely until user inputs the Bye command. + */ + private void enterTaskModeUntilByeCommand() { + CommandResult result; + do { + String userInput = ui.getUserInput(); + result = logicManager.execute(userInput); + ui.formatMessageFramedWithDivider(result.toString()); + } while (!result.isBye()); + } + + + /** + * Exits the application. + */ + private void exit() { + System.exit(0); + } + +} diff --git a/src/main/java/seedu/duke/data/DataManager.java b/src/main/java/seedu/duke/data/DataManager.java new file mode 100644 index 0000000000..bab01a0c15 --- /dev/null +++ b/src/main/java/seedu/duke/data/DataManager.java @@ -0,0 +1,224 @@ +package seedu.duke.data; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.ItemBank; +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.data.item.exercise.FutureExerciseList; +import seedu.duke.data.item.food.Food; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.data.profile.Profile; + +import java.time.LocalDate; + +/** + * Manages the data that is processed by the application. + */ +public class DataManager { + + private ExerciseList filteredExerciseItems; + private ExerciseList exerciseItems; + private FutureExerciseList futureExerciseItems; + private FoodList filteredFoodItems; + private FoodList foodItems; + private ItemBank exerciseBank; + private ItemBank foodBank; + private Profile profile; + + /** + * Handles the different types of data going into and out of the bot. + * Used for storage purposes. + * + * @param exerciseItems Exercises items to be initialized + * @param futureExerciseItems Upcoming exercise items to be initialized + * @param foodItems Food items to be initialized + * @param exerciseBank Exercise bank items to be initialized + * @param foodBank Food bank items to be initialized + * @param profile Profile to be initialized + */ + public DataManager(ExerciseList exerciseItems, FutureExerciseList futureExerciseItems, FoodList foodItems, + ItemBank exerciseBank, ItemBank foodBank, Profile profile) { + this.exerciseItems = exerciseItems; + this.futureExerciseItems = futureExerciseItems; + this.foodItems = foodItems; + this.exerciseBank = exerciseBank; + this.foodBank = foodBank; + this.profile = profile; + loadsFutureExercisesToList(); + filterExerciseListAndFoodList(); + } + + + //@@author xingjie99 + + /** + * Check whether the dates of the exercises in the future exercise list have passed. + * If the dates have passed, move the exercises in the exercise list. + */ + private void loadsFutureExercisesToList() { + int index = 0; + LocalDate today = LocalDate.now(); + while (futureExerciseItems.getSize() != 0 + && (futureExerciseItems.getItem(index).getDate().isBefore(today) + || futureExerciseItems.getItem(index).getDate().isEqual(today))) { + String name = futureExerciseItems.getItem(index).getName(); + int calories = futureExerciseItems.getItem(index).getCalories(); + LocalDate date = futureExerciseItems.getItem(index).getDate(); + exerciseItems.addItem(new Exercise(name, calories, date)); + futureExerciseItems.deleteItem(index); + } + } + //@@author + + //====================Filtered Lists methods========================= + + /** + * Returns the filtered Exercise List. + * + * @return filtered exercise list in DataManager object + */ + public ExerciseList getFilteredExerciseItems() { + return filteredExerciseItems; + } + + /** + * Returns the filtered Food List. + * + * @return filtered food list in DataManager object + */ + public FoodList getFilteredFoodItems() { + return filteredFoodItems; + } + + /** + * Filters exercise list and food list that are within 7 days before today. + */ + private void filterExerciseListAndFoodList() { + this.filteredExerciseItems = new ExerciseList(); + filterExerciseListWithPastSevenDaysRecordOnly(); + this.filteredFoodItems = new FoodList(); + filterFoodListWithPastSevenDaysRecordOnly(); + } + + /** + * Filters food list and add food items that are within 7 days before today. + */ + private void filterFoodListWithPastSevenDaysRecordOnly() { + LocalDate today = LocalDate.now(); + for (int i = foodItems.getSize() - 1; i >= 0; i--) { + Food food = (Food) foodItems.getItem(i); + if (food.getDate().isBefore(today.minusDays(7))) { + break; + } + if (isWithinPastSevenDays(food, today)) { + filteredFoodItems.addItem(food); + } + } + } + + /** + * Checks if the item is within 7 days of today. + * + * @param item The item from the item list + * @return True if the item date is not before 7 days from today, and is not after today + */ + private boolean isWithinPastSevenDays(Item item, LocalDate today) { + boolean isBeforeOrEqualToday = item.getDate().isEqual(today) || item.getDate().isBefore(today); + boolean isWithinOneWeek = item.getDate().isAfter(today.minusDays(7)); + return isBeforeOrEqualToday && isWithinOneWeek; + } + + /** + * Filters exercise list and add exercises that are within 7 days before today. + */ + private void filterExerciseListWithPastSevenDaysRecordOnly() { + LocalDate today = LocalDate.now(); + for (int i = exerciseItems.getSize() - 1; i >= 0; i--) { + Exercise exercise = (Exercise) exerciseItems.getItem(i); + if (exercise.getDate().isBefore(today.minusDays(7))) { + break; + } + if (isWithinPastSevenDays(exercise, today)) { + filteredExerciseItems.addItem(exercise); + } + } + } + + //====================ExerciseList methods========================= + + + /** + * Returns the exercise list. + * + * @return exercise list in DataManager object + */ + public ExerciseList getExerciseItems() { + return this.exerciseItems; + } + + //====================FutureExerciseList methods=================== + + + /** + * Returns the future exercise items. + * + * @return future exercise list in DataManager object + */ + public FutureExerciseList getFutureExerciseItems() { + return this.futureExerciseItems; + } + + //========================FoodList methods============================= + + /** + * Returns the food items. + * + * @return food items in DataManager object + */ + public FoodList getFoodItems() { + return this.foodItems; + } + + //=====================FoodBank methods============================ + + /** + * Returns food bank items. + * + * @return food bank items in DataManager object + */ + public ItemBank getFoodBank() { + return this.foodBank; + } + + //=====================ExerciseBank methods========================== + + /** + * Returns exercise bank items. + * + * @return exercise bank items in DataManager object + */ + public ItemBank getExerciseBank() { + return this.exerciseBank; + } + + //=====================Profile method================================ + + /** + * Replaces profile with data in {@code profile}. + * + * @param profile profile to be set + */ + public void setProfile(Profile profile) { + this.profile = profile; + } + + /** + * Returns profile. + * + * @return profile in DataManager object + */ + public Profile getProfile() { + return this.profile; + } + +} diff --git a/src/main/java/seedu/duke/data/Verifiable.java b/src/main/java/seedu/duke/data/Verifiable.java new file mode 100644 index 0000000000..f12d187465 --- /dev/null +++ b/src/main/java/seedu/duke/data/Verifiable.java @@ -0,0 +1,13 @@ +package seedu.duke.data; + +/** + * Interface that implements verifiability to various objects. + */ +public interface Verifiable { + /** + * Implements a simple check that verifies if the attribute tied to Verifiable is valid. + * + * @return True if valid, false if invalid as specified by each attribute + */ + boolean isValid(); +} diff --git a/src/main/java/seedu/duke/data/item/Item.java b/src/main/java/seedu/duke/data/item/Item.java new file mode 100644 index 0000000000..4a8d4203be --- /dev/null +++ b/src/main/java/seedu/duke/data/item/Item.java @@ -0,0 +1,158 @@ +package seedu.duke.data.item; + +import seedu.duke.data.Verifiable; +import seedu.duke.data.item.food.TimePeriod; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Item is an abstract class that contains methods used in common in Food and Exercise classes. + */ +public abstract class Item implements Verifiable { + public static final String FILE_TEXT_DELIMITER = "|"; + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + public static final DateTimeFormatter DATE_FORMATTER_FOR_STORAGE = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm"); + public static final DateTimeFormatter DATE_FORMAT_FOR_PRINTING = DateTimeFormatter.ofPattern("dd MMM yyyy"); + public static final String PREFIX_DELIMITER = "/"; + public static final int HIGHEST_CALORIE = 10000; + public static final int LOWEST_CALORIE = 0; + protected String name; + protected int calories; + + /** + * Constructor for item object. + * + * @param name The name or description of the item + * @param calories The calorie intake/burnt from the item + */ + public Item(String name, int calories) { + this.name = name; + this.calories = calories; + } + + //====================Getter and Setter Methods========================= + + /** + * Gets the name or description of the item. + * + * @return The name or description of the item + */ + public String getName() { + return this.name; + } + + /** + * Gets the calorie intake/burnt for the item. + * + * @return The calorie intake/burnt from the item + */ + public int getCalories() { + return this.calories; + } + + /** + * Updates the name or description of the item. + * + * @param name The new name or description of the item + */ + public void setName(String name) { + this.name = name; + } + + /** + * Updates the calorie intake/burnt from the item. + * + * @param calories The new calorie intake/burnt from the item + */ + public void setCalories(int calories) { + this.calories = calories; + } + + //====================Non-abstract methods========================= + + /** + * Converts the item in correct string format. + * + * @return The item name and calorie displayed in string + */ + @Override + public String toString() { + return this.getName() + " (" + this.getCalories() + " cal)"; + } + + /** + * Converts the item to external file string format. + * + * @return Name and calorie of the item in string + */ + public String toFileTextString() { + return FILE_TEXT_DELIMITER + this.getName() + FILE_TEXT_DELIMITER + this.getCalories(); + } + + /** + * Checks if the name of the item is valid. + * + * @return True if the item name is valid, false otherwise + */ + @Override + public boolean isValid() { + boolean isValidName = !name.contains(PREFIX_DELIMITER) && !name.contains(FILE_TEXT_DELIMITER); + boolean isValidCalorie = this.calories > LOWEST_CALORIE && this.calories <= HIGHEST_CALORIE; + return isValidName && isValidCalorie; + } + + //====================Abstract methods========================= + + /** + * Converts the item in string format which only shows the item name description and calorie, + * will be implemented in Food and Exercise classes. + * + * @return The item name and calorie display + */ + public abstract String toStringWithoutDateAndTime(); + + /** + * Converts the item in the string format same as toString method but with date. + * + * @return The item name, calorie and date in string + */ + public abstract String toStringWithDate(); + + /** + * Gets the date of the item, will be implemented in Food and Exercise classes. + * + * @return The date of the item + */ + public abstract LocalDate getDate(); + + /** + * Updates the date. To be implemented in Exercise class. + * + * @param date The new date of the exercise done in LocalDate + */ + public abstract void setDate(LocalDate date); + + + //====================Food Item methods========================= + + /** + * Gets the time period of the item, will be implemented in Food class. + * + * @return The time period of the food item + */ + public TimePeriod getTimePeriod() { + return null; + } + + /** + * Gets the date and time of the item, will be implemented in Food class. + * + * @return The date and time of the food item in LocalDateTime + */ + public LocalDateTime getDateTime() { + return null; + } +} diff --git a/src/main/java/seedu/duke/data/item/ItemBank.java b/src/main/java/seedu/duke/data/item/ItemBank.java new file mode 100644 index 0000000000..fa4b16520e --- /dev/null +++ b/src/main/java/seedu/duke/data/item/ItemBank.java @@ -0,0 +1,137 @@ +package seedu.duke.data.item; + +import seedu.duke.data.item.exceptions.DuplicateItemInBankException; +import seedu.duke.data.item.exceptions.ItemNotFoundInBankException; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Represents a list of Items, meant to store a repository of user-defined Exercise items or Food items. + * Contains all the methods for actions that can be done on the list. + */ +public class ItemBank { + protected static final String LS = System.lineSeparator(); + protected static final String TAB = "\t"; + protected ArrayList internalItems = new ArrayList<>(); + + /** + * Adds new item to the item bank. + * + * @param item The new item to add + * @throws DuplicateItemInBankException Throws this error when duplicate items are found + */ + public void addItem(Item item) throws DuplicateItemInBankException { + checkNoDuplicateItemName(item.getName()); + internalItems.add(item); + } + + /** + * Returns item with the given index in the item bank. + * + * @param index The index of the item + * @return The item with the given index + */ + public Item getItem(int index) { + return internalItems.get(index); + } + + /** + * Deletes the item in the bank. + * + * @param index The index of the item to delete + * @return The deleted item + */ + public Item deleteItem(int index) { + return internalItems.remove(index); + } + + /** + * Deletes all the item inside the item bank. + */ + public void clearList() { + internalItems.clear(); + } + + /** + * Returns the size of the item bank. + * + * @return The size of the array list attribute + */ + public int getSize() { + return internalItems.size(); + } + + /** + * Converts the array list to a string for printing purpose. + * + * @return The array list in string + */ + public String convertToString() { + StringBuilder listToString = new StringBuilder(); + for (int i = 0; i < internalItems.size(); i++) { + listToString.append(TAB).append(i + 1).append(". ") + .append(internalItems.get(i).toStringWithoutDateAndTime()).append(LS); + } + return listToString.toString().stripTrailing(); + } + + /** + * Gets the calories of the item which has the same name as the input item. + * + * @param inputName The input item name in string + * @return The calories value of the item which has the same name as the input item. + * @throws ItemNotFoundInBankException Throws this exception when there is no item which has the same name + * as the input item + */ + public int findCalorie(String inputName) throws ItemNotFoundInBankException { + Item matchingItem = internalItems + .stream() + .filter(item -> item.getName().equalsIgnoreCase(inputName)) + .findAny() + .orElse(null); + if (matchingItem == null) { + throw new ItemNotFoundInBankException(); + } + return matchingItem.getCalories(); + } + + /** + * Checks if there already exists an item in the bank with the same name as the input. + * Used before adding/editing items to/from the bank. + * + * @param inputName String to check against + * @throws DuplicateItemInBankException Throws this exception if there already exists an item in the bank with + * the same name (case-insensitive). + */ + public void checkNoDuplicateItemName(String inputName) throws DuplicateItemInBankException { + long numOfMatchingItems = internalItems + .stream() + .filter(item -> item.getName().equalsIgnoreCase(inputName)) + .count(); + if (numOfMatchingItems > 0) { + throw new DuplicateItemInBankException(); + } + } + + /** + * Deletes multiple items in the list. + * + * @param itemIndexArray Array of indexes to delete from + * @throws IndexOutOfBoundsException Throws this exception if any of the index in the provided array does not exist + */ + public String deleteMultipleItems(ArrayList itemIndexArray) { + Collections.sort(itemIndexArray); + StringBuilder itemsToString = new StringBuilder(); + int numberPointer = 0; + for (Integer index : itemIndexArray) { + itemsToString.append(LS) + .append(TAB) + .append(index + 1) + .append(". ") + .append(deleteItem(index - numberPointer).toStringWithoutDateAndTime()); + numberPointer++; + } + return itemsToString.toString().stripTrailing(); + } +} diff --git a/src/main/java/seedu/duke/data/item/ItemList.java b/src/main/java/seedu/duke/data/item/ItemList.java new file mode 100644 index 0000000000..da4b028416 --- /dev/null +++ b/src/main/java/seedu/duke/data/item/ItemList.java @@ -0,0 +1,186 @@ +package seedu.duke.data.item; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.stream.Collectors; + +/** + * ItemList is an abstract class that contains all the common methods for food list and exercise list. + */ +public abstract class ItemList extends ItemBank { + public static final String MESSAGE_ITEM = "%d. %s"; + public static final String ITEM_LIST_DIVIDER = "..............................................." + + "..........................................................."; + protected static final String DATE_FORMAT = "dd MMM yyyy"; + protected static final String LS = System.lineSeparator(); + protected static final String TAB = "\t"; + + + //====================Override methods========================= + + /** + * Adds an item into the item list. + * + * @param item The item class object to add + */ + @Override + public void addItem(Item item) { + this.internalItems.add(item); + sortList(); + } + + + //====================Public methods========================= + + /** + * Computes the sum of calorie of all items in the item list. + * + * @return Integer value of the sum of calorie of all the items + */ + public int getTotalCalories() { + int totalCalories = internalItems.stream().mapToInt(Item::getCalories).sum(); + assert totalCalories >= 0 : "Total calories cannot less than 0"; + return totalCalories; + } + + /** + * Computes the sum of calorie of all food items consumed in a specific date. + * + * @param date The date to query all the consumed food items + * @return Integer value of the sum of calorie of all food items consumed in the given date + */ + public int getTotalCaloriesWithDate(LocalDate date) { + int totalCalories = internalItems.stream() + .filter(i -> i.getDate().isEqual(date)) + .mapToInt(Item::getCalories) + .sum(); + assert totalCalories >= 0 : "Total calories cannot less than 0"; + return totalCalories; + } + + /** + * Sorts the item list according to date (and) time, will be implemented in FoodList and ExerciseList. + */ + public abstract void sortList(); + + /** + * Converts the item list of a specific date to a string for printing purpose, + * will be implemented in FoodList and ExerciseList. + * + * @param date The given date to query the item list + * @return The string of the item list of a specific date + */ + public abstract String convertToStringBySpecificDate(LocalDate date); + + /** + * Adds all items in the filtered list to the list, also prevents adding duplicate items. + * + * @param filteredItemList The other item list + */ + public void addAll(ItemList filteredItemList) { + LocalDate today = LocalDate.now(); + ArrayList listToRemove = (ArrayList) internalItems + .stream() + .filter(f -> f.getDate().isAfter(today.minusDays(8))) + .collect(Collectors.toList()); + internalItems.removeAll(listToRemove); + for (int i = 0; i < filteredItemList.getSize(); i++) { + internalItems.add(filteredItemList.getItem(i)); + } + this.sortList(); + } + + + //====================Common Helper methods for Food List and Exercise List========================= + + /** + * Gets the day of the week of the given date. + * + * @param currentDate The date to query the day of the week + * @return The day of the week in string + */ + protected String getDayOfWeek(LocalDate currentDate) { + String day = currentDate.getDayOfWeek().toString(); + day = day.charAt(0) + day.substring(1).toLowerCase(); + return day; + } + + /** + * Common method used in food list and exercise list to generate the item count string. + * + * @param itemListInString The StringBuilder that will contain the correct output string format + * @param size The size of the item list + * @param date The date to query the item count + * @param message The string format to display + */ + protected void convertItemCountToString(StringBuilder itemListInString, int size, LocalDate date, String message) { + itemListInString + .append(String.format(message, size, getDayOfWeek(date), + date.format(DateTimeFormatter.ofPattern(DATE_FORMAT)))) + .append(ItemList.LS); + } + + /** + * Common method used in food list and exercise list to generate the item string. + * + * @param itemListInString The StringBuilder that will contain the correct output string format + * @param index The index of the item in the list + * @param item The item to convert to string + */ + protected void convertItemToString(StringBuilder itemListInString, int index, Item item) { + itemListInString.append(ItemList.TAB).append(String.format(MESSAGE_ITEM, index, item)).append(ItemList.LS); + } + + /** + * Common method used in food list and exercise list to generate the total calorie string. + * + * @param itemListInString The StringBuilder that will contain the correct output string format + * @param totalCalories The sum of calorie for each item stored inside the list + * @param message The string format to display + */ + protected void convertTotalCaloriesToString(StringBuilder itemListInString, int totalCalories, String message) { + itemListInString.append(String.format(message, totalCalories)).append(ItemList.LS); + } + + /** + * Helper method used in deleteItem method in FoodList and ExerciseList to get the + * actual index from the entire list of the items to delete. + * + * @param index The index of the item as shown in the view f/ or view e/ command + * @param deletedItem The item to delete + * @return The actual index of the item in the entire item list + */ + protected int getActualIndex(int index, Item deletedItem) { + for (int i = 0; i < internalItems.size(); i++) { + if (isListToQuery(deletedItem, i) && isItemToDelete(deletedItem, i, index)) { + return i + index; + } else if (isListToQuery(deletedItem, i)) { + break; + } + } + return -1; + } + + /** + * Helper method used in getActualIndex to check if the current index has the same date, + * (and time period for food) as the user input, will be implemented in FoodList and ExerciseList. + * + * @param deletedItem The item to delete + * @param currentIndex The current index of the entire item list + * @return True if the current index points to the item that has the same date (and time for food) + * as the item to delete, false otherwise + */ + protected abstract boolean isListToQuery(Item deletedItem, int currentIndex); + + /** + * Helper method used in getActualIndex to check if the current index points to the item to delete, + * will be implemented in FoodList and ExerciseList accordingly. + * + * @param deletedItem The item to delete + * @param currentIndex The current index of the entire item list + * @param indexToDelete The index to delete as shown in view f/ or view e/ command + * @return true if the current index points to the item to delete, false otherwise + */ + protected abstract boolean isItemToDelete(Item deletedItem, int currentIndex, int indexToDelete); +} diff --git a/src/main/java/seedu/duke/data/item/exceptions/DuplicateItemInBankException.java b/src/main/java/seedu/duke/data/item/exceptions/DuplicateItemInBankException.java new file mode 100644 index 0000000000..f165334811 --- /dev/null +++ b/src/main/java/seedu/duke/data/item/exceptions/DuplicateItemInBankException.java @@ -0,0 +1,8 @@ +package seedu.duke.data.item.exceptions; + +/** + * Represents an error where the item to be added to the bank already exists (compared by name). + */ +public class DuplicateItemInBankException extends Exception { + +} diff --git a/src/main/java/seedu/duke/data/item/exceptions/ItemNotFoundInBankException.java b/src/main/java/seedu/duke/data/item/exceptions/ItemNotFoundInBankException.java new file mode 100644 index 0000000000..492eebd5c1 --- /dev/null +++ b/src/main/java/seedu/duke/data/item/exceptions/ItemNotFoundInBankException.java @@ -0,0 +1,7 @@ +package seedu.duke.data.item.exceptions; + +/** + * Represents an error where the item is not found in the item bank. + */ +public class ItemNotFoundInBankException extends Exception { +} diff --git a/src/main/java/seedu/duke/data/item/exercise/Exercise.java b/src/main/java/seedu/duke/data/item/exercise/Exercise.java new file mode 100644 index 0000000000..97740828ae --- /dev/null +++ b/src/main/java/seedu/duke/data/item/exercise/Exercise.java @@ -0,0 +1,95 @@ +package seedu.duke.data.item.exercise; + +import seedu.duke.data.item.Item; + +import java.time.LocalDate; + +public class Exercise extends Item { + public static final String EXERCISE_TYPE = "E"; + public static final String AT = " @ "; + protected LocalDate date; + + /** + * Constructor for exercise object when the date is not provided. + * + * @param name The name or description of the exercise. + * @param calories The calorie burnt from the exercise. + */ + public Exercise(String name, int calories) { + super(name, calories); + this.date = LocalDate.now(); + } + + /** + * Constructor for the exercise object when all attributes are provided. + * + * @param name The name or description of the exercise + * @param calories The calorie burnt from the exercise + * @param date The date when the exercise is taken + */ + public Exercise(String name, int calories, LocalDate date) { + super(name, calories); + this.date = date; + } + + + //====================Getter and Setter methods========================= + + /** + * Gets the date of the exercise taken. + * + * @return The date of the exercise done in LocalDate + */ + @Override + public LocalDate getDate() { + return date; + } + + /** + * Updates the date of the exercise taken. + * + * @param date The new date of the exercise done in LocalDate + */ + @Override + public void setDate(LocalDate date) { + this.date = date; + } + + + //====================To String methods========================= + + /** + * Converts the exercise item in string format which only shows the exercise item name description and calorie. + * + * @return The exercise item name and calorie display + */ + @Override + public String toStringWithoutDateAndTime() { + return super.toString(); + } + + /** + * Converts the Exercise item to string for printing after adding or deleting Exercise. + * + * @return String pattern of Food item with time and date + */ + public String toStringWithDate() { + return super.toString() + AT + this.getDate().format(DATE_FORMAT_FOR_PRINTING); + } + + + //====================Storage methods========================= + + /** + * Converts the exercise to external file string format. + * + * @return Name, calorie and date of the exercise in string + */ + @Override + public String toFileTextString() { + return EXERCISE_TYPE + + super.toFileTextString() + + FILE_TEXT_DELIMITER + + this.getDate().format(DATE_FORMATTER_FOR_STORAGE); + } +} diff --git a/src/main/java/seedu/duke/data/item/exercise/ExerciseList.java b/src/main/java/seedu/duke/data/item/exercise/ExerciseList.java new file mode 100644 index 0000000000..ac56025685 --- /dev/null +++ b/src/main/java/seedu/duke/data/item/exercise/ExerciseList.java @@ -0,0 +1,150 @@ +package seedu.duke.data.item.exercise; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.ItemList; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.stream.Collectors; + +public class ExerciseList extends ItemList { + public static final String MESSAGE_EXERCISE_DONE_PER_DAY = "You have done %d exercise(s) on %s (%s):"; + public static final String MESSAGE_TOTAL_CALORIE_BURNT_PER_DAY = "Total calories burnt in the day: %d cal"; + public static final String MESSAGE_TOTAL_CALORIE_BURNT_PER_WEEK = "Total calories burnt in this week: %d cal"; + public static final String MESSAGE_TOTAL_EXERCISE_DONE_PER_WEEK = "Total exercises done in this week: %d"; + + /** + * Default constructor for exercise list. + */ + public ExerciseList() { + internalItems = new ArrayList<>(); + } + + /** + * Deletes a food item according to its index number, date and time. + * + * @param index The index of the exercise as shown in the view e/ command + * @param date The date of the exercise taken + * @return The deleted exercise + */ + public Exercise deleteItem(int index, LocalDate date) { + Exercise deletedExercise = new Exercise("", 1, date); + int actualIndex = getActualIndex(index, deletedExercise); + deletedExercise = (Exercise) internalItems.remove(actualIndex); + return deletedExercise; + } + + /** + * Converts the entire exercise list to string format for printing purpose. + * + * @return The exercise list in a single string + */ + @Override + public String convertToString() { + StringBuilder exerciseListInString = extractExerciseListByEachDate(); + return exerciseListInString.toString().stripTrailing(); + } + + /** + * Converts the food list of a specific date to string format for printing purpose. + * + * @param date The date for the food list + * @return The food list of the specific date in a single string + */ + public String convertToStringBySpecificDate(LocalDate date) { + StringBuilder exerciseListInString = extractExerciseListBySpecificDate(date); + return exerciseListInString.toString().stripTrailing(); + } + + /** + * Sorts the exercise list in ascending format according to the date. + */ + public void sortList() { + this.internalItems.sort(Comparator.comparing(Item::getDate)); + } + + + //====================Private methods========================= + + /** + * Helper boolean method used in getActualIndex to determine if the exercise is the exercise to delete. + * + * @param deletedExercise The exercise to delete + * @param currentIndex The current index of the entire exercise list + * @param index The exercise index to delete as shown in view e/ + * @return True if the current exercise is the exercise to delete, false otherwise + */ + protected boolean isItemToDelete(Item deletedExercise, int currentIndex, int index) { + return internalItems.get(currentIndex + index).getDate().equals(deletedExercise.getDate()); + } + + /** + * Helper method used in getActualIndex to determine if the current index points to the correct exercise position. + * + * @param deletedExercise The exercise to delete + * @param currentIndex The current index of the entire exerciselist + * @return True if the current exercise has the same date and time period as the deletedItem, false otherwise + */ + protected boolean isListToQuery(Item deletedExercise, int currentIndex) { + return internalItems.get(currentIndex).getDate().equals(deletedExercise.getDate()); + } + + /** + * Helper function used in convertToString to extract exercises list + * according to each date presented in the entire exercise list. + * + * @return String which contains exercise lists with different date + */ + private StringBuilder extractExerciseListByEachDate() { + StringBuilder exerciseListInString = new StringBuilder(); + for (int index = 0; index < internalItems.size(); index++) { + LocalDate currentDate = internalItems.get(index).getDate(); + ExerciseList subList = new ExerciseList(); + while (index < internalItems.size() && currentDate.isEqual(internalItems.get(index).getDate())) { + subList.addItem(internalItems.get(index++)); + } + exerciseListInString.append(ITEM_LIST_DIVIDER).append(LS); + convertItemCountToString(exerciseListInString, + subList.getSize(), + currentDate, + MESSAGE_EXERCISE_DONE_PER_DAY); + for (int i = 1; i <= subList.getSize(); i++) { + convertItemToString(exerciseListInString, i, subList.getItem(i - 1)); + } + convertTotalCaloriesToString( + exerciseListInString, + this.getTotalCaloriesWithDate(currentDate), + MESSAGE_TOTAL_CALORIE_BURNT_PER_DAY); + index--; + } + exerciseListInString.append(ITEM_LIST_DIVIDER).append(LS); + exerciseListInString.append(String.format(MESSAGE_TOTAL_EXERCISE_DONE_PER_WEEK, getSize())).append(LS); + exerciseListInString.append(String.format(MESSAGE_TOTAL_CALORIE_BURNT_PER_WEEK, getTotalCalories())); + exerciseListInString.append(LS); + return exerciseListInString; + } + + /** + * Helper method used in convertToStringBySpecificDate for extracting + * exercise list which contains all the exercises done on the date. + * + * @param date The date to query all the exercises done + * @return StringBuilder type string which contains an exercise list with the given date + */ + private StringBuilder extractExerciseListBySpecificDate(LocalDate date) { + StringBuilder exerciseListInString = new StringBuilder(); + ArrayList subList = (ArrayList) this.internalItems.stream() + .filter(e -> e.getDate().isEqual(date)) + .collect(Collectors.toList()); + convertItemCountToString(exerciseListInString, subList.size(), date, MESSAGE_EXERCISE_DONE_PER_DAY); + for (int i = 1; i <= subList.size(); i++) { + convertItemToString(exerciseListInString, i, subList.get(i - 1)); + } + convertTotalCaloriesToString( + exerciseListInString, + this.getTotalCaloriesWithDate(date), + MESSAGE_TOTAL_CALORIE_BURNT_PER_DAY); + return exerciseListInString; + } +} diff --git a/src/main/java/seedu/duke/data/item/exercise/FutureExerciseList.java b/src/main/java/seedu/duke/data/item/exercise/FutureExerciseList.java new file mode 100644 index 0000000000..bc54fbd97a --- /dev/null +++ b/src/main/java/seedu/duke/data/item/exercise/FutureExerciseList.java @@ -0,0 +1,105 @@ +package seedu.duke.data.item.exercise; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.stream.Collectors; + +//@@author xingjie99 + +public class FutureExerciseList extends ExerciseList { + + private static final int ONE_DAY = 1; + private static final int ONE_WEEK = 7; + + /** + * Deletes and exercise item from the future exercise list. + * + * @param index Index of the exercise to be deleted. + * @return Exercise object removed. + */ + public Exercise deleteItem(int index) { + return (Exercise) internalItems.remove(index); + } + + /** + * Converts the entire future exercise list to string format for printing purpose. + * + * @return The future exercise list in a single string. + */ + @Override + public String convertToString() { + StringBuilder futureExerciseListToString = new StringBuilder(); + + for (int i = 0; i < internalItems.size(); i++) { + futureExerciseListToString + .append(TAB) + .append(i + 1) + .append(". ") + .append(internalItems.get(i)) + .append(" (") + .append(getDayOfWeek(internalItems.get(i).getDate())) + .append(" ") + .append(internalItems.get(i).getDate().format(DateTimeFormatter.ofPattern(DATE_FORMAT))) + .append(")") + .append(LS); + } + return futureExerciseListToString.toString().stripTrailing(); + } + + /** + * Adds all recurring exercises between two dates into the FutureExerciseList. + * + * @param description The name description of the recurring exercise + * @param calories The calorie burnt per each exercise + * @param startDate The starting date of the recurring exercise + * @param endDate The end date of the recurring exercise + * @param dayOfTheWeek The day(s) of the week for participating in the recurring exercise + */ + public void addRecurringExercises(String description, int calories, + LocalDate startDate, LocalDate endDate, ArrayList dayOfTheWeek) { + for (Integer day : dayOfTheWeek) { + int dayOfReoccurrence = startDate.getDayOfWeek().getValue(); + LocalDate currentDate = startDate; + while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) { + if (dayOfReoccurrence == day) { + super.addItem(new Exercise(description, calories, currentDate)); + currentDate = currentDate.plusDays(ONE_WEEK); + } else { + currentDate = currentDate.plusDays(ONE_DAY); + } + dayOfReoccurrence = currentDate.getDayOfWeek().getValue(); + } + super.sortList(); + } + } + + /** + * Deletes multiple items in the FutureExerciseList. + * + * @param itemIndexArray Array of indexes to delete from + * @throws IndexOutOfBoundsException Throws this exception if any of the index in the provided array does not exist + */ + @Override + public String deleteMultipleItems(ArrayList itemIndexArray) { + Collections.sort(itemIndexArray); + StringBuilder itemsToString = new StringBuilder(); + int numberPointer = 0; + for (Integer index : itemIndexArray) { + itemsToString.append(LS) + .append(TAB) + .append(index + 1) + .append(". ") + .append(getItem(index - numberPointer)) + .append(" (") + .append(getDayOfWeek(getItem(index - numberPointer).getDate())) + .append(" ") + .append(deleteItem(index - numberPointer).getDate() + .format(DateTimeFormatter.ofPattern(DATE_FORMAT))) + .append(")"); + numberPointer++; + } + return itemsToString.toString().stripTrailing(); + } +} diff --git a/src/main/java/seedu/duke/data/item/food/Food.java b/src/main/java/seedu/duke/data/item/food/Food.java new file mode 100644 index 0000000000..0e35fd1f1f --- /dev/null +++ b/src/main/java/seedu/duke/data/item/food/Food.java @@ -0,0 +1,201 @@ +package seedu.duke.data.item.food; + +import seedu.duke.data.item.Item; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class Food extends Item { + public static final String FOOD_TYPE = "F"; + private static final int EARLIEST_NIGHT_HOUR = 21; + private static final int LATEST_NIGHT_HOUR = 4; + private static final int EARLIEST_EVENING_HOUR = 17; + private static final int LATEST_EVENING_HOUR = 20; + private static final int EARLIEST_AFTERNOON_HOUR = 12; + private static final int LATEST_AFTERNOON_HOUR = 16; + private static final int EARLIEST_MORNING_HOUR = 5; + private static final int LATEST_MORNING_HOUR = 11; + public static final String MESSAGE_FOOD = "%s @ %s"; + protected LocalDateTime dateTime; + protected TimePeriod timePeriod; + + /** + * Constructor for the food object when the date and time are not provided. + * + * @param name The name or description of the food + * @param calories The calorie of the food consumed + */ + public Food(String name, int calories) { + super(name, calories); + this.dateTime = LocalDateTime.now(); //sets to current date and time + setTimePeriod(this.dateTime); + } + + /** + * Constructor for the food object when all attributes are provided. + * + * @param name The name or description of the food + * @param calories The calorie intake from the food + * @param dateTime The date and time when the food is consumed + */ + public Food(String name, int calories, LocalDateTime dateTime) { + super(name, calories); + this.dateTime = dateTime; + setTimePeriod(this.dateTime); + } + + //====================Getter and Setter methods========================= + + /** + * Gets the date and time of the food consumed. + * + * @return The date and time of the food consumed in LocalDateTime + */ + public LocalDateTime getDateTime() { + return dateTime; + } + + /** + * Updates the date and time of the food consumed. + * + * @param dateTime The new date and time of the food consumed in LocalDateTime + */ + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + setTimePeriod(this.dateTime); + } + + /** + * Gets the date of the food consumed. + * + * @return The date of the food consumed in LocalDate + */ + @Override + public LocalDate getDate() { + return this.dateTime.toLocalDate(); + } + + /** + * Updates the date of the food consumed. + * + * @param date The new date of the food consumed in LocalDate + */ + public void setDate(LocalDate date) { + LocalTime time = this.dateTime.toLocalTime(); + setDateTime(date.atTime(time)); + } + + /** + * Gets the time of the food consumed. + * + * @return The time of the food consumed in LocalTime + */ + public LocalTime getTime() { + return this.dateTime.toLocalTime(); + } + + /** + * Updates the time of the food consumed. + * + * @param time The new time of the food consumed + */ + public void setTime(LocalTime time) { + LocalDate date = this.dateTime.toLocalDate(); + setDateTime(date.atTime(time)); + } + + /** + * Gets the time period of the food consumed. + * + * @return TimePeriod enum which represents the time period + */ + public TimePeriod getTimePeriod() { + return timePeriod; + } + + //====================To String methods========================= + + /** + * Converts the Food item to string for printing in the Food List. + * + * @return String pattern of Food item with time but no date + */ + @Override + public String toString() { + return String.format(MESSAGE_FOOD, super.toString(), this.getTime().format(TIME_FORMATTER)); + } + + /** + * Converts the food item in string format which only shows the food item name description and calorie. + * + * @return The food item name and calorie display + */ + @Override + public String toStringWithoutDateAndTime() { + return super.toString(); + } + + /** + * Converts the Food item to string for printing after adding or deleting food. + * + * @return String pattern of Food item with time and date + */ + public String toStringWithDate() { + return this + ", " + this.getDate().format(DATE_FORMAT_FOR_PRINTING); + } + + + //====================Storage methods========================= + + /** + * Converts the food to external file string format. + * + * @return Name, calorie, date and time of the food in string + */ + @Override + public String toFileTextString() { + return FOOD_TYPE + + super.toFileTextString() + + FILE_TEXT_DELIMITER + + this.getDateTime().format(DATE_TIME_FORMATTER); + } + + //====================Private methods========================= + + /** + * Sets or updates time period according to the time provided in dateTime. + * + * @param dateTime Date and time provided + */ + private void setTimePeriod(LocalDateTime dateTime) { + if (isMorning(dateTime)) { + this.timePeriod = TimePeriod.MORNING; + } else if (isAfternoon(dateTime)) { + this.timePeriod = TimePeriod.AFTERNOON; + } else if (isEvening(dateTime)) { + this.timePeriod = TimePeriod.EVENING; + } else if (isNight(dateTime)) { + this.timePeriod = TimePeriod.NIGHT; + } else { + this.timePeriod = null; + } + } + + private boolean isNight(LocalDateTime dateTime) { + return dateTime.getHour() >= EARLIEST_NIGHT_HOUR || dateTime.getHour() <= LATEST_NIGHT_HOUR; + } + + private boolean isEvening(LocalDateTime dateTime) { + return dateTime.getHour() >= EARLIEST_EVENING_HOUR && dateTime.getHour() <= LATEST_EVENING_HOUR; + } + + private boolean isAfternoon(LocalDateTime dateTime) { + return dateTime.getHour() >= EARLIEST_AFTERNOON_HOUR && dateTime.getHour() <= LATEST_AFTERNOON_HOUR; + } + + private boolean isMorning(LocalDateTime dateTime) { + return dateTime.getHour() >= EARLIEST_MORNING_HOUR && dateTime.getHour() <= LATEST_MORNING_HOUR; + } + +} diff --git a/src/main/java/seedu/duke/data/item/food/FoodList.java b/src/main/java/seedu/duke/data/item/food/FoodList.java new file mode 100644 index 0000000000..791915de27 --- /dev/null +++ b/src/main/java/seedu/duke/data/item/food/FoodList.java @@ -0,0 +1,334 @@ +package seedu.duke.data.item.food; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.ItemList; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.stream.Collectors; + +public class FoodList extends ItemList { + public static final String MESSAGE_FOOD_CONSUMED = "You have consumed %d food item(s) on %s (%s):"; + public static final String MESSAGE_TOTAL_CALORIE_CONSUMED_PER_DAY = "Total calories consumed in the day: %d cal"; + public static final String MESSAGE_TOTAL_CALORIE_CONSUMED_PER_WEEK = "Total calories consumed in this week: %d cal"; + public static final String MESSAGE_MORNING = "In the morning:"; + public static final String MESSAGE_AFTERNOON = "In the afternoon:"; + public static final String MESSAGE_EVENING = "In the evening:"; + public static final String MESSAGE_NIGHT = "At night:"; + public static final String MESSAGE_NO_FOOD_IN_DATE_TIME = + "There is no food item found by the given date and time period"; + public static final String MESSAGE_NO_FOOD_IN_DATE = "There is no food item found by the given date"; + public static final String MESSAGE_TOTAL_FOOD_CONSUMED_PER_WEEK = "Total number of food consumed in this week: %d"; + + /** + * Default constructor for food list. + */ + public FoodList() { + internalItems = new ArrayList<>(); + } + + /** + * Deletes a food item according to its index number, date and time. + * + * @param index The index of the food item as shown in the view f/ command + * @param date The date of the food item consumed + * @param time The time of the food item consumed + * @return The deleted food item + */ + public Food deleteItem(int index, LocalDate date, LocalTime time) { + LocalDateTime dateTime = date.atTime(time); + Food deletedFood = new Food("", 0, dateTime); //constructs food object to get the time period + int actualIndex = getActualIndex(index, deletedFood); + //actualIndex is set to -1 if the provided index is not correct + deletedFood = (Food) internalItems.remove(actualIndex); + return deletedFood; + } + + /** + * Sorts the food list in ascending format according to the date and time. + */ + @Override + public void sortList() { + this.internalItems.sort(Comparator.comparing(Item::getDateTime)); + } + + /** + * Counts the number of food items consumed at night in the week. + * + * @return The integer value count which indicates the number of food items consumed at night + */ + public int getSupperCount() { + return (int) internalItems.stream().filter(f -> f.getTimePeriod().equals(TimePeriod.NIGHT)).count(); + } + + //====================Printing methods========================= + + /** + * Converts the entire food list to string format for printing purpose. + * + * @return The food list in a single string + */ + @Override + public String convertToString() { + StringBuilder foodListInString = extractFoodListByEachDateAndTimePeriod(); + return foodListInString.toString().stripTrailing(); + } + + /** + * Converts the food list of a specific date to string format for printing purpose. + * + * @param date The date for the food list + * @return The food list of the specific date in a single string + */ + @Override + public String convertToStringBySpecificDate(LocalDate date) { + StringBuilder foodListInString = extractFoodListBySpecificDate(date); + return foodListInString.toString().stripTrailing(); + } + + /** + * Extracts a food list according to the given date and time period and converts it to string for printing purpose. + * + * @param date The date given to query the food list + * @param timePeriod The time period given to query the food list + * @return The food list of the specific date and time period in a single string + */ + public String convertToStringBySpecificDateAndTime(LocalDate date, TimePeriod timePeriod) { + StringBuilder foodListInString = new StringBuilder(); + ArrayList subList = filterListAccordingToDateAndTimePeriod(date, timePeriod); + if (subList.size() == 0) { + foodListInString + .append(MESSAGE_NO_FOOD_IN_DATE_TIME) + .append(ItemList.LS); + return foodListInString.toString().stripTrailing(); + } + processListToString(date, timePeriod, foodListInString, subList); + return foodListInString.toString().stripTrailing(); + } + + + //====================Private methods========================= + + /** + * Helper method used in getActualIndex to determine if the current index points to the correct food position. + * + * @param deletedFood The food item to delete + * @param currentIndex The current index of the entire food list + * @return True if the current food item has the same date and time period as the deletedFood, false otherwise + */ + protected boolean isListToQuery(Item deletedFood, int currentIndex) { + boolean isSameDate = internalItems.get(currentIndex).getDate().equals(deletedFood.getDate()); + boolean isSameTimePeriod = internalItems.get(currentIndex).getTimePeriod().equals(deletedFood.getTimePeriod()); + return isSameDate && isSameTimePeriod; + } + + /** + * Helper boolean method used in getActualIndex to determine if the food item is the food to delete. + * + * @param deletedFood The food item to delete + * @param currentIndex The current index of the entire food list + * @param index The food index to delete as shown in view f/ + * @return True if the current food item is the food to delete, false otherwise + */ + protected boolean isItemToDelete(Item deletedFood, int currentIndex, int index) { + return internalItems.get(currentIndex + index).getTimePeriod().equals(deletedFood.getTimePeriod()); + } + + /** + * Helper method used in convertToStringBySpecificDateAndTime to append strings to the string builder. + * + * @param date The date to query the food list + * @param timePeriod The time period to query the food list + * @param foodListInString The StringBuilder food list which contains the correct output string + * @param subList The array list of food items that contains all the food items with same date and time + * period as the given date and timePeriod + */ + private void processListToString(LocalDate date, TimePeriod timePeriod, + StringBuilder foodListInString, ArrayList subList) { + FoodList timePeriodList = new FoodList(); + for (Item f : subList) { + if (f.getTimePeriod().equals(timePeriod)) { + timePeriodList.addItem(f); + } + } + convertItemCountToString(foodListInString, subList.size(), date, MESSAGE_FOOD_CONSUMED); + addTimePeriodMessage(timePeriod, foodListInString); + for (int i = 1; i <= timePeriodList.getSize(); i++) { + convertItemToString(foodListInString, i, timePeriodList.getItem(i - 1)); + } + convertTotalCaloriesToString( + foodListInString, + subList.stream().mapToInt(Item::getCalories).sum(), + MESSAGE_TOTAL_CALORIE_CONSUMED_PER_DAY); + } + + /** + * Helper method used in convertToStringBySpecificDateAndTime to filter the original food list. + * + * @param date The date to query the food list + * @param timePeriod The time period to query the food list + * @return The array list which contains food items with same date and time period as provided + */ + private ArrayList filterListAccordingToDateAndTimePeriod(LocalDate date, TimePeriod timePeriod) { + return (ArrayList) this.internalItems.stream() + .filter(f -> f.getDate().isEqual(date) && f.getTimePeriod().equals(timePeriod)) + .collect(Collectors.toList()); + } + + /** + * Helper method used in processListToString to retrieve the relevant message to the provided time period. + * + * @param timePeriod The time period to query the food list + * @param foodListInString The StringBuilder food list which contains the correct output string + */ + private void addTimePeriodMessage(TimePeriod timePeriod, StringBuilder foodListInString) { + switch (timePeriod) { + case MORNING: + foodListInString.append(MESSAGE_MORNING).append(ItemList.LS); + break; + case AFTERNOON: + foodListInString.append(MESSAGE_AFTERNOON).append(ItemList.LS); + break; + case EVENING: + foodListInString.append(MESSAGE_EVENING).append(ItemList.LS); + break; + case NIGHT: + foodListInString.append(MESSAGE_NIGHT).append(ItemList.LS); + break; + default: + } + } + + /** + * Helper method used in convertToString for extracting each food list + * according to the date and time presented in the entire food list. + * + * @return StringBuilder type string which contains food lists with different date and time + */ + private StringBuilder extractFoodListByEachDateAndTimePeriod() { + if (getSize() == 0) { + return new StringBuilder(); + } + StringBuilder foodListInString = new StringBuilder(); //declares as StringBuilder for mutable String object + for (int index = 0; index < internalItems.size(); index++) { + LocalDate currentDate = internalItems.get(index).getDate(); + FoodList subList = new FoodList(); + while (index < internalItems.size() && currentDate.isEqual(internalItems.get(index).getDate())) { + subList.addItem(internalItems.get(index++)); + } + assert subList.getSize() > 0 : "Sub list should not be empty."; + foodListInString.append(ITEM_LIST_DIVIDER).append(LS); + convertItemCountToString(foodListInString, subList.getSize(), currentDate, MESSAGE_FOOD_CONSUMED); + separateDifferentTimePeriodFoodList(foodListInString, subList); + convertTotalCaloriesToString( + foodListInString, + this.getTotalCaloriesWithDate(currentDate), + MESSAGE_TOTAL_CALORIE_CONSUMED_PER_DAY); + index--; //prevents double adding of index + } + foodListInString.append(ITEM_LIST_DIVIDER).append(LS); + foodListInString.append(String.format(MESSAGE_TOTAL_FOOD_CONSUMED_PER_WEEK, getSize())).append(LS); + foodListInString.append(String.format(MESSAGE_TOTAL_CALORIE_CONSUMED_PER_WEEK, getTotalCalories())); + foodListInString.append(LS); + return foodListInString; + } + + /** + * Helper method used in extractFoodListByEachDateAndTimePeriod to extract food list + * on the same date according to its different time period. + * + * @param foodListInString The StringBuilder food list. + * @param subList FoodList which contains only the food items for the same date + */ + private void separateDifferentTimePeriodFoodList(StringBuilder foodListInString, FoodList subList) { + FoodList morningList = new FoodList(); + FoodList afternoonList = new FoodList(); + FoodList eveningList = new FoodList(); + FoodList midnightList = new FoodList(); + extractFoodListByEachTimePeriod(subList, morningList, afternoonList, eveningList, midnightList); + appendWithList(foodListInString, morningList, MESSAGE_MORNING); + appendWithList(foodListInString, afternoonList, MESSAGE_AFTERNOON); + appendWithList(foodListInString, eveningList, MESSAGE_EVENING); + appendWithList(foodListInString, midnightList, MESSAGE_NIGHT); + } + + /** + * Helper method used in separateDifferentTimePeriodFoodList to append + * the StringBuilder foodListInString with each time period food list. + * + * @param foodListInString The StringBuilder food list which contains the correct output string + * @param timePeriodList The food list that contains food items that consumed within the time period + * @param periodMessage The message to indicate the time period + */ + private void appendWithList(StringBuilder foodListInString, FoodList timePeriodList, String periodMessage) { + if (timePeriodList.getSize() > 0) { + foodListInString.append(periodMessage).append(ItemList.LS); + for (int i = 1; i <= timePeriodList.getSize(); i++) { + convertItemToString(foodListInString, i, timePeriodList.getItem(i - 1)); + } + } + } + + /** + * Helper method used in separateDifferentTimePeriodFoodList to extract the larger subList into 4 smaller lists + * which contain food items with same date according to the time period. + * + * @param subList The food list that contains all the food items consumed on the same date + * @param morningList The food list that contains all the food items consumed in the morning of a given date + * @param afternoonList The food list that contains all the food items consumed in the afternoon of a given date + * @param eveningList The food list that contains all the food items consumed in the evening of a given date + * @param nightList The food list that contains all the food items consumed at night of a given date + */ + private void extractFoodListByEachTimePeriod(FoodList subList, FoodList morningList, FoodList afternoonList, + FoodList eveningList, FoodList nightList) { + for (int i = 1; i <= subList.getSize(); i++) { + switch (subList.getItem(i - 1).getTimePeriod()) { + case MORNING: + morningList.addItem(subList.getItem(i - 1)); + break; + case AFTERNOON: + afternoonList.addItem(subList.getItem(i - 1)); + break; + case EVENING: + eveningList.addItem(subList.getItem(i - 1)); + break; + case NIGHT: + nightList.addItem(subList.getItem(i - 1)); + break; + default: + } + } + } + + /** + * Helper method used in convertToStringBySpecificDate for extracting + * food list which contains all the food item consumed on the date. + * + * @param date The date to query all the food items consumed + * @return StringBuilder type string which contains a food list with the given date + */ + private StringBuilder extractFoodListBySpecificDate(LocalDate date) { + StringBuilder foodListInString = new StringBuilder(); //declares as StringBuilder for mutable String object + ArrayList subList = (ArrayList) this.internalItems.stream() + .filter(f -> f.getDate().isEqual(date)) + .collect(Collectors.toList()); + if (subList.size() == 0) { + foodListInString + .append(MESSAGE_NO_FOOD_IN_DATE) + .append(ItemList.LS); + return foodListInString; + } + convertItemCountToString(foodListInString, subList.size(), date, MESSAGE_FOOD_CONSUMED); + for (int i = 1; i <= subList.size(); i++) { + convertItemToString(foodListInString, i, subList.get(i - 1)); + } + convertTotalCaloriesToString( + foodListInString, + this.getTotalCaloriesWithDate(date), + MESSAGE_TOTAL_CALORIE_CONSUMED_PER_DAY); + return foodListInString; + } +} diff --git a/src/main/java/seedu/duke/data/item/food/TimePeriod.java b/src/main/java/seedu/duke/data/item/food/TimePeriod.java new file mode 100644 index 0000000000..2cebcf3ef6 --- /dev/null +++ b/src/main/java/seedu/duke/data/item/food/TimePeriod.java @@ -0,0 +1,5 @@ +package seedu.duke.data.item.food; + +public enum TimePeriod { + MORNING, AFTERNOON, EVENING, NIGHT +} diff --git a/src/main/java/seedu/duke/data/profile/Profile.java b/src/main/java/seedu/duke/data/profile/Profile.java new file mode 100644 index 0000000000..1ab8ed106c --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/Profile.java @@ -0,0 +1,295 @@ +package seedu.duke.data.profile; + +import seedu.duke.data.profile.attributes.ActivityFactor; +import seedu.duke.data.profile.attributes.Age; +import seedu.duke.data.profile.attributes.CalorieGoal; +import seedu.duke.data.profile.attributes.Gender; +import seedu.duke.data.profile.attributes.Height; +import seedu.duke.data.profile.attributes.Name; +import seedu.duke.data.profile.attributes.Weight; + +/** + * Profile that contains the relevant details input by user. + */ +public class Profile { + + public static final String FILE_TEXT_DELIMITER = "|"; + + private static final String LS = System.lineSeparator(); + private static final String TAB = "\t"; + private static final String INDENTED_LS = LS + TAB; + + public static final String MESSAGE_PROFILE = "Hello %1$s! This is your profile:" + LS + + "*===========================================================" + + INDENTED_LS + "Height %2$scm" + + INDENTED_LS + "Weight %3$skg" + + INDENTED_LS + "Gender %4$s" + + INDENTED_LS + "Age %5$s" + + INDENTED_LS + "Calorie Goal %6$s cal" + + INDENTED_LS + "Activity Factor %7$s" + + LS + "===========================================================*"; + + protected Name name; + protected Height height; + protected Weight weight; + protected Gender gender; + protected Age age; + protected CalorieGoal calorieGoal; + protected ActivityFactor activityFactor; + + /** + * Initializing a new Profile class with empty attributes. + */ + public Profile() { + name = new Name(); + height = new Height(); + weight = new Weight(); + gender = new Gender(); + age = new Age(); + calorieGoal = new CalorieGoal(); + activityFactor = new ActivityFactor(); + } + + /** + * Constructor for the Profile class. + * + * @param name Name of user + * @param height Height of user + * @param weight Weight of user + * @param calorieGoal Calorie target of user + * @param gender Gender of user (M/F) + * @param age Age of user + * @param activityFactor Activity level of user + */ + public Profile(Name name, Height height, Weight weight, Gender gender, + Age age, CalorieGoal calorieGoal, ActivityFactor activityFactor) { + this.name = name; + this.height = height; + this.weight = weight; + this.gender = gender; + this.age = age; + this.calorieGoal = calorieGoal; + this.activityFactor = activityFactor; + } + + /** + * A set command that enables setting of profile through passing by reference. + * + * @param name Name of user + * @param height Height of user + * @param weight Weight of user + * @param calorieGoal Calorie target of user + * @param gender Gender of user (M/F) + * @param age Age of user + * @param activityFactor Activity level of user + */ + public void setProfile(Name name, Height height, Weight weight, Gender gender, + Age age, CalorieGoal calorieGoal, ActivityFactor activityFactor) { + this.name = name; + this.height = height; + this.weight = weight; + this.gender = gender; + this.age = age; + this.calorieGoal = calorieGoal; + this.activityFactor = activityFactor; + } + + /** + * Sets the profile in various commands with the raw inputs if necessary. + * Usually used for retrieving data from storage. + * + * @param name Name of user + * @param height Height of user + * @param weight Weight of user + * @param calorieGoal Calorie target of user + * @param gender Gender of user (M/F) + * @param age Age of user + * @param activityFactor Activity level of user + */ + public void setProfileWithRawInputs(String name, double height, double weight, + char gender, int age, int calorieGoal, int activityFactor) { + this.name.setName(name); + this.height.setHeight(height); + this.weight.setWeight(weight); + this.gender.setGender(gender); + this.age.setAge(age); + this.calorieGoal.setCalorieGoal(calorieGoal); + this.activityFactor.setUserInput(activityFactor); + } + + /** + * Sets the profile name with a new Name object. + * + * @param name Name object to be set + */ + public void setProfileName(Name name) { + this.name = name; + } + + /** + * Sets the profile height with a new Height object. + * + * @param height Height object to be set + */ + public void setProfileHeight(Height height) { + this.height = height; + } + + /** + * Sets the profile weight with a new Weight object. + * + * @param weight Weight object to be set + */ + public void setProfileWeight(Weight weight) { + this.weight = weight; + } + + /** + * Sets the profile gender with a new Profile object. + * + * @param gender Gender object to be set + */ + public void setProfileGender(Gender gender) { + this.gender = gender; + } + + /** + * Sets the profile age with a new Age object. + * + * @param age Age object to be set + */ + public void setProfileAge(Age age) { + this.age = age; + } + + /** + * Sets the profile calorie goal with a new CalorieGoal object. + * + * @param calorieGoal CalorieGoal object to be set + */ + public void setProfileCalorieGoal(CalorieGoal calorieGoal) { + this.calorieGoal = calorieGoal; + } + + /** + * Sets the profile activity factor with a new ActivityFactor object. + * + * @param activityFactor ActivityFactor object to be set + */ + public void setProfileActivityFactor(ActivityFactor activityFactor) { + this.activityFactor = activityFactor; + } + + /** + * Retrieves the Name object from the profile. + * + * @return Name object + */ + public Name getProfileName() { + return this.name; + } + + /** + * Retrieves the Height object from the profile. + * + * @return Height object + */ + public Height getProfileHeight() { + return this.height; + } + + /** + * Retrieves the Weight object from the profile. + * + * @return Weight object + */ + public Weight getProfileWeight() { + return this.weight; + } + + /** + * Retrieves the Gender object from the profile. + * + * @return Gender object + */ + public Gender getProfileGender() { + return this.gender; + } + + /** + * Retrieves the Age object from the profile. + * + * @return Age object + */ + public Age getProfileAge() { + return this.age; + } + + /** + * Retrieves the CalorieGoal object from the profile. + * + * @return CalorieGoal object + */ + public CalorieGoal getProfileCalorieGoal() { + return this.calorieGoal; + } + + /** + * Retrieves the ActivityFactor object from the profile. + * + * @return ActivityFactor object + */ + public ActivityFactor getProfileActivityFactor() { + return this.activityFactor; + } + + /** + * Converts the Profile to a String for printing purposes. + * + * @return Formatted String with all Profile attributes. + */ + public String convertToString() { + return String.format(MESSAGE_PROFILE, + this.name.getName(), + this.height.getHeight(), + this.weight.getWeight(), + this.gender.getGender(), + this.age.getAge(), + this.calorieGoal.getCalorieGoal(), + this.activityFactor.getUserInput()); + } + + /** + * Converts the file into a string that is used for storage. + * + * @return String that is used for storage. + */ + public String toFileTextString() { + return name.getName() + FILE_TEXT_DELIMITER + height.getHeight() + FILE_TEXT_DELIMITER + + weight.getWeight() + FILE_TEXT_DELIMITER + gender.getGender() + FILE_TEXT_DELIMITER + + age.getAge() + FILE_TEXT_DELIMITER + calorieGoal.getCalorieGoal() + FILE_TEXT_DELIMITER + + activityFactor.getUserInput(); + } + + /** + * Check if all attributes of profile are valid. + * + * @return false if at least one of the profile attributes are invalid. + */ + public boolean checkProfileComplete() { + return name.isValid() && height.isValid() && weight.isValid() && gender.isValid() + && age.isValid() && activityFactor.isValid() && calorieGoal.isValid(); + } + + /** + * Check if any of profile attributes is valid. + * If all profile attributes are incorrect, it will be deemed as profile not present. + * + * @return true if at least one of the profile attributes is valid. + */ + public boolean checkProfilePresent() { + return name.isValid() || height.isValid() || weight.isValid() || gender.isValid() + || age.isValid() || activityFactor.isValid(); + } + +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/ActivityFactor.java b/src/main/java/seedu/duke/data/profile/attributes/ActivityFactor.java new file mode 100644 index 0000000000..26712465a1 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/ActivityFactor.java @@ -0,0 +1,73 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; +import seedu.duke.data.profile.utilities.ActivityLevel; + +/** + * Activity Factor attribute of profile. + */ +public class ActivityFactor implements Verifiable { + + public static final int LIMIT_LOWER_ACTIVITY_FACTOR = 1; + public static final int LIMIT_UPPER_ACTIVITY_FACTOR = 5; + + protected int userInput; + + public ActivityFactor() { + + } + + /** + * Constructs an activity factor object. + * + * @param activityFactor activity factor input by user + */ + public ActivityFactor(int activityFactor) { + setUserInput(activityFactor); + } + + /** + * Obtains a ActivityLevel object depending on the activity factor. + * + * @return ActivityLevel along with its associated factor. + */ + public ActivityLevel getActivityLevel() { + switch (userInput) { + case 1: + return ActivityLevel.SEDENTARY; + case 2: + return ActivityLevel.LIGHT; + case 3: + return ActivityLevel.MODERATE; + case 4: + return ActivityLevel.INTENSE; + case 5: + return ActivityLevel.EXTREME; + default: + return ActivityLevel.DEFAULT; + } + } + + /** + * Retrieves the activity factor of ActivityFactorobject. + * + * @return the activity factor of ActivityFactor object + */ + public int getUserInput() { + return userInput; + } + + /** + * Sets the activity factor of ActivityFactor object. + * + * @param userInput activity factor input by user + */ + public void setUserInput(int userInput) { + this.userInput = userInput; + } + + @Override + public boolean isValid() { + return userInput >= LIMIT_LOWER_ACTIVITY_FACTOR && userInput <= LIMIT_UPPER_ACTIVITY_FACTOR; + } +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/Age.java b/src/main/java/seedu/duke/data/profile/attributes/Age.java new file mode 100644 index 0000000000..27ca5c4202 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/Age.java @@ -0,0 +1,50 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; + +/** + * Age attribute of profile. + */ +public class Age implements Verifiable { + + public static final int LOWER_AGE_LIMIT = 10; + public static final int UPPER_AGE_LIMIT = 150; + + protected int age; + + public Age() { + + } + + /** + * Constructs an age object. + * + * @param age age input by user + */ + public Age(int age) { + setAge(age); + } + + /** + * Retrieves age of Age object. + * + * @return age of Age object + */ + public int getAge() { + return age; + } + + /** + * Sets the age of Age object. + * + * @param age age input by user + */ + public void setAge(int age) { + this.age = age; + } + + @Override + public boolean isValid() { + return age >= LOWER_AGE_LIMIT && age <= UPPER_AGE_LIMIT; + } +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/CalorieGoal.java b/src/main/java/seedu/duke/data/profile/attributes/CalorieGoal.java new file mode 100644 index 0000000000..bf58c2d554 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/CalorieGoal.java @@ -0,0 +1,55 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; + +/** + * Calorie Goal attribute of profile. + */ +public class CalorieGoal implements Verifiable { + + private static final String LS = System.lineSeparator(); + + public static final int LIMIT_UPPER_CALORIES = 10000; + public static final int LIMIT_LOWER_CALORIES = -3000; + + protected int calorieGoal; + + public CalorieGoal() { + + } + + /** + * Constructs a calorie goal object. + * + * @param calorieGoal goal input by user + */ + public CalorieGoal(int calorieGoal) { + setCalorieGoal(calorieGoal); + } + + /** + * Retrieves the calorie goal of CalorieGoal object. + * + * @return the calorie goal stored in CalorieGoal object + */ + public int getCalorieGoal() { + return calorieGoal; + } + + /** + * Sets the calorie goal for CalorieGoal object. + * + * @param calorieGoal calorie goal input by user + */ + public void setCalorieGoal(int calorieGoal) { + this.calorieGoal = calorieGoal; + } + + @Override + public boolean isValid() { + if (calorieGoal < LIMIT_LOWER_CALORIES || calorieGoal > LIMIT_UPPER_CALORIES) { + return false; + } + return true; + } +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/Gender.java b/src/main/java/seedu/duke/data/profile/attributes/Gender.java new file mode 100644 index 0000000000..175cd278d2 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/Gender.java @@ -0,0 +1,52 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; + +/** + * Gender attribute of profile. + */ +public class Gender implements Verifiable { + + private static final char GENDER_M = 'M'; + private static final char GENDER_F = 'F'; + + protected char gender; + + public Gender() { + + } + + /** + * Constructs a gender object. + * + * @param gender gender input by user. + */ + public Gender(char gender) { + this.gender = Character.toUpperCase(gender); + } + + /** + * Retrieves the gender of the Gender object. + * + * @return the gender of the gender object. + */ + public char getGender() { + return gender; + } + + /** + * Sets the gender of the Gender object. + * + * @param gender gender input by user + */ + public void setGender(char gender) { + this.gender = Character.toUpperCase(gender); + } + + @Override + public boolean isValid() { + return gender == GENDER_F || gender == GENDER_M; + } + + +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/Height.java b/src/main/java/seedu/duke/data/profile/attributes/Height.java new file mode 100644 index 0000000000..4b967a4434 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/Height.java @@ -0,0 +1,51 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; + +/** + * Height attribute of Profile. + */ +public class Height implements Verifiable { + + public static final int LOWER_HEIGHT_LIMIT = 1; + public static final int UPPER_HEIGHT_LIMIT = 300; + + protected double height; + + public Height() { + + } + + /** + * Constructs a height object. + * + * @param height height input by user. + */ + public Height(double height) { + setHeight(height); + } + + /** + * Retrieves the height of Height object. + * + * @return the height of Height object + */ + public double getHeight() { + return height; + } + + /** + * Sets the height for Height object. + * + * @param height height input by user + */ + public void setHeight(double height) { + this.height = height; + } + + @Override + public boolean isValid() { + return height >= LOWER_HEIGHT_LIMIT && height <= UPPER_HEIGHT_LIMIT; + } + +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/Name.java b/src/main/java/seedu/duke/data/profile/attributes/Name.java new file mode 100644 index 0000000000..0e023de9df --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/Name.java @@ -0,0 +1,58 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; + +/** + * Name attribute of profile. + */ +public class Name implements Verifiable { + + protected String name; + private static final String EMPTY_STRING = ""; + + public Name() { + + } + + /** + * Constructs a name object. + * + * @param name name input of user + */ + public Name(String name) { + setName(name.trim()); + } + + /** + * Retrieves the name from Name object. + * + * @return name of Name object + */ + public String getName() { + return name; + } + + /** + * Sets the name for the Name Object. + * + * @param name name input by the user + */ + public void setName(String name) { + this.name = name.trim(); + } + + @Override + public boolean isValid() { + if (name == null || name.trim().equals(EMPTY_STRING)) { + return false; + } + for (int i = 0; i < name.length(); i++) { + if (name.charAt(i) == '|' || name.charAt(i) == '/') { + return false; + } + } + return true; + } + + +} diff --git a/src/main/java/seedu/duke/data/profile/attributes/Weight.java b/src/main/java/seedu/duke/data/profile/attributes/Weight.java new file mode 100644 index 0000000000..3c3d40aceb --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/attributes/Weight.java @@ -0,0 +1,50 @@ +package seedu.duke.data.profile.attributes; + +import seedu.duke.data.Verifiable; + +/** + * Weight attribute of profile. + */ +public class Weight implements Verifiable { + + public static final int LOWER_WEIGHT_LIMIT = 1; + public static final int UPPER_WEIGHT_LIMIT = 300; + + protected double weight; + + public Weight() { + + } + + /** + * Constructs a weight object. + * + * @param weight weight input by user + */ + public Weight(double weight) { + setWeight(weight); + } + + /** + * Retrieves the weight from the Weight object. + * + * @return the weight of Weight object + */ + public double getWeight() { + return weight; + } + + /** + * Sets the weight of the Weight object. + * + * @param weight weight input by user + */ + public void setWeight(double weight) { + this.weight = weight; + } + + @Override + public boolean isValid() { + return weight >= LOWER_WEIGHT_LIMIT && weight <= UPPER_WEIGHT_LIMIT; + } +} diff --git a/src/main/java/seedu/duke/data/profile/exceptions/InvalidCharacteristicException.java b/src/main/java/seedu/duke/data/profile/exceptions/InvalidCharacteristicException.java new file mode 100644 index 0000000000..ff0067d986 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/exceptions/InvalidCharacteristicException.java @@ -0,0 +1,11 @@ +package seedu.duke.data.profile.exceptions; + +/** + * Exception that is thrown when the input human characteristics are invalid. + */ +public class InvalidCharacteristicException extends Exception { + + public InvalidCharacteristicException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/data/profile/utilities/ActivityLevel.java b/src/main/java/seedu/duke/data/profile/utilities/ActivityLevel.java new file mode 100644 index 0000000000..4540c0b84a --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/utilities/ActivityLevel.java @@ -0,0 +1,38 @@ +package seedu.duke.data.profile.utilities; + +/** + * Different levels of activity depending on the ActivityFactor in Profile. + */ +public enum ActivityLevel { + + DEFAULT(Constants.FACTOR_DEFAULT), + SEDENTARY(Constants.FACTOR_SEDENTARY), + LIGHT(Constants.FACTOR_LIGHT), + MODERATE(Constants.FACTOR_MODERATE), + INTENSE(Constants.FACTOR_INTENSE), + EXTREME(Constants.FACTOR_EXTREME); + + private final double factor; + + ActivityLevel(double factor) { + this.factor = factor; + } + + /** + * Returns the constant associated with the level of activity. + * + * @return the factor depending on level of activity + */ + public double getFactor() { + return factor; + } + + private static class Constants { + public static final double FACTOR_DEFAULT = 1.0; + public static final double FACTOR_SEDENTARY = 1.2; + public static final double FACTOR_LIGHT = 1.375; + public static final double FACTOR_MODERATE = 1.55; + public static final double FACTOR_INTENSE = 1.725; + public static final double FACTOR_EXTREME = 1.9; + } +} diff --git a/src/main/java/seedu/duke/data/profile/utilities/ProfileUtils.java b/src/main/java/seedu/duke/data/profile/utilities/ProfileUtils.java new file mode 100644 index 0000000000..8a9e3c6875 --- /dev/null +++ b/src/main/java/seedu/duke/data/profile/utilities/ProfileUtils.java @@ -0,0 +1,162 @@ +package seedu.duke.data.profile.utilities; + +import seedu.duke.data.profile.Profile; +import seedu.duke.data.profile.attributes.ActivityFactor; +import seedu.duke.data.profile.attributes.Age; +import seedu.duke.data.profile.attributes.CalorieGoal; +import seedu.duke.data.profile.attributes.Height; +import seedu.duke.data.profile.attributes.Weight; +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; + +/** + * Utilities that can be used for the Profile class. + */ +public class ProfileUtils { + + private static final String LS = System.lineSeparator(); + + public static final int NON_POSITIVE_LIMIT = 0; + + private static final String BMI_STATUS_UNDERWEIGHT = "Underweight"; + private static final String BMI_STATUS_HEALTHY = "Healthy"; + private static final String BMI_STATUS_OVERWEIGHT = "Overweight"; + private static final String BMI_STATUS_OBESE = "Obese"; + public static final double BMI_LIMIT_UNDERWEIGHT = 18.5; + public static final double BMI_LIMIT_HEALTHY = 24.9; + public static final double BMI_LIMIT_OVERWEIGHT = 29.9; + + private static final char GENDER_M = 'M'; + private static final char GENDER_F = 'F'; + + public static final double GENDER_M_WEIGHT_FACTOR = 13.397; + public static final double GENDER_M_HEIGHT_FACTOR = 4.799; + public static final double GENDER_M_AGE_FACTOR = 5.677; + public static final double GENDER_M_CONSTANT = 88.362; + public static final double GENDER_F_WEIGHT_FACTOR = 9.247; + public static final double GENDER_F_HEIGHT_FACTOR = 3.098; + public static final double GENDER_F_AGE_FACTOR = 4.330; + public static final double GENDER_F_CONSTANT = 447.593; + + public static final String ERROR_NAME = "Try not to use / and | in your name as it can make our commands invalid.\n" + + "Maybe you can replace them with \\ or - and try again!"; + public static final String ERROR_HEIGHT = "Your height cannot be of this value!" + LS + + "Maybe you can try a number from " + Height.LOWER_HEIGHT_LIMIT + " to " + Height.UPPER_HEIGHT_LIMIT + "."; + public static final String ERROR_WEIGHT = "Your weight cannot be of this value!" + LS + + "Maybe you can try a number from " + Weight.LOWER_WEIGHT_LIMIT + " to " + Weight.UPPER_WEIGHT_LIMIT + "."; + public static final String ERROR_GENDER = "Please type in M or F only!"; + public static final String ERROR_AGE = "Your age cannot be this value!" + LS + + "Maybe you can try a whole number from " + Age.LOWER_AGE_LIMIT + " to " + Age.UPPER_AGE_LIMIT + "."; + public static final String ERROR_ACTIVITY_FACTOR = "Your activity factor cannot be of this value!" + LS + + "Maybe you can try a whole number from " + + ActivityFactor.LIMIT_LOWER_ACTIVITY_FACTOR + + " to " + + ActivityFactor.LIMIT_UPPER_ACTIVITY_FACTOR + "."; + public static final String ERROR_CALORIE_GOAL = + "Your calorie goal cannot be of this value!" + LS + + "Maybe you can try a whole number from " + + CalorieGoal.LIMIT_LOWER_CALORIES + + " to " + + CalorieGoal.LIMIT_UPPER_CALORIES + "."; + + /** + * Retrieves the Basal Metabolic Rate of the user based on their activity factor indicated on the profile. + * A higher activity factor indicates a greater metabolic rate and thus more calories they burn off. + * + * @return BMR value based on their indicated activity factor + */ + public static int getBmr(Profile profile) { + double bmr = getBaseBmrValue(profile); + double factor = profile.getProfileActivityFactor().getActivityLevel().getFactor(); + return (int) Math.round(bmr * factor); + } + + /** + * Manually calculates the BMI. + * Used when the user input values that is not be stored in his/her profile. + * + * @param height Value of manual input of height + * @param weight Value of manual input of weight + * @return The calculated BMI of the manual inputs + * @throws InvalidCharacteristicException When the user inputs negative values for either height or weight + */ + public static double calculateBmi(double height, double weight) throws InvalidCharacteristicException { + checkWeightValidity(weight); + checkHeightValidity(height); + assert weight > 0 : "Weight cannot be non-positive."; + assert height > 0 : "Height cannot be non-positive."; + return computeBmi(height, weight); + } + + private static double computeBmi(double height, double weight) { + double heightInM = height / 100.0; + return Math.round((weight / (Math.pow(heightInM, 2))) * 10) / 10.0; + } + + private static void checkWeightValidity(double weight) throws InvalidCharacteristicException { + if (weight <= NON_POSITIVE_LIMIT) { + throw new InvalidCharacteristicException(ERROR_WEIGHT); + } + } + + private static void checkHeightValidity(double height) throws InvalidCharacteristicException { + if (height <= NON_POSITIVE_LIMIT) { + throw new InvalidCharacteristicException(ERROR_HEIGHT); + } + } + + /** + * Retrieves the indication with regard to the value of their BMI. + * Should not have any exceptions thrown since the other functions handled invalid cases. + * + * @param bmi The bmi of the user + * @return The status of his current body + **/ + public static String retrieveBmiStatus(double bmi) { + String result; + assert bmi > 0 : "BMI cannot be non-positive."; + if (bmi < BMI_LIMIT_UNDERWEIGHT) { + result = BMI_STATUS_UNDERWEIGHT; + } else if (bmi <= BMI_LIMIT_HEALTHY) { + result = BMI_STATUS_HEALTHY; + } else if (bmi <= BMI_LIMIT_OVERWEIGHT) { + result = BMI_STATUS_OVERWEIGHT; + } else { + result = BMI_STATUS_OBESE; + } + return result; + } + + private static double getBaseBmrValue(Profile profile) { + char gender = profile.getProfileGender().getGender(); + double weight = profile.getProfileWeight().getWeight(); + double height = profile.getProfileHeight().getHeight(); + int age = profile.getProfileAge().getAge(); + double bmr; + if (gender == GENDER_M) { + bmr = GENDER_M_WEIGHT_FACTOR * weight + + GENDER_M_HEIGHT_FACTOR * height + - GENDER_M_AGE_FACTOR * age + + GENDER_M_CONSTANT; + } else { + bmr = GENDER_F_WEIGHT_FACTOR * weight + + GENDER_F_HEIGHT_FACTOR * height + - GENDER_F_AGE_FACTOR * age + + GENDER_F_CONSTANT; + } + return bmr; + } + + /** + * Calculates the difference between food calories and exercise calories, factoring in metabolic rate. + * + * @param foodCalories Total intake consumption + * @param exerciseCalories Total output exerted + * @return The net calories of food - (exercise + BMR) + * @throws InvalidCharacteristicException Only if activity factor has been misappropriated in .txt file + */ + public static int calculateNetCalories(int foodCalories, int exerciseCalories, Profile p) + throws InvalidCharacteristicException { + return foodCalories - exerciseCalories - getBmr(p); + } + +} diff --git a/src/main/java/seedu/duke/logic/LogicManager.java b/src/main/java/seedu/duke/logic/LogicManager.java new file mode 100644 index 0000000000..9d4facf654 --- /dev/null +++ b/src/main/java/seedu/duke/logic/LogicManager.java @@ -0,0 +1,68 @@ +package seedu.duke.logic; + +import seedu.duke.data.DataManager; +import seedu.duke.logic.commands.ByeCommand; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandResult; +import seedu.duke.logic.parser.ParserManager; +import seedu.duke.storage.StorageManager; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +//@@author tlyi + +/** + * Handles the parsing and execution of all commands. + */ +public class LogicManager { + final ParserManager parserManager; + final StorageManager storageManager; + final DataManager dataManager; + + public LogicManager(StorageManager storageManager, DataManager dataManager) { + this.parserManager = new ParserManager(); + this.storageManager = storageManager; + this.dataManager = dataManager; + } + + /** + * Executes the given Command and (to be implemented) calls for storage operation if required. + * + * @param userInput Raw user input + * @return CommandResult representing result of execution of the command + */ + public CommandResult execute(String userInput) { + final Command command = parserManager.parseCommand(userInput); + command.setData(dataManager); + final CommandResult result = command.execute(); + try { + if (ByeCommand.isBye(command)) { + storageManager.saveAll(dataManager); + } + if (Command.requiresProfileStorageRewrite(command)) { + storageManager.saveProfile(dataManager.getProfile()); + } + if (Command.requiresExerciseListStorageRewrite(command)) { + dataManager.getExerciseItems().addAll(dataManager.getFilteredExerciseItems()); + storageManager.saveExerciseList(dataManager.getExerciseItems()); + } + if (Command.requiresFoodListStorageRewrite(command)) { + dataManager.getFoodItems().addAll(dataManager.getFilteredFoodItems()); + storageManager.saveFoodList(dataManager.getFoodItems()); + } + if (Command.requiresFutureExerciseListStorageRewrite(command)) { + storageManager.saveFutureExerciseList(dataManager.getFutureExerciseItems()); + } + if (Command.requiresFoodBankStorageRewrite(command)) { + storageManager.saveFoodBank(dataManager.getFoodBank()); + } + if (Command.requiresExerciseBankStorageRewrite(command)) { + storageManager.saveExerciseBank(dataManager.getExerciseBank()); + } + } catch (UnableToWriteFileException e) { + return new CommandResult(e.getMessage()); + } + return result; + } + + +} diff --git a/src/main/java/seedu/duke/logic/Statistics.java b/src/main/java/seedu/duke/logic/Statistics.java new file mode 100644 index 0000000000..99ffad9e1e --- /dev/null +++ b/src/main/java/seedu/duke/logic/Statistics.java @@ -0,0 +1,232 @@ +package seedu.duke.logic; + +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.data.profile.Profile; +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.ui.Ui; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** A class that manage the statistics of the calories. */ +public class Statistics { + private static final String FULL_BLOCK = "█"; + private static final String LS = System.lineSeparator(); + private static final String MESSAGE_CALORIE_GAIN = "Your calorie gained from food is: %d"; + private static final String MESSAGE_CALORIE_LOST = "Your calorie lost from exercise is: %d"; + private static final String MESSAGE_CALORIE_NET = "Your net calorie intake is: %d"; + private static final String MESSAGE_CALORIE_GOAL = "Your calorie goal is: %d"; + private static final String MESSAGE_CALORIE_EXACT = "You have reached your calorie goal exactly. Good job!"; + private static final String MESSAGE_CALORIE_LESS_THAN = "You are %s cal away from your goal!"; + private static final String MESSAGE_CALORIE_MORE_THAN = "You have exceeded your calorie goal by %s cal! "; + private static final String OVERVIEW_HEADER = "-*WEEKLY OVERVIEW*-" + LS + + "Hi %s, this is your calorie summary for the week." + LS; + private static final String FOOD_HEADER = "Food:" + LS + + "You have consumed %1$s cal this week from %2$s to %3$s."; + private static final String EXERCISE_HEADER = "Exercise:" + LS + + "You have burnt %1$s cal this week from %2$s to %3$s."; + private static final String FOOD_GRAPH_HEADER = "Calorie gained from food (Daily)" + LS + "%s"; + private static final String EXERCISE_GRAPH_HEADER = "Calorie burnt from exercise (Daily)" + LS + "%s"; + private static final String MESSAGE_CAUTION = LS + "** Net calories = Food consumed - Exercise output - " + + "your basal metabolic rate, where " + LS + + "your basal metabolic rate is a factor of your age, gender, " + + "height and weight retrieved from your profile." + LS + + "All calculations are done in calories."; + + private static final String GRAPH_BUILDER = "%1$s %2$s %3$s"; + private static final int MAX_DATE_OFFSET = 6; + private static final int NO_OFFSET = 0; + + private static final String MESSAGE_NET_CALORIES_INTRO = "Daily net calories**:" + LS; + private static final String MESSAGE_SUPPER_COUNT_INTRO = "Number of supper meals this week: %s"; + private static final int MAX_BAR_LENGTH = 30; + private static final int EMPTY_CALORIES = 0; + private static final String MESSAGE_DAILY_OVERVIEW = "This is your calorie overview for today:" + LS; + + + private FoodList foodItems; + private ExerciseList exerciseItems; + private Profile profile; + + public Statistics(FoodList foodItems, ExerciseList exerciseItems, Profile profile) { + this.foodItems = foodItems; + this.exerciseItems = exerciseItems; + this.profile = profile; + } + + private static Logger logger = Logger.getLogger(Statistics.class.getName()); + private LocalDate date; + + /** + * Calculate netCalories and format exerciseCalories, foodCalories, calorieGoal + * into strings. + * + * @param exerciseCalories is the total calories lost by exercising + * @param foodCalories is the total calories gained by consuming food + * @param calorieGoal is the goal set by the user + * @return formatted strings. + */ + String[] getCaloriesReport(int exerciseCalories, int foodCalories, int calorieGoal) { + int netCalories = calculateNetCalories(foodCalories, exerciseCalories); + return new String[]{String.format(MESSAGE_CALORIE_GAIN, foodCalories), + String.format(MESSAGE_CALORIE_LOST, exerciseCalories), + String.format(MESSAGE_CALORIE_NET, netCalories), + String.format(MESSAGE_CALORIE_GOAL, calorieGoal), + printCaloriesMessage(netCalories, calorieGoal)}; + } + + private int calculateNetCalories(int foodCalories, int exerciseCalories) { + try { + return ProfileUtils.calculateNetCalories(foodCalories, exerciseCalories, this.profile); + } catch (InvalidCharacteristicException e) { + return 0; + } + } + + private String getCurrentDayOverview() { + int foodCalories = foodItems.getTotalCaloriesWithDate(date); + int exerciseCalories = exerciseItems.getTotalCaloriesWithDate(date); + int calorieGoal = profile.getProfileCalorieGoal().getCalorieGoal(); + logger.log(Level.FINE, String.valueOf(calorieGoal)); + String[] messages = getCaloriesReport(exerciseCalories, foodCalories, calorieGoal); + StringBuilder currentDayOverview = new StringBuilder(MESSAGE_DAILY_OVERVIEW); + for (String message : messages) { + currentDayOverview.append(message).append(Ui.LS); + } + return currentDayOverview.toString().trim(); + } + + public String printCaloriesMessage(int netCalories, int calorieGoal) { + logger.log(Level.FINE, "preparing calories message"); + int calorieDifference = calorieGoal - netCalories; + String message; + if (calorieGoal > netCalories) { + message = String.format(MESSAGE_CALORIE_LESS_THAN, calorieDifference); + } else if (calorieGoal < netCalories) { + message = String.format(MESSAGE_CALORIE_MORE_THAN, -calorieDifference); + } else { + assert calorieDifference == 0 : "calorieDifference should be 0"; + + message = MESSAGE_CALORIE_EXACT; + } + return message; + } + + /** + * Set the date to current date. + * Date to be updated upon calling the overview command. + */ + private void setLocalDate() { + date = LocalDateTime.now().toLocalDate(); + } + + private LocalDate dateOffset(int offset) { + return date.minusDays(offset); + } + + private ArrayList getDailyFoodCalories() { + ArrayList dailyFoodCalories = new ArrayList<>(); + for (int i = MAX_DATE_OFFSET; i >= NO_OFFSET; i--) { + dailyFoodCalories.add(foodItems.getTotalCaloriesWithDate(date.minusDays(i))); + } + return dailyFoodCalories; + } + + private String getGraph(ArrayList dailyCalories) { + int maxCalories = Collections.max(dailyCalories); + StringBuilder graph = new StringBuilder(); + int dateOffset = MAX_DATE_OFFSET; + + for (int calories : dailyCalories) { + String progressBar = ""; + int numberOfBars; + numberOfBars = (int)(((double) calories / maxCalories) * MAX_BAR_LENGTH); + assert numberOfBars <= MAX_BAR_LENGTH : "30 is the max progress bar limit"; + for (int i = NO_OFFSET; i < numberOfBars; i++) { + progressBar = progressBar + FULL_BLOCK; + } + logger.log(Level.FINE, String.valueOf(numberOfBars)); + String formattedDate = getFormatDate(dateOffset); + graph.append(String.format(GRAPH_BUILDER, formattedDate, progressBar, calories)).append(Ui.LS); + dateOffset--; + } + return graph.toString(); + } + + private String getFormatDate(int dateOffset) { + String formattedDate = dateOffset(dateOffset).format(DateTimeFormatter.ofPattern("dd-MMM")); + return formattedDate; + } + + private ArrayList getDailyExerciseCalories() { + ArrayList dailyExerciseCalories = new ArrayList<>(); + for (int i = MAX_DATE_OFFSET; i >= NO_OFFSET; i--) { + dailyExerciseCalories.add(exerciseItems.getTotalCaloriesWithDate(date.minusDays(i))); + } + return dailyExerciseCalories; + } + + private int getTotalWeeklyCalories(ArrayList getCalories) { + return getCalories.stream().mapToInt(i -> i).sum(); + } + + private String getSupperCountMessage() { + int supperCount = foodItems.getSupperCount(); + return String.format(MESSAGE_SUPPER_COUNT_INTRO, supperCount); + } + + private String getNetCaloriesMessage() { + ArrayList dailyExerciseCalories = getDailyExerciseCalories(); + ArrayList dailyFoodCalories = getDailyFoodCalories(); + StringBuilder netCaloriesMessage = new StringBuilder(MESSAGE_NET_CALORIES_INTRO); + for (int i = 0; i <= MAX_DATE_OFFSET; i++) { + int exerciseCalories = dailyExerciseCalories.get(i) == null ? EMPTY_CALORIES : dailyExerciseCalories.get(i); + int foodCalories = dailyFoodCalories.get(i) == null ? EMPTY_CALORIES : dailyFoodCalories.get(i); + int netCalories = getNetCalories(foodCalories, exerciseCalories); + String formattedDate = getFormatDate(MAX_DATE_OFFSET - i); + netCaloriesMessage.append(formattedDate + " : " + netCalories).append(Ui.LS); + } + return netCaloriesMessage.toString(); + } + + private int getNetCalories(int foodCalories, int exerciseCalories) { + try { + return ProfileUtils.calculateNetCalories(foodCalories, exerciseCalories, profile); + } catch (InvalidCharacteristicException e) { + return EMPTY_CALORIES; + } + } + + /** + * An overview on user calorie intake and calorie burnt. + * + * @return String that contains summary of calories for the week. + */ + public String overviewSummary() { + setLocalDate(); // need ensure that the date is on the time of query + logger.log(Level.FINE, String.valueOf(date)); + StringBuilder overviewSummary = new StringBuilder(); + overviewSummary.append(String.format(OVERVIEW_HEADER, profile.getProfileName().getName())).append(Ui.LS) + .append(String.format(FOOD_HEADER, getTotalWeeklyCalories(getDailyFoodCalories()), + getFormatDate(MAX_DATE_OFFSET), getFormatDate(NO_OFFSET))).append(Ui.LS) + .append(String.format(FOOD_GRAPH_HEADER, getGraph(getDailyFoodCalories()))).append(Ui.LS) + .append(String.format(EXERCISE_HEADER, getTotalWeeklyCalories(getDailyExerciseCalories()), + getFormatDate(MAX_DATE_OFFSET), getFormatDate(NO_OFFSET))).append(Ui.LS) + .append(String.format(EXERCISE_GRAPH_HEADER, getGraph(getDailyExerciseCalories()))).append(Ui.LS) + .append(getNetCaloriesMessage()).append(Ui.LS) + .append(getSupperCountMessage()).append(Ui.LS) + .append(MESSAGE_CAUTION).append(Ui.LS) + .append(Ui.DIVIDER).append(Ui.LS) + .append(getCurrentDayOverview()); + return overviewSummary.toString().trim(); + } + + +} diff --git a/src/main/java/seedu/duke/logic/commands/AddExerciseBankCommand.java b/src/main/java/seedu/duke/logic/commands/AddExerciseBankCommand.java new file mode 100644 index 0000000000..dc38187644 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/AddExerciseBankCommand.java @@ -0,0 +1,45 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.exceptions.DuplicateItemInBankException; +import seedu.duke.data.item.exercise.Exercise; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, adds an Exercise item to the ExerciseBank. + */ +public class AddExerciseBankCommand extends Command { + public static final String MESSAGE_SUCCESS = "An exercise item has been added to the exercise bank:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_EXERCISE_BANK, + COMMAND_PREFIX_CALORIES + }; + + private static Logger logger = Logger.getLogger(AddExerciseBankCommand.class.getName()); + + private Exercise exercise; + + public AddExerciseBankCommand(String description, int calories) { + this.exercise = new Exercise(description, calories); + } + + @Override + public CommandResult execute() { + if (!exercise.isValid()) { + logger.log(Level.FINE, "Exercise calorie is invalid"); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + assert exercise.getCalories() > 0 : "Exercise calorie is valid"; + try { + super.exerciseBank.addItem(this.exercise); + logger.log(Level.FINE, "Exercise is successfully added to exercise bank"); + return new CommandResult(String.format(MESSAGE_SUCCESS, this.exercise)); + } catch (DuplicateItemInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_EXERCISE_ALREADY_EXISTS_IN_BANK, this.exercise.getName())); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/AddExerciseCommand.java b/src/main/java/seedu/duke/logic/commands/AddExerciseCommand.java new file mode 100644 index 0000000000..e7eba51576 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/AddExerciseCommand.java @@ -0,0 +1,60 @@ +package seedu.duke.logic.commands; + + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.exceptions.ItemNotFoundInBankException; +import seedu.duke.data.item.exercise.Exercise; + +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, adds an Exercise item to the ExerciseList. + */ +public class AddExerciseCommand extends Command { + public static final String MESSAGE_SUCCESS = "An exercise item has been added:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_EXERCISE, + COMMAND_PREFIX_CALORIES, + COMMAND_PREFIX_DATE + }; + + + private static Logger logger = Logger.getLogger(AddExerciseCommand.class.getName()); + + private final String description; + private Integer calories; + private final LocalDate date; + + public AddExerciseCommand(String description, Integer calories, LocalDate date) { + this.description = description; + this.calories = calories; + this.date = date; + } + + @Override + public CommandResult execute() { + final Exercise exercise; + + if (calories == null) { + try { + this.calories = super.exerciseBank.findCalorie(this.description); + } catch (ItemNotFoundInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_INVALID_EXERCISE_NOT_IN_BANK, this.description)); + } + } + + exercise = new Exercise(this.description, this.calories, this.date); + if (!exercise.isValid()) { + logger.log(Level.FINE, "Detected impossible calorie burnt for the exercise."); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + assert exercise.getCalories() > 0 : "Exercise calorie is valid"; + super.exerciseItems.addItem(exercise); + logger.log(Level.FINE, "Exercise is successfully added"); + return new CommandResult(String.format(MESSAGE_SUCCESS, exercise.toStringWithDate())); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/AddFoodBankCommand.java b/src/main/java/seedu/duke/logic/commands/AddFoodBankCommand.java new file mode 100644 index 0000000000..8710ad731f --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/AddFoodBankCommand.java @@ -0,0 +1,46 @@ +package seedu.duke.logic.commands; + + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.exceptions.DuplicateItemInBankException; +import seedu.duke.data.item.food.Food; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, adds a Food item to the FoodBank. + */ +public class AddFoodBankCommand extends Command { + public static final String MESSAGE_SUCCESS = "A food item has been added to the food bank:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_FOOD_BANK, + COMMAND_PREFIX_CALORIES + }; + + private static Logger logger = Logger.getLogger(AddFoodBankCommand.class.getName()); + + private Food food; + + public AddFoodBankCommand(String description, int calories) { + this.food = new Food(description, calories); + } + + @Override + public CommandResult execute() { + if (!food.isValid()) { + logger.log(Level.FINE, "Food calorie is invalid"); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + assert food.getCalories() > 0 : "Food calorie is valid"; + try { + super.foodBank.addItem(this.food); + logger.log(Level.FINE, "Food is successfully added to food bank"); + return new CommandResult(String.format(MESSAGE_SUCCESS, this.food.toStringWithoutDateAndTime())); + } catch (DuplicateItemInBankException e) { + return new CommandResult(String.format(CommandMessages.MESSAGE_FOOD_ALREADY_EXISTS_IN_BANK, + this.food.getName())); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/AddFoodCommand.java b/src/main/java/seedu/duke/logic/commands/AddFoodCommand.java new file mode 100644 index 0000000000..e12d7fe3f5 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/AddFoodCommand.java @@ -0,0 +1,61 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.exceptions.ItemNotFoundInBankException; +import seedu.duke.data.item.food.Food; + +import java.time.LocalDateTime; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, adds a Food item to the FoodList. + */ +public class AddFoodCommand extends Command { + public static final String MESSAGE_SUCCESS = "A food item has been added:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_FOOD, + COMMAND_PREFIX_CALORIES, + COMMAND_PREFIX_DATE, + COMMAND_PREFIX_TIME + }; + + + private Logger logger = Logger.getLogger(AddFoodCommand.class.getName()); + private final String description; + private Integer calories; + private final LocalDateTime dateTime; + + + public AddFoodCommand(String description, Integer calories, LocalDateTime dateTime) { + this.description = description; + this.calories = calories; + this.dateTime = dateTime; + } + + @Override + public CommandResult execute() { + final Food food; + + if (calories == null) { + try { + this.calories = super.foodBank.findCalorie(this.description); + } catch (ItemNotFoundInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_INVALID_FOOD_NOT_IN_BANK, this.description)); + } + } + + food = new Food(this.description, this.calories, this.dateTime); + if (!food.isValid()) { + logger.log(Level.FINE, "Detected impossible value for food calorie"); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + super.foodItems.addItem(food); + assert foodItems.getSize() > 0 : "The size of the food list should at least larger than 0"; + logger.log(Level.FINE, "New food item has been added to the food list"); + return new CommandResult(String.format(MESSAGE_SUCCESS, food.toStringWithDate())); + + } +} + diff --git a/src/main/java/seedu/duke/logic/commands/AddFutureExerciseCommand.java b/src/main/java/seedu/duke/logic/commands/AddFutureExerciseCommand.java new file mode 100644 index 0000000000..77bbdaae08 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/AddFutureExerciseCommand.java @@ -0,0 +1,57 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.exceptions.ItemNotFoundInBankException; +import seedu.duke.data.item.exercise.Exercise; + +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + +//@@author xingjie99 + +/** + * Represents the command that when executed, adds an Exercise item to the FutureExerciseList. + */ +public class AddFutureExerciseCommand extends Command { + public static final String MESSAGE_SUCCESS = "An exercise item for the future has been added:" + + CommandMessages.INDENTED_LS + "%s"; + + private static Logger logger = Logger.getLogger(AddFutureExerciseCommand.class.getName()); + + private final String description; + private Integer calories; + private final LocalDate date; + + public AddFutureExerciseCommand(String description, Integer calories, LocalDate date) { + this.description = description; + this.calories = calories; + this.date = date; + } + + @Override + public CommandResult execute() { + final Exercise exercise; + if (date.isAfter(LocalDate.now().plusYears(CommandMessages.ONE_YEAR))) { + return new CommandResult(CommandMessages.MESSAGE_EXERCISE_NOT_WITHIN_ONE_YEAR); + } + if (calories == null) { + try { + this.calories = super.exerciseBank.findCalorie(this.description); + } catch (ItemNotFoundInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_INVALID_EXERCISE_NOT_IN_BANK, this.description)); + } + } + + exercise = new Exercise(this.description, this.calories, this.date); + if (!exercise.isValid()) { + logger.log(Level.FINE, "Detected impossible calorie burnt for the exercise."); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + assert exercise.getCalories() > 0 : "Exercise calorie is valid"; + super.futureExerciseItems.addItem(exercise); + logger.log(Level.FINE, "Exercise is successfully added"); + return new CommandResult(String.format(MESSAGE_SUCCESS, exercise.toStringWithDate())); + } +} + diff --git a/src/main/java/seedu/duke/logic/commands/AddRecurringExerciseCommand.java b/src/main/java/seedu/duke/logic/commands/AddRecurringExerciseCommand.java new file mode 100644 index 0000000000..733683769f --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/AddRecurringExerciseCommand.java @@ -0,0 +1,95 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.exceptions.ItemNotFoundInBankException; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +//@@author xingjie99 + +/** + * Represent the command that when executed, adds all recurring Exercise items to the FutureExerciseList. + */ +public class AddRecurringExerciseCommand extends Command { + public static final String MESSAGE_INVALID_DATES = "Your start date %s should not be later than your end date %s"; + public static final String MESSAGE_INVALID_FUTURE_DATES = "Make sure that your start date (%s) " + + "and end date (%s) are in the future"; + public static final String MESSAGE_NO_EXERCISE_ADDED = "Day(s) not present between %s and %s"; + public static final String MESSAGE_SUCCESS = "Recurring exercise item for the future has been added!" + + CommandMessages.LS + "You can view your upcoming exercises by typing " + + CommandMessages.QUOTATION + Command.COMMAND_WORD_VIEW + " " + Command.COMMAND_PREFIX_UPCOMING_EXERCISE + + Command.COMMAND_PREFIX_DELIMITER + CommandMessages.QUOTATION + "!"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_RECURRING, + COMMAND_PREFIX_CALORIES, + COMMAND_PREFIX_START_DATE, + COMMAND_PREFIX_END_DATE, + COMMAND_PREFIX_DAY_OF_THE_WEEK + }; + + private final String description; + private Integer calories; + private final LocalDate startDate; + private final LocalDate endDate; + private final ArrayList dayOfTheWeek; + + private static Logger logger = Logger.getLogger(AddRecurringExerciseCommand.class.getName()); + + public AddRecurringExerciseCommand(String description, Integer calories, LocalDate startDate, + LocalDate endDate, ArrayList dayOfTheWeek) { + this.description = description; + this.calories = calories; + this.startDate = startDate; + this.endDate = endDate; + this.dayOfTheWeek = dayOfTheWeek; + } + + @Override + public CommandResult execute() { + if (this.startDate.isAfter(this.endDate)) { + logger.log(Level.FINE, "Start date is after end date"); + return new CommandResult(String.format(MESSAGE_INVALID_DATES, + this.startDate.format(CommandMessages.DATE_FORMATTER), + this.endDate.format(CommandMessages.DATE_FORMATTER))); + } else if (endDate.isAfter(LocalDate.now().plusYears(CommandMessages.ONE_YEAR))) { + return new CommandResult(CommandMessages.MESSAGE_RECURRING_EXERCISE_NOT_WITHIN_ONE_YEAR); + } + + if (calories == null) { + try { + this.calories = super.exerciseBank.findCalorie(this.description); + } catch (ItemNotFoundInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_INVALID_EXERCISE_NOT_IN_BANK, this.description)); + } + } else if (calories < Item.LOWEST_CALORIE || calories > Item.HIGHEST_CALORIE) { + logger.log(Level.FINE, "Detected impossible calorie value"); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + assert this.endDate.isAfter(this.startDate) : "End date is after start date"; + if (!this.startDate.isAfter(LocalDate.now())) { + logger.log(Level.FINE, "Recurring exercises are for future only"); + return new CommandResult(String.format(MESSAGE_INVALID_FUTURE_DATES, + this.startDate.format(CommandMessages.DATE_FORMATTER), + this.endDate.format(CommandMessages.DATE_FORMATTER))); + } + assert this.startDate.isAfter(LocalDate.now()) : "Start and end dates are in the future"; + int numberOfFutureExercises = futureExerciseItems.getSize(); + futureExerciseItems.addRecurringExercises(this.description, this.calories, + this.startDate, this.endDate, this.dayOfTheWeek); + if (futureExerciseItems.getSize() == numberOfFutureExercises) { + return new CommandResult(String.format(MESSAGE_NO_EXERCISE_ADDED, + this.startDate.format(CommandMessages.DATE_FORMATTER), + this.endDate.format(CommandMessages.DATE_FORMATTER))); + } + + logger.log(Level.FINE, "Recurring exercise is successfully added"); + return new CommandResult(MESSAGE_SUCCESS); + } +} + + + diff --git a/src/main/java/seedu/duke/logic/commands/ByeCommand.java b/src/main/java/seedu/duke/logic/commands/ByeCommand.java new file mode 100644 index 0000000000..077e1aeb70 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ByeCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.logic.commands; + +/** + * Represents the command that when executed, signaCommandMessages.LS to the application to exit. + */ +public class ByeCommand extends Command { + public static final String COMMAND_WORD = "bye"; + private static final String MESSAGE_SUCCESS = "Exiting Fitbot...." + CommandMessages.LS + + "Bye! Hope to see you again soon!!"; + + + @Override + public CommandResult execute() { + CommandResult result = new CommandResult(MESSAGE_SUCCESS); + result.setBye(true); + return result; + } + + /** + * Determines if command is the Bye command so that the main program knows when to exit. + */ + public static boolean isBye(Command command) { + return command instanceof ByeCommand; + } +} diff --git a/src/main/java/seedu/duke/logic/commands/CalculateBmiCommand.java b/src/main/java/seedu/duke/logic/commands/CalculateBmiCommand.java new file mode 100644 index 0000000000..d0abe91c58 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/CalculateBmiCommand.java @@ -0,0 +1,53 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.profile.attributes.Height; +import seedu.duke.data.profile.attributes.Weight; +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; +import seedu.duke.data.profile.utilities.ProfileUtils; + +/** + * Represents the command that when executed, calculates the BMI with the given height and weight data. + */ +public class CalculateBmiCommand extends Command { + public static final String MESSAGE_COMMAND_FORMAT = CommandMessages.QUOTATION + COMMAND_WORD_BMI + + " " + COMMAND_PREFIX_HEIGHT + COMMAND_PREFIX_DELIMITER + "Y " + + COMMAND_PREFIX_WEIGHT + COMMAND_PREFIX_DELIMITER + "Z" + + CommandMessages.QUOTATION + " where X is the height in CM and Y is the weight in KG."; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid format! " + + "Trying to calculate BMI for a certain height and weight? Use this format:" + + CommandMessages.INDENTED_LS + MESSAGE_COMMAND_FORMAT; + public static final String MESSAGE_SUCCESS = "The calculated BMI value is %1$,.1f (%2$s)"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_HEIGHT, + COMMAND_PREFIX_WEIGHT + }; + + private final Height height; + private final Weight weight; + + public CalculateBmiCommand(double height, double weight) { + this.height = new Height(height); + this.weight = new Weight(weight); + } + + private void checkIfCommandShouldExecute() throws InvalidCharacteristicException { + if (!this.weight.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_WEIGHT); + } + if (!this.height.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_HEIGHT); + } + } + + @Override + public CommandResult execute() { + try { + checkIfCommandShouldExecute(); + final double bmi = ProfileUtils.calculateBmi(this.height.getHeight(), this.weight.getWeight()); + return new CommandResult(String.format(MESSAGE_SUCCESS, bmi, ProfileUtils.retrieveBmiStatus(bmi))); + } catch (InvalidCharacteristicException e) { + return new CommandResult(e.getMessage()); + } + + } +} diff --git a/src/main/java/seedu/duke/logic/commands/CalculateBmiWithProfileCommand.java b/src/main/java/seedu/duke/logic/commands/CalculateBmiWithProfileCommand.java new file mode 100644 index 0000000000..a4545ea984 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/CalculateBmiWithProfileCommand.java @@ -0,0 +1,26 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; +import seedu.duke.data.profile.utilities.ProfileUtils; + +/** + * Represents the command that when executed, calculates the BMI of the current Profile. + */ +public class CalculateBmiWithProfileCommand extends Command { + public static final String MESSAGE_SUCCESS = "Your BMI value according to your current profile is:" + + CommandMessages.INDENTED_LS + "%1$,.1f (%2$s)"; + public static final String MESSAGE_UNINITIALIZED_PROFILE = "Your profile has not been initialized yet."; + + + @Override + public CommandResult execute() { + try { + final double height = super.profile.getProfileHeight().getHeight(); + final double weight = super.profile.getProfileWeight().getWeight(); + final double bmi = ProfileUtils.calculateBmi(height, weight); + return new CommandResult(String.format(MESSAGE_SUCCESS, bmi, ProfileUtils.retrieveBmiStatus(bmi))); + } catch (InvalidCharacteristicException e) { + return new CommandResult(MESSAGE_UNINITIALIZED_PROFILE); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/Command.java b/src/main/java/seedu/duke/logic/commands/Command.java new file mode 100644 index 0000000000..b6b45ed135 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/Command.java @@ -0,0 +1,151 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.DataManager; +import seedu.duke.data.item.ItemBank; +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.data.item.exercise.FutureExerciseList; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.data.profile.Profile; + + +/** + * Abstract class used to represent executable Commands. + * All Commands can be executed to return a CommandResult. + */ +public abstract class Command { + public static final String COMMAND_PREFIX_DELIMITER = "/"; + public static final String COMMAND_INDEX_DELIMITER = ","; + public static final String COMMAND_PREFIX_EXERCISE = "e"; + public static final String COMMAND_PREFIX_UPCOMING_EXERCISE = "u"; + public static final String COMMAND_PREFIX_RECURRING = "r"; + public static final String COMMAND_PREFIX_FOOD = "f"; + public static final String COMMAND_PREFIX_EXERCISE_BANK = "ebank"; + public static final String COMMAND_PREFIX_FOOD_BANK = "fbank"; + public static final String COMMAND_PREFIX_CALORIES = "c"; + public static final String COMMAND_PREFIX_NAME = "n"; + public static final String COMMAND_PREFIX_HEIGHT = "h"; + public static final String COMMAND_PREFIX_WEIGHT = "w"; + public static final String COMMAND_PREFIX_GOAL = "g"; + public static final String COMMAND_PREFIX_DATE = "d"; + public static final String COMMAND_PREFIX_TIME = "t"; + public static final String COMMAND_PREFIX_GENDER = "s"; + public static final String COMMAND_PREFIX_AGE = "a"; + public static final String COMMAND_PREFIX_ACTIVITY_FACTOR = "x"; + public static final String COMMAND_PREFIX_START_DATE = ":"; + public static final String COMMAND_PREFIX_END_DATE = "-"; + public static final String COMMAND_PREFIX_DAY_OF_THE_WEEK = "@"; + public static final String COMMAND_WORD_ADD = "add"; + public static final String COMMAND_WORD_DELETE = "delete"; + public static final String COMMAND_WORD_VIEW = "view"; + public static final String COMMAND_WORD_EDIT = "edit"; + public static final String COMMAND_WORD_BMI = "bmi"; + public static final String COMMAND_WORD_PROFILE = "profile"; + public static final String COMMAND_WORD_DELETE_ALL = "all"; + public static final char NULL_CHAR = '|'; + + protected Profile profile; + protected ExerciseList exerciseItems; + protected FoodList foodItems; + protected FutureExerciseList futureExerciseItems; + protected ItemBank exerciseBank; + protected ItemBank foodBank; + + + /** + * Returns the appropriate CommandResult after execution of the command. + * Each child class that inherits this class represents an executable command and will have its own implementation + * of this method specific to its functionality. + */ + public abstract CommandResult execute(); + + /** + * Provides the necessary data structures for the command to operate on. + */ + public void setData(DataManager dataManager) { + this.profile = dataManager.getProfile(); + this.exerciseItems = dataManager.getFilteredExerciseItems(); + this.foodItems = dataManager.getFilteredFoodItems(); + this.futureExerciseItems = dataManager.getFutureExerciseItems(); + this.exerciseBank = dataManager.getExerciseBank(); + this.foodBank = dataManager.getFoodBank(); + assert profile != null : "Profile supplied to command should not be null"; + assert exerciseItems != null : "Exercise items supplied to command should not be null"; + assert foodItems != null : "Food items supplied to command should not be null"; + assert futureExerciseItems != null : "Future exercise items supplied to command should not be null"; + assert exerciseBank != null : "Exercise bank supplied to command should not be null"; + assert foodBank != null : "Food bank supplied to command should not be null"; + } + + + /** + * Returns true if the command requires the profile storage file to be rewritten after execution. + * + * @param command Command that has just been executed + * @return True if profile storage file is to be rewritten after execution of the command + */ + public static boolean requiresProfileStorageRewrite(Command command) { + return command instanceof ProfileUpdateCommand; + } + + /** + * Returns true if the command requires the exercise list storage file to be rewritten after execution. + * + * @param command Command that has just been executed + * @return True if exercise list storage file is to be rewritten after execution of the command + */ + public static boolean requiresExerciseListStorageRewrite(Command command) { + return command instanceof AddExerciseCommand + || command instanceof DeleteExerciseCommand; + } + + /** + * Returns true if the command requires the food list storage file to be rewritten after execution. + * + * @param command Command that has just been executed + * @return True if food list storage file is to be rewritten after execution of the command + */ + public static boolean requiresFoodListStorageRewrite(Command command) { + return command instanceof AddFoodCommand + || command instanceof DeleteFoodCommand; + } + + /** + * Returns true if the command requires the future exercise list storage file to be rewritten after execution. + * + * @param command Command that has just been executed + * @return True if future exercise list storage file is to be rewritten after execution of the command + */ + public static boolean requiresFutureExerciseListStorageRewrite(Command command) { + return command instanceof AddFutureExerciseCommand + || command instanceof DeleteExerciseCommand + || command instanceof DeleteFutureExerciseCommand + || command instanceof AddRecurringExerciseCommand + || command instanceof EditFutureExerciseCommand; + } + + /** + * Returns true if the command requires the food bank storage file to be rewritten after execution. + * + * @param command Command that has just been executed + * @return True if food bank storage file is to be rewritten after execution of the command + */ + public static boolean requiresFoodBankStorageRewrite(Command command) { + return command instanceof AddFoodBankCommand + || command instanceof DeleteFoodBankCommand + || command instanceof EditFoodBankCommand; + } + + /** + * Returns true if the command requires the exercise bank storage file to be rewritten after execution. + * + * @param command Command that has just been executed + * @return True if food bank storage file is to be rewritten after execution of the command + */ + public static boolean requiresExerciseBankStorageRewrite(Command command) { + return command instanceof AddExerciseBankCommand + || command instanceof DeleteExerciseBankCommand + || command instanceof EditExerciseBankCommand; + } + + +} diff --git a/src/main/java/seedu/duke/logic/commands/CommandMessages.java b/src/main/java/seedu/duke/logic/commands/CommandMessages.java new file mode 100644 index 0000000000..9c9344b528 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/CommandMessages.java @@ -0,0 +1,164 @@ +package seedu.duke.logic.commands; + +import java.time.format.DateTimeFormatter; + +/** + * Contains all the general error messages that can be returned from Command classes. + */ +public class CommandMessages { + //================ styling ================ + public static final String LS = System.lineSeparator(); + public static final String TAB = "\t"; + public static final String INDENTED_LS = CommandMessages.LS + TAB; + public static final String QUOTATION = "\""; + public static final String LONG_SPACE = " "; + public static final String BANK_SPACE = " "; + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + public static final int ONE_YEAR = 1; + + //================ item type prefixes ================ + public static final String MESSAGE_EXERCISE_LIST_FORMAT = + Command.COMMAND_PREFIX_EXERCISE + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "exercise list"; + public static final String MESSAGE_FOOD_LIST_FORMAT = + Command.COMMAND_PREFIX_FOOD + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "food list"; + public static final String MESSAGE_EXERCISE_BANK_FORMAT = + Command.COMMAND_PREFIX_EXERCISE_BANK + Command.COMMAND_PREFIX_DELIMITER + BANK_SPACE + "exercise bank"; + public static final String MESSAGE_FOOD_BANK_FORMAT = + Command.COMMAND_PREFIX_FOOD_BANK + Command.COMMAND_PREFIX_DELIMITER + BANK_SPACE + "food bank"; + public static final String MESSAGE_RECURRING_EXERCISE_FORMAT = + Command.COMMAND_PREFIX_RECURRING + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "recurring exercise"; + + //================ attribute prefixes and description ================ + public static final String MESSAGE_NAME_FORMAT = + Command.COMMAND_PREFIX_NAME + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "name"; + public static final String MESSAGE_HEIGHT_FORMAT = + Command.COMMAND_PREFIX_HEIGHT + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "height (cm)"; + public static final String MESSAGE_WEIGHT_FORMAT = + Command.COMMAND_PREFIX_WEIGHT + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "weight (kg)"; + public static final String MESSAGE_AGE_FORMAT = + Command.COMMAND_PREFIX_AGE + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "age"; + public static final String MESSAGE_GOAL_FORMAT = + Command.COMMAND_PREFIX_GOAL + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "calorie goal"; + public static final String MESSAGE_GENDER_FORMAT = + Command.COMMAND_PREFIX_GENDER + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "gender (M/F)"; + public static final String MESSAGE_ACTIVITY_FACTOR_FORMAT = + Command.COMMAND_PREFIX_ACTIVITY_FACTOR + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + + "activity factor (1: Sedentary - 5: Extra Active)"; + public static final String MESSAGE_ITEM_CALORIES_FORMAT = + Command.COMMAND_PREFIX_CALORIES + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "calories"; + public static final String MESSAGE_UPCOMING_EXERCISE_LIST_FORMAT = + Command.COMMAND_PREFIX_UPCOMING_EXERCISE + Command.COMMAND_PREFIX_DELIMITER + + LONG_SPACE + "upcoming exercise list"; + public static final String MESSAGE_ITEM_DATE_FORMAT = + Command.COMMAND_PREFIX_DATE + Command.COMMAND_PREFIX_DELIMITER + LONG_SPACE + "date (DD-MM-YYYY)"; + + + //================ invalid attribute messages ================ + public static final String MESSAGE_INVALID_CALORIES = + "Please input calories as a whole number from 1 to 10 000!"; + public static final String MESSAGE_EMPTY_EXERCISE_LIST = "No exercise items yet in the past 7 days!"; + public static final String MESSAGE_EMPTY_FUTURE_EXERCISE_LIST = "No future exercise items yet!"; + public static final String MESSAGE_EMPTY_FOOD_LIST = "No food items yet in the past 7 days!"; + public static final String MESSAGE_EMPTY_EXERCISE_BANK = "No exercise items yet in the exercise bank!"; + public static final String MESSAGE_EMPTY_FOOD_BANK = "No food items yet in the food bank!"; + public static final String MESSAGE_ONLY_ONE_IN_LIST = "You have only 1 item in the list!"; + public static final String MESSAGE_LIST_OUT_OF_BOUNDS = "Please input a valid item number from 1 to %s!"; + public static final String MESSAGE_FOOD_NOT_FOUND = "Food item with index %d, date %s and time %s " + + "is not found in the food list!"; + public static final String MESSAGE_EXERCISE_NOT_FOUND = "Exercise item with index %d and date %s " + + "is not found in the exercise list!"; + public static final String MESSAGE_INVALID_EXERCISE_NOT_IN_BANK = "%s was not found in the exercise bank! " + + "Please specify the calories for this item using the prefix " + + QUOTATION + Command.COMMAND_PREFIX_CALORIES + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + "!"; + public static final String MESSAGE_INVALID_FOOD_NOT_IN_BANK = "%s was not found in the food bank! " + + "Please specify the calories for this item using the prefix " + + QUOTATION + Command.COMMAND_PREFIX_CALORIES + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + "!"; + public static final String MESSAGE_FOOD_ALREADY_EXISTS_IN_BANK = "The food with name " + + QUOTATION + "%s" + QUOTATION + + " already exists in the food bank! (Names in the bank are case insensitive)" + + LS + "Try using another name, or delete/edit the existing item first!"; + public static final String MESSAGE_EXERCISE_ALREADY_EXISTS_IN_BANK = "The exercise with name " + + QUOTATION + "%s" + QUOTATION + + " already exists in the exercise bank! (Names are case insensitive)" + + LS + "Try using another name, or delete/edit the existing item first!"; + public static final String MESSAGE_EXERCISE_NOT_WITHIN_ONE_YEAR = "Fitbot is only able to help you keep a record " + + "of one year's worth of upcoming exercises." + + LS + "Please ensure that the input date is within a year from today."; + public static final String MESSAGE_RECURRING_EXERCISE_NOT_WITHIN_ONE_YEAR = "Fitbot is only able to help you keep " + + "a record of one year's worth of upcoming exercises." + + LS + "Please ensure that both your input start date and end date is within a year from today."; + + //================ command format suggestions ================ + //add command + public static final String MESSAGE_ADD_COMMAND_INVALID_FORMAT = + "Oops! Invalid format! Trying to add to your lists?" + LS + + "Type " + QUOTATION + Command.COMMAND_WORD_ADD + QUOTATION + + " followed by one of the following prefixes:" + LS + INDENTED_LS + + MESSAGE_EXERCISE_LIST_FORMAT + INDENTED_LS + + MESSAGE_FOOD_LIST_FORMAT + INDENTED_LS + + MESSAGE_EXERCISE_BANK_FORMAT + INDENTED_LS + + MESSAGE_FOOD_BANK_FORMAT + INDENTED_LS + + MESSAGE_RECURRING_EXERCISE_FORMAT + LS + LS + + "Type " + QUOTATION + HelpCommand.COMMAND_WORD + QUOTATION + + " if you need more details on how to add to each list!"; + //delete command + public static final String MESSAGE_DELETE_COMMAND_INVALID_FORMAT = + "Oops! Invalid format! Trying to delete from your lists?" + LS + + "Type " + QUOTATION + Command.COMMAND_WORD_DELETE + QUOTATION + + " followed by one of the following prefixes:" + LS + INDENTED_LS + + MESSAGE_EXERCISE_LIST_FORMAT + INDENTED_LS + + MESSAGE_FOOD_LIST_FORMAT + INDENTED_LS + + MESSAGE_EXERCISE_BANK_FORMAT + INDENTED_LS + + MESSAGE_FOOD_BANK_FORMAT + INDENTED_LS + + MESSAGE_UPCOMING_EXERCISE_LIST_FORMAT + LS + LS + + "Type " + QUOTATION + HelpCommand.COMMAND_WORD + QUOTATION + + " if you need more details on how to delete from each list!"; + //view command + public static final String MESSAGE_VIEW_COMMAND_INVALID_FORMAT = + "Oops! Invalid format! Trying to view your lists?" + LS + + "Type " + QUOTATION + Command.COMMAND_WORD_VIEW + QUOTATION + + " followed by one of the following prefixes:" + LS + INDENTED_LS + + MESSAGE_EXERCISE_LIST_FORMAT + INDENTED_LS + + MESSAGE_FOOD_LIST_FORMAT + INDENTED_LS + + MESSAGE_EXERCISE_BANK_FORMAT + INDENTED_LS + + MESSAGE_FOOD_BANK_FORMAT + INDENTED_LS + + MESSAGE_UPCOMING_EXERCISE_LIST_FORMAT + LS + LS + + "E.G: view e/"; + //edit command + public static final String MESSAGE_EDIT_COMMAND_INVALID_FORMAT = + "Oops! Invalid format! Trying to edit your lists?" + LS + + "Type " + QUOTATION + Command.COMMAND_WORD_EDIT + QUOTATION + + " followed by one of the following prefixes:" + LS + INDENTED_LS + + MESSAGE_EXERCISE_BANK_FORMAT + INDENTED_LS + + MESSAGE_FOOD_BANK_FORMAT + INDENTED_LS + + MESSAGE_UPCOMING_EXERCISE_LIST_FORMAT + LS + LS + + "Type " + QUOTATION + HelpCommand.COMMAND_WORD + QUOTATION + + " if you need more details on how to edit each list!"; + public static final String MESSAGE_EDIT_BANK_NEED_DETAILS = + "Please specify what you wish to change about this item using the following prefixes:" + INDENTED_LS + + MESSAGE_NAME_FORMAT + INDENTED_LS + + MESSAGE_ITEM_CALORIES_FORMAT; + public static final String MESSAGE_EDIT_UPCOMING_EXERCISE_LIST_NEED_DETAILS = + "Please specify what you wish to change about this item using the following prefixes:" + INDENTED_LS + + MESSAGE_NAME_FORMAT + INDENTED_LS + + MESSAGE_ITEM_CALORIES_FORMAT + INDENTED_LS + + MESSAGE_ITEM_DATE_FORMAT; + + //profile command + public static final String MESSAGE_PROFILE_COMMAND_INVALID_FORMAT = + "Oops! Invalid format! Trying to update your profile?" + LS + + "Type " + QUOTATION + Command.COMMAND_WORD_PROFILE + QUOTATION + + " followed by any of the following prefixes to specify what you are changing!:" + LS + INDENTED_LS + + MESSAGE_NAME_FORMAT + INDENTED_LS + + MESSAGE_HEIGHT_FORMAT + INDENTED_LS + + MESSAGE_WEIGHT_FORMAT + INDENTED_LS + + MESSAGE_AGE_FORMAT + INDENTED_LS + + MESSAGE_GOAL_FORMAT + INDENTED_LS + + MESSAGE_GENDER_FORMAT + INDENTED_LS + + MESSAGE_ACTIVITY_FACTOR_FORMAT + LS + LS + + "You can use any number of prefixes depending on your needs but please include at least one!" + LS + + "E.G: profile n/John"; +} + + diff --git a/src/main/java/seedu/duke/logic/commands/CommandResult.java b/src/main/java/seedu/duke/logic/commands/CommandResult.java new file mode 100644 index 0000000000..777579db50 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/CommandResult.java @@ -0,0 +1,26 @@ +package seedu.duke.logic.commands; + +/** + * Represents the result of the execution of the commands to be displayed to the user. + */ +public class CommandResult { + public final String messageToBeShown; + private boolean isBye = false; + + public CommandResult(String messageToBeShown) { + this.messageToBeShown = messageToBeShown; + } + + @Override + public String toString() { + return this.messageToBeShown; + } + + public boolean isBye() { + return isBye; + } + + public void setBye(boolean bye) { + isBye = bye; + } +} diff --git a/src/main/java/seedu/duke/logic/commands/DeleteExerciseBankCommand.java b/src/main/java/seedu/duke/logic/commands/DeleteExerciseBankCommand.java new file mode 100644 index 0000000000..089fc298d8 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/DeleteExerciseBankCommand.java @@ -0,0 +1,80 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.Item; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, deletes an Exercise item from the ExerciseBank. + */ +public class DeleteExerciseBankCommand extends Command { + public static final String MESSAGE_COMMAND_FORMAT = CommandMessages.QUOTATION + COMMAND_WORD_DELETE + + " " + COMMAND_PREFIX_EXERCISE_BANK + COMMAND_PREFIX_DELIMITER + "X" + CommandMessages.QUOTATION + + ", where X is the item number in the exercise bank list"; + public static final String MESSAGE_SUCCESS = "An exercise item has been deleted from the exercise bank:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String MESSAGE_ITEMS_LEFT = + CommandMessages.LS + "Number of exercise item(s) left in the exercise bank: %d"; + private static final String MESSAGE_EXERCISE_CLEAR = "All exercise items in the exercise bank have been removed."; + public static final String[] EXPECTED_PREFIXES = {COMMAND_PREFIX_EXERCISE_BANK}; + private static final String MESSAGE_REMOVED_MULTIPLE_EXERCISE_BANK_ITEM = "All of the following exercise bank " + + "items have been deleted:"; + + + private static Logger logger = Logger.getLogger(DeleteExerciseBankCommand.class.getName()); + + private boolean isClear = false; + private ArrayList itemIndexArray; + + public DeleteExerciseBankCommand(boolean isClear) { + this.isClear = isClear; + } + + public DeleteExerciseBankCommand(ArrayList itemIndexArray) { + this.itemIndexArray = itemIndexArray; + } + + @Override + public CommandResult execute() { + if (super.exerciseBank.getSize() == 0) { + logger.log(Level.FINE, "Exercise bank is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_EXERCISE_BANK); + } + if (this.isClear) { + logger.log(Level.FINE, "Clearing exercise bank"); + super.exerciseBank.clearList(); + return new CommandResult(MESSAGE_EXERCISE_CLEAR); + } + logger.log(Level.FINE, "Trying to delete item now"); + try { + if ((itemIndexArray.size() == 1)) { + Item item = super.exerciseBank.deleteItem(itemIndexArray.get(0)); + return new CommandResult( + String.format(MESSAGE_SUCCESS, item.toStringWithoutDateAndTime()) + + String.format(MESSAGE_ITEMS_LEFT, super.exerciseBank.getSize())); + } + if ((itemIndexArray.stream() + .filter(number -> number > super.exerciseBank.getSize() - 1).count()) > 0) { + if (super.exerciseBank.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.exerciseBank.getSize())); + } else { + String listOfDeletedExerciseBank = super.exerciseBank.deleteMultipleItems(this.itemIndexArray); + return new CommandResult(MESSAGE_REMOVED_MULTIPLE_EXERCISE_BANK_ITEM + + listOfDeletedExerciseBank + + String.format(MESSAGE_ITEMS_LEFT, super.exerciseBank.getSize())); + } + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid exercise item index."); + if (super.exerciseBank.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.exerciseBank.getSize())); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/DeleteExerciseCommand.java b/src/main/java/seedu/duke/logic/commands/DeleteExerciseCommand.java new file mode 100644 index 0000000000..5fb5f75269 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/DeleteExerciseCommand.java @@ -0,0 +1,60 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.exercise.Exercise; + +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, deletes an Exercise item from the ExerciseList. + */ +public class DeleteExerciseCommand extends Command { + public static final String MESSAGE_SUCCESS = "An exercise item has been deleted:" + + CommandMessages.INDENTED_LS + "%s"; + private static final String MESSAGE_EXERCISE_CLEAR = "All exercise items have been removed."; + + private static Logger logger = Logger.getLogger(DeleteExerciseCommand.class.getName()); + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_EXERCISE, + COMMAND_PREFIX_DATE + }; + + + private int itemIndex; + private LocalDate date; + private boolean isClear = false; + + public DeleteExerciseCommand(int itemIndex, LocalDate date) { + this.itemIndex = itemIndex; + this.date = date; + } + + public DeleteExerciseCommand(boolean isClear) { + this.isClear = isClear; + } + + @Override + public CommandResult execute() { + if (this.isClear) { + logger.log(Level.FINE, "Clearing exercise list"); + super.exerciseItems.clearList(); + return new CommandResult(MESSAGE_EXERCISE_CLEAR); + } + if (super.exerciseItems.getSize() == 0) { + logger.log(Level.FINE, "Exercise list is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_EXERCISE_LIST); + } + logger.log(Level.FINE, "Trying to delete item now"); + try { + Exercise deletedExercise; + deletedExercise = super.exerciseItems.deleteItem(this.itemIndex, this.date); + return new CommandResult(String.format(MESSAGE_SUCCESS, + deletedExercise.toStringWithDate())); + } catch (IndexOutOfBoundsException e) { + return new CommandResult(String.format(CommandMessages.MESSAGE_EXERCISE_NOT_FOUND, + this.itemIndex + 1, + this.date.format(CommandMessages.DATE_FORMATTER))); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/DeleteFoodBankCommand.java b/src/main/java/seedu/duke/logic/commands/DeleteFoodBankCommand.java new file mode 100644 index 0000000000..80a1e3d282 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/DeleteFoodBankCommand.java @@ -0,0 +1,83 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.Item; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, deletes a Food item from the FoodBank. + */ +public class DeleteFoodBankCommand extends Command { + public static final String MESSAGE_COMMAND_FORMAT = CommandMessages.QUOTATION + COMMAND_WORD_DELETE + + " " + COMMAND_PREFIX_FOOD_BANK + COMMAND_PREFIX_DELIMITER + "X" + CommandMessages.QUOTATION + + ", where X is the item number in the food bank list"; + public static final String MESSAGE_SUCCESS = "A food item has been deleted from the food bank:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String MESSAGE_ITEMS_LEFT = + CommandMessages.LS + "Number of food item(s) left in the food bank: %d"; + private static final String MESSAGE_FOOD_CLEAR = "All food items in the food bank have been removed."; + public static final String[] EXPECTED_PREFIXES = {COMMAND_PREFIX_FOOD_BANK}; + private static final String MESSAGE_REMOVED_MULTIPLE_FOOD_BANK_ITEM = + "All of the following food bank items have been deleted:"; + + + private static Logger logger = Logger.getLogger(DeleteFoodBankCommand.class.getName()); + + + private boolean isClear = false; + private ArrayList itemIndexArray; + + + public DeleteFoodBankCommand(boolean isClear) { + this.isClear = isClear; + } + + public DeleteFoodBankCommand(ArrayList itemIndexArray) { + this.itemIndexArray = itemIndexArray; + } + + @Override + public CommandResult execute() { + if (super.foodBank.getSize() == 0) { + logger.log(Level.FINE, "food bank is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FOOD_BANK); + } + if (this.isClear) { + logger.log(Level.FINE, "Clearing food bank"); + super.foodBank.clearList(); + return new CommandResult(MESSAGE_FOOD_CLEAR); + } + + logger.log(Level.FINE, "Trying to delete item now"); + try { + if ((itemIndexArray.size() == 1)) { + Item item = super.foodBank.deleteItem(itemIndexArray.get(0)); + return new CommandResult( + String.format(MESSAGE_SUCCESS, item.toStringWithoutDateAndTime()) + + String.format(MESSAGE_ITEMS_LEFT, super.foodBank.getSize())); + } + if ((itemIndexArray.stream() + .filter(number -> number > super.foodBank.getSize() - 1).count()) > 0) { + if (super.foodBank.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.foodBank.getSize())); + } else { + String listOfDeletedFoodBank = super.foodBank.deleteMultipleItems(this.itemIndexArray); + return new CommandResult(MESSAGE_REMOVED_MULTIPLE_FOOD_BANK_ITEM + + listOfDeletedFoodBank + + String.format(MESSAGE_ITEMS_LEFT, super.foodBank.getSize())); + } + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid food item index."); + if (super.foodBank.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.foodBank.getSize())); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/DeleteFoodCommand.java b/src/main/java/seedu/duke/logic/commands/DeleteFoodCommand.java new file mode 100644 index 0000000000..56ec9e8ed7 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/DeleteFoodCommand.java @@ -0,0 +1,67 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.food.Food; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, deletes a Food item from the FoodList. + */ +public class DeleteFoodCommand extends Command { + public static final String MESSAGE_SUCCESS = "A food item has been deleted:" + + CommandMessages.INDENTED_LS + "%1$s"; + public static final String MESSAGE_FOOD_CLEAR = "All food items have been removed."; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_FOOD, + COMMAND_PREFIX_DATE, + COMMAND_PREFIX_TIME, + }; + + + private int itemIndex; + private LocalDate date; + private LocalTime time; + private boolean isClear = false; + + private static final Logger logger = Logger.getLogger(DeleteFoodCommand.class.getName()); + + public DeleteFoodCommand(int itemIndex, LocalDate date, LocalTime time) { + this.itemIndex = itemIndex; + this.date = date; + this.time = time; + } + + public DeleteFoodCommand(boolean isClear) { + this.isClear = isClear; + } + + @Override + public CommandResult execute() { + if (this.isClear) { + logger.log(Level.FINE, "Clearing food list"); + super.foodItems.clearList(); + assert foodItems.getSize() == 0 : "The size of the food list should be 0 after clear"; + return new CommandResult(MESSAGE_FOOD_CLEAR); + } + if (super.foodItems.getSize() == 0) { + logger.log(Level.FINE, "Food list is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FOOD_LIST); + } + logger.log(Level.FINE, "Trying to delete item now"); + try { + Food deletedFood; + deletedFood = super.foodItems.deleteItem(this.itemIndex, this.date, this.time); + return new CommandResult(String.format(MESSAGE_SUCCESS, + deletedFood.toStringWithDate())); + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid food item index."); + return new CommandResult(String.format(CommandMessages.MESSAGE_FOOD_NOT_FOUND, + this.itemIndex + 1, + this.date.format(CommandMessages.DATE_FORMATTER), + this.time.format(CommandMessages.TIME_FORMATTER))); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/DeleteFutureExerciseCommand.java b/src/main/java/seedu/duke/logic/commands/DeleteFutureExerciseCommand.java new file mode 100644 index 0000000000..db2c5e8982 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/DeleteFutureExerciseCommand.java @@ -0,0 +1,83 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.Item; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, deletes an Exercise item from the Future ExerciseList. + */ +public class DeleteFutureExerciseCommand extends Command { + public static final String MESSAGE_COMMAND_FORMAT = CommandMessages.QUOTATION + COMMAND_WORD_DELETE + + " " + COMMAND_PREFIX_UPCOMING_EXERCISE + COMMAND_PREFIX_DELIMITER + "X" + CommandMessages.QUOTATION + + ", where X is the item number in the future exercise list"; + public static final String MESSAGE_SUCCESS = "An exercise item for the future has been deleted:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String MESSAGE_ITEMS_LEFT = + CommandMessages.LS + "Number of upcoming exercise(s) left: %d"; + private static final String MESSAGE_FUTURE_EXERCISE_CLEAR = "All upcoming exercise items have been removed."; + public static final String[] EXPECTED_PREFIXES = {COMMAND_PREFIX_UPCOMING_EXERCISE}; + public static final String MESSAGE_REMOVED_MULTIPLE_UPCOMING_EXERCISES = "All of the following upcoming exercises " + + "have been deleted:"; + + + private static Logger logger = Logger.getLogger(DeleteFutureExerciseCommand.class.getName()); + + private boolean isClear = false; + private ArrayList itemIndexArray; + + + public DeleteFutureExerciseCommand(boolean isClear) { + this.isClear = isClear; + } + + public DeleteFutureExerciseCommand(ArrayList itemIndexArray) { + this.itemIndexArray = itemIndexArray; + } + + @Override + public CommandResult execute() { + if (super.futureExerciseItems.getSize() == 0) { + logger.log(Level.FINE, "Future exercise list is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FUTURE_EXERCISE_LIST); + } + if (this.isClear) { + logger.log(Level.FINE, "Clearing future exercise list"); + super.futureExerciseItems.clearList(); + return new CommandResult(MESSAGE_FUTURE_EXERCISE_CLEAR); + } + + logger.log(Level.FINE, "Trying to delete item now"); + try { + if ((itemIndexArray.size() == 1)) { + Item item = super.futureExerciseItems.deleteItem(itemIndexArray.get(0)); + return new CommandResult( + String.format(MESSAGE_SUCCESS, item.toStringWithDate()) + + String.format(MESSAGE_ITEMS_LEFT, super.futureExerciseItems.getSize())); + } + if ((itemIndexArray.stream() + .filter(number -> number > super.futureExerciseItems.getSize() - 1).count()) > 0) { + if (super.futureExerciseItems.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format(CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, + super.futureExerciseItems.getSize())); + } else { + String listOfDeletedFutureExercises = super.futureExerciseItems + .deleteMultipleItems(this.itemIndexArray); + return new CommandResult(MESSAGE_REMOVED_MULTIPLE_UPCOMING_EXERCISES + + listOfDeletedFutureExercises + + String.format(MESSAGE_ITEMS_LEFT, super.futureExerciseItems.getSize())); + } + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid exercise item index."); + if (super.futureExerciseItems.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format(CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, + super.futureExerciseItems.getSize())); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/EditExerciseBankCommand.java b/src/main/java/seedu/duke/logic/commands/EditExerciseBankCommand.java new file mode 100644 index 0000000000..c8df341263 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/EditExerciseBankCommand.java @@ -0,0 +1,71 @@ +package seedu.duke.logic.commands; + + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.exceptions.DuplicateItemInBankException; + +import java.util.logging.Level; +import java.util.logging.Logger; + +//@@author xingjie99 + +/** + * Represents the command that when executed, edits an item in the Exercise Bank. + */ +public class EditExerciseBankCommand extends Command { + public static final String MESSAGE_SUCCESS = "Item number %d in the exercise bank has been changed to:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_EXERCISE_BANK, + COMMAND_PREFIX_NAME, + COMMAND_PREFIX_CALORIES + }; + + + private static Logger logger = Logger.getLogger(EditExerciseBankCommand.class.getName()); + + private final int itemIndex; + private final String newName; + private final Integer newCalories; + + public EditExerciseBankCommand(int itemIndex, String newName, Integer newCalories) { + this.itemIndex = itemIndex; + this.newName = newName; + this.newCalories = newCalories; + } + + @Override + public CommandResult execute() { + if (super.exerciseBank.getSize() == 0) { + logger.log(Level.FINE, "Exercise bank list is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_EXERCISE_BANK); + } + try { + Item item = super.exerciseBank.getItem(this.itemIndex); + if (this.newName != null) { + super.exerciseBank.checkNoDuplicateItemName(this.newName); + item.setName(this.newName); + } + if (this.newCalories != null) { + if (this.newCalories < Item.LOWEST_CALORIE || this.newCalories > Item.HIGHEST_CALORIE) { + logger.log(Level.FINE, "Detected impossible exercise calorie burnt."); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + item.setCalories(this.newCalories); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, this.itemIndex + 1, + super.exerciseBank.getItem(this.itemIndex).toStringWithoutDateAndTime())); + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid exercise bank item index."); + if (super.exerciseBank.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.exerciseBank.getSize())); + } catch (DuplicateItemInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_EXERCISE_ALREADY_EXISTS_IN_BANK, this.newName)); + } + } + +} diff --git a/src/main/java/seedu/duke/logic/commands/EditFoodBankCommand.java b/src/main/java/seedu/duke/logic/commands/EditFoodBankCommand.java new file mode 100644 index 0000000000..aeac22a566 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/EditFoodBankCommand.java @@ -0,0 +1,68 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.exceptions.DuplicateItemInBankException; + +import java.util.logging.Level; +import java.util.logging.Logger; + +//@@author xingjie99 + +/** + * Represents the command that when executed, edits an item in the Food Bank. + */ +public class EditFoodBankCommand extends Command { + public static final String MESSAGE_SUCCESS = "Item number %d in the food bank has been changed to:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_FOOD_BANK, + COMMAND_PREFIX_NAME, + COMMAND_PREFIX_CALORIES + }; + + private static Logger logger = Logger.getLogger(EditFoodBankCommand.class.getName()); + + private final int itemIndex; + private final String newName; + private final Integer newCalories; + + public EditFoodBankCommand(int itemIndex, String newName, Integer newCalories) { + this.itemIndex = itemIndex; + this.newName = newName; + this.newCalories = newCalories; + } + + @Override + public CommandResult execute() { + if (super.foodBank.getSize() == 0) { + logger.log(Level.FINE, "Food bank list is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FOOD_BANK); + } + try { + Item item = super.foodBank.getItem(this.itemIndex); + if (this.newName != null) { + super.foodBank.checkNoDuplicateItemName(this.newName); + item.setName(this.newName); + } + if (this.newCalories != null) { + if (this.newCalories < Item.LOWEST_CALORIE || this.newCalories > Item.HIGHEST_CALORIE) { + logger.log(Level.FINE, "Detected impossible exercise calorie burnt."); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + item.setCalories(this.newCalories); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, this.itemIndex + 1, + item.toStringWithoutDateAndTime())); + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid food bank item index."); + if (super.foodBank.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.foodBank.getSize())); + } catch (DuplicateItemInBankException e) { + return new CommandResult(String.format( + CommandMessages.MESSAGE_FOOD_ALREADY_EXISTS_IN_BANK, this.newName)); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/EditFutureExerciseCommand.java b/src/main/java/seedu/duke/logic/commands/EditFutureExerciseCommand.java new file mode 100644 index 0000000000..6781ac7aa7 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/EditFutureExerciseCommand.java @@ -0,0 +1,76 @@ +package seedu.duke.logic.commands; + + +import seedu.duke.data.item.Item; + +import java.time.LocalDate; +import java.util.logging.Level; +import java.util.logging.Logger; + +//@@author xingjie99 + +/** + * Represents the command that when executed, edits an item in the Future Exercise List. + */ +public class EditFutureExerciseCommand extends Command { + public static final String MESSAGE_SUCCESS = "Item number %d in upcoming exercises has been changed to:" + + CommandMessages.INDENTED_LS + "%s"; + public static final String MESSAGE_INVALID_DATE = "The new date must be after today's date!"; + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_UPCOMING_EXERCISE, + COMMAND_PREFIX_NAME, + COMMAND_PREFIX_CALORIES, + COMMAND_PREFIX_DATE, + }; + + private static Logger logger = Logger.getLogger(EditFutureExerciseCommand.class.getName()); + + private final int itemIndex; + private final String newName; + private final Integer newCalories; + private final LocalDate newDate; + + public EditFutureExerciseCommand(int itemIndex, String newName, Integer newCalories, LocalDate newDate) { + this.itemIndex = itemIndex; + this.newName = newName; + this.newCalories = newCalories; + this.newDate = newDate; + } + + @Override + public CommandResult execute() { + if (super.futureExerciseItems.getSize() == 0) { + logger.log(Level.FINE, "Future exercise list is empty."); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FUTURE_EXERCISE_LIST); + } + try { + Item item = super.futureExerciseItems.getItem(this.itemIndex); + if (this.newName != null) { + item.setName(this.newName); + } + if (this.newCalories != null) { + if (this.newCalories < Item.LOWEST_CALORIE || this.newCalories > Item.HIGHEST_CALORIE) { + logger.log(Level.FINE, "Detected impossible exercise calorie burnt."); + return new CommandResult(CommandMessages.MESSAGE_INVALID_CALORIES); + } + item.setCalories(this.newCalories); + } + if (this.newDate != null) { + if (!this.newDate.isAfter(LocalDate.now())) { + return new CommandResult(MESSAGE_INVALID_DATE); + } + item.setDate(this.newDate); + } + super.futureExerciseItems.sortList(); + return new CommandResult(String.format(MESSAGE_SUCCESS, this.itemIndex + 1, + item.toStringWithDate())); + } catch (IndexOutOfBoundsException e) { + logger.log(Level.FINE, "Detected invalid exercise item index."); + if (super.futureExerciseItems.getSize() == 1) { + return new CommandResult(CommandMessages.MESSAGE_ONLY_ONE_IN_LIST); + } + return new CommandResult(String.format( + CommandMessages.MESSAGE_LIST_OUT_OF_BOUNDS, super.futureExerciseItems.getSize())); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/HelpCommand.java b/src/main/java/seedu/duke/logic/commands/HelpCommand.java new file mode 100644 index 0000000000..ffc69dccce --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/HelpCommand.java @@ -0,0 +1,181 @@ +package seedu.duke.logic.commands; + +import static seedu.duke.logic.commands.CommandMessages.LS; + +/** + * Represents the command that when executed, displays the list of available commands to the user. + */ +public class HelpCommand extends Command { + + public static final String COMMAND_WORD = "help"; + public static final String MESSAGE_ADD_HELP = "add -- Add food or exercise record to the current list."; + private static final String MESSAGE_HELP_INTRO = "Welcome to the help page." + LS + + "Below are the commands to get you started." + LS + + "More details could be found on: " + LS + + "https://tinyurl.com/fitbot-user-guide"; + private static final String MESSAGE_COMMON_NOTATIONS = "In the formats of the command, prefixes wrapped in " + + "curly brackets {} means that they are optional."; + private static final String EMPTY = ""; + public static final String MESSAGE_COMMAND_FORMAT = + CommandMessages.QUOTATION + COMMAND_WORD + CommandMessages.QUOTATION; + public static final String MESSAGE_DELETE_HELP = "delete -- Delete entry of food or exercise added from a list."; + public static final String MESSAGE_EDIT_HELP = "edit -- Edit entry of food or exercise added from a list."; + public static final String MESSAGE_VIEW_HELP = "view -- View all the food and/or exercises added."; + + private static String bmi = "bmi -- Calculate the Body-Mass-Index of user" + LS + + " Format: bmi {h/HEIGHT_IN_CM w/WEIGHT_IN_KG}" + LS + + " Prefix Input " + LS + + " h/ Height of user in cm" + LS + + " w/ Weight of user in kg" + LS + + " If no prefixes are given, bmi will be calculated using the height and weight in the profile."; + private static String help = LS + + "help -- View help for commands" + LS + + " Format: help"; + private static String profile = LS + + "profile -- View or modify profile details" + LS + + " Format: profile {n/NAME} {h/HEIGHT(CM)} {w/WEIGHT(KG)} {a/AGE} {g/CALORIEGOAL} {s/GENDER(M/F)}" + + " {x/ACTIVITYFACTOR(1-5)}" + LS + + " Prefix Input " + LS + + " n/ Name of user" + LS + + " h/ Height of user in cm" + LS + + " w/ Weight of user in kg" + LS + + " s/ Gender of user, m for male, f for female" + LS + + " a/ Age of user" + LS + + " g/ Net calorie goal of user." + LS + + " Net calorie is based on calorie of food consumed - " + + "calories burnt from exercise and bmr" + LS + + " x/ Activity factor of user, range 1 to 5" + LS + + " If no prefixes are given, user will be shown the current profile particulars."; + private static String overview = LS + + "overview -- View weekly and daily summary of calories" + LS + + " Format: overview"; + private static String bye = LS + + "bye -- Exit the program." + LS + + " Format: bye"; + private static String add = " Add Food Item" + LS + + " Format: add f/ITEM {c/CALORIES} {d/DD-MM-YYYY} {t/HHMM}" + LS + + " Prefix Input" + LS + + " f/ Description of the food" + LS + + " c/ Calories of the food" + LS + + " d/ Date of food in DD-MM-YYYY" + LS + + " t/ Time of food in HHMM" + LS + + LS + + " Add Exercise Item" + LS + + " Format: add f/ITEM {c/CALORIES} {d/DD-MM-YYYY} {t/HHMM}" + LS + + " Prefix Input" + LS + + " e/ Description of exercise" + LS + + " c/ Calories burnt from exercise" + LS + + " d/ Date of exercise in DD-MM-YYYY" + LS + + LS + + " Add Recurring Exercise to Upcoming Exercise List" + LS + + " Format: add r/ITEM c/CALORIES :/START_DATE -/END_DATE @/DAY_OF_THE_WEEK {,DAY_OF_THE_WEEK,...}" + + LS + + " Format: delete e/LIST_NO d/DD-MM-YYYY" + LS + + " Prefix Input" + LS + + " r/ Description of upcoming exercise" + LS + + " c/ Calories burnt from exercise" + LS + + " :/ Start date of exercise in DD-MM-YYYY" + LS + + " -/ End date of exercise in DD-MM-YYYY" + LS + + " @/ Workout days of the week" + LS + + LS + + " Add Food Item in Food Bank" + LS + + " Format: add fbank/ITEM c/CALORIES" + LS + + " Prefix Input" + LS + + " fbank/ Description of food" + LS + + " c/ Calories of the food" + LS + + LS + + " Add Exercise Item in ExerciseBank" + LS + + " Format: add ebank/ITEM c/CALORIES" + LS + + " Prefix Input" + LS + + " ebank/ Description of exercise" + LS + + " c/ Calories burnt from exercise"; + private static String view = LS + + " Viewing Food List" + LS + + " Format: view f/" + LS + + LS + + " View Exercise List" + LS + + " Format: view e/" + LS + + LS + + " View Upcoming Exercise List" + LS + + " Format: view u/" + LS + + LS + + " View Food Bank" + LS + + " Format: view fbank/" + LS + + LS + + " View Exercise Bank" + LS + + " Format: view ebank/"; + + private static String delete = " Deleting Food Item" + LS + + " Format: delete f/LIST_NO d/DD-MM-YYYY t/HHMM" + LS + + " Prefix Input" + LS + + " f/ Index of food in Food List within the given date" + LS + + " d/ Date of food in DD-MM-YYYY" + LS + + " t/ Time of food in HHMM" + LS + + LS + + " Delete Exercise Item" + LS + + " Format: delete e/LIST_NO d/DD-MM-YYYY" + LS + + " Prefix Input" + LS + + " e/ Index of exercise in Exercise List within the given date" + LS + + " d/ Date of exercise in DD-MM-YYYY" + LS + + " " + LS + + " Delete Upcoming Exercise Items from Upcoming Exercise List" + LS + + " Format: delete u/LIST_NO {,LIST_NO,...}" + LS + + " Prefix Input" + LS + + " u/ The index of the item within the Upcoming Exercise List" + LS + + LS + + " Delete Food Item from Food Bank" + LS + + " Format: delete fbank/LIST_NO {,LIST_NO,...}" + LS + + " Prefix Input" + LS + + " fbank/ The index of the item within the Food Bank" + LS + + LS + + " Delete Exercise Item from Exercise Bank" + LS + + " Format: delete ebank/LIST_NO {,LIST_NO,...}" + LS + + " Prefix Input" + LS + + " ebank/ The index of the item within the Exercise Bank"; + private static String edit = " Edit Food Bank" + LS + + " Format: edit fbank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}" + LS + + " Prefix Input" + LS + + " fbank/ The index of the item within the Food Bank" + LS + + " n/ New description of food name" + LS + + " c/ Calories of food" + LS + + " " + LS + + " Edit Exercise Bank" + LS + + " Format: edit ebank/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}" + LS + + " Prefix Input" + LS + + " ebank/ The index of the item within the Exercise Bank" + LS + + " n/ New description of exercise name" + LS + + " c/ Calories burnt from exercise" + LS + + LS + + " Edit Upcoming Exercise List" + LS + + " Format: edit u/LIST_NO {n/NEW_NAME} {c/NEW_CALORIES}" + LS + + " Prefix Input" + LS + + " u/ The index of the item within the Upcoming Exercise List" + LS + + " n/ New description of exercise name" + LS + + " c/ Calories burnt from exercise"; + + private String buildHelpString2() { + StringBuilder helpMessage = new StringBuilder(EMPTY); + helpMessage.append(MESSAGE_HELP_INTRO).append(LS).append(LS) + .append(MESSAGE_COMMON_NOTATIONS).append(LS).append(LS) + .append(MESSAGE_ADD_HELP).append(LS) + .append(add).append(LS).append(LS) + .append(bmi).append(LS) + .append(bye).append(LS).append(LS) + .append(MESSAGE_DELETE_HELP).append(LS) + .append(delete).append(LS).append(LS) + .append(MESSAGE_EDIT_HELP).append(LS) + .append(edit).append(LS) + .append(help).append(LS) + .append(profile).append(LS) + .append(overview).append(LS).append(LS) + .append(MESSAGE_VIEW_HELP).append(LS) + .append(view).append(LS); + + return helpMessage.toString().trim(); + } + + @Override + public CommandResult execute() { + return new CommandResult(buildHelpString2()); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/InvalidCommand.java b/src/main/java/seedu/duke/logic/commands/InvalidCommand.java new file mode 100644 index 0000000000..119bfad15c --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/InvalidCommand.java @@ -0,0 +1,18 @@ +package seedu.duke.logic.commands; + +/** + * Represents any command that cannot be executed due to invalid format. + * Contains a String of message that describes the error. + */ +public class InvalidCommand extends Command { + public final String errorMessage; + + public InvalidCommand(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public CommandResult execute() { + return new CommandResult(this.errorMessage); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/OverviewCommand.java b/src/main/java/seedu/duke/logic/commands/OverviewCommand.java new file mode 100644 index 0000000000..5418cdc251 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/OverviewCommand.java @@ -0,0 +1,18 @@ +package seedu.duke.logic.commands; + +import seedu.duke.logic.Statistics; + +/** + * Represents the command that when executed, displays the overview of calorie statistics. + */ +public class OverviewCommand extends Command { + public static final String COMMAND_WORD = "overview"; + protected Statistics statistics; + + + @Override + public CommandResult execute() { + statistics = new Statistics(super.foodItems, super.exerciseItems, super.profile); + return new CommandResult(this.statistics.overviewSummary()); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ProfileCommand.java b/src/main/java/seedu/duke/logic/commands/ProfileCommand.java new file mode 100644 index 0000000000..49c6ffaae2 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ProfileCommand.java @@ -0,0 +1,15 @@ +package seedu.duke.logic.commands; + +/** + * Represents the command that when executed, shows the value of name, height and weight in the Profile. + */ +public class ProfileCommand extends Command { + + public ProfileCommand() { + } + + @Override + public CommandResult execute() { + return new CommandResult(super.profile.convertToString()); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ProfileUpdateCommand.java b/src/main/java/seedu/duke/logic/commands/ProfileUpdateCommand.java new file mode 100644 index 0000000000..e66c7cc5bb --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ProfileUpdateCommand.java @@ -0,0 +1,96 @@ +package seedu.duke.logic.commands; + +import seedu.duke.data.profile.attributes.ActivityFactor; +import seedu.duke.data.profile.attributes.Age; +import seedu.duke.data.profile.attributes.CalorieGoal; +import seedu.duke.data.profile.attributes.Gender; +import seedu.duke.data.profile.attributes.Height; +import seedu.duke.data.profile.attributes.Name; +import seedu.duke.data.profile.attributes.Weight; +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; +import seedu.duke.data.profile.utilities.ProfileUtils; + +/** + * Represents the command that when executed, changes the value of attributes in the Profile. + */ +public class ProfileUpdateCommand extends Command { + + public static final String[] EXPECTED_PREFIXES = { + COMMAND_PREFIX_NAME, + COMMAND_PREFIX_HEIGHT, + COMMAND_PREFIX_WEIGHT, + COMMAND_PREFIX_GOAL, + COMMAND_PREFIX_AGE, + COMMAND_PREFIX_ACTIVITY_FACTOR, + COMMAND_PREFIX_GENDER + }; + + public static final String MESSAGE_SUCCESS = "Your profile has been updated!" + CommandMessages.LS + "%s"; + + private Name name; + private Weight weight; + private Height height; + private CalorieGoal calorieGoal; + private Age age; + private ActivityFactor activityFactor; + private Gender gender; + + + public ProfileUpdateCommand(String name, Double height, Double weight, Integer calorieGoal, Integer age, + Integer activityFactor, Character gender) { + this.name = name == null ? null : new Name(name); + this.height = height == null ? null : new Height(height); + this.weight = weight == null ? null : new Weight(weight); + this.calorieGoal = calorieGoal == null ? null : new CalorieGoal(calorieGoal); + this.gender = gender == NULL_CHAR ? null : new Gender(gender); + this.age = age == null ? null : new Age(age); + this.activityFactor = activityFactor == null ? null : new ActivityFactor(activityFactor); + + } + + private void checkIfCommandShouldExecute() throws InvalidCharacteristicException { + if (!height.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_HEIGHT); + } + if (!weight.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_WEIGHT); + } + if (!gender.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_GENDER); + } + if (!age.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_AGE); + } + if (!calorieGoal.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_CALORIE_GOAL); + } + if (!activityFactor.isValid()) { + throw new InvalidCharacteristicException(ProfileUtils.ERROR_ACTIVITY_FACTOR); + } + } + + @Override + public CommandResult execute() { + try { + this.name = name == null ? super.profile.getProfileName() : name; + this.height = height == null ? super.profile.getProfileHeight() : height; + this.weight = weight == null ? super.profile.getProfileWeight() : weight; + this.gender = gender == null ? super.profile.getProfileGender() : gender; + this.age = age == null ? super.profile.getProfileAge() : age; + this.calorieGoal = calorieGoal == null + ? super.profile.getProfileCalorieGoal() + : calorieGoal; + this.activityFactor = activityFactor == null + ? super.profile.getProfileActivityFactor() + : activityFactor; + + checkIfCommandShouldExecute(); + super.profile.setProfile(this.name, this.height, this.weight, + this.gender, this.age, this.calorieGoal, this.activityFactor); + return new CommandResult(String.format( + MESSAGE_SUCCESS, super.profile.convertToString())); + } catch (InvalidCharacteristicException e) { + return new CommandResult(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ViewExerciseBankCommand.java b/src/main/java/seedu/duke/logic/commands/ViewExerciseBankCommand.java new file mode 100644 index 0000000000..f3d8b5e065 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ViewExerciseBankCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.logic.commands; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, lists all the items in the ExerciseBank. + */ +public class ViewExerciseBankCommand extends Command { + public static final String MESSAGE_SUCCESS = "You have %1$d exercise(s) in your exercise bank:" + + CommandMessages.LS + "%2$s"; + + private static Logger logger = Logger.getLogger("ViewExerciseBankCommand"); + + @Override + public CommandResult execute() { + if (super.exerciseBank.getSize() == 0) { + logger.log(Level.FINE, "Exercise bank is empty"); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_EXERCISE_BANK); + } + assert exerciseBank.getSize() > 0 : "Exercise bank is not empty"; + return new CommandResult(String.format(MESSAGE_SUCCESS, super.exerciseBank.getSize(), + super.exerciseBank.convertToString())); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ViewExerciseListCommand.java b/src/main/java/seedu/duke/logic/commands/ViewExerciseListCommand.java new file mode 100644 index 0000000000..048e9bda99 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ViewExerciseListCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.logic.commands; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, lists all the items in the ExerciseList. + */ +public class ViewExerciseListCommand extends Command { + public static final String MESSAGE_SUCCESS = "Here is a summary of all the exercises you have done " + + "in the past week:" + CommandMessages.LS + "%1$s"; + + + private static Logger logger = Logger.getLogger("ViewExerciseCommand"); + + @Override + public CommandResult execute() { + if (super.exerciseItems.getSize() == 0) { + logger.log(Level.FINE, "Exercise list is empty"); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_EXERCISE_LIST); + } + assert exerciseItems.getSize() > 0 : "Exercise list is not empty"; + return new CommandResult(String.format(MESSAGE_SUCCESS, super.exerciseItems.convertToString())); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ViewFoodBankCommand.java b/src/main/java/seedu/duke/logic/commands/ViewFoodBankCommand.java new file mode 100644 index 0000000000..468ad48c15 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ViewFoodBankCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.logic.commands; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, lists all the items in the FoodBank. + */ +public class ViewFoodBankCommand extends Command { + public static final String MESSAGE_SUCCESS = "You have %1$d food(s) in your food bank:" + + CommandMessages.LS + "%2$s"; + + private static Logger logger = Logger.getLogger("ViewFoodBankCommand"); + + @Override + public CommandResult execute() { + if (super.foodBank.getSize() == 0) { + logger.log(Level.FINE, "Food bank is empty"); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FOOD_BANK); + } + assert foodBank.getSize() > 0 : "Food bank is not empty"; + return new CommandResult(String.format(MESSAGE_SUCCESS, super.foodBank.getSize(), + super.foodBank.convertToString())); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ViewFoodListCommand.java b/src/main/java/seedu/duke/logic/commands/ViewFoodListCommand.java new file mode 100644 index 0000000000..9e084f91d8 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ViewFoodListCommand.java @@ -0,0 +1,18 @@ +package seedu.duke.logic.commands; + +/** + * Represents the command that when executed, lists all the items in the FoodList. + */ +public class ViewFoodListCommand extends Command { + public static final String MESSAGE_SUCCESS = "Here is a summary of all the food items you have consumed " + + "in the past week:" + CommandMessages.LS + "%1$s"; + + + @Override + public CommandResult execute() { + if (super.foodItems.getSize() == 0) { + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FOOD_LIST); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, super.foodItems.convertToString())); + } +} diff --git a/src/main/java/seedu/duke/logic/commands/ViewFutureExerciseListCommand.java b/src/main/java/seedu/duke/logic/commands/ViewFutureExerciseListCommand.java new file mode 100644 index 0000000000..351ab7d071 --- /dev/null +++ b/src/main/java/seedu/duke/logic/commands/ViewFutureExerciseListCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.logic.commands; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the command that when executed, lists all the items in the FutureExerciseList. + */ +public class ViewFutureExerciseListCommand extends Command { + public static final String MESSAGE_FUTURE_EXERCISE = "You have %d upcoming exercise(s):" + + CommandMessages.LS + "%2$s"; + + private static Logger logger = Logger.getLogger("ViewFutureExerciseCommand"); + + @Override + public CommandResult execute() { + if (super.futureExerciseItems.getSize() == 0) { + logger.log(Level.FINE, "Future exercise list is empty"); + return new CommandResult(CommandMessages.MESSAGE_EMPTY_FUTURE_EXERCISE_LIST); + } + assert futureExerciseItems.getSize() > 0 : "Future exercise list is not empty"; + return new CommandResult(String.format(MESSAGE_FUTURE_EXERCISE, super.futureExerciseItems.getSize(), + super.futureExerciseItems.convertToString())); + } +} diff --git a/src/main/java/seedu/duke/logic/parser/AddCommandParser.java b/src/main/java/seedu/duke/logic/parser/AddCommandParser.java new file mode 100644 index 0000000000..70ef12aecc --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/AddCommandParser.java @@ -0,0 +1,201 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.AddExerciseBankCommand; +import seedu.duke.logic.commands.AddExerciseCommand; +import seedu.duke.logic.commands.AddFoodBankCommand; +import seedu.duke.logic.commands.AddFoodCommand; +import seedu.duke.logic.commands.AddFutureExerciseCommand; +import seedu.duke.logic.commands.AddRecurringExerciseCommand; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.parser.exceptions.ItemNotSpecifiedException; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.logic.parser.exceptions.ParserException; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Parses input arguments for Add commands. + */ +public class AddCommandParser implements Parser { + + public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd MMM yyyy"); + protected static final Logger logger = Logger.getLogger(AddCommandParser.class.getName()); + + @Override + public Command parse(String params) { + try { + final String itemTypePrefix = ParserUtils.extractItemTypePrefix(params); + switch (itemTypePrefix) { + case Command.COMMAND_PREFIX_EXERCISE: + return parseAddToExercise(params, itemTypePrefix); + case Command.COMMAND_PREFIX_FOOD: + return parseAddToFood(params, itemTypePrefix); + case Command.COMMAND_PREFIX_RECURRING: + return parseAddRecurring(params, itemTypePrefix); + case Command.COMMAND_PREFIX_EXERCISE_BANK: + return parseAddToExerciseBank(params, itemTypePrefix); + case Command.COMMAND_PREFIX_FOOD_BANK: + return parseAddToFoodBank(params, itemTypePrefix); + default: + throw new ItemNotSpecifiedException(); + } + } catch (ItemNotSpecifiedException e) { + return new InvalidCommand(CommandMessages.MESSAGE_ADD_COMMAND_INVALID_FORMAT); + } + } + + protected Command parseAddToExercise(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, AddExerciseCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final String description = ParserUtils.extractItemDescription(params, itemTypePrefix); + final Integer calories = ParserUtils.extractItemCalories(params); + final LocalDate date = ParserUtils.extractDate(params, false); + logger.log(Level.FINE, String.format("date detected is: %s", date)); + if (ParserUtils.isSevenDaysBeforeToday(date)) { + return new InvalidCommand(String.format(ParserMessages.MESSAGE_ERROR_ITEM_DATE_TOO_OLD, + LocalDate.now().minusDays(6).format(DATE_FORMAT), + LocalDate.now().format(DATE_FORMAT))); + } + if (ParserUtils.isFutureDate(date)) { + logger.log(Level.FINE, String.format("adding to future list")); + return new AddFutureExerciseCommand(description, calories, date); + } + return new AddExerciseCommand(description, calories, date); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + + protected Command parseAddToFood(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, AddFoodCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final String description = ParserUtils.extractItemDescription(params, itemTypePrefix); + final Integer calories = ParserUtils.extractItemCalories(params); + final LocalDateTime dateTime = ParserUtils.extractDateTime(params); + logger.log(Level.FINE, String.format("dateTime detected is: %s", dateTime)); + if (!ParserUtils.isWithinSevenDaysFromToday(dateTime.toLocalDate())) { + return new InvalidCommand(String.format(ParserMessages.MESSAGE_ERROR_ITEM_DATE_TOO_OLD, + LocalDate.now().minusDays(6).format(DATE_FORMAT), + LocalDate.now().format(DATE_FORMAT))); + } + return new AddFoodCommand(description, calories, dateTime); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected Command parseAddRecurring(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, AddRecurringExerciseCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final String description = ParserUtils.extractItemDescription(params, itemTypePrefix); + final Integer calories = ParserUtils.extractItemCalories(params); + final LocalDate startDate = extractStartDate(params); + final LocalDate endDate = extractEndDate(params); + final ArrayList dayOfTheWeek = extractDayOfTheWeek(params); + return new AddRecurringExerciseCommand(description, calories, + startDate, endDate, dayOfTheWeek); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + + protected Command parseAddToExerciseBank(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, AddExerciseBankCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final String description = ParserUtils.extractItemDescription(params, itemTypePrefix); + final Integer calories = ParserUtils.extractItemCalories(params); + if (calories == null) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_NO_CALORIES_INFO); + } + return new AddExerciseBankCommand(description, calories); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected Command parseAddToFoodBank(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, AddFoodBankCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final String description = ParserUtils.extractItemDescription(params, itemTypePrefix); + final Integer calories = ParserUtils.extractItemCalories(params); + if (calories == null) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_NO_CALORIES_INFO); + } + return new AddFoodBankCommand(description, calories); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + //@@author xingjie99 + protected static LocalDate extractStartDate(String params) + throws ParserException { + LocalDate startDate = ParserUtils.extractGeneralDate(params, Command.COMMAND_PREFIX_START_DATE); + if (startDate == null) { + logger.log(Level.FINE, "Detected empty start date input after prefix but date is required!"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_START_DATE); + } + return startDate; + } + + protected static LocalDate extractEndDate(String params) + throws ParserException { + LocalDate endDate = ParserUtils.extractGeneralDate(params, Command.COMMAND_PREFIX_END_DATE); + if (endDate == null) { + logger.log(Level.FINE, "Detected empty end date input after prefix but date is required!"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_END_DATE); + } + return endDate; + } + + protected static ArrayList extractDayOfTheWeek(String params) + throws ParserException { + try { + String numberString = ParserUtils.extractRelevantParameter(params, Command.COMMAND_PREFIX_DAY_OF_THE_WEEK); + String[] numberStringArray = numberString.split(Command.COMMAND_INDEX_DELIMITER); + ArrayList daysOfTheWeek = new ArrayList<>(); + for (int i = 0; i < numberStringArray.length; i++) { + String dayString = numberStringArray[i].trim(); + if (dayString.split(" ").length > 1) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_EXTRA_PARAMETERS); + } + Integer day = Integer.parseInt(numberStringArray[i].trim()); + if (day < ParserMessages.MONDAY || day > ParserMessages.SUNDAY) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_DAY_OF_THE_WEEK); + } + if (daysOfTheWeek.contains(day)) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_REPEATED_DAY_OF_THE_WEEK); + } + daysOfTheWeek.add(day); + } + logger.log(Level.FINE, String.format("Days of the week %s", daysOfTheWeek.toString())); + return daysOfTheWeek; + } catch (NumberFormatException e) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_DAY_OF_THE_WEEK); + } catch (MissingParamException e) { + logger.log(Level.FINE, "Detected empty day input after prefix but day is required!"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_DAY_OF_THE_WEEK); + } + } + + +} diff --git a/src/main/java/seedu/duke/logic/parser/BmiParser.java b/src/main/java/seedu/duke/logic/parser/BmiParser.java new file mode 100644 index 0000000000..f2c92fb1c6 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/BmiParser.java @@ -0,0 +1,41 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.CalculateBmiCommand; +import seedu.duke.logic.commands.CalculateBmiWithProfileCommand; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.parser.exceptions.ParserException; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Parses input arguments for Bmi commands. + */ +public class BmiParser implements Parser { + + protected static final Logger logger = Logger.getLogger(BmiParser.class.getName()); + + @Override + public Command parse(String params) { + if (params.trim().equals(ParserMessages.EMPTY)) { + //no additional parameters, assumed to be bmi from current profile + return new CalculateBmiWithProfileCommand(); + } + if (ParserUtils.hasRequiredParams(params, CalculateBmiCommand.EXPECTED_PREFIXES)) { + if (ParserUtils.hasExtraDelimiters(params, CalculateBmiCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + try { + final double height = ParserUtils.extractHeight(params); + final double weight = ParserUtils.extractWeight(params); + return new CalculateBmiCommand(height, weight); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } else { + logger.log(Level.FINE, "Detected invalid input parameters for BMI calculation."); + return new InvalidCommand(CalculateBmiCommand.MESSAGE_INVALID_COMMAND_FORMAT); + } + } +} diff --git a/src/main/java/seedu/duke/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/duke/logic/parser/DeleteCommandParser.java new file mode 100644 index 0000000000..3456b475f6 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/DeleteCommandParser.java @@ -0,0 +1,187 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.commands.DeleteExerciseBankCommand; +import seedu.duke.logic.commands.DeleteExerciseCommand; +import seedu.duke.logic.commands.DeleteFoodBankCommand; +import seedu.duke.logic.commands.DeleteFoodCommand; +import seedu.duke.logic.commands.DeleteFutureExerciseCommand; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.parser.exceptions.ExtraParamException; +import seedu.duke.logic.parser.exceptions.ItemNotSpecifiedException; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.logic.parser.exceptions.ParserException; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Parses input arguments for Delete commands. + */ +public class DeleteCommandParser implements Parser { + + protected static final Logger logger = Logger.getLogger(DeleteCommandParser.class.getName()); + + @Override + public Command parse(String params) { + try { + final String itemTypePrefix = ParserUtils.extractItemTypePrefix(params); + switch (itemTypePrefix) { + case Command.COMMAND_PREFIX_EXERCISE: + return parseDeleteFromExercise(params, itemTypePrefix); + case Command.COMMAND_PREFIX_FOOD: + return parseDeleteFromFood(params, itemTypePrefix); + case Command.COMMAND_PREFIX_UPCOMING_EXERCISE: + return parseDeleteFromFuture(params, itemTypePrefix); + case Command.COMMAND_PREFIX_EXERCISE_BANK: + return parseDeleteFromExerciseBank(params, itemTypePrefix); + case Command.COMMAND_PREFIX_FOOD_BANK: + return parseDeleteFromFoodBank(params, itemTypePrefix); + default: + throw new ItemNotSpecifiedException(); + } + } catch (ItemNotSpecifiedException e) { + return new InvalidCommand(CommandMessages.MESSAGE_DELETE_COMMAND_INVALID_FORMAT); + } + } + + protected Command parseDeleteFromExercise(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, DeleteExerciseCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + boolean isClear = extractIsClear(params, itemTypePrefix); + if (isClear) { + return new DeleteExerciseCommand(true); + } + final int itemIndex = ParserUtils.extractItemIndex(params, itemTypePrefix); + final LocalDate date = ParserUtils.extractDate(params, true); + logger.log(Level.FINE, String.format("date detected is: %s", date)); + + logger.log(Level.FINE, String.format("deleting exercise item %s from %s", itemIndex, date)); + return new DeleteExerciseCommand(itemIndex, date); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected Command parseDeleteFromFood(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, DeleteFoodCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + boolean isClear = extractIsClear(params, itemTypePrefix); + if (isClear) { + return new DeleteFoodCommand(true); + } + final int itemIndex = ParserUtils.extractItemIndex(params, itemTypePrefix); + final LocalDate date = ParserUtils.extractDate(params, true); + logger.log(Level.FINE, String.format("date detected is: %s", date)); + + + final LocalTime time = ParserUtils.extractTime(params, true); + logger.log(Level.FINE, String.format("deleting food item %s from %s %s", itemIndex, date, time)); + return new DeleteFoodCommand(itemIndex, date, time); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected Command parseDeleteFromExerciseBank(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, DeleteExerciseBankCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + boolean isClear = extractIsClear(params, itemTypePrefix); + if (isClear) { + return new DeleteExerciseBankCommand(true); + } + final ArrayList itemIndexes = extractItemIndexes(params, itemTypePrefix); + return new DeleteExerciseBankCommand(itemIndexes); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected Command parseDeleteFromFoodBank(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, DeleteFoodBankCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + boolean isClear = extractIsClear(params, itemTypePrefix); + if (isClear) { + return new DeleteFoodBankCommand(true); + } + final ArrayList itemIndexes = extractItemIndexes(params, itemTypePrefix); + return new DeleteFoodBankCommand(itemIndexes); + + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected Command parseDeleteFromFuture(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, DeleteFutureExerciseCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + boolean isClear = extractIsClear(params, itemTypePrefix); + if (isClear) { + return new DeleteFutureExerciseCommand(true); + } + final ArrayList itemIndexes = extractItemIndexes(params, itemTypePrefix); + + return new DeleteFutureExerciseCommand(itemIndexes); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected static ArrayList extractItemIndexes(String params, String prefix) + throws ParserException { + try { + String numberString = ParserUtils.extractRelevantParameter(params, prefix); + String[] numberStringArray = numberString.split(Command.COMMAND_INDEX_DELIMITER); + ArrayList indices = new ArrayList<>(); + for (int i = 0; i < numberStringArray.length; i++) { + String indexString = numberStringArray[i].trim(); + if (indexString.split(" ").length > 1) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_EXTRA_PARAMETERS); + } + Integer index = (Double.parseDouble(indexString) > Integer.MAX_VALUE) + ? Integer.MAX_VALUE + : ParserUtils.convertItemNumToItemIndex(Integer.parseInt(indexString)); + if (indices.contains(index)) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_DUPLICATE_NUMBERS); + } + if (index < 0) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_ITEM_NUM); + } + indices.add(index); + } + logger.log(Level.FINE, String.format("Indexes are %s", indices.toString())); + return indices; + } catch (NumberFormatException e) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_ITEM_NUM); + } catch (MissingParamException e) { + logger.log(Level.FINE, "Detected empty item num input after prefix but item num is required!"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_ITEM_NUM); + } + } + + protected static boolean extractIsClear(String params, String prefix) { + try { + String description = ParserUtils.extractRelevantParameterWithoutWhitespace(params, prefix); + return description.equalsIgnoreCase(Command.COMMAND_WORD_DELETE_ALL); + } catch (ExtraParamException | MissingParamException e) { + return false; + } + + } + +} + diff --git a/src/main/java/seedu/duke/logic/parser/EditCommandParser.java b/src/main/java/seedu/duke/logic/parser/EditCommandParser.java new file mode 100644 index 0000000000..cdca826861 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/EditCommandParser.java @@ -0,0 +1,122 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.commands.EditExerciseBankCommand; +import seedu.duke.logic.commands.EditFoodBankCommand; +import seedu.duke.logic.commands.EditFutureExerciseCommand; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.parser.exceptions.ItemNotSpecifiedException; +import seedu.duke.logic.parser.exceptions.ParserException; + +import java.time.LocalDate; +import java.util.logging.Logger; + +//@@author xingjie99 + +/** + * Parses input arguments for Edit commands. + */ +public class EditCommandParser implements Parser { + + protected static final Logger logger = Logger.getLogger(EditCommandParser.class.getName()); + + @Override + public Command parse(String params) { + try { + final String itemTypePrefix = ParserUtils.extractItemTypePrefix(params); + switch (itemTypePrefix) { + case Command.COMMAND_PREFIX_EXERCISE_BANK: + return parseEditExerciseBank(params, itemTypePrefix); + case Command.COMMAND_PREFIX_FOOD_BANK: + return parseEditFoodBank(params, itemTypePrefix); + case Command.COMMAND_PREFIX_UPCOMING_EXERCISE: + return parseEditUpcomingExercise(params, itemTypePrefix); + default: + throw new ItemNotSpecifiedException(); + } + } catch (ItemNotSpecifiedException e) { + return new InvalidCommand( + CommandMessages.MESSAGE_EDIT_COMMAND_INVALID_FORMAT); + } + } + + /** + * Parses input arguments for Edit command for exercise bank. + * + * @param params User input arguments + * @param itemTypePrefix Prefix of the exercise banks + * @return Command to execute + */ + protected Command parseEditExerciseBank(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, EditExerciseBankCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final int itemIndex = ParserUtils.extractItemIndex(params, itemTypePrefix); + if (ParserUtils.getNumberOfCorrectParamsDetected(params, + Command.COMMAND_PREFIX_NAME, Command.COMMAND_PREFIX_CALORIES) == 0) { + return new InvalidCommand(CommandMessages.MESSAGE_EDIT_BANK_NEED_DETAILS); + } + final String description = ParserUtils.extractName(params); + final Integer calories = ParserUtils.extractItemCalories(params); + return new EditExerciseBankCommand(itemIndex, description, calories); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + /** + * Parses input arguments for Edit command for food bank. + * + * @param params User input arguments + * @param itemTypePrefix Prefix of food bank + * @return Command to execute + */ + protected Command parseEditFoodBank(String params, String itemTypePrefix) { + try { + if (ParserUtils.hasExtraDelimiters(params, EditFoodBankCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final int itemIndex = ParserUtils.extractItemIndex(params, itemTypePrefix); + if (ParserUtils.getNumberOfCorrectParamsDetected(params, + Command.COMMAND_PREFIX_NAME, Command.COMMAND_PREFIX_CALORIES) == 0) { + return new InvalidCommand(CommandMessages.MESSAGE_EDIT_BANK_NEED_DETAILS); + } + final String description = ParserUtils.extractName(params); + final Integer calories = ParserUtils.extractItemCalories(params); + return new EditFoodBankCommand(itemIndex, description, calories); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + /** + * Parses input arguments for Edit commands for upcoming exercise. + * + * @param params User input arguments + * @param itemTypePrefix Prefix of upcoming exercise + * @return Command to execute + */ + protected Command parseEditUpcomingExercise(String params, String itemTypePrefix) + throws ItemNotSpecifiedException { + try { + if (ParserUtils.hasExtraDelimiters(params, EditFutureExerciseCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + final int itemIndex = ParserUtils.extractItemIndex(params, itemTypePrefix); + if (ParserUtils.getNumberOfCorrectParamsDetected(params, + Command.COMMAND_PREFIX_NAME, Command.COMMAND_PREFIX_CALORIES, Command.COMMAND_PREFIX_DATE) == 0) { + return new InvalidCommand(CommandMessages.MESSAGE_EDIT_UPCOMING_EXERCISE_LIST_NEED_DETAILS); + } + final String description = ParserUtils.extractName(params); + final Integer calories = ParserUtils.extractItemCalories(params); + final LocalDate date = ParserUtils.hasRequiredParams(params, Command.COMMAND_PREFIX_DATE) + ? ParserUtils.extractDate(params, false) + : null; + return new EditFutureExerciseCommand(itemIndex, description, calories, date); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/duke/logic/parser/Parser.java b/src/main/java/seedu/duke/logic/parser/Parser.java new file mode 100644 index 0000000000..e9a534c298 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/Parser.java @@ -0,0 +1,10 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.Command; + +/** + * Represents a Parser that parses input arguments into their respective Command classes. + */ +public interface Parser { + Command parse(String params); +} diff --git a/src/main/java/seedu/duke/logic/parser/ParserManager.java b/src/main/java/seedu/duke/logic/parser/ParserManager.java new file mode 100644 index 0000000000..c4d11ff276 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/ParserManager.java @@ -0,0 +1,96 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.ByeCommand; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.HelpCommand; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.commands.OverviewCommand; +import seedu.duke.logic.parser.exceptions.ExtraParamException; + +import java.util.logging.Logger; + +/** + * Parses user input for the command word + * and calls the appropriate Parser class to determine which command to execute. + */ +public class ParserManager { + + protected static final Logger logger = Logger.getLogger(ParserManager.class.getName()); + + /** + * Returns the correct command to be executed depending on user input. + * Command words are case-insensitive. + * + * @param input Raw user input string + * @return Command class representing the correct command to be executed + */ + public Command parseCommand(String input) { + + if (hasTextFileDelimiter(input)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_ILLEGAL_CHARACTER); + } + + final String[] commandAndParams = splitInputIntoCommandAndParams(input); + final String commandWord = commandAndParams[0].toLowerCase(); //case-insensitive (all lower case) + final String params = " " + commandAndParams[1]; + try { + switch (commandWord) { + case Command.COMMAND_WORD_ADD: + return new AddCommandParser().parse(params); + case Command.COMMAND_WORD_DELETE: + return new DeleteCommandParser().parse(params); + case Command.COMMAND_WORD_VIEW: + return new ViewCommandParser().parse(params); + case Command.COMMAND_WORD_EDIT: + return new EditCommandParser().parse(params); + case Command.COMMAND_WORD_BMI: + return new BmiParser().parse(params); + case Command.COMMAND_WORD_PROFILE: + return new UpdateProfileParser().parse(params); + case OverviewCommand.COMMAND_WORD: + if (!params.trim().isEmpty()) { + throw new ExtraParamException(); + } + return new OverviewCommand(); + case ByeCommand.COMMAND_WORD: + if (!params.trim().isEmpty()) { + throw new ExtraParamException(); + } + return new ByeCommand(); + case HelpCommand.COMMAND_WORD: + if (!params.trim().isEmpty()) { + throw new ExtraParamException(); + } + return new HelpCommand(); + default: + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_COMMAND_DOES_NOT_EXIST); + } + } catch (ExtraParamException e) { + return new InvalidCommand(e.getMessage()); + } + + } + + /** + * Returns a String array where 0th index is command string and 1st index is the remaining parameters. + * Command string and parameter string is assumed to be separated by the first " " in input. + * If no parameters are provided in the input, 1st index will be set to EMPTY. + * + * @param input Raw user input string + * @return String array [command, parameters] + */ + protected static String[] splitInputIntoCommandAndParams(String input) { + String[] commandAndParams = new String[2]; + final String[] inputSplit = input.trim().split(" ", 2); + //command string + commandAndParams[0] = inputSplit[0]; + //param string; if not given, set to EMPTY for error handling + commandAndParams[1] = (inputSplit.length == 2) ? inputSplit[1].trim() : ParserMessages.EMPTY; + return commandAndParams; + } + + protected static boolean hasTextFileDelimiter(String input) { + return input.contains(ParserMessages.FILE_TEXT_DELIMITER); + } + +} diff --git a/src/main/java/seedu/duke/logic/parser/ParserMessages.java b/src/main/java/seedu/duke/logic/parser/ParserMessages.java new file mode 100644 index 0000000000..70e0243f9e --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/ParserMessages.java @@ -0,0 +1,70 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.HelpCommand; + +/** + * Contains all message constants that Parser classes uses. + */ +public class ParserMessages { + protected static final String EMPTY = ""; + protected static final String LS = System.lineSeparator(); + protected static final String QUOTATION = "\""; + protected static final String MESSAGE_ERROR_COMMAND_DOES_NOT_EXIST = "Fitbot is unable to understand this command! " + + LS + "Lost? Try typing " + HelpCommand.MESSAGE_COMMAND_FORMAT + " to see the list of commands!"; + protected static final String MESSAGE_ERROR_NO_DESCRIPTION = "Please input a description for this item!"; + protected static final String MESSAGE_ERROR_NAME_EMPTY_STRING = "Please do not use an empty string as your name!"; + protected static final String MESSAGE_ERROR_NO_CALORIES_INFO = "Please input the number of calories using " + + "the prefix " + + QUOTATION + Command.COMMAND_PREFIX_CALORIES + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + "!"; + protected static final String MESSAGE_ERROR_NO_ITEM_NUM = "Please input the item number!"; + protected static final String MESSAGE_ERROR_INVALID_ITEM_NUM = "Please input the item number as a whole number " + + "greater than 0!"; + protected static final String MESSAGE_ERROR_TOO_MANY_DELIMITERS = "Please do not use the character " + + QUOTATION + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + + " other than to specify parameters relevant to this command!"; + protected static final String FILE_TEXT_DELIMITER = "|"; + protected static final String MESSAGE_ERROR_ILLEGAL_CHARACTER = "Please do not use the character " + + QUOTATION + FILE_TEXT_DELIMITER + QUOTATION + + " in your input!"; + protected static final String DATE_FORMAT = "dd-MM-yyyy"; + protected static final String TIME_FORMAT = "HHmm"; + protected static final String MESSAGE_ERROR_INVALID_DATE_FORMAT = "Invalid date! Please input a valid date as " + + "DD-MM-YYYY"; + protected static final String MESSAGE_ERROR_INVALID_TIME_FORMAT = "Invalid time! Please input a valid time as " + + "HHMM"; + protected static final String MESSAGE_ERROR_INVALID_DAY_OF_THE_WEEK = "Invalid day format! Please input day(s) " + + "between 1 and 7 with a comma in between the days." + LS + + LS + "1 : Monday" + + LS + "2 : Tuesday" + + LS + "3 : Wednesday" + + LS + "4 : Thursday" + + LS + "5 : Friday" + + LS + "6 : Saturday" + + LS + "7 : Sunday" + + LS + LS + "E.G: @/1,3,5,6"; + protected static final String MESSAGE_ERROR_NO_DATE = "Please input the date for this item using the prefix " + + QUOTATION + Command.COMMAND_PREFIX_DATE + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + "!"; + protected static final String MESSAGE_ERROR_NO_START_DATE = "Please input the start date for this item " + + "using the prefix " + QUOTATION + Command.COMMAND_PREFIX_START_DATE + Command.COMMAND_PREFIX_DELIMITER + + QUOTATION + "!"; + protected static final String MESSAGE_ERROR_NO_END_DATE = "Please input the end date for this item " + + "using the prefix " + QUOTATION + Command.COMMAND_PREFIX_END_DATE + Command.COMMAND_PREFIX_DELIMITER + + QUOTATION + "!"; + protected static final String MESSAGE_ERROR_NO_TIME = "Please input the time for this item using the prefix " + + QUOTATION + Command.COMMAND_PREFIX_TIME + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + "!"; + protected static final String MESSAGE_ERROR_NO_DAY_OF_THE_WEEK = "Please input the day(s) of reoccurrence " + + "for this item using the prefix " + + QUOTATION + Command.COMMAND_PREFIX_DAY_OF_THE_WEEK + Command.COMMAND_PREFIX_DELIMITER + QUOTATION + "!"; + protected static final String MESSAGE_ERROR_REPEATED_DAY_OF_THE_WEEK = "Please check your input of the day(s) " + + "of reoccurrence and make sure that there is no repeated day!"; + protected static final int MONDAY = 1; + protected static final int SUNDAY = 7; + protected static final String MESSAGE_ERROR_ITEM_DATE_TOO_OLD = "Please input a date that is within %s to %s!"; + protected static final String MESSAGE_ERROR_EXTRA_PARAMETERS = + "Error! There were unnecessary parameters detected. " + + "Please follow the command format and try again!"; + public static final String MESSAGE_ERROR_DUPLICATE_NUMBERS = + "Duplicate numbers found! Please input a list of unique numbers."; + +} diff --git a/src/main/java/seedu/duke/logic/parser/ParserUtils.java b/src/main/java/seedu/duke/logic/parser/ParserUtils.java new file mode 100644 index 0000000000..9b6020e221 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/ParserUtils.java @@ -0,0 +1,336 @@ +package seedu.duke.logic.parser; + +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.parser.exceptions.ExtraParamException; +import seedu.duke.logic.parser.exceptions.InvalidParamException; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.logic.parser.exceptions.ParserException; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Contains utility methods used by Parser classes to extract relevant parameters. + */ +public class ParserUtils { + protected static final Logger logger = Logger.getLogger(ParserUtils.class.getName()); + + + /** + * Extracts the item type prefix for command words that are common across item types (add, delete, view). + * Item type must be the first word after the command word. E.g "add f/pot" is valid, "add c/20 f/pot" is not + * + * @param params String containing all parameters + * @return String that is supposed to be the item prefix + */ + protected static String extractItemTypePrefix(String params) { + return params.trim().split("/", 2)[0]; + + } + + + /*================== General methods to extract different data types ==================*/ + + /** + * Extracts only the parameter that is specified by the prefix so that any additional parameter + * specified behind this string (if any) is removed. + * E.g. "John Doe w/20" is returned as "John Doe". + * NOTE: This is why users are not allowed to include the character "/" in their inputs + * other than to specify a parameter. + * + * @throws MissingParamException When expected parameter is missing + */ + protected static String extractRelevantParameter(String params, String prefix) + throws MissingParamException { + try { + String stringAfterPrefix = params.split(" " + prefix + Command.COMMAND_PREFIX_DELIMITER, 2)[1]; + if (stringAfterPrefix.contains(Command.COMMAND_PREFIX_DELIMITER)) { + return stringAfterPrefix.substring(0, + stringAfterPrefix.indexOf(Command.COMMAND_PREFIX_DELIMITER) - 1).trim(); + } + return stringAfterPrefix.trim(); + } catch (IndexOutOfBoundsException e) { + throw new MissingParamException(); + } + } + + + /** + * Extracts only the parameter required so that any additional parameter + * specified behind this string (if any) is removed. + * Specific to parameters where only one word is expected, and any word separated by a whitespace in this + * string is considered to be extra (and unnecessary). + * E.g. "f/1" returns "1", and "f/1 hello" throws ExtraParamException + * + * @throws MissingParamException When expected parameter is missing + * @throws ExtraParamException When extra words are detected for the parameter + */ + protected static String extractRelevantParameterWithoutWhitespace(String params, String prefix) + throws MissingParamException, ExtraParamException { + String[] paramsSplitByWhitespace = extractRelevantParameter(params, prefix).split(" ", 2); + if (paramsSplitByWhitespace.length == 2) { + throw new ExtraParamException(); + } + return paramsSplitByWhitespace[0]; + + } + + protected static LocalDate extractGeneralDate(String params, String prefix) + throws ParserException { + try { + String dateString = extractRelevantParameterWithoutWhitespace(params, prefix); + logger.log(Level.FINE, String.format("date string detected is: %s", dateString)); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(ParserMessages.DATE_FORMAT); + LocalDate date = LocalDate.parse(dateString, formatter); + YearMonth currentMonth = YearMonth.of(date.getYear(), date.getMonth()); + if (Integer.parseInt(dateString.substring(0, dateString.indexOf("-"))) + > currentMonth.atEndOfMonth().getDayOfMonth()) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_DATE_FORMAT); + } + return date; + } catch (DateTimeParseException e) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_DATE_FORMAT); + } catch (MissingParamException e) { + return null; + } catch (ExtraParamException e) { + throw new ParserException(e.getMessage()); + } + } + + protected static LocalTime extractGeneralTime(String params, String prefix) + throws ParserException { + try { + String timeString = extractRelevantParameterWithoutWhitespace(params, Command.COMMAND_PREFIX_TIME); + logger.log(Level.FINE, String.format("time string detected is: %s", timeString)); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(ParserMessages.TIME_FORMAT); + return LocalTime.parse(timeString, formatter); + } catch (DateTimeParseException e) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_TIME_FORMAT); + } catch (MissingParamException e) { + return null; + } catch (ExtraParamException e) { + throw new ParserException(e.getMessage()); + } + } + + + protected static Integer extractGeneralInteger(String params, String prefix) + throws InvalidParamException, ParserException { + try { + String intString = extractRelevantParameterWithoutWhitespace(params, prefix); + if (Double.parseDouble(intString) > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return Integer.parseInt(intString); + } catch (NumberFormatException e) { + throw new InvalidParamException(); + } catch (MissingParamException e) { + logger.log(Level.FINE, "No integer given, return null"); + return null; + } catch (ExtraParamException e) { + throw new ParserException(e.getMessage()); + } + } + + protected static Double extractGeneralDouble(String params, String prefix) + throws InvalidParamException, ParserException { + try { + String doubleString = extractRelevantParameterWithoutWhitespace(params, prefix); + return Double.parseDouble(doubleString); + } catch (NumberFormatException e) { + throw new InvalidParamException(); + } catch (MissingParamException e) { + logger.log(Level.FINE, "No double given but not required, return null"); + return null; + } catch (ExtraParamException e) { + throw new ParserException(e.getMessage()); + } + } + + + /*===== Methods to extract specific parameters that is needed across all Parser classes ========*/ + /*===== All methods here should throw general ParserException, with specific error messages ====*/ + /*Methods which are specific to the type of Command are found in their respective Parser classes*/ + + protected static String extractItemDescription(String params, String prefix) + throws ParserException { + try { + String description = extractRelevantParameter(params, prefix); + logger.log(Level.FINE, String.format("Item name:%s", description)); + if (description.equals(ParserMessages.EMPTY)) { + logger.log(Level.FINE, "Detected empty description"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_DESCRIPTION); + } + return description; + } catch (MissingParamException e) { + logger.log(Level.FINE, String.format("Detected missing command prefix (%s)", prefix)); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_DESCRIPTION); + } + } + + protected static Integer extractItemCalories(String params) + throws ParserException { + try { + Integer calories = extractGeneralInteger(params, Command.COMMAND_PREFIX_CALORIES); + return calories; + } catch (InvalidParamException e) { + logger.log(Level.FINE, "Detected non-digit calories input"); + throw new ParserException(CommandMessages.MESSAGE_INVALID_CALORIES); + } + } + + protected static String extractName(String params) throws ParserException { + try { + String name = extractRelevantParameter(params, Command.COMMAND_PREFIX_NAME); + if (name.equals(ParserMessages.EMPTY)) { + logger.log(Level.FINE, "Detected empty name input."); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NAME_EMPTY_STRING); + } + return name; + } catch (MissingParamException e) { + logger.log(Level.FINE, "Detected missing name prefix, returning null string."); + return null; + } + } + + protected static Double extractHeight(String params) throws ParserException { + try { + return extractGeneralDouble(params, Command.COMMAND_PREFIX_HEIGHT); + } catch (InvalidParamException e) { + logger.log(Level.FINE, "Detected non-digit height input."); + throw new ParserException(ProfileUtils.ERROR_HEIGHT); + } + } + + protected static Double extractWeight(String params) throws ParserException { + try { + return extractGeneralDouble(params, Command.COMMAND_PREFIX_WEIGHT); + } catch (InvalidParamException e) { + logger.log(Level.FINE, "Detected non-digit weight input."); + throw new ParserException(ProfileUtils.ERROR_WEIGHT); + } + } + + protected static LocalDate extractDate(String params, boolean isRequired) + throws ParserException { + LocalDate localDate = extractGeneralDate(params, Command.COMMAND_PREFIX_DATE); + if (localDate == null && isRequired) { + logger.log(Level.FINE, "Detected empty date input after prefix but date is required!"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_DATE); + } + if (localDate == null && !isRequired) { + logger.log(Level.FINE, "Detected empty date input after prefix, assuming date to be now"); + return LocalDate.now(); + } + return localDate; + } + + protected static LocalTime extractTime(String params, boolean isRequired) + throws ParserException { + LocalTime localTime = extractGeneralTime(params, Command.COMMAND_PREFIX_TIME); + if (localTime == null && isRequired) { + logger.log(Level.FINE, "Detected empty time input after prefix but time is required!"); + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_TIME); + } + if (localTime == null && !isRequired) { + logger.log(Level.FINE, "Detected empty time input after prefix, assuming time to be now"); + return LocalTime.now(); + } + return localTime; + } + + protected static LocalDateTime extractDateTime(String params) throws ParserException { + final LocalTime time = extractTime(params, false); + final LocalDate date = extractDate(params, false); + return date.atTime(time); + } + + protected static int extractItemIndex(String params, String prefix) + throws ParserException { + try { + final Integer itemNum = extractGeneralInteger(params, prefix); + if (itemNum == null) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_NO_ITEM_NUM); + } + final int itemIndex = convertItemNumToItemIndex(itemNum); + if (itemIndex < 0) { + throw new InvalidParamException(); + } + return itemIndex; + } catch (InvalidParamException e) { + throw new ParserException(ParserMessages.MESSAGE_ERROR_INVALID_ITEM_NUM); + } + } + + protected static int convertItemNumToItemIndex(int itemNum) { + return itemNum - 1; + } + + /*======================== Methods to check data validity ============================*/ + + protected static boolean isSevenDaysBeforeToday(LocalDate date) { + return date.isBefore(LocalDate.now().minusDays(6)); + } + + protected static boolean isWithinSevenDaysFromToday(LocalDate date) { + return !isSevenDaysBeforeToday(date) && !date.isAfter(LocalDate.now()); + } + + protected static boolean isFutureDate(LocalDate date) { + return date.isAfter(LocalDate.now()); + } + + protected static boolean hasRequiredParams(String params, String... prefixes) { + for (String prefix : prefixes) { + if (!params.toLowerCase().contains(" " + prefix + Command.COMMAND_PREFIX_DELIMITER)) { + return false; + } + } + return true; + } + + /** + * Returns the number of parameters detected that is valid for the specific command. + * This is required as some parameters are optional, therefore an absolute number cannot be expected. + * + * @param params User input string containing all parameters + * @param prefixes Variable number of prefixes that is valid for the specific command + * @return Number of parameters detected that is valid for the specific command + */ + protected static int getNumberOfCorrectParamsDetected(String params, String... prefixes) { + int count = 0; + for (String prefix : prefixes) { + if (params.toLowerCase().contains(" " + prefix + Command.COMMAND_PREFIX_DELIMITER)) { + count++; + } + } + logger.log(Level.FINE, String.format("no. of corrected params detected: %s", count)); + return count; + } + + /** + * Returns true if there are too many '/' characters in the parameter string. + * + * @param params User input string containing all parameters + * @param prefixes Variable number of prefixes that is valid for the specific command + */ + protected static boolean hasExtraDelimiters(String params, String... prefixes) { + final int expectedNum = getNumberOfCorrectParamsDetected(params, prefixes); + int numOfDelimiters = 0; + for (int i = 0; i < params.length(); i++) { + if (params.charAt(i) == Command.COMMAND_PREFIX_DELIMITER.charAt(0)) { + numOfDelimiters++; + } + } + logger.log(Level.FINE, String.format("no. of delimiters detected: %s", numOfDelimiters)); + return numOfDelimiters > expectedNum; + } +} diff --git a/src/main/java/seedu/duke/logic/parser/UpdateProfileParser.java b/src/main/java/seedu/duke/logic/parser/UpdateProfileParser.java new file mode 100644 index 0000000000..d25efb8a80 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/UpdateProfileParser.java @@ -0,0 +1,82 @@ +package seedu.duke.logic.parser; + +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.commands.ProfileCommand; +import seedu.duke.logic.commands.ProfileUpdateCommand; +import seedu.duke.logic.parser.exceptions.ExtraParamException; +import seedu.duke.logic.parser.exceptions.InvalidParamException; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.logic.parser.exceptions.ParserException; + +/** + * Parses input arguments for Update Profile command. + */ +public class UpdateProfileParser implements Parser { + @Override + public Command parse(String params) { + if (params.trim().isEmpty()) { //no additional parameters, assumed to be view profile command + return new ProfileCommand(); + } + if (ParserUtils.hasExtraDelimiters( + params, ProfileUpdateCommand.EXPECTED_PREFIXES)) { + return new InvalidCommand(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS); + } + if (ParserUtils.getNumberOfCorrectParamsDetected(params, ProfileUpdateCommand.EXPECTED_PREFIXES) == 0) { + return new InvalidCommand(CommandMessages.MESSAGE_PROFILE_COMMAND_INVALID_FORMAT); + } + try { + final String name = ParserUtils.extractName(params); + final Double height = ParserUtils.extractHeight(params); + final Double weight = ParserUtils.extractWeight(params); + final Integer calorieGoal = extractCalorieGoal(params); + final Integer age = extractAge(params); + final Integer activityFactor = extractActivityFactor(params); + final Character gender = extractGender(params); + return new ProfileUpdateCommand(name, height, weight, calorieGoal, age, activityFactor, gender); + } catch (ParserException e) { + return new InvalidCommand(e.getMessage()); + } + } + + protected static Integer extractCalorieGoal(String params) throws ParserException { + try { + return ParserUtils.extractGeneralInteger(params, Command.COMMAND_PREFIX_GOAL); + } catch (InvalidParamException e) { + throw new ParserException(ProfileUtils.ERROR_CALORIE_GOAL); + } + } + + protected static Integer extractAge(String params) throws ParserException { + try { + return ParserUtils.extractGeneralInteger(params, Command.COMMAND_PREFIX_AGE); + } catch (InvalidParamException e) { + throw new ParserException(ProfileUtils.ERROR_AGE); + } + } + + protected static Integer extractActivityFactor(String params) throws ParserException { + try { + return ParserUtils.extractGeneralInteger(params, Command.COMMAND_PREFIX_ACTIVITY_FACTOR); + } catch (InvalidParamException e) { + throw new ParserException(ProfileUtils.ERROR_ACTIVITY_FACTOR); + } + } + + protected static Character extractGender(String params) throws ParserException { + try { + String stringAfterPrefix = ParserUtils.extractRelevantParameterWithoutWhitespace( + params, Command.COMMAND_PREFIX_GENDER); + if (stringAfterPrefix.length() > 1) { + throw new ParserException(ProfileUtils.ERROR_GENDER); + } + return stringAfterPrefix.charAt(0); + } catch (MissingParamException e) { + return Command.NULL_CHAR; + } catch (ExtraParamException e) { + throw new ParserException(e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/duke/logic/parser/ViewCommandParser.java b/src/main/java/seedu/duke/logic/parser/ViewCommandParser.java new file mode 100644 index 0000000000..9eaf93f009 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/ViewCommandParser.java @@ -0,0 +1,39 @@ +package seedu.duke.logic.parser; + +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.commands.ViewExerciseBankCommand; +import seedu.duke.logic.commands.ViewExerciseListCommand; +import seedu.duke.logic.commands.ViewFoodBankCommand; +import seedu.duke.logic.commands.ViewFoodListCommand; +import seedu.duke.logic.commands.ViewFutureExerciseListCommand; +import seedu.duke.logic.parser.exceptions.ItemNotSpecifiedException; + +/** + * Parses input arguments for View commands. + */ +public class ViewCommandParser implements Parser { + + @Override + public Command parse(String params) { + try { + switch (params.trim()) { + case Command.COMMAND_PREFIX_EXERCISE + Command.COMMAND_PREFIX_DELIMITER: + return new ViewExerciseListCommand(); + case Command.COMMAND_PREFIX_FOOD + Command.COMMAND_PREFIX_DELIMITER: + return new ViewFoodListCommand(); + case Command.COMMAND_PREFIX_UPCOMING_EXERCISE + Command.COMMAND_PREFIX_DELIMITER: + return new ViewFutureExerciseListCommand(); + case Command.COMMAND_PREFIX_EXERCISE_BANK + Command.COMMAND_PREFIX_DELIMITER: + return new ViewExerciseBankCommand(); + case Command.COMMAND_PREFIX_FOOD_BANK + Command.COMMAND_PREFIX_DELIMITER: + return new ViewFoodBankCommand(); + default: + throw new ItemNotSpecifiedException(); + } + } catch (ItemNotSpecifiedException e) { + return new InvalidCommand(CommandMessages.MESSAGE_VIEW_COMMAND_INVALID_FORMAT); + } + } +} diff --git a/src/main/java/seedu/duke/logic/parser/exceptions/ExtraParamException.java b/src/main/java/seedu/duke/logic/parser/exceptions/ExtraParamException.java new file mode 100644 index 0000000000..0690810b6e --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/exceptions/ExtraParamException.java @@ -0,0 +1,16 @@ +package seedu.duke.logic.parser.exceptions; + +/** + * Represents an error where there are extra parameters specified for the command. + */ +public class ExtraParamException extends Exception { + private static final String ERROR_MESSAGE = "Error! There were unnecessary parameters detected. " + + "Please follow the command format and try again!"; + + /** + * General constructor that constructs an exception with a standard error message for extra parameters. + */ + public ExtraParamException() { + super(ERROR_MESSAGE); + } +} diff --git a/src/main/java/seedu/duke/logic/parser/exceptions/InvalidParamException.java b/src/main/java/seedu/duke/logic/parser/exceptions/InvalidParamException.java new file mode 100644 index 0000000000..248b7182b1 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/exceptions/InvalidParamException.java @@ -0,0 +1,18 @@ +package seedu.duke.logic.parser.exceptions; + +/** + * Represents an error where the parameter specified is invalid for the command. + */ +public class InvalidParamException extends Exception { + private static String errorMessage; + + /** + * General constructor without error message. + */ + public InvalidParamException() { + } + + public InvalidParamException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/seedu/duke/logic/parser/exceptions/ItemNotSpecifiedException.java b/src/main/java/seedu/duke/logic/parser/exceptions/ItemNotSpecifiedException.java new file mode 100644 index 0000000000..9c64bb3655 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/exceptions/ItemNotSpecifiedException.java @@ -0,0 +1,7 @@ +package seedu.duke.logic.parser.exceptions; + +/** + * Represents an error where the user did not specify which list to execute the command on. (food/exercise) + */ +public class ItemNotSpecifiedException extends Exception { +} diff --git a/src/main/java/seedu/duke/logic/parser/exceptions/MissingParamException.java b/src/main/java/seedu/duke/logic/parser/exceptions/MissingParamException.java new file mode 100644 index 0000000000..189000b9f8 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/exceptions/MissingParamException.java @@ -0,0 +1,24 @@ +package seedu.duke.logic.parser.exceptions; + +/** + * Represents an error where a parameter required for the command is not specified. + */ +public class MissingParamException extends Exception { + private static String errorMessage; + + /** + * General constructor without error message. + */ + public MissingParamException() { + } + + /** + * Constructor with specific error message. + * + * @param errorMessage Error message to be shown when exception is caught and printed + */ + public MissingParamException(String errorMessage) { + super(errorMessage); + MissingParamException.errorMessage = errorMessage; + } +} diff --git a/src/main/java/seedu/duke/logic/parser/exceptions/ParserException.java b/src/main/java/seedu/duke/logic/parser/exceptions/ParserException.java new file mode 100644 index 0000000000..b8ca5cee61 --- /dev/null +++ b/src/main/java/seedu/duke/logic/parser/exceptions/ParserException.java @@ -0,0 +1,18 @@ +package seedu.duke.logic.parser.exceptions; + +/** + * Represents a general parsing exception, to be thrown from ParserUtils to the Parser classes. + */ +public class ParserException extends Exception { + private static String errorMessage; + + /** + * General constructor without error message. + */ + public ParserException() { + } + + public ParserException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/seedu/duke/state/ActivityFactorCreator.java b/src/main/java/seedu/duke/state/ActivityFactorCreator.java new file mode 100644 index 0000000000..592778f904 --- /dev/null +++ b/src/main/java/seedu/duke/state/ActivityFactorCreator.java @@ -0,0 +1,65 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.attributes.ActivityFactor; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.ui.Ui; + +/** + * Creates a ActivityFactor Class and prompts user for valid activity factor input. + */ +public class ActivityFactorCreator extends AttributeCreator { + + private ActivityFactor activityFactor = new ActivityFactor(); + private static final String LS = System.lineSeparator(); + private static final String MESSAGE_ACTIVITY_FACTOR = "Your activity factor is %s."; + public static final String MESSAGE_INVALID_POSITIVE_INT_INPUT = "Invalid input, " + + "please input a valid positive whole number"; + private static final String MESSAGE_INTRO_ACTIVITY_FACTOR = "In terms of activity level, how active are you?" + LS + + "Based on the rubrics below, please key in 1 to 5 based on how active you are." + LS + + "1 -> Sedentary - Little or no exercise" + LS + + "2 -> Lightly Active - Light exercise or sports, around 1-3 days a week" + LS + + "3 -> Moderately Active - Regular exercise or sports, around 3-5 days a week" + LS + + "4 -> Very Active - Frequent exercise or sports, around 6-7 days a week" + LS + + "5 -> If you are extra active - Sports or exercising is your passion and a physical jobscope."; + + public ActivityFactorCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile activity factor for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public ActivityFactor createActivityFactor() throws MissingParamException { + while (!activityFactor.isValid()) { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_ACTIVITY_FACTOR); + try { + String userInput = ui.getUserInput().trim(); + setActivityFactor(userInput); + checkActivityFactor(); + } catch (NumberFormatException e) { + ui.formatMessageWithTopDivider(MESSAGE_INVALID_POSITIVE_INT_INPUT); + } + } + return activityFactor; + } + + private void checkActivityFactor() { + if (activityFactor.isValid()) { + ui.formatMessageWithTopDivider( + String.format(MESSAGE_ACTIVITY_FACTOR, + activityFactor.getUserInput())); + } else { + ui.formatMessageFramedWithDivider(ProfileUtils.ERROR_ACTIVITY_FACTOR); + } + } + + private void setActivityFactor(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + confirmInputBye(userInput); + int activityFactorInput = Integer.parseInt(userInput); + activityFactor.setUserInput(activityFactorInput); + } +} diff --git a/src/main/java/seedu/duke/state/AgeCreator.java b/src/main/java/seedu/duke/state/AgeCreator.java new file mode 100644 index 0000000000..8b694def49 --- /dev/null +++ b/src/main/java/seedu/duke/state/AgeCreator.java @@ -0,0 +1,61 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.attributes.Age; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.ui.Ui; + +/** + * Creates an Age Class and prompts user for valid age input. + */ +public class AgeCreator extends AttributeCreator { + + private Age age = new Age(); + private static final String MESSAGE_INTRO_AGE = "How old are you?"; + private static final String MESSAGE_AGE = "You are %s years old."; + private static final String MESSAGE_INVALID_POSITIVE_INT_INPUT = "Invalid input, " + + "please input a valid positive whole number"; + + + public AgeCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile age for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public Age createNewAge() throws MissingParamException { + while (!age.isValid()) { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_AGE); + try { + String userInput = ui.getUserInput().trim(); + setAge(userInput); + checkAge(); + } catch (NumberFormatException e) { + ui.formatMessageWithTopDivider(MESSAGE_INVALID_POSITIVE_INT_INPUT); + } + } + return age; + } + + private void checkAge() { + if (age.isValid()) { + ui.formatMessageWithTopDivider( + String.format(MESSAGE_AGE, + age.getAge())); + } else { + ui.formatMessageFramedWithDivider(ProfileUtils.ERROR_AGE); + } + } + + private void setAge(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + confirmInputBye(userInput); + int ageInput = (Double.parseDouble(userInput) > Integer.MAX_VALUE) + ? Integer.MAX_VALUE + : Integer.parseInt(userInput); + age.setAge(ageInput); + } +} diff --git a/src/main/java/seedu/duke/state/AttributeCreator.java b/src/main/java/seedu/duke/state/AttributeCreator.java new file mode 100644 index 0000000000..d8cdf4e8d1 --- /dev/null +++ b/src/main/java/seedu/duke/state/AttributeCreator.java @@ -0,0 +1,41 @@ +package seedu.duke.state; + +import seedu.duke.logic.Statistics; +import seedu.duke.ui.Ui; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AttributeCreator { + + + public static final int STATUS = 0; + protected Ui ui; + protected static Logger logger = Logger.getLogger(AttributeCreator.class.getName()); + + AttributeCreator(Ui ui) { + this.ui = ui; + } + + // Followed the format of Bye command to standardise bye message. + private static final String MESSAGE_SUCCESS = "Exiting Fitbot...." + Ui.LS + + "Bye! Hope to see you again soon!!"; + + protected boolean isBye(String userInput) { + logger.log(Level.FINE, String.valueOf(userInput.toLowerCase().equals("bye"))); + return userInput.toLowerCase().equals("bye"); + } + + protected void exit() { + logger.log(Level.FINE, "exiting...."); + ui.formatMessageFramedWithDivider(MESSAGE_SUCCESS); + System.exit(STATUS); + } + + protected void confirmInputBye(String userInput) { + if (isBye(userInput.trim())) { + exit(); + } + } + +} diff --git a/src/main/java/seedu/duke/state/CalorieGoalCreator.java b/src/main/java/seedu/duke/state/CalorieGoalCreator.java new file mode 100644 index 0000000000..65c745d0d9 --- /dev/null +++ b/src/main/java/seedu/duke/state/CalorieGoalCreator.java @@ -0,0 +1,67 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.attributes.CalorieGoal; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.ui.Ui; + +//@@author tttyyzzz + +/** + * Creates a CalorieGoal Class and prompts user for valid calorie goal input. + */ +public class CalorieGoalCreator extends AttributeCreator { + + + private CalorieGoal calorieGoal; + private static final String MESSAGE_CALORIE_GOAL = "You calorie goal is %s cal."; + private static final String MESSAGE_INTRO_CALORIE_GOAL = "Please input your net calorie goal."; + private static final String MESSAGE_INVALID_POSITIVE_INT_INPUT = "Invalid input, " + + "please input a valid whole number from " + + CalorieGoal.LIMIT_LOWER_CALORIES + " to " + + CalorieGoal.LIMIT_UPPER_CALORIES; + + public CalorieGoalCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile calorie goal for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public CalorieGoal createNewCalorieGoal() throws MissingParamException { + boolean checkInput = false;// check whether calorie goal has the correct input + do { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_CALORIE_GOAL); + try { + String userInput = ui.getUserInput().trim(); + setCalorieGoal(userInput); + checkCalorieGoal(); + checkInput = true; + } catch (NumberFormatException e) { + ui.formatMessageWithTopDivider(MESSAGE_INVALID_POSITIVE_INT_INPUT); + } + } while (!checkInput || !calorieGoal.isValid()); + return calorieGoal; + } + + private void checkCalorieGoal() { + if (calorieGoal.isValid()) { + ui.formatMessageWithTopDivider( + String.format(MESSAGE_CALORIE_GOAL, + calorieGoal.getCalorieGoal())); + } else { + ui.formatMessageWithTopDivider(ProfileUtils.ERROR_CALORIE_GOAL); + + } + } + + private void setCalorieGoal(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + confirmInputBye(userInput); + int calorieGoalInput = (Double.parseDouble(userInput) > Integer.MAX_VALUE) + ? Integer.MAX_VALUE : Integer.parseInt(userInput); + calorieGoal = new CalorieGoal(calorieGoalInput); + } +} diff --git a/src/main/java/seedu/duke/state/GenderCreator.java b/src/main/java/seedu/duke/state/GenderCreator.java new file mode 100644 index 0000000000..3d25a53557 --- /dev/null +++ b/src/main/java/seedu/duke/state/GenderCreator.java @@ -0,0 +1,60 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.attributes.Gender; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.ui.Ui; + +/** + * Creates a Gender Class and prompts user for valid gender input. + */ +public class GenderCreator extends AttributeCreator { + + private Gender gender = new Gender(); + public static final String MESSAGE_INTRO_GENDER = "What is your gender? (If you are a male, type 'm'" + + ", if you are a female, type 'f')"; + public static final char MALE_CHAR = 'M'; + public static final String MESSAGE_MALE = "You are a male."; + public static final String MESSAGE_FEMALE = "You are a female."; + + public GenderCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile gender for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public Gender createNewGender() throws MissingParamException { + while (!gender.isValid()) { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_GENDER); + String userInput = ui.getUserInput(); + setGender(userInput); + checkGender(); + } + return gender; + } + + private void checkGender() { + if (gender.isValid()) { + ui.formatMessageWithTopDivider( + getGenderMessage(gender.getGender())); + } else { + ui.formatMessageWithTopDivider(ProfileUtils.ERROR_GENDER); + } + } + + private void setGender(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + confirmInputBye(userInput); + if (userInput.length() == 1) { + char genderInput = userInput.charAt(0); + gender.setGender(genderInput); + } + } + + private String getGenderMessage(char gender) { + return gender == MALE_CHAR ? MESSAGE_MALE : MESSAGE_FEMALE; + } +} diff --git a/src/main/java/seedu/duke/state/HeightCreator.java b/src/main/java/seedu/duke/state/HeightCreator.java new file mode 100644 index 0000000000..81c4a116f9 --- /dev/null +++ b/src/main/java/seedu/duke/state/HeightCreator.java @@ -0,0 +1,58 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.attributes.Height; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.ui.Ui; + +/** + * Creates a Height Class and prompts user for valid height input. + */ +public class HeightCreator extends AttributeCreator { + + private static final String MESSAGE_HEIGHT = "Your height is %scm."; + private static final String MESSAGE_INTRO_HEIGHT = "What's your height? (in cm)"; + private static final String MESSAGE_INVALID_POSITIVE_DOUBLE_INPUT = "Invalid input," + + " please input a valid positive number"; + private Height height = new Height(); + + public HeightCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile height for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public Height createNewHeight() throws MissingParamException { + while (!height.isValid()) { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_HEIGHT); + try { + String userInput = ui.getUserInput(); + setHeight(userInput); + checkHeight(); + } catch (NumberFormatException e) { + ui.formatMessageWithTopDivider(MESSAGE_INVALID_POSITIVE_DOUBLE_INPUT); + } + } + return height; + } + + private void checkHeight() { + if (height.isValid()) { + ui.formatMessageWithTopDivider( + String.format(MESSAGE_HEIGHT, + height.getHeight())); + } else { + ui.formatMessageFramedWithDivider(ProfileUtils.ERROR_HEIGHT); + } + } + + private void setHeight(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + confirmInputBye(userInput); + double heightInput = Double.parseDouble(userInput); + height.setHeight(heightInput); + } +} diff --git a/src/main/java/seedu/duke/state/NameCreator.java b/src/main/java/seedu/duke/state/NameCreator.java new file mode 100644 index 0000000000..c5852866e3 --- /dev/null +++ b/src/main/java/seedu/duke/state/NameCreator.java @@ -0,0 +1,84 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.attributes.Name; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; + +import seedu.duke.ui.Ui; + +/** + * Creates a Name Class and prompts user for valid name input. + */ +public class NameCreator extends AttributeCreator { + + private static final String LS = System.lineSeparator(); + private static final String MESSAGE_BYE_DETECTED = "The command word 'bye' is detected." + LS + + "Type '1' if you wish to exit. Type '2' if you wish to set your name as 'bye'." + LS + + "Else, type in any key for Fitbot to ask for your name again."; + private static final String CHECK_REPEAT_MESSAGE = ""; + public static final String FIRST_OPTION = "1"; + public static final String SECOND_OPTION = "2"; + private Name name = new Name(); + private static final String MESSAGE_INTRO_NAME = "What's your name?"; + private static final String MESSAGE_NAME = "Nice name you have there! Hello %s."; + + public NameCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile name for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public Name createNewName() throws MissingParamException { + while (!name.isValid()) { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_NAME); + String userInput = ui.getUserInput().trim(); + if (setName(userInput)) { + continue; + } + checkName(); + } + assert name.isValid() : " name is valid"; + return name; + } + + private void checkName() { + if (name.isValid()) { + ui.formatMessageWithTopDivider( + String.format(MESSAGE_NAME, + name.getName())); + } else { + ui.formatMessageWithTopDivider(ProfileUtils.ERROR_NAME); + } + } + + private boolean setName(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + userInput = checkAndConfirmInputBye(userInput); + if (userInput.equals(CHECK_REPEAT_MESSAGE)) { + ui.formatMessageWithTopDivider(); + return true; + } + name.setName(userInput); + return false; + } + + private String checkAndConfirmInputBye(String userInput) { + boolean isBye = isBye(userInput); + if (!isBye) { + return userInput; + } + ui.formatMessageFramedWithDivider(String.format(MESSAGE_BYE_DETECTED, userInput)); + String userConfirmByeInput = ui.getUserInput().trim(); + if (userConfirmByeInput.equals(FIRST_OPTION)) { + exit(); + } else if (userConfirmByeInput.equals(SECOND_OPTION)) { + return userInput; + } + assert !userConfirmByeInput.equals("1") && !userConfirmByeInput.equals("2") : "other inputs"; + return CHECK_REPEAT_MESSAGE; + } + +} diff --git a/src/main/java/seedu/duke/state/StartState.java b/src/main/java/seedu/duke/state/StartState.java new file mode 100644 index 0000000000..7e1378f58e --- /dev/null +++ b/src/main/java/seedu/duke/state/StartState.java @@ -0,0 +1,198 @@ +package seedu.duke.state; + +import seedu.duke.data.profile.Profile; +import seedu.duke.logic.Statistics; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.storage.StorageManager; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.ui.Ui; + +import java.util.logging.Level; +import java.util.logging.Logger; + +//@@author tttyyzzz + +public class StartState { + + private Profile profile; + private StorageManager storageManager; + private Ui ui; + private static Logger logger = Logger.getLogger(Statistics.class.getName()); + + public StartState(Profile profile, StorageManager storageManager, Ui ui) { + this.profile = profile; + this.storageManager = storageManager; + this.ui = ui; + } + + /** + * Check whether user's profile is complete. + * If profile is complete, the program will exit this method. + * If the profile is partially complete, it will assist user in completing the profile. + * If all parameters of profile is incorrect or a new user, user is required to complete + * all the particulars before saving their profile data. + */ + public Profile checkAndCreateProfile() { + if (profile.checkProfileComplete()) { + logger.log(Level.FINE,"profile is complete"); + return profile; + } + if (profile.checkProfilePresent()) { + assert !profile.checkProfileComplete() : "profile is incomplete"; + logger.log(Level.FINE,"profile is partially complete"); + repairProfile(); + } else { + logger.log(Level.FINE,"profile is totally wrong or incomplete"); + createNewProfile(); + } + + System.out.println(Ui.MESSAGE_CREATE_PROFILE_SUCCESSFUL + ui.LS + ui.MESSAGE_DIRECT_HELP); + return profile; + } + + /** + * Assists user in fixing remaining profile particulars. + * The profile changes will be saved on every update. + */ + private void repairProfile() { + ui.formatMessageWithTopDivider(); + while (!profile.checkProfileComplete()) { + try { + if (!profile.getProfileName().isValid()) { + createNewProfileName(profile); // if user just enter and exit, it will cause his name to be null + } else if (!profile.getProfileHeight().isValid()) { + createNewProfileHeight(profile); + } else if (!profile.getProfileWeight().isValid()) { + createNewProfileWeight(profile); + } else if (!profile.getProfileGender().isValid()) { + createNewProfileGender(profile); + } else if (!profile.getProfileAge().isValid()) { + createNewProfileAge(profile); + } else if (!profile.getProfileActivityFactor().isValid()) { + createNewProfileActivityFactor(profile); + } else if (!profile.getProfileCalorieGoal().isValid()) { + createNewProfileCalorieGoal(profile); + } + storageManager.saveProfile(this.profile); + } catch (MissingParamException e) { + System.out.println(e.getMessage()); + } catch (UnableToWriteFileException e) { + ui.formatMessageFramedWithDivider(e.getMessage()); + } + } + ui.formatMessageWithBottomDivider(); + } + + /** + * Creates a new profile instance for new user. + * Profile will be lost if user exits the program without setting up the profile. + * Upon completing profile, the profile instance in Main will be replaced and stored in storage. + */ + private void createNewProfile() { + Profile newProfile = new Profile(); + ui.formatMessageWithTopDivider(); + while (!newProfile.checkProfileComplete()) { + try { + if (!newProfile.getProfileName().isValid()) { + createNewProfileName(newProfile); // if user just enter and exit, it will cause his name to be null + } else if (!newProfile.getProfileHeight().isValid()) { + createNewProfileHeight(newProfile); + } else if (!newProfile.getProfileWeight().isValid()) { + createNewProfileWeight(newProfile); + } else if (!newProfile.getProfileGender().isValid()) { + createNewProfileGender(newProfile); + } else if (!newProfile.getProfileAge().isValid()) { + createNewProfileAge(newProfile); + } else if (!newProfile.getProfileActivityFactor().isValid()) { + createNewProfileActivityFactor(newProfile); + } else if (!newProfile.getProfileCalorieGoal().isValid()) { + createNewProfileCalorieGoal(newProfile); + } + } catch (MissingParamException e) { + System.out.println(e.getMessage()); + } + } + this.profile = newProfile; + try { + storageManager.saveProfile(this.profile); + } catch (UnableToWriteFileException e) { + ui.formatMessageFramedWithDivider(e.getMessage()); + } + } + + /** + * Creates a valid profile activity factor for the profile instance. + * + * @param newProfile instance of a profile class. + * @throws MissingParamException if user input a string of 0 characters. + */ + private void createNewProfileActivityFactor(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"creating activity factor"); + newProfile.setProfileActivityFactor(new ActivityFactorCreator(ui).createActivityFactor()); + } + + /** + * Creates a valid profile calorie goal for the profile instance. + * + * @param newProfile instance of a profile class. + * @throws MissingParamException if user input a string of 0 characters. + */ + private void createNewProfileCalorieGoal(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"creating calorie goal"); + newProfile.setProfileCalorieGoal(new CalorieGoalCreator(ui).createNewCalorieGoal()); + } + + /** + * Creates a valid profile age for the profile instance. + * + * @param newProfile instance of a profile class. + * @throws MissingParamException if user input a string of 0 characters. + */ + private void createNewProfileAge(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"creating age"); + newProfile.setProfileAge(new AgeCreator(ui).createNewAge()); + } + + /** + * Creates a valid profile gender for the profile instance. + * + * @param newProfile instance of a profile class. + */ + private void createNewProfileGender(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"creating gender"); + newProfile.setProfileGender(new GenderCreator(ui).createNewGender()); + } + + /** + * Creates a valid profile weight for the profile instance. + * + * @param newProfile instance of a profile class. + * @throws MissingParamException if user input a string of 0 characters. + */ + private void createNewProfileWeight(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"crating weight"); + newProfile.setProfileWeight(new WeightCreator(ui).createNewWeight()); + } + + /** + * Creates a valid profile height for the profile instance. + * + * @param newProfile instance of a profile class. + * @throws MissingParamException if user input a string of 0 characters. + */ + private void createNewProfileHeight(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"creating height"); + newProfile.setProfileHeight(new HeightCreator(ui).createNewHeight()); + } + + /** + * Creates a valid profile name for the profile instance. + * + * @param newProfile instance of a profile class. + * @throws MissingParamException if user input a string of 0 characters. + */ + private void createNewProfileName(Profile newProfile) throws MissingParamException { + logger.log(Level.FINE,"crating name"); + newProfile.setProfileName(new NameCreator(ui).createNewName()); + } +} diff --git a/src/main/java/seedu/duke/state/WeightCreator.java b/src/main/java/seedu/duke/state/WeightCreator.java new file mode 100644 index 0000000000..a6e284a051 --- /dev/null +++ b/src/main/java/seedu/duke/state/WeightCreator.java @@ -0,0 +1,58 @@ +package seedu.duke.state; + + +import seedu.duke.data.profile.attributes.Weight; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.parser.exceptions.MissingParamException; +import seedu.duke.ui.Ui; + +/** + * Creates a Weight Class and prompts user for valid weight input. + */ +public class WeightCreator extends AttributeCreator { + + public static final String MESSAGE_INTRO_WEIGHT = "What's your weight? (in kg)"; + public static final String MESSAGE_WEIGHT = "Your weight is %skg."; + public static final String MESSAGE_INVALID_WEIGHT_INPUT = "Invalid input, please input a valid positive number"; + private Weight weight = new Weight(); + + public WeightCreator(Ui ui) { + super(ui); + } + + /** + * Creates a valid profile weight for the profile instance. + * + * @throws MissingParamException if user input a string of 0 characters. + */ + public Weight createNewWeight() throws MissingParamException { + while (!weight.isValid()) { + ui.formatMessageWithBottomDivider(MESSAGE_INTRO_WEIGHT); + try { + String userInput = ui.getUserInput(); + setWeight(userInput); + checkWeight(); + } catch (NumberFormatException e) { + ui.formatMessageWithTopDivider(MESSAGE_INVALID_WEIGHT_INPUT); + } + } + return weight; + } + + private void checkWeight() { + if (weight.isValid()) { + ui.formatMessageWithTopDivider( + String.format(MESSAGE_WEIGHT, + weight.getWeight())); + } else { + ui.formatMessageFramedWithDivider(ProfileUtils.ERROR_WEIGHT); + } + } + + private void setWeight(String userInput) throws MissingParamException { + ui.checkEmptyUserInput(userInput); + confirmInputBye(userInput); + double weightInput = Double.parseDouble(userInput); + weight.setWeight(weightInput); + } +} diff --git a/src/main/java/seedu/duke/storage/Storage.java b/src/main/java/seedu/duke/storage/Storage.java new file mode 100644 index 0000000000..7e00039d23 --- /dev/null +++ b/src/main/java/seedu/duke/storage/Storage.java @@ -0,0 +1,46 @@ +package seedu.duke.storage; + +import seedu.duke.data.DataManager; +import seedu.duke.storage.data.exercise.exercisebank.ExerciseBankStorage; +import seedu.duke.storage.data.exercise.exerciselist.ExerciseListStorage; +import seedu.duke.storage.data.exercise.futurelist.UpcomingStorage; +import seedu.duke.storage.data.food.foodbank.FoodBankStorage; +import seedu.duke.storage.data.food.foodlist.FoodListStorage; +import seedu.duke.storage.data.profile.ProfileStorage; + +/** + * API of the Storage component. + */ +public interface Storage extends ProfileStorage, FoodListStorage, ExerciseListStorage, + UpcomingStorage, FoodBankStorage, ExerciseBankStorage { + + String FILE_TEXT_DELIMITER = "\\|"; + String FILEPATH = "./data/"; + String FILENAME_PROFILE = "profile.txt"; + String FILEPATH_PROFILE = FILEPATH + FILENAME_PROFILE; + String FILENAME_BANK_FOOD = "food_bank.txt"; + String FILEPATH_BANK_FOOD = FILEPATH + FILENAME_BANK_FOOD; + String FILENAME_LIST_FOOD = "food_list.txt"; + String FILEPATH_LIST_FOOD = FILEPATH + FILENAME_LIST_FOOD; + String FILENAME_BANK_EXERCISE = "exercise_bank.txt"; + String FILEPATH_BANK_EXERCISE = FILEPATH + FILENAME_BANK_EXERCISE; + String FILENAME_LIST_EXERCISE = "exercise_list.txt"; + String FILEPATH_LIST_EXERCISE = FILEPATH + FILENAME_LIST_EXERCISE; + String FILENAME_LIST_FUTURE = "future_list.txt"; + String FILEPATH_LIST_FUTURE = FILEPATH + FILENAME_LIST_FUTURE; + + /** + * Loads all the data into the DataManager object. + * + * @return DataManager containing data loaded from storage + */ + DataManager loadAll(); + + /** + * Saves all the data in the DataManager object. + * Usually used when exiting the program. + * + * @param dataManager DataManager containing the data to be saved + */ + void saveAll(DataManager dataManager); +} diff --git a/src/main/java/seedu/duke/storage/StorageManager.java b/src/main/java/seedu/duke/storage/StorageManager.java new file mode 100644 index 0000000000..cd06b024c0 --- /dev/null +++ b/src/main/java/seedu/duke/storage/StorageManager.java @@ -0,0 +1,170 @@ +package seedu.duke.storage; + +import seedu.duke.data.DataManager; +import seedu.duke.data.item.ItemBank; +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.data.item.exercise.FutureExerciseList; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.data.profile.Profile; +import seedu.duke.storage.data.exercise.exercisebank.ExerciseBankStorage; +import seedu.duke.storage.data.exercise.exercisebank.ExerciseBankStorageUtils; +import seedu.duke.storage.data.exercise.exerciselist.ExerciseListStorageUtils; +import seedu.duke.storage.data.exercise.exerciselist.ExerciseListStorage; +import seedu.duke.storage.data.exercise.futurelist.FutureExerciseListStorageUtils; +import seedu.duke.storage.data.exercise.futurelist.UpcomingStorage; +import seedu.duke.storage.data.food.foodbank.FoodBankStorage; +import seedu.duke.storage.data.food.foodbank.FoodBankStorageUtils; +import seedu.duke.storage.data.food.foodlist.FoodListStorage; +import seedu.duke.storage.data.food.foodlist.FoodListStorageUtils; +import seedu.duke.storage.data.profile.ProfileStorage; +import seedu.duke.storage.data.profile.ProfileStorageUtils; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Manages the loading and saving from various storage subclasses. + */ +public class StorageManager implements Storage { + + private final ProfileStorage profileStorage; + private final ExerciseListStorage exerciseListStorage; + private final FoodListStorage foodListStorage; + private final UpcomingStorage futureExerciseListStorage; + private final FoodBankStorage foodBankStorage; + private final ExerciseBankStorage exerciseBankStorage; + + /** + * Constructor for the StorageManager object. + */ + public StorageManager() { + this.profileStorage = new ProfileStorageUtils(Storage.FILEPATH_PROFILE); + this.exerciseListStorage = new ExerciseListStorageUtils(Storage.FILEPATH_LIST_EXERCISE); + this.foodListStorage = new FoodListStorageUtils(Storage.FILEPATH_LIST_FOOD); + this.futureExerciseListStorage = new FutureExerciseListStorageUtils(Storage.FILEPATH_LIST_FUTURE); + this.foodBankStorage = new FoodBankStorageUtils(Storage.FILEPATH_BANK_FOOD); + this.exerciseBankStorage = new ExerciseBankStorageUtils(Storage.FILEPATH_BANK_EXERCISE); + } + + @Override + public DataManager loadAll() { + return new DataManager( + loadExerciseList(), + loadFutureExerciseList(), + loadFoodList(), + loadExerciseBank(), + loadFoodBank(), + loadProfile()); + } + + @Override + public void saveAll(DataManager dataManager) { + try { + saveProfile(dataManager.getProfile()); + saveExerciseList(dataManager.getExerciseItems()); + saveFoodList(dataManager.getFoodItems()); + saveFutureExerciseList(dataManager.getFutureExerciseItems()); + saveFoodBank(dataManager.getFoodBank()); + saveExerciseBank(dataManager.getExerciseBank()); + } catch (UnableToWriteFileException e) { + System.out.println("Fitbot was unable to save all your session data. :("); + } + } + + + //=================== Profile Methods ======================= + + @Override + public Profile loadProfile() { + try { + return profileStorage.loadProfile(); + } catch (UnableToReadFileException e) { + return new Profile(); + } + } + + @Override + public void saveProfile(Profile profile) throws UnableToWriteFileException { + profileStorage.saveProfile(profile); + } + + //=================== ExerciseList Methods ================== + + @Override + public ExerciseList loadExerciseList() { + try { + return exerciseListStorage.loadExerciseList(); + } catch (UnableToReadFileException e) { + return new ExerciseList(); + } + } + + @Override + public void saveExerciseList(ExerciseList exerciseList) throws UnableToWriteFileException { + exerciseListStorage.saveExerciseList(exerciseList); + } + + //================= FutureExerciseList Methods ============== + + @Override + public FutureExerciseList loadFutureExerciseList() { + try { + return futureExerciseListStorage.loadFutureExerciseList(); + } catch (UnableToReadFileException e) { + return new FutureExerciseList(); + } + } + + @Override + public void saveFutureExerciseList(FutureExerciseList futureExerciseList) throws UnableToWriteFileException { + futureExerciseListStorage.saveFutureExerciseList(futureExerciseList); + } + + //===================== FoodList Methods ==================== + + @Override + public FoodList loadFoodList() { + try { + return foodListStorage.loadFoodList(); + } catch (UnableToReadFileException e) { + return new FoodList(); + } + } + + @Override + public void saveFoodList(FoodList foodList) throws UnableToWriteFileException { + foodListStorage.saveFoodList(foodList); + } + + //================= ExerciseBank Methods ==================== + + @Override + public ItemBank loadExerciseBank() { + try { + return exerciseBankStorage.loadExerciseBank(); + } catch (UnableToReadFileException e) { + return new ItemBank(); + } + } + + @Override + public void saveExerciseBank(ItemBank exerciseBank) throws UnableToWriteFileException { + exerciseBankStorage.saveExerciseBank(exerciseBank); + } + + //===================== FoodBank Methods ==================== + + @Override + public ItemBank loadFoodBank() { + try { + return foodBankStorage.loadFoodBank(); + } catch (UnableToReadFileException e) { + return new ItemBank(); + } + } + + @Override + public void saveFoodBank(ItemBank foodBank) throws UnableToWriteFileException { + foodBankStorage.saveFoodBank(foodBank); + } + +} diff --git a/src/main/java/seedu/duke/storage/StorageUtils.java b/src/main/java/seedu/duke/storage/StorageUtils.java new file mode 100644 index 0000000000..cc75fe0f51 --- /dev/null +++ b/src/main/java/seedu/duke/storage/StorageUtils.java @@ -0,0 +1,18 @@ +package seedu.duke.storage; + +import java.util.logging.Logger; + +/** + * Utilities for the various Storages to inherit. + */ +public abstract class StorageUtils { + + protected static final Logger logger = Logger.getLogger(StorageUtils.class.getName()); + + protected String filePath; + protected String fileName; + + protected String getFileName(String path) { + return path.split("/")[2]; + } +} diff --git a/src/main/java/seedu/duke/storage/data/ItemBankDecoder.java b/src/main/java/seedu/duke/storage/data/ItemBankDecoder.java new file mode 100644 index 0000000000..41f720d808 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/ItemBankDecoder.java @@ -0,0 +1,99 @@ +package seedu.duke.storage.data; + +import seedu.duke.data.item.Item; +import seedu.duke.data.item.ItemBank; +import seedu.duke.data.item.exceptions.DuplicateItemInBankException; +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.food.Food; +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; +import seedu.duke.storage.Storage; +import seedu.duke.storage.data.exercise.exercisebank.ExerciseBankStorageUtils; +import seedu.duke.storage.data.food.foodbank.FoodBankStorageUtils; +import seedu.duke.storage.exceptions.InvalidDataException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Scanner; + +/** + * Decodes the ItemBanks from their respective data storage files. + */ +public class ItemBankDecoder { + + /** + * Retrieves data from ItemBank storage. + * + * @param filePath path of item bank to be retrieved + * @param type type of ItemBank to retrieve to + * @return the items that have been successfully loaded from the filepath + * @throws FileNotFoundException If the filepath provided is invalid + */ + public static ItemBank retrieveDataFromItemBank(String filePath, + String type) throws FileNotFoundException { + ItemBank items = new ItemBank(); + File file = new File(filePath); + Scanner in = new Scanner(file); + decodeItems(type, items, in); + return items; + } + + private static void decodeItems(String type, ItemBank items, Scanner in) { + while (in.hasNext()) { + try { + decodeFoodBankDataFromString(items, in.nextLine(), type); + } catch (InvalidDataException e) { + System.out.println(e.getMessage()); + } + } + } + + private static void decodeFoodBankDataFromString(ItemBank items, String line, + String type) throws InvalidDataException { + try { + final String[] itemDetails = line.split(Storage.FILE_TEXT_DELIMITER); + final String name = itemDetails[1]; + final int calories = Integer.parseInt(itemDetails[2]); + addToRespectiveBank(items, type, name, calories); + } catch (IndexOutOfBoundsException | NumberFormatException | NullPointerException + | DuplicateItemInBankException | InvalidCharacteristicException e) { + throw new InvalidDataException(Storage.FILENAME_BANK_FOOD, line); + } + } + + private static void addToRespectiveBank(ItemBank items, String type, String name, int calories) + throws DuplicateItemInBankException, InvalidCharacteristicException { + if (isFoodType(type)) { + addFood(items, name, calories); + } else if (isExerciseType(type)) { + addExercise(items, name, calories); + } + } + + private static void addExercise(ItemBank items, String name, int calories) + throws InvalidCharacteristicException, DuplicateItemInBankException { + final Exercise exercise = new Exercise(name, calories); + checkItemValidity(exercise); + items.addItem(exercise); + } + + private static void checkItemValidity(Item item) throws InvalidCharacteristicException { + if (!item.isValid()) { + throw new InvalidCharacteristicException(item.toString()); + } + } + + private static void addFood(ItemBank items, String name, int calories) + throws InvalidCharacteristicException, DuplicateItemInBankException { + Food food = new Food(name, calories); + checkItemValidity(food); + items.addItem(food); + } + + private static boolean isExerciseType(String type) { + return type.equals(ExerciseBankStorageUtils.TYPE); + } + + private static boolean isFoodType(String type) { + return type.equals(FoodBankStorageUtils.TYPE); + } +} diff --git a/src/main/java/seedu/duke/storage/data/ItemEncoder.java b/src/main/java/seedu/duke/storage/data/ItemEncoder.java new file mode 100644 index 0000000000..09e825bced --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/ItemEncoder.java @@ -0,0 +1,24 @@ +package seedu.duke.storage.data; + +import seedu.duke.data.item.ItemBank; + +import java.util.ArrayList; + +/** + * Encodes all the items available for FitBot (Other than Profile). + */ +public class ItemEncoder { + /** + * Encodes the list of items in the item bank in preparation for storage. + * + * @param itemBank The list of items to be encoded + * @return An ArrayList of the items to be stored + */ + public static ArrayList encode(ItemBank itemBank) { + ArrayList items = new ArrayList<>(); + for (int i = 0; i < itemBank.getSize(); i++) { + items.add(itemBank.getItem(i).toFileTextString()); + } + return items; + } +} diff --git a/src/main/java/seedu/duke/storage/data/ListDecoder.java b/src/main/java/seedu/duke/storage/data/ListDecoder.java new file mode 100644 index 0000000000..4ed80170e6 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/ListDecoder.java @@ -0,0 +1,38 @@ +package seedu.duke.storage.data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * An abstract class that inherits date parsing functionality for lists. + */ +public abstract class ListDecoder { + + protected static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + protected static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm"); + + protected static LocalDate parseDate(String date) { + return LocalDate.parse(date, DATE_FORMATTER); + } + + protected static LocalDateTime parseDateTime(String dateTime) { + return LocalDateTime.parse(dateTime, DATE_TIME_FORMATTER); + } + + protected static boolean isWithinPastTenYears(LocalDate date) { + //After 10 years ago from today + LocalDate lowerLimit = LocalDate.now().minusYears(10); + //Before tomorrow (today) + LocalDate upperLimit = LocalDate.now().plusDays(1); + return date.isAfter(lowerLimit) && date.isBefore(upperLimit); + } + + protected static boolean isWithinNextYear(LocalDate date) { + //After the past 7 days (today) + LocalDate lowerLimit = LocalDate.now().minusDays(7); + //Before 1 year from today + LocalDate upperLimit = LocalDate.now().plusYears(1); + return date.isAfter(lowerLimit) && !date.isAfter(upperLimit); + } +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/exercisebank/ExerciseBankStorage.java b/src/main/java/seedu/duke/storage/data/exercise/exercisebank/ExerciseBankStorage.java new file mode 100644 index 0000000000..42598b1878 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/exercisebank/ExerciseBankStorage.java @@ -0,0 +1,28 @@ +package seedu.duke.storage.data.exercise.exercisebank; + +import seedu.duke.data.item.ItemBank; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Interface that ensures both the storage and storage manager has the + * required functions to load and save from the exercise bank. + */ +public interface ExerciseBankStorage { + + /** + * Loads the exercise bank from the file. + * + * @return ItemBank object with the loaded bank items from the data file. + * @throws UnableToReadFileException If the file is inaccessible or due to environment variables + */ + ItemBank loadExerciseBank() throws UnableToReadFileException; + + /** + * Saves the exercise bank to the file. + * + * @throws UnableToReadFileException If the file is inaccessible or due to environment variables + */ + void saveExerciseBank(ItemBank exerciseBank) throws UnableToWriteFileException; + +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/exercisebank/ExerciseBankStorageUtils.java b/src/main/java/seedu/duke/storage/data/exercise/exercisebank/ExerciseBankStorageUtils.java new file mode 100644 index 0000000000..400e63ded9 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/exercisebank/ExerciseBankStorageUtils.java @@ -0,0 +1,47 @@ +package seedu.duke.storage.data.exercise.exercisebank; + +import seedu.duke.data.item.ItemBank; +import seedu.duke.storage.StorageUtils; +import seedu.duke.storage.data.ItemBankDecoder; +import seedu.duke.storage.data.ItemEncoder; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.storage.utilities.FileChecker; +import seedu.duke.storage.utilities.FileSaver; + +import java.io.IOException; +import java.util.logging.Level; + +/** + * This storage handles the loading and saving of exercise bank items. + */ +public class ExerciseBankStorageUtils extends StorageUtils implements ExerciseBankStorage { + + public static final String TYPE = "Exercise"; + + /** + * Constructor for the exercise bank storage. + * + * @param filePath of the exercise bank data file + */ + public ExerciseBankStorageUtils(String filePath) { + this.filePath = filePath; + fileName = getFileName(filePath); + } + + @Override + public ItemBank loadExerciseBank() throws UnableToReadFileException { + try { + FileChecker.createFileIfMissing(filePath); + return ItemBankDecoder.retrieveDataFromItemBank(filePath, TYPE); + } catch (IOException e) { + logger.log(Level.FINE, "The path is missing ", filePath); + throw new UnableToReadFileException(filePath); + } + } + + @Override + public void saveExerciseBank(ItemBank exerciseBank) throws UnableToWriteFileException { + FileSaver.saveTo(filePath, ItemEncoder.encode(exerciseBank)); + } +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListDecoder.java b/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListDecoder.java new file mode 100644 index 0000000000..3dacbe633a --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListDecoder.java @@ -0,0 +1,58 @@ +package seedu.duke.storage.data.exercise.exerciselist; + +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.storage.Storage; +import seedu.duke.storage.data.ListDecoder; +import seedu.duke.storage.exceptions.InvalidDataException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.util.Scanner; + +/** + * Decodes the exercise list from storage. + */ +public class ExerciseListDecoder extends ListDecoder { + + /** + * Retrieves exercise list or upcoming list from data. + * + * @return The exercise list with data loaded from file + * @throws FileNotFoundException If file is misplaced/missing + */ + public static ExerciseList retrieveExerciseListFromData(String filePath) throws FileNotFoundException { + ExerciseList exercises = new ExerciseList(); + File file = new File(filePath); + Scanner in = new Scanner(file); + decodeExercises(exercises, in); + return exercises; + } + + private static void decodeExercises(ExerciseList exercises, Scanner in) { + while (in.hasNext()) { + try { + decodeExerciseDataFromString(exercises, in.nextLine()); + } catch (InvalidDataException e) { + System.out.println(e.getMessage()); + } + } + } + + private static void decodeExerciseDataFromString(ExerciseList exercises, String line) throws InvalidDataException { + try { + final String[] exerciseDetails = line.split(Storage.FILE_TEXT_DELIMITER); + final String name = exerciseDetails[1]; + final int calories = Integer.parseInt(exerciseDetails[2]); + final LocalDate dateOfExercise = parseDate(exerciseDetails[3]); + final Exercise exercise = new Exercise(name, calories, dateOfExercise); + if (!exercise.isValid() || !isWithinPastTenYears(dateOfExercise)) { + throw new InvalidDataException(Storage.FILENAME_LIST_EXERCISE, line); + } + exercises.addItem(exercise); + } catch (IndexOutOfBoundsException | NumberFormatException | NullPointerException e) { + throw new InvalidDataException(Storage.FILENAME_LIST_EXERCISE, line); + } + } +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListStorage.java b/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListStorage.java new file mode 100644 index 0000000000..58e29fcdb2 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListStorage.java @@ -0,0 +1,31 @@ +package seedu.duke.storage.data.exercise.exerciselist; + +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Interface that ensures both the storage and storage manager has the + * required functions to load and save from exercise list storage. + */ +public interface ExerciseListStorage { + + /** + * Load exercises into an ExerciseList object. + * Used when the selected profile is accessed and its respective ExerciseList is loaded. + * + * @return ExerciseList object with the details from the storage file + * @throws UnableToReadFileException If the file is inaccessible or due to environment variables + */ + ExerciseList loadExerciseList() throws UnableToReadFileException; + + /** + * Saves the exercises into storage. + * Used when there is an update to the list. + * + * @param exercises ExerciseList to be saved + * @throws UnableToReadFileException If the file is inaccessible or due to environment variables + */ + void saveExerciseList(ExerciseList exercises) throws UnableToWriteFileException; + +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListStorageUtils.java b/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListStorageUtils.java new file mode 100644 index 0000000000..caf4082cb2 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/exerciselist/ExerciseListStorageUtils.java @@ -0,0 +1,44 @@ +package seedu.duke.storage.data.exercise.exerciselist; + +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.storage.StorageUtils; +import seedu.duke.storage.data.ItemEncoder; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.storage.utilities.FileChecker; +import seedu.duke.storage.utilities.FileSaver; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.logging.Level; + +/** + * This storage handles the loading and saving of exercise list items. + */ +public class ExerciseListStorageUtils extends StorageUtils implements ExerciseListStorage { + + /** + * Constructor for exercise list storage object. + * + * @param filePath of where the exercise list should be stored. + */ + public ExerciseListStorageUtils(String filePath) { + this.filePath = filePath; + this.fileName = getFileName(filePath); + } + + @Override + public ExerciseList loadExerciseList() throws UnableToReadFileException { + try { + FileChecker.createFileIfMissing(filePath); + return ExerciseListDecoder.retrieveExerciseListFromData(filePath); + } catch (IOException e) { + throw new UnableToReadFileException(filePath); + } + } + + @Override + public void saveExerciseList(ExerciseList exercises) throws UnableToWriteFileException { + FileSaver.saveTo(filePath, ItemEncoder.encode(exercises)); + } +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/futurelist/FutureExerciseListDecoder.java b/src/main/java/seedu/duke/storage/data/exercise/futurelist/FutureExerciseListDecoder.java new file mode 100644 index 0000000000..3062dbebfc --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/futurelist/FutureExerciseListDecoder.java @@ -0,0 +1,61 @@ +package seedu.duke.storage.data.exercise.futurelist; + +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.exercise.FutureExerciseList; +import seedu.duke.storage.Storage; +import seedu.duke.storage.data.ListDecoder; +import seedu.duke.storage.exceptions.InvalidDataException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.Scanner; + +/** + * Decodes the upcoming exercises from future exercise list data file. + */ +public class FutureExerciseListDecoder extends ListDecoder { + + /** + * Retrieves future exercise list from future_list.txt. + * + * @return The exercise list with data loaded from file + * @throws FileNotFoundException If file is misplaced/missing + */ + public static FutureExerciseList retrieveUpcomingListFromData(String filePath) throws FileNotFoundException { + FutureExerciseList exercises = new FutureExerciseList(); + File file = new File(filePath); + Scanner in = new Scanner(file); + decodeUpcomingExercises(exercises, in); + return exercises; + } + + private static void decodeUpcomingExercises(FutureExerciseList exercises, Scanner in) { + while (in.hasNext()) { + try { + decodeUpcomingExerciseDataFromString(exercises, in.nextLine()); + } catch (InvalidDataException e) { + System.out.println(e.getMessage()); + } + } + } + + private static void decodeUpcomingExerciseDataFromString(FutureExerciseList exercises, + String line) throws InvalidDataException { + try { + final String[] exerciseDetails = line.split(Storage.FILE_TEXT_DELIMITER); + final String name = exerciseDetails[1]; + final int calories = Integer.parseInt(exerciseDetails[2]); + final LocalDate dateOfExercise = parseDate(exerciseDetails[3]); + final Exercise exercise = new Exercise(name, calories, dateOfExercise); + if (!exercise.isValid() || !isWithinNextYear(dateOfExercise)) { + throw new InvalidDataException(Storage.FILENAME_LIST_FUTURE, line); + } + exercises.addItem(new Exercise(name, calories, dateOfExercise)); + } catch (IndexOutOfBoundsException | NumberFormatException | NullPointerException + | DateTimeParseException e) { + throw new InvalidDataException(Storage.FILENAME_LIST_FUTURE, line); + } + } +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/futurelist/FutureExerciseListStorageUtils.java b/src/main/java/seedu/duke/storage/data/exercise/futurelist/FutureExerciseListStorageUtils.java new file mode 100644 index 0000000000..07de46289f --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/futurelist/FutureExerciseListStorageUtils.java @@ -0,0 +1,45 @@ +package seedu.duke.storage.data.exercise.futurelist; + +import seedu.duke.data.item.exercise.FutureExerciseList; +import seedu.duke.storage.StorageUtils; +import seedu.duke.storage.data.ItemEncoder; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.storage.utilities.FileChecker; +import seedu.duke.storage.utilities.FileSaver; + +import java.io.IOException; +import java.util.logging.Level; + +/** + * Storage that handles the saving and loading of data files of upcoming exercises in future exercise storage. + */ +public class FutureExerciseListStorageUtils extends StorageUtils implements UpcomingStorage { + + /** + * Constructor for future exercise list storage. + * + * @param filePath of where the future exercise list should be stored + */ + public FutureExerciseListStorageUtils(String filePath) { + this.filePath = filePath; + this.fileName = getFileName(filePath); + } + + @Override + public FutureExerciseList loadFutureExerciseList() throws UnableToReadFileException { + try { + FileChecker.createFileIfMissing(filePath); + return FutureExerciseListDecoder.retrieveUpcomingListFromData(filePath); + } catch (IOException e) { + logger.log(Level.FINE, "The path is missing ", filePath); + throw new UnableToReadFileException(fileName); + } + } + + @Override + public void saveFutureExerciseList(FutureExerciseList futureExercises) throws UnableToWriteFileException { + FileSaver.saveTo(filePath, ItemEncoder.encode(futureExercises)); + } + +} diff --git a/src/main/java/seedu/duke/storage/data/exercise/futurelist/UpcomingStorage.java b/src/main/java/seedu/duke/storage/data/exercise/futurelist/UpcomingStorage.java new file mode 100644 index 0000000000..61a5d3e1d3 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/exercise/futurelist/UpcomingStorage.java @@ -0,0 +1,29 @@ +package seedu.duke.storage.data.exercise.futurelist; + +import seedu.duke.data.item.exercise.FutureExerciseList; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Interface that ensures both the storage and storage manager has the + * required functions to load and save from the future exercise list in storage. + */ +public interface UpcomingStorage { + + /** + * Loads the future exercise list from storage. + * + * @return the future exercise list with data loaded in + * @throws UnableToReadFileException If the file is inaccessible or due to environment variables + */ + FutureExerciseList loadFutureExerciseList() throws UnableToReadFileException; + + /** + * Saves the future exercise list into storage. + * + * @param futureExercises the future exercises to be saved to data + * @throws UnableToWriteFileException If the file is inaccessible or due to environment variables + */ + void saveFutureExerciseList(FutureExerciseList futureExercises) throws UnableToWriteFileException; + +} diff --git a/src/main/java/seedu/duke/storage/data/food/foodbank/FoodBankStorage.java b/src/main/java/seedu/duke/storage/data/food/foodbank/FoodBankStorage.java new file mode 100644 index 0000000000..0dfb80ee52 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/food/foodbank/FoodBankStorage.java @@ -0,0 +1,27 @@ +package seedu.duke.storage.data.food.foodbank; + +import seedu.duke.data.item.ItemBank; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Interface that ensures the storage device has a load and save food bank method. + */ +public interface FoodBankStorage { + + /** + * Loads the food bank file from data storage. + * + * @return FoodBank object from data storage + * @throws UnableToReadFileException if the filepath given is inaccessible or I/O was interrupted + */ + ItemBank loadFoodBank() throws UnableToReadFileException; + + /** + * Saves the Food Bank into storage. + * Used when there is an update to the Food Bank. + * + * @param foodBank FoodBank that is to be saved + */ + void saveFoodBank(ItemBank foodBank) throws UnableToWriteFileException; +} diff --git a/src/main/java/seedu/duke/storage/data/food/foodbank/FoodBankStorageUtils.java b/src/main/java/seedu/duke/storage/data/food/foodbank/FoodBankStorageUtils.java new file mode 100644 index 0000000000..888daf6842 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/food/foodbank/FoodBankStorageUtils.java @@ -0,0 +1,47 @@ +package seedu.duke.storage.data.food.foodbank; + +import seedu.duke.data.item.ItemBank; +import seedu.duke.storage.StorageUtils; +import seedu.duke.storage.data.ItemBankDecoder; +import seedu.duke.storage.data.ItemEncoder; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.storage.utilities.FileChecker; +import seedu.duke.storage.utilities.FileSaver; + +import java.io.IOException; +import java.util.logging.Level; + +/** + * A Storage class that handles the saving and loading of the FoodBank. + */ +public class FoodBankStorageUtils extends StorageUtils implements FoodBankStorage { + + public static final String TYPE = "Food"; + + /** + * Constructs the food bank storage handler with its respective path. + * + * @param path the directory to save the food bank file + */ + public FoodBankStorageUtils(String path) { + this.filePath = path; + this.fileName = getFileName(path); + } + + @Override + public ItemBank loadFoodBank() throws UnableToReadFileException { + try { + FileChecker.createFileIfMissing(filePath); + return ItemBankDecoder.retrieveDataFromItemBank(filePath, TYPE); + } catch (IOException e) { + logger.log(Level.FINE, "The path is missing ", filePath); + throw new UnableToReadFileException(filePath); + } + } + + @Override + public void saveFoodBank(ItemBank foodBank) throws UnableToWriteFileException { + FileSaver.saveTo(filePath, ItemEncoder.encode(foodBank)); + } +} diff --git a/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListDecoder.java b/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListDecoder.java new file mode 100644 index 0000000000..581c2c0c84 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListDecoder.java @@ -0,0 +1,59 @@ +package seedu.duke.storage.data.food.foodlist; + +import seedu.duke.data.item.food.Food; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.storage.Storage; +import seedu.duke.storage.data.ListDecoder; +import seedu.duke.storage.exceptions.InvalidDataException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.util.Scanner; + +/** + * Decodes the food list from the storage file. + */ +public class FoodListDecoder extends ListDecoder { + + /** + * Retrieves food list from food_list.txt. + * + * @return The food list with data loaded from file + * @throws FileNotFoundException If file is misplaced/missing + */ + public static FoodList retrieveFoodListFromData(String filePath) throws FileNotFoundException { + FoodList foodItems = new FoodList(); + File file = new File(filePath); + Scanner in = new Scanner(file); + decodeFoodItems(foodItems, in); + return foodItems; + } + + private static void decodeFoodItems(FoodList foodItems, Scanner in) { + while (in.hasNext()) { + try { + decodeFoodDataFromString(foodItems, in.nextLine()); + } catch (InvalidDataException e) { + System.out.println(e.getMessage()); + } + } + } + + private static void decodeFoodDataFromString(FoodList foodItems, String line) throws InvalidDataException { + try { + final String[] foodDetails = line.split(Storage.FILE_TEXT_DELIMITER); + final String name = foodDetails[1]; + final int calories = Integer.parseInt(foodDetails[2]); + final LocalDateTime dateTimeOfFood = parseDateTime(foodDetails[3]); + final Food food = new Food(name, calories, dateTimeOfFood); + if (!food.isValid() || !isWithinPastTenYears(dateTimeOfFood.toLocalDate())) { + throw new InvalidDataException(Storage.FILENAME_LIST_FOOD, line); + } + foodItems.addItem(new Food(name, calories, dateTimeOfFood)); + } catch (IndexOutOfBoundsException | NumberFormatException | NullPointerException | DateTimeException e) { + throw new InvalidDataException(Storage.FILENAME_LIST_FOOD, line); + } + } +} diff --git a/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListStorage.java b/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListStorage.java new file mode 100644 index 0000000000..de1b0d96a5 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListStorage.java @@ -0,0 +1,29 @@ +package seedu.duke.storage.data.food.foodlist; + +import seedu.duke.data.item.food.FoodList; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Interface that ensures both the storage and storage manager has the + * required functions to load and save from food list. + */ +public interface FoodListStorage { + + /** + * Load food items into a FoodList object. + * Used when the selected profile is accessed and its respective ExerciseList is loaded. + * + * @return FoodList object with the details from the storage file + * @throws UnableToReadFileException If the file is inaccessible or due to environment variables + */ + FoodList loadFoodList() throws UnableToReadFileException; + + /** + * Saves the food items into the respective food list data file. + * + * @param foodItems food items to be saved to the storage file + * @throws UnableToWriteFileException If the file is inaccessible or due to environment variables + */ + void saveFoodList(FoodList foodItems) throws UnableToWriteFileException; +} diff --git a/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListStorageUtils.java b/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListStorageUtils.java new file mode 100644 index 0000000000..b8547b4589 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/food/foodlist/FoodListStorageUtils.java @@ -0,0 +1,44 @@ +package seedu.duke.storage.data.food.foodlist; + +import seedu.duke.data.item.food.FoodList; +import seedu.duke.storage.StorageUtils; +import seedu.duke.storage.data.ItemEncoder; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.storage.utilities.FileChecker; +import seedu.duke.storage.utilities.FileSaver; + +import java.io.IOException; +import java.util.logging.Level; + +/** + * Storage that handles the saving and loading of data files of upcoming exercises in future exercise storage. + */ +public class FoodListStorageUtils extends StorageUtils implements FoodListStorage { + + /** + * Constructor for food list storage object. + * + * @param filePath of where the food list should be saved + */ + public FoodListStorageUtils(String filePath) { + this.filePath = filePath; + this.fileName = getFileName(filePath); + } + + @Override + public FoodList loadFoodList() throws UnableToReadFileException { + try { + FileChecker.createFileIfMissing(filePath); + return FoodListDecoder.retrieveFoodListFromData(filePath); + } catch (IOException e) { + logger.log(Level.FINE, "The path is missing ", filePath); + throw new UnableToReadFileException(fileName); + } + } + + @Override + public void saveFoodList(FoodList foodItems) throws UnableToWriteFileException { + FileSaver.saveTo(filePath, ItemEncoder.encode(foodItems)); + } +} diff --git a/src/main/java/seedu/duke/storage/data/profile/ProfileDecoder.java b/src/main/java/seedu/duke/storage/data/profile/ProfileDecoder.java new file mode 100644 index 0000000000..2737fcd366 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/profile/ProfileDecoder.java @@ -0,0 +1,114 @@ +package seedu.duke.storage.data.profile; + +import seedu.duke.data.profile.Profile; +import seedu.duke.data.profile.attributes.ActivityFactor; +import seedu.duke.data.profile.attributes.Age; +import seedu.duke.data.profile.attributes.CalorieGoal; +import seedu.duke.data.profile.attributes.Gender; +import seedu.duke.data.profile.attributes.Height; +import seedu.duke.data.profile.attributes.Name; +import seedu.duke.data.profile.attributes.Weight; +import seedu.duke.storage.Storage; +import seedu.duke.storage.exceptions.InvalidDataException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Scanner; + +/** + * Decodes the profile from storage data. + */ +public class ProfileDecoder { + + /** + * Retrieves profile data from profile.txt + * + * @return The profile object with its corresponding characteristics + * @throws FileNotFoundException If the file is misplaced/missing + */ + public static Profile retrieveProfileFromData(String filePath) throws FileNotFoundException { + try { + File file = new File(filePath); + Scanner in = new Scanner(file); + if (in.hasNext()) { + return decodeProfile(in.nextLine()); + } + } catch (InvalidDataException e) { + System.out.println(e.getMessage()); + } + throw new FileNotFoundException(); + } + + private static Profile decodeProfile(String line) throws InvalidDataException { + try { + final String[] profileDetails = line.split(Storage.FILE_TEXT_DELIMITER); + final Name name = decodeName(profileDetails[0]); + final Height height = decodeHeight(profileDetails[1]); + final Weight weight = decodeWeight(profileDetails[2]); + final Gender gender = decodeGender(profileDetails[3]); + final Age age = decodeAge(profileDetails[4]); + final CalorieGoal calorieGoal = decodeCalorieGoal(profileDetails[5]); + final ActivityFactor activityFactor = decodeActivityFactor(profileDetails[6]); + return new Profile(name, height, weight, gender, age, calorieGoal, activityFactor); + } catch (IndexOutOfBoundsException e) { + throw new InvalidDataException(Storage.FILENAME_PROFILE, line); + } + } + + private static Name decodeName(String detail) { + return new Name(detail); + } + + private static Height decodeHeight(String detail) { + try { + return new Height(Double.parseDouble(detail)); + } catch (NumberFormatException e) { + //Returns an invalid height for startup to detect + return new Height(Double.MIN_VALUE); + } + } + + private static Weight decodeWeight(String detail) { + try { + return new Weight(Double.parseDouble(detail)); + } catch (NumberFormatException e) { + //Returns an invalid weight for startup to detect + return new Weight(Double.MIN_VALUE); + } + } + + private static Gender decodeGender(String detail) { + if (detail.length() > 1) { + //Returns an invalid gender for startup to detect + return new Gender('X'); + } + return new Gender(detail.charAt(0)); + } + + private static Age decodeAge(String detail) { + try { + return new Age(Integer.parseInt(detail)); + } catch (NumberFormatException e) { + //Returns an invalid Age for startup to detect + return new Age(Integer.MIN_VALUE); + } + } + + private static CalorieGoal decodeCalorieGoal(String detail) { + try { + return new CalorieGoal(Integer.parseInt(detail)); + } catch (NumberFormatException e) { + //Returns an invalid CalorieGoal for startup to detect + return new CalorieGoal(Integer.MIN_VALUE); + } + } + + private static ActivityFactor decodeActivityFactor(String detail) { + try { + return new ActivityFactor(Integer.parseInt(detail)); + } catch (NumberFormatException e) { + //Returns an invalid ActivityFactor for startup to detect + return new ActivityFactor(Integer.MIN_VALUE); + } + } +} diff --git a/src/main/java/seedu/duke/storage/data/profile/ProfileEncoder.java b/src/main/java/seedu/duke/storage/data/profile/ProfileEncoder.java new file mode 100644 index 0000000000..d9b993b7cb --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/profile/ProfileEncoder.java @@ -0,0 +1,25 @@ +package seedu.duke.storage.data.profile; + +import seedu.duke.data.profile.Profile; + +import java.util.ArrayList; + +/** + * Encodes the profile attributes into an ArrayList to be saved. + */ +public class ProfileEncoder { + + /** + * Encodes profile for storage. + * + * @param profile The profile object to be stored. + * @return An arraylist that contains the profile details to save. + */ + public static ArrayList encode(Profile profile) { + return new ArrayList() { + { + add(profile.toFileTextString()); + } + }; + } +} diff --git a/src/main/java/seedu/duke/storage/data/profile/ProfileStorage.java b/src/main/java/seedu/duke/storage/data/profile/ProfileStorage.java new file mode 100644 index 0000000000..9caeca7e8f --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/profile/ProfileStorage.java @@ -0,0 +1,27 @@ +package seedu.duke.storage.data.profile; + +import seedu.duke.data.profile.Profile; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +/** + * Interface that ensures the storage device has a load and save profile method. + */ +public interface ProfileStorage { + /** + * Loads the profile file from data storage. + * + * @return Profile object from data storage + * @throws UnableToReadFileException if the filepath given is inaccessible or I/O was interrupted + */ + Profile loadProfile() throws UnableToReadFileException; + + /** + * Saves the profile details into storage. + * Used when there is an update to any profile attribute. + * + * @param profile Profile of the current user + * @throws UnableToWriteFileException if the filepath given is inaccessible or I/O was interrupted + */ + void saveProfile(Profile profile) throws UnableToWriteFileException; +} diff --git a/src/main/java/seedu/duke/storage/data/profile/ProfileStorageUtils.java b/src/main/java/seedu/duke/storage/data/profile/ProfileStorageUtils.java new file mode 100644 index 0000000000..6236069083 --- /dev/null +++ b/src/main/java/seedu/duke/storage/data/profile/ProfileStorageUtils.java @@ -0,0 +1,45 @@ +package seedu.duke.storage.data.profile; + +import seedu.duke.data.profile.Profile; +import seedu.duke.storage.StorageUtils; +import seedu.duke.storage.exceptions.UnableToReadFileException; +import seedu.duke.storage.exceptions.UnableToWriteFileException; +import seedu.duke.storage.utilities.FileChecker; +import seedu.duke.storage.utilities.FileSaver; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.logging.Level; + +/** + * A Storage class that handles the saving and loading of the Profile. + */ +public class ProfileStorageUtils extends StorageUtils implements ProfileStorage { + + /** + * Constructs the profile storage handler with its respective path. + * + * @param path the directory to save the profile file + */ + public ProfileStorageUtils(String path) { + this.filePath = path; + this.fileName = getFileName(path); + } + + @Override + public Profile loadProfile() throws UnableToReadFileException { + try { + FileChecker.createFileIfMissing(filePath); + return ProfileDecoder.retrieveProfileFromData(filePath); + } catch (IOException e) { + throw new UnableToReadFileException(fileName); + } + } + + @Override + public void saveProfile(Profile profile) throws UnableToWriteFileException { + FileSaver.saveTo(filePath, ProfileEncoder.encode(profile)); + logger.log(Level.FINE, "Saved profile."); + } + +} diff --git a/src/main/java/seedu/duke/storage/exceptions/InvalidDataException.java b/src/main/java/seedu/duke/storage/exceptions/InvalidDataException.java new file mode 100644 index 0000000000..e0bd9a85d6 --- /dev/null +++ b/src/main/java/seedu/duke/storage/exceptions/InvalidDataException.java @@ -0,0 +1,19 @@ +package seedu.duke.storage.exceptions; + +/** + * Exception that is thrown when the data has been modified and become unreadable. + */ +public class InvalidDataException extends Exception { + private static final String LS = System.lineSeparator(); + private static final String DIVIDER = "............................................................................" + + ".............................."; + private static final String ERROR_MESSAGE = DIVIDER + LS + + "There is an invalid line in %1$s. " + LS + + "The values are either out of accepted range or we are unable to read it." + LS + + "\"%2$s\" will not be loaded into the bot." + LS + DIVIDER; + + public InvalidDataException(String file, String inputData) { + super(String.format(ERROR_MESSAGE, file, inputData)); + } + +} diff --git a/src/main/java/seedu/duke/storage/exceptions/UnableToReadFileException.java b/src/main/java/seedu/duke/storage/exceptions/UnableToReadFileException.java new file mode 100644 index 0000000000..d2f3d9bf61 --- /dev/null +++ b/src/main/java/seedu/duke/storage/exceptions/UnableToReadFileException.java @@ -0,0 +1,13 @@ +package seedu.duke.storage.exceptions; + +/** + * An error that is thrown when the bot is unable to access the file or create the file. + */ +public class UnableToReadFileException extends Exception { + private static final String ERROR_MESSAGE = " file is inaccessible due to an environment error." + + System.lineSeparator() + "Please restart Fitbot and try again! :("; + + public UnableToReadFileException(String fileName) { + super(fileName + ERROR_MESSAGE); + } +} diff --git a/src/main/java/seedu/duke/storage/exceptions/UnableToWriteFileException.java b/src/main/java/seedu/duke/storage/exceptions/UnableToWriteFileException.java new file mode 100644 index 0000000000..d3ace0e1bb --- /dev/null +++ b/src/main/java/seedu/duke/storage/exceptions/UnableToWriteFileException.java @@ -0,0 +1,13 @@ +package seedu.duke.storage.exceptions; + +/** + * An error that indicates if there is an environment error with the file it is trying to write to. + */ +public class UnableToWriteFileException extends Exception { + private static final String ERROR_MESSAGE = "Unable to write to file, something went wrong while saving! " + + "Please restart Fitbot and try again! :("; + + public UnableToWriteFileException() { + super(ERROR_MESSAGE); + } +} diff --git a/src/main/java/seedu/duke/storage/utilities/FileChecker.java b/src/main/java/seedu/duke/storage/utilities/FileChecker.java new file mode 100644 index 0000000000..90cf0df5b6 --- /dev/null +++ b/src/main/java/seedu/duke/storage/utilities/FileChecker.java @@ -0,0 +1,42 @@ +package seedu.duke.storage.utilities; + +import seedu.duke.storage.exceptions.UnableToReadFileException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * A File utility that checks and creates the directory and files if they are missing. + */ +public class FileChecker { + + private static final String FILEPATH = "./data/"; + + /** + * Creates the directory and file if it is unable to find the file. + * + * @param path The file path that it is checking + * @throws UnableToReadFileException If it is unable to find the path specified + */ + public static void createFileIfMissing(String path) throws IOException { + final File file = new File(path); + createDirectory(); + checkFileExists(file); + } + + private static void checkFileExists(File fileToCheck) throws IOException { + if (!fileToCheck.exists()) { + fileToCheck.createNewFile(); + } + } + + private static void createDirectory() throws IOException { + Files.createDirectories(Paths.get(FILEPATH)); + } + + private static String getFileName(String path) { + return path.split("/")[2]; + } +} diff --git a/src/main/java/seedu/duke/storage/utilities/FileSaver.java b/src/main/java/seedu/duke/storage/utilities/FileSaver.java new file mode 100644 index 0000000000..b9cc895559 --- /dev/null +++ b/src/main/java/seedu/duke/storage/utilities/FileSaver.java @@ -0,0 +1,39 @@ +package seedu.duke.storage.utilities; + +import seedu.duke.storage.exceptions.UnableToWriteFileException; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +/** + * A File utility that saves the data into their corresponding file paths. + */ +public class FileSaver { + + /** + * Saves an ArrayList of strings to a given filepath. + * + * @param path filepath of the ArrayList to be stored + * @throws UnableToWriteFileException If the saving is interrupted by an environment variable + */ + public static void saveTo(String path, ArrayList toSave) throws UnableToWriteFileException { + try { + FileWriter fw = new FileWriter(path); + writeTo(fw, toSave); + closeFile(fw); + } catch (IOException e) { + throw new UnableToWriteFileException(); + } + } + + private static void writeTo(FileWriter fw, ArrayList toSave) throws IOException { + for (String item : toSave) { + fw.write(item + System.lineSeparator()); + } + } + + private static void closeFile(FileWriter fw) throws IOException { + fw.close(); + } +} diff --git a/src/main/java/seedu/duke/ui/Ui.java b/src/main/java/seedu/duke/ui/Ui.java new file mode 100644 index 0000000000..5ad7c20b6f --- /dev/null +++ b/src/main/java/seedu/duke/ui/Ui.java @@ -0,0 +1,118 @@ +package seedu.duke.ui; + +import seedu.duke.logic.commands.HelpCommand; +import seedu.duke.logic.parser.exceptions.MissingParamException; + +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class deals with interaction with user on CLI. + * Also helps to change color of output if required. + */ +public class Ui { + + public static final String TAB = "\t"; + public static final String DIVIDER = "___________________________________________" + + "_______________________________________________________________"; + public static final String STAR_DIVIDER = "*====================================================" + + "====================================================*"; + public static final String LS = System.lineSeparator(); + public static final String INDENTED_LS = LS + TAB; + private static final String FITBOT_V0 = " ______ _ _ _ _" + + LS + + " | ____(_) | | | | |" + + LS + + " | |__ _| |_| |__ ___ | |_" + + LS + + " | __| | | __| '_ \\ / _ \\| __|" + + LS + + " | | | | |_| |_) | (_) | |_" + + LS + + " |_| |_|\\__|_.__/ \\___/ \\__|"; + private static final String MESSAGE_WELCOME = "Welcome to Fitbot!" + + " Fitbot is here to help you to keep track of your calories."; + public static final String MESSAGE_DIRECT_HELP = "You can start by typing a command or view the list of " + + "available commands by typing " + HelpCommand.MESSAGE_COMMAND_FORMAT + "." + LS + DIVIDER; + private static final String MESSAGE_FIX_PROFILE = LS + STAR_DIVIDER + LS + " " + + "Fitbot realised that some of your profile " + + "attributes are missing." + + LS + " " + "Please follow the instructions below so that your profile can be complete."; + private static final String MESSAGE_NEW_PROFILE = LS + STAR_DIVIDER + LS + " " + + "Fitbot realised that your profile has not been created." + + " Let's start creating a profile below!"; + private static final String MESSAGE_EMPTY_INPUT = "Input cannot be empty"; + public static final String MESSAGE_CREATE_PROFILE_SUCCESSFUL = LS + + " " + + "Profile created successfully!" + LS + STAR_DIVIDER + LS; + + private Scanner scanner; + + public Ui() { + this.scanner = new Scanner(System.in); + this.printStartApplicationPage(); + } + + public String getUserInput() { + return scanner.nextLine(); + } + + private static Logger logger = Logger.getLogger(Ui.class.getName()); + + /** + * Surround strings with lines for user to differentiate results. + * + * @param messages is the strings that need to be printed on CLI + */ + public void formatMessageFramedWithDivider(String... messages) { + System.out.println(DIVIDER); + for (String message : messages) { + System.out.println(message); + } + System.out.println(DIVIDER); + } + + public void formatMessageWithTopDivider(String... messages) { + System.out.println(DIVIDER); + for (String message : messages) { + System.out.println(message); + } + } + + public void formatMessageWithBottomDivider(String... messages) { + for (String message : messages) { + System.out.println(message); + } + System.out.println(DIVIDER); + } + + public void printStartApplicationPage() { + logger.log(Level.FINE, "start of application"); + System.out.println(FITBOT_V0 + LS + MESSAGE_WELCOME); + } + + public void printStartMessage(boolean isProfileComplete, boolean isProfilePresent) { + if (isProfileComplete) { + System.out.println(MESSAGE_DIRECT_HELP); + return; + } + if (isProfilePresent) { + System.out.println(MESSAGE_FIX_PROFILE); + } else { + System.out.println(MESSAGE_NEW_PROFILE); + } + } + + /** + * Checks if user input is empty. + * + * @param userInput input from the user. + * @throws MissingParamException if input length is 0 (missing). + */ + public void checkEmptyUserInput(String userInput) throws MissingParamException { + if (userInput.length() == 0) { + throw new MissingParamException(MESSAGE_EMPTY_INPUT); + } + } +} diff --git a/src/test/java/seedu/duke/data/profile/ProfileTest.java b/src/test/java/seedu/duke/data/profile/ProfileTest.java new file mode 100644 index 0000000000..da53c336be --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/ProfileTest.java @@ -0,0 +1,127 @@ +package seedu.duke.data.profile; + +import org.junit.jupiter.api.Test; +import seedu.duke.data.profile.exceptions.InvalidCharacteristicException; +import seedu.duke.data.profile.utilities.ProfileUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +class ProfileTest { + + @Test + void calculateBmi_twoDoubleInputs_expectDoubleReturned() throws InvalidCharacteristicException { + final double height = 171.2; + final double weight = 59.8; + assertEquals(20.4, ProfileUtils.calculateBmi(height, weight)); + } + + @Test + void calculateBmi_negativeHeightInput_expectInvalidCharacteristicException() { + final double height = -171.2; + final double weight = 59.8; + assertThrows(InvalidCharacteristicException.class, + () -> ProfileUtils.calculateBmi(height, weight)); + } + + @Test + void calculateBmi_negativeWeightInput_expectInvalidCharacteristicException() { + final double height = 171.2; + final double weight = -59.8; + assertThrows(InvalidCharacteristicException.class, + () -> ProfileUtils.calculateBmi(height, weight)); + } + + @Test + void retrieveBmiStatus_validBmiInputs_expectCorrectStatuses() { + final String expectedStatusUnderweight = "Underweight"; + final String expectedStatusHealthy = "Healthy"; + final String expectedStatusOverweight = "Overweight"; + final String expectedStatusObese = "Obese"; + final double bmiUnderweight = 10.5; + final double bmiHealthy = 22.5; + final double bmiOverweight = 25.5; + final double bmiObese = 30.0; + + assertEquals(expectedStatusUnderweight, ProfileUtils.retrieveBmiStatus(bmiUnderweight)); + assertEquals(expectedStatusHealthy, ProfileUtils.retrieveBmiStatus(bmiHealthy)); + assertEquals(expectedStatusOverweight, ProfileUtils.retrieveBmiStatus(bmiOverweight)); + assertEquals(expectedStatusObese, ProfileUtils.retrieveBmiStatus(bmiObese)); + } + + @Test + void getBmrValuesMen_validInputs_expectCorrectBmrValues() { + Profile p = new Profile(); + final String name = "John"; + final double height = 170.1; + final double weight = 60; + final int calorieGoal = 300; + final char gender = 'M'; + final int age = 22; + final int activityFactor = 1; + final double baseBmr = 1583.5979; + final int bmrSedentary = (int) Math.round(baseBmr * 1.2); + final int bmrLight = (int) Math.round(baseBmr * 1.375); + final int bmrModerate = (int) Math.round(baseBmr * 1.55); + final int bmrIntense = (int) Math.round(baseBmr * 1.725); + final int bmrExtreme = (int) Math.round(baseBmr * 1.9); + + p.setProfileWithRawInputs(name, height, weight, gender, age, calorieGoal, activityFactor); + assertEquals(bmrSedentary, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(2); + assertEquals(bmrLight, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(3); + assertEquals(bmrModerate, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(4); + assertEquals(bmrIntense, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(5); + assertEquals(bmrExtreme, ProfileUtils.getBmr(p)); + } + + @Test + void getBmrValuesFemale_validInputs_expectCorrectBmrValues() { + Profile p = new Profile(); + final String name = "Mary"; + final double height = 160.1; + final double weight = 45.2; + final int calorieGoal = 300; + final char gender = 'F'; + final int age = 20; + final int activityFactor = 1; + final double baseBmr = 1274.9472; + final int bmrSedentary = (int) Math.round(baseBmr * 1.2); + final int bmrLight = (int) Math.round(baseBmr * 1.375); + final int bmrModerate = (int) Math.round(baseBmr * 1.55); + final int bmrIntense = (int) Math.round(baseBmr * 1.725); + final int bmrExtreme = (int) Math.round(baseBmr * 1.9); + + p.setProfileWithRawInputs(name, height, weight, gender, age, calorieGoal, activityFactor); + assertEquals(bmrSedentary, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(2); + assertEquals(bmrLight, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(3); + assertEquals(bmrModerate, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(4); + assertEquals(bmrIntense, ProfileUtils.getBmr(p)); + p.getProfileActivityFactor().setUserInput(5); + assertEquals(bmrExtreme, ProfileUtils.getBmr(p)); + } + + @Test + void toFileTextString_validInputs_expectCorrectString() { + Profile p = new Profile(); + String name = "John"; + double height = 170.1; + double weight = 60; + int calorieGoal = 300; + char gender = 'M'; + int age = 22; + int activityFactor = 1; + + p.setProfileWithRawInputs(name, height, weight, gender, age, calorieGoal, activityFactor); + String correctOutput = "John|170.1|60.0|M|22|300|1"; + assertEquals(correctOutput, p.toFileTextString()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/ActivityFactorTest.java b/src/test/java/seedu/duke/data/profile/attributes/ActivityFactorTest.java new file mode 100644 index 0000000000..62dd0b0af4 --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/ActivityFactorTest.java @@ -0,0 +1,32 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class ActivityFactorTest { + + @Test + void createActivityFactor_validFactors_expectTrue() { + final ActivityFactor a1 = new ActivityFactor(1); + final ActivityFactor a2 = new ActivityFactor(2); + final ActivityFactor a3 = new ActivityFactor(3); + final ActivityFactor a4 = new ActivityFactor(4); + final ActivityFactor a5 = new ActivityFactor(5); + assertTrue(a1.isValid()); + assertTrue(a2.isValid()); + assertTrue(a3.isValid()); + assertTrue(a4.isValid()); + assertTrue(a5.isValid()); + } + + @Test + void createActivityFactor_invalidFactors_expectFalse() { + final ActivityFactor a1 = new ActivityFactor(-1); + final ActivityFactor a2 = new ActivityFactor(6); + assertFalse(a1.isValid()); + assertFalse(a2.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/AgeTest.java b/src/test/java/seedu/duke/data/profile/attributes/AgeTest.java new file mode 100644 index 0000000000..812e2439f1 --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/AgeTest.java @@ -0,0 +1,38 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class AgeTest { + + @Test + void createAge_positiveInputs_expectTrue() { + final Age a1 = new Age(10); + final Age a2 = new Age(25); + final Age a3 = new Age(45); + final Age a4 = new Age(57); + final Age a5 = new Age(80); + assertTrue(a1.isValid()); + assertTrue(a2.isValid()); + assertTrue(a3.isValid()); + assertTrue(a4.isValid()); + assertTrue(a5.isValid()); + } + + @Test + void createAge_nonPositiveInput_expectFalse() { + final Age a1 = new Age(0); + final Age a2 = new Age(-25); + final Age a3 = new Age(-45); + final Age a4 = new Age(-57); + final Age a5 = new Age(-80); + assertFalse(a1.isValid()); + assertFalse(a2.isValid()); + assertFalse(a3.isValid()); + assertFalse(a4.isValid()); + assertFalse(a5.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/CalorieGoalTest.java b/src/test/java/seedu/duke/data/profile/attributes/CalorieGoalTest.java new file mode 100644 index 0000000000..2e64e4dfe6 --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/CalorieGoalTest.java @@ -0,0 +1,26 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class CalorieGoalTest { + + @Test + void createCalorieGoal_validGoalInputs_expectTrue() { + final CalorieGoal c1 = new CalorieGoal(2499); + final CalorieGoal c2 = new CalorieGoal(-2499); + assertTrue(c1.isValid()); + assertTrue(c2.isValid()); + } + + @Test + void createCalorieGoal_invalidGoalInputs_expectTrue() { + final CalorieGoal c1 = new CalorieGoal(239030); + final CalorieGoal c2 = new CalorieGoal(-23435); + assertFalse(c1.isValid()); + assertFalse(c2.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/GenderTest.java b/src/test/java/seedu/duke/data/profile/attributes/GenderTest.java new file mode 100644 index 0000000000..b3dc676f92 --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/GenderTest.java @@ -0,0 +1,26 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class GenderTest { + + @Test + void createGender_validGenderInputs_expectTrue() { + final Gender g1 = new Gender('F'); + final Gender g2 = new Gender('M'); + assertTrue(g1.isValid()); + assertTrue(g2.isValid()); + } + + @Test + void createCalorieGoal_invalidGoalInputs_expectTrue() { + final Gender g1 = new Gender('S'); + final Gender g2 = new Gender('D'); + assertFalse(g1.isValid()); + assertFalse(g2.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/HeightTest.java b/src/test/java/seedu/duke/data/profile/attributes/HeightTest.java new file mode 100644 index 0000000000..d60a6e86e8 --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/HeightTest.java @@ -0,0 +1,26 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class HeightTest { + + @Test + void createHeight_validHeightInputs_expectTrue() { + final Height h1 = new Height(190.0); + final Height h2 = new Height(165.0); + assertTrue(h1.isValid()); + assertTrue(h2.isValid()); + } + + @Test + void createHeight_invalidHeightInputs_expectFalse() { + final Height h1 = new Height(0); + final Height h2 = new Height(-10); + assertFalse(h1.isValid()); + assertFalse(h2.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/NameTest.java b/src/test/java/seedu/duke/data/profile/attributes/NameTest.java new file mode 100644 index 0000000000..1cc4b3f7de --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/NameTest.java @@ -0,0 +1,26 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class NameTest { + + @Test + void createName_validNameInputs_expectTrue() { + final Name n1 = new Name("John"); + final Name n2 = new Name("qwertyuiop<>:{},.;'[]'+=-!@#$%^&*()~`"); + assertTrue(n1.isValid()); + assertTrue(n2.isValid()); + } + + @Test + void createName_invalidNameInputs_expectFalse() { + final Name n1 = new Name("Hello/ooooo"); + final Name n2 = new Name("Delim|ters"); + assertFalse(n1.isValid()); + assertFalse(n2.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/data/profile/attributes/WeightTest.java b/src/test/java/seedu/duke/data/profile/attributes/WeightTest.java new file mode 100644 index 0000000000..1a22ee8f1e --- /dev/null +++ b/src/test/java/seedu/duke/data/profile/attributes/WeightTest.java @@ -0,0 +1,26 @@ +package seedu.duke.data.profile.attributes; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class WeightTest { + + @Test + void createWeight_validWeightInputs_expectTrue() { + final Weight w1 = new Weight(60); + final Weight w2 = new Weight(70.25); + assertTrue(w1.isValid()); + assertTrue(w2.isValid()); + } + + @Test + void createWeight_invalidWeightInputs_expectFalse() { + final Weight w1 = new Weight(0); + final Weight w2 = new Weight(-10); + assertFalse(w1.isValid()); + assertFalse(w2.isValid()); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/exercise/ExerciseListTest.java b/src/test/java/seedu/duke/exercise/ExerciseListTest.java new file mode 100644 index 0000000000..f22423ec68 --- /dev/null +++ b/src/test/java/seedu/duke/exercise/ExerciseListTest.java @@ -0,0 +1,113 @@ +package seedu.duke.exercise; + +import org.junit.jupiter.api.Test; +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.exercise.ExerciseList; + +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; + + +class ExerciseListTest { + + @Test + void addExercise_exerciseClassParameter_expectAddInList() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Jumping Jacks", 100)); + assertEquals("Jumping Jacks (100 cal)", exerciseList.getItem(0).toString()); + } + + @Test + void deleteExercise_exerciseIndex_expectDeleteFromList() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Running", 250)); + exerciseList.addItem(new Exercise("Jumping Jacks", 100)); + exerciseList.deleteItem(0, LocalDate.now()); + assertEquals("Jumping Jacks (100 cal)", exerciseList.getItem(0).toString()); + } + + @Test + void deleteExercise_exerciseIndex_expectCorrectNumberOfTasksLeft() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Running", 250, + LocalDate.parse("19-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + exerciseList.addItem(new Exercise("Jumping Jacks", 100, + LocalDate.parse("19-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + System.out.println(exerciseList.deleteItem(0, + LocalDate.parse("19-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + assertEquals(1, exerciseList.getSize()); + } + + @Test + void deleteExercise_invalidIndex_expectException() { + ExerciseList exerciseList = new ExerciseList(); + assertThrows(IndexOutOfBoundsException.class, () -> exerciseList.deleteItem(0, + LocalDate.parse("19-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + } + + @Test + void totalCalorie_someExercises_expectTotalCalorie() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Running", 250)); + exerciseList.addItem(new Exercise("Jumping Jacks", 100)); + exerciseList.addItem(new Exercise("Skipping", 200)); + exerciseList.addItem(new Exercise("Swimming", 300)); + assertEquals(850, exerciseList.getTotalCalories()); + } + + @Test + void sortExerciseList_callSortExerciseListMethod_expectSortedList() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Running", 250, + LocalDate.parse("2021-10-16", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Jumping Jacks", 100, + LocalDate.parse("2021-10-19", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Skipping", 200, + LocalDate.parse("2021-10-18", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Swimming", 300, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.sortList(); + System.out.println(exerciseList.convertToString()); + } + + @Test + void totalExerciseCaloriesForSingleDate_oneLocalDateInput_expectSumOfCalorieOnThatDay() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Running", 250, + LocalDate.parse("2021-10-16", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Jumping Jacks", 100, + LocalDate.parse("2021-10-19", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Skipping", 200, + LocalDate.parse("2021-10-18", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Swimming", 300, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Jump rope", 453, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Biking", 420, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + assertEquals(1173, exerciseList.getTotalCaloriesWithDate( + LocalDate.parse("17-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + } + + @Test + void printExerciseListByOneGivenDate_inputLocalDate_expectExerciseListOfTheDayOnly() { + ExerciseList exerciseList = new ExerciseList(); + exerciseList.addItem(new Exercise("Running", 250, + LocalDate.parse("2021-10-16", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Jumping Jacks", 100, + LocalDate.parse("2021-10-19", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Skipping", 200, + LocalDate.parse("2021-10-18", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Swimming", 300, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Jump rope", 453, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + exerciseList.addItem(new Exercise("Biking", 420, + LocalDate.parse("2021-10-17", DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + System.out.println(exerciseList.convertToStringBySpecificDate( + LocalDate.parse("17-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + } +} diff --git a/src/test/java/seedu/duke/food/FoodListTest.java b/src/test/java/seedu/duke/food/FoodListTest.java new file mode 100644 index 0000000000..856f9c2e52 --- /dev/null +++ b/src/test/java/seedu/duke/food/FoodListTest.java @@ -0,0 +1,296 @@ +package seedu.duke.food; + +import org.junit.jupiter.api.Test; +import seedu.duke.data.item.food.Food; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.data.item.food.TimePeriod; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FoodListTest { + @Test + void addFoodUsingFoodClassParameter_foodClassParameter_expectExistsInList() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + assertEquals("chicken rice (607 cal) @ 10:20, 16 Oct 2021", + foodList.getItem(foodList.getSize() - 1).toStringWithDate()); + } + + @Test + void printNonEmptyFoodList_nonEmptyFoodList_expectCorrectOutputString() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-19 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1650", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("McDonald's Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1430", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck's Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.sortList(); + System.out.println(foodList.convertToString()); + } + + @Test + void printEmptyFoodList_emptyFoodList_expectEmptyString() { + FoodList foodList = new FoodList(); + assertEquals("", foodList.convertToString()); + } + + @Test + void deleteExistingFoodItem_validIndexInput_expectDeleteSuccessful() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-19 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1650", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("McDonald's Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1430", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck's Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.sortList(); + foodList.deleteItem(1, LocalDateTime.parse("16-10-2021 1020", + DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")).toLocalDate(), + LocalDateTime.parse("16-10-2021 1020", + DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")).toLocalTime()); + System.out.println(foodList.convertToString()); + assertNotEquals(8, foodList.getSize()); + } + + @Test + void deleteNonExistingFoodItem_invalidIndexInput_expectIndexOutOfBoundException() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + assertThrows(IndexOutOfBoundsException.class, () -> foodList.deleteItem(3)); + } + + @Test + void deleteNegativeIndexFoodItem_negativeIndexInput_expectIndexOutOfBoundException() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + assertThrows(IndexOutOfBoundsException.class, () -> foodList.deleteItem(-1)); + } + + @Test + void deleteNonDigitIndexFoodItem_nonDigitIndexInput_expectNumberFormatException() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + assertThrows(NumberFormatException.class, () -> foodList.deleteItem(Integer.parseInt("a"))); + } + + @Test + void deleteFromEmptyFoodList_emptyFoodList_expectIndexOutOfBoundException() { + FoodList foodList = new FoodList(); + assertThrows(IndexOutOfBoundsException.class, () -> foodList.deleteItem(1)); + } + + @Test + void deleteAllFoodItems_callDeleteAllMethod_expectEmptyList() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607)); + foodList.addItem(new Food("yong tau foo", 536)); + foodList.addItem(new Food("mcspicy alacarte", 528)); + foodList.addItem(new Food("char kway teow", 744)); + foodList.clearList(); + assertEquals(0, foodList.getSize()); + } + + @Test + void deleteAllFromEmptyFoodList_emptyFoodList_expectEmptyList() { + FoodList foodList = new FoodList(); + foodList.clearList(); + assertEquals(0, foodList.getSize()); + } + + @Test + void totalFoodCalories_callTotalCaloriesMethod_expectSumOfFoodCalories() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607)); + foodList.addItem(new Food("yong tau foo", 536)); + foodList.addItem(new Food("mcspicy alacarte", 528)); + foodList.addItem(new Food("char kway teow", 744)); + assertEquals(2415, foodList.getTotalCalories()); + } + + @Test + void totalFoodCalories_emptyFoodList_expectZeroSum() { + FoodList foodList = new FoodList(); + assertEquals(0, foodList.getTotalCalories()); + } + + @Test + void totalFoodCaloriesForSingleDate_oneLocalDateInput_expectSumOfCalorieOnThatDay() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-19 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1650", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("McDonald’s Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1430", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck’s Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + assertEquals(1683, foodList.getTotalCaloriesWithDate( + LocalDate.parse("16-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + } + + @Test + void printFoodListByOneGivenDate_inputLocalDate_expectFoodListOfThatDayOnly() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-19 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1650", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("McDonald's Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1430", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck's Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + System.out.println(foodList.convertToStringBySpecificDate( + LocalDate.parse("16-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + } + + @Test + void printFoodListWithDifferentDateAndTimePeriod_inputDateAndTime_expectFoodListWithDateAndTimePeriodSeparated() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1320", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-17 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1850", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("McDonald's Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1030", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck's Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("17-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("fish soup", 300, + LocalDateTime.parse("16-10-2021 1920", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("nasi lemak", 430, + LocalDateTime.parse("16-10-2021 0120", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.sortList(); + System.out.println(foodList.convertToString()); + } + + @Test + void countSupperWithNonEmptyFoodList_callCountSupperMethod_expectCorrectIntegerSupperCount() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1320", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-17 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1850", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("McDonald's Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1030", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck's Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("17-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("fish soup", 300, + LocalDateTime.parse("16-10-2021 1920", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("nasi lemak", 430, + LocalDateTime.parse("16-10-2021 0120", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("cheese chicken burger", 430, + LocalDateTime.parse("18-10-2021 0420", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + assertEquals(2, foodList.getSupperCount()); + } + + @Test + void printFoodListBySpecificDateAndTime_inputDateAndTimePeriod_expectFoodListForTheDateAndTimePeriodOnly() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("2021-10-16 1320", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("char kway teow", 744, + LocalDateTime.parse("2021-10-17 1900", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("mcspicy alacarte", 528, + LocalDateTime.parse("2021-10-18 1850", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("maggie mee", 276, + LocalDateTime.parse("16-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("yong tau foo", 536, + LocalDateTime.parse("2021-10-17 1450", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("McDonald's Medium Fries", 380, + LocalDateTime.parse("16-10-2021 1030", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("Starbuck's Old-fashioned Glazed Donut", 420, + LocalDateTime.parse("17-10-2021 1020", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("fish soup", 300, + LocalDateTime.parse("16-10-2021 1920", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("nasi lemak", 430, + LocalDateTime.parse("16-10-2021 0120", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.addItem(new Food("cheese chicken burger", 430, + LocalDateTime.parse("18-10-2021 0420", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + foodList.sortList(); + System.out.println(foodList.convertToStringBySpecificDateAndTime( + LocalDate.parse("16-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")), + TimePeriod.MORNING)); + } + + @Test + void printFoodListBySpecificDateWithEmptyFoodList_emptyFoodList_expectErrorMessage() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("17-10-2021 2359", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + assertEquals("There is no food item found by the given date", foodList.convertToStringBySpecificDate( + LocalDate.parse("16-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + } + + @Test + void printFoodListBySpecificDateAndTimePeriodWithEmptyFoodList_emptyFoodList_expectErrorMessage() { + FoodList foodList = new FoodList(); + foodList.addItem(new Food("chicken rice", 607, + LocalDateTime.parse("17-10-2021 2359", DateTimeFormatter.ofPattern("dd-MM-yyyy HHmm")))); + assertEquals("There is no food item found by the given date and time period", + foodList.convertToStringBySpecificDateAndTime( + LocalDate.parse("16-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")), TimePeriod.MORNING)); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/logic/StatisticsTest.java b/src/test/java/seedu/duke/logic/StatisticsTest.java new file mode 100644 index 0000000000..587e6831bd --- /dev/null +++ b/src/test/java/seedu/duke/logic/StatisticsTest.java @@ -0,0 +1,85 @@ +package seedu.duke.logic; + + +import org.junit.jupiter.api.Test; +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.data.item.food.Food; +import seedu.duke.data.item.food.FoodList; +import seedu.duke.data.profile.Profile; +import seedu.duke.data.profile.attributes.ActivityFactor; +import seedu.duke.data.profile.attributes.Age; +import seedu.duke.data.profile.attributes.CalorieGoal; +import seedu.duke.data.profile.attributes.Gender; +import seedu.duke.data.profile.attributes.Height; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.ArrayList; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StatisticsTest { + private final String ls = System.lineSeparator(); + private FoodList foodItems; + private ExerciseList exerciseItems; + private Profile profile; + Statistics stats = new Statistics(new FoodList(), new ExerciseList(), new Profile()); + + @Test + void printCalorieResult_netCaloriesAndCalorieGoal_messages() { + int caloriesGoal = 5000; + int foodCalories = 3000; + int exerciseCalories = 2000; + int netCalories = foodCalories - exerciseCalories; + String message = stats.printCaloriesMessage(netCalories, caloriesGoal); + assertEquals("You are 4000 cal away from your goal!", message); + } + + @Test + void printCalories_foodCaloriesExerciseCaloriesAndCalorieGoal_messageArray() { + this.profile = new Profile(); + profile.setProfileAge(new Age(12)); + profile.setProfileHeight(new Height(170.0)); + profile.setProfileGender(new Gender('M')); + profile.setProfileActivityFactor(new ActivityFactor(3)); + int caloriesGoal = 5000; + int foodCalories = 3000; + int exerciseCalories = 2000; + String expectedResult = "You are 6448 cal away from your goal!"; + assertEquals(expectedResult, stats.getCaloriesReport(foodCalories, exerciseCalories, caloriesGoal)[4]); + } + + @Test + void getCurrentDayOverview_foodAndExerciseInput_messageArray() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + this.profile = new Profile(); + profile.setProfileAge(new Age(21)); + profile.setProfileHeight(new Height(170.0)); + profile.setProfileGender(new Gender('M')); + profile.getProfileCalorieGoal().setCalorieGoal(0); + this.foodItems = new FoodList(); + this.exerciseItems = new ExerciseList(); + profile.setProfileActivityFactor(new ActivityFactor(3)); + this.foodItems.addItem(new Food("food1", 21)); + this.exerciseItems.addItem(new Exercise("wwe", 12)); + Method method = stats.getClass().getDeclaredMethod("getCurrentDayOverview"); + method.setAccessible(true); + String result = (String) method.invoke(stats); + String expected = "This is your calorie overview for today:" + ls + + "Your calorie gained from food is: 0" + ls + + "Your calorie lost from exercise is: 0" + ls + + "Your net calorie intake is: -448" + ls + + "Your calorie goal is: 0" + ls + + "You are 448 cal away from your goal!"; + assertEquals(expected, result); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/logic/parser/ParserManagerTest.java b/src/test/java/seedu/duke/logic/parser/ParserManagerTest.java new file mode 100644 index 0000000000..031a5e044f --- /dev/null +++ b/src/test/java/seedu/duke/logic/parser/ParserManagerTest.java @@ -0,0 +1,263 @@ +package seedu.duke.logic.parser; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.data.profile.Profile; +import seedu.duke.data.profile.utilities.ProfileUtils; +import seedu.duke.logic.commands.AddExerciseBankCommand; +import seedu.duke.logic.commands.AddExerciseCommand; +import seedu.duke.logic.commands.AddFoodBankCommand; +import seedu.duke.logic.commands.AddFoodCommand; +import seedu.duke.logic.commands.AddRecurringExerciseCommand; +import seedu.duke.logic.commands.ByeCommand; +import seedu.duke.logic.commands.CalculateBmiCommand; +import seedu.duke.logic.commands.CalculateBmiWithProfileCommand; +import seedu.duke.logic.commands.Command; +import seedu.duke.logic.commands.CommandMessages; +import seedu.duke.logic.commands.HelpCommand; +import seedu.duke.logic.commands.InvalidCommand; +import seedu.duke.logic.commands.OverviewCommand; +import seedu.duke.logic.commands.ProfileUpdateCommand; +import seedu.duke.logic.commands.ViewExerciseBankCommand; +import seedu.duke.logic.commands.ViewExerciseListCommand; +import seedu.duke.logic.commands.ViewFoodBankCommand; +import seedu.duke.logic.commands.ViewFoodListCommand; +import seedu.duke.logic.commands.ViewFutureExerciseListCommand; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +class ParserManagerTest { + + private ParserManager parser; + + @BeforeEach + public void setUp() { + parser = new ParserManager(); + } + + @Test + void parseCommand_invalidCommandWord_commandDoesNotExistMessage() { + parseAndAssertIncorrectWithMessage(ParserMessages.MESSAGE_ERROR_COMMAND_DOES_NOT_EXIST, + "potato", "hi"); + } + + @Test + void parseCommand_containsTextFileDelimiter_illegalCharacterMessage() { + parseAndAssertIncorrectWithMessage(ParserMessages.MESSAGE_ERROR_ILLEGAL_CHARACTER, + "potato | as", "add |", "name h|o"); + } + + @Test + void parseAddCommand_correctInput_addCommand() { + parseAndAssertCommandType("add f/potato c/20", AddFoodCommand.class); + parseAndAssertCommandType("add f/potato c/20", AddFoodCommand.class); + parseAndAssertCommandType("add e/potato c/20", AddExerciseCommand.class); + parseAndAssertCommandType("add fbank/potato c/20", AddFoodBankCommand.class); + parseAndAssertCommandType("add ebank/potato c/20", AddExerciseBankCommand.class); + parseAndAssertCommandType("add ebank/exercise c/250", AddExerciseBankCommand.class); + parseAndAssertCommandType("add r/exercise c/250 :/11-12-2021 -/30-12-2021 @/1,2,3", + AddRecurringExerciseCommand.class); + parseAndAssertCommandType("add r/exercise c/250 :/11-12-2021 -/30-12-2021 @/1", + AddRecurringExerciseCommand.class); + } + + @Test + void parseAddCommand_tooManyDelimiters_tooManyDelimitersMessage() { + parseAndAssertIncorrectWithMessage(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS, + "add f/pota/to c/20", + "add fbank/tac/o c/20", + "add ebank/tac/o/o c/20", + "add r/exer/cise/ c/250 :/11-12-2021 -/30-12-2021 @/1"); + } + + @Test + void parseAddCommand_extraParameters_invalidCommand() { + parseAndAssertCommandType("add f/potato c/20 abc", InvalidCommand.class); + parseAndAssertCommandType("add f/potato c/20 a", InvalidCommand.class); + parseAndAssertCommandType("add e/potato c/20 12", InvalidCommand.class); + parseAndAssertCommandType("add fbank/potato c/20 sjsjd", InvalidCommand.class); + parseAndAssertCommandType("add ebank/potato c/20 qqaz", InvalidCommand.class); + parseAndAssertCommandType("add ebank/exercise c/250 c", InvalidCommand.class); + parseAndAssertCommandType("add r/exercise c/250 :/11-12-2021 -/30-12-2021 @/1,2,3 abc", + InvalidCommand.class); + parseAndAssertCommandType("add r/exercise c/250 :/11-12-2021 asd -/30-12-2021 @/1,2,3 abc", + InvalidCommand.class); + } + + @Test + void parseAddCommand_itemTypeNotSpecified_invalidCommand() { + parseAndAssertCommandType("add", InvalidCommand.class); + parseAndAssertCommandType("add a/", InvalidCommand.class); + parseAndAssertCommandType("add a", InvalidCommand.class); + parseAndAssertCommandType("add c/20 f/potato", InvalidCommand.class); + } + + @Test + void parseAddCommand_nameNotGiven_invalidCommand() { + parseAndAssertCommandType("add f/ c/120", InvalidCommand.class); + parseAndAssertCommandType("add e/ c/120", InvalidCommand.class); + } + + @Test + void parseAddCommand_caloriesNotANumber_caloriesNotNumberMessage() { + parseAndAssertIncorrectWithMessage(CommandMessages.MESSAGE_INVALID_CALORIES, + "add f/potato c/potato", "add e/hiit c/potato"); + } + + @Test + void parseAddRecurringExerciseCommand_daysNotInRange_invalidCommand() { + parseAndAssertCommandType("add r/exercise c/250 :/11-12-2021 -/30-12-2021 @/1,2,9", + InvalidCommand.class); + parseAndAssertCommandType("add r/exercise c/250 :/11-12-2021 -/30-12-2021 @/-1", + InvalidCommand.class); + } + + @Test + void parseByeCommand_correctInput_byeCommand() { + parseAndAssertCommandType("bye", ByeCommand.class); + parseAndAssertCommandType("ByE", ByeCommand.class); + } + + @Test + void parseCalculateBmiWithProfileCommand_correctInput_calculateBmiWithProfileCommand() { + parseAndAssertCommandType("bmi", CalculateBmiWithProfileCommand.class); + parseAndAssertCommandType("BMI", CalculateBmiWithProfileCommand.class); + } + + @Test + void parseCalculateBmiCommand_correctInput_calculateBmiCommand() { + parseAndAssertCommandType("bmi h/50 w/20", CalculateBmiCommand.class); + parseAndAssertCommandType("BMI w/20 h/50", CalculateBmiCommand.class); + } + + @Test + void parseCalculateBmiCommand_parametersNotGiven_invalidCommand() { + parseAndAssertCommandType("bmi w/20", InvalidCommand.class); + parseAndAssertCommandType("bmi h/20", InvalidCommand.class); + } + + @Test + void parseCalculateBmiCommand_parametersInvalid_errorMessage() { + parseAndAssertIncorrectWithMessage( + ProfileUtils.ERROR_HEIGHT, + "BMI w/20 h/potato"); + parseAndAssertIncorrectWithMessage( + ProfileUtils.ERROR_WEIGHT, + "BMI w/potato h/20"); + } + + @Test + void parseDeleteCommand_itemTypeNotSpecified_invalidCommand() { + parseAndAssertCommandType("delete", InvalidCommand.class); + parseAndAssertCommandType("delete a/", InvalidCommand.class); + } + + @Test + void parseDeleteFoodCommand_missingParameters_errorMessage() { + parseAndAssertIncorrectWithMessage(ParserMessages.MESSAGE_ERROR_NO_DATE, "delete f/1", + "delete f/1 t/2359"); + parseAndAssertIncorrectWithMessage(ParserMessages.MESSAGE_ERROR_NO_TIME, "delete f/1 d/25-12-2021"); + } + + @Test + void parseDeleteCommand_itemNumInvalid_invalidCommand() { + parseAndAssertCommandType("delete fbank/ ", InvalidCommand.class); + parseAndAssertCommandType("delete ebank/", InvalidCommand.class); + parseAndAssertCommandType("delete fbank/potato", InvalidCommand.class); + parseAndAssertCommandType("delete ebank/potato", InvalidCommand.class); + parseAndAssertCommandType("delete u/", InvalidCommand.class); + parseAndAssertCommandType("delete u/potato", InvalidCommand.class); + } + + @Test + void parseHelpCommand_correctInput_helpCommand() { + parseAndAssertCommandType("help", HelpCommand.class); + } + + @Test + void parseOverviewCommand_correctInput_overviewCommand() { + parseAndAssertCommandType("overview", OverviewCommand.class); + } + + @Test + void parseProfileUpdateCommand_correctInput_ProfileCreateCommand() { + parseAndAssertCommandType("profile n/hello w/50 h/80", ProfileUpdateCommand.class); + parseAndAssertCommandType("profile g/100 a/23 s/F x/2", ProfileUpdateCommand.class); + parseAndAssertCommandType("profile h/50 n/hello potato g/20 w/20 a/23 s/F x/2", ProfileUpdateCommand.class); + } + + @Test + void parseProfileCreateCommand_parametersNotGiven_invalidCommand() { + parseAndAssertCommandType("profile n/ ", InvalidCommand.class); + parseAndAssertCommandType("profile h/", InvalidCommand.class); + parseAndAssertCommandType("profile w/", InvalidCommand.class); + parseAndAssertCommandType("profile n/ h/", InvalidCommand.class); + } + + @Test + void parseProfileCreateCommand_parametersInvalid_tooManyDelimitersMessage() { + parseAndAssertIncorrectWithMessage(ParserMessages.MESSAGE_ERROR_TOO_MANY_DELIMITERS, + "profile n/hi n/hello", "profile n/h/i n/hello", "profile n/h/i z/hello"); + parseAndAssertIncorrectWithMessage(ProfileUtils.ERROR_WEIGHT, + "profile w/hello"); + parseAndAssertIncorrectWithMessage(ProfileUtils.ERROR_HEIGHT, + "profile h/hello"); + parseAndAssertIncorrectWithMessage(ProfileUtils.ERROR_ACTIVITY_FACTOR, + "profile x/hello"); + parseAndAssertIncorrectWithMessage(ProfileUtils.ERROR_AGE, + "profile a/hello"); + parseAndAssertIncorrectWithMessage(ProfileUtils.ERROR_CALORIE_GOAL, + "profile g/hello"); + parseAndAssertIncorrectWithMessage(ProfileUtils.ERROR_GENDER, + "profile s/fm"); + + } + + @Test + void parseViewCommand_correctInput_viewCommand() { + parseAndAssertCommandType("view e/", ViewExerciseListCommand.class); + parseAndAssertCommandType("view f/", ViewFoodListCommand.class); + parseAndAssertCommandType("view fbank/", ViewFoodBankCommand.class); + parseAndAssertCommandType("view ebank/", ViewExerciseBankCommand.class); + parseAndAssertCommandType("view u/", ViewFutureExerciseListCommand.class); + } + + @Test + void parseViewCommand_itemTypeNotSpecified_invalidCommand() { + parseAndAssertCommandType("view a/", InvalidCommand.class); + parseAndAssertCommandType("view a", InvalidCommand.class); + parseAndAssertCommandType("view ef/", InvalidCommand.class); + } + + + /* + * Utility methods ==================================================================================== + * Adapted from AddressBook-Level2 + * https://github.com/se-edu/addressbook-level2/blob/master/test/java/seedu/addressbook/parser/ParserTest.java + */ + + /** + * Asserts that parsing the given inputs will return IncorrectCommand with the given feedback message. + */ + private void parseAndAssertIncorrectWithMessage(String errorMessage, String... inputs) { + for (String input : inputs) { + final InvalidCommand result = parseAndAssertCommandType(input, InvalidCommand.class); + assertEquals(result.errorMessage, errorMessage); + } + } + + /** + * Parses input and asserts the class/type of the returned command object. + * + * @param input to be parsed + * @param expectedCommandClass expected class of returned command + * @return the parsed command object + */ + private T parseAndAssertCommandType(String input, Class expectedCommandClass) { + final Command result = parser.parseCommand(input); + assertTrue(result.getClass().isAssignableFrom(expectedCommandClass)); + return (T) result; + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/ActivityFactorCreatorTest.java b/src/test/java/seedu/duke/state/ActivityFactorCreatorTest.java new file mode 100644 index 0000000000..3f61e9824e --- /dev/null +++ b/src/test/java/seedu/duke/state/ActivityFactorCreatorTest.java @@ -0,0 +1,84 @@ +package seedu.duke.state; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class ActivityFactorCreatorTest { + ActivityFactorCreator activityFactorCreator = new ActivityFactorCreator(new Ui()); + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final String ls = System.lineSeparator(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @Test + void setActivityFactor_oneInvalidInput_throwsException() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method method = activityFactorCreator.getClass().getDeclaredMethod("setActivityFactor", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(activityFactorCreator, "dg")); + } + + @Test + void setActivityFactor_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = activityFactorCreator.getClass().getDeclaredMethod("setActivityFactor", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(activityFactorCreator, "")); + } + + @Test + void setActivityFactor_spacesInput_throwsException() throws NoSuchMethodException { + Method method = activityFactorCreator.getClass().getDeclaredMethod("setActivityFactor", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(activityFactorCreator, " ")); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(activityFactorCreator, " " + ls)); + } + + @Test + void checkActivityFactor_activityFactorInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = activityFactorCreator.getClass().getDeclaredMethod("setActivityFactor", String.class); + Method checkMethod = activityFactorCreator.getClass().getDeclaredMethod("checkActivityFactor"); + method.setAccessible(true); + method.invoke(activityFactorCreator, "0"); + checkMethod.setAccessible(true); + checkMethod.invoke(activityFactorCreator); + String expected = "_________________________________________________________" + + "_________________________________________________" + ls + + "Your activity factor cannot be of this value!" + ls + + "Maybe you can try a whole number from 1 to 5." + ls + + "__________________________________________________________________" + + "________________________________________"; + Assertions.assertEquals(expected, outputStreamCaptor.toString() + .trim()); + String expected1 = expected + ls + + "__________________________________________________________" + + "________________________________________________" + ls + + "Your activity factor is 4."; + method.invoke(activityFactorCreator, "4"); + checkMethod.invoke(activityFactorCreator); + Assertions.assertEquals(expected1, outputStreamCaptor.toString() + .trim()); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/AgeCreatorTest.java b/src/test/java/seedu/duke/state/AgeCreatorTest.java new file mode 100644 index 0000000000..bac9e9a59e --- /dev/null +++ b/src/test/java/seedu/duke/state/AgeCreatorTest.java @@ -0,0 +1,83 @@ +package seedu.duke.state; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Assertions; + +class AgeCreatorTest { + AgeCreator ageCreator = new AgeCreator(new Ui()); + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final String ls = System.lineSeparator(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @Test + void setAge_oneInvalidInput_throwsException() + throws NoSuchMethodException { + Method method = ageCreator.getClass().getDeclaredMethod("setAge",String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(ageCreator,"adg")); + } + + @Test + void setAge_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = ageCreator.getClass().getDeclaredMethod("setAge",String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(ageCreator,"")); + } + + @Test + void setAge_spacesInput_throwsException() throws NoSuchMethodException { + Method method = ageCreator.getClass().getDeclaredMethod("setAge",String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(ageCreator," ")); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(ageCreator," " + ls)); + } + + @Test + void checkAge_ageInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = ageCreator.getClass().getDeclaredMethod("setAge",String.class); + Method checkMethod = ageCreator.getClass().getDeclaredMethod("checkAge"); + method.setAccessible(true); + method.invoke(ageCreator,"1"); + checkMethod.setAccessible(true); + checkMethod.invoke(ageCreator); + String expected = "_________________________________________________________________" + + "_________________________________________" + ls + + "Your age cannot be this value!" + ls + + "Maybe you can try a whole number from 10 to 150." + ls + + "___________________________________________________________" + + "_______________________________________________"; + Assertions.assertEquals(expected, outputStreamCaptor.toString() + .trim()); + String expected1 = expected + ls + + "_________________________________________________" + + "_________________________________________________________" + ls + + "You are 20 years old."; + method.invoke(ageCreator,"20"); + checkMethod.invoke(ageCreator); + Assertions.assertEquals(expected1, outputStreamCaptor.toString() + .trim()); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/CalorieGoalCreatorTest.java b/src/test/java/seedu/duke/state/CalorieGoalCreatorTest.java new file mode 100644 index 0000000000..3a2aac940c --- /dev/null +++ b/src/test/java/seedu/duke/state/CalorieGoalCreatorTest.java @@ -0,0 +1,81 @@ +package seedu.duke.state; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class CalorieGoalCreatorTest { + CalorieGoalCreator calorieGoalCreator = new CalorieGoalCreator(new Ui()); + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final String ls = System.lineSeparator(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @Test + void setCalorieGoal_oneInvalidInput_throwsException() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method method = calorieGoalCreator.getClass().getDeclaredMethod("setCalorieGoal", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(calorieGoalCreator, "dg")); + } + + @Test + void setCalorieGoal_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = calorieGoalCreator.getClass().getDeclaredMethod("setCalorieGoal", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(calorieGoalCreator, "")); + } + + @Test + void setCalorieGoal_spacesInput_throwsException() throws NoSuchMethodException { + Method method = calorieGoalCreator.getClass().getDeclaredMethod("setCalorieGoal", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(calorieGoalCreator, " ")); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(calorieGoalCreator, " " + ls)); + } + + @Test + void checkCalorieGoal_calorieGoalInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = calorieGoalCreator.getClass().getDeclaredMethod("setCalorieGoal", String.class); + Method checkMethod = calorieGoalCreator.getClass().getDeclaredMethod("checkCalorieGoal"); + method.setAccessible(true); + method.invoke(calorieGoalCreator, "0"); + checkMethod.setAccessible(true); + checkMethod.invoke(calorieGoalCreator); + String expected = "____________________________________________________________" + + "______________________________________________" + ls + + "You calorie goal is 0 cal."; + Assertions.assertEquals(expected, outputStreamCaptor.toString() + .trim()); + String expected1 = expected + ls + + "________________________________________________________" + + "__________________________________________________" + ls + + "You calorie goal is 4 cal."; + method.invoke(calorieGoalCreator, "4"); + checkMethod.invoke(calorieGoalCreator); + Assertions.assertEquals(expected1, outputStreamCaptor.toString() + .trim()); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/GenderCreatorTest.java b/src/test/java/seedu/duke/state/GenderCreatorTest.java new file mode 100644 index 0000000000..fcdd5b6619 --- /dev/null +++ b/src/test/java/seedu/duke/state/GenderCreatorTest.java @@ -0,0 +1,62 @@ +package seedu.duke.state; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class GenderCreatorTest { + GenderCreator genderCreator = new GenderCreator(new Ui()); + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final String ls = System.lineSeparator(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @Test + void setGender_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = genderCreator.getClass().getDeclaredMethod("setGender",String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(genderCreator,"")); + } + + @Test + void checkGender_genderInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = genderCreator.getClass().getDeclaredMethod("setGender",String.class); + Method checkMethod = genderCreator.getClass().getDeclaredMethod("checkGender"); + method.setAccessible(true); + method.invoke(genderCreator,"0"); + checkMethod.setAccessible(true); + checkMethod.invoke(genderCreator); + String expected = "____________________________________________________________________" + + "______________________________________" + ls + + "Please type in M or F only!"; + Assertions.assertEquals(expected, outputStreamCaptor.toString() + .trim()); + String expected1 = expected + ls + + "_____________________________________________________________________" + + "_____________________________________" + ls + + "You are a male."; + method.invoke(genderCreator,"m"); + checkMethod.invoke(genderCreator); + Assertions.assertEquals(expected1, outputStreamCaptor.toString() + .trim()); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/HeightCreatorTest.java b/src/test/java/seedu/duke/state/HeightCreatorTest.java new file mode 100644 index 0000000000..55de4e559b --- /dev/null +++ b/src/test/java/seedu/duke/state/HeightCreatorTest.java @@ -0,0 +1,83 @@ +package seedu.duke.state; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class HeightCreatorTest { + HeightCreator heightCreator = new HeightCreator(new Ui()); + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final String ls = System.lineSeparator(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @Test + void setHeight_oneInvalidInput_throwsException() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method method = heightCreator.getClass().getDeclaredMethod("setHeight", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(heightCreator, "dg")); + } + + @Test + void setHeight_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = heightCreator.getClass().getDeclaredMethod("setHeight", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(heightCreator, "")); + } + + @Test + void setHeight_spacesInput_throwsException() throws NoSuchMethodException { + Method method = heightCreator.getClass().getDeclaredMethod("setHeight", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(heightCreator, " ")); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(heightCreator, " " + ls)); + } + + @Test + void checkHeight_heightInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = heightCreator.getClass().getDeclaredMethod("setHeight", String.class); + Method checkMethod = heightCreator.getClass().getDeclaredMethod("checkHeight"); + method.setAccessible(true); + method.invoke(heightCreator, "0"); + checkMethod.setAccessible(true); + checkMethod.invoke(heightCreator); + String expected = "________________________________________________________" + + "__________________________________________________" + ls + + "Your height cannot be of this value!" + ls + + "Maybe you can try a number from 1 to 300." + ls + + "________________________________________________" + + "__________________________________________________________"; + Assertions.assertEquals(expected, outputStreamCaptor.toString() + .trim()); + String expected1 = expected + ls + + "___________________________________________________________" + + "_______________________________________________" + ls + + "Your height is 4.0cm."; + method.invoke(heightCreator, "4"); + checkMethod.invoke(heightCreator); + Assertions.assertEquals(expected1, outputStreamCaptor.toString() + .trim()); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/NameCreatorTest.java b/src/test/java/seedu/duke/state/NameCreatorTest.java new file mode 100644 index 0000000000..e158894398 --- /dev/null +++ b/src/test/java/seedu/duke/state/NameCreatorTest.java @@ -0,0 +1,33 @@ +package seedu.duke.state; + + +import org.junit.jupiter.api.Assertions; + +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + + + +class NameCreatorTest { + NameCreator nameCreator = new NameCreator(new Ui()); + + @Test + void setName_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = nameCreator.getClass().getDeclaredMethod("setName",String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(nameCreator,"")); + } + + @Test + void checkName_nameInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = nameCreator.getClass().getDeclaredMethod("setName",String.class); + method.setAccessible(true); + boolean result = (boolean) method.invoke(nameCreator,"bywere"); + Assertions.assertEquals(result, false); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/state/WeightCreatorTest.java b/src/test/java/seedu/duke/state/WeightCreatorTest.java new file mode 100644 index 0000000000..c3693ffe69 --- /dev/null +++ b/src/test/java/seedu/duke/state/WeightCreatorTest.java @@ -0,0 +1,85 @@ +package seedu.duke.state; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.ui.Ui; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + + +class WeightCreatorTest { + WeightCreator weightCreator = new WeightCreator(new Ui()); + + private final PrintStream standardOut = System.out; + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + private final String ls = System.lineSeparator(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + } + + @Test + void setWeight_oneInvalidInput_throwsException() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method method = weightCreator.getClass().getDeclaredMethod("setWeight", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(weightCreator, "dg")); + } + + @Test + void setWeight_emptyInput_throwsException() + throws NoSuchMethodException { + Method method = weightCreator.getClass().getDeclaredMethod("setWeight", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, () -> method.invoke(weightCreator, "")); + } + + @Test + void setWeight_spacesInput_throwsException() throws NoSuchMethodException { + Method method = weightCreator.getClass().getDeclaredMethod("setWeight", String.class); + method.setAccessible(true); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(weightCreator, " ")); + Assertions.assertThrows(InvocationTargetException.class, + () -> method.invoke(weightCreator, " " + ls)); + } + + @Test + void checkWeight_weightInstance_validMessage() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Method method = weightCreator.getClass().getDeclaredMethod("setWeight", String.class); + Method checkMethod = weightCreator.getClass().getDeclaredMethod("checkWeight"); + method.setAccessible(true); + method.invoke(weightCreator, "0"); + checkMethod.setAccessible(true); + checkMethod.invoke(weightCreator); + String expected = "___________________________________________________" + + "_______________________________________________________" + ls + + "Your weight cannot be of this value!" + ls + + "Maybe you can try a number from 1 to 300." + ls + + "__________________________________________________________________" + + "________________________________________"; + Assertions.assertEquals(expected, outputStreamCaptor.toString() + .trim()); + String expected1 = expected + ls + + "_____________________________________________________________" + + "_____________________________________________" + ls + + "Your weight is 4.0kg."; + method.invoke(weightCreator, "4"); + checkMethod.invoke(weightCreator); + Assertions.assertEquals(expected1, outputStreamCaptor.toString() + .trim()); + } + + @AfterEach + public void tearDown() { + System.setOut(standardOut); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/storage/data/ItemEncoderTest.java b/src/test/java/seedu/duke/storage/data/ItemEncoderTest.java new file mode 100644 index 0000000000..0ca8f6f599 --- /dev/null +++ b/src/test/java/seedu/duke/storage/data/ItemEncoderTest.java @@ -0,0 +1,72 @@ +package seedu.duke.storage.data; + +import org.junit.jupiter.api.Test; +import seedu.duke.data.item.exercise.Exercise; +import seedu.duke.data.item.exercise.ExerciseList; +import seedu.duke.data.item.food.Food; +import seedu.duke.data.item.food.FoodList; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ItemEncoderTest { + + @Test + void encodeItems_validInputsInUnsortedOrder_expectSortedOrderWhenEncoded() { + FoodList foodList = new FoodList(); + + //The food items are not added in order + foodList.addItem(new Food("peanut butter waffle", 403, + LocalDateTime.parse("2021-10-16 1020", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("peanut butter waffle1", 403, + LocalDateTime.parse("2021-11-16 1030", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("peanut butter waffle2", 403, + LocalDateTime.parse("2021-12-16 1040", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + foodList.addItem(new Food("peanut butter waffle3", 403, + LocalDateTime.parse("2021-09-16 1050", DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm")))); + ArrayList testingList = ItemEncoder.encode(foodList); + + //But the expected list should be in order when encoded + ArrayList expectedList = new ArrayList<>() { + { + add("F|peanut butter waffle3|403|16-09-2021 1050"); + add("F|peanut butter waffle|403|16-10-2021 1020"); + add("F|peanut butter waffle1|403|16-11-2021 1030"); + add("F|peanut butter waffle2|403|16-12-2021 1040"); + } + }; + assertEquals(expectedList, testingList); + } + + @Test + void encodeExerciseItems_validInputsInUnsortedOrder_expectSortedOrderWhenEncoded() { + ExerciseList exerciseList = new ExerciseList(); + + //The exercise items are not added in order + exerciseList.addItem(new Exercise("Running 21k", 1200, + LocalDate.parse("19-12-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + exerciseList.addItem(new Exercise("Running 10k", 780, + LocalDate.parse("19-11-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + exerciseList.addItem(new Exercise("Running 2.4k", 250, + LocalDate.parse("19-09-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + exerciseList.addItem(new Exercise("Running 5k", 530, + LocalDate.parse("19-10-2021", DateTimeFormatter.ofPattern("dd-MM-yyyy")))); + ArrayList testingList = ItemEncoder.encode(exerciseList); + + //But the expected list should be in order when encoded + ArrayList expectedList = new ArrayList<>() { + { + add("E|Running 2.4k|250|19-09-2021"); + add("E|Running 5k|530|19-10-2021"); + add("E|Running 10k|780|19-11-2021"); + add("E|Running 21k|1200|19-12-2021"); + } + }; + assertEquals(expectedList, testingList); + } + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/ui/UiTest.java b/src/test/java/seedu/duke/ui/UiTest.java new file mode 100644 index 0000000000..fce507bdd3 --- /dev/null +++ b/src/test/java/seedu/duke/ui/UiTest.java @@ -0,0 +1,22 @@ +package seedu.duke.ui; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import seedu.duke.logic.parser.exceptions.MissingParamException; + + +class UiTest { + @Test + void checkEmptyUserInput_emptyStringInput_throwsException() { + Ui ui = new Ui(); + Assertions.assertThrows(MissingParamException.class, () -> ui.checkEmptyUserInput("")); + } + + @Test + void checkEmptyUserInput_oneStringInput_doesNotThrowException() { + Ui ui = new Ui(); + Assertions.assertDoesNotThrow(() -> ui.checkEmptyUserInput("1")); + Assertions.assertDoesNotThrow(() -> ui.checkEmptyUserInput("this")); + Assertions.assertDoesNotThrow(() -> ui.checkEmptyUserInput(" ")); + } +} \ No newline at end of file diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..9414648e9d 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,13 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| + ______ _ _ _ _ + | ____(_) | | | | | + | |__ _| |_| |__ ___ | |_ + | __| | | __| '_ \ / _ \| __| + | | | | |_| |_) | (_) | |_ + |_| |_|\__|_.__/ \___/ \__| +Welcome to Fitbot! Fitbot is here to help you to keep track of your calories. -What is your name? -Hello James Gosling +*========================================================================================================* + Fitbot realised that your profile has not been created. Let's start creating a profile below! +__________________________________________________________________________________________________________ +What's your name? +__________________________________________________________________________________________________________ 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