diff --git a/.gitignore b/.gitignore index 2873e189e1..9f614ebd4f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +data/ +META-INF/MANIFEST.MF diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..a9d7db9c0a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..5d9825616f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..94a25f7f4c --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index ea82051fab..3b28db3ca4 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'application' id 'checkstyle' id 'com.github.johnrengelman.shadow' version '7.1.2' + // id 'org.openjfx.javafxplugin' version '0.1.0' } repositories { @@ -43,4 +44,10 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } + +//javafx { +// version = "21" +// modules = [ 'javafx.controls' ] +//} diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..7af8512dd1 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,11 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) + +Display | Name | Github Profile | Portfolio +--------|:-------------:|:-----------------------------------------:|:---------: +![img.png](images/hafiz.png) | Hafizuddin | [Github](https://github.com/hafizuddin-a) | [Portfolio](team/hafizuddin-a.md) +![](images/Junxiang.jpg) | Heng Junxiang | [Github](https://github.com/Cohii2) | [Portfolio](team/Cohii2.md) +![](images/Shaoliang.png) | He Shaoliang | [Github](https://github.com/monkescripts) | [Portfolio](team/monkescripts.md) +![](https://via.placeholder.com/100.png?text=Photo) | Ayagari Mukund| [Github](https://github.com/mukund1403) | [Portfolio](team/mukund1403.md) +![](https://via.placeholder.com/100.png?text=Photo) | Avril Guok | [Github](https://github.com/avrilgk) | [Portfolio](team/avrilgk.md) + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..ba49669ae5 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,37 +2,428 @@ ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +We would like to acknowledge Splitwise for the inspiration for the project. The Splitwise application is a popular expense-sharing application that allows users to split bills and expenses with friends, family, and roommates. We have adapted the core features of Splitwise to create a simplified version of the application for our project. We would also like to acknowledge Shao Liang for the inspiration of the project's name. + +## Design + +### Architecture + +The high-level overview of the application is provided in the Architecture Diagram below. + +Users interact with the application through the Command Line Interface. +Application Storage is located in `/data/groups/` +and each `Group` object is stored in its own `.txt` file after program's runtime. + +![Architecture_Diagram](images/ArchitectureDiagram.jpg) + +### Balance + +Below is a Class Diagram for the Balance Feature: + +![Balance Structure](images/BalanceStructure.png) + +### Group Storage + +The Group Storage feature allows users to save group information to files when exiting groups or ending the program. It also enables users to load group information from files when entering groups. This feature ensures that changes made to a group are persisted across sessions and that users can resume their interactions with groups seamlessly. + +Below is the class diagram for the Group Storage feature: + +![Class Diagram](images/GroupStorageClassDiagram.png) + +The Group Storage feature: +- `GroupStorage` class: Responsible for saving and loading group information to and from files. +- `FileIO` interface: Defines the contract for file input/output operations. +- `FileIOImpl` class: Implements the `FileIO` interface for file input/output operations. +- `GroupNameChecker` class: Provides methods to check if a group file exists and to generate the file name for a group. +- `Group` class: Represents a group with members and expenses. It interacts with the `GroupStorage` class to save and load group information. +- `GroupFilePaths` class: Contains constants for file paths used by the `GroupStorage` feature. + +The implementation of the Group Storage feature is covered in the next section. + + +### Expenses +The Expenses feature allows users to add and delete expenses. It does this through an intermediate +ExpenseCommand class which handles Parser input and calls the Expense constructor to create a new expense. + +Below is the class diagram for Expense feature: + +![ExpenseClassDiagram.png](images/ExpenseClassDiagram.png) + +The Expense feature: +- `Expense` class: Represents a new expense with total Amount, payer, payee list with amount owed by each payee and description of expense +- `ExpenseCommand` class: Responsible for taking in the user input and creating a new object of the Expense class. Also handles deletion of expense objects. + - It contains the `addEqualExpense` and `addUnequalExpense` methods to handle both kinds of expenses. + - It also uses the `Money` class to add the correct currency to totalAmount and payee Amounts. + +The implementation of Expense is covered in the next section. + +## Implementation + +### Help menu feature + +#### Implementation + +The "help" feature is facilitated by the `Help` class. +It provides a static method `printHelp` to print out a guide on how to use the commands in the application. +`printHelp` can be used in the event the user issues an invalid command + +### Group Creation feature + +#### Implementation + +The "Group Creation" feature is facilitated by the `Group` class. It provides methods to create a new group and manage +group membership. The implementation of this feature is as follows: + +The Group class maintains a list of members as a `private List` field called `members`. + +The `createGroup(String groupName)` method is responsible for creating a new group. It performs the following steps: + +1. Checks if a group with the given `groupName` already exists using the `isGroup(String groupName)` method. +2. If the group does not exist, creates a new `Group` object with the provided `groupName`. +3. Prints a success message indicating that the group has been created. +4. Adds the new `Group` object to the `groups` list. +5. Returns the newly created `Group` object. +6. If the group already exists, prints an error message indicating that the group already exists. + +### Add Member to Group feature + +#### Implementation + +The "Add Member to Group" feature is facilitated by the `Group` class. It extends the `Group` class with methods to manage group membership and allows users to add new members to an existing group. Additionally, it implements the following operations: + +- `Group#addMember(String memberName)` — Adds a new member to the group with the given `memberName`. +- `Group#isMember(String memberName)` — Checks if a user with the given `memberName` is already a member of the group. + +These operations are exposed in the `GroupCommand` class as `GroupCommand#addMember(String memberName)`. + +Given below is an example usage scenario and how the "Add Member to Group" feature behaves at each step. + +Step 1. The user launches the application and enters a group named "Project Team" using the `group Project Team` command. The `Group` object for "Project Team" will be initialized with an empty `members` list. + +Step 2. The user executes the `member John` command to add a new member named "John" to the "Project Team" group. The `member` command calls `GroupCommand#addMember("John")`, which in turn calls `Group#addMember("John")`. This operation checks if "John" is already a member of the group using `Group#isMember("John")`. Since "John" is not a member, a new `User` object with the name "John" is created and added to the `members` list of the "Project Team" group. + +![Sequence Diagram](images/addMember1.png) + +Step 3. The user executes the `member Emily` command to add another member named "Emily" to the "Project Team" group. Similar to step 2, the `member` command calls `GroupCommand#addMember("Emily")`, which then calls `Group#addMember("Emily")`. After checking that "Emily" is not already a member, a new `User` object with the name "Emily" is created and added to the `members` list of the "Project Team" group. + +Step 4. The user tries to add "John" again to the "Project Team" group by executing the `member John` command. However, since "John" is already a member of the group, the `Group#isMember("John")` check in `Group#addMember("John")` returns `true`. As a result, an error message is displayed to the user, indicating that "John" is already a member of the group, and no duplicate member is added. + +![Sequence Diagram](images/addMember2.png) + +The following sequence diagram illustrates the flow of the "Add Member to Group" feature: + +![Sequence Diagram](images/addMember3.png) + + +### Expenses feature + +#### Implementation + +The Expenses feature is facilitated through the Expense class and ExpenseCommand class. It allows users to add a new Expense through creation of +a new Expense object. Users can specify amount paid, the payee, and the members of the group involved in the +transaction. + +It implements the following operations: + + ++ `Expense#addExpense` - Creates a new expense and adds it to the expense list ++ `Expense#deleteExpense` - Deletes an existing expense +These operations are exposed in the ExpenseCommand class through the `addExpense` and `deleteExpense` functions respectively. + + +Additionally, it implements the following: ++ `Expenses#payer()` - Gives the name of the member who paid for the expense ++ `Expenses#totalAmount()` - Returns the total amount of the expense ++ `Expenses#payees()` - Returns all the members involved in the transaction ++ `Expenses#currency()` - Returns the currency of the expense + +These operations are exposed in the Expense class through the `getPayerName()`, `getTotalAmount()`, `getPayees()`, and `getCurrency()` +functions respectively. + + +The following sequence diagrams show the process of adding an equal split and unequal split expense: + +![Sequence Diagram](images/equal.png) +![Sequence Diagram](images/unequal.png) + +### Balance feature + +#### Implementation + + +The Balance feature is facilitated through the Balance class. +It allows a user to view a printed list of other users in the Group, and the amount that is owed by/to each user. + +Each `Balance` object contains a String of a user `userName`, +and a Map `balanceList`. This Map uses String of other users' usernames as Key, and +a list of Money objects owed by/to the user. + +To print a user's Balance List, perform the following steps: + +1. Create a `Balance` object with the params String `userName` and the current Group `group`. +2. From the `members` and `expenseList` List items in `group`, the Map `balanceList` is populated. +3. Call method `printBalance()` to print the contents of the Map `balanceList`. + +Below is the Sequence Diagram for the Balance Feature: + +![Sequence Diagram](images/balance.png) + +### Settle feature + +#### Implementation + +The Settle feature is facilitated through the Settle class. +It allows a user to settle the debts between two users in a Group. + + The `Settle` class contains a `settleDebt(String userName1, String userName2)` method. +This method takes in two Strings `userName1` and `userName2` as parameters, representing the two users to settle the +debt between. + +The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed +by `userName2` to `userName1`. It then prints out the total amount that is owed between the two users, and prompts the user to enter the amount to +settle the debt. + +The method then prints out the amount that is owed by `userName1` to `userName2`, and the amount that is owed +by `userName2` to `userName1` after the settlement. + +![Sequence Diagram](images/settle.png) + +### Group Storage feature + +#### Implementation + +The "Group Storage" feature is facilitated by the `GroupStorage` class. It extends the functionality of the `Group` class by providing methods to save and load group information to and from files. The `GroupStorage` class interacts with the `FileIO` interface for file input/output operations. Additionally, it implements the following key operations: + +- `GroupStorage#saveGroupToFile(Group group)` — Saves the group information to a file when a user exits a group or ends the program. +- `GroupStorage#loadGroupFromFile(String groupName)` — Loads the group information from a file when a user enters a group. + +These operations are invoked from the `Group` class when the user performs specific actions related to groups. + +Given below is an example usage scenario and how the "Group Storage" feature behaves at each step. + +Step 1. The user launches the application and tries to create a group named "Project Team" using the `create Project Team` command. The `Group#getOrCreateGroup(String groupName)` method is called to retrieve or create the group. + +Step 2. Inside the `Group#getOrCreateGroup(String groupName)` method, it checks if the group already exists in memory. If not, it uses the `GroupNameChecker` class to check if the group file exists. If the group file doesn't exist, a new `Group` object is created, and the user is placed in the newly created group. + +Step 3. The user executes various commands to add members and expenses to the "Project Team" group. These changes are made to the `Group` object in memory. + +Step 4. The user executes the `exit Project Team` command to exit the "Project Team" group. This command invokes the `Group#exitGroup(String groupName)` method, which in turn calls the `GroupStorage#saveGroupToFile(Group group)` method to save the current state of the "Project Team" group to a file. The saving process includes writing the group name, members, and expenses to the file in a structured format. + +![Sequence Diagram](images/groupStorage1.png) + +Step 5. Later, the user decides to enter the "Project Team" group again using the `enter Project Team` command. The `Group#enterGroup(String groupName)` method is called to enter the group. + +Step 6. Inside the `Group#enterGroup(String groupName)` method, it first checks if the group exists in memory. If not, it uses the `GroupNameChecker` class to check if the group file exists. If the group file exists, it invokes the `GroupStorage#loadGroupFromFile(String groupName)` method to load the group information from the file. + +![Sequence Diagram](images/groupStorage2.png) + +The `GroupStorage#loadGroupFromFile(String groupName)` method reads the group information from the file, creates a new `Group` object, and populates it with the loaded data. This includes the group name, members, and expenses. The loaded `Group` object is then returned to the `Group` class. + +Step 7. The user continues to interact with the "Project Team" group, making changes to its members and expenses. These changes are made to the loaded `Group` object in memory. + +Step 8. When the user ends the program using the `bye` command, the `GroupStorage#saveGroupToFile(Group group)` method is invoked again to save the current state of all loaded groups to their respective files. This ensures that any changes made during the session are persisted. + +**Design Considerations:** + +- **Alternative 1 (current choice):** Saving group information to files when exiting groups or ending the program, and loading group information when entering groups. + - Pros: Minimizes file I/O operations and reduces the overhead of constantly saving and loading group information. + - Cons: Changes made to a group are not persisted until the user explicitly exits the group or ends the program. +- **Alternative 2:** Saving group information after every command that modifies the group, and loading group information whenever a group is accessed. + - Pros: Ensures that changes are immediately persisted and reduces the risk of data loss in case of unexpected program termination. + - Cons: Increases file I/O operations and may impact performance, especially for frequent group modifications. + +### Luck feature + +#### Implementation + +The Luck features offers users the unique opportunity to clear their debts by playing with a slot machine. +User enters the command `luck` in the parser to enter the slot machine game. +Parser checks whether the user is in a group and also whether the said group has more than one user. +To win in the slot machine, all three slots in the MIDDLE row must show the same character +The user can either key in `/reroll` or `/exit` to either roll the slot machine or leave the game. +For every new `/reroll`, an additional Expense of 10USD would be created and allocated to a random member in the group +For a win, the user clears all his debts within the group. + +Following structural diagram shows the dependency of Luck class + +![Luck Structure](images/LuckStructure.png) + +Following sequence diagram shows user calls `startGambling()` within Luck class and a new SlotMachine class is instantiated +For every reroll, fillSlots() is called which fills individual slots in the 3 x 3 slot machine with a randomised character + +![Sequence Diagram](images/luckGambling.png) -## Design & implementation -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} ## Product scope + ### Target user profile -{Describe the target user profile} +Our target users are people who share expenses with friends, family, roommates, or colleagues. + +The application gives an accurate and simple way to represent unsettled debts between users and their friends ### Value proposition -{Describe the value proposition: what problem does it solve?} +Our application provides a simple and efficient way for users to manage shared expenses with friends, family, or colleagues. It allows users to create groups, add members to groups, and split expenses accurately among group members. The application helps users keep track of their debts and settle them easily. By automating the process of splitting expenses and tracking debts, the application saves users time and effort, making it a valuable tool for managing shared expenses. Users that are able to type fast and are familiar with the command line interface will find this application useful. ## User Stories -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|----------|----------------------------------------------------------------|-------------------------------------------------------------| +| v1.0 | new user | see usage instructions | refer to them when I forget how to use the application | +| v1.0 | user | add a new expense with description, amount, and users involved | split the expense equally | +| v1.0 | user | create a new group | split expenses with different groups | +| v1.0 | user | add a new member to a group | split expenses with different members in the group | +| v1.0 | user | check how much I owe each member in the group | keep track of my debts | +| v2.0 | user | settle debts between two users in the group | clear debts between users | +| v2.0 | user | save group information to files when exiting groups or ending the program | resume interactions with groups seamlessly | +| v2.0 | user | load group information from files when entering groups | resume interactions with groups seamlessly | +| v2.1 | user | add expenses in different currencies | split expenses accurately in different currencies | ## Non-Functional Requirements -{Give non-functional requirements} +1. **Performance**: The application should respond to user commands within a reasonable time frame, even when handling large amounts of data. +2. **Reliability**: The application should be robust and handle errors gracefully, providing informative error messages to users. +3. **Usability**: The application should have a user-friendly interface that is easy to navigate and understand. +4. **Portability**: The application should be platform-independent and run on different operating systems without requirements for additional software. ## Glossary -* *glossary item* - Definition +- **Group**: A collection of users who share expenses and debts with each other. +- **User**: An individual who is a member of a group and participates in sharing expenses. +- **Expense**: A transaction involving a group of users where one user pays for an expense that is shared among multiple users. +- **Balance**: The amount of money that a user owes or is owed by other users in a group. +- **Settle**: The process of clearing debts between two users in a group. +- **Group Storage**: The feature that allows users to save and load group information to and from files. +- **Currency**: A system of money used in a particular country or region. +- **Debt**: An amount of money that is owed by one user to another user in a group. ## Instructions for manual testing {Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} + +Important note: The provided test cases are independent of each other and can be run in any order. Test cases that require multiple steps are clearly outlined with the expected outcomes at each step. All test cases are designed to be run on a clean slate, i.e., without any existing groups or expenses in the system. + + +### Test Case: Help Menu + +1. Run the application. +2. Enter the `help` command. +3. Verify that the help menu is displayed with a list of available commands and their descriptions. + +Expected outcome: The help menu is displayed with a list of available commands and their descriptions as shown below: + +``` +Welcome, here is a list of commands: +help: Access help menu. +create : Create a group. +exit : Exit current group. +member : Add a member to the group. +expense /amount /currency /paid /user /user ...: Add an expense SPLIT EQUALLY. +expense /unequal /amount /currency /paid /user /user ...: Add an expense SPLIT UNEQUALLY. +list: List all expenses in the group. +balance : Show user's balance. +settle /user : Settle the amount between two users. +luck : luck is in the air tonight +``` + +### Test Case: Group Creation + +1. Run the application. +2. Enter the `create Project Team` command. +3. Verify that a new group named "Project Team" is created successfully. + +Expected outcome: A success message is displayed indicating that the group "Project Team" has been created. + +``` +Project Team created. +You are now in Project Team. +``` + +### Test Case: Add Member to Group + +1. Run the application. +2. Enter the `create Project Team` command. +3. Enter the `member John` command. +4. Enter the `member Emily` command. +5. Enter the `member John` command again. +6. Verify the output messages at each step. + +Expected outcome: +- Step 2: A success message is displayed indicating that the group "Project Team" has been created. +``` +Project Team created. +You are now in Project Team. +``` +- Step 3: A success message is displayed indicating that the member "John" has been added to the group. +``` +John has been added to Project Team. +``` + +- Step 4: A success message is displayed indicating that the member "Emily" has been added to the group. +``` +Emily has been added to Project Team. +``` +- Step 5: An error message is displayed indicating that the member "John" is already a member of the group. +``` +John is already a member of Project Team. +``` + +### Test Case: Add Expense + +1. Run the application. +2. Enter the `create Project Team` command. +3. Enter the `member John` and `member Emily` commands. +4. Add an expense to the "Project Team" group using the `expense Dinner /amount 100 /paid John /user Emily` command. +5. Verify the output messages at each step. + +Expected outcome: A success message is displayed indicating that the expense "Dinner" has been added to the group with the correct details. + +``` +Added new expense with description Dinner and amount SGD 100.00 paid by John. The split is: +Emily : SGD 50.00 +John : SGD 50.00 +``` + +### Test Case: Save and Load Group Information + +1. Run the application. +2. Create a new group named "Family" using the `create Family` command. +3. Add members "Alice" and "Bob" to the "Family" group using the `member Alice` and `member Bob` commands. +4. Add an expense to the "Family" group using the `expense Dinner /amount 50 /paid Alice /user Bob` command. +5. Exit the "Family" group using the `exit Family` command. +6. Exit the application using the `bye` command. +7. Run the application again. +8. Enter the `enter Family` command. +9. Verify that the group "Family" is loaded with the members "Alice" and "Bob" and the expense "Dinner" with the correct details. + +Expected outcome: +- Step 2: A success message is displayed indicating that the group "Family" has been created. +``` +Family created. +You are now in Family. +``` +- Step 3: Success messages are displayed indicating that the members "Alice" and "Bob" have been added to the group. +``` +Alice has been added to Family. +Bob has been added to Family. +``` +- Step 4: A success message is displayed indicating that the expense "Dinner" has been added to the group. +``` +Added new expense with description Dinner and amount SGD 50.00 paid by Alice. The split is: +Bob : SGD 25.00 +Alice : SGD 25.00 +``` +- Step 5: A success message is displayed indicating that the group "Family" has been saved to a file. +``` +Group data saved successfully. +You have exited Family. +``` +- Step 8: A success message is displayed indicating that the group "Family" has been entered. +``` +Group loaded successfully. +You are now in Family. +``` + diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..f56eb1b732 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,4 @@ -# Duke - -{Give product intro here} +# Split-liang (An app to help you split expenses with friends in a fun way!) Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..ae388a8755 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,41 +2,378 @@ ## Introduction -{Give a product intro} +Split-liang is an application that helps you split expenses with friends in a fun way! If you are tired of keeping track and calculating who owes who, Split-liang is here to help you. With Split-liang, you can create groups, add members, add expenses, and settle debts between members. Split-liang will automatically calculate the amount each member owes or is owed. You can also play slots to remove your debts! + +## Content Page +1. [Quick Start](#quick-start) +2. [Features](#features) + - [Viewing help: `help`](#viewing-help-help) + - [Creating a group: `create`](#creating-a-group-create) + - [Entering a group: `enter`](#entering-a-group-enter) + - [Deleting a group: `delete group`](#deleting-a-group-delete) + - [Add members to group: `member`](#add-members-to-group-member) + - [Exiting a group: `exit`](#exiting-a-group-exit) + - [Create a new expense: `expense`](#create-a-new-expense-expense) + - [List all expenses for a group: `list`](#list-expenses-list) + - [Delete an expense: `delete expense`](#delete-an-expense-delete-expense) + - [Show balance of user: `balance`](#show-balance-of-user-balance) + - [Settle expenses: `settle`](#settle-expenses-settle) + - [Trying your luck: `luck`](#trying-your-luck-luck) + - [Saving the data](#saving-the-data) + - [Saying goodbye: `bye`](#saying-goodbye-bye) + +-------------------------------------------------------------------------------------------------------------------- + ## Quick Start -{Give steps to get started quickly} +1. Ensure that you have Java 11 or above installed in your computer. +2. Download the latest version of `Split-liang` from [here](https://github.com/AY2324S2-CS2113-T15-3/tp/releases). +3. Copy the file to the folder you want to use as the home folder for your Split-liang. +4. Open a command terminal, `cd` to the folder where the jar file is located. Run the command `java -jar Split-liang.jar`. The application should start and display the welcome message. + +## Command Structure + +A command has the general format: +``` +COMMAND ARGUMENT /param PARAM1 /param PARAM2 ... +``` + +## Features + +### Viewing help: `help` + +This command will display a message explaining how to use the application. + +Format: `help` + +Example: `help` + +Output: +``` +Welcome, here is a list of commands: +help: Access help menu. +create : Create a group. +exit : Exit current group. +member : Add a member to the group. +expense /amount /currency /paid /user /user ...: Add an expense SPLIT EQUALLY. +expense /unequal /amount /currency /paid /user /user ...: Add an expense SPLIT UNEQUALLY. +list: List all expenses in the group. +balance : Show user's balance. +settle /user : Settle the amount between two users. +luck : luck is in the air tonight +``` + +-------------------------------------------------------------------------------------------------------------------- +### GROUP COMMANDS + +#### Creating a group: `create` + +Creates a new group with the specified group name. + +Format: `create GROUP_NAME` + +- `GROUP_NAME` is the name of the group. +- `GROUP_NAME` can contain whitespaces but cannot be empty. +- `GROUP_NAME` must be unique. It cannot be the same as an existing group name. +- `GROUP_NAME` is not case-sensitive. +- `GROUP_NAME` cannot contain special characters. +- User must not be in a group to create a new group. + +Example: `create Friends` + +This command will create a new group named 'Friends'. + +#### Entering a group: `enter` + +Enters an existing group with the specified group name. + +Format: `enter GROUP_NAME` + +- `GROUP_NAME` is the name of the group. +- `GROUP_NAME` must be an existing group name. +- User must not be in a group to enter a new group. + +Example: `enter Friends` + +This command will enter the group named 'Friends'. + +#### Deleting a group: `delete group GROUP_NAME` + +Deletes an existing group with the specified group name. + +Format: `delete group GROUP_NAME` + +- `GROUP_NAME` is the name of the group to be deleted. +- `GROUP_NAME` must be an existing group name. + +Example: `delete group Friends` + +#### Add members to group: `member` + +Adds a new member to the group. + +Format: `member USER_NAME` + +- `USER_NAME` is the name of the user to be added to the group. It is in alphanumeric format. +- `USER_NAME` must be unique. It cannot be the same as an existing member's name. +- `USER_NAME` can contain whitespaces but cannot be empty. +- `USER_NAME` is not case-sensitive. +- `USER_NAME` can contain special characters. + +Example: `member Alice` + +This command will add a new member named 'Alice' to the group. + +Output: `Alice has been added to group.` + +#### Exiting a group: `exit` + +Exits the current group. + +Format: `exit GROUP_NAME` + +- `GROUP_NAME` is the name of the group. +<<<<<<< HEAD +- `GROUP_NAME` must be an existing group. +- User must be in a group to exit the group. +- If the user is not in any group, an exception will be thrown. + +Example: `exit Friends` + +This command will exit the group named 'Friends'. + +-------------------------------------------------------------------------------------------------------------------- +### EXPENSE COMMANDS + +#### Create a new expense: `expense` + +Create a new expense for a given group. + +##### 1. Create expense split equally + +Format:`expense DESCRIPITON /amount AMOUNT /paid PAYER_USER_NAME /user USER_NAME /user USER_NAME` + +- `PAYER_USER_NAME` is the username of the person who paid for the transaction. +- `USER_NAME` is the username of payees. Each expense can have multiple payees but only one payer. +- `AMOUNT` has to be a valid float value. It will be split equally between all members including the payer. +- If `AMOUNT` value entered is greater than 2 decimal places, it will round off to 2 decimal places before calculating split. +- Range of `AMOUNT` is from 0.01 to 999999.99. +- The payer name (`PAYER_USER_NAME`) and all payees (`USER_NAME`) must be existing members of the group. +Otherwise, exception will be thrown. +- Once the expense is created, the success message will be displayed. +- The expense will be added to a list of expenses. -1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +Examples: +- `expense dinner /amount 9.00 /paid Alice /user Bob /user Charlie` + This command will create a new expense with total amount of SGD 9.00 with a split of: + Alice: SGD 3.00, Bob: SGD 3.00, and Charlie: SGD 3.00. -## Features -{Give detailed description of each feature} +- `expense lunch /amount 10.00 /paid Alice /user Bob /user Charlie` + This command will create a new expense with total amount of SGD 10 with a split of: + Alice: SGD 3.33, Bob: SGD 3.33, and Charlie: SGD 3.33. -### Adding a todo: `todo` -Adds a new item to the list of todo items. +Expenses with unequal split and Expense with different currencies take in the base parameters of Expenses split equally +plus one additional parameter. All the rules mentioned for equally split expenses still apply. -Format: `todo n/TODO_NAME d/DEADLINE` +##### 2. Create expense split unequally -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Format:`expense DESCRIPITON /unequal /amount TOTAL_AMOUNT +/paid PAYER_USER_NAME /user USER_NAME AMOUNT_OWED /user USER_NAME AMOUNT_OWED` -Example of usage: +- `/unequal` indicates that the expense will be split according to the `AMOUNT_OWED` by each user. +- The payer's amount owed will be automatically calculated by the program and does not have to be inputted. -`todo n/Write the rest of the User Guide d/next week` +Examples: +- `expense dinner /unequal /amount 9.00 /paid Alice /user Bob 3 /user Charlie 4` + This command will create a new expense with total amount of SGD 9.00 with a split of: + Alice: SGD 2.00, Bob: SGD 3.00, and Charlie: SGD 4.00 (Alice's split is automatically calculated) -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` + +- `expense dinner /unequal /amount 14.00 /paid Alice /user Bob 5 /user Charlie 6` + This command will create a new expense with total amount of SGD 14 with a split of: + Alice: SGD 3.00, Bob: SGD 5.00, and Charlie: SGD 6.00 (Alice's split is automatically calculated) + +##### 3. Create expense with different currency + +Format: `expense DESCRIPTION /currency CURRENCY /amount TOTAL_AMOUNT +/paid PAYER_USER_NAME /user USER_NAME AMOUNT_OWED /user USER_NAME AMOUNT_OWED` + +- `/currency` indicates the currency of the transaction. +- The currency has to be from the currency list mentioned [here](#list-of-currencies). +Otherwise, the app will throw an error. +- If no `/currency` is present in the command, the program will use SGD as the default currency. +- The currency feature can be used with expenses split equally or unequally. +- Only one currency can be entered per transaction. + +Examples: +- `expense dinner /currency USD /unequal /amount 9.00 /paid Alice /user Bob 3 /user Charlie 4` + This command will create a new expense with total amount of USD 9.00 with a split of: + Alice: USD 2.00, Bob: USD 3.00, and Charlie: USD 4.00 (Alice's split is automatically calculated) + + +- `expense dinner /amount 9.00 /paid Alice /user Bob /user Charlie` + This command will create a new expense with total amount of USD 9.00 with a split of: + Alice: USD 3.00, Bob: USD 3.00, and Charlie: USD 3.00 + +
+ +#### List expenses: `list` + +Lists all expenses for the current group + +Format: `list` +- The indexes shown in front of each expense can be used with `delete expense` to delete an expense (see below). + +
+ +#### Delete an expense: `delete expense` + +Deletes an expense based on index specified from list command. + +Format: `delete expense LIST_INDEX` +- The `LIST_INDEX` needs to be a valid index from the Expenses list. To check expenses list use the `list` command +(see above). +- If the `LIST_INDEX` does not exist, an exception will be thrown. +- The `LIST_INDEX` starts from 1. +- The `LIST_INDEX` has to be an integer value. + +Example: +- `delete expense 3` +This command deletes the expense at index 3 on the expense list. +-------------------------------------------------------------------------------------------------------------------- +### BALANCE COMMAND + +#### Show balance of user: `balance` + +Shows list of members the user owes money to. + +Format: `balance USER_NAME` + +- `USER_NAME` is the name of the user whose balance is to be displayed. +- The balance list will show the user's name and the amount they owe to each member. +- `USER_NAME` must be an existing member of the group. + +Example: +``` +balance Alice + + /\_/\ + ( ^.^ ) + > ^ < +<----------SUCCESS-----------> +User Alice's Balance List: + Bob : 5.00SGD + Eve : 10.00SGD + Eve : -10.00AUD +End of Balance List +<----------------------------> +``` + +This command will display the balance of the user named Alice. +In this case, Bob owes Alice `5.00SGD`, +Eve owes Alice `10.00SGD` +and Alice owes Eve `10.00AUD` + +-------------------------------------------------------------------------------------------------------------------- +### SETTLE COMMAND + +#### Settle expenses: `settle` + +Settles the expenses between two users in the group. + +Format: `settle USER_NAME1 /user USER_NAME2` + +- `USER_NAME1` is the name of the first user. +- `USER_NAME2` is the name of the second user. +- `/user` is a keyword to indicate the start of the second user's name. +- The two users must be existing members of the group. + +Example: `settle Alice /user Bob` + +This command will settle the expenses between Alice and Bob, showing what Alice owes Bob. + +-------------------------------------------------------------------------------------------------------------------- +### LUCK COMMAND + +#### Trying your luck: `luck USER_NAME1` + +Play slots to remove debts + +Format: `luck USER_NAME1`` + +- Enters the slot machine + - `/reroll` to reroll the slots + - `/exit` to exit the slot machine + - Example: `/reroll` + +This command enable users play slots to remove their debts + +-------------------------------------------------------------------------------------------------------------------- +### Saving the data + +Split-liang automatically saves the data in each group to `GROUP_NAME.txt` in the `data\groups` folder after the user exits the group using the `exit GROUP_NAME` command or exits the application using the `bye` command. There is no need to save manually. + +The group data is loaded automatically when the user enters the group using the `enter GROUP_NAME` command. + +- The data won't be saved if the application is closed using the `X` button on the window. It will also not be saved when the application is closed using the `Ctrl + C` command in the terminal. +- The data folder is located in the same directory as the jar file. +- The data folder contains the data for each group in a separate text file. +- The data folder is created automatically if it does not exist. +- The data folder can be deleted to clear all data. +- Corrupted data files will be ignored and not loaded. +- The data is saved in a human-readable format. + +-------------------------------------------------------------------------------------------------------------------- +### Exiting the Application +#### Saying goodbye: `bye` + +This command exits the application. + +-------------------------------------------------------------------------------------------------------------------- +## List of currencies + +### The currencies currently supported by the Expenses feature: + +| Currency | Tag to Enter (This is what should be entered after `/currency`) when creating new expense | +|-------------------|-------------------------------------------------------------------------------------------| +| US Dollar | USD e.g. `/currency USD` | +| Singapore Dollar | SGD e.g. `/currency SGD` | +| Chinese Yuan | RMB e.g. `/currency RMB` | +| Euro | EUR e.g. `/currency EUR` | +| Japanese Yen | JPY e.g. `/currency JPY` | +| Australian Dollar | AUD e.g. `/currency AUD` | +| Malaysian Ringgit | MYR e.g. `/currency MYR` | + +-------------------------------------------------------------------------------------------------------------------- ## FAQ -**Q**: How do I transfer my data to another computer? +1. **Q: How do I create a new group?** + - A: To create a new group, use the `create group` command followed by the group name. +2. **Q: How do I transfer my data to another device?** + - A: You can copy the `data` folder to the new device to transfer your data. +3. **Q: Can I enter a new expense or add a new member without being in a group?** + - A: No you need to first create a new group or enter an existing one after which you can carry out those actions. -**A**: {your answer here} +-------------------------------------------------------------------------------------------------------------------- ## Command Summary +
-{Give a 'cheat sheet' of commands here} - -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Action | Format, Examples | +|------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| Help | `help` | +| Create group | `create GROUP_NAME`
e.g. `create Friends` | +| Enter group | `enter GROUP_NAME`
e.g. `enter Friends` | +| Delete group | `delete group GROUP_NAME`
e.g. `delete group Friends` | +| Add member | `member USER_NAME`
e.g. `member Alice` | +| Exit group | `exit GROUP_NAME`
e.g. `exit Friends` | +| Add expense | `expense DESCRIPTION /amount AMOUNT /paid USER_NAME /user USER_NAME GROUP_NAME`
e.g. `expense lunch /amount 20 /paid Alice /user Bob` | +| List expenses | `list`
e.g. `list` | +| Delete expense | `delete expense LIST_INDEX`
e.g. `delete expense 3` | +| Balance | `balance USER_NAME`
e.g. `balance Alice` | +| Settle expenses | `settle USER_NAME1 /user USER_NAME2`
e.g. `settle Alice /user Bob` | +| Luck | `luck USER_NAME1`
e.g. `luck Alice` +| Exit application | `bye`
| diff --git a/docs/diagrams/BalanceCommand_handleBalance.puml b/docs/diagrams/BalanceCommand_handleBalance.puml new file mode 100644 index 0000000000..bbc0ca3173 --- /dev/null +++ b/docs/diagrams/BalanceCommand_handleBalance.puml @@ -0,0 +1,47 @@ +@startuml +Actor User +User -> ":BalanceCommand" : handleBalance +activate ":BalanceCommand" +create ":ExpensesException" +":BalanceCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":BalanceCommand" +deactivate ":ExpensesException" +":BalanceCommand" -> ":Group" : isMember +activate ":Group" +":Group" --> ":BalanceCommand" +deactivate ":Group" +create ":ExpensesException" +":BalanceCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":BalanceCommand" +deactivate ":ExpensesException" +create ":Balance" +":BalanceCommand" -> ":Balance" : new +activate ":Balance" +":Balance" -> ":Balance" : addExpense +activate ":Balance" +":Balance" -> ":Balance" : addMoney +activate ":Balance" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" -> ":Balance" : subtractMoney +activate ":Balance" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" --> ":BalanceCommand" +deactivate ":Balance" +":BalanceCommand" -> ":Balance" : printBalance +activate ":Balance" +":Balance" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Balance" +deactivate ":UserInterface" +":Balance" --> ":BalanceCommand" +deactivate ":Balance" +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/Balance_structure.puml b/docs/diagrams/Balance_structure.puml new file mode 100644 index 0000000000..a5ecb93976 --- /dev/null +++ b/docs/diagrams/Balance_structure.puml @@ -0,0 +1,41 @@ +@startuml +digraph g { + rankdir="TB" + splines=polyline + + +'nodes + + Balance1407063755[ + label=< + +
+ + +
+ + + +
Balance
# balanceList: Map<String, List<Money>>
# userName: String
- addExpense(expense: Expense)
+ addMoney(moneyList: List<Money>, money: Money)
+ printBalance()
> + style=filled + margin=0 + shape=plaintext + fillcolor="#FFFFFF" +]; + + BalanceCommand1418370849[ + label=< + +
+ +
BalanceCommand
+ handleBalance(argument: String)
> + style=filled + margin=0 + shape=plaintext + fillcolor="#FFFFFF" +]; + +BalanceCommand1418370849 -> Balance1407063755[label="handleBalance() -> printBalance()"]; + +} +@enduml \ No newline at end of file diff --git a/docs/diagrams/ExpenseCommandClassDiagram.puml b/docs/diagrams/ExpenseCommandClassDiagram.puml new file mode 100644 index 0000000000..9a6bcb1ad2 --- /dev/null +++ b/docs/diagrams/ExpenseCommandClassDiagram.puml @@ -0,0 +1,158 @@ +@startuml + +/' diagram meta data start +config=StructureConfiguration; +{ + "projectClassification": { + "searchMode": "OpenProject", // OpenProject, AllProjects + "includedProjects": "", + "pathEndKeywords": "*.impl", + "isClientPath": "", + "isClientName": "", + "isTestPath": "", + "isTestName": "", + "isMappingPath": "", + "isMappingName": "", + "isDataAccessPath": "", + "isDataAccessName": "", + "isDataStructurePath": "", + "isDataStructureName": "", + "isInterfaceStructuresPath": "", + "isInterfaceStructuresName": "", + "isEntryPointPath": "", + "isEntryPointName": "", + "treatFinalFieldsAsMandatory": false + }, + "graphRestriction": { + "classPackageExcludeFilter": "", + "classPackageIncludeFilter": "", + "classNameExcludeFilter": "", + "classNameIncludeFilter": "", + "methodNameExcludeFilter": "", + "methodNameIncludeFilter": "", + "removeByInheritance": "", // inheritance/annotation based filtering is done in a second step + "removeByAnnotation": "", + "removeByClassPackage": "", // cleanup the graph after inheritance/annotation based filtering is done + "removeByClassName": "", + "cutMappings": false, + "cutEnum": true, + "cutTests": true, + "cutClient": true, + "cutDataAccess": false, + "cutInterfaceStructures": false, + "cutDataStructures": false, + "cutGetterAndSetter": true, + "cutConstructors": true + }, + "graphTraversal": { + "forwardDepth": 6, + "backwardDepth": 6, + "classPackageExcludeFilter": "", + "classPackageIncludeFilter": "", + "classNameExcludeFilter": "", + "classNameIncludeFilter": "", + "methodNameExcludeFilter": "", + "methodNameIncludeFilter": "", + "hideMappings": false, + "hideDataStructures": false, + "hidePrivateMethods": true, + "hideInterfaceCalls": true, // indirection: implementation -> interface (is hidden) -> implementation + "onlyShowApplicationEntryPoints": false, // root node is included + "useMethodCallsForStructureDiagram": "ForwardOnly" // ForwardOnly, BothDirections, No + }, + "details": { + "aggregation": "GroupByClass", // ByClass, GroupByClass, None + "showClassGenericTypes": true, + "showMethods": true, + "showMethodParameterNames": true, + "showMethodParameterTypes": true, + "showMethodReturnType": true, + "showPackageLevels": 2, + "showDetailedClassStructure": true + }, + "rootClass": "seedu.duke.commands.ExpenseCommand", + "extensionCallbackMethod": "" // qualified.class.name#methodName - signature: public static String method(String) +} +diagram meta data end '/ + + + +digraph g { + rankdir="TB" + splines=polyline + + +'nodes +subgraph cluster_3094955 { + label=duke + labeljust=l + fillcolor="#ececec" + style=filled + + Expense1407063755[ + label=< + +
+ + + + +
+ + + +
Expense
- description: String
- payees: ArrayList<Pair<String, Money>>
- payerName: String
- totalAmount: Money
+ clear()
+ clearPayeeValue(payeeName: String)
+ successMessageString(): String
> + style=filled + margin=0 + shape=plaintext + fillcolor="#FFFFFF" +]; + + +subgraph cluster_507334344 { + label=storage + labeljust=l + fillcolor="#d8d8d8" + style=filled + +} + +subgraph cluster_867077269 { + label=commands + labeljust=l + fillcolor="#d8d8d8" + style=filled + + ExpenseCommand1418370849[ + label=< + +
+ + + + + + + + + + +
ExpenseCommand
+ addExpense(params: HashMap<String, ArrayList<String>>, argument: String, userInput: String)
+ addEqualExpense(payeeList: ArrayList<String>, payees: ArrayList<Pair<String, Money>>, totalAmountAndCurrency: Money, payerName: String, argument: String): Expense
+ addUnequalExpense(payeeList: ArrayList<String>, payees: ArrayList<Pair<String, Money>>, totalAmountAndCurrency: Money, payerName: String, argument: String): Expense
- checkDescription(argument: String)
- checkPayeeInGroup(payee: String)
+ deleteExpense(listIndex: String)
+ getCurrency(currencyString: String): CurrencyConversions
+ getListIndex(listIndex: String, listSize: int): int
+ getTotal(params: HashMap<String, ArrayList<String>>): Float
- mergeBack(splitArray: String[]): String
> + style=filled + margin=0 + shape=plaintext + fillcolor="#FFFFFF" +]; +} +} + +'edges + + + +ExpenseCommand1418370849 -> Expense1407063755[label="addExpense() -> successMessageString()"]; + + + +} +@enduml \ No newline at end of file diff --git a/docs/diagrams/ExpenseCommand_addEqualExpense.puml b/docs/diagrams/ExpenseCommand_addEqualExpense.puml new file mode 100644 index 0000000000..a2d82fab52 --- /dev/null +++ b/docs/diagrams/ExpenseCommand_addEqualExpense.puml @@ -0,0 +1,52 @@ +@startuml +Actor User +User -> ":ExpenseCommand" : addEqualExpense +activate ":ExpenseCommand" +create ":Money" +":ExpenseCommand" -> ":Money" : new +activate ":Money" +":Money" --> ":ExpenseCommand" +deactivate ":Money" +":ExpenseCommand" -> ":ExpenseCommand" : checkPayeeInGroup +activate ":ExpenseCommand" +":ExpenseCommand" -> ":Group" : isMember +activate ":Group" +":Group" --> ":ExpenseCommand" +deactivate ":Group" +create ":ExpensesException" +":ExpenseCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":ExpenseCommand" +deactivate ":ExpensesException" +":ExpenseCommand" --> ":ExpenseCommand" +deactivate ":ExpenseCommand" +create ":Pair" +":ExpenseCommand" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":ExpenseCommand" +deactivate ":Pair" +":ExpenseCommand" -> ":ExpenseCommand" : checkPayeeInGroup +activate ":ExpenseCommand" +":ExpenseCommand" -> ":Group" : isMember +activate ":Group" +":Group" --> ":ExpenseCommand" +deactivate ":Group" +create ":ExpensesException" +":ExpenseCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":ExpenseCommand" +deactivate ":ExpensesException" +":ExpenseCommand" --> ":ExpenseCommand" +deactivate ":ExpenseCommand" +create ":Pair" +":ExpenseCommand" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":ExpenseCommand" +deactivate ":Pair" +create ":Expense" +":ExpenseCommand" -> ":Expense" : new +activate ":Expense" +":Expense" --> ":ExpenseCommand" +deactivate ":Expense" +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/ExpenseCommand_addUnequalExpense.puml b/docs/diagrams/ExpenseCommand_addUnequalExpense.puml new file mode 100644 index 0000000000..f49e2f35d5 --- /dev/null +++ b/docs/diagrams/ExpenseCommand_addUnequalExpense.puml @@ -0,0 +1,63 @@ +@startuml +Actor User +User -> ":ExpenseCommand" : addUnequalExpense +activate ":ExpenseCommand" +create ":ExpensesException" +":ExpenseCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":ExpenseCommand" +deactivate ":ExpensesException" +":ExpenseCommand" -> ":ExpenseCommand" : mergeBack +activate ":ExpenseCommand" +":ExpenseCommand" --> ":ExpenseCommand" +deactivate ":ExpenseCommand" +":ExpenseCommand" -> ":ExpenseCommand" : checkPayeeInGroup +activate ":ExpenseCommand" +":ExpenseCommand" -> ":Group" : isMember +activate ":Group" +":Group" --> ":ExpenseCommand" +deactivate ":Group" +create ":ExpensesException" +":ExpenseCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":ExpenseCommand" +deactivate ":ExpensesException" +":ExpenseCommand" --> ":ExpenseCommand" +deactivate ":ExpenseCommand" +create ":Money" +":ExpenseCommand" -> ":Money" : new +activate ":Money" +":Money" --> ":ExpenseCommand" +deactivate ":Money" +create ":Pair" +":ExpenseCommand" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":ExpenseCommand" +deactivate ":Pair" +create ":ExpensesException" +":ExpenseCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":ExpenseCommand" +deactivate ":ExpensesException" +create ":ExpensesException" +":ExpenseCommand" -> ":ExpensesException" : new +activate ":ExpensesException" +":ExpensesException" --> ":ExpenseCommand" +deactivate ":ExpensesException" +create ":Money" +":ExpenseCommand" -> ":Money" : new +activate ":Money" +":Money" --> ":ExpenseCommand" +deactivate ":Money" +create ":Pair" +":ExpenseCommand" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":ExpenseCommand" +deactivate ":Pair" +create ":Expense" +":ExpenseCommand" -> ":Expense" : new +activate ":Expense" +":Expense" --> ":ExpenseCommand" +deactivate ":Expense" +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/GroupStorageClassDiagram.puml b/docs/diagrams/GroupStorageClassDiagram.puml new file mode 100644 index 0000000000..64f6025f6d --- /dev/null +++ b/docs/diagrams/GroupStorageClassDiagram.puml @@ -0,0 +1,40 @@ +@startuml +hide circle +hide empty members +skinparam classAttributeIconSize 0 + +class GroupStorage { + + isLoading: boolean {static} + + saveGroupToFile(group: Group): void + + loadGroupFromFile(groupName: String): Group + + deleteGroupFile(groupName: String): void +} + +interface FileIO<> { + + getFileReader(filePath: String): BufferedReader + + getFileWriter(filePath: String): BufferedWriter + + deleteFile(filePath: String): boolean +} + +class FileIOImpl implements FileIO { +} + +class Group { + - groupName: String + - expenseList: List + + addMember(memberName: String): User + + addExpense(expense: Expense): void + + getGroupName(): String + + getMembers(): List + + getExpenseList(): List +} + +class GroupNameChecker { + + doesGroupNameExist(groupNameToCheck: String): boolean +} + +GroupStorage "1" --> "1" FileIO : uses +GroupStorage "1" --> "*" Group : manages +Group "1" --> "1" GroupNameChecker : uses +Group "*" --> "1" GroupStorage : uses +@enduml \ No newline at end of file diff --git a/docs/diagrams/Group_settle.puml b/docs/diagrams/Group_settle.puml new file mode 100644 index 0000000000..41faba0516 --- /dev/null +++ b/docs/diagrams/Group_settle.puml @@ -0,0 +1,89 @@ +@startuml +Actor User +User -> ":Group" : settle +activate ":Group" +":Group" -> ":Group" : findUser +activate ":Group" +":Group" --> ":Group" +deactivate ":Group" +":Group" -> ":Group" : findUser +activate ":Group" +":Group" --> ":Group" +deactivate ":Group" +":Group" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Group" +deactivate ":UserInterface" +create ":Balance" +":Group" -> ":Balance" : new +activate ":Balance" +":Balance" -> ":Balance" : addExpense +activate ":Balance" +":Balance" -> ":Balance" : addMoney +activate ":Balance" +":Balance" -> ":Money" : addition +activate ":Money" +":Money" --> ":Balance" +deactivate ":Money" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" -> ":Balance" : subtractMoney +activate ":Balance" +":Balance" -> ":Money" : subtraction +activate ":Money" +":Money" --> ":Balance" +deactivate ":Money" +":Balance" -> ":Money" : multiplication +activate ":Money" +":Money" --> ":Balance" +deactivate ":Money" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" --> ":Balance" +deactivate ":Balance" +":Balance" --> ":Group" +deactivate ":Balance" +create ":Money" +":Group" -> ":Money" : new +activate ":Money" +":Money" --> ":Group" +deactivate ":Money" +":Group" -> ":Money" : addition +activate ":Money" +":Money" -> ":Money" : convertToSGD + +activate ":Money" +":Money" --> ":Money" +deactivate ":Money" +":Money" --> ":Money" +deactivate ":Money" +":Money" -> ":Money" : convertToSGD +activate ":Money" +":Money" --> ":Money" +deactivate ":Money" +":Money" --> ":Money" +deactivate ":Money" +":Money" --> ":Money" +deactivate ":Money" +":Money" --> ":Group" +deactivate ":Money" +":Group" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Group" +deactivate ":UserInterface" +":Group" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Group" +deactivate ":UserInterface" +create ":Pair" +":Group" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":Group" +deactivate ":Pair" +create ":Expense" +":Group" -> ":Expense" : new +activate ":Expense" +":Expense" --> ":Group" +deactivate ":Expense" +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/Luck_structure.puml b/docs/diagrams/Luck_structure.puml new file mode 100644 index 0000000000..8bdc93bb75 --- /dev/null +++ b/docs/diagrams/Luck_structure.puml @@ -0,0 +1,45 @@ +@startuml +digraph g { + rankdir="TB" + splines=polyline + +'nodes +Luck1407063755[ + label=< + +
+ + +
+ + + +
Luck
- currentGroup: Group
- username: String
- calculateDebt(): Expense
+ printWelcome()
+ startGambling()
> + style=filled + margin=0 + shape=plaintext + fillcolor="#FFFFFF" +]; + +SlotMachine1407063755[ + label=< + +
+ +
+ + + +
SlotMachine
- slotMachine: List<List<Character>>
- fillSlot(startRow: int, startCol: int, character: char)
- fillSlots()
# reroll()
> + style=filled + margin=0 + shape=plaintext + fillcolor="#FFFFFF" +]; + +'edges +Luck1407063755 -> SlotMachine1407063755[label="startGambling() -> reroll()"]; + + +} +@enduml \ No newline at end of file diff --git a/docs/diagrams/Parser_handleUserInput.puml b/docs/diagrams/Parser_handleUserInput.puml new file mode 100644 index 0000000000..ec64e9510f --- /dev/null +++ b/docs/diagrams/Parser_handleUserInput.puml @@ -0,0 +1,615 @@ +@startuml +participant Actor +Actor -> Parser : handleUserInput +activate Parser +Parser -> GroupCommand : exitGroup +activate GroupCommand +GroupCommand -> Group : exitGroup +activate Group +Group -> GroupStorage : saveGroupToFile +activate GroupStorage +GroupStorage -> GroupFilePath : createGroupDirectory +activate GroupFilePath +GroupFilePath --> GroupStorage +deactivate GroupFilePath +GroupStorage -> GroupFilePath : getFilePath +activate GroupFilePath +GroupFilePath --> GroupStorage +deactivate GroupFilePath +GroupStorage -> FileIO : getFileWriter +activate FileIO +FileIO --> GroupStorage +deactivate FileIO +GroupStorage -> GroupStorage : saveGroupName +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +GroupStorage -> GroupStorage : saveMembers +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +GroupStorage -> GroupStorage : saveExpenses +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +create GroupSaveException +GroupStorage -> GroupSaveException : new +activate GroupSaveException +GroupSaveException --> GroupStorage +deactivate GroupSaveException +GroupStorage --> Group +deactivate GroupStorage +Group --> GroupCommand +deactivate Group +GroupCommand --> Parser +deactivate GroupCommand +Parser -> Help : printHelp +activate Help +Help --> Parser +deactivate Help +Parser -> GroupCommand : createGroup +activate GroupCommand +GroupCommand -> Group : getOrCreateGroup +activate Group +Group -> GroupNameChecker : doesGroupNameExist +activate GroupNameChecker +GroupNameChecker -> GroupNameChecker : checkGroupNameInDirectory +activate GroupNameChecker +GroupNameChecker --> GroupNameChecker +deactivate GroupNameChecker +GroupNameChecker --> Group +deactivate GroupNameChecker +create Group +Group -> Group : new +activate Group +Group --> Group +deactivate Group +Group -> GroupNameChecker : doesGroupNameExist +activate GroupNameChecker +GroupNameChecker -> GroupNameChecker : checkGroupNameInDirectory +activate GroupNameChecker +GroupNameChecker --> GroupNameChecker +deactivate GroupNameChecker +GroupNameChecker --> Group +deactivate GroupNameChecker +Group --> GroupCommand +deactivate Group +GroupCommand --> Parser +deactivate GroupCommand +create ExpensesException +Parser -> ExpensesException : new +activate ExpensesException +ExpensesException --> Parser +deactivate ExpensesException +Parser -> GroupCommand : deleteGroup +activate GroupCommand +create GroupStorage +GroupCommand -> GroupStorage : new +activate GroupStorage +GroupStorage --> GroupCommand +deactivate GroupStorage +GroupCommand -> GroupNameChecker : doesGroupNameExist +activate GroupNameChecker +GroupNameChecker -> GroupNameChecker : checkGroupNameInDirectory +activate GroupNameChecker +GroupNameChecker -> GroupNameChecker : extractGroupNameFromFile +activate GroupNameChecker +GroupNameChecker --> GroupNameChecker +deactivate GroupNameChecker +GroupNameChecker --> GroupNameChecker +deactivate GroupNameChecker +GroupNameChecker --> GroupCommand +deactivate GroupNameChecker +GroupCommand -> GroupStorage : deleteGroupFile +activate GroupStorage +GroupStorage -> GroupFilePath : getFilePath +activate GroupFilePath +GroupFilePath --> GroupStorage +deactivate GroupFilePath +GroupStorage -> FileIO : deleteFile +activate FileIO +FileIO --> GroupStorage +deactivate FileIO +create GroupDeleteException +GroupStorage -> GroupDeleteException : new +activate GroupDeleteException +create UniversalExceptions +GroupDeleteException -> UniversalExceptions : new +activate UniversalExceptions +UniversalExceptions --> GroupDeleteException +deactivate UniversalExceptions +GroupDeleteException --> GroupStorage +deactivate GroupDeleteException +create GroupDeleteException +GroupStorage -> GroupDeleteException : new +activate GroupDeleteException +create UniversalExceptions +GroupDeleteException -> UniversalExceptions : new +activate UniversalExceptions +UniversalExceptions --> GroupDeleteException +deactivate UniversalExceptions +GroupDeleteException --> GroupStorage +deactivate GroupDeleteException +GroupStorage --> GroupCommand +deactivate GroupStorage +GroupCommand --> Parser +deactivate GroupCommand +Parser -> ExpenseCommand : deleteExpense +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand -> ExpenseCommand : getListIndex +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +ExpenseCommand -> Expense : toString +activate Expense +Expense --> ExpenseCommand +deactivate Expense +ExpenseCommand -> Group : deleteExpense +activate Group +Group --> ExpenseCommand +deactivate Group +ExpenseCommand --> Parser +deactivate ExpenseCommand +create ExpensesException +Parser -> ExpensesException : new +activate ExpensesException +ExpensesException --> Parser +deactivate ExpensesException +Parser -> GroupCommand : addMember +activate GroupCommand +GroupCommand -> Group : addMember +activate Group +Group -> Group : isMember +activate Group +Group --> Group +deactivate Group +create User +Group -> User : new +activate User +User --> Group +deactivate User +Group --> GroupCommand +deactivate Group +GroupCommand --> Parser +deactivate GroupCommand +Parser -> GroupCommand : enterGroup +activate GroupCommand +GroupCommand -> Group : enterGroup +activate Group +Group -> GroupNameChecker : doesGroupNameExist +activate GroupNameChecker +GroupNameChecker -> GroupNameChecker : checkGroupNameInDirectory +activate GroupNameChecker +GroupNameChecker --> GroupNameChecker +deactivate GroupNameChecker +GroupNameChecker --> Group +deactivate GroupNameChecker +Group -> GroupStorage : loadGroupFromFile +activate GroupStorage +GroupStorage -> GroupFilePath : getFilePath +activate GroupFilePath +GroupFilePath --> GroupStorage +deactivate GroupFilePath +GroupStorage -> FileIO : getFileReader +activate FileIO +FileIO --> GroupStorage +deactivate FileIO +GroupStorage -> GroupStorage : loadGroupName +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +create GroupLoadException +GroupStorage -> GroupLoadException : new +activate GroupLoadException +GroupLoadException --> GroupStorage +deactivate GroupLoadException +GroupStorage -> GroupStorage : loadMembers +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +GroupStorage -> GroupStorage : loadExpenses +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +create GroupLoadException +GroupStorage -> GroupLoadException : new +activate GroupLoadException +GroupLoadException --> GroupStorage +deactivate GroupLoadException +create GroupLoadException +GroupStorage -> GroupLoadException : new +activate GroupLoadException +GroupLoadException --> GroupStorage +deactivate GroupLoadException +create GroupLoadException +GroupStorage -> GroupLoadException : new +activate GroupLoadException +GroupLoadException --> GroupStorage +deactivate GroupLoadException +GroupStorage --> Group +deactivate GroupStorage +Group --> GroupCommand +deactivate Group +GroupCommand --> Parser +deactivate GroupCommand +Parser -> GroupCommand : exitGroup +activate GroupCommand +GroupCommand -> Group : exitGroup +activate Group +Group -> GroupStorage : saveGroupToFile +activate GroupStorage +GroupStorage -> GroupFilePath : createGroupDirectory +activate GroupFilePath +GroupFilePath --> GroupStorage +deactivate GroupFilePath +GroupStorage -> GroupFilePath : getFilePath +activate GroupFilePath +GroupFilePath --> GroupStorage +deactivate GroupFilePath +GroupStorage -> FileIO : getFileWriter +activate FileIO +FileIO --> GroupStorage +deactivate FileIO +GroupStorage -> GroupStorage : saveGroupName +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +GroupStorage -> GroupStorage : saveMembers +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +GroupStorage -> GroupStorage : saveExpenses +activate GroupStorage +GroupStorage --> GroupStorage +deactivate GroupStorage +create GroupSaveException +GroupStorage -> GroupSaveException : new +activate GroupSaveException +GroupSaveException --> GroupStorage +deactivate GroupSaveException +GroupStorage --> Group +deactivate GroupStorage +Group --> GroupCommand +deactivate Group +GroupCommand --> Parser +deactivate GroupCommand +Parser -> ExpenseCommand : addExpense +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand -> ExpenseCommand : getTotal +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +ExpenseCommand -> ExpenseCommand : getCurrency +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +create Money +ExpenseCommand -> Money : new +activate Money +Money --> ExpenseCommand +deactivate Money +ExpenseCommand -> ExpenseCommand : checkDescription +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +ExpenseCommand -> ExpenseCommand : addUnequalExpense +activate ExpenseCommand +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand -> ExpenseCommand : mergeBack +activate ExpenseCommand +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +ExpenseCommand -> ExpenseCommand : checkPayeeInGroup +activate ExpenseCommand +ExpenseCommand -> Group : isMember +activate Group +Group --> ExpenseCommand +deactivate Group +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +create Money +ExpenseCommand -> Money : new +activate Money +Money --> ExpenseCommand +deactivate Money +create Pair +ExpenseCommand -> Pair : new +activate Pair +Pair --> ExpenseCommand +deactivate Pair +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +create Money +ExpenseCommand -> Money : new +activate Money +Money --> ExpenseCommand +deactivate Money +create Pair +ExpenseCommand -> Pair : new +activate Pair +Pair --> ExpenseCommand +deactivate Pair +create Expense +ExpenseCommand -> Expense : new +activate Expense +Expense -> Expense : printSuccessMessage +activate Expense +Expense --> Expense +deactivate Expense +Expense --> ExpenseCommand +deactivate Expense +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +ExpenseCommand -> ExpenseCommand : addEqualExpense +activate ExpenseCommand +create Money +ExpenseCommand -> Money : new +activate Money +Money --> ExpenseCommand +deactivate Money +ExpenseCommand -> ExpenseCommand : checkPayeeInGroup +activate ExpenseCommand +ExpenseCommand -> Group : isMember +activate Group +Group --> ExpenseCommand +deactivate Group +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +create Pair +ExpenseCommand -> Pair : new +activate Pair +Pair --> ExpenseCommand +deactivate Pair +ExpenseCommand -> ExpenseCommand : checkPayeeInGroup +activate ExpenseCommand +ExpenseCommand -> Group : isMember +activate Group +Group --> ExpenseCommand +deactivate Group +create ExpensesException +ExpenseCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ExpenseCommand +deactivate ExpensesException +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +create Pair +ExpenseCommand -> Pair : new +activate Pair +Pair --> ExpenseCommand +deactivate Pair +create Expense +ExpenseCommand -> Expense : new +activate Expense +Expense -> Expense : printSuccessMessage +activate Expense +Expense --> Expense +deactivate Expense +Expense --> ExpenseCommand +deactivate Expense +ExpenseCommand --> ExpenseCommand +deactivate ExpenseCommand +ExpenseCommand -> Group : addExpense +activate Group +Group --> ExpenseCommand +deactivate Group +ExpenseCommand -> Parser : group -> +activate Parser +Parser -> Group : settle +activate Group +Group -> Group : findUser +activate Group +Group --> Group +deactivate Group +Group -> Group : findUser +activate Group +Group --> Group +deactivate Group +Group -> Group : calculateOutstandingAmount +activate Group +Group --> Group +deactivate Group +Group -> Group : updateBalancesAfterSettlement +activate Group +Group --> Group +deactivate Group +Group --> Parser +deactivate Group +Parser --> ExpenseCommand +deactivate Parser +ExpenseCommand --> Parser +deactivate ExpenseCommand +create LuckException +Parser -> LuckException : new +activate LuckException +create UniversalExceptions +LuckException -> UniversalExceptions : new +activate UniversalExceptions +UniversalExceptions --> LuckException +deactivate UniversalExceptions +LuckException --> Parser +deactivate LuckException +create LuckException +Parser -> LuckException : new +activate LuckException +create UniversalExceptions +LuckException -> UniversalExceptions : new +activate UniversalExceptions +UniversalExceptions --> LuckException +deactivate UniversalExceptions +LuckException --> Parser +deactivate LuckException +create Luck +Parser -> Luck : new +activate Luck +Luck --> Parser +deactivate Luck +Parser -> Luck : printWelcome +activate Luck +Luck --> Parser +deactivate Luck +Parser -> Luck : startGambling +activate Luck +create SlotMachine +Luck -> SlotMachine : new +activate SlotMachine +SlotMachine -> SlotMachine : fillSlots +activate SlotMachine +SlotMachine -> SlotMachine : fillSlot +activate SlotMachine +SlotMachine --> SlotMachine +deactivate SlotMachine +SlotMachine --> SlotMachine +deactivate SlotMachine +SlotMachine --> Luck +deactivate SlotMachine +Luck -> SlotMachine : reroll +activate SlotMachine +SlotMachine -> SlotMachine : fillSlots +activate SlotMachine +SlotMachine -> SlotMachine : fillSlot +activate SlotMachine +SlotMachine --> SlotMachine +deactivate SlotMachine +SlotMachine --> SlotMachine +deactivate SlotMachine +SlotMachine --> Luck +deactivate SlotMachine +Luck --> Parser +deactivate Luck +Parser -> ListCommand : printList +activate ListCommand +create ExpensesException +ListCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> ListCommand +deactivate ExpensesException +ListCommand -> Expense : toString +activate Expense +Expense --> ListCommand +deactivate Expense +ListCommand --> Parser +deactivate ListCommand +Parser -> BalanceCommand : handleBalance +activate BalanceCommand +create ExpensesException +BalanceCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> BalanceCommand +deactivate ExpensesException +BalanceCommand -> Group : isMember +activate Group +Group --> BalanceCommand +deactivate Group +create ExpensesException +BalanceCommand -> ExpensesException : new +activate ExpensesException +ExpensesException --> BalanceCommand +deactivate ExpensesException +create Balance +BalanceCommand -> Balance : new +activate Balance +create Balance +Balance -> Balance : new +activate Balance +Balance -> Balance : addExpense +activate Balance +Balance --> Balance +deactivate Balance +Balance --> Balance +deactivate Balance +Balance --> BalanceCommand +deactivate Balance +BalanceCommand -> Balance : printBalance +activate Balance +Balance -> UserInterface : printMessage +activate UserInterface +UserInterface --> Balance +deactivate UserInterface +Balance --> BalanceCommand +deactivate Balance +BalanceCommand --> Parser +deactivate BalanceCommand +Parser -> Help : printHelp +activate Help +Help --> Parser +deactivate Help +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/addMember1.puml b/docs/diagrams/addMember1.puml new file mode 100644 index 0000000000..518c84bfe0 --- /dev/null +++ b/docs/diagrams/addMember1.puml @@ -0,0 +1,29 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":User" as JohnUser + +User -> GroupCommand: "member John" +activate GroupCommand +GroupCommand -> Group: addMember("John") +activate Group + +Group -> Group: isMember("John") +activate Group +deactivate Group + +Group -> JohnUser: new User("John") +activate JohnUser +JohnUser --> Group: johnUser +deactivate JohnUser + +Group -> Group: members.add(johnUser) +activate Group +deactivate Group + +Group --> GroupCommand: "John has been added to group" +deactivate Group +GroupCommand --> User: "John has been added to group" +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/addMember2.puml b/docs/diagrams/addMember2.puml new file mode 100644 index 0000000000..71b2db9fe1 --- /dev/null +++ b/docs/diagrams/addMember2.puml @@ -0,0 +1,22 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group + +User -> GroupCommand: "member John" +activate GroupCommand + +GroupCommand -> Group: addMember("John") +activate Group + +Group -> Group: isMember("John") +activate Group +Group --> Group: true +deactivate Group + +Group --> GroupCommand: "John is already a member of group" +deactivate Group + +GroupCommand --> User: "John is already a member of group" +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/addMember3.puml b/docs/diagrams/addMember3.puml new file mode 100644 index 0000000000..a70938a58d --- /dev/null +++ b/docs/diagrams/addMember3.puml @@ -0,0 +1,34 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":User" as NewUser + +User -> GroupCommand: "member USER_NAME" +activate GroupCommand + +GroupCommand -> Group: addMember(memberName) +activate Group + +alt is valid member name + alt is not a member + + Group -> NewUser: new User(memberName) + activate NewUser + NewUser --> Group: newUser + deactivate NewUser + + Group --> GroupCommand: success message + else is already a member + + Group --> GroupCommand: failure message + end +else is invalid member name + + Group --> GroupCommand: failure message + deactivate Group +end + +GroupCommand --> User: command result +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/groupStorage1.puml b/docs/diagrams/groupStorage1.puml new file mode 100644 index 0000000000..f76456b461 --- /dev/null +++ b/docs/diagrams/groupStorage1.puml @@ -0,0 +1,47 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":GroupStorage" as GroupStorage +participant ":FileIO" as FileIO + +User -> GroupCommand: "exit Project Team" +activate GroupCommand + +GroupCommand -> Group: exitGroup("Project Team") +activate Group + +Group -> GroupStorage: saveGroupToFile(projectTeamGroup) +activate GroupStorage + +GroupStorage -> FileIO: getFileWriter(filePath) +activate FileIO +FileIO --> GroupStorage: writer +deactivate FileIO + +GroupStorage -> GroupStorage: saveGroupName(writer, groupName) +activate GroupStorage +deactivate GroupStorage + +GroupStorage -> GroupStorage: saveMembers(writer, members) +activate GroupStorage +deactivate GroupStorage + +GroupStorage -> GroupStorage: saveExpenses(writer, expenses) +activate GroupStorage +deactivate GroupStorage + +GroupStorage -> FileIO: writer.close() +activate FileIO +FileIO --> GroupStorage +deactivate FileIO + +GroupStorage --> Group +deactivate GroupStorage + +Group --> GroupCommand +deactivate Group + +GroupCommand --> User +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/groupStorage2.puml b/docs/diagrams/groupStorage2.puml new file mode 100644 index 0000000000..9ec37461ed --- /dev/null +++ b/docs/diagrams/groupStorage2.puml @@ -0,0 +1,57 @@ +@startuml +actor User +participant ":GroupCommand" as GroupCommand +participant ":Group" as Group +participant ":GroupStorage" as GroupStorage +participant ":FileIO" as FileIO + +User -> GroupCommand: "enter Project Team" +activate GroupCommand + +GroupCommand -> Group: enterGroup("Project Team") +activate Group + +alt group does not exist in memory + + alt group file exists + + Group -> GroupStorage: loadGroupFromFile("Project Team") + activate GroupStorage + + GroupStorage -> FileIO: getFileReader(filePath) + activate FileIO + FileIO --> GroupStorage: reader + deactivate FileIO + + GroupStorage -> GroupStorage: loadGroupName(reader) + activate GroupStorage + deactivate GroupStorage + + GroupStorage -> GroupStorage: loadMembers(reader, group) + activate GroupStorage + deactivate GroupStorage + + GroupStorage -> GroupStorage: loadExpenses(reader, group) + activate GroupStorage + deactivate GroupStorage + + GroupStorage -> FileIO: reader.close() + activate FileIO + FileIO --> GroupStorage + deactivate FileIO + + GroupStorage --> Group: loadedGroup + deactivate GroupStorage + else group file does not exist + + Group --> GroupCommand: group does not exist + end +else group exists in memory + + Group --> GroupCommand: group found in memory + deactivate Group +end + +GroupCommand --> User +deactivate GroupCommand +@enduml \ No newline at end of file diff --git a/docs/diagrams/luck_sequence.puml b/docs/diagrams/luck_sequence.puml new file mode 100644 index 0000000000..5f952aca9a --- /dev/null +++ b/docs/diagrams/luck_sequence.puml @@ -0,0 +1,151 @@ +@startuml +Actor User +User -> ":LuckCommand" : handleLuck +activate ":LuckCommand" +create ":LuckException" +":LuckCommand" -> ":LuckException" : new +activate ":LuckException" +create ":UniversalExceptions" +":LuckException" -> ":UniversalExceptions" : new +activate ":UniversalExceptions" +":UniversalExceptions" --> ":LuckException" +deactivate ":UniversalExceptions" +":LuckException" --> ":LuckCommand" +deactivate ":LuckException" +create ":LuckException" +":LuckCommand" -> ":LuckException" : new +activate ":LuckException" +create ":UniversalExceptions" +":LuckException" -> ":UniversalExceptions" : new +activate ":UniversalExceptions" +":UniversalExceptions" --> ":LuckException" +deactivate ":UniversalExceptions" +":LuckException" --> ":LuckCommand" +deactivate ":LuckException" +create ":Luck" +":LuckCommand" -> ":Luck" : new +activate ":Luck" +":Luck" --> ":LuckCommand" +deactivate ":Luck" +":LuckCommand" -> ":Luck" : printWelcome +activate ":Luck" +":Luck" --> ":LuckCommand" +deactivate ":Luck" +":LuckCommand" -> ":Luck" : startGambling +activate ":Luck" +create ":SlotMachine" +":Luck" -> ":SlotMachine" : new +activate ":SlotMachine" +":SlotMachine" -> ":SlotMachine" : fillSlots +activate ":SlotMachine" +":SlotMachine" -> ":SlotMachine" : fillSlot +activate ":SlotMachine" +":SlotMachine" --> ":SlotMachine" +deactivate ":SlotMachine" +":SlotMachine" --> ":SlotMachine" +deactivate ":SlotMachine" +":SlotMachine" --> ":Luck" +deactivate ":SlotMachine" +":Luck" -> ":Help" : printHelp +activate ":Help" +":Help" --> ":Luck" +deactivate ":Help" +":Luck" -> ":SlotMachine" : reroll +activate ":SlotMachine" +":SlotMachine" -> ":SlotMachine" : fillSlots +activate ":SlotMachine" +":SlotMachine" -> ":SlotMachine" : fillSlot +activate ":SlotMachine" +":SlotMachine" --> ":SlotMachine" +deactivate ":SlotMachine" +":SlotMachine" --> ":SlotMachine" +deactivate ":SlotMachine" +":SlotMachine" --> ":Luck" +deactivate ":SlotMachine" +":Luck" -> ":Group" : settleAll +activate ":Group" +":Group" -> ":Group" : findUser +activate ":Group" +":Group" --> ":Group" +deactivate ":Group" +":Group" -> ":Group" : settle +activate ":Group" +":Group" -> ":Group" : findUser +activate ":Group" +":Group" --> ":Group" +deactivate ":Group" +":Group" -> ":Group" : findUser +activate ":Group" +":Group" --> ":Group" +deactivate ":Group" +":Group" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Group" +deactivate ":UserInterface" +create ":Balance" +":Group" -> ":Balance" : new +activate ":Balance" +":Balance" --> ":Group" +deactivate ":Balance" +create ":Money" +":Group" -> ":Money" : new +activate ":Money" +":Money" --> ":Group" +deactivate ":Money" +":Group" -> ":Money" : addition +activate ":Money" +":Money" --> ":Group" +deactivate ":Money" +":Group" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Group" +deactivate ":UserInterface" +":Group" -> ":UserInterface" : printMessage +activate ":UserInterface" +":UserInterface" --> ":Group" +deactivate ":UserInterface" +create ":Pair" +":Group" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":Group" +deactivate ":Pair" +create ":Expense" +":Group" -> ":Expense" : new +activate ":Expense" +":Expense" --> ":Group" +deactivate ":Expense" +":Group" --> ":Group" +deactivate ":Group" +":Group" --> ":Luck" +deactivate ":Group" +":Luck" -> ":Luck" : calculateDebt +activate ":Luck" +create ":Money" +":Luck" -> ":Money" : new +activate ":Money" +":Money" --> ":Luck" +deactivate ":Money" +create ":Pair" +":Luck" -> ":Pair" : new +activate ":Pair" +":Pair" --> ":Luck" +deactivate ":Pair" +create ":Expense" +":Luck" -> ":Expense" : new +activate ":Expense" +":Expense" --> ":Luck" +deactivate ":Expense" +":Luck" --> ":Luck" +deactivate ":Luck" +":Luck" -> ":Expense" : successMessageString +activate ":Expense" +":Expense" --> ":Luck" +deactivate ":Expense" +":Luck" -> ":Group" : addExpense +activate ":Group" +":Group" --> ":Luck" +deactivate ":Group" +":Luck" --> ":LuckCommand" +deactivate ":Luck" +return +@enduml \ No newline at end of file diff --git a/docs/images/ArchitectureDiagram.jpg b/docs/images/ArchitectureDiagram.jpg new file mode 100644 index 0000000000..f9aa85438f Binary files /dev/null and b/docs/images/ArchitectureDiagram.jpg differ diff --git a/docs/images/BalanceStructure.png b/docs/images/BalanceStructure.png new file mode 100644 index 0000000000..7621c6834b Binary files /dev/null and b/docs/images/BalanceStructure.png differ diff --git a/docs/images/ExpenseClassDiagram.png b/docs/images/ExpenseClassDiagram.png new file mode 100644 index 0000000000..b0b5cfe418 Binary files /dev/null and b/docs/images/ExpenseClassDiagram.png differ diff --git a/docs/images/GroupCommand.png b/docs/images/GroupCommand.png new file mode 100644 index 0000000000..629b2d3570 Binary files /dev/null and b/docs/images/GroupCommand.png differ diff --git a/docs/images/GroupStorageClassDiagram.png b/docs/images/GroupStorageClassDiagram.png new file mode 100644 index 0000000000..add46f40d3 Binary files /dev/null and b/docs/images/GroupStorageClassDiagram.png differ diff --git a/docs/images/Junxiang.jpg b/docs/images/Junxiang.jpg new file mode 100644 index 0000000000..65bc3f0ed9 Binary files /dev/null and b/docs/images/Junxiang.jpg differ diff --git a/docs/images/LuckStructure.png b/docs/images/LuckStructure.png new file mode 100644 index 0000000000..c00a9c38e8 Binary files /dev/null and b/docs/images/LuckStructure.png differ diff --git a/docs/images/Shaoliang.png b/docs/images/Shaoliang.png new file mode 100644 index 0000000000..5387e23053 Binary files /dev/null and b/docs/images/Shaoliang.png differ diff --git a/docs/images/addMember1.png b/docs/images/addMember1.png new file mode 100644 index 0000000000..5b5902889a Binary files /dev/null and b/docs/images/addMember1.png differ diff --git a/docs/images/addMember2.png b/docs/images/addMember2.png new file mode 100644 index 0000000000..d409fb2d2a Binary files /dev/null and b/docs/images/addMember2.png differ diff --git a/docs/images/addMember3.png b/docs/images/addMember3.png new file mode 100644 index 0000000000..7779b7d334 Binary files /dev/null and b/docs/images/addMember3.png differ diff --git a/docs/images/balance.png b/docs/images/balance.png new file mode 100644 index 0000000000..6ddaa310aa Binary files /dev/null and b/docs/images/balance.png differ diff --git a/docs/images/equal.png b/docs/images/equal.png new file mode 100644 index 0000000000..9427691e40 Binary files /dev/null and b/docs/images/equal.png differ diff --git a/docs/images/groupStorage1.png b/docs/images/groupStorage1.png new file mode 100644 index 0000000000..dfee4b60d8 Binary files /dev/null and b/docs/images/groupStorage1.png differ diff --git a/docs/images/groupStorage2.png b/docs/images/groupStorage2.png new file mode 100644 index 0000000000..9ccffcea2e Binary files /dev/null and b/docs/images/groupStorage2.png differ diff --git a/docs/images/hafiz.png b/docs/images/hafiz.png new file mode 100644 index 0000000000..63f392f6fc Binary files /dev/null and b/docs/images/hafiz.png differ diff --git a/docs/images/luckGambling.png b/docs/images/luckGambling.png new file mode 100644 index 0000000000..7dfcb073ba Binary files /dev/null and b/docs/images/luckGambling.png differ diff --git a/docs/images/settle.png b/docs/images/settle.png new file mode 100644 index 0000000000..e2e0a03bd6 Binary files /dev/null and b/docs/images/settle.png differ diff --git a/docs/images/unequal.png b/docs/images/unequal.png new file mode 100644 index 0000000000..c987b55759 Binary files /dev/null and b/docs/images/unequal.png differ diff --git a/docs/team/Cohii2.md b/docs/team/Cohii2.md new file mode 100644 index 0000000000..e3321505c3 --- /dev/null +++ b/docs/team/Cohii2.md @@ -0,0 +1,59 @@ +# Heng Junxiang - Project Portfolio Page + +## Overview + +Split-liang is a CLI application that helps you split expenses with friends in a fun way! If you can type fast, +Split-liang can help you manage your expenses faster than traditional GUI apps + +### Summary of Contributions + +* **Code Contributions**: + [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=Cohii2&tabRepo=AY2324S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + + +* **Enhancements implemented**: + + 1. Implemented Parser + * Parser feature + * What it does: Reads user input from Command Line and parses user input. + Splits user input into `command`, `argument` and `params` as inputs for other functions. + * Test Code for Parser feature. + + 2. Implemented Balance Class + * Balance feature + * What it does: allows the user to see their Balance. Users can view a list of other users that + owe them money/are owed money by them. + * Test Code for Balance feature. + + 3. Implemented User Inferface + * PrintMessage feature + * What it does: Prints a frame and a cat graphic around outputs. + Frame has "SUCCESS" or "ERROR" messages so users can understand type of message printed. + + +* **Contributions to UG** + * Added documentation relating to Balance function [#88](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/88) + + +* **Contributions to DG** + * Added Architecture Diagram [#175](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/175) + * Added the implementation for the Balance Class [#193](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/193) + + +* **Contributions to team-based tasks** + * Debug code and Fix Checkstyle Errors. + + +* **Review/mentoring contributions** + * PRs reviewed (with non-trivial review comments): + [#64](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/64) + [#68](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/68) + [#169](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/169) + + + +* **Contributions beyond the project team** + * [Bug Catching for Mock PE](https://github.com/Cohii2/ped/issues) + + + diff --git a/docs/team/avrilgk.md b/docs/team/avrilgk.md new file mode 100644 index 0000000000..91bdc5f9c9 --- /dev/null +++ b/docs/team/avrilgk.md @@ -0,0 +1,65 @@ +# Avril Guok - Project Portfolio Page + +## Overview + +Split-liang is a CLI application that helps you split expenses with friends in a fun way! If you can type fast, +Split-liang can help you manage your expenses faster than traditional GUI apps + +### Summary of Contributions + +* **Code Contributions**: + [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=avrilgk&tabRepo=AY2324S2-CS2113-T15-3/tp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false + ) + +* **Enhancements implemented**: + + 1. Implemented features relating to group creation + * Group creation feature + * What it does: allows the user to create a new group with a specified group name + * Group entering feature + * What it does: allows the user to enter an existing group with a specified group name + * Group exit feature + * What it does: allows the user to exit the current group + * Group deletion feature + * What it does: allows the user to delete an existing group with a specified group name + + 2. Implemented settle function + * What it does: Allows the user to settle the debts within the group + * This feature can be used for both scenarios where the bill is split equally or unequally. Entering the + settle + function between Person A /user Person B will settle expenses between the two, showing what A owes B. + * The settle function will also show the total amount owed by each person in the group. + * After settling, the balance of each person will be 0. + +* **Contributions to UG** + * Added documentation relating to the group function (create, enter, exit, delete group) + * Added documentation relating to the settle function + + +* **Contributions to DG** + * Added the implementation for the group creation feature. + * Added the implementation for the settle feature. + + +* **Contributions to team-based tasks** + * Added a short introduction to Split-Liang in UG + * Added FAQs in UG + + +* **Review/mentoring contributions** + * Examples of PRs reviewed (with non-trivial review comments): + * + [#56](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/56) + * + [#64](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/64) + * + [#152](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/152) + * + [#182](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/182) + +* **Contributions beyond the project team** + + https://github.com/avrilgk/ped/issues + + + diff --git a/docs/team/hafizuddin-a.md b/docs/team/hafizuddin-a.md new file mode 100644 index 0000000000..c00204987a --- /dev/null +++ b/docs/team/hafizuddin-a.md @@ -0,0 +1,37 @@ +# Hafizuddin - Project Portfolio Page + +### Project: Split-liang + +Split-liang is a CLI application that helps you split expenses with friends in a fun way! If you can type fast, Split-liang can help you manage your expenses faster than traditional GUI apps. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to add members to groups. + * What it does: allows the user to add names of members to groups so that expenses can be split among them. + +* **New Feature**: Added database storage for groups and members. + * What it does: allows the user to save the groups and members to a file so that they can be accessed later. + +* **Code contributed**: [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=hafizuddin-a&tabRepo=AY2324S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Documentation**: + * User Guide: + * Added documentation for the addition of members to groups [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + * Added documentation for the storage of groups and members [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + * Added command summary in a table[\#159](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/159) + * Added content page for the user guide [\#159](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/159) + * Developer Guide: + * Added implementation for the addition of members to groups [\#87](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/87) + * Added implementation for the storage of group data [\#162](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/162) + * Added 3 sequence diagrams for the addition of members to groups [\#171](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/171) + * Added 2 sequence diagram for the storage of group data [\#171](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/171) + * Added test cases for creation of groups [\#171](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/171) + * Added test cases for the addition of members to groups [\#171](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/171) + * Added test cases for the storage of group data [\#171](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/171) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#55](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/55) + * Triaged and assigned all the bugs during demo to other contributors [\#176](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/176) + 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/monkescripts.md b/docs/team/monkescripts.md new file mode 100644 index 0000000000..9d413f358e --- /dev/null +++ b/docs/team/monkescripts.md @@ -0,0 +1,43 @@ +### Project: Split-liang + +Split-liang is a CLI application that helps you split expenses with friends in a fun way! If you can type fast, Split-liang can help you manage your expenses faster than traditional GUI apps. + +* **Code Contributions**: + [RepoSense Link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=MonkeScripts&tabRepo=AY2324S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + +* **Enhancements implemented**: + + 1. Implemented Help Menu + * Simple print method to print the list of commands + + 2. Implemented Currency Conversion: + * Allow users to enter different currencies in Split-liang + * Handy for managing bills while travelling overseas + * Able to add, subtract, multiply, divide different currencies and choosing which currency to represent the result in + * Offer a convenient conversion to base currency SGD if necessary + + 3. Implemented Slots feature: + * Offer a unique and exciting slot machine game for users to clear their debts + * Users spend $10USD to roll the slots at a chance to clear their debts + * To win in slots, all 3 middle slots must show the same character + * If the slots show different characters, a new debt of $10USD owed by the player would be credited to a random user in the same group + +* **Contributions to UG** + * Added documentation relating to the help menu + * Added documentation relating to the slot machine, Luck class + +* **Contributions to DG** + * Added implementation for Luck + * Added sequence diagrams for Luck, Creating Group and expenses + +* **Contributions to team-based tasks** + * Catching bugs and checkstyle violations + +* **Review/mentoring contributions** + * PRs reviewed (with non-trivial review comments): + * [comment1](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/30) + * [comment2](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/31) + * [comment3](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/155) + +* **Contributions beyond the project team** +* [other repo](https://github.com/MonkeScripts/ped) \ No newline at end of file diff --git a/docs/team/mukund1403.md b/docs/team/mukund1403.md new file mode 100644 index 0000000000..4c69f48b54 --- /dev/null +++ b/docs/team/mukund1403.md @@ -0,0 +1,44 @@ +# Ayagari Mukund - Project Portfolio Page + +### Project: Split-liang + +Split Liang is an application that helps you split expenses with friends in a fun way! If you are tired of keeping track +of calculating who owes who, Split-liang is here to help you. Split-liang allows you to keep track through the creation +of groups and expenses so that you can simultaneously manage expenses with multiple friend groups! + + +### Summary of Contributions + +Given below are my contributions to the project: + +- **New Feature** Added the ability to add expenses to a group. + - What it does: allows a new expense to be added to the group. The expense added can be split equally or unequally + between users. It can also be in any currency. + - Justification: This is a key feature to the working of the app since it helps with the expense tracking. + - Highlights: The merging of existing code with the Money class required major refactoring of the code since all + float amounts had to be replaced with the instances of the Money object. + +- **New Feature** Added the ability to list all expenses within a group. + - What it does: shows all the expenses in the group along with all the details. + - Justification: Users can see all expenses to keep track or even choose which ones to delete + +- **New Feature** Added the ability to delete expenses within a group. + - What it does: Allows users to delete existing expenses by referring to list index. + - Justification: This feature gives users greater flexibility in managing their expenses and also allows them to + fix mistakes they make when keying in the expense details. + + +- **Code contributed**: [RepoSense link](https://nus-cs2113-ay2324s2.github.io/tp-dashboard/?search=t15&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2024-02-23&tabOpen=true&tabType=authorship&tabAuthor=mukund1403&tabRepo=AY2324S2-CS2113-T15-3%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false) + + +- **Documentation**: + - User Guide: + - Added documentation for addition of expense to a group [\#179](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/179/commits) + - Added documentation to list all members in a group [\#179](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/179/commits) + - Added documentation for deletion of expense from a group [\#179](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/179/commits) + - Added documentation for currencies available [\#179](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/179/commits) + - Developer Guide: + - Added implementation of expense feature + +- **Community**: + - PRs reviewed (with non-trivial review comments): [\#65](https://github.com/AY2324S2-CS2113-T15-3/tp/pull/65) diff --git a/src/main/java/seedu/duke/Balance.java b/src/main/java/seedu/duke/Balance.java new file mode 100644 index 0000000000..1cce67e570 --- /dev/null +++ b/src/main/java/seedu/duke/Balance.java @@ -0,0 +1,153 @@ +package seedu.duke; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +//@@author Cohii2 +public class Balance { + protected String userName; + protected Map> balanceList; + + public Balance(String userName, Group group) { + this(userName, group.getExpenseList(), group.getMembers()); + } + + /** + * Populates balanceList with list of Users and list of Expenses in Group. + * + * @param userName The name of User to print the balance of. + * @param expenses The list of Expenses in Group. + * @param users The list of Users in Group + */ + public Balance(String userName, List expenses, List users) { + this.userName = userName; + this.balanceList = new HashMap<>(); + + // Populate balanceList with other Users from Group + for (User user : users) { + if (!user.getName().equals(userName)) { + balanceList.put(user.getName(), new ArrayList()); + } + } + + // Add Expenses to balanceList + for (Expense expense : expenses) { + addExpense(expense); + } + } + + public Map> getBalanceList() { + return balanceList; + } + + /** + * Adds an Expense to balanceList. + * + * @param expense The Expense to be added. + */ + private void addExpense(Expense expense) { + String payerName = expense.getPayerName(); + List> payees = expense.getPayees(); + + if(payerName.equals(userName)) { + for(Pair payee : payees) { + + String payeeName = payee.getKey(); + Money payeeMoney = payee.getValue(); + + if (payeeName.equals(userName)) { + continue; + } + + List moneyList = balanceList.get(payeeName); + addMoney(moneyList, payeeMoney); + } + } else { + for(Pair payee : payees) { + String payeeName = payee.getKey(); + Money payeeMoney = payee.getValue(); + + if (!payeeName.equals(userName)) { + continue; + } + + List moneyList = balanceList.get(payerName); + subtractMoney(moneyList, payeeMoney); + } + } + } + + /** + * Adds a Money object to a List of Money. + * If money shares a currency with an entry in moneyList, perform addition with the existing entry. + * Else, append Money as a new entry to the List. + * + * @param moneyList The current List of Money. + * @param money The Money object to be added. + */ + public void addMoney(List moneyList, Money money){ + for(int i = 0; i < moneyList.size(); i++){ + CurrencyConversions currency = moneyList.get(i).getCurrency(); + boolean isSameCurrency = currency.equals(money.getCurrency()); + + if(!isSameCurrency){ + continue; + } + + Money oldItem = moneyList.get(i); + Money newItem= oldItem.addition(money, currency); + moneyList.set(i, newItem); + return; + } + + // no matching currency in moneyList + moneyList.add(money); + } + + /** + * Subtracts a Money object from a List of Money. + * If money shares a currency with an entry in moneyList, perform subtraction with the existing entry. + * Else, append -Money as a new entry to the List. + * + * @param moneyList The current List of Money. + * @param money The Money object to be subtracted. + */ + public void subtractMoney(List moneyList, Money money){ + for(int i = 0; i < moneyList.size(); i++){ + CurrencyConversions currency = moneyList.get(i).getCurrency(); + boolean isSameCurrency = currency.equals(money.getCurrency()); + + if(!isSameCurrency){ + continue; + } + + Money oldItem = moneyList.get(i); + Money newItem= oldItem.subtraction(money, currency); + moneyList.set(i, newItem); + return; + } + + // no matching currency in moneyList + moneyList.add(money.multiplication(-1f, money.getCurrency())); + } + + public void printBalance() { + StringBuilder printOutput = new StringBuilder(); + printOutput.append(String.format("User %s's Balance List:", userName)); + + for (Map.Entry> entry : balanceList.entrySet()) { + String user = entry.getKey(); + for(Money money : entry.getValue()){ + // if entry is less than $0.001 don't print it + if(money.getAmount() <= 0.001f && money.getAmount() >= -0.001f){ + continue; + } + printOutput.append(String.format("\n %s : %s", user, money)); + } + } + + printOutput.append("\nEnd of Balance List"); + UserInterface.printMessage(printOutput.toString(), MessageType.SUCCESS); + } +} diff --git a/src/main/java/seedu/duke/CurrencyConversions.java b/src/main/java/seedu/duke/CurrencyConversions.java new file mode 100644 index 0000000000..bc4a144951 --- /dev/null +++ b/src/main/java/seedu/duke/CurrencyConversions.java @@ -0,0 +1,39 @@ +package seedu.duke; +//@@author MonkeScripts +//all rates are relative to 1 SGD +public enum CurrencyConversions { + USD("USD", 0.74F), + RMB("RMB", 5.35F), + EUR("EUR", 0.687F), + JPY("JPY", 112.12F), + AUD("AUD", 1.12F), + MYR("MYR", 3.50F), + SGD("SGD", 1.00F), + + ; + + private final String name; + private final float rate; + + CurrencyConversions(String name, float rate) { + this.name = name; + this.rate = rate; + } + + String getName() { + return this.name; + } + + float getRate() { + return this.rate; + } + + float getInverseRate() { + return (1.00f / this.rate); + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 5c74e68d59..9cf1621280 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,5 +1,8 @@ package seedu.duke; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.exceptions.LuckException; + import java.util.Scanner; public class Duke { @@ -7,15 +10,33 @@ 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"; + String logo = + ".------..------..------..------..------..------..------..------..------..------.\n" + + "|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. |\n" + + "| :/\\: || :/\\: || :/\\: || (\\/) || :/\\: || :/\\: || (\\/) || (\\/) || :(): || :/\\: |\n" + + "| :\\/: || (__) || (__) || :\\/: || (__) || (__) || :\\/: || :\\/: || ()() || :\\/: |\n" + + "| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G|\n" + + "`------'`------'`------'`------'`------'`------'`------'`------'`------'`------'\n"; + System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); + System.out.println("Start splitting your expenses now!"); Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); + + Help.printHelp(); + + while(in.hasNextLine()) { + String userInput = in.nextLine(); + Parser parser = new Parser(userInput); + + try { + parser.handleUserInput(); + } catch (Parser.EndProgramException e) { + break; + } catch (ExpensesException | LuckException e) { + UserInterface.printMessage(e.getMessage(), MessageType.ERROR); + } + } + System.out.println("Goodbye!"); } } diff --git a/src/main/java/seedu/duke/Expense.java b/src/main/java/seedu/duke/Expense.java new file mode 100644 index 0000000000..7ce9d19d00 --- /dev/null +++ b/src/main/java/seedu/duke/Expense.java @@ -0,0 +1,116 @@ +//@@author mukund1403 +package seedu.duke; +import seedu.duke.storage.GroupStorage; +import java.util.ArrayList; + + +/** + * A class to add a new expense + */ +public class Expense { + private final String payerName; + private Money totalAmount; + private ArrayList> payees = new ArrayList<>(); + + + private final String description; + + /** + * Constructor to create new Equal Expense + * @param payerName : The name of the user who paid for the Expense + * @param description : Description of the expense + * @param totalAmount : The total amount before being divided + * @param payees : ArrayList of pairs containing names of people who are involved in the transaction and + * the amount they owe (Index 0 is the payer and will also be added to the payees but as last index) + */ + public Expense(String payerName, String description, Money totalAmount, ArrayList> payees){ + this.payees = payees; + this.payerName = payerName; + this.totalAmount = totalAmount; + this.description = description; + } + + //@@author Cohii2 + public String getPayerName() { + return payerName; + } + + /** + * @return : float showing the total amount before division + */ + public float getTotalAmount() { + return totalAmount.getAmount(); + } + + public ArrayList> getPayees() { + return payees; + } + + //@@author mukund1403 + public String getDescription() { + return description; + } + + public CurrencyConversions getCurrency() { + return totalAmount.getCurrency(); + } + + public void clearPayeeValue(String payeeName) { + // replace the value of the payee with 0 + for (int i = 0; i < payees.size(); i++) { + if (payees.get(i).getKey().equals(payeeName)) { + Money amount = new Money(0, payees.get(i).getValue().getCurrency()); + payees.set(i, new Pair<>(payeeName, amount)); + } + } + } + + /** + * Converts the Expense to string form containing all the expense details + * @return : A string containing expense details in a tabular format + */ + @Override + public String toString() { + StringBuilder expensesDetails = new StringBuilder(); + expensesDetails.append("description: ").append(description).append("\n\tamount: ") + .append(totalAmount.getCurrency()) + .append(String.format(" %.2f", totalAmount.getAmount())).append("\n\tpaid by: ") + .append(payerName).append("\n\tThe split is:\n"); + for (Pair payee : payees) { + expensesDetails.append("\t\t").append(payee.getKey()).append(" : ") + .append(payee.getValue().getCurrency()) + .append(String.format(" %.2f", payee.getValue().getAmount())).append("\n"); + } + return expensesDetails.toString(); + } + + public String successMessageString() { + StringBuilder successMessage = new StringBuilder(); + if (!(GroupStorage.isLoading) && !(this instanceof Settle)) { + successMessage.append("Added new expense with description ") + .append(description).append(" and amount ") + .append(String.format(totalAmount.getCurrency() + " %.2f", totalAmount.getAmount())) + .append(" paid by ").append(payerName).append(". The split is:\n"); + + for (Pair payee : payees) { + successMessage.append(payee.getKey()).append(" : ") + .append(String.format(payee.getValue().getCurrency() + + " %.2f", payee.getValue().getAmount())).append("\n"); + } + } + return String.valueOf(successMessage); + } + + public void clear() { + float amount = totalAmount.getAmount(); + CurrencyConversions currency = totalAmount.getCurrency(); + totalAmount = new Money(amount, currency); + payees = new ArrayList<>(); + } + + public String getPayer() { + return payerName; + } +} + + diff --git a/src/main/java/seedu/duke/Group.java b/src/main/java/seedu/duke/Group.java new file mode 100644 index 0000000000..90217ec8b0 --- /dev/null +++ b/src/main/java/seedu/duke/Group.java @@ -0,0 +1,399 @@ +//@@author avrilgk + +package seedu.duke; + +import seedu.duke.exceptions.GroupLoadException; +import seedu.duke.exceptions.GroupSaveException; +import seedu.duke.storage.GroupNameChecker; +import seedu.duke.storage.GroupStorage; +import seedu.duke.storage.FileIOImpl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class Group { + static Map groups = new HashMap<>(); + static Optional currentGroupName = Optional.empty(); + private static final GroupStorage groupStorage = new GroupStorage(new FileIOImpl()); + + private static List members = null; + + private static GroupNameChecker groupNameChecker = new GroupNameChecker(); + + private final String groupName; + + private final List expenseList; + + public Group(String groupName) { + this.groupName = groupName; + this.members = new ArrayList<>(); + this.expenseList = new ArrayList<>(); + } + + /** + * Retrieves an existing group by its name or creates a new one if it does not exist. + * It ensures that a user cannot create or join a new group without exiting their current group. + * + * @param groupName The name of the group to get or create. + * @return The existing or newly created group. + * @throws IllegalStateException If trying to create or join a new group while already in another group. + */ + + //@@ author avrilgk + public static Optional getOrCreateGroup(String groupName) { + + // Check if group name is empty + if (groupName == null || groupName.trim().isEmpty()) { + System.out.println("Group name cannot be empty. Please try again."); + return Optional.empty(); + } + + if (currentGroupName.isPresent()) { + System.out.println("You are currently in " + currentGroupName.get() + + ". Exit current group before creating another one."); + return Optional.empty(); + } + + Optional group = Optional.ofNullable(groups.get(groupName)); + + // Check if user is accessing a group they are already in + if (group.isPresent() && getCurrentGroup().isPresent() && getCurrentGroup().get().equals(group.get())) { + System.out.println("You are already in " + groupName); + return Optional.empty(); + } + + // Create a new group if it doesn't exist + if (group.isEmpty() && !groupNameChecker.doesGroupNameExist(groupName)) { + Group newGroup = new Group(groupName); + groups.put(groupName, newGroup); + System.out.println(groupName + " created."); + currentGroupName = Optional.of(groupName); + group = Optional.of(newGroup); + System.out.println("You are now in " + groupName); + } else if (groupNameChecker.doesGroupNameExist(groupName)) { + System.out.println("Group already exists. Use 'enter " + groupName + "' to enter the group."); + return Optional.empty(); + } + + assert group.isPresent() : "Group should be created and present"; + assert currentGroupName.isPresent() : "Current group name should be set"; + assert currentGroupName.get().equals(groupName) : "Current group name should match " + + "the created or retrieved group"; + assert groups.containsKey(groupName) : "Groups map should contain the new or retrieved group"; + + return group; + } + + /** + * Enter existing group. + * + * @param groupName The name of the group to enter. + * @return The existing group. + */ + public static Optional enterGroup(String groupName) { + + if (groupName == null || groupName.trim().isEmpty()) { + System.out.println("Group name cannot be empty. Please try again."); + return Optional.empty(); + } + + if (currentGroupName.isPresent()) { + System.out.println("You are currently in " + currentGroupName.get() + + ". Exit current group before entering another one."); + return Optional.empty(); + } + + Optional group = Optional.ofNullable(groups.get(groupName)); + if (group.isEmpty()) { + //@@author hafizuddin-a + GroupNameChecker groupNameChecker = new GroupNameChecker(); + if (!groupNameChecker.doesGroupNameExist(groupName)) { + System.out.println("Group does not exist."); + return Optional.empty(); + } + + try { + // If the group doesn't exist in memory, try loading it from file + Optional loadedGroup = Optional.ofNullable(groupStorage.loadGroupFromFile(groupName)); + if (loadedGroup.isPresent()) { + groups.put(groupName, loadedGroup.get()); + group = loadedGroup; + } else { + //@@ author avrilgk + System.out.println("Unable to load group from file."); + return Optional.empty(); + } + // @@author hafizuddin-a + } catch (GroupLoadException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "Failed to load group from file."; + } + System.out.println(errorMessage); + return Optional.empty(); + } + } + //@@author avrilgk + currentGroupName = Optional.of(groupName); + System.out.println("You are now in " + groupName); // don't change this to cat + return group; + } + + /** + * Exits the current group. + * If the user is not in any group, it displays a message asking the user to try again. + */ + public static void exitGroup(String groupName) { + + if (currentGroupName.isPresent()) { + if (!currentGroupName.get().equals(groupName)) { + System.out.println("Invalid group name. Please enter the correct group name."); + return; + } + //@@author hafizuddin-a + try { + groupStorage.saveGroupToFile(groups.get(currentGroupName.get())); + System.out.println("Group data saved successfully."); + } catch (GroupSaveException e) { + System.out.println("Error saving group: " + e.getMessage()); + } + //@@author avrilgk + System.out.println("You have exited " + currentGroupName.get() + "."); + currentGroupName = Optional.empty(); + } else { + System.out.println("You are not currently in a group."); + } + } + + /** + * Retrieves the current group. + * + * @return The current group, or null if the user is not in any group. + */ + public static Optional getCurrentGroup() { + return currentGroupName.map(groups::get); + } + + /** + * Checks if the user is currently in a group. + * + * @return true if the user is in a group, false otherwise. + */ + public static boolean isInGroup() { + return currentGroupName.isPresent(); + } + + /** + * Checks if a user with the given name is a member of the group. + * + * @param memberName The name of the member to check. + * @return true if the user is a member of the group, false otherwise. + */ + public static boolean isMember(String memberName) { + for (User member : members) { + if (member.getName().equalsIgnoreCase(memberName)) { + return true; + } + } + return false; + } + + /** + * Adds a new member to the group. + * + * @param memberName The name of the member to add. + * @return The newly added user, or null if the user is already a member of the group. + */ + public User addMember(String memberName) { + if (memberName == null || memberName.isEmpty()) { + UserInterface.printMessage("Please provide a valid member name.", MessageType.ERROR); + return null; + } + + if (isMember(memberName)) { + UserInterface.printMessage(memberName + " is already a member of " + groupName + ".", MessageType.ERROR); + return null; + } + + User newMember = new User(memberName); + members.add(newMember); + if (!GroupStorage.isLoading) { + UserInterface.printMessage(memberName + " has been added to " + groupName + ".", MessageType.SUCCESS); + } + return newMember; + } + + /** + * Adds a new expense to the group. + * + * @param expense The Expense object to add. + */ + public void addExpense(Expense expense) { + expenseList.add(expense); + } + + public void deleteExpense(int expenseIndex) { + expenseList.remove(expenseIndex); + } + + /** + * Retrieves the name of the group. + * + * @return The name of the group. + */ + public String getGroupName() { + return groupName; + } + + /** + * Retrieves the list of members in the group. + * + * @return The list of members in the group. + */ + public List getMembers() { + return new ArrayList<>(members); + } + + /** + * Retrieves the list of expenses in the group. + * + * @return The list of expenses in the group. + */ + public List getExpenseList() { + return new ArrayList<>(expenseList); + } + + /** + * Settles the outstanding amount between two users. + * + * @param payerName The name of the user who will pay the outstanding amount. + * @param payeeName The name of the user who will receive the outstanding amount. + */ + //@@author avrilgk + public void settle(String payerName, String payeeName) { + User payer = findUser(payerName); + User payee = findUser(payeeName); + + if (payer == null || payee == null) { + UserInterface.printMessage("User not found.", MessageType.ERROR); + return; + } + + Balance balance = new Balance(payee.getName(), expenseList, members); + List moneyList = balance.getBalanceList().get(payer.getName()); + + Money total = new Money(0f, CurrencyConversions.SGD); + for (Money money : moneyList) { + total = total.addition(money, CurrencyConversions.SGD); + } + + if (total.getAmount() <= 0f) { + UserInterface.printMessage( + payerName + " has no outstanding balance with " + payeeName + ); + return; + } + + if (total.getAmount() > 0f) { + UserInterface.printMessage( + payerName + " should pay " + payeeName + " " + String.format("SGD %.2f", total.getAmount()) + ); + } + + ArrayList> repaymentList = new ArrayList<>(); + for (Money money : moneyList) { + repaymentList.add(new Pair<>(payee.getName(), money)); + } + + Expense repaymentExpense = new Expense(payer.getName(), "Settlement", total, repaymentList); + expenseList.add(repaymentExpense); + System.out.println(payerName + " has settled the full amount with " + payeeName); + } + + public void settleAll(String payerName) { + User payer = findUser(payerName); + if (payer == null) { + System.out.println("User not found."); + return; + } + for (User member : members) { + if (member.getName().equals(payerName)) { + continue; + } + settle(payerName, member.getName()); + } + } + + /** + * Calculates the balance between two users. + * + * @param payerName The name of the user who will pay the outstanding amount. + * @param payeeName The name of the user who will receive the outstanding amount. + * @return The balance between the two users. + */ + private float calculateBalance(String payerName, String payeeName) { + ArrayList payeeExpense = getPayerExpense(payeeName); + ArrayList payerExpense = getPayerExpense(payerName); + + float balance = 0; + + for (Expense expense : payeeExpense) { + for (Pair payee : expense.getPayees()) { + if (payee.getKey().equals(payerName)) { + balance += payee.getValue().getAmount(); + } + } + } + + for (Expense expense : payerExpense) { + for (Pair payee : expense.getPayees()) { + if (payee.getKey().equals(payeeName)) { + balance -= payee.getValue().getAmount(); + } + } + } + + return balance; + } + + /** + * Retrieves the expenses paid by a user. + * + * @param payerName The name of the user to retrieve expenses for. + * @return The list of expenses paid by the user. + */ + public ArrayList getPayerExpense(String payerName) { + ArrayList payerExpenses = new ArrayList<>(); + + for (Expense expense : expenseList) { + if (expense.getPayerName().equals(payerName)) { + payerExpenses.add(expense); + } + } + + return payerExpenses; + } + + /** + * Finds a user by their name. + * + * @param userName The name of the user to find. + * @return The user with the given name, or null if the user is not found. + */ + + private User findUser(String userName) { + for (User user : members) { + if (user.getName().equals(userName)) { + return user; + } + } + return null; + } + + public static void removeGroup(String groupName) { + groups.remove(groupName); + } +} diff --git a/src/main/java/seedu/duke/Help.java b/src/main/java/seedu/duke/Help.java new file mode 100644 index 0000000000..1207ad4f37 --- /dev/null +++ b/src/main/java/seedu/duke/Help.java @@ -0,0 +1,27 @@ +package seedu.duke; + +public class Help { + private static final String prompt = + "Welcome, here is a list of commands:\n" + + "help: Access help menu.\n" + + "create : Create a group.\n" + + "exit : Exit current group.\n" + + "member : Add a member to the group.\n" + + "expense /amount " + + "/currency /paid " + + "/user /user ...: " + + "Add an expense SPLIT EQUALLY.\n" + + "expense /unequal /amount " + + "/currency /paid " + + "/user " + + "/user ...: " + + "Add an expense SPLIT UNEQUALLY.\n" + + "list: List all expenses in the group.\n" + + "balance : Show user's balance.\n" + + "settle /user : Settle the amount between two users.\n" + + "luck : luck is in the air tonight"; + + static void printHelp() { + System.out.println(prompt); + } +} diff --git a/src/main/java/seedu/duke/Luck.java b/src/main/java/seedu/duke/Luck.java new file mode 100644 index 0000000000..3b4c4cb4c0 --- /dev/null +++ b/src/main/java/seedu/duke/Luck.java @@ -0,0 +1,127 @@ +package seedu.duke; + +import seedu.duke.exceptions.ExpensesException; + +import java.util.ArrayList; +import java.util.Random; +import java.util.Scanner; + + +public class Luck { + private static final String icon = + " .=*+::. \n" + + " =*=-:::. \n" + + " .:=+**#***####*+=-:. \n" + + " :+#%%%%###%########%%%%*=: \n" + + " .=#%%%####################%%%#=. \n" + + " +#%%%####%%%###%%%###%%%%#*###%%#= \n" + + " :##%%########%%###%%###%%#########%%#: \n" + + " .##%#*#%%%%#######*##*#######%%%##*#%%#: \n" + + " *%%%##%%###%%%############%%%%%%%%##%%%#. \n" + + " .%#*****+*****++*++*****+**++***********%= \n" + + " :%*-::.-=-:::-==:==-:::-=:-===---==-.::+%+ \n" + + " :%*=:.+@@@%%@@@*=@@@#%%@@%-@@@@%@@@@-.-+#+ .=-.. \n" + + " :%*=.=%+*=+***@-%%+*=+##*@=*%++==**+%::+#=.##--- \n" + + " :%*=.#%-++=--*%-@#-++=--*@+=%-+*=--*@=:+#+ -::-: \n" + + " :%*=.#@%@%=-*@%-@@%@%=-#@@+=@%@%=-#@@=:+#+ -+: \n" + + " -%*=:=@@%---%@@:%@@%---%@@=*@@%---%@@::+#+ =*: \n" + + " -%*=:.#@@###@@@*+@@@###@@@-@@@@###@@=.-+#+ -+: \n" + + " -%*=:.:*##**+***:********+:***=+=**-..:+%*--+*: \n" + + " :%*+=====-==========-=-===============+*%*---- \n" + + " +##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##*. \n" + + " =******************************************* \n" + + " =++=++++++++++++++++++++++++++++++++++++++++ \n" + + " =*++++++++++++++++++++++++++++++++++++++++*+ \n" + + " :----------------------------------------::. "; + private static final String welcome = + "Gamble Gamble Gamble! Crazy Slots!!!\n" + + "10 USD Per round, given to random user in your group!!!\n" + + "Win if all 3 MIDDLE slots are " + + "the same and clear your debts!!!"; + + private Group currentGroup; + private String username; + + /** + * Slot Machine interface + * + * @param group The Group the selected User is in + * @param username The username of the user + */ + public Luck(Group group, String username) { + this.currentGroup = group; + this.username = username; + } + + /** + * Prints welcome message + * + */ + public void printWelcome() { + System.out.println(icon); + System.out.println(welcome); + } + + + public void startGambling() throws ExpensesException { + SlotMachine slotMachine = new SlotMachine(9, 9); + System.out.println(slotMachine); + Scanner in = new Scanner(System.in); + System.out.println("/exit to leave. /reroll to roll again"); + while (in.hasNextLine()) { + String userInput = in.nextLine(); + switch (userInput.trim()) { + case "/exit": + System.out.println("leaving the gambling den"); + Help.printHelp(); + return; + case "/reroll": + System.out.println("/exit to leave. /reroll to roll again"); + slotMachine.reroll(); + System.out.println(slotMachine); + if (slotMachine.isWin()) { + System.out.println("All debts clear!!"); + currentGroup.settleAll(this.username); + break; + } + System.out.println("Poor, reroll again to win. " + + "Win if all 3 MIDDLE slots are the same"); + //add expenses to random user + Expense buyInDebt = calculateDebt(); + System.out.println(buyInDebt.successMessageString()); + this.currentGroup.addExpense(buyInDebt); + break; + default: + System.out.println("/exit to leave. /reroll to roll again"); + break; + } + } + } + + /** + * Creates an expense from each reroll + * @return of $10USD expense paid by user to a random person in the group + */ + private Expense calculateDebt() throws ExpensesException { + Random rand = new Random(); + int randomUserIndex = rand.nextInt( + this.currentGroup.getMembers().size()); + while(this.currentGroup.getMembers() + .get(randomUserIndex).getName().equals(username)) { + randomUserIndex = rand.nextInt(this.currentGroup. + getMembers().size()); + } + User randomLuckyUser = this.currentGroup.getMembers(). + get(randomUserIndex); + String buyInDescription = "unlucky moments"; + Money buyIn = new Money(10.0f, CurrencyConversions.USD); + Pair paymentDetails = + new Pair(this.username, buyIn); + ArrayList> payee = + new ArrayList>(); + payee.add(paymentDetails); + return new Expense(randomLuckyUser.getName(), + buyInDescription, buyIn, payee); + } + +} diff --git a/src/main/java/seedu/duke/MessageType.java b/src/main/java/seedu/duke/MessageType.java new file mode 100644 index 0000000000..02adedde71 --- /dev/null +++ b/src/main/java/seedu/duke/MessageType.java @@ -0,0 +1,6 @@ +package seedu.duke; + +public enum MessageType { + SUCCESS, + ERROR; +} diff --git a/src/main/java/seedu/duke/Money.java b/src/main/java/seedu/duke/Money.java new file mode 100644 index 0000000000..68a6c9c1c1 --- /dev/null +++ b/src/main/java/seedu/duke/Money.java @@ -0,0 +1,63 @@ +package seedu.duke; +//@@author MonkeScripts +public class Money { + private final float amount; + private final CurrencyConversions currency; + + public Money(float amount, CurrencyConversions currency) { + this.amount = amount; + this.currency = currency; + } + + Money convertToSGD() { + float amountInSGD = this.amount * this.currency.getInverseRate(); + return new Money(amountInSGD, CurrencyConversions.SGD); + } + + Money convertToOther(CurrencyConversions resultCurrency) { + float amountInSGD = this.amount * this.currency.getInverseRate(); + float foreignAmount = amountInSGD * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money addition(Money other, CurrencyConversions resultCurrency) { + float amountInSGD = this.convertToSGD().getAmount(); + float otherAmountInSGD = other.convertToSGD().getAmount(); + float foreignAmount = (amountInSGD + otherAmountInSGD) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money subtraction(Money other, CurrencyConversions resultCurrency) { + float amountInSGD = this.convertToSGD().getAmount(); + float otherAmountInSGD = other.convertToSGD().getAmount(); + float foreignAmount = (amountInSGD - otherAmountInSGD) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money multiplication(float constant, CurrencyConversions resultCurrency) { + float amountInSGD = this.convertToSGD().getAmount(); + float foreignAmount = (amountInSGD * constant) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + Money division(float constant, CurrencyConversions resultCurrency) { + float amountInSGD = this.convertToSGD().getAmount(); + float foreignAmount = (amountInSGD / constant) + * resultCurrency.getRate(); + return new Money(foreignAmount, resultCurrency); + } + + public float getAmount() { + return this.amount; + } + public CurrencyConversions getCurrency() { + return this.currency; + } + @Override + public String toString() { + return String.format("%.2f%s", this.amount, this.currency); + } +} diff --git a/src/main/java/seedu/duke/Pair.java b/src/main/java/seedu/duke/Pair.java new file mode 100644 index 0000000000..4ce900436e --- /dev/null +++ b/src/main/java/seedu/duke/Pair.java @@ -0,0 +1,27 @@ +package seedu.duke; + +//@@author cs2030-reused +//Utility class for Java Pair given in cs2030 module +public class Pair { + private final T t; + private final U u; + + public Pair(T t, U u) { + this.t = t; + this.u = u; + } + + public T getKey() { + return this.t; + } + + public U getValue() { + return this.u; + } + + @Override + public String toString() { + return "(" + this.t + ", " + this.u + ")"; + } +} +//@@author diff --git a/src/main/java/seedu/duke/Parser.java b/src/main/java/seedu/duke/Parser.java new file mode 100644 index 0000000000..cdd85a43f5 --- /dev/null +++ b/src/main/java/seedu/duke/Parser.java @@ -0,0 +1,227 @@ +package seedu.duke; + +import seedu.duke.commands.BalanceCommand; +import seedu.duke.commands.ExpenseCommand; +import seedu.duke.commands.GroupCommand; +import seedu.duke.commands.ListCommand; +import seedu.duke.commands.LuckCommand; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.exceptions.LuckException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public class Parser { + /** + * List of parameters to extract from user input. + * For example, "/amount (amount)". + * Add new Keys to extract additional user parameters for future functionality. + */ + private static final String[] paramKeys = {"amount", "paid", "user", "currency"}; + + private final String userInput; + + /** + * First word of user input. + */ + private String command = null; + + /** + * Input between first word and '/' character. + * For example, "(command) (argument) /(parameter) (parameter input)...". + */ + private String argument = null; + + /** + * Additional parameters provided by user. + */ + private HashMap> params = createParams(); + + public static class EndProgramException extends Exception { + + } + + /** + * Constructor for Test purposes. + */ + public Parser(String userInput, String command, String argument, + String[] amount, String[] paid, String[] user, String[] currency) { + this.userInput = userInput; + this.command = command; + this.argument = argument; + this.params.put("amount", new ArrayList<>(List.of(amount))); + this.params.put("paid", new ArrayList<>(List.of(paid))); + this.params.put("user", new ArrayList<>(List.of(user))); + this.params.put("currency", new ArrayList<>(List.of(currency))); + } + + public Parser(String userInput) { + this.userInput = userInput; + this.parseUserInput(); + } + + /** + * Creates a new HashMap with Keys equal to additional parameters users might input. + * Values are arrays that store user input. + * + * @return HashMap with Keys in 'additionalFields' and empty array Values. + */ + private HashMap> createParams() { + HashMap> additionalInfo = new HashMap<>(); + + for (String paramKey : paramKeys) { + additionalInfo.put(paramKey, new ArrayList<>()); + } + + return additionalInfo; + } + + /** + * Process the String userInput and populates corresponding fields of Parser object. + */ + public void parseUserInput() { + String[] tokens = userInput.split(" ", 2); + this.command = tokens[0].toLowerCase().trim(); + + if (tokens.length == 1) { + return; + } + + String[] arguments = tokens[1].split("/"); + if(arguments.length < 1) { + return; + } + + this.argument = arguments[0].trim(); + + for (int i = 1; i < arguments.length; i++) { + String[] subTokens = arguments[i].split(" ", 2); + if (subTokens.length == 1) { + continue; + } + + String subCommand = subTokens[0].toLowerCase().trim(); + String subArgument = subTokens[1].trim(); + if (!subArgument.isEmpty() && params.containsKey(subCommand)) { + params.get(subCommand).add(subArgument); + } + } + } + + /** + * Returns String summarising contents of Parser object. + * For easier debug printing. + * + * @return Contents of Parser object. + */ + @Override + public String toString() { + StringBuilder parser = new StringBuilder(); + + parser.append("command: ").append(command).append("\n"); + + parser.append("argument: ").append(argument).append("\n"); + + for (String paramKey : paramKeys) { + parser.append(paramKey).append(": "); + for (String item : params.get(paramKey)) { + parser.append(item).append(" "); + } + parser.append("\n"); + } + + return parser.toString(); + } + + public void handleUserInput() throws EndProgramException, ExpensesException, LuckException { + switch (command) { + case "bye": + if (Group.isInGroup()) { + argument = Group.getCurrentGroup().get().getGroupName(); + GroupCommand.exitGroup(argument); + } + throw new EndProgramException(); + case "help": + // Help code here + Help.printHelp(); + assert (true); + break; + case "create": + GroupCommand.createGroup(argument); + break; + case "delete": + if (argument == null || argument.isEmpty()) { + throw new ExpensesException("Mention if you want to delete an expense or group."); + } + + String[] deleteTypeAndValue = argument.split(" "); + String deleteType = deleteTypeAndValue[0]; + String deleteValue; + try{ + deleteValue = deleteTypeAndValue[1]; + } catch (ArrayIndexOutOfBoundsException e){ + throw new ExpensesException("Mention if you want to delete an expense or group" + + " and which group or expense you want to delete."); + } + + if(deleteType.equals("group")){ + GroupCommand.deleteGroup(deleteValue); + } else if(deleteType.equals("expense")){ + ExpenseCommand.deleteExpense(deleteValue); + } else { + throw new ExpensesException("Mention if you want to delete an expense or group."); + } + + break; + case "member": + GroupCommand.addMember(argument); + break; + case "enter": + GroupCommand.enterGroup(argument); + break; + case "exit": + GroupCommand.exitGroup(argument); + break; + case "expense": + ExpenseCommand.addExpense(params, argument, userInput); + break; + case "settle": + //@@author avrilgk + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + + if(params.get("user").isEmpty() || params.get("user").size() > 1){ + System.out.println("Invalid command. Syntax: settle payerName /user payeeName"); + return; + } + + String payer = argument; + String payee = params.get("user").get(0); + + Group.getCurrentGroup().ifPresent(group -> group.settle(payer, payee)); + + break; + //@@author + case "luck": + LuckCommand.handleLuck(argument); + break; + case "list": + ListCommand.printList(); + break; + case "balance": + BalanceCommand.handleBalance(argument); + break; + default: + System.out.println("That is not a command. " + + "Please use one of the commands given here"); + Help.printHelp(); + break; + } + } + +} diff --git a/src/main/java/seedu/duke/Settle.java b/src/main/java/seedu/duke/Settle.java new file mode 100644 index 0000000000..b46d44289e --- /dev/null +++ b/src/main/java/seedu/duke/Settle.java @@ -0,0 +1,43 @@ +//@@author avrilgk +package seedu.duke; + + +import java.util.ArrayList; + +/** + * The Settle class represents a transaction between two users. + * It extends the Expense class and has a payer, payee and amount. + *

