diff --git a/.gitignore b/.gitignore
index 2873e189e1..deda43cc32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ bin/
/text-ui-test/ACTUAL.TXT
text-ui-test/EXPECTED-UNIX.TXT
+EventList.txt
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..c5f3f6b9c7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index d5e548e85f..eb15d4f52b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,11 +29,11 @@ test {
}
application {
- mainClass = "seedu.duke.Duke"
+ mainClass = "seedu.moneymind.Moneymind"
}
shadowJar {
- archiveBaseName = "duke"
+ archiveBaseName = "moneymind"
archiveClassifier = null
}
@@ -44,3 +44,7 @@ checkstyle {
run{
standardInput = System.in
}
+
+run{
+ enableAssertions = true
+}
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index 0f072953ea..db5d8fa2ca 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -1,9 +1,8 @@
# About us
-Display | Name | Github Profile | Portfolio
---------|:----:|:--------------:|:---------:
- | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
+Display | Name | Github Profile | Portfolio
+--------|:--------------------------:|:-------------------------------------------:|:---------:
+ | Zhao Lixiuqi | [Github](https://github.com/alexgoexercise) | [Portfolio](team/Zhao_Lixiuqi.md)
+ | Toh Hongfeng | [Github](https://github.com/Toh-HongFeng) | [Portfolio](team/toh-hongfeng.md)
+ | Li Mingyuan | [Github](https://github.com/mingyuannus) | [Portfolio](team/johndoe.md)
+ | Nguyen Duc Thang | [Github](https://github.com/Mnsd05) | [Portfolio](team/mnsd05.md)
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 64e1f0ed2b..45cd328431 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -1,38 +1,382 @@
-# Developer Guide
+---
+layout: page
+title: Developer Guide
+---
+
+
+
MoneyMind Developer Guide
+ "Mind your Money"
+
+
+Moneymind is a Command Line Interface (CLI) application that helps you manage your personal finances. With Moneymind, you can keep track of your budgets, expenses, and categorize them for better organization.
+
+* Table of Contents
+{:toc}
## Acknowledgements
{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well}
-## Design & implementation
+## Design
{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.}
+### Architecture
+
+
+
+The ***Architecture Diagram*** given above explains the high-level design of the App.
+
+Given below is a quick overview of how each component interact with each other.
+
+The main components in the architecture are:
+
+* `MoneyMind`: The main program of the application,
+it initializes the other components in the correct sequence and is responsible for shut down the application.
+* `UI`: The user interface of the application.
+* `Storage`: The storage of data of the application.
+* `Data`: The data classes used in the application, including Event, Category and CategoryList.
+* `Command`: The command of the application, including AddCommand, DeleteCommand, ListCommand, etc.
+
+
+
+The *sequence diagram* above shows how the components interact with each other for the scenario
+where the user issues the command `event buy lunch e/10`.
+
+The following sections will explain the architecture in more detail.
+
+### Storage component
+
+**API**: `Storage.java`
+
+
+
+The `Storage` component,
+* can save category and event data in txt format, and read it back. (txt format is chosen because it is human readable)
+* depended on by `CategoryList.java` and `CategoryCommand.java` to load data to ArrayList and HashMap, and takes in an ArrayList of Category objects as parameter to save data.
+
+When the user first starts the application:
+
+When the program is running:
+
+
+### Data component
+
+**API**: `Category.java`, `CategoryList.java`, `Event.java`
+
+The data structure of MoneyMind follows a very simple design.
+
+`CategoryList`: It uses an ArrayList to store all the categories.
+Each category is an object of `Category` class.
+
+`Category`: It uses an ArrayList to store all the events.
+Each event is an object of `Event` class.
-## Product scope
+`Event`: It stores the information of each event.
+
+The details of the definition of `Category` and `Event` can be found
+in the [Glossary](#appendix-d--glossary) section.
+
+### Commands component
+
+
+
+**API**: `Command.java`
+
+The `Command` interface and classes,
+* every command type is represented by a class that implements the Command interface
+* can perform actions by executing different commands, such as adding category, adding event, deleting category, deleting event, viewing category, viewing event
+* also depends on `CategoryList.java`, `CategoryCommand.java`, `Event.java`, to execute the commands.
+
+**API**: `Parser.java`
+
+The `Parser` class,
+* responsible for parsing user input and creating the appropriate command classes
+* also depends on `CategoryList.java`, `CategoryCommand.java`, `Event.java`, to execute the commands.
+
+## Implementation
+
+### Parser component
+
+The Parser class contains several private methods, each of which is responsible for creating a specific type of Command
+object based on the user input. Each method takes in an array of strings as an argument, which contains
+the user input split into keyword and the content. The method then checks the first word in the array to determine which
+type of Command object to create. If the first word matches a specific keyword (e.g. "bye"), the corresponding Command
+object is created and returned. If the keyword is not recognized, an InvalidCommandException is thrown. Some methods
+also perform additional checks on the input string to ensure that it is formatted correctly. If the input is in correct
+format, the corresponding Command object is created and returned. Otherwise, an InvalidCommandException is thrown.
+
+### Commands component
+
+The Command class is an interface that is implemented by all the different types of Command objects. It contains methods
+that are common to all Command objects, such as execute() and isExit(). The execute() method contains the logic to
+perform certain tasks and is called when the Command object is to be executed. The isExit() method is called to
+check if the Command object is an ExitCommand object, which signals the end of the program.
+
+### Exceptions component
+
+The Exceptions class contains several types of custom Exception objects that are thrown when the user input is not
+in the correct format.
+
+### Category component
+
+The components use arraylist to store events for each category and
+store all categories in a category list.
+
+### Event component
+
+The components implement event class to store the information of each event.
+
+## Appendix A: Product Scope
### Target user profile
-{Describe the target user profile}
+-------------------------
+
+
+
+
+* **Name**: NUS Student
+* **Age**: 18-25 years old
+* **Occupation**: Student
+* **Education**: Currently enrolled in NUS
+* **Income**: Limited income sources (part-time jobs, allowances from parents, etc.)
+* **Technology usage**: Tech-savvy and comfortable using a CLI-based application
+* **Interests**: Campus life, budgeting, saving, optimizing spending
+* **Financial goals**: Prioritize spending, cut down on unnecessary expenses, improve financial health
+* **Challenges**: Limited income, lack of financial knowledge, lack of financial discipline
+* **Needs**: A simple, easy-to-use application to help manage expenses and track spending habits
+
+Overall, NUS Student is a financially-conscious individual who wants to prioritize spending and save for the future while
+enjoying a good campus life. They are comfortable with technology and prefer an efficient and easy-to-use tool like MoneyMind
+to manage their finances. They are interested in tracking their expenses by category and exploring new ways to
+optimize their spending to achieve their financial goals.
### Value proposition
-{Describe the value proposition: what problem does it solve?}
+If you are a NUS Student who wants to prioritize spending and save for the future while enjoying a good campus life,
+MoneyMind is the perfect application for you. It is a simple, easy-to-use application that helps you manage your expenses
+and track your spending habits. You can easily categorize your expenses and view your spending history by category.
+
+As the application is CLI-based, you can easily navigate through the application and perform the necessary tasks if
+you are a fast-typer. You can also use the application on any mainstream OS as long as you have Java 11 or above installed.
+
+
+## Appendix B: User Stories
+
+| Version | As a ... | I want to ... | So that I can ... |
+|-----|------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
+| v1.0 | user | **add** an one time expense | keep track of how much I spend |
+| | user | **delete** a one time expense | ammend the record in case I add the wrong expense |
+| | user | **categorize** my expenses into different categories such as food, transportation, entertainment, etc. | better understand where my money is going |
+| v2.0 | user | **edit** one time expenses | change when I type wrongly |
+| | user | **search** for specific expenses by keyword or date range | easily find and review my past spending |
+
+## Appendix C: Non Functional Requirements
+
+1. MoneyMind should work on any mainstream OS as long as it has Java 11 or above installed.
+2. The user is expected to be a fast-typer. MoneyMind is not optimised for users that are not
+used to CLI applications.
+3. This application is not built for mobile devices. It is recommended to use a laptop.
+4. The user is expected to have a good habit of constantly and systematically recording their
+expenses. MoneyMind is not optimised for users that only record their expense on ad-hoc basis.
+5. Other than the above, please enjoy using MoneyMind!
+
+## Appendix D: Glossary
+
+* *budget* - A budget is a financial plan that outlines an individual's or
+organization's expected income and expenses over a specific period of time.
+In context given, the budget here is scaled down to NUS students' expenses over
+different categories.
+Budgets can typically include categories for different types of expenses,
+such as housing, transportation, food, entertainment, and savings.
+* *expense* - An expense is the cost incurred by an organization or individual.
+In context given, the expense here is scaled down to NUS students' spending over
+different events that they are engaged in.
+* *event* - An event is a specific occurrence of expenses that is planned or occurs.
+It can be a one time expense like buying a pair of sneakers or recurring expenses like
+electricity bills.
+* *category* - A category is a group of events that are related to each other. For example,
+food, transportation, entertainment, etc.
+* *one time expense* - An expense that occurs only once.
+* *recurring expense* - An expense that occurs repeatedly, in the context of MoneyMind, the
+frequency is set to monthly.
+
+## Appendix E: Instructions for Manual Testing
+
+### Launch
+
+1. Download the latest MoneyMind.jar and save it to an empty folder.
+2. Open a command prompt in the folder and run the command java -jar MoneyMind.jar. The output should be similar to the below.
+
+```
+Loading file...
+Welcome to Moneymind
+ __ __ __ __ _ _
+ | \/ | | \/ (_) | |
+ | \ / | ___ _ __ ___ _ _| \ / |_ _ __ __| |
+ | |\/| |/ _ \| '_ \ / _ \ | | | |\/| | | '_ \ / _` |
+ | | | | (_) | | | | __/ |_| | | | | | | | | (_| |
+ |_| |_|\___/|_| |_|\___|\__, |_| |_|_|_| |_|\__,_|
+ __/ |
+ |___/
+How may I help you?
+Type 'summary' to see the summary of all the commands you can use.
+Type 'help' to see the details of all the commands.
+```
+
+### View summary of commands
+
+Type summary and press enter. The output should be similar to the below.
+
+```
+Here are the commands you can use:
+1. help
+2. summary
+3. category
+4. event
+5. view
+6. edit
+7. delete
+8. search
+9. bye
+```
+
+### View details of commands
+
+Type help and press enter. The output should be similar to the below.
+
+```
+Here are the commands you can use:
+1. help - show detailed instructions on how to use the app
+Format: help
+Example: help
+
+2. summary - show a summary of the commands that you can use
+Format: summary
+Example: summary
+
+3. category - add a category to your list
+Format: category [(optional) b/]
+Example: category food b/2000
+
+4. event - add an event to a category
+Format: event e/ [(optional) t/]
+Example: event lunch e/10 t/01/01/2020 12:00
+(time is optional and the format is dd/mm/yyyy hh:mm)
+
+5. view - view all the events in a category or all the categories
+You can view all the events in a category by specifying the category name
+Format: view
+Example: view food
+(category name is optional and if you do not enter a category name, all the categories will be shown)
+
+6. edit - edit the expense for an event or budget for a category
+Format: edit c/ [(optional) e/]
+Example: edit c/food e/1
+
+7. delete - delete an event or a category
+Format: delete c/ [(optional) e/]
+Example: delete c/food e/1
+Example: delete c/food
+
+8. search - search for matching events and categories
+Format: search
+Example: search bill
+
+9. bye - exit the app
+Format: bye
+Example: bye
+```
+
+The use of "/" is not allowed anywhere in the user input except for the time parameter in the event command or
+when using in specifier in the command.
+### Add a category
+
+| Cases | Example | Expected |
+|--------------------------------------|-------------------------------|-----------------------------------------------------|
+| correct format without budget | category food | category food with budget 0 is added |
+| correct format with budget | category food b/1000 | category food with budget 1000 is added |
+| incorrect format or empty parameters | category food / b/ | show correct format message |
+| empty description | category | show empty description message |
+| not a natural integer for budget | category food b/-1 | remind user to type non-negative integer for budget |
+| big number for budget | category food b/9999999999999 | remind user to type value under limit |
+| existed category | category food | remind user that the category already existed |
+
+
+### Add an event
+
+| Cases | Example | Expected |
+|--------------------------------------|-------------------------------------|------------------------------------------------------|
+| correct format without time | event sugar e/12 | one time expense sugar is added |
+| correct format with time | event sugar e/12 t/10/03/2020 12:00 | monthly recurring expense sugar is added |
+| incorrect format or empty parameters | event e/12 t/10/03/2020 12:00 | show correct format message |
+| empty description | event | show empty description message |
+| not a natural integer for expense | event sugar e/as | remind user to type non-negative integer for expense |
+| big number for expense | event sugar e/9999999999999 | remind user to type value under limit |
+| incorrect format for time | event sugar e/12 t/August | remind user to use correct format for time |
+
+Upon successfully pass the command input, the user will be prompted to select a category to add the event to.
+
+| Cases | Example | Expected |
+|-----------------------|---------|-------------------------------------------------------------------------------------------------------|
+| existing category | food | one time expense sugar is added to category food |
+| non-existing category | book | notify the user the category does not exist and the user can type back to go back to the main program |
+
+### View all the events in a category or all the categories
+
+| Cases | Example | Expected |
+|---------------------------------------|-----------|--------------------------------------------|
+| the user only type view | view | every category and event are shown to user |
+| the user type view with category name | view book | show every events in category book to user |
+| non-existing category | view car | notify user the category does not exist |
+
+### Edit the expense for an event or budget for a category
+
+| Cases | Example | Expected |
+|----------------------------------------|-----------------------------|----------------------------------------------------------|
+| correct format without event index | edit c/food | prepare to edit budget of food |
+| correct format with event index | edit c/food e/1 | prepare to edit expense for first event in category food |
+| incorrect format or empty parameters | edit e/1 c/food | show correct format message |
+| empty description | edit | show empty description message |
+| not a positive integer for event index | edit c/food e/0 | remind user to type positive integer for event index |
+| big number for event index | edit c/food e/9999999999999 | remind user to type value under limit |
+| non-existing category | edit c/car | notify the user the category does not exist |
+| non-existing event index | edit c/food e/12 | notify the user the event does not exist |
+
+Upon successfully pass the command input, the user will be prompted to enter the new expense or budget.
+
+| Cases | Example | Expected |
+|----------------------------|--------------|---------------------------------------------------------------------------------------------------------|
+| non-negative integer | 12 | the value is changed to 12 |
+| not a non-negative integer | as | remind the user to enter a non-negative value and the user can type back to go back to the main program |
+| big positive integer | 999999999999 | remind the user about the limit and the user can type back to go back to the main program |
+
+### Delete an event or a category
-## User Stories
+| Cases | Example | Expected |
+|----------------------------------------|-------------------------------|------------------------------------------------------|
+| correct format without event index | delete c/food | delete food category |
+| correct format with event index | delete c/food e/1 | delete first event in food category |
+| incorrect format or empty parameters | delete e/1 c/food | show correct format message |
+| empty description | delete | show empty description message |
+| not a positive integer for event index | delete c/food e/0 | remind user to type positive integer for event index |
+| big number for event index | delete c/food e/9999999999999 | remind user to type value under limit |
+| non-existing category | delete c/car | notify the user the category does not exist |
+| non-existing event index | delete c/food e/12 | notify the user the event does not exist |
-|Version| As a ... | I want to ... | So that I can ...|
-|--------|----------|---------------|------------------|
-|v1.0|new user|see usage instructions|refer to them when I forget how to use the application|
-|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list|
-## Non-Functional Requirements
+### Search for matching events and categories
-{Give non-functional requirements}
+| Cases | Example | Expected |
+|--------------------------------------|------------|------------------------------------------------------------------------------------------------------------------------------|
+| empty description | search | remind user to include keyword after search |
+| with keyword | search foo | show all matching categories and events which contain the keyword and show top 3 most similar matching categories and events |
-## Glossary
+### Exit the app
-* *glossary item* - Definition
+Type bye and press enter to exit the app. The output should be similar to the following:
-## Instructions for manual testing
+```
+Bye. Hope to see you again soon!
+```
-{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..c8a9238d1b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,8 +1,8 @@
-# Duke
+# MoneyMind
-{Give product intro here}
+MoneyMind is a desktop application for managing your finances, optimized for use via a Command Line Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, MoneyMind can get your finance management tasks done faster than traditional GUI apps.
-Useful links:
+To get to know more about MoneyMind, here are the 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..13f8ed8e50 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -1,42 +1,197 @@
-# User Guide
+---
+layout: page
+title: user Guide
+---
-## Introduction
+
+
MoneyMind User Guide
+ "Mind your Money"
+
-{Give a product intro}
+Moneymind is a Command Line Interface (CLI) application that helps you manage your personal finances. With Moneymind, you can keep track of your budgets, expenses, and categorize them for better organization.
+
+* Table of Contents
+{:toc}
## Quick Start
-{Give steps to get started quickly}
+1. Download and install Moneymind on your computer and ensure that you have Java 11 or above installed.
+
+2. Ensure you have Java11 or above installed on your computer.
+
+3. Open the terminal and navigate to the directory where Moneymind is installed.
+
+4. Run the following command to start the Moneymind app: `java -jar moneymind.jar`.
-1. Ensure that you have Java 11 or above installed.
-1. Down the latest version of `Duke` from [here](http://link.to/duke).
+5. Type the command in the command box and press Enter to execute.
## Features
-{Give detailed description of each feature}
+"/" is a special character that is used to start command parameters. Other uses of "/" are not allowed except for t/ in event command
+
+### Add an Event: `event`
+Add an event which is to be recorded by Moneymind. Event must be placed in a category.
+
+Format: `event NAME e/ [(optional) t/]`
+
+* `NAME` is the name of the event.
+* `` is the expense number of the event.
+* `` is the date of the monthly recurring event, in the pattern of "dd/MM/yyyy hh:mm".
+Invalid patterns are replaced with the current System date.
+* The `t/` parameters is optional.
+* This gives you the flexibility to add a one-time expense or monthly recurring expenses.
+* Monthly recurring expenses will be require the user to update the expense number every month.
+* In the context of this application, the 'MM', 'yyyy', 'hh' and 'mm' in the data format of `` is less important
+as the frequency is set as *monthly*. Hence, whatever valid month input that the user types
+in, the event will always be recorded as a monthly recurring event based on its date.
+To avoid confusion, the user is advised to type in the month and year of the first occurrence of the event
+which they want to record down.
+
+Example of usage:
+
+`event bus travel fee e/2 t/01/01/2020 12:00`
+
+`event bubble tea e/6`
+
+### Add a Category: `category`
+
+Add a category of event to be recorded on Moneymind.
+
+Format: `category NAME [(optional) b/]`
+
+* `NAME` is the name of the category.
+* `` is the total amount of budget
+set to the category of events.
+* The `b/` parameter is optional. If there is no b/, the default budget is set to 0
+
+Example of usage:
+
+`category electricity bill`
+
+`category travel to Thailand b/2000`
+
+### Delete an Event or Category: `delete`
+
+Delete an event or category from Moneymind.
+
+Format: `delete c/ [(optional) e/]`
+
+* `` is the name of the category.
+* `` is the position of the event (1 based).
+* The `e/` parameter is optional so that the user can choose between deleting a single event or the whole category.
+
+Example of usage:
+
+`delete c/electricity bill`
+
+`delete c/food e/1`
+
+### Edit an Event: `edit`
+
+Edit the details of an event on Moneymind.
+
+Format: `edit c/ [(optional) e/] `
+
+* `` is the name of the category the user want to edit.
+* `` is the position of the event that the user want to edit (1 based).
+* The user will be prompted to enter the new details of the event or category
+
+Example of usage:
+
+`edit c/overseas travel e/1`
-### Adding a todo: `todo`
-Adds a new item to the list of todo items.
+Ok, the current expense for book hotel is: 200;
-Format: `todo n/TODO_NAME d/DEADLINE`
+Your new expense would be:
-* The `DEADLINE` can be in a natural language format.
-* The `TODO_NAME` cannot contain punctuation.
+`100`
-Example of usage:
+Ok, the new expense is now changed to: 100 sgd
-`todo n/Write the rest of the User Guide d/next week`
+### View Events in Categories: `view`
-`todo n/Refactor the User Guide to remove passive voice d/13/04/2020`
+This commands allows the user to view all events in one category or view all categories in moneymind.
+
+Format: `view [(optional) ]`
+
+* `` is the name of the category that the user wants to view.
+* The user can view all the categories by entering `view` without any category name
+
+Example of usage:
+
+`view food` : view all events in the food category
+
+`view` : view all categories in moneymind
+
+### Search for a Category or Event: `search`
+
+Searches through all categories and and events to find items that contains the provided keyword along with the top 3 categories and events that are similar to the keyword. The results will contain 4 sections:
+1. Matching categories: All categories that contain the provided keyword in their names
+2. Matching events: All events that contain the provided keyword in their names, along with the categories they belong to
+3. Similar categories: Top 3 categories with names that are most similar to the provided keyword
+4. Similar events: Top 3 categories with names that are most similar to the provided keyword, along with the categories they belong to
+
+Format: `search `
+
+Example of usage:
+
+`search bill`
+
+### Reminders
+
+When starting up the program, it will remind the user of the
+upcoming events in the next 5 days.
+
+`Appoaching expenses:`
+`Category_Name has an event: Event_Name in 2 days`
+`Category_Name_2 has an event: Event_Name_2 in 3 days`
+
+### Check the Instruction Menu - 1: `help`
+
+This command allows the user to view the **detailed** instruction menu.
+
+Format: `help`
+
+Example of usage:
+
+`help`
+
+### Check the Instruction Menu - 2: `summary`
+
+This command allows the user to view the **summary** of commands that they can use.
+
+Format: `summary`
+
+Example of usage:
+
+`summary`
+
+### Exit the Program: `bye`
+
+This command allows the user to exit the program.
+
+Format: `bye`
+
+Example of usage:
+
+`bye`
+
+---
## FAQ
-**Q**: How do I transfer my data to another computer?
+**Q**: Am I able to keep my data after I exit the program?
-**A**: {your answer here}
+**A**: Yes. Your data will be saved automatically after you exit the program, and loaded the next time you use the program.
## Command Summary
-{Give a 'cheat sheet' of commands here}
-
-* Add todo `todo n/TODO_NAME d/DEADLINE`
+* `event` - Add an event to Moneymind
+* `category` - Add a category to Moneymind
+* `delete` - Delete an event or category from Moneymind
+* `edit` - Edit an event in Moneymind
+* `view` - View all events in a category or view all categories in Moneymind
+* `help` - Show the detailed instructions of commands
+* `summary` - Show the summary of commands
+* `search` - Search categories and events given a specific keyword
+* `bye` - Exit the program
diff --git a/docs/diagrams/Architecture.puml b/docs/diagrams/Architecture.puml
new file mode 100644
index 0000000000..2705dc7a5e
--- /dev/null
+++ b/docs/diagrams/Architecture.puml
@@ -0,0 +1,24 @@
+@startuml
+!include Style.puml
+!include
+!include
+!theme cerulean
+
+package " "<> {
+ class MoneyMind
+ class UI
+ class Storage
+ class Command
+ class Data
+ class Parser
+}
+
+MoneyMind --> UI
+Storage ..> MoneyMind
+Storage --> Data
+Data --> Storage
+MoneyMind --> Parser
+Parser --> Command
+Command --> Data
+
+@enduml
diff --git a/docs/diagrams/ArchitectureSequence.puml b/docs/diagrams/ArchitectureSequence.puml
new file mode 100644
index 0000000000..7c877ab26d
--- /dev/null
+++ b/docs/diagrams/ArchitectureSequence.puml
@@ -0,0 +1,58 @@
+@startuml
+'https://plantuml.com/sequence-diagram
+!include style.puml
+title Architecture Sequence Diagram
+
+Actor User as user #crimson
+participant MoneyMind as mm #lightblue
+participant UI as ui #lightpink
+participant Storage as storage #lightgrey
+
+
+
+user -[#crimson]> mm : EventCommand buy lunch e/10
+activate mm #lightblue
+
+mm -> ps : parseNextCommand()
+activate ps #orange
+
+ps -> ps : createEventCommand()
+activate ps #orange
+create EventCommand
+ps -> EventCommand
+activate EventCommand
+EventCommand --> ps
+deactivate EventCommand
+ps --> ps
+deactivate ps #orange
+ps --> mm
+deactivate ps #orange
+
+mm -> EventCommand : isExit()
+activate EventCommand
+EventCommand --> mm
+deactivate EventCommand
+
+mm -> EventCommand : execute()
+activate EventCommand
+create Data
+EventCommand -> Data
+activate Data
+Data --> EventCommand
+deactivate Data
+EventCommand --> mm
+deactivate EventCommand
+
+mm -> storage : save()
+activate storage #lightgrey
+storage --> mm
+deactivate storage #lightgrey
+
+
+
+
+
+
+
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/CommandParser.puml b/docs/diagrams/CommandParser.puml
new file mode 100644
index 0000000000..b539d2f3d5
--- /dev/null
+++ b/docs/diagrams/CommandParser.puml
@@ -0,0 +1,114 @@
+@startuml
+top to bottom direction
+skinparam linetype polyline
+skinparam classAttributeIconSize 0
+
+class ByeCommand {
+ + isExit(): boolean
+ + execute(Ui): void
+}
+class CategoryCommand {
+ + categoryMap: HashMap
+ - name: String
+ - budget: int
+ + execute(Ui): void
+ + isExit(): boolean
+}
+interface Command << interface >> {
+ + execute(Ui): void
+ + isExit(): boolean
+}
+class DeleteCommand {
+ + NULL_CATEGORY_ASSERTION: String
+ + NO_CATEGORY_MESSAGE: String
+ - eventIndex: int
+ - categoryName: String
+ + CATEGORY_DELETION_MESSAGE: String
+ + NON_EXISTENT_EVENT: String
+ + EVENT_DELETION_MESSAGE: String
+ - isEvent: boolean
+ - deleteEvent(): void
+ - deleteCategory(): void
+ + execute(Ui): void
+ + isExit(): boolean
+}
+class EditCommand {
+ - isReady: boolean
+ - userInput: String
+ - categoryIndex: int
+ - eventIndex: int
+ - categoryName: String
+ + EXPENSE: String
+ - checkNegative(int): void
+ - isEditSuccessful(String): boolean
+ - prepareEditEvent(): void
+ - getUserInputUntilNonEmpty(): void
+ - checkExpenseUnderLimit(String): void
+ + execute(Ui): void
+ + isExit(): boolean
+}
+class EventCommand {
+ - eventName: String
+ - expense: int
+ - userInput: String
+ - time: String
+ - addEventToCategory(String, Event): void
+ - isAddEventSuccessful(String): boolean
+ + execute(Ui): void
+ - getUserInputUntilNonEmpty(): void
+ + isExit(): boolean
+}
+class HelpCommand {
+ + isExit(): boolean
+ + execute(Ui): void
+}
+class Parser {
+ - createByeCommand(String[]): Command
+ + parseNextCommand(String): Command
+ - createHelpCommand(String[]): Command
+ - checkNegative(int): void
+ - createEditCommand(String[]): Command
+ - checkBigNumber(String): void
+ - createCategoryCommand(String[]): Command
+ - createViewCommand(String[]): Command
+ - createSearchCommand(String[]): Command
+ - createDeleteCommand(String[]): Command
+ - createEventCommand(String[]): Command
+}
+class SearchCommand {
+ - query: String
+ + isExit(): boolean
+ + calculateLevenshteinDistance(String, String): int
+ + sortCategoryBySimilarity(ArrayList, HashMap): void
+ + assignItemsBySimilarity(Set, Set, HashMap, HashMap, String): void
+ + execute(Ui): void
+ + calculateSimilarityDistance(String, String): int
+ + sortEventBySimilarity(ArrayList, HashMap): void
+ + getCategoryOfEvent(Event): Category?
+}
+class ViewCommand {
+ - isCategorySpecified: boolean
+ - categoryName: String
+ + isExit(): boolean
+ - viewAll(): void
+ + execute(Ui): void
+ - viewOne(): void
+}
+
+ByeCommand -[#008200,dashed]-^ Command
+CategoryCommand -[#008200,dashed]-^ Command
+DeleteCommand -[#008200,dashed]-^ Command
+EditCommand -[#008200,dashed]-^ Command
+EventCommand -[#008200,dashed]-^ Command
+HelpCommand -[#008200,dashed]-^ Command
+Parser -[#595959,dashed]-> ByeCommand : "«create»"
+Parser -[#595959,dashed]-> CategoryCommand : "«create»"
+Parser -[#595959,dashed]-> DeleteCommand : "«create»"
+Parser -[#595959,dashed]-> EditCommand : "«create»"
+Parser -[#595959,dashed]-> EventCommand : "«create»"
+Parser -[#595959,dashed]-> HelpCommand : "«create»"
+Parser -[#595959,dashed]-> SearchCommand : "«create»"
+Parser -[#595959,dashed]-> ViewCommand : "«create»"
+SearchCommand -[#008200,dashed]-^ Command
+ViewCommand -[#008200,dashed]-^ Command
+@enduml
diff --git a/docs/diagrams/StorageClass.puml b/docs/diagrams/StorageClass.puml
new file mode 100644
index 0000000000..a7b05341e1
--- /dev/null
+++ b/docs/diagrams/StorageClass.puml
@@ -0,0 +1,62 @@
+@startuml Storage
+scale 2
+skinparam packageStyle rectangle
+package seedu.moneymind.storage {
+ class Storage
+ class CategoriesToString
+ class GenerateCategoryHashMap
+ class ReadFromFile
+ class StringToCategories
+}
+class moneymind
+moneymind "1" *--> "1" Storage : storage
+class Storage {
+ -File textFile
+ -ArrayList savedCategories
+ -HashMap savedCategoryHashMap
+ +Storage(String filePath)
+ +void save(ArrayList categories)
+ -void moneymindWrite(String textToWrite)
+ +void load()
+ +ArrayList getSavedCategories()
+ +HashMap getSavedCategoryHashMap()
+}
+Storage ..> Category
+Storage ..> CategoriesToString : <> categoriesToString()
+Storage ..> StringToCategories : <> stringToCategories()
+Storage ..> GenerateCategoryHashMap : <> generateCategoryHashMap()
+Storage .right.> ReadFromFile : <> readFromFile()
+class StringToCategories {
+ +{static} ArrayList stringToCategories(String savedCategories)
+ -{static} Event loadEvent(String savedLine)
+ -{static} Category loadCategory(String savedLine)
+}
+StringToCategories ..> Category
+StringToCategories ..> Event
+StringToCategories ..> Strings
+StringToCategories ..> UserDate
+StringToCategories -up[hidden]- GenerateCategoryHashMap
+class CategoriesToString {
+ +{static} String categoriesToString(ArrayList categories)
+ -{static} String eventsToString(ArrayList event)
+}
+CategoriesToString ..> Category
+CategoriesToString ..> Event
+CategoriesToString ..> Strings
+class GenerateCategoryHashMap {
+ +{static} HashMap generateCategoryHashMap(ArrayList categories)
+}
+GenerateCategoryHashMap ..> Category
+class ReadFromFile {
+ +{static} String readFromFile(File file)
+}
+ReadFromFile ..> Strings : <> NEW_LINE
+class Strings {
+ +{static} final String NEW_LINE
+ +{static} final String STORAGE_CATEGORY_NAME
+ +{static} final String STORAGE_NEXT_VARIABLE
+}
+class UserDate {
+ +{static} String updateDate(String oldDate)
+}
+@enduml
diff --git a/docs/diagrams/StorageRunSequence.puml b/docs/diagrams/StorageRunSequence.puml
new file mode 100644
index 0000000000..f1fdffd2c0
--- /dev/null
+++ b/docs/diagrams/StorageRunSequence.puml
@@ -0,0 +1,40 @@
+@startuml StorageRunSequenceDiagram
+title Storage Run Sequence Diagram
+
+-> Moneymind : run()
+activate Moneymind
+critical try
+Moneymind -> Storage : load()
+activate Storage
+Storage -> ReadFromFile : readFromFile(textFile : File)
+ReadFromFile --> Storage : String fileString
+Storage -> StringToCategory : stringToCategory(fileString : String)
+StringToCategory --> Storage : categories : ArrayList
+Storage -> GenerateCategoryHashMap : generateCategoryHashMap(categories : ArrayList)
+GenerateCategoryHashMap --> Storage : categoryHashMap : HashMap
+Storage --> Moneymind
+deactivate Storage
+
+Moneymind -> Storage : getSavedCategories()
+activate Storage
+Storage --> Moneymind : savedCategories : ArrayList
+deactivate Storage
+
+Moneymind -> Storage : getSavedCategoryHashMap()
+activate Storage
+Storage --> Moneymind : savedCategoryHashMap : HashMap
+deactivate Storage
+end
+Moneymind -> Storage : save(ArrayList categories)
+activate Storage
+Storage -> CategoriesToString : categoriesToString(categories : ArrayList)
+CategoriesToString --> Storage : String categoriesString
+Storage -> Storage : moneymindWrite(textToWrite : String)
+activate Storage
+Storage --> Storage
+deactivate Storage
+Storage --> Moneymind
+deactivate Storage
+<-- Moneymind
+deactivate Moneymind
+destroy Moneymind
\ No newline at end of file
diff --git a/docs/diagrams/StorageSequence.puml b/docs/diagrams/StorageSequence.puml
new file mode 100644
index 0000000000..53c3bcf9a9
--- /dev/null
+++ b/docs/diagrams/StorageSequence.puml
@@ -0,0 +1,41 @@
+@startuml StorageSequenceDiagram
+title Storage Constructor Sequence Diagram
+' create Moneymind
+-> Moneymind : MoneyMind()
+activate Moneymind
+create Storage
+Moneymind -> Storage : Storage("EventList.txt")
+activate Storage
+Storage -> File : File(filePath : String)
+activate File
+File --> Storage : textFile : File
+deactivate File
+' try block
+critical try
+Storage -> File : createNewFile()
+activate File
+File --> Storage : isFileCreated : Boolean
+deactivate File
+' if block
+alt !isFileCreated
+Storage -> System.out : println("Loading file...")
+activate System.out
+deactivate System.out
+' else block
+else isFileCreated
+Storage -> System.out : println("Creating file...")
+activate System.out
+deactivate System.out
+end
+' catch block
+opt catch (Exception e)
+Storage -> System.out : println("Error creating file...")
+activate System.out
+deactivate System.out
+end
+end
+Storage --> Moneymind : storage : Storage
+deactivate Storage
+<-- Moneymind : moneymind : MoneyMind
+deactivate Moneymind
+@enduml
diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml
new file mode 100644
index 0000000000..3e90a7da7d
--- /dev/null
+++ b/docs/diagrams/style.puml
@@ -0,0 +1,8 @@
+!define LOGIC_COLOR #3333C4
+!define LOGIC_COLOR_T1 #7777DB
+!define LOGIC_COLOR_T2 #5252CE
+!define LOGIC_COLOR_T3 #1616B0
+!define LOGIC_COLOR_T4 #101086
+
+!define BLOCK_COLOUR_T1 #blue
+!define BLOCK_COLOUR_T2 #blue
diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png
new file mode 100644
index 0000000000..bcdddd715a
Binary files /dev/null and b/docs/images/ArchitectureDiagram.png differ
diff --git a/docs/images/ArchitectureSequence.png b/docs/images/ArchitectureSequence.png
new file mode 100644
index 0000000000..be8323c80a
Binary files /dev/null and b/docs/images/ArchitectureSequence.png differ
diff --git a/docs/images/CommandParser.png b/docs/images/CommandParser.png
new file mode 100644
index 0000000000..56b79815ae
Binary files /dev/null and b/docs/images/CommandParser.png differ
diff --git a/docs/images/NUS_Students.png b/docs/images/NUS_Students.png
new file mode 100644
index 0000000000..ab13e390b1
Binary files /dev/null and b/docs/images/NUS_Students.png differ
diff --git a/docs/images/Storage.png b/docs/images/Storage.png
new file mode 100644
index 0000000000..d4b0221bec
Binary files /dev/null and b/docs/images/Storage.png differ
diff --git a/docs/images/StorageRunSequenceDiagram.png b/docs/images/StorageRunSequenceDiagram.png
new file mode 100644
index 0000000000..51a9e3c5f7
Binary files /dev/null and b/docs/images/StorageRunSequenceDiagram.png differ
diff --git a/docs/images/StorageSequenceDiagram.png b/docs/images/StorageSequenceDiagram.png
new file mode 100644
index 0000000000..14938d0613
Binary files /dev/null and b/docs/images/StorageSequenceDiagram.png differ
diff --git a/docs/team/Zhao_Lixiuqi.md b/docs/team/Zhao_Lixiuqi.md
new file mode 100644
index 0000000000..5d1d6b0d3a
--- /dev/null
+++ b/docs/team/Zhao_Lixiuqi.md
@@ -0,0 +1,30 @@
+# Zhao Lixiuqi - Project Portfolio Page
+
+## Moneymind
+MoneyMind is a desktop app for managing your finances,
+optimized for use via a Command Line Interface (CLI).
+It is written in Java, has cross-platform compatibility with Windows,
+Linux and MacOS, and has about 3 kLoC.
+
+### Summary of Contributions
+* Code contributed: [RepoSense link](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17&tabOpen=true&tabType=authorship&tabAuthor=alexgoexercise&tabRepo=AY2223S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false)
+* Feature implemented: Wrote the basic data structure for the project
+ * What it does: It stores the data of the application in a list of categories and a list of events.
+ * Justification: This feature is integral to the product because it is essential to perform any useful tasks.
+ It enables the application to store the data of the application in a list of categories and a list of events, improving both the user experience and the functionality of the application.
+* Feature implemented: Added the ability to add or delete a category or event and store them into the data structure
+ * What it does: allows the user to add or delete a new category to the list of categories, or add or delete an event to a category.
+ * Justification: This feature is important because it is the basic functionality of the application.
+* Contributions to the UG:
+ * Created the basic structure for the UG including documentation for most of the features (
+ including `event`, `category`, `view` and so on)
+ * Added the command summary section.
+ * Added the quick start section.
+* Contributions to the DG:
+ * Constructed the skeleton structure for DG.
+ * Added the overall architecture.
+ * Added the data structure architecture.
+ * Added the value proposition.
+ * Added the user stories.
+ * Added the non-functional requirements.
+ * Added the glossary section.
\ No newline at end of file
diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md
deleted file mode 100644
index ab75b391b8..0000000000
--- a/docs/team/johndoe.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# John Doe - Project Portfolio Page
-
-## Overview
-
-
-### Summary of Contributions
diff --git a/docs/team/mingyuannus.md b/docs/team/mingyuannus.md
new file mode 100644
index 0000000000..f75ff4f78d
--- /dev/null
+++ b/docs/team/mingyuannus.md
@@ -0,0 +1,26 @@
+# Li Mingyuan - Project Portfolio Page
+
+## Moneymind
+MoneyMind is a desktop app for managing your finances, optimized for use via a Command Line Interface (CLI). It is written in Java, has cross-platform compatibility with Windows, Linux and MacOS, and has about 3 kLoC.
+
+### Summary of Contributions
+* Code contributed: [RepoSense link](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=mingyuannus&tabRepo=AY2223S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false)
+* Feature implemented: Created the main class
+ * What it does: The main class for the program which handles the main loop of the program.
+ * Justification: Required for the program to function and is implemented using OOP best practices.
+* Feature implemented: Added command to search for categories and events
+ * What it does: Allows users to search for categories and events matching a certain keyword.
+ * Justification: Users who have many categories and events may want to quickly find categories or events without having to manually find it from the entire list of categories or events.
+* Feature implemented: Implemented the command interface architecture
+ * What it does: Ensures that all commands has a specific set of methods by inheriting from the command interface.
+ * Justification: Better conforms to OOP principles and let any command be executed in a consistent way, and that new commands can be easily added by simply implementing the required methods.
+* Feature implemented: Added exception to handle invalid commands
+ * What is does: Exception class that is thrown when an invalid command error occurs and contains logic to show error message.
+ * Justification: Provides a uniform way of handling exceptions due to invalid user input regardless of the type of command.
+* Documentation
+ * User guide
+ * Added documentation for the search command
+ * Formatting and other tweaks
+ * Developer guide
+ * Added class diagram for commands component and parser component
+ * Formatting and other tweaks
diff --git a/docs/team/mnsd05.md b/docs/team/mnsd05.md
new file mode 100644
index 0000000000..55ccf18c7b
--- /dev/null
+++ b/docs/team/mnsd05.md
@@ -0,0 +1,23 @@
+# Nguyen Duc Thang - Project Portfolio Page
+
+## Moneymind
+MoneyMind is a desktop app for managing your finances, optimized for use via a Command Line Interface (CLI). It is written in Java, has cross-platform compatibility with Windows, Linux and MacOS, and has about 3 kLoC.
+
+### Summary of Contributions
+* Code contributed: [RepoSense link](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17&tabOpen=true&tabType=authorship&tabAuthor=Mnsd05&tabRepo=AY2223S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false)
+* Feature implemented: Added the ability to parse user input
+ * What it does: Ensure the user input is in correct format before executing the command
+ * Justification: This feature is integral to perform any useful tasks. It enables the application to process and interpret user input accurately and efficiently, improving both the user experience and the functionality of the application.
+ * Why it is hard to implement: Because it requires to handle all the possible exceptions that may occur when parsing the user input and to define custom exceptions to handle the unexpected cases like integer overflow, dummy input, etc. Most importantly, I have to come up with complex and efficient regexes to parse the user input.
+* Features implemented: Added the logic to execute every feature of the product except "Search", "Bye" and "Summary" commands
+ * What it does: Add, delete categories and view all the events in a category. Add, delete, edit events and view every event in the list.
+ * Justification: Those features are important because it contains the core functionality of the application.
+* Contributions to the UG:
+ * Added documentation for the format of "event", "edit", "delete" and "view" commands and
+ examples of using them
+* Contributions to the DG:
+ * Added implementation details of the `command', 'parser', 'exception', 'category' and 'event' components.
+ * Added manual testing instructions.
+* Review/mentoring contributions:
+ * Find bugs for teammates
+ * Resolve merge conflicts
diff --git a/docs/team/toh-hongfeng.md b/docs/team/toh-hongfeng.md
new file mode 100644
index 0000000000..b5e330b9a0
--- /dev/null
+++ b/docs/team/toh-hongfeng.md
@@ -0,0 +1,30 @@
+# Toh Hong Feng - Project Portfolio Page
+
+## Moneymind
+MoneyMind is a desktop app for managing your finances, optimized for use via a Command Line Interface (CLI). It is written in Java, has cross-platform compatibility with Windows, Linux and MacOS, and has about 3 kLoC.
+
+### Summary of Contributions
+* Code contributed: [RepoSense link](https://nus-cs2113-ay2223s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2023-02-17&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=Toh-HongFeng&tabRepo=AY2223S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false)
+* Feature implemented: Added the ability to save and load the program's data
+ * What it does: allows the user to automatically save and load the current list of categories and expenses to a text file
+ * Justification: This feature is integral to the product because a user can save current session to continue in the future, or to send to another friend who can also use the list.
+* Feature implemented: Added the ability to reminder users in advance of upcoming expenses
+ * What it does: allows the user to use the date of an monthly recurring expense as a reminder for the user to prepare for the expense. This feature automatically updates the reminder date when the user adds a new expense to its next occurrence.
+ * Justification: This feature is important because it allows the user to be reminded of upcoming expenses, and to plan their finances accordingly.
+* Contributions to the UG:
+ * Added documentation for the `reminder` feature
+ * Added Q&A for the `storage` feature
+ * Initial draft of the `Features` section of the UG
+* Contributions to the DG:
+ * Added implementation details of the `storage` feature
+* Contributions to team-based tasks:
+ * Set up team organization and repository on GitHub
+ * Managed releases [v1.0](https://github.com/AY2223S2-CS2113-T15-3/tp/releases/tag/v1.0), [v2.0](https://github.com/AY2223S2-CS2113-T15-3/tp/releases/tag/v2.0) on GitHub
+* Review/mentoring contributions:
+ * Resolve merge conflicts for v1.0 (PRs [#8](https://github.com/AY2223S2-CS2113-T15-3/tp/pull/14), [#23](https://github.com/AY2223S2-CS2113-T15-3/tp/pull/23))
+* Documentation:
+ * User Guide:
+ * Created initial draft of `Features` section of UG
+ * Developer Guide:
+ * Added implementation details of the `storage` component
+ * Added implementation details of the `reminder` component
\ No newline at end of file
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/moneymind/Moneymind.java b/src/main/java/seedu/moneymind/Moneymind.java
new file mode 100644
index 0000000000..f662d74c54
--- /dev/null
+++ b/src/main/java/seedu/moneymind/Moneymind.java
@@ -0,0 +1,88 @@
+package seedu.moneymind;
+
+import java.util.Scanner;
+
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.command.CategoryCommand;
+import seedu.moneymind.command.Command;
+import seedu.moneymind.exceptions.InvalidCommandException;
+import seedu.moneymind.storage.Storage;
+import seedu.moneymind.parser.Parser;
+import seedu.moneymind.string.Strings;
+import seedu.moneymind.ui.Ui;
+
+import static seedu.moneymind.string.Strings.DATA_FILE;
+import static seedu.moneymind.string.Strings.WHITE_SPACE;
+
+public class Moneymind {
+
+ public static Scanner in;
+ private Parser parser;
+ private Storage storage;
+ private Ui ui;
+ private String userInput;
+
+ public Moneymind() {
+ this.parser = new Parser();
+ this.storage = new Storage(DATA_FILE);
+ this.ui = new Ui();
+ this.in = new Scanner(System.in);
+ }
+
+ /**
+ * Runs the program until termination.
+ */
+ public void run() {
+ ui.greet();
+ boolean isExit = false;
+ try {
+ storage.load();
+ CategoryList.categories = storage.getSavedCategories();
+ CategoryCommand.categoryMap = storage.getSavedCategoryHashMap();
+ } catch (Exception e) {
+ ui.loadingError(e);
+ }
+ try {
+ System.out.println(Reminder.checkCategoryReminder(CategoryList.categories));
+ } catch (Exception e) {
+ ui.error(e);
+ }
+ while (!isExit) {
+ try {
+ getInput();
+ // remove extra spaces
+ String refinedUserInput = userInput.trim().replaceAll(Strings.EXTRA_SPACE_REGEX_FORMAT, WHITE_SPACE);
+ Command command = parser.parseNextCommand(refinedUserInput);
+ if (command.isExit()) {
+ ui.goodbye();
+ isExit = true;
+ } else {
+ assert !command.isExit() : "Command must have a valid execute method";
+ command.execute(ui); // should also accept storage object as parameter
+ storage.save(CategoryList.categories);
+ }
+ } catch (InvalidCommandException e) {
+ e.showErrorMessage();
+ } catch (Exception e) {
+ ui.error(e);
+ }
+ }
+ }
+
+ /**
+ * Gets the user input and allows the user to enter a new input if the input is empty.
+ */
+ private void getInput() {
+ System.out.println(Strings.HORIZONTAL_LINE);
+ System.out.println();
+ do {
+ userInput = in.nextLine();
+ } while (userInput.trim().isEmpty());
+ System.out.println(Strings.HORIZONTAL_LINE);
+ }
+
+ public static void main(String[] args) {
+ new Moneymind().run();
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/Reminder.java b/src/main/java/seedu/moneymind/Reminder.java
new file mode 100644
index 0000000000..72ae90fd0f
--- /dev/null
+++ b/src/main/java/seedu/moneymind/Reminder.java
@@ -0,0 +1,58 @@
+package seedu.moneymind;
+
+import java.util.ArrayList;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+import static seedu.moneymind.string.Strings.NEW_LINE;
+import static seedu.moneymind.string.Strings.HORIZONTAL_LINE;
+import static seedu.moneymind.string.Strings.EMPTY_STRING;
+import static seedu.moneymind.UserDate.isApproaching;
+import static seedu.moneymind.UserDate.numberDaysAway;
+
+/**
+ * A class to remind the user of nearing expenses.
+ */
+public class Reminder {
+
+ /**
+ * Checks if there are any nearing expenses.
+ *
+ * @param categories The list of categories.
+ * @return A string containing the nearing expenses.
+ */
+ public static String checkCategoryReminder(ArrayList categories) {
+ String reminder = EMPTY_STRING;
+ for (Category category : categories) {
+ reminder += checkEventReminder(category);
+ }
+ if (reminder.isBlank()) {
+ return EMPTY_STRING;
+ } else {
+ return HORIZONTAL_LINE + NEW_LINE + "Approaching expenses:" + NEW_LINE + reminder;
+ }
+ }
+
+ private static String checkEventReminder(Category category) {
+ String reminder = EMPTY_STRING;
+ for (Event event : category.getEvents()) {
+ if (event.isOneTimeExpense() == false && isApproaching(event.getTime())) {
+ reminder += category.getName() + " has an event: " + event.getDescription()
+ + stringOfDaysAway(event);
+ }
+ }
+ return reminder;
+ }
+
+ private static String stringOfDaysAway(Event event) {
+ if (numberDaysAway(event.getTime()) > 1) {
+ return " in " + numberDaysAway(event.getTime()) + " days" + NEW_LINE;
+ } else if (numberDaysAway(event.getTime()) == 1) {
+ return " in 1 day" + NEW_LINE;
+ } else {
+ return "today" + NEW_LINE;
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/UserDate.java b/src/main/java/seedu/moneymind/UserDate.java
new file mode 100644
index 0000000000..307bdbafe2
--- /dev/null
+++ b/src/main/java/seedu/moneymind/UserDate.java
@@ -0,0 +1,80 @@
+package seedu.moneymind;
+
+import static java.time.temporal.ChronoUnit.DAYS;
+import static seedu.moneymind.string.Strings.DATE_FORMAT;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Deals with the current system date and time.
+ */
+public class UserDate {
+
+ /**
+ * Returns the current system date.
+ *
+ * @return The current system date.
+ */
+ public static String getSystemDate() {
+ LocalDateTime now = LocalDateTime.now();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+ String formattedDate = now.format(formatter);
+ return formattedDate;
+ }
+
+ /**
+ * Returns true if the date is valid.
+ *
+ * @param date The date to be checked.
+ * @return True if the date is valid.
+ */
+ public static Boolean isValidDate(String date) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+ try {
+ LocalDateTime.parse(date, formatter);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if the date is approaching.
+ *
+ * @param date The date to be checked.
+ * @return True if the date is approaching.
+ */
+ public static Boolean isApproaching(String date) {
+ return (numberDaysAway(date) <= 5 && numberDaysAway(date) >= 0);
+ }
+
+ /**
+ * Returns the number of days away from the current date.
+ *
+ * @param date The date to be checked.
+ * @return The number of days away from the current date.
+ */
+ public static Integer numberDaysAway(String date) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime inputDate = LocalDateTime.parse(date, formatter);
+ return (int) now.until(inputDate, DAYS);
+ }
+
+ /**
+ * Returns the date of the next monthly expense.
+ *
+ * @param oldDate The date to be updated.
+ * @return The date after updating.
+ */
+ public static String updateDate(String oldDate) {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
+ LocalDateTime inputDate = LocalDateTime.parse(oldDate, formatter);
+ while (inputDate.isBefore(LocalDateTime.now())) {
+ inputDate = inputDate.plusMonths(1);
+ }
+ return inputDate.format(formatter);
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/category/Category.java b/src/main/java/seedu/moneymind/category/Category.java
new file mode 100644
index 0000000000..15aec7427a
--- /dev/null
+++ b/src/main/java/seedu/moneymind/category/Category.java
@@ -0,0 +1,89 @@
+package seedu.moneymind.category;
+
+import seedu.moneymind.event.Event;
+
+import java.util.ArrayList;
+import static seedu.moneymind.string.Strings.NO_EVENTS_IN_THIS_CATEGORY_MESSAGE;
+import static seedu.moneymind.string.Strings.DOT;
+
+/**
+ * Represents a category.
+ */
+public class Category {
+
+ public ArrayList events = new ArrayList<>();
+ private String name;
+ private int budget;
+
+ /**
+ * A constructor with name.
+ */
+ public Category(String name) {
+ this.name = name;
+ }
+
+ /**
+ * A constructor with name and budget.
+ */
+ public Category(String name, int budget) {
+ this.name = name;
+ this.budget = budget;
+ }
+
+ /**
+ * Gets the name of the category.
+ *
+ * @return the name of the category
+ */
+ public String getName() {
+ return name;
+ }
+
+ public ArrayList getEvents() {
+ return events;
+ }
+
+ /**
+ * Add the event to the list.
+ */
+ public void addEvent(Event event) {
+ events.add(event);
+ }
+
+ /**
+ * Gets the list of events.
+ */
+ public void viewEventList() {
+ if (events.size() == 0) {
+ System.out.println(NO_EVENTS_IN_THIS_CATEGORY_MESSAGE);
+ return;
+ }
+ for (int i = 0; i < events.size(); i++) {
+ System.out.println(i + 1 + DOT + events.get(i).toString());
+ }
+ }
+
+ public int getBudget() {
+ return budget;
+ }
+
+ public void setBudget(int budget) {
+ this.budget = budget;
+ }
+
+ /**
+ * Gets the total one time expense of the category.
+ *
+ * @return the total one time expense of the category
+ */
+ public int getTotalOneTimeExpense() {
+ int totalExpense = 0;
+ for (int i = 0; i < events.size(); i++) {
+ if (events.get(i).isOneTimeExpense()) {
+ totalExpense += events.get(i).getExpense();
+ }
+ }
+ return totalExpense;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/category/CategoryList.java b/src/main/java/seedu/moneymind/category/CategoryList.java
new file mode 100644
index 0000000000..f74df2f5db
--- /dev/null
+++ b/src/main/java/seedu/moneymind/category/CategoryList.java
@@ -0,0 +1,22 @@
+package seedu.moneymind.category;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a list of categories.
+ */
+public class CategoryList {
+
+ public static ArrayList categories = new ArrayList();
+
+ /**
+ * Gets the category with specific index in the list.
+ *
+ * @param index the index of the category in the list
+ * @return the category with specific index in the list
+ */
+ public static Category getCategory(int index) {
+ return categories.get(index);
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/ByeCommand.java b/src/main/java/seedu/moneymind/command/ByeCommand.java
new file mode 100644
index 0000000000..2e008990f7
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/ByeCommand.java
@@ -0,0 +1,21 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.ui.Ui;
+
+
+/**
+ * Represents the command to exit the program.
+ */
+public class ByeCommand implements Command {
+
+ @Override
+ public void execute(Ui ui) {
+ ui.goodbye();
+ }
+
+ @Override
+ public boolean isExit() {
+ return true;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/CategoryCommand.java b/src/main/java/seedu/moneymind/command/CategoryCommand.java
new file mode 100644
index 0000000000..531f8c8b74
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/CategoryCommand.java
@@ -0,0 +1,56 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.ui.Ui;
+import seedu.moneymind.string.Strings;
+
+import java.util.HashMap;
+
+import static seedu.moneymind.string.Strings.NEW_CATEGORY_ADDED_MESSAGE;
+
+/**
+ * Represents the command to add a new category.
+ */
+public class CategoryCommand implements Command {
+
+ public static HashMap categoryMap = new HashMap<>();
+ private final String name;
+ private int budget;
+
+ /**
+ * Constructs a new CategoryCommand object and adds the category.
+ *
+ * @param name the name of the category
+ */
+ public CategoryCommand(String name, int budget) {
+ this.name = name;
+ this.budget = budget;
+ }
+
+ /**
+ * Adds the category.
+ */
+ @Override
+ public void execute(Ui ui) {
+ if (categoryMap.get(name) != null) {
+ System.out.println(Strings.EXISTED_CATEGORY);
+ return;
+ }
+ Category category;
+ if (budget == 0) {
+ category = new Category(name);
+ } else {
+ category = new Category(name, budget);
+ }
+ CategoryList.categories.add(category);
+ categoryMap.put(name, CategoryList.categories.size() - 1);
+ System.out.println(NEW_CATEGORY_ADDED_MESSAGE + name);
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/Command.java b/src/main/java/seedu/moneymind/command/Command.java
new file mode 100644
index 0000000000..7c69be7cef
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/Command.java
@@ -0,0 +1,14 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.ui.Ui;
+
+/**
+ * Represents a command interface with hidden internal logic and the ability to be executed.
+ */
+public interface Command {
+
+ void execute(Ui ui);
+
+ boolean isExit();
+
+}
diff --git a/src/main/java/seedu/moneymind/command/DeleteCommand.java b/src/main/java/seedu/moneymind/command/DeleteCommand.java
new file mode 100644
index 0000000000..47033a228d
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/DeleteCommand.java
@@ -0,0 +1,99 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.ui.Ui;
+
+import static seedu.moneymind.string.Strings.NULL_CATEGORY_ASSERTION;
+import static seedu.moneymind.string.Strings.NO_CATEGORY_MESSAGE;
+import static seedu.moneymind.string.Strings.NON_EXISTENT_EVENT;
+import static seedu.moneymind.string.Strings.EVENT_DELETION_MESSAGE;
+import static seedu.moneymind.string.Strings.CATEGORY_DELETION_MESSAGE;
+
+/**
+ * Represents the command to delete an event or a category.
+ */
+public class DeleteCommand implements Command {
+
+ private final String categoryName;
+ private int eventIndex;
+ private final boolean isEvent;
+
+ /**
+ * Constructs a new DeleteCommand object and deletes the event.
+ *
+ * @param categoryName the name of the category
+ * @param eventIndex the index of the event
+ */
+ public DeleteCommand(String categoryName, int eventIndex) {
+ this.categoryName = categoryName;
+ this.eventIndex = eventIndex;
+ assert categoryName != null : NULL_CATEGORY_ASSERTION;
+ this.isEvent = true;
+ }
+
+ /**
+ * Constructs a new DeleteCommand object and deletes the category.
+ *
+ * @param categoryName the name of the category
+ */
+ public DeleteCommand(String categoryName) {
+ this.categoryName = categoryName;
+ assert categoryName != null : NULL_CATEGORY_ASSERTION;
+ this.isEvent = false;
+ }
+
+ /**
+ * Deletes the event.
+ */
+ private void deleteEvent() {
+ if (CategoryCommand.categoryMap.get(categoryName) == null) {
+ System.out.println(NO_CATEGORY_MESSAGE);
+ return;
+ }
+ int categoryIndex = CategoryCommand.categoryMap.get(categoryName);
+ Category category = CategoryList.categories.get(categoryIndex);
+ if (eventIndex >= category.getEvents().size()) {
+ System.out.println(NON_EXISTENT_EVENT);
+ return;
+ }
+ String eventName = category.getEvents().get(eventIndex).getDescription();
+ category.getEvents().remove(eventIndex);
+ System.out.println(EVENT_DELETION_MESSAGE + eventName);
+ }
+
+ /**
+ * Deletes the category.
+ */
+ private void deleteCategory() {
+ if (CategoryCommand.categoryMap.get(categoryName) == null) {
+ System.out.println(NO_CATEGORY_MESSAGE);
+ return;
+ }
+ int categoryIndex = CategoryCommand.categoryMap.get(categoryName);
+ CategoryList.categories.remove(categoryIndex);
+ CategoryCommand.categoryMap.remove(categoryName);
+ // loop through the hashmap and update the index of the categories
+ for (String key : CategoryCommand.categoryMap.keySet()) {
+ if (CategoryCommand.categoryMap.get(key) > categoryIndex) {
+ CategoryCommand.categoryMap.put(key, CategoryCommand.categoryMap.get(key) - 1);
+ }
+ }
+ System.out.println(CATEGORY_DELETION_MESSAGE + categoryName);
+ }
+
+ @Override
+ public void execute(Ui ui) {
+ if (isEvent) {
+ deleteEvent();
+ } else {
+ deleteCategory();
+ }
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/EditCommand.java b/src/main/java/seedu/moneymind/command/EditCommand.java
new file mode 100644
index 0000000000..3f404862c4
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/EditCommand.java
@@ -0,0 +1,184 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.Moneymind;
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.exceptions.IntegerOverflowException;
+import seedu.moneymind.exceptions.NegativeNumberException;
+import seedu.moneymind.ui.Ui;
+
+import static seedu.moneymind.string.Strings.EDIT_EXPENSE_LIMIT_MESSAGE;
+import static seedu.moneymind.string.Strings.EDIT_BUDGET_LIMIT_MESSAGE;
+import static seedu.moneymind.string.Strings.SUBTLE_BUG_MESSAGE;
+import static seedu.moneymind.string.Strings.BACK;
+import static seedu.moneymind.string.Strings.ENTERING_NON_NEGATIVE_NUMBER_MESSAGE;
+import static seedu.moneymind.string.Strings.NEGATIVE_INTEGER_DETECTING_REGEX;
+import static seedu.moneymind.string.Strings.INTEGER_DETECTING_REGEX;
+import static seedu.moneymind.string.Strings.NON_EXISTENT_EVENT;
+import static seedu.moneymind.string.Strings.NO_CATEGORY_MESSAGE;
+import static seedu.moneymind.string.Strings.EMPTY_STRING;
+import static seedu.moneymind.string.Strings.TYPING_NEW_EXPENSE_MESSAGE;
+import static seedu.moneymind.string.Strings.TYPING_NEW_BUDGET_MESSAGE;
+import static seedu.moneymind.string.Strings.NEW_EXPENSE_MESSAGE;
+import static seedu.moneymind.string.Strings.NEW_BUDGET_MESSAGE;
+
+/**
+ * Edits the budget of a category or expense of an event.
+ */
+public class EditCommand implements Command {
+
+ private boolean isEvent;
+ private String categoryName;
+ private int eventIndex;
+ private String userInput;
+ private boolean isReady = true;
+ private int categoryIndex;
+
+ /**
+ * Constructs a new EditCommand object for editing an event.
+ *
+ * @param categoryName The name of the category.
+ * @param eventIndex The index of the event.
+ */
+ public EditCommand (String categoryName, int eventIndex) {
+ this.isEvent = true;
+ this.categoryName = categoryName;
+ this.eventIndex = eventIndex;
+ }
+
+ /**
+ * Constructs a new EditCommand object for editing a category.
+ *
+ * @param categoryName The name of the category.
+ */
+ public EditCommand (String categoryName) {
+ this.isEvent = false;
+ this.categoryName = categoryName;
+ }
+
+ /**
+ * Checks whether the category and event to be edited exist.
+ */
+ private void prepareEditEvent() {
+ if (CategoryCommand.categoryMap.get(categoryName) == null) {
+ System.out.println(NO_CATEGORY_MESSAGE);
+ isReady = false;
+ return;
+ }
+ categoryIndex = CategoryCommand.categoryMap.get(categoryName);
+ Category category = CategoryList.categories.get(categoryIndex);
+ if (eventIndex >= category.getEvents().size()) {
+ System.out.println(NON_EXISTENT_EVENT);
+ isReady = false;
+ return;
+ }
+ if (isReady) {
+ String eventName = category.getEvents().get(eventIndex).getDescription();
+ System.out.println("The current event expense for " + eventName + " is: " +
+ category.getEvents().get(eventIndex).getExpense());
+ System.out.println(TYPING_NEW_EXPENSE_MESSAGE);
+ }
+ }
+
+ /**
+ * Checks whether the category to be edited exists.
+ */
+ private void prepareEditCategory() {
+ if (CategoryCommand.categoryMap.get(categoryName) == null) {
+ System.out.println(NO_CATEGORY_MESSAGE);
+ isReady = false;
+ return;
+ }
+ categoryIndex = CategoryCommand.categoryMap.get(categoryName);
+ Category category = CategoryList.categories.get(categoryIndex);
+ if (isReady) {
+ System.out.println("The current budget for " + categoryName + " is: " +
+ category.getBudget());
+ System.out.println(TYPING_NEW_BUDGET_MESSAGE);
+ }
+ }
+
+ /**
+ * Check if the user input is valid for editing an expense or a budget.
+ *
+ * @param userInput The user input.
+ * @return True if the user input is valid, false otherwise.
+ */
+ private boolean isEditSuccessful(String userInput) {
+ try {
+ checkNegative(userInput);
+ checkAmountUnderLimit(userInput);
+ checkNumber(userInput);
+ return true;
+ } catch (IntegerOverflowException error) {
+ if (isEvent) {
+ System.out.println(EDIT_EXPENSE_LIMIT_MESSAGE);
+ } else {
+ System.out.println(EDIT_BUDGET_LIMIT_MESSAGE);
+ }
+ } catch (NumberFormatException | NegativeNumberException error) {
+ System.out.println(ENTERING_NON_NEGATIVE_NUMBER_MESSAGE);
+ } catch (Exception error) {
+ System.out.println(SUBTLE_BUG_MESSAGE);
+ }
+ return false;
+ }
+
+ private void checkNumber(String newAmount) throws NumberFormatException {
+ Integer.parseInt(newAmount);
+ }
+
+ private void checkNegative(String newAmount) throws NegativeNumberException {
+ if (newAmount.matches(NEGATIVE_INTEGER_DETECTING_REGEX)) {
+ throw new NegativeNumberException();
+ }
+ }
+
+ private void checkAmountUnderLimit(String newAmount) throws IntegerOverflowException {
+ if (newAmount.matches(INTEGER_DETECTING_REGEX) && newAmount.length() > 9) {
+ throw new IntegerOverflowException();
+ }
+ }
+
+ @Override
+ public void execute(Ui ui) {
+ if (isEvent) {
+ prepareEditEvent();
+ } else {
+ prepareEditCategory();
+ }
+ if (!isReady) {
+ return;
+ }
+ getUserInputUntilNonEmpty();
+ while (!isEditSuccessful(userInput)) {
+ getUserInputUntilNonEmpty();
+ if (userInput.equals(BACK)) {
+ break;
+ }
+ }
+ if (!userInput.equals(BACK)) {
+ if (isEvent) {
+ System.out.println(NEW_EXPENSE_MESSAGE + userInput);
+ CategoryList.categories.get(categoryIndex).getEvents().
+ get(eventIndex).setExpense(Integer.parseInt(userInput));
+ } else {
+ System.out.println(NEW_BUDGET_MESSAGE + userInput);
+ CategoryList.categories.get(categoryIndex).setBudget(Integer.parseInt(userInput));
+ }
+ }
+
+ }
+
+ private void getUserInputUntilNonEmpty() {
+ do {
+ userInput = Moneymind.in.nextLine();
+ } while (userInput.equals(EMPTY_STRING));
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/EventCommand.java b/src/main/java/seedu/moneymind/command/EventCommand.java
new file mode 100644
index 0000000000..95aedcf2ee
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/EventCommand.java
@@ -0,0 +1,113 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.Moneymind;
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.event.Event;
+import seedu.moneymind.ui.Ui;
+import static seedu.moneymind.string.Strings.NULL_EVENT_ASSERTION;
+import static seedu.moneymind.string.Strings.NON_NEGATIVE_EXPENSE_ASSERTION;
+import static seedu.moneymind.string.Strings.EVENT_ADDED_MESSAGE;
+import static seedu.moneymind.string.Strings.SUBTLE_BUG_MESSAGE;
+import static seedu.moneymind.string.Strings.SELECTING_CATEGORY_MESSAGE;
+import static seedu.moneymind.string.Strings.GO_BACK_MESSAGE;
+import static seedu.moneymind.string.Strings.BACK;
+import static seedu.moneymind.string.Strings.CATEGORY_DOES_NOT_EXIST_MESSAGE;
+import static seedu.moneymind.string.Strings.EMPTY_STRING;
+
+/**
+ * Adds a one time expense event or monthly recurring event to a category.
+ */
+public class EventCommand implements Command {
+
+ private String userInput;
+ private String eventName;
+ private String time;
+ private int expense;
+
+ /**
+ * Constructs a new EventCommand object for adding monthly recurring event.
+ *
+ * @param eventName The name of the event.
+ * @param expense The expense of the event.
+ * @param time The time of the event.
+ */
+ public EventCommand(String eventName, int expense, String time) {
+ this.eventName = eventName;
+ this.expense = expense;
+ this.time = time;
+ assert eventName != null : NULL_EVENT_ASSERTION;
+ assert expense >= 0 : NON_NEGATIVE_EXPENSE_ASSERTION;
+ }
+
+ /**
+ * Constructs a new EventCommand object for adding one time expense event.
+ *
+ * @param eventName The name of the event.
+ * @param expense The expense of the event.
+ */
+ public EventCommand(String eventName, int expense) {
+ this.eventName = eventName;
+ this.expense = expense;
+ this.time = EMPTY_STRING;
+ assert eventName != null : NULL_EVENT_ASSERTION;
+ assert expense >= 0 : NON_NEGATIVE_EXPENSE_ASSERTION;
+ }
+
+ /**
+ * Add an event to a category.
+ */
+ private void addEventToCategory(String categoryName, Event event) {
+ int categoryPosition = CategoryCommand.categoryMap.get(categoryName);
+ Category category = CategoryList.getCategory(categoryPosition);
+ category.addEvent(event);
+ System.out.println(EVENT_ADDED_MESSAGE + event.getDescription());
+ }
+
+ /**
+ * Checks if the user correctly chooses an existing category.
+ *
+ * @param userInput The user input.
+ * @return True if the user input is valid.
+ */
+ private boolean isAddEventSuccessful(String userInput) {
+ try {
+ if (time == EMPTY_STRING) {
+ addEventToCategory(userInput, new Event(eventName, expense));
+ } else {
+ addEventToCategory(userInput, new Event(eventName, expense, time));
+ }
+ return true;
+ } catch (NullPointerException error) {
+ System.out.println(CATEGORY_DOES_NOT_EXIST_MESSAGE);
+ } catch (Exception error) {
+ System.out.println(SUBTLE_BUG_MESSAGE);
+ }
+ return false;
+ }
+
+ @Override
+ public void execute(Ui ui) {
+ System.out.println(SELECTING_CATEGORY_MESSAGE);
+ getUserInputUntilNonEmpty();
+ while (!isAddEventSuccessful(userInput)) {
+ System.out.println(GO_BACK_MESSAGE);
+ getUserInputUntilNonEmpty();
+ if (userInput.equals(BACK)) {
+ break;
+ }
+ }
+ }
+
+ private void getUserInputUntilNonEmpty() {
+ do {
+ userInput = Moneymind.in.nextLine();
+ } while (userInput.equals(EMPTY_STRING));
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/HelpCommand.java b/src/main/java/seedu/moneymind/command/HelpCommand.java
new file mode 100644
index 0000000000..fe81466aa1
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/HelpCommand.java
@@ -0,0 +1,40 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.ui.Ui;
+
+import static seedu.moneymind.string.Strings.INTRODUCTION_HELP_COMMAND;
+import static seedu.moneymind.string.Strings.HELP_INSTRUCTION;
+import static seedu.moneymind.string.Strings.SUMMARY_INSTRUCTION;
+import static seedu.moneymind.string.Strings.CATEGORY_INSTRUCTION;
+import static seedu.moneymind.string.Strings.EVENT_INSTRUCTION;
+import static seedu.moneymind.string.Strings.VIEW_INSTRUCTION;
+import static seedu.moneymind.string.Strings.EDIT_INSTRUCTION;
+import static seedu.moneymind.string.Strings.DELETE_INSTRUCTION;
+import static seedu.moneymind.string.Strings.SEARCH_INSTRUCTION;
+import static seedu.moneymind.string.Strings.BYE_INSTRUCTION;
+
+/**
+ * Represents the help command.
+ */
+public class HelpCommand implements Command {
+
+ @Override
+ public void execute(Ui ui) {
+ System.out.println(INTRODUCTION_HELP_COMMAND);
+ System.out.println(HELP_INSTRUCTION);
+ System.out.println(SUMMARY_INSTRUCTION);
+ System.out.println(CATEGORY_INSTRUCTION);
+ System.out.println(EVENT_INSTRUCTION);
+ System.out.println(VIEW_INSTRUCTION);
+ System.out.println(EDIT_INSTRUCTION);
+ System.out.println(DELETE_INSTRUCTION);
+ System.out.println(SEARCH_INSTRUCTION);
+ System.out.println(BYE_INSTRUCTION);
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/SearchCommand.java b/src/main/java/seedu/moneymind/command/SearchCommand.java
new file mode 100644
index 0000000000..ca943d063c
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/SearchCommand.java
@@ -0,0 +1,226 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+import seedu.moneymind.ui.Ui;
+
+import java.util.Set;
+import java.util.LinkedHashSet;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+import static seedu.moneymind.category.CategoryList.categories;
+import static seedu.moneymind.ui.Ui.printMatchingCategories;
+import static seedu.moneymind.ui.Ui.printSimilarCategories;
+import static seedu.moneymind.ui.Ui.printMatchingEvents;
+import static seedu.moneymind.ui.Ui.printSimilarEvents;
+
+/**
+ * Command for searching categories and events based on
+ * a query string provided by the user. Displays both categories
+ * and events that contain the query string and the top 3 most similar
+ * categories and events that do not contain the query string exactly.
+ */
+public class SearchCommand implements Command {
+ private final String query;
+
+ public SearchCommand(String query) {
+ this.query = query;
+ }
+
+ @Override
+ public void execute(Ui ui) {
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ ArrayList similarCategoriesList = new ArrayList<>();
+ ArrayList similarEventsList = new ArrayList<>();
+
+ similarCategoriesList.addAll(similarCategories.keySet());
+ similarEventsList.addAll(similarEvents.keySet());
+
+ sortCategoryBySimilarity(similarCategoriesList, similarCategories);
+ sortEventBySimilarity(similarEventsList, similarEvents);
+
+ printMatchingCategories(matchingCategories);
+ printMatchingEvents(matchingEvents);
+ printSimilarCategories(similarCategoriesList);
+ printSimilarEvents(similarEventsList);
+ }
+
+ /**
+ * Assign categories and events to appropriate collections,
+ * depending on whether they match the query string or are
+ * just similar to it.
+ * @param matchingCategories Set containing categories that match the query string.
+ * @param matchingEvents Set containing events that match the query string.
+ * @param similarCategories Hash map containing categories that are similar to the query string.
+ * @param similarEvents Hash map containing events that are similar to the query string.
+ * @param query The query string
+ */
+ public void assignItemsBySimilarity(
+ Set matchingCategories,
+ Set matchingEvents,
+ HashMap similarCategories,
+ HashMap similarEvents,
+ String query
+ ) {
+ for (Category category : categories) {
+ // calculate similarity between this category and query, and add to the appropriate list
+ int categorySimilarityDistance = calculateSimilarityDistance(query, category.getName());
+ if (categorySimilarityDistance == 0) {
+ matchingCategories.add(category);
+ similarCategories.remove(category);
+ } else if (
+ similarCategories.get(category) != null &&
+ similarCategories.get(category) < categorySimilarityDistance) {
+ similarCategories.put(category, categorySimilarityDistance);
+ } else {
+ similarCategories.putIfAbsent(category, categorySimilarityDistance);
+ }
+
+ for (Event event : category.getEvents()) {
+ // calculate similarity between this event and query, and add to the appropriate list
+ int eventSimilarityDistance = calculateSimilarityDistance(query, event.getDescription());
+ if (eventSimilarityDistance == 0) {
+ matchingEvents.add(event);
+ similarEvents.remove(event);
+ } else if (similarEvents.get(event) != null &&
+ similarEvents.get(event) < eventSimilarityDistance) {
+ similarEvents.put(event, eventSimilarityDistance);
+ } else {
+ similarEvents.putIfAbsent(event, eventSimilarityDistance);
+ }
+ }
+ }
+ }
+
+ /**
+ * Calculate the smallest similarity distance between a query string
+ * and all possible substrings of the same length of a value string.
+ * If value string is shorter than the query string, then the roles are
+ * reversed, but the two strings are compared directly instead of using
+ * equal length substrings.
+ * @param queryString The query string.
+ * @param valueString The value string.
+ * @return The smallest Levenshtein distance between the 2 strings.
+ */
+ public int calculateSimilarityDistance(String queryString, String valueString) {
+ if (queryString.length() >= valueString.length()) {
+ // immediately calculate similarity and return
+ return calculateLevenshteinDistance(valueString, queryString);
+ }
+
+ // list of all similarity to all possible substrings of same length
+ ArrayList distances = new ArrayList();
+
+ int lengthDifference = valueString.length() - queryString.length();
+
+ // for every possible substring, calculate similarity distance to query
+ for (int i = 0; i < lengthDifference; i++) {
+ String substring = valueString.substring(i, i+queryString.length());
+ int distance = calculateLevenshteinDistance(queryString, substring);
+ distances.add(distance);
+ }
+
+ // find the lowest similarity distance and return
+ int min = distances.get(0);
+ for (int distance : distances) {
+ if (distance < min) {
+ min = distance;
+ }
+ }
+ return min;
+ }
+
+ /**
+ * Calculates the Levenshtein distance between 2 strings. Not case-sensitive.
+ * @param firstString The first string.
+ * @param secondString The second string.
+ * @return An integer representing the Levenshtein distance between the 2 strings.
+ */
+ public int calculateLevenshteinDistance(String firstString, String secondString) {
+ firstString = firstString.toLowerCase();
+ secondString = secondString.toLowerCase();
+
+ int[] costs = new int[secondString.length() + 1];
+ for (int i = 0; i <= firstString.length(); i++) {
+ int lastValue = i;
+ for (int j = 0; j <= secondString.length(); j++) {
+ if (i == 0) {
+ costs[j] = j;
+ } else if (j > 0) {
+ int newValue = costs[j - 1];
+ if (firstString.charAt(i - 1) != secondString.charAt(j - 1)) {
+ newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
+ }
+ costs[j - 1] = lastValue;
+ lastValue = newValue;
+ }
+ }
+ if (i > 0) {
+ costs[secondString.length()] = lastValue;
+ }
+ }
+ return costs[secondString.length()];
+ }
+
+ /**
+ * Sorts a list of categories depending on its similarity distance to a query string.
+ * The similarity distance should be provided by a hash map.
+ * @param input The list of categories
+ * @param set The hash map of categories and their similarity distances
+ */
+ public void sortCategoryBySimilarity(ArrayList input, HashMap set) {
+ Comparator comparator = (firstCategory, secondCategory) -> {
+ int result = set.get(firstCategory) - set.get(secondCategory);
+ if (result == 0) {
+ result = firstCategory.getName().hashCode() - secondCategory.getName().hashCode();
+ }
+ return result;
+ };
+ input.sort(comparator);
+ }
+
+ /**
+ * Sorts a list of events depending on its similarity distance to a query string.
+ * The similarity distance should be provided by a hash map.
+ * @param input The list of events
+ * @param set The hash map of events and their similarity distances
+ */
+ public void sortEventBySimilarity(ArrayList input, HashMap set) {
+ Comparator comparator = (firstEvent, secondEvent) -> {
+ int result = set.get(firstEvent) - set.get(secondEvent);
+ if (result == 0) {
+ result = firstEvent.getDescription().hashCode() - secondEvent.getDescription().hashCode();
+ }
+ return result;
+ };
+ input.sort(Comparator.comparingInt(set::get));
+ }
+
+ /**
+ * Finds the category of an event
+ * @param event The event to find category for
+ * @return Category of the event
+ */
+ public static Category getCategoryOfEvent(Event event) {
+ for (Category category : categories) {
+ if (category.events.contains(event)) {
+ return category;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/command/SummaryCommand.java b/src/main/java/seedu/moneymind/command/SummaryCommand.java
new file mode 100644
index 0000000000..6710623adb
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/SummaryCommand.java
@@ -0,0 +1,20 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.ui.Ui;
+
+import static seedu.moneymind.string.Strings.SUMMARY_LIST;
+
+public class SummaryCommand implements Command{
+ /**
+ * Shows the summary of commands.
+ */
+ @Override
+ public void execute(Ui ui) {
+ System.out.println(SUMMARY_LIST);
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+}
diff --git a/src/main/java/seedu/moneymind/command/ViewCommand.java b/src/main/java/seedu/moneymind/command/ViewCommand.java
new file mode 100644
index 0000000000..4413101966
--- /dev/null
+++ b/src/main/java/seedu/moneymind/command/ViewCommand.java
@@ -0,0 +1,101 @@
+package seedu.moneymind.command;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.event.Event;
+import seedu.moneymind.ui.Ui;
+
+import static seedu.moneymind.string.Strings.NULL_CATEGORY_ASSERTION;
+import static seedu.moneymind.string.Strings.NO_CATEGORY_MESSAGE;
+import static seedu.moneymind.string.Strings.NULL_CATEGORY_LIST_ASSERTION;
+import static seedu.moneymind.string.Strings.NO_CATEGORIES_TO_VIEW;
+import static seedu.moneymind.string.Strings.DOT;
+import static seedu.moneymind.string.Strings.SHOW_CATEGORY_MESSAGE;
+import static seedu.moneymind.string.Strings.BIG_WHITE_SPACE;
+import static seedu.moneymind.string.Strings.EXCEEDED_BUDGET_WARNING_MESSAGE;
+
+/**
+ * Views the categories and events.
+ */
+public class ViewCommand implements Command {
+
+ private String categoryName;
+ private final boolean isCategorySpecified;
+
+ /**
+ * Constructs a new ViewCommand object and views a single category.
+ *
+ * @param categoryName the name of the category
+ */
+ public ViewCommand(String categoryName) {
+ this.categoryName = categoryName;
+ assert categoryName != null : NULL_CATEGORY_ASSERTION;
+ this.isCategorySpecified = true;
+ }
+
+ /**
+ * Constructs a new ViewCommand object and views all the categories.
+ */
+ public ViewCommand() {
+ assert CategoryList.categories != null : NULL_CATEGORY_LIST_ASSERTION;
+ this.isCategorySpecified = false;
+ }
+
+ /**
+ * Views all events in a single category.
+ */
+ private void viewOne() {
+ if (CategoryCommand.categoryMap.get(categoryName) == null) {
+ System.out.println(NO_CATEGORY_MESSAGE);
+ return;
+ }
+ int categoryIndex = CategoryCommand.categoryMap.get(categoryName);
+ Category category = CategoryList.categories.get(categoryIndex);
+ category.viewEventList();
+ if (category.getTotalOneTimeExpense() > category.getBudget()) {
+ System.out.println(EXCEEDED_BUDGET_WARNING_MESSAGE);
+ }
+ }
+
+ /**
+ * Views all the categories and events.
+ */
+ private void viewAll() {
+ if (CategoryList.categories.size() == 0) {
+ System.out.println(NO_CATEGORIES_TO_VIEW);
+ return;
+ }
+ System.out.println(SHOW_CATEGORY_MESSAGE);
+ int category_count = 1;
+ for (Category category : CategoryList.categories) {
+ System.out.println(category_count + ") Category: " + category.getName() +
+ " (budget: " + category.getBudget() + ")");
+ category_count++;
+ int count = 1;
+ // print all the events in the category with index
+ for (Event event : category.getEvents()) {
+ System.out.println(BIG_WHITE_SPACE + count + DOT + event.toString());
+ count++;
+ }
+ if (category.getTotalOneTimeExpense() > category.getBudget()) {
+ System.out.println(BIG_WHITE_SPACE + EXCEEDED_BUDGET_WARNING_MESSAGE);
+ }
+ }
+
+ }
+
+ @Override
+ public void execute(Ui ui) {
+ if (isCategorySpecified) {
+ viewOne();
+ } else {
+ viewAll();
+ }
+ }
+
+ @Override
+ public boolean isExit() {
+ return false;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/event/Event.java b/src/main/java/seedu/moneymind/event/Event.java
new file mode 100644
index 0000000000..2bef169010
--- /dev/null
+++ b/src/main/java/seedu/moneymind/event/Event.java
@@ -0,0 +1,113 @@
+package seedu.moneymind.event;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+import static seedu.moneymind.UserDate.isValidDate;
+import static seedu.moneymind.UserDate.getSystemDate;
+import static seedu.moneymind.string.Strings.DATE_FORMAT;
+
+public class Event {
+
+ private boolean isOneTimeExpense;
+ private String description;
+ private String time;
+ private int expense;
+
+ /**
+ * A constructor with both description and budget.
+ */
+ public Event(String description, int expense) {
+ this.description = description;
+ this.expense = expense;
+ LocalDateTime myDateObject = LocalDateTime.now();
+ DateTimeFormatter myFormatObject = DateTimeFormatter.ofPattern(DATE_FORMAT);
+ this.time = myDateObject.format(myFormatObject);
+ this.isOneTimeExpense = true;
+ }
+
+ /**
+ * A constructor with all parameters.
+ */
+ public Event(String description, int expense, String time) {
+ this.description = description;
+ this.expense = expense;
+ this.time = time;
+ this.isOneTimeExpense = false;
+ }
+
+ /**
+ * Gets the description of the event.
+ *
+ * @return the description of the event
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Gets the expense of the event.
+ *
+ * @return the expense of the event
+ */
+ public int getExpense() {
+ return expense;
+ }
+
+
+ /**
+ * Sets the expense of the event.
+ *
+ * @param expense the expense of the event
+ */
+ public void setExpense(int expense) {
+ this.expense = expense;
+ }
+
+ /**
+ * Sets the time of the event.
+ * If the time is not valid, the system date will be used.
+ *
+ * @param time the time of the event
+ */
+ public String setTime(String time) {
+ if (isValidDate(time)) {
+ this.time = time;
+ } else {
+ this.time = getSystemDate();
+ }
+ return this.time;
+ }
+
+ /**
+ * Gets the time of the event.
+ *
+ * @return the time of the event
+ */
+ public String getTime() {
+ return time;
+ }
+
+ /**
+ * Gets the boolean value of isOneTimeExpense.
+ *
+ * @return the boolean value of isOneTimeExpense
+ */
+ public boolean isOneTimeExpense() {
+ return isOneTimeExpense;
+ }
+
+ /**
+ * Returns a string representation of the event.
+ *
+ * @return a string representation of the event
+ */
+ public String toString() {
+ if (isOneTimeExpense) {
+ return description + " (expense: " + expense + ") (time added: " + time + ")";
+ }
+ return description + " (expense: " + expense + ") (start time: " + time + ")";
+ }
+
+}
+
diff --git a/src/main/java/seedu/moneymind/exceptions/IntegerOverflowException.java b/src/main/java/seedu/moneymind/exceptions/IntegerOverflowException.java
new file mode 100644
index 0000000000..cae18e84b6
--- /dev/null
+++ b/src/main/java/seedu/moneymind/exceptions/IntegerOverflowException.java
@@ -0,0 +1,8 @@
+package seedu.moneymind.exceptions;
+
+/**
+ * Throws exception when the user input an integer that is too large.
+ */
+public class IntegerOverflowException extends Exception {
+ // no code is needed here
+}
diff --git a/src/main/java/seedu/moneymind/exceptions/InvalidCommandException.java b/src/main/java/seedu/moneymind/exceptions/InvalidCommandException.java
new file mode 100644
index 0000000000..10391eb62a
--- /dev/null
+++ b/src/main/java/seedu/moneymind/exceptions/InvalidCommandException.java
@@ -0,0 +1,17 @@
+package seedu.moneymind.exceptions;
+
+/**
+ * Throws exception when the user input an invalid command.
+ */
+public class InvalidCommandException extends Exception {
+ private String errorMessage;
+
+ public InvalidCommandException(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public void showErrorMessage() {
+ System.out.println(errorMessage);
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/exceptions/InvalidTimeFormatException.java b/src/main/java/seedu/moneymind/exceptions/InvalidTimeFormatException.java
new file mode 100644
index 0000000000..7f185a29e5
--- /dev/null
+++ b/src/main/java/seedu/moneymind/exceptions/InvalidTimeFormatException.java
@@ -0,0 +1,8 @@
+package seedu.moneymind.exceptions;
+
+/**
+ * Throws exception when the user input a time in the wrong format.
+ */
+public class InvalidTimeFormatException extends Exception {
+ // no code is needed here
+}
diff --git a/src/main/java/seedu/moneymind/exceptions/NegativeNumberException.java b/src/main/java/seedu/moneymind/exceptions/NegativeNumberException.java
new file mode 100644
index 0000000000..c8385923af
--- /dev/null
+++ b/src/main/java/seedu/moneymind/exceptions/NegativeNumberException.java
@@ -0,0 +1,8 @@
+package seedu.moneymind.exceptions;
+
+/**
+ * Throws exception when the user input a negative number.
+ */
+public class NegativeNumberException extends Exception {
+ // no code is needed here
+}
diff --git a/src/main/java/seedu/moneymind/exceptions/NonPositiveNumberException.java b/src/main/java/seedu/moneymind/exceptions/NonPositiveNumberException.java
new file mode 100644
index 0000000000..27f1fb7dbe
--- /dev/null
+++ b/src/main/java/seedu/moneymind/exceptions/NonPositiveNumberException.java
@@ -0,0 +1,8 @@
+package seedu.moneymind.exceptions;
+
+/**
+ * Throws exception when the user input a non-positive number.
+ */
+public class NonPositiveNumberException extends Exception {
+ // no code is needed here
+}
diff --git a/src/main/java/seedu/moneymind/parser/Parser.java b/src/main/java/seedu/moneymind/parser/Parser.java
new file mode 100644
index 0000000000..881aa07e3c
--- /dev/null
+++ b/src/main/java/seedu/moneymind/parser/Parser.java
@@ -0,0 +1,397 @@
+package seedu.moneymind.parser;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import seedu.moneymind.command.ByeCommand;
+import seedu.moneymind.command.CategoryCommand;
+import seedu.moneymind.command.Command;
+import seedu.moneymind.command.DeleteCommand;
+import seedu.moneymind.command.EditCommand;
+import seedu.moneymind.command.EventCommand;
+import seedu.moneymind.command.HelpCommand;
+import seedu.moneymind.command.SummaryCommand;
+import seedu.moneymind.command.SearchCommand;
+import seedu.moneymind.command.ViewCommand;
+import seedu.moneymind.exceptions.IntegerOverflowException;
+import seedu.moneymind.exceptions.InvalidCommandException;
+import seedu.moneymind.exceptions.InvalidTimeFormatException;
+import seedu.moneymind.exceptions.NegativeNumberException;
+import seedu.moneymind.exceptions.NonPositiveNumberException;
+import static seedu.moneymind.UserDate.isValidDate;
+
+import static seedu.moneymind.string.Strings.BYE;
+import static seedu.moneymind.string.Strings.CATEGORY;
+import static seedu.moneymind.string.Strings.DELETE;
+import static seedu.moneymind.string.Strings.EDIT;
+import static seedu.moneymind.string.Strings.EVENT;
+import static seedu.moneymind.string.Strings.HELP;
+import static seedu.moneymind.string.Strings.INVALID_INPUT;
+import static seedu.moneymind.string.Strings.NULL_INPUT_ASSERTION;
+import static seedu.moneymind.string.Strings.SEARCH;
+import static seedu.moneymind.string.Strings.SUMMARY;
+import static seedu.moneymind.string.Strings.VIEW;
+import static seedu.moneymind.string.Strings.WHITE_SPACE;
+import static seedu.moneymind.string.Strings.EMPTY_STRING;
+import static seedu.moneymind.string.Strings.NO_DESCRIPTION_FOR_BYE;
+import static seedu.moneymind.string.Strings.NO_DESCRIPTION_FOR_HELP;
+import static seedu.moneymind.string.Strings.NO_DESCRIPTION_FOR_SUMMARY;
+import static seedu.moneymind.string.Strings.DELETE_REGEX;
+import static seedu.moneymind.string.Strings.EDIT_REGEX;
+import static seedu.moneymind.string.Strings.CATEGORY_REGEX;
+import static seedu.moneymind.string.Strings.EVENT_REGEX;
+import static seedu.moneymind.string.Strings.NEGATIVE_INTEGER_DETECTING_REGEX;
+import static seedu.moneymind.string.Strings.INTEGER_DETECTING_REGEX;
+import static seedu.moneymind.string.Strings.ZERO_MATCHING_REGEX;
+import static seedu.moneymind.string.Strings.EMPTY_DELETION;
+import static seedu.moneymind.string.Strings.INDEX_LIMIT_MESSAGE;
+import static seedu.moneymind.string.Strings.BUDGET_LIMIT_MESSAGE;
+import static seedu.moneymind.string.Strings.POSITIVE_INTEGER_FOR_EVENT_INDEX;
+import static seedu.moneymind.string.Strings.DELETE_FORMAT;
+import static seedu.moneymind.string.Strings.EDIT_FORMAT;
+import static seedu.moneymind.string.Strings.CATEGORY_FORMAT;
+import static seedu.moneymind.string.Strings.EVENT_FORMAT;
+import static seedu.moneymind.string.Strings.REMINDING_MESSAGE_ABOUT_NOT_LETTING_EMPTY;
+import static seedu.moneymind.string.Strings.EMPTY_DESCRIPTION_FOR_CATEGORY;
+import static seedu.moneymind.string.Strings.EMPTY_DESCRIPTION_FOR_EVENT;
+import static seedu.moneymind.string.Strings.EMPTY_DESCRIPTION_FOR_EDIT;
+import static seedu.moneymind.string.Strings.SUBTLE_BUG_MESSAGE;
+import static seedu.moneymind.string.Strings.EXPENSE_LIMIT_MESSAGE;
+import static seedu.moneymind.string.Strings.NON_NEGATIVE_INTEGER_FOR_BUDGET;
+import static seedu.moneymind.string.Strings.NON_NEGATIVE_INTEGER_FOR_EXPENSE;
+import static seedu.moneymind.string.Strings.ENTERING_VALID_TIME_FORMAT_MESSAGE;
+import static seedu.moneymind.string.Strings.NO_SEARCH_KEYWORD_MESSAGE;
+
+
+/**
+ * Parses user input strings into corresponding command objects that can be executed by the MoneyMind application.
+ */
+public class Parser {
+
+ private static final int KEYWORD_INDEX = 0;
+ private static final int DESCRIPTION_INDEX = 1;
+ private static final int LENGTH_FOR_SINGLE_WORD_COMMAND = 1;
+ private static final int MAXIMUM_INDEX = 2;
+ private static final int LENGTH_FOR_BYE_COMMAND = 1;
+ private static final int LENGTH_FOR_HELP_COMMAND = 1;
+ private static final int LENGTH_FOR_SUMMARY_COMMAND = 1;
+ private static final int LENGTH_FOR_VIEW_ALL_COMMAND = 1;
+ private static final int MINIMUM_LENGTH_FOR_SEARCH_COMMAND = 2;
+ private static final int VIEW_CATEGORY_INDEX = 1;
+ private static final int DEFAULT_BUDGET = 0;
+
+ /**
+ * Parses user input and returns a command object that can be executed by the application.
+ *
+ * @param input the user input string to parse.
+ * @return a command object that corresponds to the keyword in user input.
+ * @throws Exception if there was an error while parsing the user input or constructing the corresponding command.
+ */
+ public Command parseNextCommand(String input) throws Exception {
+ assert input != null : NULL_INPUT_ASSERTION;
+ String[] separatedKeywordAndDescription = input.split(WHITE_SPACE, MAXIMUM_INDEX);
+ String keyword = separatedKeywordAndDescription[KEYWORD_INDEX];
+
+ switch (keyword) {
+ case BYE:
+ return prepareByeCommand(separatedKeywordAndDescription);
+ case HELP:
+ return prepareHelpCommand(separatedKeywordAndDescription);
+ case SUMMARY:
+ return prepareSummaryCommand(separatedKeywordAndDescription);
+ case VIEW:
+ return prepareViewCommand(separatedKeywordAndDescription);
+ case DELETE:
+ return prepareDeleteCommand(separatedKeywordAndDescription);
+ case EVENT:
+ return prepareEventCommand(separatedKeywordAndDescription);
+ case CATEGORY:
+ return prepareCategoryCommand(separatedKeywordAndDescription);
+ case SEARCH:
+ return prepareSearchCommand(separatedKeywordAndDescription);
+ case EDIT:
+ return prepareEditCommand(separatedKeywordAndDescription);
+ default:
+ throw new InvalidCommandException(INVALID_INPUT);
+ }
+ }
+
+ /**
+ * Constructs a bye command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "bye" keyword,
+ * and the second string should be empty or null.
+ * @return a bye command object.
+ * @throws InvalidCommandException if the user input contains additional text beyond the "bye" keyword.
+ */
+ private Command prepareByeCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ try {
+ if (separatedKeywordAndDescription.length > LENGTH_FOR_BYE_COMMAND) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ return new ByeCommand();
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(NO_DESCRIPTION_FOR_BYE);
+ }
+ }
+
+ /**
+ * Constructs a help command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "help" keyword,
+ * and the second string should be empty or null.
+ * @return a help command object.
+ * @throws InvalidCommandException if the user input contains additional text beyond the "help" keyword.
+ */
+ private Command prepareHelpCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ try {
+ if (separatedKeywordAndDescription.length > LENGTH_FOR_HELP_COMMAND) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ return new HelpCommand();
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(NO_DESCRIPTION_FOR_HELP);
+ }
+ }
+
+ /**
+ * Constructs a summary command object if the user input is valid.
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "summary" keyword,
+ * and the second string should be empty or null.
+ * @return a summary command object.
+ * @throws InvalidCommandException
+ */
+ private Command prepareSummaryCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ try {
+ if (separatedKeywordAndDescription.length > LENGTH_FOR_SUMMARY_COMMAND) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ return new SummaryCommand();
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(NO_DESCRIPTION_FOR_SUMMARY);
+ }
+ }
+
+ /**
+ * Constructs a view command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "view" keyword,
+ * and the second string may contain additional text specifying a category name.
+ * @return a view command object to view one category if the user input contains a category name, or a view command
+ * object to view all categories if the user input does not contain a category name.
+ */
+ private Command prepareViewCommand(String[] separatedKeywordAndDescription) {
+ if (separatedKeywordAndDescription.length == LENGTH_FOR_VIEW_ALL_COMMAND) {
+ return new ViewCommand();
+ } else {
+ return new ViewCommand(separatedKeywordAndDescription[VIEW_CATEGORY_INDEX]);
+ }
+ }
+
+ /**
+ * Constructs a delete command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "delete" keyword,
+ * and the second string should contain additional text specifying a category name
+ * and an optional event index to delete.
+ * @return a delete command object to delete a whole category if the user input contains a category name
+ * but no event index, or a delete command object to delete an event in a category if the user input contains a
+ * category name and an event index.
+ * @throws InvalidCommandException if the user input is not in the correct format,
+ * or if there is a bug in the application.
+ */
+ private Command prepareDeleteCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ Pattern pattern = Pattern.compile(DELETE_REGEX);
+ if (separatedKeywordAndDescription.length == LENGTH_FOR_SINGLE_WORD_COMMAND) {
+ throw new InvalidCommandException(EMPTY_DELETION);
+ }
+ Matcher matcher = pattern.matcher(separatedKeywordAndDescription[DESCRIPTION_INDEX]);
+ try {
+ if (!matcher.find()) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ String categoryName = matcher.group(1);
+ if (matcher.group(2) == null) {
+ return new DeleteCommand(categoryName);
+ }
+ checkNonPositive(matcher.group(2));
+ checkBigNumber(matcher.group(2));
+ int eventIndex = Integer.parseInt(matcher.group(2));
+ // the operation below switches the eventIndex to 0-based indexing
+ return new DeleteCommand(categoryName, eventIndex - 1);
+ } catch (IntegerOverflowException error) {
+ throw new InvalidCommandException(INDEX_LIMIT_MESSAGE);
+ } catch (NumberFormatException | NonPositiveNumberException error) {
+ throw new InvalidCommandException(POSITIVE_INTEGER_FOR_EVENT_INDEX);
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(DELETE_FORMAT + "\n" + REMINDING_MESSAGE_ABOUT_NOT_LETTING_EMPTY);
+ } catch (Exception error) {
+ throw new InvalidCommandException(SUBTLE_BUG_MESSAGE);
+ }
+ }
+
+ /**
+ * Constructs an event command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "event" keyword,
+ * and the second string should contain additional text specifying an event
+ * description, expense, and optional time for recurring events.
+ * @return a one time expense event command object if the user input does not contain a time, or a monthly
+ * recurring expense event command object if the user input contains a time.
+ * @throws InvalidCommandException if the user input is not in the correct format,
+ * or if there is a bug in the application.
+ */
+ private Command prepareEventCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ Pattern pattern = Pattern.compile(EVENT_REGEX);
+ try {
+ Matcher matcher = pattern.matcher(separatedKeywordAndDescription[DESCRIPTION_INDEX]);
+ if (!matcher.find()) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ String eventName = matcher.group(1);
+ checkNegative(matcher.group(2));
+ checkBigNumber(matcher.group(2));
+ int expenseNumber = Integer.parseInt(matcher.group(2));
+ String time = matcher.group(3);
+ if (matcher.group(3) == null) {
+ return new EventCommand(eventName, expenseNumber);
+ }
+ checkValidTimeFormat(time);
+ return new EventCommand(eventName, expenseNumber, time);
+ } catch (IndexOutOfBoundsException error) {
+ throw new InvalidCommandException(EMPTY_DESCRIPTION_FOR_EVENT);
+ } catch (IntegerOverflowException error) {
+ throw new InvalidCommandException(EXPENSE_LIMIT_MESSAGE);
+ } catch (NegativeNumberException | NumberFormatException error) {
+ throw new InvalidCommandException(NON_NEGATIVE_INTEGER_FOR_EXPENSE);
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(EVENT_FORMAT + "\n" + REMINDING_MESSAGE_ABOUT_NOT_LETTING_EMPTY);
+ } catch (InvalidTimeFormatException error) {
+ throw new InvalidCommandException(ENTERING_VALID_TIME_FORMAT_MESSAGE);
+ } catch (Exception error) {
+ throw new InvalidCommandException(SUBTLE_BUG_MESSAGE);
+ }
+ }
+
+ /**
+ * Constructs a category command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "category" keyword,
+ * and the second string should contain additional text specifying a category name and an optional budget.
+ * @return a category command object to add a category with default budget 0
+ * if the user input contains a category name but no budget, or a category command object to add a category
+ * with a budget if the user input contains a category name and a budget.
+ * @throws InvalidCommandException if the user input is not in the correct format,
+ * or if there is a bug in the application.
+ */
+ private Command prepareCategoryCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ Pattern pattern = Pattern.compile(CATEGORY_REGEX);
+ try {
+ Matcher matcher = pattern.matcher(separatedKeywordAndDescription[DESCRIPTION_INDEX]);
+ if (!matcher.find()) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ String categoryName = matcher.group(1);
+ if (matcher.group(2) == null) {
+ return new CategoryCommand(categoryName, DEFAULT_BUDGET);
+ }
+ checkNegative(matcher.group(2));
+ checkBigNumber(matcher.group(2));
+ int budget = Integer.parseInt(matcher.group(2));
+ return new CategoryCommand(categoryName, budget);
+ } catch (IntegerOverflowException error) {
+ throw new InvalidCommandException(BUDGET_LIMIT_MESSAGE);
+ } catch (IndexOutOfBoundsException error) {
+ throw new InvalidCommandException(EMPTY_DESCRIPTION_FOR_CATEGORY);
+ } catch (NegativeNumberException | NumberFormatException error) {
+ throw new InvalidCommandException(NON_NEGATIVE_INTEGER_FOR_BUDGET);
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(CATEGORY_FORMAT + "\n" + REMINDING_MESSAGE_ABOUT_NOT_LETTING_EMPTY);
+ } catch (Exception error) {
+ throw new InvalidCommandException(SUBTLE_BUG_MESSAGE);
+ }
+ }
+
+ /**
+ * Constructs a search command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "search" keyword,
+ * and the second string should contain additional text specifying a keyword to search for.
+ * @return a search command object to search for a keyword.
+ * @throws InvalidCommandException if the user input is not in the correct format,
+ * or if there is a bug in the application.
+ */
+ private Command prepareSearchCommand(String[] separatedKeywordAndDescription) throws InvalidCommandException {
+ if (separatedKeywordAndDescription.length < MINIMUM_LENGTH_FOR_SEARCH_COMMAND) {
+ throw new InvalidCommandException(NO_SEARCH_KEYWORD_MESSAGE);
+ }
+ return new SearchCommand(separatedKeywordAndDescription[DESCRIPTION_INDEX]);
+ }
+
+ /**
+ * Constructs an edit command object if the user input is valid.
+ *
+ * @param separatedKeywordAndDescription an array of two strings: the first string should be the "edit" keyword,
+ * and the second string should contain additional text specifying a category name
+ * and an optional event index to edit.
+ * @return an edit command object to edit a budget of a category if the user input contains a category name
+ * but no event index, or an edit command object to edit an event expense in a category if the user input
+ * contains a category name and an event index.
+ * @throws InvalidCommandException if the user input is not in the correct format,
+ * or if there is a bug in the application.
+ */
+ private Command prepareEditCommand(String[] separatedKeywordAndDescription) throws Exception {
+ Pattern pattern = Pattern.compile(EDIT_REGEX);
+ if (separatedKeywordAndDescription.length == LENGTH_FOR_SINGLE_WORD_COMMAND) {
+ throw new InvalidCommandException(EMPTY_DESCRIPTION_FOR_EDIT);
+ }
+ Matcher matcher = pattern.matcher(separatedKeywordAndDescription[DESCRIPTION_INDEX]);
+ try {
+ if (!matcher.find()) {
+ throw new InvalidCommandException(EMPTY_STRING);
+ }
+ String categoryName = matcher.group(1);
+ if (matcher.group(2) == null) {
+ return new EditCommand(categoryName);
+ }
+ checkNonPositive(matcher.group(2));
+ checkBigNumber(matcher.group(2));
+ int eventIndex = Integer.parseInt(matcher.group(2));
+ // the operation below switches the eventIndex to 0-based indexing
+ return new EditCommand(categoryName, eventIndex - 1);
+ } catch (IntegerOverflowException error) {
+ throw new InvalidCommandException(INDEX_LIMIT_MESSAGE);
+ } catch (NumberFormatException | NonPositiveNumberException error) {
+ throw new InvalidCommandException(POSITIVE_INTEGER_FOR_EVENT_INDEX);
+ } catch (InvalidCommandException error) {
+ throw new InvalidCommandException(EDIT_FORMAT + "\n" + REMINDING_MESSAGE_ABOUT_NOT_LETTING_EMPTY);
+ } catch (Exception error) {
+ throw new InvalidCommandException(SUBTLE_BUG_MESSAGE);
+ }
+ }
+
+ private void checkValidTimeFormat(String time) throws InvalidTimeFormatException {
+ if (!isValidDate(time)) {
+ throw new InvalidTimeFormatException();
+ }
+ }
+
+ private void checkNegative(String number) throws NegativeNumberException {
+ if (number.matches(NEGATIVE_INTEGER_DETECTING_REGEX)) {
+ throw new NegativeNumberException();
+ }
+ }
+
+ private void checkNonPositive(String number) throws NonPositiveNumberException {
+ if (number.matches(NEGATIVE_INTEGER_DETECTING_REGEX) || number.matches(ZERO_MATCHING_REGEX)) {
+ throw new NonPositiveNumberException();
+ }
+ }
+
+ private void checkBigNumber(String number) throws IntegerOverflowException {
+ if (number.matches(INTEGER_DETECTING_REGEX) && number.length() > 9) {
+ throw new IntegerOverflowException();
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/storage/CategoriesToString.java b/src/main/java/seedu/moneymind/storage/CategoriesToString.java
new file mode 100644
index 0000000000..dcb140c956
--- /dev/null
+++ b/src/main/java/seedu/moneymind/storage/CategoriesToString.java
@@ -0,0 +1,53 @@
+package seedu.moneymind.storage;
+
+import java.util.ArrayList;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+import static seedu.moneymind.string.Strings.STORAGE_CATEGORY_NAME;
+import static seedu.moneymind.string.Strings.STORAGE_NEXT_VARIABLE;
+import static seedu.moneymind.string.Strings.NEW_LINE;
+import static seedu.moneymind.string.Strings.checkForStorageDelimiter;
+
+/**
+ * Converts the ArrayList of categories to a String.
+ */
+public class CategoriesToString {
+
+ /**
+ * Converts the ArrayList of categories to a String.
+ *
+ * @param categories The ArrayList of categories to be converted.
+ * @return The String representation of the ArrayList of categories.
+ */
+ public static String categoriesToString(ArrayList categories) {
+ String categoriesAsString = "";
+ for (Category category : categories) {
+ categoriesAsString += STORAGE_CATEGORY_NAME +
+ checkForStorageDelimiter(category.getName());
+ if (category.getBudget() != 0) {
+ categoriesAsString += STORAGE_CATEGORY_NAME + category.getBudget();
+ }
+ categoriesAsString += NEW_LINE + eventsToString(category.getEvents());
+ }
+ return categoriesAsString;
+ }
+
+ /**
+ * Converts the ArrayList of events to a String.
+ *
+ * @param events The ArrayList of events to be converted.
+ * @return The String representation of the ArrayList of events.
+ */
+ private static String eventsToString(ArrayList events) {
+ String eventsAsString = "";
+ for (Event event : events) {
+ eventsAsString += STORAGE_NEXT_VARIABLE + checkForStorageDelimiter(event.getDescription())
+ + STORAGE_NEXT_VARIABLE + event.getExpense() + STORAGE_NEXT_VARIABLE
+ + event.getTime() + STORAGE_NEXT_VARIABLE + event.isOneTimeExpense() + NEW_LINE;
+ }
+ return eventsAsString;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/storage/GenerateCategoryHashMap.java b/src/main/java/seedu/moneymind/storage/GenerateCategoryHashMap.java
new file mode 100644
index 0000000000..e6cbce4e97
--- /dev/null
+++ b/src/main/java/seedu/moneymind/storage/GenerateCategoryHashMap.java
@@ -0,0 +1,27 @@
+package seedu.moneymind.storage;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import seedu.moneymind.category.Category;
+
+/**
+ * Generates a HashMap of a given ArrayList of categories.
+ */
+public class GenerateCategoryHashMap {
+
+ /**
+ * Generates a HashMap of a given ArrayList of categories.
+ *
+ * @param savedCategories The ArrayList of categories.
+ * @return The HashMap of the categories.
+ */
+ public static HashMap generateCategoryHashMap(ArrayList savedCategories) {
+ HashMap savedCategoryHashMap = new HashMap<>();
+ for (int i = 0; i < savedCategories.size(); i++) {
+ savedCategoryHashMap.put(savedCategories.get(i).getName(), i);
+ }
+ return savedCategoryHashMap;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/storage/ReadFromFile.java b/src/main/java/seedu/moneymind/storage/ReadFromFile.java
new file mode 100644
index 0000000000..aa59c9b886
--- /dev/null
+++ b/src/main/java/seedu/moneymind/storage/ReadFromFile.java
@@ -0,0 +1,36 @@
+package seedu.moneymind.storage;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.Scanner;
+
+import static seedu.moneymind.string.Strings.NEW_LINE;
+
+/**
+ * Reads from a file.
+ */
+public class ReadFromFile {
+
+ /**
+ * Reads from a file.
+ *
+ * @param textFile the path of the file to be read
+ * @return the saved tasks
+ * @throws Exception
+ */
+ public static String readFromFile(File textFile) throws Exception {
+ Scanner in;
+ try {
+ in = new Scanner(textFile);
+ } catch (FileNotFoundException e) {
+ throw new Exception("File not found.");
+ }
+ String fileString = "";
+ while (in.hasNextLine()) {
+ fileString += in.nextLine() + NEW_LINE;
+ }
+ in.close();
+ return fileString;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/storage/Storage.java b/src/main/java/seedu/moneymind/storage/Storage.java
new file mode 100644
index 0000000000..80abe0d1c1
--- /dev/null
+++ b/src/main/java/seedu/moneymind/storage/Storage.java
@@ -0,0 +1,96 @@
+package seedu.moneymind.storage;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import seedu.moneymind.category.Category;
+
+import static seedu.moneymind.storage.CategoriesToString.categoriesToString;
+import static seedu.moneymind.storage.GenerateCategoryHashMap.generateCategoryHashMap;
+import static seedu.moneymind.storage.ReadFromFile.readFromFile;
+import static seedu.moneymind.storage.StringToCategories.stringToCategories;
+
+/**
+ * Represents the storage of the data.
+ */
+public class Storage {
+
+ private File textFile;
+ private ArrayList savedCategories;
+ private HashMap savedCategoryHashMap;
+
+ /**
+ * Constructor for Storage class.
+ *
+ * @param filePath The path of the file to be created.
+ */
+ public Storage(String filePath) {
+ this.textFile = new File(filePath);
+
+ try {
+ Boolean isFileCreated = textFile.createNewFile();
+ System.out.println((!isFileCreated) ? "Loading file..." : "Creating file...");
+ } catch (Exception e) {
+ System.out.println("Error creating file..." + e.getMessage());
+ }
+ }
+
+ /**
+ * Saves the events to the file.
+ *
+ * @param categories The ArrayList of events to be saved.
+ */
+ public void save(ArrayList categories) {
+ String textToWrite = categoriesToString(categories);
+
+ moneymindWrite(textToWrite);
+ }
+
+ /**
+ * Writes the text to the file.
+ *
+ * @param textToWrite The text to be written to the file.
+ */
+ private void moneymindWrite(String textToWrite) {
+ try {
+ FileWriter moneymindWriter = new FileWriter(textFile);
+ moneymindWriter.write(textToWrite);
+ moneymindWriter.close();
+ } catch (IOException e) {
+ System.out.println("Error writing to file: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Loads the events from the file.
+ *
+ * @throws Exception If there is an error loading the file.
+ */
+ public void load() throws Exception {
+ String savedDataString = readFromFile(textFile);
+ savedCategories = stringToCategories(savedDataString);
+ savedCategoryHashMap = generateCategoryHashMap(savedCategories);
+ }
+
+ /**
+ * Returns the ArrayList created from the saved data.
+ *
+ * @return The ArrayList created from the saved data.
+ */
+ public ArrayList getSavedCategories() {
+ return savedCategories;
+ }
+
+ /**
+ * Returns the HashMap created from the saved data.
+ *
+ * @return The HashMap created from the saved data.
+ */
+ public HashMap getSavedCategoryHashMap() {
+ return savedCategoryHashMap;
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/storage/StringToCategories.java b/src/main/java/seedu/moneymind/storage/StringToCategories.java
new file mode 100644
index 0000000000..07d8c557d7
--- /dev/null
+++ b/src/main/java/seedu/moneymind/storage/StringToCategories.java
@@ -0,0 +1,80 @@
+package seedu.moneymind.storage;
+
+import java.util.ArrayList;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+import static seedu.moneymind.string.Strings.STORAGE_CATEGORY_NAME;
+import static seedu.moneymind.string.Strings.STORAGE_NEXT_VARIABLE;
+import static seedu.moneymind.UserDate.updateDate;
+import static seedu.moneymind.string.Strings.NEW_LINE;
+
+/**
+ * Converts the String representation of the ArrayList of categories to an ArrayList of categories.
+ */
+public class StringToCategories {
+
+ /**
+ * Converts the String representation of the ArrayList of categories to an ArrayList of categories.
+ *
+ * @param savedCategories The String representation of the ArrayList of categories to be converted.
+ * @return The ArrayList of categories.
+ * @throws IllegalArgumentException If the String representation of the ArrayList of categories is invalid.
+ */
+ public static ArrayList stringToCategories(String savedCategories) throws IllegalArgumentException {
+ String[] savedStrings = savedCategories.split(NEW_LINE);
+ ArrayList categories = new ArrayList<>();
+ for (String savedLine : savedStrings) {
+ if (savedLine.startsWith(STORAGE_CATEGORY_NAME)) {
+ categories.add(loadCategory(savedLine));
+ } else if (savedLine.startsWith(STORAGE_NEXT_VARIABLE)) {
+ categories.get(categories.size() - 1).addEvent(loadEvent(savedLine));
+ } else if (!savedLine.equals("")) {
+ throw new IllegalArgumentException("Invalid format found in storage file.");
+ }
+ }
+ return categories;
+ }
+
+ /**
+ * Converts the String representation of an event to an event.
+ *
+ * @param savedLine The String representation of the event to be converted.
+ * @return The event.
+ * @throws IllegalArgumentException If the String representation of the event is invalid.
+ */
+ private static Event loadEvent(String savedLine) throws IllegalArgumentException {
+ String[] savedEvent = savedLine.split(STORAGE_NEXT_VARIABLE);
+ Boolean isOneTimeExpense = Boolean.parseBoolean(savedEvent[4]);
+ Event eventToBeAdded;
+ if (isOneTimeExpense) {
+ eventToBeAdded = new Event(savedEvent[1], Integer.parseInt(savedEvent[2]));
+ eventToBeAdded.setTime(savedEvent[3]);
+ return eventToBeAdded;
+ } else if (!isOneTimeExpense) {
+ return new Event(savedEvent[1], Integer.parseInt(savedEvent[2]), updateDate(savedEvent[3]));
+ } else {
+ throw new IllegalArgumentException("Invalid event format in storage file.");
+ }
+ }
+
+ /**
+ * Converts the String representation of a category to a category.
+ *
+ * @param savedLine The String representation of the category to be converted.
+ * @return The category.
+ * @throws IllegalArgumentException If the String representation of the category is invalid.
+ */
+ private static Category loadCategory(String savedLine) throws IllegalArgumentException {
+ String[] savedCategory = savedLine.split(STORAGE_CATEGORY_NAME);
+ if (savedCategory.length == 2) {
+ return new Category(savedCategory[1]);
+ } else if (savedCategory.length == 3) {
+ return new Category(savedCategory[1], Integer.parseInt(savedCategory[2]));
+ } else {
+ throw new IllegalArgumentException("Invalid category format in storage file.");
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/moneymind/string/Strings.java b/src/main/java/seedu/moneymind/string/Strings.java
new file mode 100644
index 0000000000..fc06d60312
--- /dev/null
+++ b/src/main/java/seedu/moneymind/string/Strings.java
@@ -0,0 +1,165 @@
+package seedu.moneymind.string;
+
+public class Strings {
+
+ public static final String BIG_WHITE_SPACE = " ";
+ public static final String EXCEEDED_BUDGET_WARNING_MESSAGE = "Reminder: The total one time expense" +
+ " have exceeded the budget for this category!";
+ public static final String TYPING_NEW_EXPENSE_MESSAGE = "Your new expense would be:";
+ public static final String TYPING_NEW_BUDGET_MESSAGE = "Your new budget would be:";
+ public static final String NEW_EXPENSE_MESSAGE = "Ok, the new expense is now changed to: ";
+ public static final String NEW_BUDGET_MESSAGE = "Ok, the new budget is now changed to: ";
+ public static final String NEW_CATEGORY_ADDED_MESSAGE = "New category added: ";
+ public static final String EVENT_DELETION_MESSAGE = "Event deleted: ";
+ public static final String CATEGORY_DELETION_MESSAGE = "Category deleted: ";
+ public static final String NON_EXISTENT_EVENT = "Event does not exist";
+ public static final String ENTERING_VALID_TIME_FORMAT_MESSAGE = "Please enter a valid time in the format of " +
+ "dd/mm/yyyy hh:mm";
+ public static final String NEGATIVE_INTEGER_DETECTING_REGEX = "^-\\d+";
+ public static final String ZERO_MATCHING_REGEX = "^0";
+ public static final String INTEGER_DETECTING_REGEX = "\\d+";
+ public static final String LOAD_ERROR_RISK_MESSAGE =
+ "Please correct the save file and restart the program, or risk data loss.";
+ public static final String ERROR_LOADING_FILE = "Error loading file. ";
+ public static final String LOGO = " __ __ __ __ _ _ \n" +
+ " | \\/ | | \\/ (_) | |\n" +
+ " | \\ / | ___ _ __ ___ _ _| \\ / |_ _ __ __| |\n" +
+ " | |\\/| |/ _ \\| '_ \\ / _ \\ | | | |\\/| | | '_ \\ / _` |\n" +
+ " | | | | (_) | | | | __/ |_| | | | | | | | | (_| |\n" +
+ " |_| |_|\\___/|_| |_|\\___|\\__, |_| |_|_|_| |_|\\__,_|\n" +
+ " __/ | \n" +
+ " |___/ \n";
+ public static final String GREETING = "Welcome to Moneymind\n" + LOGO + "How may I help you?";
+ public static final String OFFER_HELP = "Type 'help' to see the details of all the commands.";
+ public static final String OFFER_HELP_FOR_COMMAND =
+ "Type 'summary' to see the summary of all the commands you can use.\n" +
+ "Type 'help' to see the details of all the commands.";
+ public static final String ERROR = "OOPS!!! I'm sorry, but I don't know what that means :-(";
+ public static final String DATE_FORMAT = "dd/MM/yyyy HH:mm";
+ public static final String DATA_FILE = "EventList.txt";
+ public static final String BUDGET_LIMIT_MESSAGE =
+ "The budget limit is 999999999$, please try to give a smaller budget";
+ public static final String INDEX_LIMIT_MESSAGE =
+ "The limit for index is 999999999, please try to give a smaller index";
+ public static final String EXPENSE_LIMIT_MESSAGE =
+ "The expense limit is 999999999$, please try to give a smaller expense";
+ public static final String NO_CATEGORY_MESSAGE = "Sorry, the category you are looking for does not exist";
+ public static final String DOT = ".";
+ public static final String NO_CATEGORIES_TO_VIEW = "There are no categories to view";
+ public static final String NO_SEARCH_RESULTS = "Sorry, there is matching search results.";
+ public static final String NULL_CATEGORY_ASSERTION = "Category name should not be null";
+ public static final String NULL_CATEGORY_LIST_ASSERTION = "Category list should not be null";
+ public static final String INTRODUCTION_HELP_COMMAND = "Here are the commands you can use:";
+ public static final String SUMMARY = "summary";
+ public static final String SHOW_SUMMARY = "Here are the commands you can use:\n";
+ public static final String SUMMARY_LIST = SHOW_SUMMARY + "1. help\n" +
+ "2. summary\n" +
+ "3. category\n" +
+ "4. event\n" +
+ "5. view\n" +
+ "6. edit\n" +
+ "7. delete\n" +
+ "8. search\n" +
+ "9. bye\n";
+ public static final String HELP_INSTRUCTION = "1. help - show detailed instructions on how to use the app\n"
+ + "Format: help\n" + "Example: help\n";
+ public static final String SUMMARY_INSTRUCTION = "2. summary - show a summary of the commands that you can use\n"
+ + "Format: summary\n" + "Example: summary\n";
+ public static final String CATEGORY_INSTRUCTION = "3. category - add a category to your list\n" +
+ "Format: category [(optional) b/]\n" + "Example: category food b/2000\n";
+ public static final String EVENT_INSTRUCTION = "4. event - add an event to a category\n" +
+ "Format: event e/ [(optional) t/]\n" +
+ "Example: event lunch e/10 t/01/01/2020 12:00\n" +
+ "(time is optional and the format is dd/mm/yyyy hh:mm)\n";
+ public static final String VIEW_INSTRUCTION =
+ "5. view - view all the events in a category or all the categories\n" +
+ "You can view all the events in a category by specifying the category name\n" +
+ "Format: view \n" + "Example: view food\n" +
+ "(category name is optional and if you do not enter a category name, all the categories will be shown)\n";
+ public static final String EDIT_INSTRUCTION = "6. edit - edit the expense for an event or budget for a category\n" +
+ "Format: edit c/ [(optional) e/]\n" +
+ "Example: edit c/food e/1\n";
+ public static final String DELETE_INSTRUCTION = "7. delete - delete an event or a category\n" +
+ "Format: delete c/ [(optional) e/]\n" +
+ "Example: delete c/food e/1\n" + "Example: delete c/food\n";
+ public static final String SEARCH_INSTRUCTION = "8. search - search for matching events and categories\n" +
+ "Format: search \n" +
+ "Example: search bill\n";
+ public static final String BYE_INSTRUCTION = "9. bye - exit the app\n" + "Format: bye\n" + "Example: bye\n";
+ public static final String SELECTING_CATEGORY_MESSAGE = "Please select the category you want to add the event to:";
+ public static final String GO_BACK_MESSAGE = "Please try again or enter back to go back to the main program";
+ public static final String EDIT_EXPENSE_LIMIT_MESSAGE = "The expense limit is 999999999$\n" + GO_BACK_MESSAGE;
+ public static final String EDIT_BUDGET_LIMIT_MESSAGE = "The budget limit is 999999999$\n" + GO_BACK_MESSAGE;
+ public static final String BACK = "back";
+ public static final String EVENT_ADDED_MESSAGE = "New event added: ";
+ public static final String NULL_EVENT_ASSERTION = "Event name cannot be null";
+ public static final String NON_NEGATIVE_EXPENSE_ASSERTION = "Expense cannot be negative";
+ public static final String NO_EVENTS_IN_THIS_CATEGORY_MESSAGE = "Oops! You have no events in this category.";
+ public static final String SHOW_CATEGORY_MESSAGE = "Here are all the categories in your list:\n";
+ public static final String BYE_MESSAGE = "Bye. Hope to see you again soon!";
+ public static final String HORIZONTAL_LINE = "____________________________________________________________";
+ public static final String WHITE_SPACE = " ";
+ public static final String BYE = "bye";
+ public static final String VIEW = "view";
+ public static final String DELETE = "delete";
+ public static final String EVENT = "event";
+ public static final String CATEGORY = "category";
+ public static final String SEARCH = "search";
+ public static final String INVALID_INPUT = "OOPS!!! I'm sorry, but I don't know what that means :-(";
+ public static final String DELETE_FORMAT = "Please following the correct format: " +
+ "delete c/ [(optional) e/]";
+ public static final String REMINDING_MESSAGE_ABOUT_NOT_LETTING_EMPTY = "Remember do not leave any things " +
+ "inside the brackets empty!";
+ public static final String EMPTY_DELETION = "OOPS!!! The description of a delete cannot be empty.";
+ public static final String SUBTLE_BUG_MESSAGE = "OOPS!!! Something went wrong, please report to the developer.";
+ public static final String EVENT_REGEX = "^(?[^/]+)(?:\\s+e\\/(?[^/]+))(?:\\s+t\\/(?.+))?$";
+ public static final String CATEGORY_REGEX = "^(?[^/]+?)(?:\\s+b\\/(?[^/]+))?$";
+ public static final String EDIT_REGEX = "^c\\/(?=\\S)([^/]*?)(?:\\s+e\\/([^/]+))?\\s*$";
+ public static final String DELETE_REGEX = "^c\\/(?=\\S)([^/]*?)(?:\\s+e\\/([^/]+))?\\s*$";
+ public static final String EVENT_FORMAT = "Please following the correct format: " +
+ "event e/ [(optional) t/]";
+ public static final String CATEGORY_FORMAT = "Please following the correct format: " +
+ "category [(optional) b/]";
+ public static final String EDIT_FORMAT = "Please following the correct format: " +
+ "edit c/ [(optional) e/]";
+ public static final String EMPTY_DESCRIPTION_FOR_EVENT = "OOPS!!! The description of an event cannot be empty.";
+ public static final String EMPTY_DESCRIPTION_FOR_CATEGORY =
+ "OOPS!!! The description of a category cannot be empty.";
+ public static final String NULL_INPUT_ASSERTION = "Input cannot be null";
+ public static final String STORAGE_NEXT_VARIABLE = "&&next_detail&&";
+ public static final String NEW_LINE = System.lineSeparator();
+ public static final String STORAGE_CATEGORY_NAME = "&&new_category&&";
+ public static final String STORAGE_DEFAULT_STRING = "&&default_value&&";
+ public static final String EXISTED_CATEGORY = "Category already exists";
+ public static final String EXTRA_SPACE_REGEX_FORMAT = "\\s+";
+ public static final String HELP = "help";
+ public static final String NO_DESCRIPTION_FOR_BYE = "Bye command should not have any description";
+ public static final String NO_DESCRIPTION_FOR_HELP = "Help command should not have any description";
+ public static final String NO_DESCRIPTION_FOR_SUMMARY = "Summary command should not have any description";
+ public static final String POSITIVE_INTEGER_FOR_EVENT_INDEX = "Please give a positive integer for event index";
+ public static final String NON_NEGATIVE_INTEGER_FOR_EXPENSE = "Please give a non-negative integer for expense";
+ public static final String EMPTY_DESCRIPTION_FOR_EDIT = "OOPS!!! The description of an edit cannot be empty.";
+ public static final String EMPTY_STRING = "";
+ public static final String NON_NEGATIVE_INTEGER_FOR_BUDGET = "Please give a non-negative integer for budget";
+ public static final String EDIT = "edit";
+ public static final String CATEGORY_DOES_NOT_EXIST_MESSAGE = "Category does not exist!";
+ public static final String ENTERING_NON_NEGATIVE_NUMBER_MESSAGE =
+ "Please enter a non-negative number or enter back to go back to the main program";
+ public static final String NO_SEARCH_KEYWORD_MESSAGE = "Please use the format: search ";
+
+ /**
+ * Returns the input string with all the storage delimiter replaced with the default string.
+ *
+ * @param input The input string.
+ * @return The input string with all the storage delimiter replaced with the default string.
+ */
+ public static String checkForStorageDelimiter(String input) {
+ if (input.contains(STORAGE_CATEGORY_NAME) || input.contains(STORAGE_NEXT_VARIABLE)) {
+ System.out.println("Storage delimiter detected and replaced with: &&default_value&&");
+ return input.replace(STORAGE_CATEGORY_NAME, STORAGE_DEFAULT_STRING)
+ .replace(STORAGE_NEXT_VARIABLE, STORAGE_DEFAULT_STRING);
+ } else {
+ return input;
+ }
+ }
+}
diff --git a/src/main/java/seedu/moneymind/ui/Ui.java b/src/main/java/seedu/moneymind/ui/Ui.java
new file mode 100644
index 0000000000..6f1d8de2fa
--- /dev/null
+++ b/src/main/java/seedu/moneymind/ui/Ui.java
@@ -0,0 +1,135 @@
+package seedu.moneymind.ui;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.command.CategoryCommand;
+import seedu.moneymind.event.Event;
+
+import static seedu.moneymind.command.SearchCommand.getCategoryOfEvent;
+import static seedu.moneymind.string.Strings.HORIZONTAL_LINE;
+import static seedu.moneymind.string.Strings.GREETING;
+import static seedu.moneymind.string.Strings.OFFER_HELP_FOR_COMMAND;
+import static seedu.moneymind.string.Strings.BYE_MESSAGE;
+import static seedu.moneymind.string.Strings.ERROR_LOADING_FILE;
+import static seedu.moneymind.string.Strings.LOAD_ERROR_RISK_MESSAGE;
+import static seedu.moneymind.string.Strings.ERROR;
+import static seedu.moneymind.string.Strings.NO_SEARCH_RESULTS;
+import static seedu.moneymind.string.Strings.DOT;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Interacts with the user via the command line.
+ */
+public class Ui {
+
+ public void greet() {
+ System.out.println(GREETING);
+ System.out.println(OFFER_HELP_FOR_COMMAND);
+ }
+
+ public void goodbye() {
+ System.out.println(BYE_MESSAGE);
+ System.out.println(HORIZONTAL_LINE);
+ }
+
+ public void loadingError(Exception e) {
+ System.out.println(HORIZONTAL_LINE + ERROR_LOADING_FILE + e.getMessage());
+ System.out.println(LOAD_ERROR_RISK_MESSAGE);
+ }
+
+ /**
+ * Prints the error message when an exception is caught.
+ * @param error Exception object containing the error message.
+ */
+ public void error(Exception error) {
+ System.out.println(ERROR + "\n" + error);
+ }
+
+ /**
+ * Prints search results for categories that match the query string.
+ * @param matchingCategories Set containing categories that match the query string.
+ */
+ public static void printMatchingCategories(Set matchingCategories) {
+ System.out.println("Matching Categories:");
+
+ if (matchingCategories.size() == 0) {
+ System.out.println(NO_SEARCH_RESULTS);
+ }
+
+ for (Category category : matchingCategories) {
+ int categoryIndex = CategoryCommand.categoryMap.get(category.getName());
+ System.out.println((categoryIndex + 1) + DOT + category.getName());
+ }
+ }
+
+ /**
+ * Prints search results for events that match the query string.
+ * @param matchingEvents Set containing events that match the query string.
+ */
+ public static void printMatchingEvents(Set matchingEvents) {
+ System.out.println("\nMatching Events:");
+
+ if (matchingEvents.size() == 0) {
+ System.out.println(NO_SEARCH_RESULTS);
+ }
+
+ for (Event event : matchingEvents) {
+ Category categoryOfEvent = getCategoryOfEvent(event);
+ assert categoryOfEvent != null;
+ String categoryName = categoryOfEvent.getName();
+ System.out.println(event.getDescription() + " (Category: " + categoryName + ")");
+ }
+ }
+
+ /**
+ * Prints search results for categories that are similar to the query string.
+ * @param similarCategoriesList Array list containing the similar categories, sorted by similarity distance.
+ */
+ public static void printSimilarCategories(ArrayList similarCategoriesList) {
+ System.out.println("\nSimilar Categories:");
+
+ int similarCategoryCount = 3;
+ if (similarCategoriesList.size() < 3) {
+ similarCategoryCount = similarCategoriesList.size();
+ }
+
+ if (similarCategoryCount == 0) {
+ System.out.println(NO_SEARCH_RESULTS);
+ return;
+ }
+
+ for (int i = 0; i < similarCategoryCount; i++) {
+ Category category = similarCategoriesList.get(i);
+ int categoryIndex = CategoryCommand.categoryMap.get(category.getName());
+ System.out.println((categoryIndex + 1) + DOT + category.getName());
+ }
+ }
+
+ /**
+ * Prints search results for events that are similar to the query string.
+ * @param similarEventsList Array list containing the similar events, sorted by similarity distance.
+ */
+ public static void printSimilarEvents(ArrayList similarEventsList) {
+ System.out.println("\nSimilar Events:");
+
+ int similarEventsCount = 3;
+ if (similarEventsList.size() < 3) {
+ similarEventsCount = similarEventsList.size();
+ }
+
+ if (similarEventsCount == 0) {
+ System.out.println(NO_SEARCH_RESULTS);
+ return;
+ }
+
+ for (int i = 0; i < similarEventsCount; i++) {
+ Event event = similarEventsList.get(i);
+ Category categoryOfEvent = getCategoryOfEvent(event);
+ assert categoryOfEvent != null;
+ String categoryName = categoryOfEvent.getName();
+ System.out.println(event.getDescription() + " (Category: " + categoryName + ")");
+ }
+ }
+
+}
diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java
deleted file mode 100644
index 2dda5fd651..0000000000
--- a/src/test/java/seedu/duke/DukeTest.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package seedu.duke;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.Test;
-
-class DukeTest {
- @Test
- public void sampleTest() {
- assertTrue(true);
- }
-}
diff --git a/src/test/java/seedu/moneymind/ReminderTest.java b/src/test/java/seedu/moneymind/ReminderTest.java
new file mode 100644
index 0000000000..4bedf54070
--- /dev/null
+++ b/src/test/java/seedu/moneymind/ReminderTest.java
@@ -0,0 +1,41 @@
+package seedu.moneymind;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.moneymind.Reminder.checkCategoryReminder;
+import static seedu.moneymind.string.Strings.NEW_LINE;
+import static seedu.moneymind.string.Strings.HORIZONTAL_LINE;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+public class ReminderTest {
+
+ private ArrayList storageTestData() {
+ LocalDateTime now = LocalDateTime.now().plusDays(2);
+ String date = now.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"));
+ ArrayList storageTestData = new ArrayList();
+ Category food = new Category("food");
+ food.addEvent(new Event("McDonalds", 10));
+ food.addEvent(new Event("KFC", 20, "13/02/2024 12:00"));
+ storageTestData.add(food);
+ Category transport = new Category("transport");
+ transport.addEvent(new Event("Grab", 10, "31/01/2023 12:00"));
+ transport.addEvent(new Event("Uber", 20, date));
+ storageTestData.add(transport);
+ return storageTestData;
+ }
+
+ @Test
+ void checkCategoryReminder_input_output() {
+ String expectedOutput = "Approaching expenses:" + NEW_LINE
+ + "transport has an event: Uber in 1 day" + NEW_LINE;
+ String actualOutput = checkCategoryReminder(storageTestData());
+ assertEquals((HORIZONTAL_LINE + NEW_LINE + expectedOutput), actualOutput, actualOutput);
+ }
+}
diff --git a/src/test/java/seedu/moneymind/category/CategoryTest.java b/src/test/java/seedu/moneymind/category/CategoryTest.java
new file mode 100644
index 0000000000..5e1251052b
--- /dev/null
+++ b/src/test/java/seedu/moneymind/category/CategoryTest.java
@@ -0,0 +1,42 @@
+package seedu.moneymind.category;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.moneymind.event.Event;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CategoryTest {
+
+ Event event1 = new Event("McDonalds", 10);
+ Event event2 = new Event("KFC", 20);
+ Category food = new Category("food");
+ Category transport = new Category("transport", 100);
+
+ /**
+ * Sets up the test data.
+ */
+ private void setup() {
+ food.addEvent(event1);
+ food.addEvent(event2);
+ }
+
+ @Test
+ void getName() {
+ setup();
+ assertEquals("food", food.getName());
+ }
+
+ @Test
+ void getBudget() {
+ setup();
+ assertEquals(0, food.getBudget());
+ assertEquals(100, transport.getBudget());
+ }
+
+ @Test
+ void getTotalExpense() {
+ setup();
+ assertEquals(30, food.getTotalOneTimeExpense());
+ }
+}
diff --git a/src/test/java/seedu/moneymind/commands/ByeCommandTest.java b/src/test/java/seedu/moneymind/commands/ByeCommandTest.java
new file mode 100644
index 0000000000..4ec249c94d
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/ByeCommandTest.java
@@ -0,0 +1,23 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ByeCommandTest extends CommandTest {
+
+ public ByeCommandTest() {
+ super();
+ }
+
+ @Test
+ void byeCommand_typeBye_expectByeMessage() {
+ setup();
+ String terminalOutput = executeInput("bye").toString();
+ assertEquals("Bye. Hope to see you again soon!" + System.lineSeparator()
+ + "____________________________________________________________"
+ + System.lineSeparator(), terminalOutput);
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/CategoryCommandTest.java b/src/test/java/seedu/moneymind/commands/CategoryCommandTest.java
new file mode 100644
index 0000000000..1e607d6a41
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/CategoryCommandTest.java
@@ -0,0 +1,106 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+import seedu.moneymind.category.CategoryList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class CategoryCommandTest extends CommandTest {
+
+ public CategoryCommandTest() {
+ super();
+ }
+
+ @Test
+ void addCategory_oneCategoryAndNoBudget_expectThreeCategoryInCategoryList() {
+ setup();
+ executeInput("category travel");
+ assertEquals(3, CategoryList.categories.size());
+ assertEquals("travel", CategoryList.categories.get(2).getName());
+ assertEquals(0, CategoryList.categories.get(2).getBudget());
+ clear();
+ }
+
+ @Test
+ void addCategory_sameCategory_expectCategoryExistsMessage() {
+ setup();
+ String terminalOutput = executeInput("category food").toString();
+ assertEquals("Category already exists" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ assertEquals("food", CategoryList.categories.get(0).getName());
+ clear();
+ }
+
+ @Test
+ void addCategory_dummyBudget_expectGivingPositiveIntegerMessage() {
+ setup();
+ String terminalOutput = executeInput("category travel b/ad").toString();
+ assertEquals("Please give a non-negative integer for budget" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ assertEquals("food", CategoryList.categories.get(0).getName());
+ clear();
+ }
+
+ @Test
+ void addCategory_negativeBudget_expectGivingPositiveIntegerMessage() {
+ setup();
+ String terminalOutput = executeInput("category travel b/-12").toString();
+ assertEquals("Please give a non-negative integer for budget" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ clear();
+ }
+
+ @Test
+ void addCategory_oneCategoryWithBudget_expectThreeCategoryInCategoryList() {
+ setup();
+ executeInput("category travel b/100");
+ assertEquals(3, CategoryList.categories.size());
+ assertEquals("travel", CategoryList.categories.get(2).getName());
+ assertEquals(100, CategoryList.categories.get(2).getBudget());
+ clear();
+ }
+
+ @Test
+ void addCategory_emptyDescription_expectEmptyDescriptionMessage() {
+ setup();
+ String terminalOutput = executeInput("category").toString();
+ assertEquals("OOPS!!! The description of a category cannot be empty."
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ clear();
+ }
+
+ @Test
+ void addCategory_emptyCategoryName_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("category b/100").toString();
+ assertEquals("Please following the correct format: category [(optional) b/]\n" +
+ "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ clear();
+ }
+
+ @Test
+ void addCategory_emptyBudget_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("category travel b/").toString();
+ assertEquals("Please following the correct format: category [(optional) b/]\n" +
+ "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ clear();
+ }
+
+ @Test
+ void addCategory_spareSlashSpecifier_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("category fsd / b/123").toString();
+ assertEquals("Please following the correct format: category [(optional) b/]\n" +
+ "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/CommandTest.java b/src/test/java/seedu/moneymind/commands/CommandTest.java
new file mode 100644
index 0000000000..c89ae638c7
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/CommandTest.java
@@ -0,0 +1,67 @@
+package seedu.moneymind.commands;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.command.CategoryCommand;
+import seedu.moneymind.parser.Parser;
+import seedu.moneymind.event.Event;
+import seedu.moneymind.exceptions.InvalidCommandException;
+import seedu.moneymind.ui.Ui;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+public class CommandTest {
+ Category food;
+ Category book;
+ Event salad;
+ Event pizza;
+ Event harryPotter;
+ Event lordOfTheRings;
+
+ public CommandTest() {
+ food = new Category("food");
+ book = new Category("book");
+ salad = new Event("salad", 100);
+ pizza = new Event("pizza", 200);
+ harryPotter = new Event("Harry Potter", 70);
+ lordOfTheRings = new Event("Lord of the Rings", 90);
+ }
+
+ public void setup() {
+ food.addEvent(salad);
+ food.addEvent(pizza);
+ book.addEvent(harryPotter);
+ book.addEvent(lordOfTheRings);
+ CategoryList.categories.add(food);
+ CategoryList.categories.add(book);
+ CategoryCommand.categoryMap.put("food", 0);
+ CategoryCommand.categoryMap.put("book", 1);
+ }
+
+ public ByteArrayOutputStream executeInput(String input) {
+ Ui ui = new Ui();
+ Parser parser = new Parser();
+ // Get help from chatGPT for the next 2 lines
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(outputStream));
+ try {
+ seedu.moneymind.command.Command command = parser.parseNextCommand(input);
+ command.execute(ui);
+ } catch (InvalidCommandException error) {
+ error.showErrorMessage();
+ } catch (Exception e) {
+ // no code needed here
+ }
+ return outputStream;
+ }
+
+ /**
+ * Clears all static variables
+ */
+ public static void clear() {
+ CategoryList.categories.clear();
+ CategoryCommand.categoryMap.clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/DeleteCommandTest.java b/src/test/java/seedu/moneymind/commands/DeleteCommandTest.java
new file mode 100644
index 0000000000..7aa2e0c191
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/DeleteCommandTest.java
@@ -0,0 +1,111 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+import seedu.moneymind.category.CategoryList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class DeleteCommandTest extends CommandTest {
+
+ public DeleteCommandTest() {
+ super();
+ }
+
+ @Test
+ void deleteWholeCategory_oneCategory_expectOneCategoryInList() {
+ setup();
+ executeInput("delete c/food");
+ assertEquals(1, CategoryList.categories.size());
+ assertEquals("book", CategoryList.categories.get(0).getName());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_oneBookEvent_expectOneEventInBookCategory() {
+ setup();
+ executeInput("delete c/book e/1");
+ assertEquals(1, book.events.size());
+ assertEquals("Lord of the Rings", book.events.get(0).getDescription());
+ clear();
+ }
+
+ @Test
+ void deleteWholeCategory_nonExistedCategory_expectCategoryDoesNotExistMessage() {
+ setup();
+ String terminalOutput = executeInput("delete c/travel").toString();
+ assertEquals("Sorry, the category you are looking for does not exist" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, CategoryList.categories.size());
+ clear();
+ }
+
+ @Test
+ void deleteCommand_emptyDescription_expectEmptyDeleteMessage() {
+ setup();
+ String terminalOutput = executeInput("delete").toString();
+ assertEquals("OOPS!!! The description of a delete cannot be empty." +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_emptyCategory_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("delete c/ e/1").toString();
+ assertEquals("Please following the correct format: delete c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_outOfBoundEventIndex_expectEventDoesNotExist() {
+ setup();
+ String terminalOutput = executeInput("delete c/food e/3").toString();
+ assertEquals("Event does not exist" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_dummyInputForEventIndex_expectPositiveIntegerForEventIndexMessage() {
+ setup();
+ String terminalOutput = executeInput("delete c/food e/abc").toString();
+ assertEquals("Please give a positive integer for event index" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_negativeEventIndex_expectPositiveIntegerForEventIndexMessage() {
+ setup();
+ String terminalOutput = executeInput("delete c/food e/-1").toString();
+ assertEquals("Please give a positive integer for event index" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_emptyEventIndex_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("delete c/food e/").toString();
+ assertEquals("Please following the correct format: delete c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void deleteEvent_spareSlash_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("delete c/food/ e/1").toString();
+ assertEquals("Please following the correct format: delete c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/EditCommandTest.java b/src/test/java/seedu/moneymind/commands/EditCommandTest.java
new file mode 100644
index 0000000000..cb88e8740c
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/EditCommandTest.java
@@ -0,0 +1,148 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+import seedu.moneymind.Moneymind;
+import seedu.moneymind.string.Strings;
+
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class EditCommandTest extends CommandTest {
+
+ public EditCommandTest() {
+ super();
+ }
+
+ @Test
+ void editEvent_oneEvent_expectEventEdited() {
+ setup();
+ String newExpense = "12" + Strings.NEW_LINE;
+ Moneymind.in = new Scanner(newExpense);
+ String terminalOutput = executeInput("edit c/food e/1").toString();
+ assertEquals("The current event expense for salad is: 100" + System.lineSeparator() +
+ "Your new expense would be:" + System.lineSeparator() +
+ "Ok, the new expense is now changed to: 12" + System.lineSeparator(), terminalOutput);
+ assertEquals(12, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_dummyInput_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("edit hsadis").toString();
+ assertEquals("Please following the correct format: edit c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!" +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_emptyDescription_expectEmptyEditCommandMessage() {
+ setup();
+ String terminalOutput = executeInput("edit").toString();
+ assertEquals("OOPS!!! The description of an edit cannot be empty." +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_emptyCategory_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/ e/1").toString();
+ assertEquals("Please following the correct format: edit c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!" +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_emptyEventIndex_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/food e/").toString();
+ assertEquals("Please following the correct format: edit c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!" +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_negativeEventIndex_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/food e/-1").toString();
+ assertEquals("Please give a positive integer for event index" + System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_dummyEventIndex_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/food e/abc").toString();
+ assertEquals("Please give a positive integer for event index" + System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_nonExistedEventIndex_expectEventDoesNotExistMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/food e/3").toString();
+ assertEquals("Event does not exist" + System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_nonExistedCategory_expectCategoryDoesNotExistMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/travel e/1").toString();
+ assertEquals("Sorry, the category you are looking for does not exist" + System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_negativeEditAmount_expectPositiveNumberMessage() {
+ setup();
+ String newExpense = "-12" + Strings.NEW_LINE;
+ Moneymind.in = new Scanner(newExpense);
+ String terminalOutput = executeInput("edit c/food e/1").toString();
+ assertEquals("The current event expense for salad is: 100" + System.lineSeparator() +
+ "Your new expense would be:" + System.lineSeparator() +
+ "Please enter a non-negative number or enter back to go back to the main program"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_dummyEditAmount_expectNumberMessage() {
+ setup();
+ String newExpense = "asfd" + Strings.NEW_LINE;
+ Moneymind.in = new Scanner(newExpense);
+ String terminalOutput = executeInput("edit c/food e/1").toString();
+ assertEquals("The current event expense for salad is: 100" + System.lineSeparator() +
+ "Your new expense would be:" + System.lineSeparator() +
+ "Please enter a non-negative number or enter back to go back to the main program"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+ @Test
+ void editEvent_spareSlash_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("edit c/food/ e/1").toString();
+ assertEquals("Please following the correct format: edit c/ [(optional) e/]\n"
+ + "Remember do not leave any things inside the brackets empty!" +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(100, food.events.get(0).getExpense());
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/EventCommandTest.java b/src/test/java/seedu/moneymind/commands/EventCommandTest.java
new file mode 100644
index 0000000000..f6730d91ba
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/EventCommandTest.java
@@ -0,0 +1,178 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+import seedu.moneymind.Moneymind;
+import seedu.moneymind.category.Category;
+import seedu.moneymind.category.CategoryList;
+import seedu.moneymind.command.CategoryCommand;
+import seedu.moneymind.string.Strings;
+
+import java.util.Scanner;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class EventCommandTest extends CommandTest {
+ public EventCommandTest() {
+ super();
+ }
+
+ @Test
+ void addEvent_oneNonRecurringEvent_expectThreeEventsInFoodCategory() {
+ setup();
+ String categoryName = "food" + Strings.NEW_LINE; // replace with the correct input string
+ Moneymind.in = new Scanner(categoryName);
+ executeInput("event banana e/20");
+ assertEquals("banana", food.events.get(2).getDescription(),
+ "expected: banana, actual: " + food.events.get(2).getDescription());
+ assertEquals(20, food.events.get(2).getExpense(),
+ "expected: 20, actual: " + food.events.get(2).getExpense());
+ clear();
+ }
+
+ @Test
+ void addEvent_oneRecurringEvent_expectThreeEventsInFoodCategory() {
+ setup();
+ String categoryName = "food" + Strings.NEW_LINE; // replace with the correct input string
+ Moneymind.in = new Scanner(categoryName);
+ executeInput("event banana e/20 t/13/02/2024 12:00");
+ assertEquals("banana", food.events.get(2).getDescription(),
+ "expected: banana, actual: " + food.events.get(2).getDescription());
+ assertEquals(20, food.events.get(2).getExpense(),
+ "expected: 20, actual: " + food.events.get(2).getExpense());
+ clear();
+ }
+
+ @Test
+ void addEvent_oneEventWithNoExpense_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("event banana").toString();
+ assertEquals("Please following the correct format: event e/ [(optional) t/]\n"
+ + "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void addEvent_emptyDescription_expectEmptyEventMessage() {
+ setup();
+ String terminalOutput = executeInput("event").toString();
+ assertEquals("OOPS!!! The description of an event cannot be empty." +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void addEvent_emptyEventName_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("event e/20").toString();
+ assertEquals("Please following the correct format: event e/ [(optional) t/]\n"
+ + "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void addEvent_emptyExpense_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("event banana e/").toString();
+ assertEquals(2, food.events.size());
+ assertEquals("Please following the correct format: event e/ [(optional) t/]\n"
+ + "Remember do not leave any things inside the brackets empty!" + System.lineSeparator(),
+ terminalOutput);
+ clear();
+ }
+
+ @Test
+ void addEvent_negativeExpense_expectGivingPositiveIntegerMessage() {
+ setup();
+ String terminalOutput = executeInput("event banana e/-20").toString();
+ assertEquals("Please give a non-negative integer for expense" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void addEvent_dummyExpense_expectGivingPositiveIntegerMessage() {
+ setup();
+ String terminalOutput = executeInput("event banana e/abc").toString();
+ assertEquals("Please give a non-negative integer for expense" + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+
+ @Test
+ void addEvent_oneEventToNewCategory_expectNewCategoryInCategoryList() {
+ setup();
+ Category travel = new Category("travel", 200);
+ CategoryList.categories.add(travel);
+ CategoryCommand.categoryMap.put("travel", 2);
+ String categoryName = "travel" + Strings.NEW_LINE; // replace with the correct input string
+ Moneymind.in = new Scanner(categoryName);
+ String terminalOutput = executeInput("event buy tent e/20").toString();
+ assertEquals("Please select the category you want to add the event to:" + System.lineSeparator() +
+ "New event added: buy tent" + System.lineSeparator(), terminalOutput);
+ assertEquals(3, CategoryList.categories.size());
+ assertEquals("buy tent", travel.events.get(0).getDescription());
+ clear();
+ }
+
+ @Test
+ void addEvent_oneEventToNewCategoryWithDummyCategoryInput_expectTryAgainMessage() {
+ setup();
+ Category travel = new Category("travel", 200);
+ CategoryList.categories.add(travel);
+ CategoryCommand.categoryMap.put("travel", 2);
+ String categoryName = "haha" + Strings.NEW_LINE; // replace with the correct input string
+ Moneymind.in = new Scanner(categoryName);
+ String terminalOutput = executeInput("event buy tent e/20").toString();
+ assertEquals("Please select the category you want to add the event to:" + System.lineSeparator() +
+ "Category does not exist!" + System.lineSeparator() +
+ "Please try again or enter back to go back to the main program" +
+ System.lineSeparator(), terminalOutput);
+ assertEquals(0, travel.events.size());
+ clear();
+ }
+
+ @Test
+ void addEvent_spareSlash_expectCorrectFormatMessage() {
+ setup();
+ String terminalOutput = executeInput("event banana/ e/20").toString();
+ assertEquals("Please following the correct format: event e/ [(optional) t/]\n"
+ + "Remember do not leave any things inside the brackets empty!"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+ @Test
+ void addEvent_oneEventWithCorrectTime_expectCorrectTime() {
+ setup();
+ String categoryName = "food" + Strings.NEW_LINE; // replace with the correct input string
+ Moneymind.in = new Scanner(categoryName);
+ executeInput("event banana e/20 t/13/02/2024 12:00");
+ assertEquals("banana", food.events.get(2).getDescription(),
+ "expected: banana, actual: " + food.events.get(2).getDescription());
+ assertEquals(20, food.events.get(2).getExpense(),
+ "expected: 20, actual: " + food.events.get(2).getExpense());
+ assertEquals("13/02/2024 12:00", food.events.get(2).getTime(),
+ "expected: 13/02/2024 12:00, actual: " + food.events.get(2).getTime());
+ clear();
+ }
+
+ @Test
+ void addEvent_oneEventWithIncorrectTime_expectTimeFormatMessage() {
+ setup();
+ String categoryName = "food" + Strings.NEW_LINE; // replace with the correct input string
+ Moneymind.in = new Scanner(categoryName);
+ String terminalOutput = executeInput("event banana e/20 t/13/02/2024 12:00:00").toString();
+ assertEquals("Please enter a valid time in the format of dd/mm/yyyy hh:mm"
+ + System.lineSeparator(), terminalOutput);
+ assertEquals(2, food.events.size());
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/HelpCommandTest.java b/src/test/java/seedu/moneymind/commands/HelpCommandTest.java
new file mode 100644
index 0000000000..9e96ca14f0
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/HelpCommandTest.java
@@ -0,0 +1,54 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HelpCommandTest extends CommandTest {
+
+ public HelpCommandTest() {
+ super();
+ }
+
+ @Test
+ void helpCommand_typeHelp_expectHelpMessage() {
+ setup();
+ String terminalOutput = executeInput("help").toString();
+ String expected = "Here are the commands you can use:" + System.lineSeparator()
+ + "1. help - show detailed instructions on how to use the app\n" +
+ "Format: help\n" + "Example: help\n" + System.lineSeparator()
+ + "2. summary - show a summary of the commands that you can use\n" +
+ "Format: summary\n" + "Example: summary\n" + System.lineSeparator()
+ + "3. category - add a category to your list\n" +
+ "Format: category [(optional) b/]\n" +
+ "Example: category food b/2000\n" + System.lineSeparator()
+ + "4. event - add an event to a category\n" +
+ "Format: event e/ [(optional) t/]\n" +
+ "Example: event lunch e/10 t/01/01/2020 12:00\n" +
+ "(time is optional and the format is dd/mm/yyyy hh:mm)\n"
+ + System.lineSeparator()
+ + "5. view - view all the events in a category or all the categories\n" +
+ "You can view all the events in a category by specifying the category name\n" +
+ "Format: view \n" +
+ "Example: view food\n" +
+ "(category name is optional and if you do not enter a category name, " +
+ "all the categories will be shown)\n"
+ + System.lineSeparator()
+ + "6. edit - edit the expense for an event or budget for a category\n" +
+ "Format: edit c/ [(optional) e/]\n" +
+ "Example: edit c/food e/1\n" + System.lineSeparator()
+ + "7. delete - delete an event or a category\n" +
+ "Format: delete c/ [(optional) e/]\n" +
+ "Example: delete c/food e/1\n" +
+ "Example: delete c/food\n" + System.lineSeparator() +
+
+ "8. search - search for matching events and categories\n" +
+ "Format: search \n" +
+ "Example: search bill\n" + System.lineSeparator() +
+
+ "9. bye - exit the app\n" + "Format: bye\n" + "Example: bye\n" + System.lineSeparator();
+ assertEquals(expected, terminalOutput);
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/commands/SearchCommandTest.java b/src/test/java/seedu/moneymind/commands/SearchCommandTest.java
new file mode 100644
index 0000000000..d3a64f8904
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/SearchCommandTest.java
@@ -0,0 +1,234 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+import seedu.moneymind.category.Category;
+import seedu.moneymind.command.SearchCommand;
+import seedu.moneymind.event.Event;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SearchCommandTest extends CommandTest {
+
+ public SearchCommandTest() {
+ super();
+ }
+
+ @Test
+ void searchCommand_assignItemsBySimilarity_matchNoCategory() {
+ String query = "test";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ assertEquals(0, matchingCategories.size());
+ assertEquals(0, matchingEvents.size());
+ assertEquals(2, similarCategories.size());
+ assertEquals(4, similarEvents.size());
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_assignItemsBySimilarity_matchOneCategory() {
+ String query = "book";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ assertEquals(1, matchingCategories.size());
+ assertEquals(0, matchingEvents.size());
+ assertEquals(1, similarCategories.size());
+ assertEquals(4, similarEvents.size());
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_assignItemsBySimilarity_matchMultipleCategory() {
+ String query = "oo";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ assertEquals(2, matchingCategories.size());
+ assertEquals(0, matchingEvents.size());
+ assertEquals(0, similarCategories.size());
+ assertEquals(4, similarEvents.size());
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_assignItemsBySimilarity_matchNoEvent() {
+ String query = "test";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ assertEquals(0, matchingCategories.size());
+ assertEquals(0, matchingEvents.size());
+ assertEquals(2, similarCategories.size());
+ assertEquals(4, similarEvents.size());
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_assignItemsBySimilarity_matchOneEvent() {
+ String query = "lord";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ assertEquals(0, matchingCategories.size());
+ assertEquals(1, matchingEvents.size());
+ assertEquals(2, similarCategories.size());
+ assertEquals(3, similarEvents.size());
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_assignItemsBySimilarity_matchMultipleEvent() {
+ String query = "e";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+
+ assertEquals(0, matchingCategories.size());
+ assertEquals(2, matchingEvents.size());
+ assertEquals(2, similarCategories.size());
+ assertEquals(2, similarEvents.size());
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_sortCategoryBySimilarity_expectSorted() {
+ String query = "test";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+ ArrayList similarCategoriesList = new ArrayList<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+ similarCategoriesList.addAll(similarCategories.keySet());
+ command.sortCategoryBySimilarity(similarCategoriesList, similarCategories);
+
+ boolean sorted = true;
+ for (int i = 0; i < similarCategoriesList.size()-1; i++) {
+ int currentCategoryValue = similarCategories.get(similarCategoriesList.get(i));
+ int nextCategoryValue = similarCategories.get(similarCategoriesList.get(i+1));
+ if (currentCategoryValue > nextCategoryValue) {
+ sorted = false;
+ }
+ }
+
+ assertEquals(2, similarCategoriesList.size());
+ assertTrue(sorted);
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_sortEventBySimilarity_expectSorted() {
+ String query = "test";
+
+ SearchCommand command = new SearchCommand(query);
+
+ Set matchingCategories = new LinkedHashSet<>();
+ Set matchingEvents = new LinkedHashSet<>();
+ HashMap similarCategories = new HashMap<>();
+ HashMap similarEvents = new HashMap<>();
+ ArrayList similarEventsList = new ArrayList<>();
+
+ setup();
+
+ command.assignItemsBySimilarity(matchingCategories, matchingEvents, similarCategories, similarEvents, query);
+ similarEventsList.addAll(similarEvents.keySet());
+ command.sortEventBySimilarity(similarEventsList, similarEvents);
+
+ boolean sorted = true;
+ for (int i = 0; i < similarEventsList.size()-1; i++) {
+ int currentEventValue = similarEvents.get(similarEventsList.get(i));
+ int nextEventValue = similarEvents.get(similarEventsList.get(i+1));
+ if (currentEventValue > nextEventValue) {
+ sorted = false;
+ }
+ }
+
+ assertEquals(4, similarEventsList.size());
+ assertTrue(sorted);
+
+ clear();
+ }
+
+ @Test
+ void searchCommand_noQuery_expectErrorMessage() {
+ setup();
+ String terminalOutput = executeInput("search").toString();
+ String expected = "Please use the format: search " + System.lineSeparator();
+ assertEquals(expected, terminalOutput);
+ clear();
+ }
+}
diff --git a/src/test/java/seedu/moneymind/commands/ViewCommandTest.java b/src/test/java/seedu/moneymind/commands/ViewCommandTest.java
new file mode 100644
index 0000000000..aa0799e950
--- /dev/null
+++ b/src/test/java/seedu/moneymind/commands/ViewCommandTest.java
@@ -0,0 +1,48 @@
+package seedu.moneymind.commands;
+
+import org.junit.jupiter.api.Test;
+import seedu.moneymind.event.Event;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ViewCommandTest extends CommandTest {
+
+ public ViewCommandTest() {
+ super();
+ }
+
+ @Test
+ void viewCommand_viewAll_expectEverythingToBePrintedOut() {
+ setup();
+ Event nutrition = new Event("nutrition", 100, "10/10/2010 12:00");
+ food.addEvent(nutrition);
+ String terminalOutput = executeInput("view").toString();
+ String expected = "Here are all the categories in your list:\n" + System.lineSeparator()
+ + "1) Category: food (budget: 0)" + System.lineSeparator()
+ + " 1." + salad + System.lineSeparator()
+ + " 2." + pizza + System.lineSeparator()
+ + " 3." + nutrition + System.lineSeparator()
+ + " Reminder: The total one time expense have exceeded the budget for this category!"
+ + System.lineSeparator()
+ + "2) Category: book (budget: 0)" + System.lineSeparator()
+ + " 1." + harryPotter + System.lineSeparator()
+ + " 2." + lordOfTheRings + System.lineSeparator()
+ + " Reminder: The total one time expense have exceeded the budget for this category!"
+ + System.lineSeparator();
+ assertEquals(expected, terminalOutput);
+ clear();
+ }
+
+ @Test
+ void viewCommand_viewOneCategory_expectCategoryToBePrintedOut() {
+ setup();
+ String terminalOutput = executeInput("view food").toString();
+ String expected = "1." + salad + System.lineSeparator()
+ + "2." + pizza + System.lineSeparator()
+ + "Reminder: The total one time expense have exceeded the budget for this category!"
+ + System.lineSeparator();
+ assertEquals(expected, terminalOutput);
+ clear();
+ }
+
+}
diff --git a/src/test/java/seedu/moneymind/event/EventTest.java b/src/test/java/seedu/moneymind/event/EventTest.java
new file mode 100644
index 0000000000..3f63d2a81c
--- /dev/null
+++ b/src/test/java/seedu/moneymind/event/EventTest.java
@@ -0,0 +1,52 @@
+package seedu.moneymind.event;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class EventTest {
+ private static final String DD_MM_YYYY = "dd/MM/yyyy HH:mm";
+ Event salad = new Event("salad", 100);
+ Event pizza = new Event("pizza", 200, "10/10/2010");
+ Event harryPotter = new Event("Harry Potter", 70);
+ Event lordOfTheRings = new Event("Lord of the Rings", 90);
+
+ @Test
+ void getDescription() {
+ assertEquals("10/10/2010", pizza.getTime());
+ }
+
+ @Test
+ void getExpense() {
+ assertEquals(100, salad.getExpense());
+ assertEquals(200, pizza.getExpense());
+ assertEquals(70, harryPotter.getExpense());
+ assertEquals(90, lordOfTheRings.getExpense());
+ }
+
+ @Test
+ void setExpense() {
+ harryPotter.setExpense(100);
+ assertEquals(100, harryPotter.getExpense());
+ lordOfTheRings.setExpense(200);
+ lordOfTheRings.setExpense(300);
+ assertEquals(300, lordOfTheRings.getExpense());
+ }
+
+ @Test
+ void getTime() {
+ assertEquals("10/10/2010", pizza.getTime());
+ }
+
+ @Test
+ void testToString() {
+ LocalDateTime myDateObject = LocalDateTime.now();
+ DateTimeFormatter myFormatObject = DateTimeFormatter.ofPattern(DD_MM_YYYY);
+ String time = myDateObject.format(myFormatObject);
+ assertEquals("salad (expense: 100)" + " (time added: " + time + ")", salad.toString());
+ assertEquals("pizza (expense: 200) (start time: 10/10/2010)", pizza.toString());
+ }
+}
diff --git a/src/test/java/seedu/moneymind/storage/FormatStorageTest.java b/src/test/java/seedu/moneymind/storage/FormatStorageTest.java
new file mode 100644
index 0000000000..1b8cceca17
--- /dev/null
+++ b/src/test/java/seedu/moneymind/storage/FormatStorageTest.java
@@ -0,0 +1,77 @@
+package seedu.moneymind.storage;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.moneymind.storage.CategoriesToString.categoriesToString;
+import static seedu.moneymind.storage.StringToCategories.stringToCategories;
+import static seedu.moneymind.string.Strings.STORAGE_CATEGORY_NAME;
+import static seedu.moneymind.string.Strings.NEW_LINE;
+import static seedu.moneymind.string.Strings.STORAGE_NEXT_VARIABLE;
+
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+public class FormatStorageTest {
+
+ private ArrayList storageTestData() {
+ ArrayList storageTestData = new ArrayList();
+ Category food = new Category("food");
+ food.addEvent(new Event("McDonalds", 10, "13/02/2030 12:00"));
+ food.addEvent(new Event("KFC", 20, "13/02/2030 12:00"));
+ storageTestData.add(food);
+ Category transport = new Category("transport");
+ transport.addEvent(new Event("Grab", 10, "13/02/2030 12:00"));
+ transport.addEvent(new Event("Uber", 20, "13/02/2030 12:00"));
+ storageTestData.add(transport);
+ return storageTestData;
+ }
+
+ @Test
+ void categoriesToString_storageTestData_formattedString() {
+ String expected = STORAGE_CATEGORY_NAME + "food" + NEW_LINE
+ + STORAGE_NEXT_VARIABLE + "McDonalds" + STORAGE_NEXT_VARIABLE + "10"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE + STORAGE_NEXT_VARIABLE + "KFC" + STORAGE_NEXT_VARIABLE + "20"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE + STORAGE_CATEGORY_NAME + "transport" + NEW_LINE
+ + STORAGE_NEXT_VARIABLE + "Grab" + STORAGE_NEXT_VARIABLE + "10"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE + STORAGE_NEXT_VARIABLE + "Uber" + STORAGE_NEXT_VARIABLE + "20"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE;
+ assertEquals(expected, categoriesToString(storageTestData()));
+ }
+
+ @Test
+ void stringToCategories_storageTestData_formattedString() {
+ String testSavedText = STORAGE_CATEGORY_NAME + "food" + NEW_LINE
+ + STORAGE_NEXT_VARIABLE + "McDonalds" + STORAGE_NEXT_VARIABLE + "10"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE + STORAGE_NEXT_VARIABLE + "KFC" + STORAGE_NEXT_VARIABLE + "20"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE + STORAGE_CATEGORY_NAME + "transport" + NEW_LINE
+ + STORAGE_NEXT_VARIABLE + "Grab" + STORAGE_NEXT_VARIABLE + "10"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE + STORAGE_NEXT_VARIABLE + "Uber" + STORAGE_NEXT_VARIABLE + "20"
+ + STORAGE_NEXT_VARIABLE + "13/02/2030 12:00" + STORAGE_NEXT_VARIABLE + "false"
+ + NEW_LINE;
+ ArrayList newList = stringToCategories(testSavedText);
+ assertTrue(newList.size() == 2);
+ assertTrue(newList.get(0).getName().equals("food"));
+ assertTrue(newList.get(0).getEvents().size() == 2);
+ assertTrue(newList.get(0).getEvents().get(0).getDescription().equals("McDonalds"));
+ assertTrue(newList.get(0).getEvents().get(0).getExpense() == 10);
+ assertTrue(newList.get(0).getEvents().get(1).getDescription().equals("KFC"));
+ assertTrue(newList.get(0).getEvents().get(1).getExpense() == 20);
+ assertTrue(newList.get(1).getName().equals("transport"));
+ assertTrue(newList.get(1).getEvents().size() == 2);
+ assertTrue(newList.get(1).getEvents().get(0).getDescription().equals("Grab"));
+ assertTrue(newList.get(1).getEvents().get(0).getExpense() == 10);
+ assertTrue(newList.get(1).getEvents().get(1).getDescription().equals("Uber"));
+ assertTrue(newList.get(1).getEvents().get(1).getExpense() == 20);
+ }
+}
diff --git a/src/test/java/seedu/moneymind/storage/GenerateCategoryHashMapTest.java b/src/test/java/seedu/moneymind/storage/GenerateCategoryHashMapTest.java
new file mode 100644
index 0000000000..8382c1b4d6
--- /dev/null
+++ b/src/test/java/seedu/moneymind/storage/GenerateCategoryHashMapTest.java
@@ -0,0 +1,40 @@
+package seedu.moneymind.storage;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.moneymind.storage.GenerateCategoryHashMap.generateCategoryHashMap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.junit.jupiter.api.Test;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+public class GenerateCategoryHashMapTest {
+
+ private ArrayList storageTestData() {
+ ArrayList storageTestData = new ArrayList();
+ Category food = new Category("food");
+ food.addEvent(new Event("McDonalds", 10));
+ food.addEvent(new Event("KFC", 20));
+ storageTestData.add(food);
+ Category transport = new Category("transport");
+ transport.addEvent(new Event("Grab", 10));
+ transport.addEvent(new Event("Uber", 20));
+ storageTestData.add(transport);
+ return storageTestData;
+ }
+
+ private HashMap storageTestDataHashMap() {
+ HashMap storageTestDataHashMap = new HashMap();
+ storageTestDataHashMap.put("food", 0);
+ storageTestDataHashMap.put("transport", 1);
+ return storageTestDataHashMap;
+ }
+
+ @Test
+ void generateCategoryHashMap_storageTestCase_equalToInput() {
+ assertEquals(storageTestDataHashMap(), generateCategoryHashMap(storageTestData()));
+ }
+}
diff --git a/src/test/java/seedu/moneymind/storage/ReadFromFileTest.java b/src/test/java/seedu/moneymind/storage/ReadFromFileTest.java
new file mode 100644
index 0000000000..3bc49d74de
--- /dev/null
+++ b/src/test/java/seedu/moneymind/storage/ReadFromFileTest.java
@@ -0,0 +1,25 @@
+package seedu.moneymind.storage;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static seedu.moneymind.storage.ReadFromFile.readFromFile;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+
+public class ReadFromFileTest {
+ @Test
+ void readFromFile_testFile_noExceptionThrown() {
+ new Storage("testFile.txt");
+ File testFile = new File("testFile.txt");
+ assertDoesNotThrow(() -> readFromFile(testFile));
+ testFile.delete();
+ }
+
+ @Test
+ void readFromFile_nonExistentFile_exceptionThrown() {
+ File testFile = new File("nonExistentFile.txt");
+ assertThrows(Exception.class, () -> readFromFile(testFile));
+ }
+}
diff --git a/src/test/java/seedu/moneymind/storage/StorageTest.java b/src/test/java/seedu/moneymind/storage/StorageTest.java
new file mode 100644
index 0000000000..fa86fb0ad8
--- /dev/null
+++ b/src/test/java/seedu/moneymind/storage/StorageTest.java
@@ -0,0 +1,107 @@
+package seedu.moneymind.storage;
+
+import org.junit.jupiter.api.Test;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import seedu.moneymind.category.Category;
+import seedu.moneymind.event.Event;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class StorageTest {
+
+ private ArrayList storageTestData() {
+ ArrayList storageTestData = new ArrayList();
+ Category food = new Category("food");
+ food.addEvent(new Event("McDonalds", 10));
+ food.addEvent(new Event("KFC", 20));
+ storageTestData.add(food);
+ Category transport = new Category("transport");
+ transport.addEvent(new Event("Grab", 10));
+ transport.addEvent(new Event("Uber", 20));
+ storageTestData.add(transport);
+ return storageTestData;
+ }
+
+ private HashMap storageTestDataHashMap() {
+ HashMap storageTestDataHashMap = new HashMap();
+ storageTestDataHashMap.put("food", 0);
+ storageTestDataHashMap.put("transport", 1);
+ return storageTestDataHashMap;
+ }
+
+ @Test
+ void constructor_nullFilePath_nullPointerException() {
+ assertThrows(NullPointerException.class, () -> new Storage(null));
+ }
+
+ @Test
+ void constructor_validFilePath_fileCreated() {
+ new Storage("testFile.txt");
+ File testFile = new File("testFile.txt");
+ assertTrue(testFile.exists());
+ assertTrue(!testFile.isDirectory());
+ testFile.delete();
+ }
+
+ @Test
+ void save_nullArrayList_nullPointerException() {
+ Storage storage = new Storage("testFile.txt");
+ assertThrows(NullPointerException.class, () -> storage.save(null));
+ new File("testFile.txt").delete();
+ }
+
+ @Test
+ void save_emptyArrayList_noExceptionThrown() {
+ Storage storage = new Storage("testFile.txt");
+ assertDoesNotThrow(() -> storage.save(new ArrayList()));
+ new File("testFile.txt").delete();
+ }
+
+ @Test
+ void save_validArrayList_saveFileUpdated() throws InterruptedException {
+ Storage storage = new Storage("testFile.txt");
+ File testFile = new File("testFile.txt");
+ Long timeStamp = testFile.lastModified();
+ Thread.sleep(5);
+ storage.save(storageTestData());
+ assertTrue(testFile.lastModified() != timeStamp);
+ testFile.delete();
+ }
+
+ @Test
+ void load_noInput_noExceptionThrown() {
+ Storage storage = new Storage("testFile.txt");
+ assertDoesNotThrow(() -> storage.load());
+ new File("testFile.txt").delete();
+ }
+
+ @Test
+ void getSavedCategories_validArrayList_equalToInput() throws NoSuchFieldException, IllegalAccessException {
+ ArrayList expectedOutput = storageTestData();
+ Storage storage = new Storage("testFile.txt");
+ Field fieldUnderTest = Storage.class.getDeclaredField("savedCategories");
+ fieldUnderTest.setAccessible(true);
+ fieldUnderTest.set(storage, expectedOutput);
+ assertEquals(expectedOutput, storage.getSavedCategories());
+ new File("testFile.txt").delete();
+ }
+
+ @Test
+ void getSavedCategoryHashMap_validHashMap_equalToInput() throws NoSuchFieldException,
+ SecurityException, IllegalArgumentException, IllegalAccessException {
+ HashMap expectedOutput = storageTestDataHashMap();
+ Storage storage = new Storage("testFile.txt");
+ Field fieldUnderTest = Storage.class.getDeclaredField("savedCategoryHashMap");
+ fieldUnderTest.setAccessible(true);
+ fieldUnderTest.set(storage, expectedOutput);
+ assertEquals(expectedOutput, storage.getSavedCategoryHashMap());
+ new File("testFile.txt").delete();
+ }
+}
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 892cb6cae7..b7b02dc5dc 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,9 +1,23 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
+Creating file...
+Welcome to Moneymind
+ __ __ __ __ _ _
+ | \/ | | \/ (_) | |
+ | \ / | ___ _ __ ___ _ _| \ / |_ _ __ __| |
+ | |\/| |/ _ \| '_ \ / _ \ | | | |\/| | | '_ \ / _` |
+ | | | | (_) | | | | __/ |_| | | | | | | | | (_| |
+ |_| |_|\___/|_| |_|\___|\__, |_| |_|_|_| |_|\__,_|
+ __/ |
+ |___/
+How may I help you?
+Type 'summary' to see the summary of all the commands you can use.
+Type 'help' to see the details of all the commands.
-What is your name?
-Hello James Gosling
+____________________________________________________________
+
+____________________________________________________________
+OOPS!!! I'm sorry, but I don't know what that means :-(
+____________________________________________________________
+
+____________________________________________________________
+Bye. Hope to see you again soon!
+____________________________________________________________
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index f6ec2e9f95..8e24493055 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -1 +1,2 @@
-James Gosling
\ No newline at end of file
+echo test
+bye