+ * Each Settle object represents a single transaction where one user (the payer) + * pays another user (the payee) a certain amount. + */ + +public class Settle extends Expense { + private final User payer; + private final User payee; + private final double amount; + + /** + * Constructs a new Settle object. + * + * @param payer The user who is making the payment + * @param payee The user who is receiving the payment + * @param amount The amount of the payment + */ + public Settle(User payer, User payee, float amount) { + super(payer.getName(), "", new Money(amount , CurrencyConversions.SGD), new ArrayList<>()); + this.payer = payer; + this.payee = payee; + this.amount = amount; + } + + @Override + public String getPayer() { + return payer.getName(); + } + + @Override + public String toString() { + return payer.getName() + " paid " + payee.getName() + " " + amount; + } +} diff --git a/src/main/java/seedu/duke/SlotMachine.java b/src/main/java/seedu/duke/SlotMachine.java new file mode 100644 index 0000000000..3b912d6a9c --- /dev/null +++ b/src/main/java/seedu/duke/SlotMachine.java @@ -0,0 +1,103 @@ +package seedu.duke; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +public class SlotMachine { + private static final String slotOutputs = "%^#@$*!~"; + private List> slotMachine; + + /** + * Generates a 3x3 slot machine matrix + * Each slot takes up 3 rows and 3 columns + * + * @param numRows total number of rows + * @param numCols total number of columns + */ + SlotMachine(int numRows, int numCols) { + // create slot machine matrix + slotMachine = new ArrayList<>(numRows); + for (int row = 0; row < numRows; row++) { + List currentRow = new ArrayList<>(numCols); + for (int j = 0; j < numCols; j++) { + // Initialize each cell with empty char + currentRow.add(' '); + } + slotMachine.add(currentRow); + } + // randomise each slot + fillSlots(); + } + + // fill each slot with random characters + private void fillSlots() { + Random rand = ThreadLocalRandom.current(); + for (int row = 0; row < slotMachine.size(); row += 3) { + for (int col = 0; col < slotMachine.get(0).size(); col += 3) { + char randomChar = slotOutputs.charAt(rand.nextInt(slotOutputs.length())); + //one char in each slot + fillSlot(row, col, randomChar); + } + } + } + + // fill one slot with the same random character + private void fillSlot(int startRow, int startCol, char character) { + for (int i = startRow; i < startRow + 3; i++) { + for (int j = startCol; j < startCol + 3; j++) { + slotMachine.get(i).set(j, character); + } + } + } + + // randomise characters + void reroll() { + fillSlots(); + } + + // override toString method to print the slot machine + @Override + public String toString() { + StringBuilder matrix = new StringBuilder(); + + // Draw the slot machine + for (int i = 0; i < slotMachine.size(); i += 3) { + // Draw the top of the boxes + for (int j = 0; j < slotMachine.get(0).size(); j += 3) { + matrix.append("_____"); + } + matrix.append("\n"); + // Draw the content of each slot + for (int row = i; row < i + 3; row++) { + for (int j = 0; j < slotMachine.get(0).size(); j += 3) { + matrix.append("|"); + for (int col = j; col < j + 3; col++) { + matrix.append(slotMachine.get(row).get(col)); + } + matrix.append("|"); + } + matrix.append("\n"); + } + // Draw the bottom of the boxes + for (int j = 0; j < slotMachine.get(0).size(); j += 3) { + matrix.append("_____"); + } + matrix.append("\n"); + } + return matrix.toString(); + } + // check if all characters in the middle rows are the same + boolean isWin() { + for (int i = slotMachine.size() / 2 - 1; i <= slotMachine.size() / 2 + 1; i++) { + char firstChar = slotMachine.get(i).get(0); + for (int j = 1; j < slotMachine.get(i).size(); j++) { + if (slotMachine.get(i).get(j) != firstChar) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/duke/User.java b/src/main/java/seedu/duke/User.java new file mode 100644 index 0000000000..01b42dc971 --- /dev/null +++ b/src/main/java/seedu/duke/User.java @@ -0,0 +1,13 @@ +package seedu.duke; + +public class User { + private String userName; + + public User(String userName) { + this.userName = userName; + } + + public String getName() { + return userName; + } +} diff --git a/src/main/java/seedu/duke/UserInterface.java b/src/main/java/seedu/duke/UserInterface.java new file mode 100644 index 0000000000..eece804de7 --- /dev/null +++ b/src/main/java/seedu/duke/UserInterface.java @@ -0,0 +1,47 @@ +package seedu.duke; + + +public class UserInterface { + + private static final String SUCCESS_BORDER = "<----------SUCCESS----------->"; + private static final String ERROR_BORDER = "<-----------ERROR------------>"; + private static final String DEFAULT_BORDER = "<---------------------------->"; + private static final String HAPPY_CAT = + " /\\_/\\\n" + + " ( ^.^ )\n" + + " > ^ <"; + private static final String GRUMPY_CAT = + " /\\_/\\\n" + + " ( >_< )\n" + + " > ^ <"; + + private static final String SAD_CAT = + " /\\_/\\\n" + + " ( ._. )\n" + + " > ^ <"; + + public static void printMessage(String message, MessageType type) { + switch (type) { + case SUCCESS: + System.out.println(HAPPY_CAT); + System.out.println(SUCCESS_BORDER); + break; + case ERROR: + System.out.println(GRUMPY_CAT); + System.out.println(ERROR_BORDER); + break; + default: + break; + } + + System.out.println(message); + System.out.println(DEFAULT_BORDER); + } + + public static void printMessage(String message) { + System.out.println(SAD_CAT); + System.out.println(DEFAULT_BORDER); + System.out.println(message); + System.out.println(DEFAULT_BORDER); + } +} diff --git a/src/main/java/seedu/duke/commands/BalanceCommand.java b/src/main/java/seedu/duke/commands/BalanceCommand.java new file mode 100644 index 0000000000..8b6dc20f78 --- /dev/null +++ b/src/main/java/seedu/duke/commands/BalanceCommand.java @@ -0,0 +1,36 @@ +package seedu.duke.commands; + +import seedu.duke.Balance; +import seedu.duke.Group; +import seedu.duke.exceptions.ExpensesException; + +import java.util.Optional; + +public class BalanceCommand { + /** + * Checks if user is currently in a Group, and if User specified is in said Group. + * Creates a Balance object and prints User's balance if so. + * + * @param argument The name of User to print the balance of. + * @throws ExpensesException If user is not in a Group, or specified User is not in said Group. + */ + public static void handleBalance(String argument) throws ExpensesException{ + // Checks if user is currently in a Group + // named 'currentGroup1' to prevent conflict with previous declaration + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + assert currentGroup.isPresent() : "Group should be created and present"; + + // Checks if user specified is in Current Group + if (!currentGroup.get().isMember(argument)) { + String exceptionMessage = argument + " is not in current Group!"; + throw new ExpensesException(exceptionMessage); + } + + Balance balance = new Balance(argument, currentGroup.get()); + balance.printBalance(); + } +} diff --git a/src/main/java/seedu/duke/commands/ExpenseCommand.java b/src/main/java/seedu/duke/commands/ExpenseCommand.java new file mode 100644 index 0000000000..cbe646622b --- /dev/null +++ b/src/main/java/seedu/duke/commands/ExpenseCommand.java @@ -0,0 +1,278 @@ +package seedu.duke.commands; +//@@author mukund1403 + +import seedu.duke.Group; +import seedu.duke.Pair; +import seedu.duke.CurrencyConversions; +import seedu.duke.Money; +import seedu.duke.Expense; +import seedu.duke.UserInterface; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.MessageType; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; +import java.util.Optional; + +public class ExpenseCommand { + + /** + * Takes in an expense from Parser and creates a new expense object. + * Adds the new expense object into the expense list + * @param params : The parameters for the expense including amount, payer, payeeList + * @param argument : The description of the expense + * @param userInput : A string containing users input + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + //@@author Cohii2 + public static void addExpense(HashMap > params,String argument, String userInput) + throws ExpensesException { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + throw new ExpensesException("Not signed in to a Group! Use 'create ' to create Group"); + } + + String[] expenseParams = {"amount", "paid", "user"}; + for (String expenseParam : expenseParams) { + if (params.get(expenseParam).isEmpty()) { + throw new ExpensesException("No " + expenseParam + " for expenses! Add /" + expenseParam); + } + } + //@@author mukund1403 + float totalAmount = getTotal(params); + String currencyString; + if(params.get("currency").isEmpty()){ + currencyString = ""; + } else { + currencyString = params.get("currency").get(0); + } + + CurrencyConversions currency = getCurrency(currencyString); + + Money amountAndCurrency = new Money(totalAmount, currency); + ArrayList payeeList = params.get("user"); + String payerName = params.get("paid").get(0); + + checkDescription(argument); + + Expense newTransaction; + ArrayList> payees = new ArrayList<>(); + if(userInput.contains("/unequal")){ + newTransaction = addUnequalExpense(payeeList, payees, amountAndCurrency, payerName, argument); + } else { + newTransaction = addEqualExpense(payeeList, payees, amountAndCurrency, payerName, argument); + } + UserInterface.printMessage(newTransaction.successMessageString(),MessageType.SUCCESS); + currentGroup.get().addExpense(newTransaction); + } + + //@@author mukund1403 + + /** + * The method deletes expense from the expenses list + * @param listIndex : The index from the list, the user wishes to delete (will be 1 indexed) + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + public static void deleteExpense(String listIndex) throws ExpensesException { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + List expenseList = currentGroup.get().getExpenseList(); + int listSize = expenseList.size(); + int index = getListIndex(listIndex, listSize) - 1; + String deletedExpenseDescription = expenseList.get(index).toString(); + currentGroup.get().deleteExpense(index); + UserInterface.printMessage("Deleted expense:\n" + deletedExpenseDescription,MessageType.SUCCESS); + //System.out.println("Deleted expense:\n" + deletedExpenseDescription); + } + + /** + * Gets the total amount for the expense + * @param params : The parameters for the expense including amount, payer, payeeList + * @return : The total amount as a float + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + public static Float getTotal(HashMap> params) throws ExpensesException { + float totalAmount; + String amount = params.get("amount").get(0); + try { + totalAmount = Float.parseFloat(amount); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter expense with amount as a proper number. (Good bug to start with tbh!)"; + throw new ExpensesException(exceptionMessage); + } + int maxNumberHandled = 2000000000; + if(totalAmount <= 0){ + String exceptionMessage = "Expense amount cannot be 0 or a negative number " + + "(Can try using special characters. I have not handled that!)"; + throw new ExpensesException(exceptionMessage); + } else if(totalAmount > maxNumberHandled) { + String exceptionMessage = "This amount is too big for a small computer like me to handle :(. " + + "Please use a smaller amount"; + throw new ExpensesException(exceptionMessage); + } + return totalAmount; + } + + /** + * Checks if the currency entered is valid and returns it + * @param currencyString : The currency in String format + * @return : The currency for the expense + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + public static CurrencyConversions getCurrency(String currencyString) + throws ExpensesException { + if(currencyString.isEmpty()){ + return CurrencyConversions.SGD; + } else { + switch(currencyString.toUpperCase()) { + case "USD": + return CurrencyConversions.USD; + case "RMB": + return CurrencyConversions.RMB; + case "EUR": + return CurrencyConversions.EUR; + case "JPY": + return CurrencyConversions.JPY; + case "AUD": + return CurrencyConversions.AUD; + case "MYR": + return CurrencyConversions.MYR; + case "SGD": + return CurrencyConversions.SGD; + default: + throw new ExpensesException("Sorry! Either you have entered the currency name incorrectly or" + + " the app does not currently support this currency :("); + } + } + } + + /** + * Checks if the expenses list index provided is valid and returns it + * @param listIndex : The String containing the list index + * @param listSize : The total size of the expenses list + * @return : The list index as integer + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + public static int getListIndex(String listIndex, int listSize) throws ExpensesException { + int index; + try{ + index = Integer.parseInt(listIndex); + } catch(NumberFormatException e){ + String exceptionMessage = "Enter a list index that is an Integer"; + throw new ExpensesException(exceptionMessage); + } + + if(index > listSize){ + String exceptionMessage = "List index is greater than list size"; + throw new ExpensesException(exceptionMessage); + } else if (index <= 0){ + String exceptionMessage = "List index cannot be 0 or negative"; + throw new ExpensesException(exceptionMessage); + } + return index; + } + + /** + * Takes in an expense split unequally and creates a new expense + * @param payeeList : List of people who owe money for the expense + * @param payees : List of people who owe money and how much they owe + * @param totalAmountAndCurrency : Money object containing the total amount and the currency of the expense + * @param payerName : Name of the person who paid for the expense + * @param argument : Description of the expense + * @return : The Expense object created + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + public static Expense addUnequalExpense(ArrayList payeeList, ArrayList> payees, + Money totalAmountAndCurrency, + String payerName, String argument) throws ExpensesException{ + float totalAmount = totalAmountAndCurrency.getAmount(); + float amountDueByPayees = 0; + int payeeInfoMinLength = 2; + CurrencyConversions currency = totalAmountAndCurrency.getCurrency(); + for (String payee : payeeList) { + String[] payeeInfo = payee.split(" "); + + if (payeeInfo.length < payeeInfoMinLength) { + String exceptionMessage = "Amount due for payee with name " + + payeeInfo[0] + " is empty. Enter it and try again"; + throw new ExpensesException(exceptionMessage); + } + String payeeName = mergeBack(payeeInfo); + checkPayeeInGroup(payeeName); + try { + float amountDue = Float.parseFloat(payeeInfo[payeeInfo.length - 1]); + amountDueByPayees += amountDue; + Money amountDueAndCurrency = new Money(amountDue, currency); + payees.add(new Pair<>(payeeName, amountDueAndCurrency)); + } catch (NumberFormatException e) { + String exceptionMessage = "Re-enter amount due for payee with name " + + payeeName + " as a proper number."; + throw new ExpensesException(exceptionMessage); + } + } + if (amountDueByPayees > totalAmount) { + String exceptionMessage = "The amount split between users is greater than total amount. Try again."; + throw new ExpensesException(exceptionMessage); + } + float amountDueForPayer = totalAmount - amountDueByPayees; + Money amountDueAndCurrency = new Money(amountDueForPayer, currency); + payees.add(new Pair<>(payerName, amountDueAndCurrency)); + return new Expense(payerName, argument, totalAmountAndCurrency, payees); + } + + + /** + * Takes in an expense split equally and creates a new expense + * @param payeeList : List of people who owe money for the expense + * @param payees : List of people who owe money and how much they owe + * @param totalAmountAndCurrency : Money object containing the total amount and the currency of the expense + * @param payerName : Name of the person who paid for the expense + * @param argument : Description of the expense + * @return : The Expense object created + * @throws ExpensesException : An exception for expenses class that prints the relevant error message + */ + public static Expense addEqualExpense(ArrayList payeeList, ArrayList> payees, + Money totalAmountAndCurrency, + String payerName,String argument)throws ExpensesException { + float totalAmount = totalAmountAndCurrency.getAmount(); + CurrencyConversions currency = totalAmountAndCurrency.getCurrency(); + float amountDue = totalAmount / (payeeList.size() + 1); + Money amountDueAndCurrency = new Money(amountDue, currency); + for (String payee : payeeList) { + checkPayeeInGroup(payee); + payees.add(new Pair<>(payee, amountDueAndCurrency)); + } + checkPayeeInGroup(payerName); + payees.add(new Pair<>(payerName, amountDueAndCurrency)); + return new Expense(payerName, argument, totalAmountAndCurrency, payees); + } + + private static void checkDescription(String argument) throws ExpensesException { + if(argument.isEmpty()){ + System.out.println("Warning! Empty description"); + } else if(argument.contains("◇")){ + throw new ExpensesException("Special characters not allowed in description! " + + "(Good try trynna catch a bug!)"); + } + } + + private static void checkPayeeInGroup(String payee) + throws ExpensesException { + if(!Group.isMember(payee)){ + throw new ExpensesException(payee + " is not a member of the group!"); + } + } + + private static String mergeBack(String[] splitArray){ + String mergedString = ""; + for(int i = 0; i < splitArray.length-2; i++){ + mergedString += splitArray[i].trim() + " "; + } + mergedString += splitArray[splitArray.length-2]; + return mergedString; + } +} diff --git a/src/main/java/seedu/duke/commands/GroupCommand.java b/src/main/java/seedu/duke/commands/GroupCommand.java new file mode 100644 index 0000000000..300dee97ea --- /dev/null +++ b/src/main/java/seedu/duke/commands/GroupCommand.java @@ -0,0 +1,99 @@ +//@@author hafizuddin-a + +package seedu.duke.commands; + +import seedu.duke.Group; +import seedu.duke.exceptions.GroupDeleteException; +import seedu.duke.storage.FileIO; +import seedu.duke.storage.FileIOImpl; +import seedu.duke.storage.GroupNameChecker; +import seedu.duke.storage.GroupStorage; + +import java.util.Optional; + +/** + * Represents a command handler for group-related operations. + * Provides static methods to create groups, add members to groups, and exit groups. + */ +public class GroupCommand { + /** + * Creates a new group or retrieves an existing group with the specified name. + * + * @param groupName the name of the group to create or retrieve + */ + public static void createGroup(String groupName) { + try { + Group.getOrCreateGroup(groupName); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); + } + } + + //@@author avrilgk + + /** + * Deletes the current group. + * If the user is not currently in a group, prints a message indicating so. + * + * @param groupName the name of the group to delete + */ + public static void deleteGroup(String groupName) { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isPresent() && currentGroup.get().getGroupName().equals(groupName)) { + System.out.println("Please exit current group before deleting group."); + return; + } + + try { + FileIO fileIO = new FileIOImpl(); + GroupStorage groupStorage = new GroupStorage(fileIO); + GroupNameChecker groupNameChecker = new GroupNameChecker(); + if (groupNameChecker.doesGroupNameExist(groupName)) { + groupStorage.deleteGroupFile(groupName); + Group.removeGroup(groupName); + System.out.println("The group " + groupName + " has been deleted."); + } else { + System.out.println("The group " + groupName + " does not exist."); + } + } catch (GroupDeleteException e) { + System.out.println("Failed to delete the group: " + e.getMessage()); + } + } + //@@author hafizuddin-a + /** + * Adds a member with the specified name to the current group. + * If the user is not currently in a group, prints a message asking them to create or join a group first. + * + * @param memberName the name of the member to add + */ + public static void addMember(String memberName) { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + System.out.println("Please create or join a group first."); + return; + } + + currentGroup.get().addMember(memberName); + } + + //@@author avrilgk + + /** + * Enters an existing group with the specified name. + * + * @param groupName the name of the group to enter + */ + + public static void enterGroup(String groupName) { + Group.enterGroup(groupName); + } + + /** + * Exits the current group. + * If the user is not currently in a group, prints a message indicating so. + */ + public static void exitGroup(String groupName) { + Group.exitGroup(groupName); + } +} + diff --git a/src/main/java/seedu/duke/commands/ListCommand.java b/src/main/java/seedu/duke/commands/ListCommand.java new file mode 100644 index 0000000000..88e36005fa --- /dev/null +++ b/src/main/java/seedu/duke/commands/ListCommand.java @@ -0,0 +1,25 @@ +package seedu.duke.commands; +//@@author mukund1403 +import seedu.duke.Expense; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.Group; + +import java.util.List; +import java.util.Optional; + +public class ListCommand { + public static void printList() throws ExpensesException { + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new ExpensesException(exceptionMessage); + } + List expenses = currentGroup.get().getExpenseList(); + System.out.println("The expenses for this group are:\n"); + int i = 1; + for(Expense expense : expenses){ + System.out.println(i + ". " + expense.toString()); + i++; + } + } +} diff --git a/src/main/java/seedu/duke/commands/LuckCommand.java b/src/main/java/seedu/duke/commands/LuckCommand.java new file mode 100644 index 0000000000..1a75cbe836 --- /dev/null +++ b/src/main/java/seedu/duke/commands/LuckCommand.java @@ -0,0 +1,35 @@ +package seedu.duke.commands; + +import seedu.duke.Group; +import seedu.duke.Luck; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.exceptions.LuckException; + +import java.util.Optional; + +public class LuckCommand { + /** + * Checks if user is currently in a Group + * Checks if the group has more than one person. + * Creates a Luck object + * Prints the welcome message and start the slot machine. + * @throws LuckException If user is not in a Group + * @throws LuckException If the group has less than 2 people. + */ + public static void handleLuck(String argument) throws LuckException, ExpensesException { + // Checks if user is currently in a Group + Optional currentGroup = Group.getCurrentGroup(); + if (currentGroup.isEmpty()) { + String exceptionMessage = "Not signed in to a Group! Use 'create ' to create Group"; + throw new LuckException(exceptionMessage); + } + Group selectedGroup = currentGroup.get(); + if (selectedGroup.getMembers().size() <= 1){ + String exceptionMessage = "You need more people to get lucky!!!"; + throw new LuckException(exceptionMessage); + } + Luck newLuck = new Luck(selectedGroup, argument); + newLuck.printWelcome(); + newLuck.startGambling(); + } +} diff --git a/src/main/java/seedu/duke/exceptions/ExpensesException.java b/src/main/java/seedu/duke/exceptions/ExpensesException.java new file mode 100644 index 0000000000..0fd159561e --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/ExpensesException.java @@ -0,0 +1,15 @@ +package seedu.duke.exceptions; + +public class ExpensesException extends Exception { + public ExpensesException(String s, Throwable err){ + super(s,err); + } + + public ExpensesException(String s){ + super(s); + } + + public ExpensesException() { + + } +} diff --git a/src/main/java/seedu/duke/exceptions/GroupDeleteException.java b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java new file mode 100644 index 0000000000..89f7c224b3 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/GroupDeleteException.java @@ -0,0 +1,16 @@ +package seedu.duke.exceptions; + +/** + * Represents an exception that occurs during the deletion of a group. + * This exception is thrown when there is an error or failure in the group deletion process. + */ +public class GroupDeleteException extends UniversalExceptions { + /** + * Constructs a new GroupDeleteException with the specified detail message. + * + * @param message the detail message describing the exception + */ + public GroupDeleteException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/exceptions/GroupLoadException.java b/src/main/java/seedu/duke/exceptions/GroupLoadException.java new file mode 100644 index 0000000000..389a2ad48f --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/GroupLoadException.java @@ -0,0 +1,16 @@ +package seedu.duke.exceptions; + +/** + * Represents an exception that occurs when loading group information fails. + * This exception is thrown when an error occurs while loading a group from a file. + */ +public class GroupLoadException extends UniversalExceptions { + /** + * Constructs a new GroupLoadException with the specified detail message. + * + * @param message The detail message. + */ + public GroupLoadException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/exceptions/GroupSaveException.java b/src/main/java/seedu/duke/exceptions/GroupSaveException.java new file mode 100644 index 0000000000..ea739307b5 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/GroupSaveException.java @@ -0,0 +1,16 @@ +package seedu.duke.exceptions; + +/** + * Represents an exception that occurs when saving group information fails. + * This exception is thrown when an error occurs while saving a group to a file. + */ +public class GroupSaveException extends UniversalExceptions { + /** + * Constructs a new GroupSaveException with the specified detail message. + * + * @param message The detail message. + */ + public GroupSaveException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/exceptions/LuckException.java b/src/main/java/seedu/duke/exceptions/LuckException.java new file mode 100644 index 0000000000..c160606e40 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/LuckException.java @@ -0,0 +1,7 @@ +package seedu.duke.exceptions; + +public class LuckException extends UniversalExceptions { + public LuckException(String message) { + super(message); + } +} diff --git a/src/main/java/seedu/duke/exceptions/UniversalExceptions.java b/src/main/java/seedu/duke/exceptions/UniversalExceptions.java new file mode 100644 index 0000000000..445a81526c --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/UniversalExceptions.java @@ -0,0 +1,17 @@ +package seedu.duke.exceptions; + +public class UniversalExceptions extends Exception { + private final String errorMessage; + UniversalExceptions(String errorMessage) { + this.errorMessage = errorMessage; + } + + String getErrorMessage() { + return this.errorMessage; + } + + @Override + public String toString() { + return this.errorMessage; + } +} diff --git a/src/main/java/seedu/duke/storage/FileIO.java b/src/main/java/seedu/duke/storage/FileIO.java new file mode 100644 index 0000000000..53770ddfe9 --- /dev/null +++ b/src/main/java/seedu/duke/storage/FileIO.java @@ -0,0 +1,38 @@ +package seedu.duke.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; + +/** + * Represents the file I/O operations. + * Defines methods for reading from and writing to files. + */ +public interface FileIO { + /** + * Returns a BufferedReader for reading from the specified file. + * + * @param filePath The path of the file to read from. + * @return A BufferedReader for reading from the file. + * @throws IOException If an I/O error occurs while creating the reader. + */ + BufferedReader getFileReader(String filePath) throws IOException; + + /** + * Returns a BufferedWriter for writing to the specified file. + * + * @param filePath The path of the file to write to. + * @return A BufferedWriter for writing to the file. + * @throws IOException If an I/O error occurs while creating the writer. + */ + BufferedWriter getFileWriter(String filePath) throws IOException; + + /** + * Deletes the file at the specified file path. + * + * @param filePath The path of the file to be deleted. + * @return true if the file was successfully deleted, false otherwise. + * @throws IOException If an I/O error occurs while deleting the file. + */ + boolean deleteFile(String filePath) throws IOException; +} diff --git a/src/main/java/seedu/duke/storage/FileIOImpl.java b/src/main/java/seedu/duke/storage/FileIOImpl.java new file mode 100644 index 0000000000..1b7bb9a165 --- /dev/null +++ b/src/main/java/seedu/duke/storage/FileIOImpl.java @@ -0,0 +1,52 @@ +package seedu.duke.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +/** + * Implements the FileIO interface. + * Provides concrete implementations for file I/O operations using BufferedReader and BufferedWriter. + */ +public class FileIOImpl implements FileIO { + /** + * Returns a BufferedReader for reading from the specified file. + * + * @param filePath The path of the file to read from. + * @return A BufferedReader for reading from the file. + * @throws IOException If an I/O error occurs while creating the reader. + */ + @Override + public BufferedReader getFileReader(String filePath) throws IOException { + return new BufferedReader(new FileReader(filePath)); + } + + /** + * Returns a BufferedWriter for writing to the specified file. + * + * @param filePath The path of the file to write to. + * @return A BufferedWriter for writing to the file. + * @throws IOException If an I/O error occurs while creating the writer. + */ + @Override + public BufferedWriter getFileWriter(String filePath) throws IOException { + return new BufferedWriter(new FileWriter(filePath)); + } + + /** + * Deletes the file at the specified file path. + * + * @param filePath The path of the file to be deleted. + * @return true if the file was successfully deleted, false otherwise. + * @throws IOException If an I/O error occurs while deleting the file. + */ + @Override + public boolean deleteFile(String filePath) throws IOException { + File file = new File(filePath); + return file.delete(); + } +} diff --git a/src/main/java/seedu/duke/storage/GroupFilePath.java b/src/main/java/seedu/duke/storage/GroupFilePath.java new file mode 100644 index 0000000000..858eb1afe7 --- /dev/null +++ b/src/main/java/seedu/duke/storage/GroupFilePath.java @@ -0,0 +1,59 @@ +package seedu.duke.storage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Represents the file path management for group files. + * Provides methods to get the file path for a group and create the groups directory. + */ +public class GroupFilePath { + private static String groupsDirectory = "data/groups"; + private static final String GROUP_FILE_EXTENSION = ".txt"; + + /** + * Returns the file path for the group file. + * + * @param groupName The name of the group. + * @return The file path for the group file. + */ + public static String getFilePath(String groupName) { + assert groupName != null && !groupName.isEmpty() : "Group name cannot be null or empty"; + return groupsDirectory + "/" + groupName + GROUP_FILE_EXTENSION; + } + + /** + * Creates the groups directory if it does not exist. + * + * @throws IOException If an I/O error occurs while creating the directory. + */ + public static void createGroupDirectory() throws IOException { + Path path = Paths.get(groupsDirectory); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } + + /** + * Sets the directory where group files are stored. + *

+ * This method allows changing the default groups directory to a custom directory. + * It is useful for testing purposes or when the groups need to be stored in a different location. + *

+ * Assertions are used to check that the provided directory is not null or empty. + * If the assertion fails, an {@code AssertionError} will be thrown, indicating a programming error. + * + * @param directory the directory where group files should be stored + * @throws AssertionError if the provided directory is null or empty + */ + public static void setGroupsDirectory(String directory) { + assert directory != null && !directory.isEmpty() : "Groups directory cannot be null or empty"; + groupsDirectory = directory; + } + + public static String getGroupsDirectory() { + return groupsDirectory; + } +} diff --git a/src/main/java/seedu/duke/storage/GroupNameChecker.java b/src/main/java/seedu/duke/storage/GroupNameChecker.java new file mode 100644 index 0000000000..86b64812a1 --- /dev/null +++ b/src/main/java/seedu/duke/storage/GroupNameChecker.java @@ -0,0 +1,75 @@ +package seedu.duke.storage; + +import seedu.duke.MessageType; +import seedu.duke.UserInterface; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class GroupNameChecker { + + /** + * Checks if a specific group name exists in the saved files. + * + * @param groupNameToCheck the group name to check for + * @return true if the group name exists, false otherwise + */ + public boolean doesGroupNameExist(String groupNameToCheck) { + Path groupsDirectory = getGroupsDirectoryPath(); + if (groupsDirectory == null) { + return false; + } + return checkGroupNameInDirectory(groupsDirectory, groupNameToCheck); + } + + /** + * Gets the Path object for the groups directory. + * + * @return the Path object for the groups directory, or null if an error occurs + */ + private Path getGroupsDirectoryPath() { + try { + return Paths.get(GroupFilePath.getGroupsDirectory()); + } catch (Exception e) { + UserInterface.printMessage("An error occurred while getting the groups directory path: " + + e.getMessage(), MessageType.ERROR); + return null; + } + } + + /** + * Checks if the given group name exists within the specified directory. + * + * @param directoryPath the path to the directory containing group files + * @param groupNameToCheck the group name to check for + * @return true if the group name exists, false otherwise + */ + private boolean checkGroupNameInDirectory(Path directoryPath, String groupNameToCheck) { + try (DirectoryStream stream = Files.newDirectoryStream(directoryPath, "*.txt")) { + for (Path file : stream) { + if (extractGroupNameFromFile(file).equals(groupNameToCheck)) { + return true; + } + } + } catch (IOException e) { + System.out.println("Data directory not found: " + e.getMessage() + ". Creating new directory..."); + } + return false; + } + + /** + * Extracts the group name from a file path. + * + * @param file the Path object of the file + * @return the extracted group name + */ + private String extractGroupNameFromFile(Path file) { + String fileName = file.getFileName().toString(); + return fileName.substring(0, fileName.lastIndexOf('.')); + } +} + + diff --git a/src/main/java/seedu/duke/storage/GroupStorage.java b/src/main/java/seedu/duke/storage/GroupStorage.java new file mode 100644 index 0000000000..f6434a4c68 --- /dev/null +++ b/src/main/java/seedu/duke/storage/GroupStorage.java @@ -0,0 +1,247 @@ +package seedu.duke.storage; + +import seedu.duke.CurrencyConversions; +import seedu.duke.Expense; +import seedu.duke.Group; +import seedu.duke.MessageType; +import seedu.duke.Money; +import seedu.duke.Pair; +import seedu.duke.User; +import seedu.duke.UserInterface; +import seedu.duke.commands.ExpenseCommand; +import seedu.duke.exceptions.ExpensesException; +import seedu.duke.exceptions.GroupDeleteException; +import seedu.duke.exceptions.GroupLoadException; +import seedu.duke.exceptions.GroupSaveException; + + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the storage manager for group data. + * Handles the saving and loading of group information to and from files. + */ +public class GroupStorage { + public static boolean isLoading = false; + + private static final String MEMBERS_HEADER = "Members:"; + private static final String EXPENSES_HEADER = "Expenses:"; + private static final String EXPENSE_DELIMITER = "#"; + private static final String PAYEE_DELIMITER = ":"; + private static final String PAYEE_DATA_DELIMITER = ","; + + private final FileIO fileIO; + + /** + * Constructs a GroupStorage object with the specified FileIO dependency. + * + * @param fileIO the FileIO instance for file input/output operations + */ + public GroupStorage(FileIO fileIO) { + this.fileIO = fileIO; + } + + /** + * Saves the group information to a file. + * + * @param group the group to save + * @throws GroupSaveException if an error occurs while saving the group information + */ + public void saveGroupToFile(Group group) throws GroupSaveException { + try { + GroupFilePath.createGroupDirectory(); + String filePath = GroupFilePath.getFilePath(group.getGroupName()); + BufferedWriter writer = fileIO.getFileWriter(filePath); + + saveGroupName(writer, group.getGroupName()); + saveMembers(writer, group.getMembers()); + saveExpenses(writer, group.getExpenseList()); + + writer.close(); + } catch (IOException e) { + throw new GroupSaveException("An error occurred while saving the group information."); + } + } + + /** + * Saves the group name to the file. + * + * @param writer the BufferedWriter for writing to the file + * @param groupName the name of the group + * @throws IOException if an I/O error occurs while writing to the file + */ + private void saveGroupName(BufferedWriter writer, String groupName) throws IOException { + writer.write(groupName); + writer.newLine(); + } + + /** + * Saves the group members to the file. + * + * @param writer the BufferedWriter for writing to the file + * @param members the list of members in the group + * @throws IOException if an I/O error occurs while writing to the file + */ + private void saveMembers(BufferedWriter writer, List members) throws IOException { + writer.write(MEMBERS_HEADER); + writer.newLine(); + for (User member : members) { + writer.write(member.getName()); + writer.newLine(); + } + } + + /** + * Saves the group expenses to the file. + * + * @param writer the BufferedWriter for writing to the file + * @param expenses the list of expenses in the group + * @throws IOException if an I/O error occurs while writing to the file + */ + private void saveExpenses(BufferedWriter writer, List expenses) throws IOException { + writer.write(EXPENSES_HEADER); + writer.newLine(); + for (Expense expense : expenses) { + StringBuilder expenseData = new StringBuilder(); + expenseData.append(expense.getTotalAmount()).append(EXPENSE_DELIMITER) + .append(expense.getCurrency()).append(EXPENSE_DELIMITER) + .append(expense.getPayerName()).append(EXPENSE_DELIMITER) + .append(expense.getDescription()).append(EXPENSE_DELIMITER); + + List payeeData = new ArrayList<>(); + for (Pair payee : expense.getPayees()) { + payeeData.add(payee.getKey() + PAYEE_DELIMITER + payee.getValue().getAmount() + + PAYEE_DELIMITER + payee.getValue().getCurrency()); + } + expenseData.append(String.join(PAYEE_DATA_DELIMITER, payeeData)); + + writer.write(expenseData.toString()); + writer.newLine(); + } + } + + /** + * Loads the group information from a file. + * + * @param groupName the name of the group to load + * @return the loaded group + * @throws GroupLoadException if an error occurs while loading the group information + */ + public Group loadGroupFromFile(String groupName) throws GroupLoadException { + isLoading = true; + try { + String filePath = GroupFilePath.getFilePath(groupName); + BufferedReader reader = fileIO.getFileReader(filePath); + + Group group = loadGroupName(reader); + if (group == null) { + throw new GroupLoadException("Invalid group data file. Unable to load group name."); + } + + try { + loadMembers(reader, group); + loadExpenses(reader, group); + } catch (IOException e) { + throw new GroupLoadException("Error loading group members or expenses: " + e.getMessage()); + } + + reader.close(); + return group; + } catch (IOException e) { + throw new GroupLoadException("An error occurred while loading the group: " + e.getMessage()); + } catch (Exception e) { + throw new GroupLoadException("An unexpected error occurred while loading the group: " + e.getMessage()); + } finally { + isLoading = false; + UserInterface.printMessage("Group loaded successfully.", MessageType.SUCCESS); + } + } + + /** + * Loads the group name from the file. + * + * @param reader the BufferedReader for reading from the file + * @return the loaded group + * @throws IOException if an I/O error occurs while reading from the file + */ + private Group loadGroupName(BufferedReader reader) throws IOException { + String line = reader.readLine(); + if (line != null && !line.isEmpty()) { + return new Group(line.trim()); + } + return null; + } + + /** + * Loads the group members from the file. + * + * @param reader the BufferedReader for reading from the file + * @param group the group to add the loaded members to + * @throws IOException if an I/O error occurs while reading from the file + */ + private void loadMembers(BufferedReader reader, Group group) throws IOException { + String header = reader.readLine(); + if (header == null || !header.equals(MEMBERS_HEADER)) { + throw new IOException("Invalid group data file. Missing or invalid 'Members:' header."); + } + + String line; + while ((line = reader.readLine()) != null && !line.equals(EXPENSES_HEADER)) { + group.addMember(line); + } + } + + /** + * Loads the group expenses from the file. + * + * @param reader the BufferedReader for reading from the file + * @param group the group to add the loaded expenses to + * @throws IOException if an I/O error occurs while reading from the file + */ + private void loadExpenses(BufferedReader reader, Group group) throws IOException, ExpensesException { + String line; + while ((line = reader.readLine()) != null) { + String[] expenseData = line.split(EXPENSE_DELIMITER, 5); + float totalAmount = Float.parseFloat(expenseData[0]); + String currencyString = expenseData[1]; + String payerName = expenseData[2]; + String description = expenseData[3]; + String[] payeeData = expenseData[4].split(PAYEE_DATA_DELIMITER); + + CurrencyConversions currency = ExpenseCommand.getCurrency(currencyString); + ArrayList> payeeList = new ArrayList<>(); + for (String payee : payeeData) { + String[] payeeInfo = payee.split(PAYEE_DELIMITER); + String payeeName = payeeInfo[0]; + float amountDue = Float.parseFloat(payeeInfo[1]); + Money amountAndCurrency = new Money(amountDue, currency); + payeeList.add(new Pair<>(payeeName, amountAndCurrency)); + } + Money amountAndCurrency = new Money(totalAmount, currency); + Expense expense = new Expense(payerName, description, amountAndCurrency, payeeList); + group.addExpense(expense); + } + } + + /** + * Deletes the group file for the specified group name. + * + * @param groupName the name of the group whose file needs to be deleted + * @throws GroupDeleteException if an error occurs while deleting the group file + */ + public void deleteGroupFile(String groupName) throws GroupDeleteException { + try { + String filePath = GroupFilePath.getFilePath(groupName); + boolean deleted = fileIO.deleteFile(filePath); + if (!deleted) { + throw new GroupDeleteException("Failed to delete the group file."); + } + } catch (IOException e) { + throw new GroupDeleteException("An error occurred while deleting the group file: " + e.getMessage()); + } + } +} diff --git a/src/test/java/seedu/duke/AddUserTest.java b/src/test/java/seedu/duke/AddUserTest.java new file mode 100644 index 0000000000..5298c9137c --- /dev/null +++ b/src/test/java/seedu/duke/AddUserTest.java @@ -0,0 +1,38 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class AddUserTest { + + @Test + public void testUser() { + try { + User user = new User("John"); + assertEquals("John", user.getName()); + } catch (Exception e) { + fail("Exception occurred while creating a User object: " + e.getMessage()); + } + } + + @Test + public void testAddUserToGroup() { + String groupName = "TestGroup"; + Optional group = Group.getOrCreateGroup(groupName); + if (group.isEmpty()) { + System.out.println("Group does not exist."); + return; + } + + User user = group.get().addMember("TestUser"); + + assertTrue(group.get().getMembers().contains(user), "User was not added to the group"); + } +} + + diff --git a/src/test/java/seedu/duke/BalanceTest.java b/src/test/java/seedu/duke/BalanceTest.java new file mode 100644 index 0000000000..36e3e4e6e5 --- /dev/null +++ b/src/test/java/seedu/duke/BalanceTest.java @@ -0,0 +1,85 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ExpensesException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +//@@author Cohii2 +public class BalanceTest { + @Test + public void testConstructor() throws ExpensesException { + List users = new ArrayList<>(); + users.add(new User("member1")); + users.add(new User("member2")); + users.add(new User("member3")); + + List expenses = new ArrayList<>(); + expenses.add(new Expense("member1", "expense1", + new Money(20f, CurrencyConversions.SGD), + new ArrayList<>(Arrays.asList( + new Pair<>("member2", new Money(5.0f, CurrencyConversions.SGD)), + new Pair<>("member3", new Money(10.0f, CurrencyConversions.SGD)) + )))); + expenses.add(new Expense("member2", "expense2", + new Money(30f, CurrencyConversions.SGD), + new ArrayList<>(Arrays.asList( + new Pair<>("member1", new Money(10.0f, CurrencyConversions.SGD)), + new Pair<>("member3", new Money(10.0f, CurrencyConversions.SGD)) + )))); + expenses.add(new Expense("member3", "expense3", + new Money(100f, CurrencyConversions.AUD), + new ArrayList<>(Arrays.asList( + new Pair<>("member1", new Money(50.0f, CurrencyConversions.AUD)) + )))); + + Balance member1Balance = new Balance("member1", expenses, users); + member1Balance.printBalance(); + Balance member2Balance = new Balance("member2", expenses, users); + member2Balance.printBalance(); + Balance member3Balance = new Balance("member3", expenses, users); + member3Balance.printBalance(); + + testList(moneyListMaker(new Money(-5.00f, CurrencyConversions.SGD)), + member1Balance.getBalanceList().get("member2")); + testList(moneyListMaker(new Money(10.0f, CurrencyConversions.SGD), + new Money(-50.0f, CurrencyConversions.AUD)), + member1Balance.getBalanceList().get("member3")); + + testList(moneyListMaker(new Money(5.0f, CurrencyConversions.SGD)), + member2Balance.getBalanceList().get("member1")); + testList(moneyListMaker(new Money(10.0f, CurrencyConversions.SGD)), + member2Balance.getBalanceList().get("member3")); + + testList(moneyListMaker(new Money(-10.0f, CurrencyConversions.SGD)), + member3Balance.getBalanceList().get("member2")); + testList(moneyListMaker(new Money(-10.0f, CurrencyConversions.SGD), + new Money(50.0f, CurrencyConversions.AUD)), + member3Balance.getBalanceList().get("member1")); + } + + public void testList(List l1, List l2){ + assert(l1.size() == l2.size()); + for(int i = 0; i < l1.size(); i++){ + assertEquals(l1.get(i).getCurrency(), l2.get(i).getCurrency()); + assertEquals(l1.get(i).getAmount(), l2.get(i).getAmount()); + } + } + + public List moneyListMaker(Money money1, Money money2){ + List moneyList = new ArrayList<>(); + moneyList.add(money1); + moneyList.add(money2); + return moneyList; + } + + public List moneyListMaker(Money money1){ + List moneyList = new ArrayList<>(); + moneyList.add(money1); + return moneyList; + } +} diff --git a/src/test/java/seedu/duke/CurrencyConversionsTest.java b/src/test/java/seedu/duke/CurrencyConversionsTest.java new file mode 100644 index 0000000000..3833753e36 --- /dev/null +++ b/src/test/java/seedu/duke/CurrencyConversionsTest.java @@ -0,0 +1,24 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +public class CurrencyConversionsTest { + @Test + void testCurrencyConversions() { + assert (CurrencyConversions.AUD.getName().equals("AUD")); + assert (CurrencyConversions.AUD.getRate() == 1.12F); + assert (CurrencyConversions.AUD.getInverseRate() == 1.00F / 1.12F); + assert (CurrencyConversions.USD.getName().equals("USD")); + assert (CurrencyConversions.USD.getRate() == 0.74F); + assert (CurrencyConversions.RMB.getName().equals("RMB")); + assert (CurrencyConversions.RMB.getRate() == 5.35F); + assert (CurrencyConversions.EUR.getName().equals("EUR")); + assert (CurrencyConversions.EUR.getRate() == 0.687F); + assert (CurrencyConversions.JPY.getName().equals("JPY")); + assert (CurrencyConversions.JPY.getRate() == 112.12F); + assert (CurrencyConversions.MYR.getName().equals("MYR")); + assert (CurrencyConversions.MYR.getRate() == 3.50F); + assert (CurrencyConversions.SGD.getName().equals("SGD")); + assert (CurrencyConversions.SGD.getRate() == 1.00F); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java index 2dda5fd651..f3d4ed2e55 100644 --- a/src/test/java/seedu/duke/DukeTest.java +++ b/src/test/java/seedu/duke/DukeTest.java @@ -1,12 +1,10 @@ package seedu.duke; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; -class DukeTest { +public class DukeTest { @Test - public void sampleTest() { - assertTrue(true); + public void groupTest() { + // test code here } } diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java new file mode 100644 index 0000000000..f663cfa1e1 --- /dev/null +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -0,0 +1,109 @@ +package seedu.duke; +//@@author mukund1403 + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import seedu.duke.commands.ExpenseCommand; +import seedu.duke.exceptions.ExpensesException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +class ExpenseTest{ + @Test + void newExpenseTest() { + ArrayList> payees = new ArrayList<>(Arrays.asList( + new Pair<>("cohii", new Money(2.0f, CurrencyConversions.SGD)), + new Pair<>("shao", new Money(3.2f, CurrencyConversions.SGD)), + new Pair<>("avril", new Money(1.0f, CurrencyConversions.SGD)), + new Pair<>("hafiz", new Money(2.0f, CurrencyConversions.SGD)), + new Pair<>("mukund", new Money(1.8f, CurrencyConversions.SGD)) + )); + + Money totalAmountAndCurrency = new Money(10,CurrencyConversions.SGD); + Expense testExpense1 = new Expense("mukund","disneyland", + totalAmountAndCurrency, payees); + + assertEquals(testExpense1.getPayees(),payees); + } + + @Test + public void amountNotFloatTest() { + ArrayList amountArrayList = new ArrayList<>(); + amountArrayList.add("b"); + HashMap> params = new HashMap<>(); + params.put("amount",amountArrayList); + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getTotal(params), + "Function should throw Expenses exception since exception occured in expenses class."); + assertEquals("Re-enter expense with amount as a proper number. " + + "(Good bug to start with tbh!)", e.getMessage(), + "Exception message should indicate that amount entered was not a number"); + + } + + @Test + public void listIndexGreaterTest(){ + String listIndex = "20"; + int listSize = 2; + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex, listSize), + "Function should throw ExpensesException"); + assertEquals("List index is greater than list size",e.getMessage(), + "Exception message should indicate that listIndex entered was greater than list size."); + } + + @Test + public void listIndexNegativeTest(){ + String listIndex = "-1"; + int listSize = 2; + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex, listSize), + "Function should throw ExpensesException"); + assertEquals("List index cannot be 0 or negative",e.getMessage(), + "Exception message should indicate that listIndex entered was less than or equal to 0."); + } + + @Test + public void listIndexNotIntegerTest(){ + String listIndex1 = "a"; + int listSize = 2; + Exception e1 = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex1, listSize), + "Function should throw ExpensesException"); + assertEquals("Enter a list index that is an Integer",e1.getMessage(), + "Exception message should indicate that listIndex entered was not an integer."); + + String listIndex2 = "5.0"; + Exception e2 = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.getListIndex(listIndex1, listSize), + "Function should throw ExpensesException"); + assertEquals("Enter a list index that is an Integer",e2.getMessage(), + "Exception message should indicate that listIndex entered was not an integer."); + } + + @Test + public void unequalExpenseTest() { + ArrayList payeeList = new ArrayList<>(Arrays.asList( + ("cohii"), + ("shao 1"), + ("avril 5.5"), + ("hafiz"), + ("mukund 2") + )); + ArrayList> payees = new ArrayList<>(); + Money totalAmount = new Money(10,CurrencyConversions.SGD); + String payerName = "mukund"; + String argument = "disneyland"; + Exception e = Assertions.assertThrows(ExpensesException.class, + () -> ExpenseCommand.addUnequalExpense(payeeList, payees, totalAmount, payerName, argument), + "Function should throw ExpensesException"); + assertEquals("Amount due for payee with name cohii" + + " is empty. Enter it and try again", e.getMessage(), + "Exception message should indicate that amount for user has not been entered"); + } +} diff --git a/src/test/java/seedu/duke/GroupTest.java b/src/test/java/seedu/duke/GroupTest.java new file mode 100644 index 0000000000..847662018b --- /dev/null +++ b/src/test/java/seedu/duke/GroupTest.java @@ -0,0 +1,74 @@ +//@@author avrilgk + +package seedu.duke; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GroupTest { + @BeforeEach + public void setup() { + // Clear all groups and reset the current group name before each test + Group.groups.clear(); + Group.currentGroupName = Optional.empty(); + } + + @Test + public void testGetOrCreateGroup() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + assertTrue(Group.groups.containsKey(groupName), "Group should be created"); + } + + @Test + public void testEnterGroup() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + assertEquals(groupName, Group.currentGroupName.get(), "Current group name should match the expected name"); + } + + @Test + public void testExitGroup() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + Group.exitGroup(groupName); + assertTrue(Group.currentGroupName.isEmpty(), "Current group name should be empty"); + } + + @Test + public void testNullGroup() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + Group.exitGroup(groupName); + Group.exitGroup(groupName); + assertTrue(Group.currentGroupName.isEmpty(), "Current group name should be empty"); + } + + @Test + public void testAddMember() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + Group.getCurrentGroup().get().addMember("Alice"); + assertTrue(Group.getCurrentGroup().get().isMember("Alice"), "Alice should be a member of the group"); + } + + @Test + public void testAddMultipleMembers() { + String groupName = "TestGroup"; + Group.getOrCreateGroup(groupName); + Group.enterGroup(groupName); + Group.getCurrentGroup().get().addMember("Alice"); + Group.getCurrentGroup().get().addMember("Bob"); + assertTrue(Group.getCurrentGroup().get().isMember("Alice"), "Alice should be a member of the group"); + assertTrue(Group.getCurrentGroup().get().isMember("Bob"), "Bob should be a member of the group"); + } +} diff --git a/src/test/java/seedu/duke/HelpTest.java b/src/test/java/seedu/duke/HelpTest.java new file mode 100644 index 0000000000..822b9b184c --- /dev/null +++ b/src/test/java/seedu/duke/HelpTest.java @@ -0,0 +1,46 @@ +package seedu.duke; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +public class HelpTest { + private static final String prompt = + "Welcome, here is a list of commands:\n" + + "help: Access help menu.\n" + + "create : Create a group.\n" + + "exit : Exit current group.\n" + + "member : Add a member to the group.\n" + + "expense /amount " + + "/currency /paid " + + "/user /user ...: " + + "Add an expense SPLIT EQUALLY.\n" + + "expense /unequal /amount " + + "/currency /paid " + + "/user " + + "/user ...: " + + "Add an expense SPLIT UNEQUALLY.\n" + + "list: List all expenses in the group.\n" + + "balance : Show user's balance.\n" + + "settle /user : Settle the amount between two users.\n" + + "luck : luck is in the air tonight"; + + @Test + public void dummyTest() { + assertEquals(2, 2); + } + @Test + public void testPrint() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + System.setOut(ps); + printHelp(); + String output = baos.toString(); + assertEquals(prompt, output); + } + + static void printHelp() { + System.out.print(prompt); + } +} diff --git a/src/test/java/seedu/duke/LuckTest.java b/src/test/java/seedu/duke/LuckTest.java new file mode 100644 index 0000000000..ba4d437eaf --- /dev/null +++ b/src/test/java/seedu/duke/LuckTest.java @@ -0,0 +1,52 @@ +package seedu.duke; + + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class LuckTest { + private static final String testOutput = + + " .=*+::. \n" + + " =*=-:::. \n" + + " .:=+**#***####*+=-:. \n" + + " :+#%%%%###%########%%%%*=: \n" + + " .=#%%%####################%%%#=. \n" + + " +#%%%####%%%###%%%###%%%%#*###%%#= \n" + + " :##%%########%%###%%###%%#########%%#: \n" + + " .##%#*#%%%%#######*##*#######%%%##*#%%#: \n" + + " *%%%##%%###%%%############%%%%%%%%##%%%#. \n" + + " .%#*****+*****++*++*****+**++***********%= \n" + + " :%*-::.-=-:::-==:==-:::-=:-===---==-.::+%+ \n" + + " :%*=:.+@@@%%@@@*=@@@#%%@@%-@@@@%@@@@-.-+#+ .=-.. \n" + + " :%*=.=%+*=+***@-%%+*=+##*@=*%++==**+%::+#=.##--- \n" + + " :%*=.#%-++=--*%-@#-++=--*@+=%-+*=--*@=:+#+ -::-: \n" + + " :%*=.#@%@%=-*@%-@@%@%=-#@@+=@%@%=-#@@=:+#+ -+: \n" + + " -%*=:=@@%---%@@:%@@%---%@@=*@@%---%@@::+#+ =*: \n" + + " -%*=:.#@@###@@@*+@@@###@@@-@@@@###@@=.-+#+ -+: \n" + + " -%*=:.:*##**+***:********+:***=+=**-..:+%*--+*: \n" + + " :%*+=====-==========-=-===============+*%*---- \n" + + " +##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##*. \n" + + " =******************************************* \n" + + " =++=++++++++++++++++++++++++++++++++++++++++ \n" + + " =*++++++++++++++++++++++++++++++++++++++++*+ \n" + + " :----------------------------------------::. \n" + + "Gamble Gamble Gamble! Crazy Slots!!!\n" + + "10 USD Per round, given to random user in your group!!!\n" + + "Win if all 3 MIDDLE slots are the same and clear your debts!!!\n"; + + void testLuck() { + Group newGroup = new Group("lmao"); + String username = "heehee"; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + System.setOut(ps); + Luck newLuck = new Luck(newGroup, username); + newLuck.printWelcome(); + String output = baos.toString(); + assertEquals(testOutput, output); + } +} diff --git a/src/test/java/seedu/duke/MoneyTest.java b/src/test/java/seedu/duke/MoneyTest.java new file mode 100644 index 0000000000..a6c4485f92 --- /dev/null +++ b/src/test/java/seedu/duke/MoneyTest.java @@ -0,0 +1,58 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; +//@@author MonkeScripts +public class MoneyTest { + @Test + void testMoney() { + Money a = new Money(10.00F, CurrencyConversions.MYR); + assert(a.getAmount() == 10.00); + assert(a.getCurrency().equals(CurrencyConversions.MYR)) ; + } + + @Test + void testAddition() { + Money sg = new Money(10.00F, CurrencyConversions.SGD); + Money malaysia = new Money(10.00F, CurrencyConversions.MYR); + //total in SGD + Money total = sg.addition(malaysia, CurrencyConversions.SGD); + System.out.println(total.getAmount()); + assert(total.getAmount() == 10.00F * CurrencyConversions.SGD.getRate() + + 10.00F * CurrencyConversions.MYR.getInverseRate()); + assert(total.getCurrency().equals(CurrencyConversions.SGD)); + assert(total.getAmount() == sg.convertToSGD().getAmount() + + malaysia.convertToSGD().getAmount()); + assert(total.getCurrency().equals(CurrencyConversions.SGD)); + //total in MYR + total = sg.addition(malaysia, CurrencyConversions.MYR); + assert(total.getAmount() == 10.00 * CurrencyConversions.MYR.getRate() + + 10.00); + assert(total.getAmount() == + sg.convertToOther(CurrencyConversions.MYR).getAmount() + malaysia.getAmount()); + assert(total.getCurrency().equals(CurrencyConversions.MYR)); + } + + @Test + void testMultiplication() { + Money jap = new Money(10000.00F, CurrencyConversions.JPY); + //multiplied by 3-fold and then converted to euro + Money multiplied = jap.multiplication(3, CurrencyConversions.EUR); + assert(multiplied.getAmount() == new Money( + 30000.00F, CurrencyConversions.JPY). + convertToOther(CurrencyConversions.EUR).getAmount()); + assert(multiplied.getCurrency().equals(CurrencyConversions.EUR)); + } + + @Test + void testAdditionAndMultiplication() { + Money sg = new Money(10.00F, CurrencyConversions.SGD); + Money malaysia = new Money(10.00F, CurrencyConversions.MYR); + //compute total = sg + 3 * malaysia, converted to euro; + Money total = sg.addition(malaysia.multiplication( + 3, CurrencyConversions.MYR), CurrencyConversions.EUR); + assert(total.getAmount() == + sg.addition(new Money(30.00F, CurrencyConversions.MYR), + CurrencyConversions.EUR).getAmount()); + } + +} diff --git a/src/test/java/seedu/duke/ParserTest.java b/src/test/java/seedu/duke/ParserTest.java new file mode 100644 index 0000000000..2b8d53c027 --- /dev/null +++ b/src/test/java/seedu/duke/ParserTest.java @@ -0,0 +1,27 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +public class ParserTest { + + public void testParser(String userInput, String command, String argument, + String[] amount, String[] paid, String[] user, String[] currency){ + Parser parserFromInput = new Parser(userInput); + System.out.println(parserFromInput); + + Parser parserFromParams = new Parser(userInput, command, argument, amount, paid, user, currency); + System.out.println(parserFromParams); + + assert parserFromInput.toString().equals(parserFromParams.toString()); + } + + @Test + public void test1(){ + testParser("command argument /amount amount /paid paid /currency SGD /user user1 /user user2", + "command", "argument", + new String[]{"amount"}, + new String[]{"paid"}, + new String[]{"user1", "user2"}, + new String[]{"SGD"}); + } +} diff --git a/src/test/java/seedu/duke/SettleTest.java b/src/test/java/seedu/duke/SettleTest.java new file mode 100644 index 0000000000..f1f7c465c8 --- /dev/null +++ b/src/test/java/seedu/duke/SettleTest.java @@ -0,0 +1,67 @@ +//@@author avrilgk + +package seedu.duke; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ExpensesException; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SettleTest { + + private Group group; + + @BeforeEach + public void setup() throws ExpensesException { + group = new Group("Test Group"); + User payer = new User("Alice"); + User payee = new User("Bob"); + group.addMember(payer.getName()); + group.addMember(payee.getName()); + ArrayList> payees = new ArrayList<>(); + Money payeeAmount = new Money(50.0f,CurrencyConversions.SGD); + payees.add(new Pair<>(payee.getName(), payeeAmount)); + Money totalAmount = new Money(100.0f,CurrencyConversions.SGD); + Expense expense = new Expense(payer.getName(), "Test Expense", totalAmount, payees); + group.addExpense(expense); + } + + @Test + public void testSettleCreation() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, 50.0F); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob 50.0", settle.toString()); + } + + @Test + public void testSettleCreationWithNegativeAmount() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, -50.0F); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob -50.0", settle.toString()); + } + + @Test + public void testSettleCreationWithZeroAmount() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, 0.0F); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob 0.0", settle.toString()); + } + + @Test + public void testSettleCreationWithZeroAmountAndNegativeAmount() { + User payer = new User("Alice"); + User payee = new User("Bob"); + Settle settle = new Settle(payer, payee, 0.0F); + assertEquals("Alice", settle.getPayer()); + assertEquals("Alice paid Bob 0.0", settle.toString()); + } +} diff --git a/src/test/java/seedu/duke/UserInterfaceTest.java b/src/test/java/seedu/duke/UserInterfaceTest.java new file mode 100644 index 0000000000..fd9f6e4889 --- /dev/null +++ b/src/test/java/seedu/duke/UserInterfaceTest.java @@ -0,0 +1,13 @@ +package seedu.duke; + +import org.junit.jupiter.api.Test; + +public class UserInterfaceTest { + @Test + public void printTest() { + UserInterface.printMessage("Success", MessageType.SUCCESS); + UserInterface.printMessage("Message"); + UserInterface.printMessage("Error", MessageType.ERROR); + + } +} diff --git a/src/test/java/seedu/duke/storage/GroupStorageTest.java b/src/test/java/seedu/duke/storage/GroupStorageTest.java new file mode 100644 index 0000000000..3a9bdd1171 --- /dev/null +++ b/src/test/java/seedu/duke/storage/GroupStorageTest.java @@ -0,0 +1,80 @@ +package seedu.duke.storage; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.GroupLoadException; +import seedu.duke.exceptions.GroupSaveException; +import seedu.duke.Group; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GroupStorageTest { + private static final String TEST_GROUPS_DIRECTORY = "src/test/data/groups"; + + private GroupStorage groupStorage; + + @BeforeAll + static void setUpTestDirectory() { + GroupFilePath.setGroupsDirectory(TEST_GROUPS_DIRECTORY); + } + + @BeforeEach + void setUp() throws IOException { + FileIO fileIO = new FileIOImpl(); + groupStorage = new GroupStorage(fileIO); + + // Create the test groups directory if it doesn't exist + Files.createDirectories(Path.of(TEST_GROUPS_DIRECTORY)); + } + + @Test + void saveGroupToFile_validGroup_successfullySaves() throws GroupSaveException { + Group group = createSampleGroup(); + + groupStorage.saveGroupToFile(group); + + Path filePath = Path.of(TEST_GROUPS_DIRECTORY, "sample_group.txt"); + assertTrue(Files.exists(filePath)); + } + + @Test + void loadGroupFromFile_validFile_successfullyLoads() throws IOException, GroupLoadException { + createSampleGroupFile(); + + Group loadedGroup = groupStorage.loadGroupFromFile("sample_group"); + + assertNotNull(loadedGroup); + assertEquals("sample_group", loadedGroup.getGroupName()); + assertEquals(2, loadedGroup.getMembers().size()); + } + + @Test + void loadGroupFromFile_nonExistentFile_throwsGroupLoadException() { + assertThrows(GroupLoadException.class, () -> groupStorage.loadGroupFromFile("nonexistent_group")); + } + + private Group createSampleGroup() { + Group group = Group.getOrCreateGroup("sample_group").get(); + group.addMember("user1"); + group.addMember("user2"); + return group; + } + + private void createSampleGroupFile() throws IOException { + Path filePath = Path.of(TEST_GROUPS_DIRECTORY, "sample_group.txt"); + String fileContent = "sample_group\n" + + "Members:\n" + + "user1\n" + + "user2\n" + + "Expenses:\n"; + Files.writeString(filePath, fileContent); + } +} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..001f2fa26a 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,33 @@ Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| +.------..------..------..------..------..------..------..------..------..------. +|S.--. ||P.--. ||L.--. ||I.--. ||T.--. ||L.--. ||I.--. ||A.--. ||N.--. ||G.--. | +| :/\: || :/\: || :/\: || (\/) || :/\: || :/\: || (\/) || (\/) || :(): || :/\: | +| :\/: || (__) || (__) || :\/: || (__) || (__) || :\/: || :\/: || ()() || :\/: | +| '--'S|| '--'P|| '--'L|| '--'I|| '--'T|| '--'L|| '--'I|| '--'A|| '--'N|| '--'G| +`------'`------'`------'`------'`------'`------'`------'`------'`------'`------' -What is your name? -Hello James Gosling +Start splitting your expenses now! +Welcome, here is a list of commands: +help: Access help menu. +create : Create a group. +exit : Exit current group. +member : Add a member to the group. +expense /amount /currency /paid /user /user ...: Add an expense SPLIT EQUALLY. +expense /unequal /amount /currency /paid /user /user ...: Add an expense SPLIT UNEQUALLY. +list: List all expenses in the group. +balance : Show user's balance. +settle /user : Settle the amount between two users. +luck : luck is in the air tonight +That is not a command. Please use one of the commands given here +Welcome, here is a list of commands: +help: Access help menu. +create : Create a group. +exit : Exit current group. +member : Add a member to the group. +expense /amount /currency /paid /user /user ...: Add an expense SPLIT EQUALLY. +expense /unequal /amount /currency /paid /user /user ...: Add an expense SPLIT UNEQUALLY. +list: List all expenses in the group. +balance : Show user's balance. +settle /user : Settle the amount between two users. +luck : luck is in the air tonight +Goodbye!