diff --git a/.github/ISSUE_TEMPLATE/bug-report--customised-template-.md b/.github/ISSUE_TEMPLATE/bug-report--customised-template-.md new file mode 100644 index 0000000000..6b970d63ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report--customised-template-.md @@ -0,0 +1,26 @@ +--- +name: Bug report (Customised Template) +about: Template to provide bug reports +title: '' +labels: type.Bug +assignees: '' + +--- + +**Bug Description** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Actual Behaviour** +Provide any output of what actually happened, and the stack trace if possible. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug-report--github-template-.md b/.github/ISSUE_TEMPLATE/bug-report--github-template-.md new file mode 100644 index 0000000000..4bb878712a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report--github-template-.md @@ -0,0 +1,38 @@ +--- +name: Bug report (Github Template) +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/user-stories.md b/.github/ISSUE_TEMPLATE/user-stories.md new file mode 100644 index 0000000000..c13e29d6de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/user-stories.md @@ -0,0 +1,10 @@ +--- +name: User Stories +about: Blank template (with some tags set) for adding new user stories +title: '' +labels: type.Story +assignees: '' + +--- + + diff --git a/.gitignore b/.gitignore index f69985ef1f..adc4acc9ed 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.txt text-ui-test/EXPECTED-UNIX.TXT +src/main/java/seedu/duke/Mytest.java +trips.json diff --git a/README.md b/README.md index 4eef34421f..1e0b7f8143 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,5 @@ Steps for publishing documentation to the public: 1. Scroll down to the `GitHub Pages` section. 1. Set the `source` as `master branch /docs folder`. 1. Optionally, use the `choose a theme` button to choose a theme for your documentation. + +*temp change* diff --git a/build.gradle b/build.gradle index b0c5528fb5..d856adeb79 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ repositories { dependencies { testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.0' + implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' } test { @@ -41,6 +42,7 @@ checkstyle { toolVersion = '8.23' } -run{ +run { standardInput = System.in + enableAssertions = true } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..cc53169f65 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -2,8 +2,8 @@ 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) +![](images/leeyikai.jpeg) | Lee Yi Kai | [Github](https://github.com/leeyikai) | [Portfolio](team/leeyikai.md) +![](images/xiyuan_profile.jpg) | Li Xi Yuan | [Github](https://github.com/lixiyuan416) | [Portfolio](team/lixiyuan416.md) +![](images/joshualeeky.jpg) | Lee Keng Yong Joshua | [Github](https://github.com/joshualeeky) | [Portfolio](team/joshualeeky.md) +![](https://via.placeholder.com/100.png?text=Photo) | Lee Qi An | [Github](https://github.com/itsleeqian) | [Portfolio](team/itsleeqian.md) +![](images/yuzhao.jpeg) | Liang Yuzhao | [Github](https://github.com/yeezao) | [Portfolio](team/yeezao.md) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..c1539b1eac 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,37 +2,330 @@ ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +Third party library used: GSON under Apache License 2.0 (repo [here](https://github.com/google/gson)) -## Design & implementation +## Design -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Architecture +![](images/Architectural_Diagram.JPG) -## Product scope -### Target user profile +The ***Architecture Diagram*** given above explains the high-level design of the App. -{Describe the target user profile} +Given below is a quick overview of main components and how they interact with each other. -### Value proposition +**Main components of the architecture** -{Describe the value proposition: what problem does it solve?} +`Main` is responsible for initialising the different components correctly at app launch, and connecting them with one another. -## User Stories +`Commons` represents a collection of classes used by multiple components. +The major classes in `Commons` are `Trip`, `Expense` and `Person`. +Further elaboration on these classes will be in the following sections later. -|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| +The remaining components are as follows: -## Non-Functional Requirements +`Ui`: The User Interface of the App. -{Give non-functional requirements} +`Parser`: The command handler and executor. Holds the main logic for handling any command. -## Glossary +`Storage`: Holds the data of the App in memory, and also reads and writes data to the hard disk. -* *glossary item* - Definition +**How the architecture components interact with one another** -## Instructions for manual testing +The ***Sequence Diagram*** below shows a brief overview of how the components interact with each other. +For this particular interaction, the user has issued the command +`create` with the correct input parameters. -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +![](images/Architectural_Sequence_Diagram.JPG) + +The sections below provide more details of the components and classes in them. + + +### `Trip` Class + +The `Trip` class contains attributes storing the details of trips added by the user, +and is a container class for the expenses (each expense being represented by an +instance of the `Expense` class) and persons (each person being represented by an +instance of the `Person` class) tagged to the trip. The class diagram below illustrates +the interactions of the `Trip` class with other classes. + +![](images/classDiagTrip.png) + +A trip is created when the `Parser` class calls its `executeCreate()` method to instantiate +a new instance of `Trip`. The newly-created trip is then added to the `ArrayList` +in the Storage class. + +Although the program is able to store zero trips, in order for it to work at any appreciable level, +there must be at least one trip added by the user (either through input or through loading from the +save file) in order for any other features to be available. If there are no trips added, the program +will repeatedly prompt the user to add a new trip. + +The sequence diagram below shows what happens when the user creates a new trip. If a duplicate +trip is input, the app will confirm with the user if he/she wishes to add the duplicate trip. + +![](images/tripSeq.png) + +### `Person` Class +Below details the UML diagram for the `Person` class. + +![](images/Person_Diagram.JPG) + +The Person Class, +Represents an individual that participated in an expense or a whole trip. +* A user-defined amount of `Person` objects will be created by the user during the create function of the `Trip` Class. +* Every time an object is created of the `Expense` Class, the user may define the people who were involved in the expense, however the people who are added to the expense must be already a `Person` object in the `Trip` object that the expense was made. +* One `Person` object who was involved in the expense will then be appointed as the payer of the group, the user will then have to indicate how much (in foreign currency) each of the participating persons spent for that particular expense. This is then stored and updated in each of the respective `Person` object’s `moneyOwed` HashMap, where a positive double refers to how much the person owes the respective Person object (i.e. the key of the HashMap) and a negative double refers to how much the Person object (i.e. the key of the HashMap) owes to that instance of the Person object. + * Example: If the HashMap = {person2 = 22, person3 = -11} in the person1 object, then person1 owes person2 $22 and person3 owes person1 $11. + +### `Ui` Class + +The `Ui` class handles everything that the user sees, which includes feedback, error messages, user prompts, and displaying help. All of Ui's methods are static, and are meant to be called by other classes. + +
+ +**Methods of 'Ui' can be broadly categorised into 2 types:** +1. Error and feedback messages, which do not contain references to other classes, and are mainly `System.out.println` statements. + +2. Methods that calculates and prints the state of the program. These methods contain references + to other classes, and may require input parameters. An example will be + `public static void printAmount(Person person, Trip trip)`, which generates the total amount + spent and the repayment instructions for `person` in that `trip`. + +
+ +The `help` command is implemented by `Ui.displayHelp()`, and has 3 different states, as shown in the diagram below: + +![](images/HelpCommandStates.png) + + + +### `Parser` Class + +The `Parser` class handles all input and executes the corresponding actions based on the user input. +It consists of methods that will execute commands that is input by the user which is crucial to the functionality +of our program. + +`Parser` depends on other classes for the respective inputs and outputs.`Duke` calls the `Parser` class in order for the +necessary commands to be executed. However, not all functionality is stored here. Rather, `Parser` acts like an interface +that handles all the logic required to pass in the correct information into the different classes to execute. + +The following partial class diagram depicts the relation of the Parser class with other classes that it interacts with: + +![](images/ParserClassDiagram.png) + +The `Parser` class, +- Reads in the user input and determines if the command entered is valid. +- Parser class will then pass it to the abstract class CommandHandler which will then pass it to CommandExecutor, any +exceptions thrown here will be caught by the CommandHandler. +- The CommandExecutor will then execute the commands provided with the relevant method calls from the other classes in the +program, again if there is an exception thrown at this stage, it will be caught in the CommandHandler class. + +The following partial sequence diagram dictates the flow of events when the user enters a command into the program. + +![](images/ParserSequenceDiagram.png) + +### `Expense` Class +The `Expense` class deals with most functionalities related to adding an expense inside a trip. The following partial class diagram +shows the interactions between the `Expense` class and other classes and interfaces. + + +![](images/ExpenseClassDiagram.png) + +The `Expense` class, +- Stores amount spent +- Stores description +- Stores category +- Stores persons involved + +The sequence diagram below shows how an expense is initialised. + +![](images/ExpenseSequenceDiagram.png) + + +When `CommandExecutor` calls the `executeCreateExpense()` function, the open trip will be retrieved, and an expense will be initialized. +During the initialization fo a new `Expense`, the amount spent for the expense is set using `setAmountSpent()`, the category is set +using `setCategory` and the date of the `Expense` is being prompted using `promptDate()`. + +In `promptDate()`, the date is checked if it is valid and will only return if it is. Otherwise, the program will keep prompting the user. + +If no date is entered, `LocalDate` will return the date which the user entered the expense. +Otherwise, `LocalDate` will parse the date according to the given format. + +If there is only 1 `Person` in the expense, then `Expense` will call the corresponding methods in `ExpenseSplitter`. `CommandExecutor` will +call `addExpense()` and `setLastExpense` to add the expense to the trip and set it as the last expense added. Then, a success message is printed using +`printExpensesAddedSuccess()` of `Ui`. + + +### `Storage` Component + +The `Storage` component consists of two classes - the `Storage` class, which stores data for the current instance of the program, +and `FileStorage` class, which interacts with the save file. + +The interaction between the two classes is illustrated in the diagram below: + +![](images/StorageCompClassDiag.png) + +- The `Storage` class stores the user data after it has been read from the save file. It also stores the list of supported +currencies, the current open trip (set to `null` if there is no open trip), and the trip which the user last interacted with and +(set to `null` if the trip was deleted). +- The `FileStorage` class contains methods to read from and write to a save file, and to create a new save file. + +The sequence diagram illustrating the process of reading from a save file is below: + +![](images/ReadFromFileSeqDiag.png) + +The sequence diagram illustrating the process of writing to a save file is below: + +![](images/WriteToFileSeqDiag.png) + + +#### `FileStorage` implementation + +The Gson library we use to serialise and deserialise data to and from the JSON format does not properly parse LocalDate +objects, given that LocalDate cannot be directly instantiated. As a result, using the default implementation of Gson +to serialise LocalDate causes an `InaccessibleObjectException` when attempting to deserialise a LocalDate object. To overcome +this, we implemented a custom serialiser and deserialiser specifically for LocalDate, adapted from the Gson User Guide +[here](https://github.com/google/gson/blob/master/UserGuide.md#TOC-Custom-Serialization-and-Deserialization). + +The custom serialiser and deserialiser is implemented as inner classes within the `FileStorage` class. + +## Appendix: Requirements + +### Appendix A: Product scope + +**Target user profile** +- Students who are travelling overseas and sharing expenses with a group +- Comfortable with CLI desktop apps +- Prefers typing to mouse interaction for data input + +**Value proposition** +- Suitable for batch input of expenses +- PayMeBack data is in JSON, which is lightweight and easily transferable + +### Appendix B: User Stories + +| As a ... | I want to ... | So that I can ...| +|----------|---------------|------------------| +New user|See help instructions|refer to them when I forget how to use the application +User|See my nett expenses|I can manage my budget +User|Enter my name|I can keep track of whose expenses it is +User|Enter names of other people|I can track who I went where with +User|Enter the category of my expenses|I can see how much I spent in certain areas +User|Enter the location of my expenses|I can see where I spent my money +User|Edit the location|I can change the location later on if i need to be more specific +User|Enter the exchange rate of the currency of country I am visiting|I can repay people back in my local currency correctly +User|Filter the expenses based on categories|I can better categorise my own spending +User|Filter the expenses based on who is involved|I can better settle expenses with individual person(s) +User|Start a new trip and save the previous one|I can record all my travels +User|Delete wrong entries|In case I added something wrongly +User|Delete whole trips|In case I don’t want to remember the trips +User|See history of my expenses, classified into trips|I can have a record to refer to +User|Display total amount spent in local and foreign currency|For accounting purposes +User|Know how much my friends has to pay me at the end of the trip|I will not go broke +User|Cancel an operation instead of re-entering my data when prompted|save time and run other commands immediately + + + +### Appendix C: Non-Functional Requirements + +- The app should work on all mainstream OSes (Windows, macOS, Linux) with Java 11 or later installed. +- The app should be able to store 1000 trips, each with 1000 expenses, without any noticeable slowdown in performance. +- A user with an above-average typing speed in English should be able to complete tasks on the app quicker than using a +GUI. +- The app should be able to handle corrupted save files. +- An advanced user should be able to manipulate the save file directly, and the app must be able to handle this scenario +without error. + + +### Appendix D: Instructions for manual testing + +#### Setting up + +1. Ensure that you have Java 11 or above installed. +2. Download the latest version of `PayMeBack` from [here](https://github.com/AY2122S1-CS2113T-T12-2/tp/releases), + and move the downloaded file to your preferred folder. +3. Open any command-line application (such as Terminal, Command Prompt, or Powershell) and navigate to the folder + containing your downloaded copy of `PayMeBack`. +4. In the command-line interface, type `java -jar PayMeBack.jar`. +5. If the program starts successfully, you should see the following on your screen: + + +``` +Welcome to + ____ __ ___ ____ __ + / __ \____ ___ __/ |/ ___ / __ )____ ______/ /__ + / /_/ / __ `/ / / / /|_/ / _ \/ __ / __ `/ ___/ //_/ + / ____/ /_/ / /_/ / / / / __/ /_/ / /_/ / /__/ ,< +/_/ \__,_/\__, /_/ /_/\___/_____/\__,_/\___/_/|_| + /____/ + +``` + + +#### Manual Testing + +The following is a non-exhaustive list of common commands that may be used to operate the program, and +the expected behaviour for each. For more information on commands, please refer to the [User Guide](https://ay2122s1-cs2113t-t12-2.github.io/tp/UserGuide.html). + +#### Creating Trip + +- `create` syntax with missing attribute (e.g. missing date `create /USA /USD /0.74 /ben, jerry`) +
+Expected: an error message shows, providing the input syntax for creating a trip. + + +- `create` syntax with incorrect type/format (e.g. incorrect date format `create /USA /hello /USD /0.74 /ben, jerry`) +
+ Expected: + - For some errors, the program will prompt the user to correct the input for that attribute on the spot. + - For other errors, the program will display an error message, and the user will need to re-enter the full command again. + +- `create` with a new trip with the same attributes as an already existing trip. +
+Expected: the program will warn the user, and asks for confirmation from the user on whether to add the duplicate trip. + +#### Opening and deleting trip + +- `open` or `delete` with an incorrect trip number (wrong format/trip number doesn't exist +e.g. `open something` or `delete 1000` when only 2 trips are stored.) +
+Expected: An error will inform the user that the trip number does not exist. + +#### Editing trip + +- `edit` syntax with incorrect or missing attribute (e.g. edit 1 -location). +
+Expected: An error will inform the user, providing the input syntax for editing a trip. + +#### Viewing summary of expenses + +- `summary` with a random string or a name that does not exist in the trip +e.g. 'summary abcdefg' or 'summary %32!3'. +
+Expected: An error will inform the user that the string or name does not exist in the trip. + +#### Creating expenses +- Create an expense inside a trip. When prompted for date, enter an input like `testing123`, or enter an invalid date like `31-02-2021` +
+ Expected: Program will prompt you to re-enter date in DD-MM-YYYY format. + +- Create an Expense with the people with identical names (case-insensitive). e.g. `expense 2113 food duke, dUkE /nice dinner`. +
+ Expected: An error will inform the user that they have entered people with the same name. + +- Create an Expense with invalid parameters (negative, zero or non-number). e.g. `expense -2113 category Duke, Duke2 /description` or + `expense 0 category Duke, Duke2 /description` or `expense notNumber cateogry Duke, Duke2 /description`. +
+ Expected: An error will inform the user that the amount is invalid and request the user to enter the amount again. + +#### Viewing and deleting expenses + +- `view` and `delete` with an invalid expense number (wrong format/trip number doesn't exist). + e.g. `view something` or `delete 1000` when only 2 expenses are added. +
+ Expected: An error will inform the user that the expense number does not exist. + +- `view last` and `delete last` when you have already deleted your most recent expense. +e.g. `delete last` followed by `delete last` or `view last`. +
+Expected: An error will inform the user that they have no recently added an expense. diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..b192da1510 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,10 @@ -# Duke +# PayMeBack + +PayMeBack is a CLI-based expense tracker for a group of friends travelling overseas together. + +In such a group, it is easier for an individual to pay for expenses on behalf of the group rather than having each individual pay for their share for every activity. At the end of the day, it is quite troublesome to manually calculate how much money each individual owes to another. PayMeBack is designed to make this process easy and fuss-free, by helping you calculate how much each person owes every other person. + -{Give product intro here} Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index abd9fbe891..75e5c67bd6 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,41 +2,794 @@ ## Introduction -{Give a product intro} +PayMeBack is a CLI-based expense tracker for a group of friends travelling overseas together. -## Quick Start +In such a group, it is easier for an individual to pay for expenses on behalf of the group rather than having each +individual pay for their share for every activity. At the end of the day, it is quite troublesome to +manually calculate how much money each individual owes to another. PayMeBack is designed to make this process +easy and fuss-free, by helping you calculate how much each person owes every other person. + +
+ +## Table of Contents +* [Using this guide](#using-this-guide) +* [Quick Start](#quick-start) +* [Features](#features) + * [Saving your data](#saving-your-data) + * [Loading your saved data](#loading-your-saved-data) + * [Trips](#trips) + * [Create Trip](#--create-trip) + * [Open Trip](#--open-trip) + * [Close Trip](#--close-trip) + * [List Trips](#--list-trips) + * [View People in Trip](#--view-people-in-trip) + * [Delete Trip](#--delete-trip) + * [Edit Trip](#--edit-trip) + * [Expenses](#expenses) + * [Create Expense](#--create-expense) + * [List Expenses](#--list-expenses) + * [View an Expense](#--view-an-expense) + * [Filter Expenses by Attribute](#--filter-expenses-by-attribute) + * [Delete Expense](#--delete-expense) + * [Settling Expenses](#settling-expenses) + * [Amount](#amount) + * [Optimize Transactions](#optimize-transactions) + * [Summary of Expenses](#summary-of-expenses) +* [FAQ](#faq) +* [Command Summary](#command-summary) + +
+ +## Using this guide + +- Text bounded by `this formatting` refers to elements displayed in your window as the program is running. +It can either be an input entered by the user, or an output displayed by the program. +- All entries enclosed in square brackets, "[" and "]" refer to user-defined inputs. +For example, input values displayed as `[something]` can be determined by the user, where `something` is the data +entered by the user. -{Give steps to get started quickly} +
+ +## Quick Start 1. Ensure that you have Java 11 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `PayMeBack` from [here](https://github.com/AY2122S1-CS2113T-T12-2/tp/releases), +and move the downloaded file to your preferred folder. +3. Open any command-line application (such as Terminal, Command Prompt, or Powershell) and navigate to the folder + containing your downloaded copy of `PayMeBack`. +4. In the command-line interface, type `java -jar PayMeBack.jar`. +5. If the program starts successfully, you should see the following on your screen: + + +``` +Welcome to + ____ __ ___ ____ __ + / __ \____ ___ __/ |/ ___ / __ )____ ______/ /__ + / /_/ / __ `/ / / / /|_/ / _ \/ __ / __ `/ ___/ //_/ + / ____/ /_/ / /_/ / / / / __/ /_/ / /_/ / /__/ ,< +/_/ \__,_/\__, /_/ /_/\___/_____/\__,_/\___/_/|_| + /____/ + +``` +
+ +
+ +## Features + +### Saving your data + +By default, the program attempts to save your data to a file named `trips.json` located in the same directory +as the app each time after you run a command. This includes the `quit` command. + +If the save attempt is unsuccessful, you will see an error message. The program will then attempt to save again the next time you run a command. + +
+ +### Loading your saved data + +The first time you open the program, or if the save file was deleted or moved, the program will create a save file +named `trips.json` for you. + +The next time you open the program, it will attempt to load your data from the save file. If the data is successfully +loaded, you will see the following message: +``` +Your saved data was successfully loaded! +``` + +If there is an error loading your save file, please go to the [FAQ](#faq) for more information. + +
+ +### Trips + +#### - Create Trip + +Creates a new Trip in the program. This command will only work when there is no trip open. + +Input syntax: +``` +create /[location] /[date] /[foreign-currency] /[exchange-rate] /[persons-in-trip] +``` + +All fields are compulsory. Note the following: +- `[location]` is the location of the trip. Any string can be entered. +- `[date]` must follow the format of dd-mm-yyyy. +- `[foriegn-currency]` is the 3-digit ISO code of the foreign currency (e.g USD, GBP). Currently there are 30 currencies supported, the currencies' names and ISO codes are listed below. + - Some currencies will not have symbols as some terminals may not be able to support displaying of certain symbols. + - Supported currencies will be rounded to either 2 decimal places or to the nearest whole number, + depending on the currency's smallest denomination. + - The program is still runnable with unknown currencies, however there will be no symbol and the decimal place may not be accurate. +- `[exchange-rate]` should be how much 1 of your home currency costs in foreign currency. + - Example: SGD $1 is equivalent to USD $0.74, hence the `exchange-rate` will be 0.74. + - Note that the default home currency is SGD. To change the home currency, please refer to [Edit Trip](#--edit-trip). +- `persons-in-trip` should be separated with commas. + +#### Compatible Currencies + +Currency Name | ISO Code | Is symbol
available? +--- | :---: | :---: | +United States Dollar | USD | Yes +Singapore Dollar | SGD | Yes +Australian Dollar | AUD | Yes +Canadian Dollar | CAD | Yes +New Zealand Dollar | NZD | Yes +Euro | EUR | No +Pound Sterling| GBP | No +Malaysian Ringgit | MYR | Yes +Hong Kong Dollar | HKD | Yes +Thai Baht | THB | No +Russian Ruble | RUB | No +South African Rand | ZAR | Yes +Turkish Lira | TRY | No +Brazilian Real | BRL | Yes +Danish Krone | DKK | Yes +Polish Zloty | PLN | No +Israeli New Shekel | ILS | No +Saudi Riyal | SAR | Yes +Chinese Yuan | CNY | No +Japanese Yen | JPY | No +South Korean Won | KRW | No +Indonesian Rupiah | IDR | Yes +Indian Rupee | INR | Yes +Swiss Franc | CHF | Yes +Swedish Krona | SEK | Yes +Norwegian Krone | NOK | Yes +Mexican Peso | MXN | Yes +New Taiwan Dollar | TWD | Yes +Hungarian Forint | HUF | Yes +Czech Koruna | CZK | Yes +Chilean Peso | CLP | Yes +Philippine Peso | PHP | No +United Arab Emirates Dirham | AED | No +Colombian Peso | COP | Yes +Romanian Leu | RON | Yes + +For example, + +Input: + +``` +create /United States of America /02-02-2021 /USD /0.74 /Ben, Jerry, Tom +``` + +If successful, the output will be as follows: + +``` +Your trip to United States of America on 02 Feb 2021 has been successfully added! +``` + +
+ +#### - Open Trip + +Opens a Trip, which allows the user to interact with other functionalities of our app, such as adding and viewing expenses. + +Input syntax: +``` +open [trip-number] +``` + +For example, + +Input: + +```` +open 1 +```` +If successful, the output will be as follows: +``` +You have opened the following trip: +United States of America | 02 Feb 2021 +``` + +You can also run the `open` command while a Trip is already open. This will close the currently-opened +Trip, and open the specified Trip in the most recent command. + +*Note: Only one Trip can be open at any time.* + +
+ +#### - Close Trip + +Closes the current Trip you are in. This allows you to interact with trips (e.g. add a new trip, delete a trip, or list all trips) + +This command can only be used if a Trip is already open. + +Input syntax: +``` +close +``` +For example, + +Input: +```` +close +```` +If successful, the output will be as follows: +``` +You have closed the following trip: +America | 02 Feb 2021 +``` + +
+ +#### - List Trips + +Lists all the Trips that you have created along with their index numbers. + +This command can only be used if no Trip is open. If a trip is open, this command will list down your expenses instead. See [List Expenses](#--list-expenses) +for more information on listing expenses. + +Input syntax: +``` +list +``` +For example, + +Input: +```` +list +```` +If successful, the output will be as follows: +``` +List of Trips: + 1. London | 27 Oct 2021 +``` + +
+ +#### - View People in Trip + +Lists the persons involved in a particular Trip. + +This command can only be used if you have a Trip opened. + +Input syntax: +``` +people +``` + +
+ +For example, + +Input: +```` +people +```` + + +If successful, the output will be as follows: +``` +These are the people involved in this trip: + Ben + Jerry + Tom +``` + +
+ +#### - Delete Trip + +Deletes a Trip from the program. + +This command can only be used when no trip is open. If a trip is open, this command will delete an expense instead. See [Delete Expense](#--delete-expense) +for more information on deleting expenses. + +Input syntax: +``` +delete [trip-number] +``` +- `[trip-number]` is the index of the Trip you wish to delete, which can be found by using `list` command while no Trip is open. +- `delete last` to delete the trip you last interacted with. + + +For example, + +Input: + +```` +delete 1 +```` +If successful, the output will be as follows: +``` +Your trip to America on 02 Feb 2021 has been successfully removed. +``` +
+ +
+ +#### - Edit Trip + +Edit the attributes of a Trip. This command can only be used when no trip is open. + +Input syntax: +``` +edit [trip-num] -[attribute] [new-value] +``` +All fields are compulsory. Note the following: +- The following are the attributes that can be edited along with their corresponding syntax: + - Location: `-location` + - Date: `-date` + - Exchange Rate: `-exchangerate` + - Foreign Currency ISO Code: `-forcur` + - Home Currency ISO Code: `-homecur` +- The hyphen preceding `[attribute]` is part of the syntax. +- `[trip-number]` is the index of the Trip you wish to edit, which can be found by using `list` command while no Trip is open. +- `last` can be used for `[trip num]`. This will modify the last trip you interacted with. + + +For example, + +Input: + +```` +edit 2 -location japan +```` +If successful, the output will be as follows: +``` +The location of your trip has been changed from tokyo to japan. +``` +
+ +### Expenses +#### - Create Expense +Creates a new expense entry for the currently opened Trip. + +This command can only be used if you have a Trip opened. + +
+ +Input syntax: +```` +expense [amount] [category] [people] /[description] +```` +All fields are compulsory. Note the following: +- `[amount]` is the total amount spent on the expense. + +- `[category]` is a category tag for the expense. Only 1 category per Expense is allowed. + +- `[people]` denotes the people involved in the expense. Multiple people involved are to be separated by commas. + - Entering `-all` will add every person in the trip to the expense + +- `[description]` is the description of the expense. + +For example, + +Input: +```` +expense 30 food Ben, Jerry /In-and-Out Burgers +```` + +PayMeBack will then ask you to enter the date of the expense: +```` +Enter date of expense: + Press enter to use today's date +```` +- The date must follow the format of dd-mm-yyyy. +- Pressing the enter key without keying in anything will use the current date. +- If the expense only has one person involved, the steps below will be skipped and the expense will be automatically added. + +PayMeBack will then ask for the name of the person who paid for the expense: +```` +Who paid for the expense?: +```` +- The name entered has to be part of the expense or PayMeBack will request for the name again. -## Features +
-{Give detailed description of each feature} +PayMeBack will then ask for the amounts spent by each person involved along +with how much of the amount has yet to be assigned: +```` +Who paid for the expense?: Ben +Enter "equal" if expense is to be evenly split, enter individual spending otherwise +There is USD $30.00 left to be assigned. How much did Tom spend?: +```` +- The program will automatically cycle through every person involved in the expense. +- Entering `equal` when the program asks for the amount spent for the first person will cause the program to evenly split the expense among all the people involved in it. + - Note: If the amount is not perfectly divisible by the number of people, the payer will bear the surplus or deficit. -### Adding a todo: `todo` -Adds a new item to the list of todo items. +If there is no amount remaining but there are still people left to be assigned, PayMeBack will prompt the user if they would like to assign 0 to the rest of the people involved in the expense: +```` +There is USD $30.00 left to be assigned. How much did Ben spend?: 30 +There will be people involved that don't need to pay, are you sure? (y/n): +```` +- Entering `y` will allow PayMeBack to assign 0 to the remaining people involved in the expense. +- Entering `n` will result in PayMeBack requesting for the name of the person who paid for the expense again, followed by the same sequence of steps. -Format: `todo n/TODO_NAME d/DEADLINE` +PayMeBack will automatically prompt the user to assign the remaining amount when it requests for the amount spent of the last person in the expense: +```` +There is USD $30.00 left to be assigned. How much did Ben spend?: 20 +Assign the remaining USD $10.00 to Jerry? (y/n): +```` +- Entering `y` will allow PayMeBack to assign the amount to the persons stated. +- Entering `n` will result in PayMeBack requesting for the name of the person who paid for the expense again, followed by the same sequence of steps. -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +If successful, the output will be as follows: +```` +Your expense has been added successfully +```` -Example of usage: +
-`todo n/Write the rest of the User Guide d/next week` +
-`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +#### - List Expenses + +List all expenses in the current opened trip. + +This command can only be used if a trip is open. If no trip is currently open, this command will list trips instead. See [List Trips](#--list-trips) +for more information on listing trips. + +Input syntax: +```` +list +```` + +For example, + +Input: +```` +list +```` + +If successful, the output will be as follows: +```` +List of Expenses: + 1. In-and-Out Burgers | 03 Feb 2021 + 2. Gift shopping at mall | 03 Feb 2021 + 3. Dinner at Taco Bell | 03 Feb 2021 +```` + +
+ +#### - View an Expense +Shows the details of a particular expense of a trip. + +This command can only be used if a trip is open, and there is at least 1 expense. + +Input syntax: +```` +view [expense-number] +```` +- Note that entering `view` without an index will print all expenses in the currently opened trip. +- Enter `view last` to view the last added expense. + - Note: If the last added expense is deleted, you will not be able to use this command. + +
+ +For example, + +Input: +```` +view 1 +```` + +If successful, the output will be as follows: +```` + In-and-Out Burgers + Date: 03 Feb 2021 + Amount Spent: USD $30.00 + People involved: + 1) Ben, USD $20.00 + 2) Jerry, USD $10.00 + Payer: Ben + Category: food +```` + + +
+ +#### - Filter Expenses By Attribute +Allows the user to view specific expenses and index numbers based on an attribute of their choice. + +This command can only be used if a trip is open, and there is at least 1 expense. + +Input syntax: +```` +view filter [expense-attribute] [search-keyword] +```` +- `[expense-attribute]` can be either `[category]`, `[description]`, `[payer]`, `[person]` or `[date]`. + +For example, if the user would like to search for all expenses in the category "food", + +Input: +```` +Enter your command: view filter category food +```` + +
+ +If successful, the output will be as follows: +```` +1. In-and-Out Burgers + Date: 03 Feb 2021 + Amount Spent: USD $30.00 + People involved: + 1) Ben, USD $20.00 + 2) Jerry, USD $10.00 + Payer: Ben + Category: food + +3. Dinner at Taco Bell + Date: 03 Feb 2021 + Amount Spent: USD $20.00 + People involved: + 1) Ben, USD $8.00 + 2) Jerry, USD $6.00 + 3) Tom, USD $6.00 + Payer: Tom + Category: food +```` + + +
+ +#### - Delete Expense + +Deletes an expense from a trip. + +This command can only be used if a trip is open, and there is at least 1 expense. If no trip is open, this command will delete trips instead. See [Delete Trip](#--delete-trip) +for more information on deleting trips. + +Input syntax: +``` +delete [expense-number] +``` +- `[expense-number]` is the index of the expense you wish to delete, +and can be found by using `list` while a trip is open. +- `delete last` to delete the last added expense. + - Note: After deletion, you will not be able to use this command again until you add another expense. + +
+ +For example, + +Input: +```` +delete 1 +```` +If successful, the output will be as follows: +``` +Your expense of SGD $50.00 has been successfully removed. +``` +
+ +### Settling Expenses +There are 2 commands that you can run to get a list of who pays who (WPW) to +settle expenses. `amount` displays the WPW for 1 person, while `optimize` displays the WPW for everyone in +the trip. + +
+ +#### Amount +Shows the transactions that a person will have to engage in so that the person will not owe or be owed any money. +User needs to have a trip opened and have expenses added to use this command. Note that this list is not optimized, +if you would like to settle all payments, please use `optimize` instead. + +Input syntax: +``` +amount [person-in-trip] +``` + +For example, + +Input: + +``` +amount Ben +``` + +If successful, the output will be as follows: + +``` +Ben spent USD $350.50 (SGD $473.65) on the trip so far +Ben owes USD $30.00 (SGD $40.54) to Jerry +Ben does not owe anything to Dick +Ben owes USD $7.00 (SGD $9.46) to Eve +``` +
+ +#### Optimize Transactions + +Shows the most optimized number of transactions to ensure that everyone is being paid back. User needs to have opened a trip and have expenses to use the command. + +Input syntax: + +``` +optimize +``` + +For example, + +Input: + +``` +optimize +``` + +If successful, the output will be as follows: + +``` +Here is the optimized payment transactions: +yuzhao needs to pay USD $8.00 (SGD $8.00) to yikai +yuzhao needs to pay USD $13.00 (SGD $13.00) to qian +``` + +If no transactions are required, the user will see this message: + +Output: +``` +All are paid! :) +``` +
+ +### Summary of Expenses +Shows an overall summary of individual expenses in a trip. + +This command can only be used if you have opened a trip. + +The input syntax is as follows: +```` +summary [name] +```` +- `[name]` is an optional argument. +- Note that entering `summary` without a name will print the summary of everybody in the opened trip. + +
+ +For example, + +Input: + +``` +summary +``` + +If successful, the output will be as follows: +``` +Ben has spent USD $50.00 (SGD $67.57) on 3 expenses on the following categories: +food: USD $30.00 (SGD $40.54) +travel: USD $20.00 (SGD $27.03) + +Jerry has spent USD $150.00 (SGD $202.71) on 6 expenses on the following categories: +food: USD $30.00 (SGD $40.54) +travel: USD $20.00 (SGD $27.03) +leisure: USD $100.00 (SGD $135.14) + +Tom has spent USD $75.00 (SGD $101.35) on 3 expenses on the following categories: +food: USD $25.00 (SGD $33.78) +travel: USD $20.00 (SGD $27.03) +shopping: USD $30.00 (SGD $40.54) +``` + +### Help + +Shows a quick help message, depending on which stage the user is at. + +Input syntax: + +``` +help +``` + +For example, + +Input: + +``` +help +``` + +Output for if no trip is open: +```` +Type "open [trip number]" to open your trip +While a trip is open, type "expense" to create an expense for that trip +Type "quit" to exit +```` + +
+ +Output for if trip is open: +```` +You are inside a trip. Trip specific commands: + +```` ## FAQ -**Q**: How do I transfer my data to another computer? +**Q**: How do I transfer my data to another computer? + +**A**: To transfer your data to another device, simply copy over the "trips.json" file in the same directory as this +app to the device you wish to use. Ensure that the save file is stored in the same directory as the PayMeBack +app on your destination device before starting the program. + +**IMPORTANT!** Before transferring data to the destination device, please ensure that the app is not running on the destination device. +If the app is running when the save file is pasted, you may accidentally overwrite the data in the save file when you run a command. + +**Q**: There was an error loading my save file. What should I do? -**A**: {your answer here} +**A**: If you see an error message when starting up the app, it is likely that your save file is corrupted. If you have modified the save file directly using another +application, you should try to undo those changes. Also ensure that your save file is in the same directory as +the jar file. + +If the file cannot be recovered, we recommend that you run the app again. When prompted to overwrite the +corrupted save file, enter `y`. You may then proceed to re-enter your data. + +**Q**: I made a mistake while entering a command. How can I exit the process and start again? + +**A**: If the app is asking you to correct an erroneous input, but you wish to cancel the process, +type `-cancel`. This will return you to the preceding state of the program, allowing you to enter +any command. + +
## Command Summary +Hyphen before square brackets (eg `summary -[name]`) denotes optional arguments + +### General commands + +Action | Command syntax +---|--- +Display help|`help` +Quit|`quit` +Cancel operation ([see FAQ](#faq)) | `-cancel` + +
+ +### Trip commands + +Action | Command syntax | Example +---|---|--- +Create trip | `create /[location] /[date] /[foreign-currency-ISO-code] /[exchange-rate] /[persons-in-trip]`|`create /America /02-02-2021 /USD /0.74 /Ben, Jerry, Tom` +Open trip | `open [trip-number]` | `open 1` +Close trip | `close` | `close` +List trips | `list` when no trip is opened| `list` +List persons involved in a trip | `people` | `people` +Delete trip | `delete [trip-number]`|`delete 1` +Edit trip | `edit [trip num] [attribute] [new value]`

attributes: -location, -date, -exchangerate, -forcur, -homecur | `edit 1 -location Afghanistan` + +
+ +
+ +### Expense commands +Open a trip to use + +Action | Command syntax | Example +---|---|--- +Create expense|`expense [amount] [category] [people] /[description]`|`expense 30 food Ben, Jerry /In-and-Out Burgers` +List expenses |`list` when inside a trip| `list` +View an expense in detail |`view [expense-number]`| `view 1` +View filtered expenses in detail | `view filter [expense-attribute] [search-keyword]`

`expense-attribute: category, payer, person, description`|`view filter category food` +Delete expense|`delete [expense-number]`|`delete 1` +View list of expense| `summary -[name]`
If name is not provided, displays all expenses like `list`| `summary` or `summary Ben` +View expense settlement actions of a person| `amount [person-in-trip]` | `amount Ben` +View optimized settlement actions for everyone in the trip|`optimize`|`optimize` + + + + -{Give a 'cheat sheet' of commands here} -* Add todo `todo n/TODO_NAME d/DEADLINE` diff --git a/docs/diagrams/ArchitecturalDiagram.puml b/docs/diagrams/ArchitecturalDiagram.puml new file mode 100644 index 0000000000..4c7b6eb042 --- /dev/null +++ b/docs/diagrams/ArchitecturalDiagram.puml @@ -0,0 +1,31 @@ +@startuml +!include style.puml +!include +!include +!include + + +Package " "<>{ + Class Ui COLOR_DARK_GREEN + Class Parser LOGIC_COLOR + Class Storage STORAGE_COLOR + Class Main #grey + Class Commons LOGIC_COLOR_T2 +} + +Class "<$user>" as User COLOR_BROWN + +Class "<$documents>" as File UI_COLOR_T2 + + +User ..> Ui +Ui -[#green]> Parser +Parser -[#blue]-> Storage +Main -[#grey]-> Ui +Main -[#grey]-> Parser +Main -[#grey]-> Storage +Main -down[hidden]-> Commons + +Storage .right[STORAGE_COLOR].>File + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ArchitecturalSequenceDiagram.puml b/docs/diagrams/ArchitecturalSequenceDiagram.puml new file mode 100644 index 0000000000..812a2dbcf2 --- /dev/null +++ b/docs/diagrams/ArchitecturalSequenceDiagram.puml @@ -0,0 +1,40 @@ +@startuml +!include style.puml +!include +'https://plantuml.com/sequence-diagram + +autonumber +Actor User as user USER_COLOR +Participant "Ui" as ui UI_COLOR +Participant "Parser" as parser LOGIC_COLOR +Participant "Storage" as storage STORAGE_COLOR + +user -[USER_COLOR]> ui : readUserInput("create ...") +activate ui UI_COLOR + +ui -[UI_COLOR]> parser : parseUserInput("create...") +activate parser LOGIC_COLOR + +parser -[LOGIC_COLOR_T2]> parser : handleValidCommand() +activate parser LOGIC_COLOR_T1 + +parser -[LOGIC_COLOR]> storage : addNewTrip() +activate storage STORAGE_COLOR + +storage -[STORAGE_COLOR]> storage : Save to file +activate storage STORAGE_COLOR_T1 +storage --[STORAGE_COLOR]> storage +deactivate storage + +storage --[STORAGE_COLOR]> parser +deactivate storage + +parser --[LOGIC_COLOR_T2]> parser +deactivate parser + +parser --[LOGIC_COLOR]> ui +deactivate parser + +ui--[UI_COLOR]> user +deactivate ui +@enduml \ No newline at end of file diff --git a/docs/diagrams/ExpenseClassDiagram.puml b/docs/diagrams/ExpenseClassDiagram.puml new file mode 100644 index 0000000000..2979110760 --- /dev/null +++ b/docs/diagrams/ExpenseClassDiagram.puml @@ -0,0 +1,46 @@ +@startuml + +hide circle +skinparam classAttributeIconSize 0 + +class "Expense"{ + - amountSpent : double + - description : String + - personsList : ArrayList + - category : String + - date : LocalDate + - payer : Person + - amountSplit : HashMap + - {static} inputPattern : DateTimeFormatter + - {static} outputPattern : DateTimeFormatter + + + Expense(in : String) + + promptDate() : LocalDate + + toString() : String + + getPersonExpense() : String + + setAmountSpent(in : String) + - checkValidPersons(in : String) : ArrayList + - isDateValid(in : String) : Boolean +} + + +interface "<> \n ExpenseSummarizer" { + + {static} getIndividualExpenseSummary(in : Person) + - {static} roundToLocal(amount : double, currTrip : Trip, categories : HashMap) +} + +class "Person" { + - name : String + - moneyOwed : HashMap + - optimizedMoneyOwed : HashMap + + + setMoneyOwed(inPerson : Person, amount : double) + + getMoneyOwed() : HashMap + + setOptimizedMoneyOwed(inPerson : Person) +} + + +"Expense" ..|> "<> \n ExpenseSummarizer" +"Expense" o-- "Person" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ExpenseSequenceDiagram.puml b/docs/diagrams/ExpenseSequenceDiagram.puml new file mode 100644 index 0000000000..fc981ea690 --- /dev/null +++ b/docs/diagrams/ExpenseSequenceDiagram.puml @@ -0,0 +1,66 @@ +@startuml +'https://plantuml.com/sequence-diagram + +participant "{abstract}\n:CommandExecutor" as CommandExecutor +participant "\n:Trip" as Trip +participant "\n:Storage" as Storage +participant "\n:Expense" as Expense +participant "\n:Ui" as Ui +participant "\n:LocalDate" as LocalDate +participant "{abstract}\n:ExpenseSplitter" as ExpenseSplitter + + +activate Ui +activate Trip +activate CommandExecutor +activate Storage +CommandExecutor -> Trip : executeCreateExpense() +Trip -> Storage +Storage -> Storage : checkOpenTrip() +Storage --> Trip +Trip --> CommandExecutor +CommandExecutor -> Expense : Expense() +Activate Expense +Expense -> Expense : setAmountSpent +activate Expense +Expense --> Expense +deactivate Expense +Expense -> Expense : setCategory +activate Expense +Expense --> Expense +deactivate Expense +Expense -> Expense : promptDate +activate Expense +loop Date not valid +Expense -> Ui : receiveUserInput() +Ui --> Expense +end +alt input date is empty +activate LocalDate +Expense -> LocalDate : now() +LocalDate --> Expense +else input date not empty +Expense -> LocalDate : parse() +LocalDate --> Expense +end +deactivate LocalDate + +activate ExpenseSplitter +alt only 1 person involved +Expense -> ExpenseSplitter : updateOnePersonSpending() +ExpenseSplitter --> Expense +else more than 1 person involved +Expense -> ExpenseSplitter : updateIndividualSpending() +ExpenseSplitter --> Expense +end +deactivate ExpenseSplitter +Expense --> CommandExecutor + +CommandExecutor -> Trip : addExpense() +CommandExecutor -> Trip : setLastExpense() +CommandExecutor -> Ui : printExpenseAddedSuccess() + + + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ParserClassDiagram.puml b/docs/diagrams/ParserClassDiagram.puml new file mode 100644 index 0000000000..5496329d54 --- /dev/null +++ b/docs/diagrams/ParserClassDiagram.puml @@ -0,0 +1,49 @@ +@startuml +'https://plantuml.com/class-diagram + +hide circle +class Parser +class Expense +interface "<> \n ExpenseSummary" as ExpenseSummary +interface "<> \n PaymentOptimizer" as PaymentOptimizer +class Trip +interface "<> \n FilterFinder" as FilterFinder + + +abstract "{abstract} \n CommandHandler" as CommandHandler { +handleCreateTrip(String) +handleEditTrip(String) +handleOpenTrip(String) +handleTripSummary(String) +handleViewTrip(String) +handleDelete(String) +handleList() +handleCreateExpense(String) +handleAmount(String) +handlePeople() +handleOptimize() +} + +abstract "{abstract} \n CommandExecutor" as CommandExecutor { +executeCreateTrip(String) +executeEditTrip(String) +executeOpen(String) +executeSummary(String) +executeView(String) +executeDelete(String) +executeList() +executeCreateExpense(String) +executeAmount(String) +executePeople() +executeOptimize() +} + +CommandHandler <|-up- Parser +CommandExecutor <|-up- CommandHandler + +CommandExecutor -[dashed]-> Expense +CommandExecutor -[dashed]-> ExpenseSummary +CommandExecutor -[dashed]-> PaymentOptimizer +CommandExecutor -[dashed]-> Trip +CommandExecutor -[dashed]-> FilterFinder +@enduml \ No newline at end of file diff --git a/docs/diagrams/ParserSequenceDiagram.puml b/docs/diagrams/ParserSequenceDiagram.puml new file mode 100644 index 0000000000..2d39d621e2 --- /dev/null +++ b/docs/diagrams/ParserSequenceDiagram.puml @@ -0,0 +1,56 @@ +@startuml +'https://plantuml.com/sequence-diagram + +actor User +participant ":Parser" as Parser +participant "{abstract}\n:CommandHandler" as CommandHandler +participant "{abstract}\n:CommandExecutor" as CommandExecutor + +'Parser check for valid input +loop userInput != Quit +User -> Parser: userInput +activate Parser +Parser -> Parser: parseUserInput +activate Parser +Parser -> Parser: checkValidInput +alt ValidInput +activate Parser +Parser -> CommandHandler: handleCommand +activate CommandHandler + +else userInput == Quit || Help || Close +User <-- Parser: Show relevant output + +else +User <-- Parser: Request for command again + + +deactivate Parser +deactivate Parser +deactivate Parser +end ValidInput + +CommandHandler -> CommandExecutor: executeCommand + +activate CommandExecutor +alt invalidInputParameters +CommandHandler <-- CommandExecutor: Error with input parameters +User <-- CommandHandler: Error message + +else +CommandExecutor -> : Call relevant functions + +alt invalidParameters +CommandExecutor <-- : Error with input parameters +CommandHandler <-- CommandExecutor: Error with input parameters +deactivate CommandExecutor +User <-- CommandHandler: Error message +deactivate CommandHandler + +else +User <-- : Show relevant output + +end invalidInputParameters +end invalidParameters +end loop +@enduml \ No newline at end of file diff --git a/docs/diagrams/PersonDiagram.puml b/docs/diagrams/PersonDiagram.puml new file mode 100644 index 0000000000..8ab34838c2 --- /dev/null +++ b/docs/diagrams/PersonDiagram.puml @@ -0,0 +1,28 @@ +@startuml +!include style.puml +'https://plantuml.com/component-diagram + +Class Expense COLOR_BROWN + +Class Person LOGIC_COLOR_T2 { +name:String +moneyOwed: HashMap +optimizedMoneyOwed: HashMap + +{method} + setMoneyOwed(Person, double) +{method} + setOptimizedMoneyOwed(Person) +} + +Class Trip COLOR_ORANGE + +show Person members + +Expense -right[COLOR_BROWN]-> "*"Person +Trip -left[COLOR_ORANGE]-> "*"Person + +note top + Persons in an expense + is a subset of Persons in a Trip. +end note + +@enduml \ No newline at end of file diff --git a/docs/diagrams/ReadFromFileSeqDiag.puml b/docs/diagrams/ReadFromFileSeqDiag.puml new file mode 100644 index 0000000000..c5844af77a --- /dev/null +++ b/docs/diagrams/ReadFromFileSeqDiag.puml @@ -0,0 +1,63 @@ +@startuml +'https://plantuml.com/sequence-diagram + +'autonumber + +activate Storage +-> Storage: readFromFile(String) + +Storage -> FileStorage: readFromFile(String) +activate FileStorage + +break FileNotFoundException + + FileStorage --> Storage: FileNotFoundException + Storage -> Storage: createNewFile(String) + activate Storage + Storage -> FileStorage: newBlankFile(String) + FileStorage --> Storage + Storage --> Storage + deactivate Storage + +end + +break NoSuchElementException + FileStorage --> Storage: NoSuchElementException +end + +FileStorage --> Storage + +Storage -> FileStorage: getGson() +FileStorage --> Storage + +Storage -> Storage: fromJson(String, Type) +activate Storage + +break JsonParseException + + Storage -> Storage: askOverwriteOrClose() + activate Storage + + alt overwrite + Storage -> Storage: createNewFile(String) + activate Storage + Storage -> FileStorage: newBlankFile(String) + FileStorage --> Storage + Storage --> Storage + deactivate Storage + else close + <-- Storage: System.exit(1) + end + Storage --> Storage + deactivate Storage + +end + +Storage --> Storage +deactivate Storage + + + +<-- Storage + +@enduml \ No newline at end of file diff --git a/docs/diagrams/StorageCompClassDiag.puml b/docs/diagrams/StorageCompClassDiag.puml new file mode 100644 index 0000000000..6c00f492d7 --- /dev/null +++ b/docs/diagrams/StorageCompClassDiag.puml @@ -0,0 +1,38 @@ +@startuml +'https://plantuml.com/class-diagram + +hide circle +skinparam classAttributeIconSize 0 + +class Storage +class FileStorage + +Storage --> FileStorage + +class Storage { + - {static} listOfTrips : ArrayList + - {static} openTrip : Trip + - {static} lastTrip : Trip + - {static} validCommands : ArrayList + - {static} availableCurrency : Hashmap + + + {static} writeToFile() + + {static} readFromFile() + + {static} createNewFile() + - {static} askOverwriteOrClose() + + {static} getOpenTrip() : Trip + + {static} setOpenTrip() + + {static} closeTrip() +} + +class FileStorage { + - {static} gson : Gson + + + {static} writeToFile() + + {static} readFromFile() : String + + {static} newBlankFile() + - {static} initializeFileWriter() : FileWriter + + {static} initializeGson() +} + +@enduml \ No newline at end of file diff --git a/docs/diagrams/WriteToFileSeqDiag.puml b/docs/diagrams/WriteToFileSeqDiag.puml new file mode 100644 index 0000000000..cca239e644 --- /dev/null +++ b/docs/diagrams/WriteToFileSeqDiag.puml @@ -0,0 +1,29 @@ +@startuml +'https://plantuml.com/sequence-diagram + +'autonumber +activate Storage +-> Storage: writeToFile(String) + +Storage -> FileStorage: getGson() +activate FileStorage +FileStorage --> Storage +Storage -> Storage: toJson(ArrayList) +activate Storage +Storage --> Storage +deactivate Storage + +Storage -> FileStorage: writeToFile(String, String) + +FileStorage -> FileStorage: initializeFileWriter(String) +activate FileStorage + +FileStorage --> FileStorage +deactivate FileStorage + +FileStorage --> Storage +deactivate FileStorage + +<-- Storage + +@enduml \ No newline at end of file diff --git a/docs/diagrams/classDiagTrip.puml b/docs/diagrams/classDiagTrip.puml new file mode 100644 index 0000000000..0e7200744c --- /dev/null +++ b/docs/diagrams/classDiagTrip.puml @@ -0,0 +1,47 @@ +@startuml +'https://plantuml.com/class-diagram + +hide circle +skinparam classAttributeIconSize 0 + +class Trip +class Expense +class Person +class Storage +class CommandExecutor +class Ui + +class Trip { + -location : String + -dateOfTrip : LocalDate + -exchangeRate : double + -foreignCurrency : String + -foreignCurrencyFormat : String + -foreignCurrencySymbol : String + -listOfExpenses : ArrayList + -listOfPersons : ArrayList + + +getFilteredExpenses(expenseCategory : String, expenseAttribute : String) + +getDateOfTripString() : String + +setDateOfTrip(dateOfTrip : String) + +setExchangeRate(exchangeRateString : String) + +setForeignCurrency(foreignCurrency : String) + +setListOfPersons(listOfPersons : ArrayList) + +addExpense(expense : Expense) + -splitPeople(peopleChained : String) : ArrayList +} + +Trip --* "*" Expense : contains the list of > +Trip --* "*" Person : contains the list of > + +Storage <--> "*" Trip : contains the list of > + +Trip --> Ui : prints messages > +Ui --> Trip : receives user input > + +Note left of Ui : Receiving user input only occurs when\nthere is erroneous user input in the\noriginal input + +CommandExecutor --> Trip : can create, edit and delete > + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/colors.puml b/docs/diagrams/colors.puml new file mode 100644 index 0000000000..4cc30fdbd8 --- /dev/null +++ b/docs/diagrams/colors.puml @@ -0,0 +1,20 @@ +!define COLOR_WHITE #FFFFFF +!define COLOR_SILVER #C0C0C0 +!define COLOR_GREY #A5A8AC +!define COLOR_BLACK #000000 +!define COLOR_BROWN #800000 +!define COLOR_RED #FF0000 +!define COLOR_YELLOW #FFFF00 +!define COLOR_BRIGHT_GREEN #00FF00 +!define COLOR_DARK_GREEN #008000 +!define COLOR_LIGHT_BLUE #00FFFF +!define COLOR_TEAL #008080 +!define COLOR_DARK_BLUE #0000FF +!define COLOR_PINK #FF00FF +!define COLOR_PURPLE #800080 +!define COLOR_FAINT_TEAL #BBF1F1 +!define COLOR_FAINT_GREEN #C2F5CC +!define COLOR_LIGHT_PURPLE #CBC3E3 +!define COLOR_LIGHT_BROWN #C89D7C +!define COLOR_ORANGE #CF5300 +!define COLOR_FAINT_ORANGE #FFD580 \ No newline at end of file diff --git a/docs/diagrams/helpState.puml b/docs/diagrams/helpState.puml new file mode 100644 index 0000000000..8d1e4c7b5e --- /dev/null +++ b/docs/diagrams/helpState.puml @@ -0,0 +1,20 @@ +@startuml + +title Help Command States + +start + +if (Are there any trips that exist?) then (Yes) + if (Is trip opened?) then (Yes) + :[In trip] Help; + stop + else (No) + :[Not Opened] Help; + stop + endif +else (No) + :[No Trips] Help; +endif +stop + +@enduml \ No newline at end of file diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml new file mode 100644 index 0000000000..a775e8c48f --- /dev/null +++ b/docs/diagrams/style.puml @@ -0,0 +1,62 @@ +!include colors.puml + +!define UI_COLOR #1D8900 +!define UI_COLOR_T1 #83E769 +!define UI_COLOR_T2 #3FC71B +!define UI_COLOR_T3 #166800 +!define UI_COLOR_T4 #0E4100 + +!define LOGIC_COLOR COLOR_DARK_BLUE +!define LOGIC_COLOR_T1 #C8C8FA +!define LOGIC_COLOR_T2 #6A6ADC +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 + +!define STORAGE_COLOR COLOR_ORANGE +!define STORAGE_COLOR_T1 #FFE374 +!define STORAGE_COLOR_T2 #EDC520 +!define STORAGE_COLOR_T3 #806600 +!define STORAGE_COLOR_T2 #544400 + +!define USER_COLOR #000000 + +skinparam BackgroundColor #FFFFFFF + +skinparam Shadowing false + +skinparam Class { + FontColor #FFFFFF + BorderThickness 1 + BorderColor #FFFFFF + StereotypeFontColor #FFFFFF + FontName Arial +} + +skinparam Actor { + BorderColor USER_COLOR + Color USER_COLOR + FontName Arial +} + +skinparam Sequence { + MessageAlign center + BoxFontSize 15 + BoxPadding 0 + BoxFontColor #FFFFFF + FontName Arial +} + +skinparam Participant { + FontColor #FFFFFFF + Padding 20 +} + +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +hide footbox +hide members +hide circle \ No newline at end of file diff --git a/docs/diagrams/tripSeq.puml b/docs/diagrams/tripSeq.puml new file mode 100644 index 0000000000..e506d58962 --- /dev/null +++ b/docs/diagrams/tripSeq.puml @@ -0,0 +1,38 @@ +@startuml +'https://plantuml.com/sequence-diagram + +participant "<>\nDuke" as d +participant "<>\nParser" as p +participant "<>\nUi" as ui + +loop until user inputs 'quit' + +activate d +d->p : parserUserInput(userInput) +activate p +p->p: handleValidCommand("create", inputParams) +activate p +return +p->p: handleCreateTrip(inputParams) +activate p +return +p->p: executeCreateTrip(inputParams) +activate p + +alt inputParams syntax valid + opt Trip is Duplicate &\nUser does not want to add + p-->d + end +create participant ":Trip" as t +p->t++ +return + +else inputParams syntax Invalid + +p->ui: printCreateFormatError() +activate ui +return +end +return +end +@enduml \ No newline at end of file diff --git a/docs/images/Architectural_Diagram.JPG b/docs/images/Architectural_Diagram.JPG new file mode 100644 index 0000000000..447b6c011e Binary files /dev/null and b/docs/images/Architectural_Diagram.JPG differ diff --git a/docs/images/Architectural_Sequence_Diagram.JPG b/docs/images/Architectural_Sequence_Diagram.JPG new file mode 100644 index 0000000000..bd8cf25df5 Binary files /dev/null and b/docs/images/Architectural_Sequence_Diagram.JPG differ diff --git a/docs/images/Expense Sequence Diagram.jpeg b/docs/images/Expense Sequence Diagram.jpeg new file mode 100644 index 0000000000..acab3d9a51 Binary files /dev/null and b/docs/images/Expense Sequence Diagram.jpeg differ diff --git a/docs/images/ExpenseClassDiagram.png b/docs/images/ExpenseClassDiagram.png new file mode 100644 index 0000000000..24d4a45fd2 Binary files /dev/null and b/docs/images/ExpenseClassDiagram.png differ diff --git a/docs/images/ExpenseSequenceDiagram.png b/docs/images/ExpenseSequenceDiagram.png new file mode 100644 index 0000000000..206b7ef1f1 Binary files /dev/null and b/docs/images/ExpenseSequenceDiagram.png differ diff --git a/docs/images/HelpCommandStates.png b/docs/images/HelpCommandStates.png new file mode 100644 index 0000000000..42086a8bef Binary files /dev/null and b/docs/images/HelpCommandStates.png differ diff --git a/docs/images/ParserClassDiagram.png b/docs/images/ParserClassDiagram.png new file mode 100644 index 0000000000..359e06630e Binary files /dev/null and b/docs/images/ParserClassDiagram.png differ diff --git a/docs/images/ParserSequenceDiagram.png b/docs/images/ParserSequenceDiagram.png new file mode 100644 index 0000000000..6464a8cecf Binary files /dev/null and b/docs/images/ParserSequenceDiagram.png differ diff --git a/docs/images/Person_Diagram.JPG b/docs/images/Person_Diagram.JPG new file mode 100644 index 0000000000..4cda6f1477 Binary files /dev/null and b/docs/images/Person_Diagram.JPG differ diff --git a/docs/images/ReadFromFileSeqDiag.png b/docs/images/ReadFromFileSeqDiag.png new file mode 100644 index 0000000000..c0e573ad4b Binary files /dev/null and b/docs/images/ReadFromFileSeqDiag.png differ diff --git a/docs/images/StorageCompClassDiag.png b/docs/images/StorageCompClassDiag.png new file mode 100644 index 0000000000..36a525d557 Binary files /dev/null and b/docs/images/StorageCompClassDiag.png differ diff --git a/docs/images/WriteToFileSeqDiag.png b/docs/images/WriteToFileSeqDiag.png new file mode 100644 index 0000000000..08cc2781f0 Binary files /dev/null and b/docs/images/WriteToFileSeqDiag.png differ diff --git a/docs/images/classDiagTrip.png b/docs/images/classDiagTrip.png new file mode 100644 index 0000000000..07f8873755 Binary files /dev/null and b/docs/images/classDiagTrip.png differ diff --git a/docs/images/joshualeeky.jpg b/docs/images/joshualeeky.jpg new file mode 100644 index 0000000000..a5b4419834 Binary files /dev/null and b/docs/images/joshualeeky.jpg differ diff --git a/docs/images/leeyikai.jpeg b/docs/images/leeyikai.jpeg new file mode 100644 index 0000000000..7a147d766a Binary files /dev/null and b/docs/images/leeyikai.jpeg differ diff --git a/docs/images/tripSeq.png b/docs/images/tripSeq.png new file mode 100644 index 0000000000..5956835643 Binary files /dev/null and b/docs/images/tripSeq.png differ diff --git a/docs/images/xiyuan_profile.jpg b/docs/images/xiyuan_profile.jpg new file mode 100644 index 0000000000..d4fa136e67 Binary files /dev/null and b/docs/images/xiyuan_profile.jpg differ diff --git a/docs/images/yuzhao.jpeg b/docs/images/yuzhao.jpeg new file mode 100644 index 0000000000..637aa44c0c Binary files /dev/null and b/docs/images/yuzhao.jpeg differ diff --git a/docs/team/itsleeqian.md b/docs/team/itsleeqian.md new file mode 100644 index 0000000000..dcaa51992d --- /dev/null +++ b/docs/team/itsleeqian.md @@ -0,0 +1,38 @@ +# Lee Qi An - Project Portfolio Page + +## Overview +PayMeBack is a CLI based app that helps individuals manage their expenses when travelling or going out with others. + +### Summary of Contributions +The following sections summarize my contributions to my team's project. + +#### Code +Detailed code contribution information can be viewed via RepoSense [here](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=itsleeqian&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=authorship&tabAuthor=itsleeqian&tabRepo=AY2122S1-CS2113T-T12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code&authorshipIsBinaryFileTypeChecked=false). + +#### Enhancements implemented +I have contributed the following to our project PayMeBack: +1. The `summary` command, which allows users to view the summary of expenses of any individual (or everyone) throughout the trip. +2. The `list` command, which handles listing of all trips and expenses. +3. The `people` command, which lists all individuals in a trip. +4. Reminding the user of the correct syntax whenever the user enters something wrongly. + +#### UG Contributions +I added and maintained the following UG sections: List, Summary of Expenses, View people and the Table of Contents. + +Additionally, I was also in charge of finalizing the formatting and readability of the UG, standardizing various aspects of every section. + +#### DG Contribution +I have contributed the following to the DG: + +1. The whole of the Architecture section, including the Architectural diagram and architectural sequence diagram. +2. The UML diagram for Person + +#### Team based tasks Contribution +1. I helped to review my members' pull requests into our team repository, ensuring that the incoming code has no conflicts with the existing code. I would also discuss with the relevant members for any further improvements on the incoming code, and clarify any misunderstandings that appear. +2. Throughout the project, I helped to spot and rectify various bugs and issues that popped up. +3. I have also contributed significantly to cleaning up PE-D issues (closing duplicate issues and non issues) + +#### Review/mentoring contributions +Throughout the team project, I made sure that I was fully aware of all the different functions and enhancements added by my teammates. +By doing so, I was then able to review the code properly and guide my teammates on any possible improvements. +Having complete understanding of the code also allowed me to help clarify any doubts and misunderstandings from my teammates regarding our project. \ No newline at end of file diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/joshualeeky.md b/docs/team/joshualeeky.md new file mode 100644 index 0000000000..c45a9072a4 --- /dev/null +++ b/docs/team/joshualeeky.md @@ -0,0 +1,61 @@ +# Project Portfolio Page - Joshua Lee + +## Overview +_PayMeBack_ is an expense tracker targeted at groups travelling overseas. It aims to help groups simplify the process of +repayment for overseas expenses, by consolidating all expenses and issuing a summary report at the end, so users can easily +identify who they need to pay. + +## Summary of Contributions +The following section provides a summary of what I have contributed to the project. + +### Code Contributed +I have contributed over 1800 lines of code, split among documentation, test and functional code.
+My code contributions can be found via this +[link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/#breakdown=true&search=joshualeeky). + +I have contributed the following features to PayMeBack: +1. The Person class in its entirety. +2. Part of the Expense class constructor that helps determine if the names that have been entered are valid. +3. The ExpenseSplitter interface which helps keep track of how much the people involved in the expense owe the payer. +4. The `amount` command which gives a brief overview of the amount of money that is owed between the specified person +and the other people involved in the trip. + +### Enhancements implemented +I have contributed the following enhancements to PayMeBack: +1. The refactoring of the Parser class to help with overall neatness of the project. +2. The printing of currency in a user-friendly manner including the HashMap of the various currency ISO-codes and symbols. +3. Implementation of `last` for expenses to allow users to delete or view their most recently added expense. +4. Implemented a check to ensure that the people added into a trip or expense do not contain people with the same name. +5. Implementation of the command `-cancel` which allows users to abort a process when asked to correct an erroneous +input (completed in collaboration with @yeezao). + + +### Contributions to the UG +For the user guide, I contributed mainly to the features that I had implemented, which includes the section on the command +`amount` as well as the entirety of the section regarding the command `expense`. I also aided in the creation of +the table of available currencies. The user guide can be found +[here](https://ay2122s1-cs2113t-t12-2.github.io/tp/UserGuide.html). + +### Contributions to the DG +For the developer guide, I contributed to the sequence as well as the class diagram of the `parser` class and the class' +overall description. The developer guide can be found +[here](https://ay2122s1-cs2113t-t12-2.github.io/tp/DeveloperGuide.html). + +### Contributions to team-based tasks +In terms of contributions to team-based tasks, I feel like I have contributed most significantly in the following areas: +1. I helped to review members' pull requests into the GitHub repository while ensuring that whatever is being pulled is +in line with the proposed intent. +2. I assisted in the refactoring of functions to help with the overall neatness of the project. +3. I openly reached out to help any teammate who may have any bugs in their code with whatever tips and advice +I could offer. +4. I actively sought out bugs in the program and placed them on the issue tracker on GitHub to be addressed by the team. + +### Review/mentoring contributions +Throughout the project I ensured I was fully aware of the different functions and enhancements each member was adding +into the project. This allowed me to understand the scope and capabilities of the program so that I would be able to +guide the team in areas of improvement if needed. This also allowed me to help with any misunderstandings a member may +have with the overall structure of the program. + +### Contributions beyond the project team +I took part in peer review exercises seriously to give sincere and meaningful feedback to other teams to help them +improve their projects. \ No newline at end of file diff --git a/docs/team/leeyikai.md b/docs/team/leeyikai.md new file mode 100644 index 0000000000..e494a5df9b --- /dev/null +++ b/docs/team/leeyikai.md @@ -0,0 +1,43 @@ +# Lee Yi Kai - Project Portfolio Page + +## Overview +PayMeBack helps travellers who are on a budget manage their finances when travelling with friends. + +## Summary of Contributions +This section documents the summary of the contributions that I have done in this project. + +### Code Contributed +I have contributed over 900 lines of code that is split among functional code, tests and documentation. My code can be +found at this [link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=leeyikai) + +### Enhancements Implemented +I have implemented the following enhancements + +- Filtering of expenses by category, payer, description, which allows the user to search for expenses based on their + specific requirements. +- Payment optimization, where the number of transactions required to ensure that everyone is paid back is reduced. + +### Contributions to the UG +I have contributed to these sections in the UG + +- `view` section, where I have contributed most of the initial content, before my teammates helped to refine it to + make it clearer for the users. +- `optimize` section, where I help write the explanations so that the user can have a clearer understanding of what the + function does. + +### Contributions to the DG +I have contributed to these sections in the DG + +- `Expense` section, where I wrote the majority of the explanation and did up 2 diagrams, one to show the interactions + the `Expense` and other classes and interfaces. I also did up a comprehensive sequence diagram that illustrated clearly the flow of the initialization of an `Expense`. + +### Contributions to team-based tasks +I contributed in these areas for team-based tasks + +- Code enhancements: I ensured that the code that people were requesting to merge in were neat, and I recommended multiple ways for the code to be neatened such as removing magic numbers/strings. + +### Review/Mentoring Contributions +I contributed in review/mentoring in these areas + +- I suggested ways for the code to be refactored so that our overall readability can be improved. This was largely done during our meetings. +- I thoroughly tested other team's code and gave them valuable inputs to improve their interface. diff --git a/docs/team/lixiyuan416.md b/docs/team/lixiyuan416.md new file mode 100644 index 0000000000..97b478ea75 --- /dev/null +++ b/docs/team/lixiyuan416.md @@ -0,0 +1,58 @@ +# Project Portfolio Page - Li Xi Yuan + +## Summary of Contributions + +### Code +[Reposense Link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=lixiyuan416&sort=groupTitle&sortWithin=title&since=2021-09-25&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false) + +### Enhancements implemented +- **Ui** + - Added and maintained the `help` command + - Added, maintained, and debugged user prompts on screen when wrong command syntax is input by the user, for a large majority of the functionalities. + +- **Expenses** + - Added `expense` class + - Added date input and validation when creating an expense + - Added initial expense split confirmation in [#143](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/143) + - This is so that users are aware some people in the expense don't need to pay + +- **Trips** + - Added expense filtering by date and person involved + - Justification: Helpful filter options for users when viewing expenses in a trip + +- **Testing** + - Added tests for view filter methods in `ExpenseTest` + - Added `ParserTest` + +### UG Contribution +- **Initial Draft** + - Added expense section +- **Improved UG** + - Proofread and added missing functionalities (edit trip, settling expenses, view/delete last) +- **Added Command summary section** + +- **Added FAQ section** + +### DG Contribution +- **Initial Draft** + - Added `Expense` section, with [sequence diagram](../images/Expense%20Sequence%20Diagram.jpeg) + +- **Final Submission** + - Ui section - [Link to diagram](../images/HelpCommandStates.png) + - Added appendixes, completed Appendixes A-B + - Added [sequence diagram](../images/tripSeq.png) and [class diagram](../images/classDiagTrip.png) for Trip + +### Team based tasks Contribution +- **Added bug report in Issues** + - [#247](https://github.com/AY2122S1-CS2113T-T12-2/tp/issues/247) + +- **Bug fixes** + - View index [#144](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/144) + +- **Issued request to improve code quality** + - [#62](https://github.com/AY2122S1-CS2113T-T12-2/tp/issues/62) + + +### Review/mentoring contributions +- **Non trivial PR reviews with comments** + - [#219](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/219), [#146](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/146), [#141](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/141) diff --git a/docs/team/yeezao.md b/docs/team/yeezao.md new file mode 100644 index 0000000000..f2c7a395d5 --- /dev/null +++ b/docs/team/yeezao.md @@ -0,0 +1,78 @@ +# Liang Yuzhao - Project Portfolio Page + +## Overview of Project + +_PayMeBack_ is an expense tracker targeted at groups travelling overseas. It aims to help groups simplify the process of +repayment for overseas expenses, by consolidating all expenses and issuing a summary report at the end, so users can easily +identify who they need to pay. + +_PayMeBack_ is a greenfield project. My main responsibilities in this project included: +- As the team member most proficient in Java, to provide guidance and mentorship to other team members on Java. +- To manage the team organisation and repository. +- To implement and test new features in the program. +- To maintain high quality of code and workflows in collaboration with other members of the team. + +### Summary of Contributions + +#### Code Contributions + +I have contributed over >1800 lines of code and documentation in total. +Detailed code contribution information can be viewed via RepoSense [here](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=authorship&tabAuthor=yeezao&tabRepo=AY2122S1-CS2113T-T12-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false). + +- Provided basic skeleton for `Ui`, `Parser`, `Trip`, `Expense`, `Person` classes. +- Implemented and tested `FileStorage` class, and some of `Ui`, `Trip`, `Storage`, `Parser`, and `ForceCancelException` classes. +- Wrote a large part, or the entirety, of `FileTest`, `TripTest`, and `ValidityCheckerTest`. + +#### Enhancements and Features Implemented: + +- **New**: Using `last` to get the trip which the user last interacted with. +- **New**: Reading from and writing to a save file using the JSON format (through serialisation/deserialisation with Google Gson). +- **Enhancement**: Adding custom serialiser/deserialiser to Gson for LocalDate objects. +- **Enhancement**: Ability to assist user in detecting and overwriting a corrupted save file. +- **New**: Allowing users to abort a process when asked to correct an erroneous input (completed in collaboration with @joshualeeky) + +#### Contributions to User Guide: + +The following sections in the [User Guide](https://ay2122s1-cs2113t-t12-2.github.io/tp/UserGuide.html) were largely or wholly written by me: + +- Saving your data, Loading your saved data +- Create Trip, Open Trip, Close Trip, Delete Trip, Edit Trip +- FAQ #2 and #3 + +In addition, I made contributions to the following sections: + +- Introduction, Using this guide, Quick Start +- FAQ #1 + +#### Contributions to Developer Guide: + +The following sections in the [Developer Guide](https://ay2122s1-cs2113t-t12-2.github.io/tp/DeveloperGuide.html) were largely or wholly written by me: + +- `Storage` component, and its related diagrams +- Non-functional Requirements + +In addition, I made contributions to the following sections: + +- Some text in `Trip` class +- Manual testing instructions for trip-related features + +#### Contributions to Team Tasks: + +- Setup and administration of organisation and team repository, and reviewing pull requests +- Adding templates for User Stories and Bug Reporting in Issues +- Gradle modifications (for enabling assertions and adding Gson dependency) +- Managed `v2.0` and `v2.1` release +- Cleanup of PE-D issues (marking duplicates, rejecting non-issues) +- General maintenance of issue tracker (milestone and label assignment) + +#### Review and Mentoring contributions + +- Comments on PRs - [#31](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/31#discussion_r723066635), + [#42](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/42#discussion_r725532182), [#55](https://github.com/AY2122S1-CS2113T-T12-2/tp/pull/55#discussion_r726785554) +- Bug reports - [#135](https://github.com/AY2122S1-CS2113T-T12-2/tp/issues/135), [#49](https://github.com/AY2122S1-CS2113T-T12-2/tp/issues/49), [#45](https://github.com/AY2122S1-CS2113T-T12-2/tp/issues/45), +- Mentoring on JUnit tests to simulate and test user inputs and outputs + +#### Contributions beyond the team + +- Replying to module forum posts ([#74](https://github.com/nus-cs2113-AY2122S1/forum/issues/74#issuecomment-922768286)) +- Reporting of bugs on module website ([#93](https://github.com/nus-cs2113-AY2122S1/forum/issues/93)) \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..315d6a2911 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'tp' + diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java index 5c74e68d59..682d9bb975 100644 --- a/src/main/java/seedu/duke/Duke.java +++ b/src/main/java/seedu/duke/Duke.java @@ -1,21 +1,66 @@ package seedu.duke; +import seedu.duke.parser.Parser; + +import com.google.gson.Gson; + +import java.io.IOException; import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; public class Duke { + + private static boolean isProgramRunning = true; + + /** + * Prints the welcome message and initializes {@link Scanner}, {@link Logger}, and {@link Gson}, + * and reads the user's save file. + */ + private static void beginWelcome() { + + Ui.printWelcome(); + + Scanner in = new Scanner(System.in); + Storage.setScanner(in); + Logger logger = Logger.getLogger("ProgramLogger"); + //Temporary disable logger + logger.setLevel(Level.OFF); + Storage.setLogger(logger); + + FileStorage.initializeGson(); + Storage.readFromFile(Storage.FILE_PATH); + + } + + /** + * Reads and returns user input with leading and trailing whitespaces removed. + * + * @param in {@link Scanner} to read user input + * @return user input with leading and trailing whitespaces removed + */ + private static String readUserInput(Scanner in) { + Ui.printPendingCommand(); + return in.nextLine().strip(); + } + /** * Main entry-point for the java.duke.Duke application. */ public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); + beginWelcome(); + + while (isProgramRunning) { + isProgramRunning = Parser.parseUserInput(readUserInput(Storage.getScanner())); + try { + Storage.writeToFile(Storage.FILE_PATH); + } catch (IOException e) { + Ui.printCouldNotSaveMessage(); + //e.printStackTrace(); + } + } + } + } diff --git a/src/main/java/seedu/duke/FileStorage.java b/src/main/java/seedu/duke/FileStorage.java new file mode 100644 index 0000000000..896f2d3845 --- /dev/null +++ b/src/main/java/seedu/duke/FileStorage.java @@ -0,0 +1,115 @@ +//@@author yeezao + +package seedu.duke; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Scanner; +import java.util.logging.Level; + +public class FileStorage { + + private static Gson gson; + + /** + * Gets the {@link FileWriter} from {@link FileStorage#initializeFileWriter(String)}, and writes the + * JSON String to the file. + * + * @param jsonString parsed JSON string containing all data from the program. + * @throws IOException if writing to file fails. + */ + public static void writeToFile(String jsonString, String filePath) throws IOException { + Storage.getLogger().log(Level.INFO, "starting write to save file"); + FileWriter fileWriter = initializeFileWriter(filePath); + fileWriter.write(jsonString); + fileWriter.close(); + } + + /** + * Reads the raw JSON String from the indicated save file. + * + * @param filePath path of the JSON file to be read from. + * @return JSON String from the file. + * @throws FileNotFoundException if there is no file corresponding to the filePath. + */ + public static String readFromFile(String filePath) throws FileNotFoundException { + File file = new File(filePath); + Scanner scanner = new Scanner(file); + return scanner.nextLine(); + } + + /** + * Creates a new blank file at the given filePath. + * + * @param filePath path location to create the file at + * @throws IOException if file creation fails (thrown from {@link FileWriter}). + */ + public static void newBlankFile(String filePath) throws IOException { + FileWriter fileWriter = initializeFileWriter(filePath); + fileWriter.close(); + } + + /** + * Initializes a new instance of {@link FileWriter} with the given filePath. + * + * @param filePath path location to create the file at. + * @return instance of {@link FileWriter}. + * @throws IOException if the FileWriter could not be created. + * + * @see FileStorage#newBlankFile(String) + * @see FileStorage#writeToFile(String, String) + */ + private static FileWriter initializeFileWriter(String filePath) throws IOException { + return new FileWriter(filePath); + } + + /** + * Registers the custom serializers and deserializers for {@link LocalDate} type, and creates an + * instance of {@link Gson} stored in {@link FileStorage}. + */ + public static void initializeGson() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateSerializer()); + gsonBuilder.registerTypeAdapter(LocalDate.class, new LocalDateDeserializer()); + FileStorage.gson = gsonBuilder.create(); + } + + public static Gson getGson() { + return gson; + } + + private static class LocalDateSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } + } + + private static class LocalDateDeserializer implements JsonDeserializer { + + @Override + public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String dateInString = json.getAsJsonPrimitive().getAsString(); + DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return LocalDate.parse(dateInString, pattern); + } + } + +} diff --git a/src/main/java/seedu/duke/Person.java b/src/main/java/seedu/duke/Person.java new file mode 100644 index 0000000000..ddbc6cfdb4 --- /dev/null +++ b/src/main/java/seedu/duke/Person.java @@ -0,0 +1,46 @@ +package seedu.duke; + + +import java.util.HashMap; + +//@@author joshualeeky +public class Person { + private String name; + private HashMap moneyOwed = new HashMap<>(); + private HashMap optimizedMoneyOwed = new HashMap<>(); + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setMoneyOwed(Person person, double amount) { + double originalAmount = moneyOwed.get(person.getName()); + moneyOwed.put(person.getName(), originalAmount + amount); + } + + public void setName(String name) { + this.name = name; + } + + public HashMap getMoneyOwed() { + return this.moneyOwed; + } + + public HashMap getOptimizedMoneyOwed() { + return this.optimizedMoneyOwed; + } + + public void setOptimizedMoneyOwed(Person person) { + optimizedMoneyOwed.put(person.getName(), 0.0); + } + + @Override + public String toString() { + return this.getName(); + } + +} diff --git a/src/main/java/seedu/duke/Storage.java b/src/main/java/seedu/duke/Storage.java new file mode 100644 index 0000000000..67f5bd6f8c --- /dev/null +++ b/src/main/java/seedu/duke/Storage.java @@ -0,0 +1,264 @@ +package seedu.duke; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.trip.Trip; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Storage { + + public static final String FILE_PATH = "trips.json"; + public static final String LAST_INTERACTED = "last"; + private static ArrayList listOfTrips = new ArrayList<>(); + private static Trip openTrip = null; + private static Trip lastTrip = null; + + private static Scanner scanner; + private static Logger logger; + + //@@author joshualeeky + private static final ArrayList validCommands = new ArrayList<>( + Arrays.asList("create", "edit", "view", "open", "list", "summary", + "delete", "expense", "quit", "help", "amount", "people", "close", "optimize")); + + private static final HashMap availableCurrency = new HashMap<>() {{ + put("USD", new String[]{"$", "%.02f"}); + put("SGD", new String[]{"$", "%.02f"}); + put("AUD", new String[]{"$", "%.02f"}); + put("CAD", new String[]{"$", "%.02f"}); + put("NZD", new String[]{"$", "%.02f"}); + put("EUR", new String[]{"", "%.02f"}); //€ + put("GBP", new String[]{"", "%.02f"}); //£ + put("MYR", new String[]{"RM", "%.02f"}); + put("HKD", new String[]{"$", "%.02f"}); + put("THB", new String[]{"", "%.02f"}); //฿ + put("RUB", new String[]{"", "%.02f"}); //₽ + put("ZAR", new String[]{"R", "%.02f"}); + put("TRY", new String[]{"", "%.02f"}); //₺ + put("BRL", new String[]{"R$", "%.02f"}); + put("DKK", new String[]{"Kr.", "%.02f"}); + put("PLN", new String[]{"", "%.02f"}); //zł + put("ILS", new String[]{"", "%.02f"}); //₪ + put("SAR", new String[]{"SR", "%.02f"}); + put("CNY", new String[]{"", "%.0f"}); //¥ + put("JPY", new String[]{"", "%.0f"}); //¥ + put("KRW", new String[]{"", "%.0f"}); //₩ + put("IDR", new String[]{"Rp", "%.0f"}); + put("INR", new String[]{"Rs", "%.0f"}); + put("CHF", new String[]{"SFr.", "%.0f"}); + put("SEK", new String[]{"kr", "%.0f"}); + put("NOK", new String[]{"kr", "%.0f"}); + put("MXN", new String[]{"$", "%.0f"}); + put("TWD", new String[]{"NT$", "%.0f"}); + put("HUF", new String[]{"Ft", "%.0f"}); + put("CZK", new String[]{"Kc", "%.0f"}); + put("CLP", new String[]{"$", "%.0f"}); + put("PHP", new String[]{"", "%.0f"}); //₱ + put("AED", new String[]{"", "%.0f"}); //د.إ + put("COP", new String[]{"$", "%.0f"}); + put("RON", new String[]{"lei", "%.0f"}); + }}; + + public static HashMap getAvailableCurrency() { + return availableCurrency; + } + + public static double formatForeignMoneyDouble(double money) throws ForceCancelException { + return Double.parseDouble(String.format(Storage.getOpenTrip().getForeignCurrencyFormat(), money)); + } + + public static double formatRepaymentMoneyDouble(double money) throws ForceCancelException { + return Double.parseDouble(String.format(Storage.getOpenTrip().getRepaymentCurrencyFormat(), money)); + } + + //@@author + + //@@author yeezao + /** + * Serializes the {@link Storage#listOfTrips} into a JSON String using {@link Gson} + * to be written to the save file. + * + * @throws IOException if {@link FileStorage#writeToFile(String, String)} fails + * + * @see FileStorage#writeToFile(String, String) + */ + public static void writeToFile(String filePath) throws IOException { + String jsonString = FileStorage.getGson().toJson(listOfTrips); + FileStorage.writeToFile(jsonString, filePath); + } + + /** + * Parsers the JSON string returned from {@link FileStorage#readFromFile(String)} + * to populate the {@link Storage#listOfTrips}. + * + * @see FileStorage#readFromFile(String) + */ + public static void readFromFile(String filePath) { + try { + String jsonString = FileStorage.readFromFile(filePath); + Type tripType = new TypeToken>(){}.getType(); + listOfTrips = FileStorage.getGson().fromJson(jsonString, tripType); + Ui.printFileLoadedSuccessfully(); + } catch (JsonParseException e) { + Ui.printJsonParseError(); + askOverwriteOrClose(); + } catch (NoSuchElementException e) { + Ui.printEmptyFileWarning(); + } catch (FileNotFoundException e) { + Ui.printFileNotFoundError(); + createNewFile(FILE_PATH); + } + } + + private static final int EXIT_ERROR_CODE = 1; + + /** + * Creates a new blank file at the specified file path ({@link Storage#FILE_PATH}). + * + * @see FileStorage#newBlankFile(String) + */ + public static void createNewFile(String filePath) { + try { + FileStorage.newBlankFile(filePath); + Ui.newFileSuccessfullyCreated(); + } catch (IOException ex) { + Ui.printCreateFileFailure(); + System.exit(EXIT_ERROR_CODE); + } + } + + /** + * If {@link Storage#readFromFile(String)} throws a {@link JsonParseException}, asks the user whether to overwrite + * the corrupted file or close the program. + * + * @see Storage#createNewFile(String) + */ + private static void askOverwriteOrClose() { + + while (true) { + Ui.printJsonParseUserInputPrompt(); + String input = scanner.nextLine().strip(); + if (input.contains(Ui.USER_QUIT)) { + Ui.goodBye(); + Storage.getLogger().log(Level.WARNING, "JSON Parse failed, user requests program end"); + System.exit(EXIT_ERROR_CODE); + return; + } else if (input.contains(Ui.USER_CONTINUE)) { + createNewFile(FILE_PATH); + return; + } + } + } + + + public static Scanner getScanner() { + return scanner; + } + + public static void setScanner(Scanner scanner) { + Storage.scanner = scanner; + } + //@@author + + public static ArrayList getValidCommands() { + return validCommands; + } + + //@@author yeezao + /** + * Gets the currently open trip. If no trip is open, asks the user to enter a trip index to open that trip. + * + * @return the currently opened trip + */ + public static Trip getOpenTrip() throws ForceCancelException { + if (openTrip == null) { + Ui.printNoOpenTripError(); + promptUserForValidTrip(); + Ui.printOpenTripMessage(openTrip); + } + lastTrip = openTrip; + return openTrip; + } + + /** + * If the user enters an invalid trip number, asks the user to re-enter a valid trip number. + * + * @see Storage#getOpenTrip() + */ + private static void promptUserForValidTrip() throws ForceCancelException { + try { + System.out.print("Please enter the trip you would like to open: "); + String input = Ui.receiveUserInput(); + int tripIndex = Integer.parseInt(input) - 1; + setOpenTrip(tripIndex); + } catch (NumberFormatException e) { + Ui.argNotNumber(); + Ui.promptForTripIndex(); + promptUserForValidTrip(); + } + } + + /** + * Checks if there is an open trip or not. + * @return true if there is an open trip + */ + public static boolean checkOpenTrip() { + return openTrip != null; + } + + /** + * Opens the trip at the specified tripIndex, and sets that trip as the last modified trip. + * @param tripIndex index of the trip inside {@link Storage#listOfTrips} to be opened + */ + public static void setOpenTrip(int tripIndex) { + openTrip = listOfTrips.get(tripIndex); + lastTrip = openTrip; + } + + /** + * Closes the currently active trip, sets it as the last trip, and sets the open trip as null. + */ + public static void closeTrip() { + Trip tripToBeClosed = openTrip; + openTrip = null; + lastTrip = tripToBeClosed; + Ui.printTripClosed(tripToBeClosed); + } + + public static Logger getLogger() { + return logger; + } + + public static void setLogger(Logger logger) { + Storage.logger = logger; + } + + public static ArrayList getListOfTrips() { + return listOfTrips; + } + + public static Trip getLastTrip() { + return lastTrip; + } + + public static void setLastTrip(Trip lastTrip) { + Storage.lastTrip = lastTrip; + } + + public static void setListOfTrips(ArrayList listOfTrips) { + Storage.listOfTrips = listOfTrips; + } + +} diff --git a/src/main/java/seedu/duke/Ui.java b/src/main/java/seedu/duke/Ui.java new file mode 100644 index 0000000000..d4e83d58a4 --- /dev/null +++ b/src/main/java/seedu/duke/Ui.java @@ -0,0 +1,605 @@ +package seedu.duke; + +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.expense.Expense; +import seedu.duke.parser.Parser; +import seedu.duke.trip.Trip; + +import java.util.ArrayList; +import java.util.HashMap; + +public class Ui { + + public static final String USER_CONTINUE = "y"; + public static final String USER_QUIT = "n"; + + public static void printOptimizedAmounts() throws ForceCancelException { + boolean isAllPaid = true; + Trip openTrip = Storage.getOpenTrip(); + ArrayList listOfPersons = openTrip.getListOfPersons(); + HashMap currentHashMap; + String nameOfPersonPaying; + String nameOfPersonReceiving; + Double amountOwed; + for (Person personPaying : listOfPersons) { + for (Person personReceiving : listOfPersons) { + nameOfPersonPaying = personPaying.getName(); + nameOfPersonReceiving = personReceiving.getName(); + currentHashMap = personPaying.getOptimizedMoneyOwed(); + amountOwed = currentHashMap.get(nameOfPersonReceiving); + if (!personPaying.equals(personReceiving) && amountOwed < 0) { + if (isAllPaid) { + System.out.println("Here are the optimized payment transactions:"); + } + System.out.println(nameOfPersonPaying + " needs to pay " + + stringForeignMoney(-amountOwed) + + " (" + stringRepaymentMoney(-amountOwed) + ")" + + " to " + personReceiving); + isAllPaid = false; + } + } + } + if (isAllPaid) { + System.out.println("All are paid! :)"); + } + } + + public static String receiveUserInput() throws ForceCancelException { + String userInput = Storage.getScanner().nextLine().strip(); + if (Parser.doesUserWantToForceCancel(userInput)) { + throw new ForceCancelException(); + } + return userInput; + } + + public static void printPendingCommand() { + System.out.print("Enter your command: "); + } + + public static void printWelcome() { + String logo = System.lineSeparator() + + " ____ __ ___ ____ __ " + System.lineSeparator() + + " / __ \\____ ___ __/ |/ ___ / __ )____ ______/ /__" + System.lineSeparator() + + " / /_/ / __ `/ / / / /|_/ / _ \\/ __ / __ `/ ___/ //_/" + System.lineSeparator() + + " / ____/ /_/ / /_/ / / / / __/ /_/ / /_/ / /__/ ,< " + System.lineSeparator() + + "/_/ \\__,_/\\__, /_/ /_/\\___/_____/\\__,_/\\___/_/|_| " + System.lineSeparator() + + " /____/ " + System.lineSeparator(); + System.out.println("Welcome to" + logo); + } + + public static void goodBye() { + System.out.println("Exiting the program now. Goodbye!"); + } + + public static void newTripSuccessfullyCreated(Trip newTrip) { + System.out.println("Your trip to " + newTrip.getLocation() + " on " + + newTrip.getDateOfTripString() + " has been successfully added!"); + } + + + //@@author joshualeeky + public static String stringForeignMoney(double val) { + try { + return Storage.getOpenTrip().getForeignCurrency() + " " + + Storage.getOpenTrip().getForeignCurrencySymbol() + + String.format(Storage.getOpenTrip().getForeignCurrencyFormat(), val); + } catch (ForceCancelException e) { + printForceCancelled(); + return null; + } + } + + public static String stringRepaymentMoney(double val) { + try { + return Storage.getOpenTrip().getRepaymentCurrency() + " " + + Storage.getOpenTrip().getRepaymentCurrencySymbol() + + String.format(Storage.getOpenTrip().getRepaymentCurrencyFormat(), + val / Storage.getOpenTrip().getExchangeRate()); + } catch (ForceCancelException e) { + printForceCancelled(); + return null; + } + } + + public static void askAutoAssignRemainder(Person person, double remainder) { + System.out.print("Assign the remaining " + stringForeignMoney(remainder) + + " to " + person.getName() + "? (y/n): "); + } + + //@@author + + + public static void printListOfPeople(ArrayList people) { + for (Person person : people) { + System.out.println("\t" + person.getName()); + } + } + + public static void printExpenseDetails(Expense e) { + System.out.println(e); + } + + + public static void printFilteredExpenses(Expense e, int index) { + System.out.println((index + 1) + ". " + e); + } + + public static void printExpenseAddedSuccess() { + System.out.println("Your expense has been added successfully!"); + } + + public static void printExpensesInList(ArrayList listOfExpenses) { + if (listOfExpenses.isEmpty()) { + printNoExpensesError(); + return; + } + System.out.println("List of Expenses: "); + for (int i = 0; i < listOfExpenses.size(); i++) { + System.out.print("\t"); + System.out.println(i + 1 + ". " + + listOfExpenses.get(i).getDescription() + " | " + + listOfExpenses.get(i).getStringDate()); + } + } + + public static void printOpenTripMessage(Trip trip) { + System.out.println("You have opened the following trip: " + + System.lineSeparator() + + trip.getLocation() + " | " + trip.getDateOfTripString()); + System.out.println(); + } + + public static void printCreateFormatError() { + System.out.println("Please format your inputs as follows: " + + System.lineSeparator() + + "create /[place] /[date DD-MM-YYYY] /[currency ISO] /[exchange rate] /[persons-in-trip]."); + System.out.println("Separate person-in-trip with commas"); + } + + public static void printExpenseFormatError() { + System.out.println("Please format your inputs as follows: " + + System.lineSeparator() + + "expense [amount] [category] [people] /[description]."); + } + + public static void printEditFormatError() { + System.out.println("Please format your inputs as follows: " + + System.lineSeparator() + + "edit [trip num] [attribute] [new value]" + + System.lineSeparator() + + "attributes: -location, -date, -exchangerate, -forcur, -homecur" + + System.lineSeparator()); + } + + + public static void printFilterFormatError() { + System.out.println("Please format your inputs as follows: " + + System.lineSeparator() + + "view filter [category, description, payer, person, date] [search keyword]"); + } + + + public static void printExchangeRateFormatError() { + System.out.print("Please re-enter your exchange rate as a decimal number (e.g. 1.32): "); + } + + public static void printInvalidAmountError() { + System.out.print("Please re-enter your expense amount as a positive number (i.e > 0): "); + } + + public static void printDateTimeFormatError() { + System.out.print("The entered date is invalid. Please enter the date again: "); + } + + public static void dateInvalidError() { + System.out.println("Sorry, the date you entered is invalid. Please enter the date again: "); + } + + public static void printIsoFormatError() { + System.out.print("Please re-enter your currency ISO (e.g. JPY, USD): "); + } + + public static void printUnknownCommandError() { + System.out.println("Sorry, we didn't recognize your entry. Please try again, or enter help " + + "to learn more."); + } + + public static void printSingleUnknownTripIndexError() { + System.out.println("Invalid trip number, try again"); + System.out.println("Syntax: open [trip number]"); + System.out.println("--------------------------"); + printAllTrips(); + System.out.println("--------------------------"); + } + + public static void printUnknownTripIndexError() { + System.out.println("Sorry, no such trip number exists. Please check your trip number and try again."); + } + + public static void printUnknownExpenseIndexError() { + System.out.println("Sorry, no such expense number exists. Please check your expense number and try again."); + } + + public static void printNoTripError() { + System.out.println("You have not created a trip yet. Please create a trip using the keyword 'create'."); + } + + public static void printDeleteTripSuccessful(Trip tripDeleted) { + System.out.println("Your trip to " + tripDeleted.getLocation() + " on " + + tripDeleted.getDateOfTripString() + " has been successfully removed."); + } + + public static void printDeleteExpenseSuccessful(Double expenseAmount) { + System.out.println("Your expense of " + stringForeignMoney(expenseAmount) + " has been successfully removed."); + } + + public static void printNoExpensesError() { + System.out.println("There are no expenses in your trip, please add an expense using the keyword 'expense'."); + } + + public static void printNoPersonFound(String string) { + System.out.println("There are no persons with the name of [" + string + "] in this trip."); + } + + public static void printSummaryFormatError() { + System.out.println("Please format your inputs as follows: " + + System.lineSeparator() + + "\"summary\" or \"summary [person name]\"."); + } + + public static void printNoMatchingExpenseError() { + System.out.println("No matching expenses found."); + } + + public static void printNoOpenTripError() { + System.out.println("You have not opened any trip yet. Please open a trip to proceed further."); + printAllTrips(); + } + + //@@author itsleeqian + public static void printAllTrips() { + System.out.println("List of Trips: "); + ArrayList listOfTrips = Storage.getListOfTrips(); + for (int i = 0; i < listOfTrips.size(); i++) { + System.out.print("\t"); + System.out.println(i + 1 + ". " + + listOfTrips.get(i).getLocation() + " | " + + listOfTrips.get(i).getDateOfTripString()); + } + } + //@@author + + public static void emptyArgForOpenCommand() { + System.out.println(); + System.out.println("Which trip to open?"); + System.out.println("Syntax: open [trip number]"); + System.out.println("--------------------------"); + printAllTrips(); + System.out.println("--------------------------"); + + } + + public static void argNotNumber() { + System.out.println("Input is not a number"); + } + + public static void promptForTripIndex() { + System.out.print("The number you entered is not valid, "); + } + + public static void emptyArgForDeleteTripCommand() { + System.out.println(); + System.out.println("Which trip to delete?"); + System.out.println("Syntax: delete [trip number]"); + System.out.println("---------------------------"); + printAllTrips(); + System.out.println("---------------------------"); + } + + public static void emptyArgForDeleteExpenseCommand() throws ForceCancelException { + System.out.println(); + System.out.println("Which expense to delete?"); + System.out.println("Syntax: delete [expense number]"); + System.out.println("---------------------------"); + printExpensesInList(Storage.getOpenTrip().getListOfExpenses()); + System.out.println("---------------------------"); + } + + public static void invalidAmountFormat() { + System.out.println("The syntax for amount you have entered is invalid. " + + "Please format as follows: amount [person]."); + } + + public static void invalidArgForAmount() throws ForceCancelException { + Trip currTrip = Storage.getOpenTrip(); + System.out.println("The person you entered is not in the opened trip."); + System.out.println("These are the people involved in this trip:"); + Ui.printListOfPeople(currTrip.getListOfPersons()); + System.out.println(); + } + + + public static void printGetPersonPaid() { + System.out.print("Who paid for the expense?: "); + } + + public static void printHowMuchDidPersonSpend(String name, double amountRemaining) { + System.out.print("There is " + stringForeignMoney(amountRemaining) + " left to be assigned." + + " How much did " + name + " spend?: "); + } + + public static void printPersonNotInExpense() { + System.out.println("The person you entered is not in the expense, please try again."); + } + + //@@author joshualeeky + public static void printAmount(Person person, Trip trip) { + System.out.println(person.getName() + " spent " + + stringForeignMoney(person.getMoneyOwed().get(person.getName())) + + " (" + stringRepaymentMoney(person.getMoneyOwed().get(person.getName())) + ") on the trip so far"); + + for (Person otherPerson : trip.getListOfPersons()) { + if (otherPerson != person) { + if (person.getMoneyOwed().get(otherPerson.getName()) > 0) { + System.out.println(otherPerson.getName() + " owes " + + stringForeignMoney(person.getMoneyOwed().get(otherPerson.getName())) + + " (" + stringRepaymentMoney(person.getMoneyOwed().get(otherPerson.getName())) + ")" + + " to " + person.getName()); + } else if (person.getMoneyOwed().get(otherPerson.getName()) < 0) { + System.out.println(person.getName() + " owes " + + stringForeignMoney(-person.getMoneyOwed().get(otherPerson.getName())) + + " (" + stringRepaymentMoney(-person.getMoneyOwed().get(otherPerson.getName())) + ")" + + " to " + otherPerson.getName()); + } else { + System.out.println(person.getName() + " does not owe anything to " + otherPerson.getName()); + } + } + } + } + //@@author + + public static void printIncorrectAmount(double amount) { + System.out.println("The amount you have entered is not possible. The total " + + "of the expense should equal " + stringForeignMoney(amount)); + } + + public static void printPeopleInvolved(ArrayList personArrayList) { + System.out.println("These are the people who are involved in the expense: "); + printListOfPeople(personArrayList); + } + + //@@author lixiyuan416 + public static void displayHelp() { + if (Storage.getListOfTrips().isEmpty()) { + System.out.println("You have no trips! Create one to get started!"); + System.out.println(); + System.out.println("Syntax: create /[location] /[date dd-mm-yyyy] " + + "/[foreign-currency-ISO-code] /[exchange-rate] /[persons-in-trip]"); + System.out.println("\t Separate each person-in-trip with commas"); + System.out.println(); + System.out.println("quit: exit the program"); + System.out.println(); + } else if (!Storage.checkOpenTrip()) { + System.out.println("You have trips, but have not opened any"); + System.out.println(); + System.out.println("list: list all your trips"); + System.out.println("open [trip number]: Open a trip"); + System.out.println("delete [trip number]: Delete a trip"); + System.out.println(); + System.out.println("create /[location] /[date dd-mm-yyyy] " + + "/[foreign-currency-ISO-code] /[exchange-rate] /[persons-in-trip]: Creates a trip"); + System.out.println("Separate persons-in-trip with commas"); + System.out.println(); + System.out.println("edit [trip num] [attribute] [new value]: edit trip attributes"); + System.out.println("\tattributes: -location, -date, -exchangerate, -forcur, -homecur"); + System.out.println("\tNote the hyphen in the attribute"); + System.out.println("\tlast can be used for [trip num]"); + System.out.println(); + System.out.println("quit: exit the program"); + System.out.println(); + } else { + System.out.println("You are inside a trip. Here is a list of what you can do:"); + System.out.println(); + System.out.println("\texpense: creates an expense"); + System.out.println("\t\texpense [amount] [category] [people] /[description]"); + System.out.println("\t\t Separate each person with a comma"); + System.out.println(); + System.out.println("\tlist: List all expenses of the trip"); + System.out.println(); + System.out.println("\tpeople: List of persons in the trip"); + System.out.println(); + System.out.println("\tdelete [expense num]: delete an expense"); + System.out.println("\t\t\"delete last\" to delete last expense"); + System.out.println(); + System.out.println("\tview filter [options] [search keyword]: list filtered expenses."); + System.out.println("\t\tfilter options: [category, description, payer, person, date]"); + System.out.println(); + System.out.println("\tview -[index]: View expense in detail"); + System.out.println("\t\tViews all expenses if index not provided. \"view last\" to view last expense"); + System.out.println(); + System.out.println("\tsummary -[name]: Shows all much a person has spent on this trip in total."); + System.out.println("\t\tDisplays summary for everyone in the trip if index not provided"); + System.out.println(); + System.out.println("\toptimize: Displays who-pays-who instructions to " + + "settle expense repayment at the end of the trip"); + System.out.println("\tamount [person]: Displays who-pays-who instructions for one person, unoptimized"); + System.out.println(); + System.out.println("\tclose: Closes the current trip"); + System.out.println("\topen [trip num]: Closes the current trip, opens another trip"); + System.out.println(); + System.out.println("\tYou can also create or edit a trip, " + + "but it's recommended to close the current trip first"); + System.out.println("\tquit: exit the program"); + System.out.println(); + } + } + //@@author + + public static void printInvalidFilterError() { + System.out.println("Please filter using the following valid filter attributes: " + + System.lineSeparator() + + "[category], [description], [payer], [person]"); + } + + public static void printFileNotFoundError() { + System.out.println("No preloaded data found! We have created a file for you."); + } + + public static void printJsonParseError() { + System.out.println("We couldn't read your save file. It may be corrupted, " + + "or may have been wrongly modified outside the program."); + System.out.println("To overwrite your current save file and start with a new save file, enter 'y'. " + + "Otherwise, enter 'n' to exit the program."); + System.out.println("IMPORTANT: if you start with a new save file, your previous data will be erased. " + + "This operation is irreversible."); + } + + public static void printJsonParseUserInputPrompt() { + System.out.print("Would you like to overwrite your save file? (y/n): "); + } + + public static void printErrorWithInitialAmount() { + System.out.println("Please check the amount you entered for the expense or " + + "the amount you allocated to each person again."); + } + + public static void sameNameInTripError() { + System.out.println("You have entered people with the same name, please recreate the trip ensuring there are no " + + "repeated names for the trip."); + } + + public static void sameNameInExpenseError() { + System.out.println("You have entered people with the same name."); + System.out.println("Please reenter the names of the participants of the expense, " + + "ensuring there are no repeats:"); + } + + public static void printNoLastTripError() { + System.out.println("You may have deleted the most recently modified trip. " + + "Please try again with the trip number of the trip you wish to edit."); + } + + public static void printCreateFileFailure() { + System.out.println("The save file could not be created. Exiting the program now..."); + } + + public static void newFileSuccessfullyCreated() { + System.out.println("A new save file has been created!"); + } + + public static void printEmptyFileWarning() { + System.out.println("A save file was found, but it is empty."); + System.out.println("If you wish to recover the contents of your save file, please exit the program now."); + System.out.println("Otherwise, you may continue to use the program."); + } + + public static void printInvalidPeople(ArrayList names) throws ForceCancelException { + final Trip currTrip = Storage.getOpenTrip(); + for (String name : names) { + if (names.indexOf(name) == names.size() - 1) { + System.out.print(name); + } else if (names.indexOf(name) == names.size() - 2) { + System.out.print(name + " and "); + } else if (names.indexOf(name) < names.size() - 2) { + System.out.print(name + ", "); + } + } + if (names.size() == 1) { + System.out.print(" is "); + } else { + System.out.print(" are "); + } + System.out.println("not part of the trip."); + System.out.println("These are the names of the people who are part of the trip:"); + printListOfPeople(currTrip.getListOfPersons()); + System.out.println("Please enter the names of the people who are involved in this expense again, " + + "separated by a comma:"); + } + + public static void printTripClosed(Trip trip) { + System.out.println("You have closed the following trip:" + + System.lineSeparator() + + trip.getLocation() + " | " + trip.getDateOfTripString()); + } + + //@@author lixiyuan416 + public static void equalSplitPrompt() { + System.out.println("Enter \"equal\" if expense is to be evenly split, enter individual spending otherwise"); + } + + public static void askUserToConfirm() { + System.out.print("There will be people involved that don't need to pay, are you sure? (y/n): "); + } + + public static void expenseDateInvalid() { + System.out.println("\tPlease enter date as DD-MM-YYYY, or enter nothing to use today's date"); + } + + public static void expensePromptDate() { + System.out.println("Enter date of expense:"); + System.out.println("\tPress enter to use today's date"); + } + + public static void noRecentExpenseError() { + System.out.println("You have not recently added an expense."); + } + + public static void viewFilterDateFormatInvalid() { + System.out.println("\tPlease enter date as DD-MM-YYYY"); + } + //@@author + + public static void changeForeignCurrencySuccessful(Trip tripToEdit, String original) { + System.out.println("Your foreign spending currency has been changed from " + + original + " to " + tripToEdit.getForeignCurrency() + "."); + } + + public static void changeHomeCurrencySuccessful(Trip tripToEdit, String original) { + System.out.println("Your home currency has been changed from " + + original + " to " + tripToEdit.getRepaymentCurrency() + "."); + } + + public static void changeExchangeRateSuccessful(Trip tripToEdit, double original) { + System.out.println("The exchange rate has been changed from " + + original + " to " + tripToEdit.getExchangeRate() + "."); + } + + public static void changeDateSuccessful(Trip tripToEdit, String original) { + System.out.println("The date of your trip has been changed from " + + original + " to " + tripToEdit.getDateOfTripString() + "."); + } + + public static void changeLocationSuccessful(Trip tripToEdit, String original) { + System.out.println("The location of your trip has been changed from " + + original + " to " + tripToEdit.getLocation() + "."); + } + + public static void printCouldNotSaveMessage() { + System.out.println("Sorry, there was an error saving your data. We'll try to save your data again" + + "the next time you enter a command."); + } + + public static void printFileLoadedSuccessfully() { + System.out.println(); + System.out.println("Your saved data was successfully loaded!"); + System.out.println(); + } + + public static void printForceCancelled() { + System.out.println("You have chosen to cancel this operation."); + } + + public static void locationIsBlank() { + System.out.println("No location was entered. Please enter your trip location: "); + } + + public static void noPersonsAdded() { + System.out.println("No persons were added to this trip. Please enter the names of the people in this trip: "); + } + + public static void duplicateTripWarning() { + System.out.println("A trip with similar information may already exist. Please confirm if you wish to proceed" + + " with creating this trip."); + System.out.print("Enter 'y' if you wish to create this trip, or 'n' to cancel: "); + } +} diff --git a/src/main/java/seedu/duke/exceptions/ForceCancelException.java b/src/main/java/seedu/duke/exceptions/ForceCancelException.java new file mode 100644 index 0000000000..d3f0564599 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/ForceCancelException.java @@ -0,0 +1,4 @@ +package seedu.duke.exceptions; + +public class ForceCancelException extends Exception { +} diff --git a/src/main/java/seedu/duke/exceptions/InvalidAmountException.java b/src/main/java/seedu/duke/exceptions/InvalidAmountException.java new file mode 100644 index 0000000000..09ccbadf04 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/InvalidAmountException.java @@ -0,0 +1,4 @@ +package seedu.duke.exceptions; + +public class InvalidAmountException extends Exception { +} diff --git a/src/main/java/seedu/duke/exceptions/NoExpensesException.java b/src/main/java/seedu/duke/exceptions/NoExpensesException.java new file mode 100644 index 0000000000..08240a5e51 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/NoExpensesException.java @@ -0,0 +1,4 @@ +package seedu.duke.exceptions; + +public class NoExpensesException extends Exception { +} diff --git a/src/main/java/seedu/duke/exceptions/SameNameException.java b/src/main/java/seedu/duke/exceptions/SameNameException.java new file mode 100644 index 0000000000..ed60544379 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/SameNameException.java @@ -0,0 +1,4 @@ +package seedu.duke.exceptions; + +public class SameNameException extends Exception { +} diff --git a/src/main/java/seedu/duke/exceptions/TripNotOpenException.java b/src/main/java/seedu/duke/exceptions/TripNotOpenException.java new file mode 100644 index 0000000000..dc16ae1d25 --- /dev/null +++ b/src/main/java/seedu/duke/exceptions/TripNotOpenException.java @@ -0,0 +1,4 @@ +package seedu.duke.exceptions; + +public class TripNotOpenException extends Exception { +} diff --git a/src/main/java/seedu/duke/expense/Expense.java b/src/main/java/seedu/duke/expense/Expense.java new file mode 100644 index 0000000000..d938654a2a --- /dev/null +++ b/src/main/java/seedu/duke/expense/Expense.java @@ -0,0 +1,273 @@ +package seedu.duke.expense; + +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.InvalidAmountException; +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.Ui; +import seedu.duke.parser.Parser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.logging.Level; + +public class Expense implements ExpenseSplitter { + private double amountSpent; + private String description; + private ArrayList personsList; + private String category; + private LocalDate date; + private Person payer; + private HashMap amountSplit = new HashMap<>(); + private static final DateTimeFormatter inputPattern = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static final DateTimeFormatter outputPattern = DateTimeFormatter.ofPattern("dd MMM yyyy"); + + private static final int NUMBER_OF_INPUTS = 3; + private static final int EXPENSE_INDEX = 0; + private static final int CATEGORY_INDEX = 1; + private static final int PERSONS_INDEX = 2; + private static final int PERSONS_AND_DESCRIPTION_INDEX = 2; + private static final int INDEX_OF_PERSON = 0; + + /** + * Legacy constructor for Expense. Used as stub for testing. Does not include parsing. + * + * @param amountSpent amount spent in the expense. + * @param category category which is expense is being spent in. + * @param personsList list of people that were involved in the expense. + * @param description description of expense. + */ + //@@author lixiyuan416 + public Expense(double amountSpent, String description, ArrayList personsList, + String category, LocalDate date, Person payer, HashMap amountSplit) { + this.amountSpent = amountSpent; + this.description = description; + this.category = category; + this.date = date; + this.payer = payer; + this.amountSplit = amountSplit; + this.personsList = personsList; + } + + /** + * Constructor for {@link Expense} class - contains parsing and amount assignment. + * + * @param inputDescription String of user input to be parsed and assigned to expense attributes + */ + public Expense(String inputDescription) throws ForceCancelException { + + + String[] expenseInfo = inputDescription.split(" ", NUMBER_OF_INPUTS); + setAmountSpent(expenseInfo[EXPENSE_INDEX]); + setCategory(expenseInfo[CATEGORY_INDEX].toLowerCase()); + this.description = getDescriptionParse(expenseInfo[PERSONS_AND_DESCRIPTION_INDEX]); + this.personsList = checkValidPersons(expenseInfo[PERSONS_INDEX]); + this.date = promptDate(); + if (personsList.size() == 1) { + ExpenseSplitter.updateOnePersonSpending(this, personsList.get(INDEX_OF_PERSON)); + } else { + ExpenseSplitter.updateIndividualSpending(this); + } + } + + //@@author joshualeeky + + /** + * Extracts the description of the expense from the user input. + * + * @param userInput the string that the user enters. + * @return description of the expense. + */ + private static String getDescriptionParse(String userInput) { + return userInput.split("/")[1].strip(); + } + + /** + * Obtains a list of Person objects from a string of names separated by a comma. Also checks if there are repeated + * names that were entered in the expense. + * + * @param userInput the string that the user enters. + * @return ArrayList containing the Person objects included in the expense. + */ + private static ArrayList checkValidPersons(String userInput) throws ForceCancelException { + String[] listOfPeople = userInput.split("/")[0].split(","); + ArrayList listOfPeopleNamesUpperCase = new ArrayList<>(); + ArrayList validListOfPeople = new ArrayList<>(); + ArrayList invalidListOfPeople = new ArrayList<>(); + if (listOfPeople.length == 1 && listOfPeople[0].strip().equalsIgnoreCase("-all")) { + return Storage.getOpenTrip().getListOfPersons(); + } + boolean isThereRepeatedName = false; + for (String name : listOfPeople) { + boolean isValidPerson = false; + for (Person person : Storage.getOpenTrip().getListOfPersons()) { + if (name.strip().equalsIgnoreCase(person.getName())) { + validListOfPeople.add(person); + isValidPerson = true; + break; + } + } + if (listOfPeopleNamesUpperCase.contains(name.strip().toUpperCase())) { + isThereRepeatedName = true; + } else if (!isValidPerson) { + invalidListOfPeople.add(name.strip()); + } + listOfPeopleNamesUpperCase.add(name.strip().toUpperCase()); + } + if (!invalidListOfPeople.isEmpty()) { + Ui.printInvalidPeople(invalidListOfPeople); + String newUserInput = Ui.receiveUserInput(); + return checkValidPersons(newUserInput); + } else if (isThereRepeatedName) { + Ui.sameNameInExpenseError(); + String newUserInput = Ui.receiveUserInput(); + return checkValidPersons(newUserInput); + } + return validListOfPeople; + } + + + //@@author lixiyuan416 + + /** + * Prompts user for date. + * + * @return today's date if user input is an empty string, otherwise keeps prompting user until a valid date is given + */ + public LocalDate promptDate() throws ForceCancelException { + Ui.expensePromptDate(); + String inputDate = Ui.receiveUserInput(); + while (!isDateValid(inputDate)) { + inputDate = Ui.receiveUserInput(); + Storage.getLogger().log(Level.INFO, "Invalid date format entered"); + } + if (inputDate.isEmpty()) { + return LocalDate.now(); + } + return LocalDate.parse(inputDate, inputPattern); + } + + private Boolean isDateValid(String date) { + if (date.isEmpty()) { + return true; + } + try { + LocalDate.parse(date, inputPattern); + //Additional check for weird dates like feb 31 + if (!Parser.doesDateReallyExist(date)) { + Ui.expenseDateInvalid(); + return false; + } + return true; + } catch (DateTimeParseException e) { + Ui.expenseDateInvalid(); + return false; + } + } + + + @Override + public String toString() { + return ("\t" + this.getDescription() + + System.lineSeparator() + + "\t" + "Date: " + this.getStringDate() + + System.lineSeparator() + + "\t" + "Amount Spent: " + Ui.stringForeignMoney(this.getAmountSpent()) + + System.lineSeparator() + + "\t" + "People involved:" + + System.lineSeparator() + + getPersonExpense() + + "\t" + "Payer: " + this.getPayer() + + System.lineSeparator() + + "\t" + "Category: " + this.category) + + System.lineSeparator(); + } + //@@author + + public String getPersonExpense() { + StringBuilder returnString = new StringBuilder(); + String name; + String formattedSpace = "\t"; + for (Person p : personsList) { + name = p.getName(); + returnString.append(formattedSpace); + returnString.append(formattedSpace); + returnString.append(personsList.indexOf(p) + 1).append(") "); + returnString.append(name).append(", "); + returnString.append(Ui.stringForeignMoney(getAmountSplit().get(name))); + returnString.append(System.lineSeparator()); + } + return returnString.toString(); + } + + //@@author itsleeqian + public void setAmountSpent(String amount) throws ForceCancelException { + try { + this.amountSpent = Double.parseDouble(amount); + if (this.amountSpent <= 0) { + throw new InvalidAmountException(); + } + this.amountSpent = Double.parseDouble(amount); + this.amountSpent = Storage.formatForeignMoneyDouble(this.amountSpent); + } catch (InvalidAmountException e) { + Ui.printInvalidAmountError(); + String newInput = Ui.receiveUserInput(); + setAmountSpent(newInput); + } + } + //@@author + + public String getDescription() { + return description; + } + + public void setDate(String date) { + this.date = LocalDate.parse(date, inputPattern); + } + + public void setCategory(String category) { + this.category = category; + } + + public String getCategory() { + return category; + } + + public LocalDate getDate() { + return date; + } + + public String getStringDate() { + return date.format(outputPattern); + } + + public ArrayList getPersonsList() { + return personsList; + } + + public double getAmountSpent() { + return amountSpent; + } + + + public void setPayer(Person person) { + this.payer = person; + } + + public Person getPayer() { + return payer; + } + + + public void setAmountSplit(Person person, double amount) { + amountSplit.put(person.getName(), amount); + } + + public HashMap getAmountSplit() { + return amountSplit; + } + +} diff --git a/src/main/java/seedu/duke/expense/ExpenseSplitter.java b/src/main/java/seedu/duke/expense/ExpenseSplitter.java new file mode 100644 index 0000000000..251b91e415 --- /dev/null +++ b/src/main/java/seedu/duke/expense/ExpenseSplitter.java @@ -0,0 +1,230 @@ +package seedu.duke.expense; + +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.parser.Parser; +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.Ui; + +import java.util.HashMap; + +public interface ExpenseSplitter { + double EPSILON = 0.001; + + //@@author joshualeeky + + /** + * If there is only one person that is part of the expense, the function automatically assigns the amount spent + * on the expense to the person in the expense. + * @param expense Expense that is being added into the current trip. + * @param person Person who is part of the trip. + */ + static void updateOnePersonSpending(Expense expense, Person person) { + person.setMoneyOwed(person, expense.getAmountSpent()); + expense.setPayer(person); + expense.setAmountSplit(person, expense.getAmountSpent()); + } + + /** + * Updates the spending of each individual who is entered into the expense. + * @param expense Expense that is being added. + * @throws ForceCancelException Cancel the creation of the expense anytime an input is required by the user. + */ + static void updateIndividualSpending(Expense expense) throws ForceCancelException { + Person payer = getValidPersonInExpenseFromString(expense); + expense.setPayer(payer); + HashMap amountBeingPaid = new HashMap<>(); + Ui.equalSplitPrompt(); + double total = 0.0; + for (Person person : expense.getPersonsList()) { + double amountRemaining = expense.getAmountSpent() - total; + if (amountRemaining < EPSILON) { + assignZeroToRemaining(expense, amountBeingPaid, payer); + return; + } else if (checkLastPersonInExpense(expense, person)) { + assignRemainder(person, payer, amountRemaining, expense, amountBeingPaid); + return; + } else { + amountBeingPaid = assignNormal(person, payer, amountRemaining, total, amountBeingPaid, expense); + } + if (amountBeingPaid != null) { + total += amountBeingPaid.get(person); + } else { + return; + } + } + } + + /** + * Helper function for updateIndividualSpending(). Updates the amountBeingPaid HashMap while checking if the amount + * entered is higher than the amount spent for the expense as well as checking if the value entered is valid. + * @param person the current Person who is being assigned a value + * @param payer the Person who paid for the Expense + * @param amountRemaining the amount of money left to be assigned + * @param total the total amount of money that has been assigned + * @param amountBeingPaid HashMap of the amount each Person is assigned + * @param expense the expense that is being added to the current Trip. + * @return updated amountBeingPaid HashMap. + * @throws ForceCancelException if the user chooses to cancel the operation. + */ + private static HashMap assignNormal(Person person, Person payer, double amountRemaining, + double total, HashMap amountBeingPaid, Expense expense) throws ForceCancelException { + Ui.printHowMuchDidPersonSpend(person.getName(), amountRemaining); + String amountString = Ui.receiveUserInput(); + if (checkAssignEqual(amountBeingPaid, amountString)) { + assignEqualAmounts(payer, expense, amountBeingPaid); + return null; + } else { + try { + double amount = Double.parseDouble(amountString); + amount = Storage.formatForeignMoneyDouble(amount); + total += amount; + if (total > expense.getAmountSpent() || amount < 0) { + Ui.printIncorrectAmount(expense.getAmountSpent()); + updateIndividualSpending(expense); + return null; + } else { + amountBeingPaid.put(person, amount); + return amountBeingPaid; + } + } catch (NumberFormatException e) { + Ui.argNotNumber(); + return assignNormal(person, payer, amountRemaining, total, amountBeingPaid, expense); + } + } + } + + /** + * Helper function for updateIndividualSpending(). checks if the user input is 'equal' to indicate that the user + * would like to split the expense equally among the people involved in the expense. Also checks to ensure that + * the command being entered is before the user enters any amount for any person. + * @param amountBeingPaid Hashmap containing the people who the user has assigned values to. + * @param amountString the user input. + * @return true or false boolean value to indicate if the user has used 'equal'. + */ + private static boolean checkAssignEqual(HashMap amountBeingPaid, String amountString) { + return amountString.equalsIgnoreCase("equal") && amountBeingPaid.isEmpty(); + } + + /** + * Helper function for updateIndividualSpending(). Checks if a Person is the last to be assigned an amount + * in the expense. + * @param expense Expense that is being checked. + * @param person Person to be checked. + * @return true or false boolean value to indicate if it is the last person in the expense. + */ + private static boolean checkLastPersonInExpense(Expense expense, Person person) { + return expense.getPersonsList().indexOf(person) == expense.getPersonsList().size() - 1; + } + + /** + * Helper function for updateIndividualSpending(). Assigns the rest of the Persons the amount 0 if the amount + * for the expense has been fully paid for already. Requires user confirmation. + * @param expense Expense that is being added to the current trip. + * @param amountBeingPaid HashMap containing the people who the user has assigned values to. + * @param payer Person who is paying for the expense. + * @throws ForceCancelException Cancels the creation of the expense in the event the user wishes to + * stop creating the expense. + */ + private static void assignZeroToRemaining(Expense expense, HashMap amountBeingPaid, Person payer) + throws ForceCancelException { + Ui.askUserToConfirm(); + if (Parser.getUserToConfirm()) { + for (Person person : expense.getPersonsList()) { + if (!amountBeingPaid.containsKey(person)) { + amountBeingPaid.put(person, 0d); + } + } + assignAmounts(payer, expense,amountBeingPaid); + } else { + Ui.printErrorWithInitialAmount(); + updateIndividualSpending(expense); + } + } + + /** + * Helper function for updateIndividualSpending(). Assigns the remaining amount of the expense to the last person + * in the expense list after receiving approval from the user. + * @param person the Person who is to be assigned the amount. + * @param payer the Person who paid for the expense. + * @param amountRemaining the amount to be assigned to the last person. + * @param expense the expense that is being added. + * @param amountBeingPaid the HashMap containing how much each Person spent on the expense. + * @throws ForceCancelException allows the user to cancel the command. + */ + private static void assignRemainder(Person person, Person payer, double amountRemaining, Expense expense, + HashMap amountBeingPaid) throws ForceCancelException { + Ui.askAutoAssignRemainder(person, amountRemaining); + if (Parser.getUserToConfirm()) { + amountBeingPaid.put(person, Storage.formatForeignMoneyDouble(amountRemaining)); + assignAmounts(payer, expense, amountBeingPaid); + } else { + Ui.printErrorWithInitialAmount(); + updateIndividualSpending(expense); + } + } + + /** + * Helper function for updateIndividualSpending(). Checks the input String from the user to ensure that the name + * entered is of a person who is part of the Expense. + * @param expense Expense that is being checked. + * @return Person who is associated to the String that was input by the user, if the user input is invalid and the + * String is not of a name of a Person in the Expense, the function will ask the user for an input again. + * @throws ForceCancelException Allows the user to cancel anytime there is an input required by the user. + */ + private static Person getValidPersonInExpenseFromString(Expense expense) throws ForceCancelException { + Ui.printGetPersonPaid(); + String name = Ui.receiveUserInput(); + for (Person person : expense.getPersonsList()) { + if (name.equalsIgnoreCase(person.getName())) { + return person; + } + } + Ui.printPersonNotInExpense(); + Ui.printPeopleInvolved(expense.getPersonsList()); + return getValidPersonInExpenseFromString(expense); + } + + /** + * Helper function for updateIndividualSpending(). Divides the amount spent on the expense equally among all the + * participants in the Expense. If there is a deficit or surplus, the payer will bear the extra or loss. + * @param payer Person who is paying for the Expense. + * @param expense Expense that is being added to the current Trip. + * @param amountBeingPaid The amount that is being paid for the Expense. + */ + private static void assignEqualAmounts(Person payer, Expense expense, HashMap amountBeingPaid) + throws ForceCancelException { + double total = 0.0; + double amount = Storage.formatForeignMoneyDouble(expense.getAmountSpent() / expense.getPersonsList().size()); + for (Person people : expense.getPersonsList()) { + amountBeingPaid.put(people, amount); + total += amount; + } + if (total != expense.getAmountSpent()) { + double payerAmount = amountBeingPaid.get(payer) + (expense.getAmountSpent() - total); + amountBeingPaid.put(payer, payerAmount); + } + assignAmounts(payer, expense, amountBeingPaid); + } + + /** + * Helper function for updateIndividualSpending(). Stores the value that each Person is assigned in their own Person + * class to be used for other functions, also stores the value each Person owes the payer of the Expense. + * @param payer Person who paid for the Expense. + * @param expense The Expense that is being added to the current Trip. + * @param amountBeingPaid HashMap containing the people who the user has assigned values to. + */ + private static void assignAmounts(Person payer, Expense expense, HashMap amountBeingPaid) { + for (Person person : expense.getPersonsList()) { + if (person == payer) { + person.setMoneyOwed(person, amountBeingPaid.get(person)); + expense.setAmountSplit(person, amountBeingPaid.get(person)); + continue; + } + payer.setMoneyOwed(person, amountBeingPaid.get(person)); + person.setMoneyOwed(payer, -amountBeingPaid.get(person)); + person.setMoneyOwed(person, amountBeingPaid.get(person)); + expense.setAmountSplit(person, amountBeingPaid.get(person)); + } + } +} diff --git a/src/main/java/seedu/duke/parser/CommandExecutor.java b/src/main/java/seedu/duke/parser/CommandExecutor.java new file mode 100644 index 0000000000..04d03b8181 --- /dev/null +++ b/src/main/java/seedu/duke/parser/CommandExecutor.java @@ -0,0 +1,483 @@ +package seedu.duke.parser; + +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.NoExpensesException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.exceptions.TripNotOpenException; +import seedu.duke.trip.Trip; +import seedu.duke.Storage; +import seedu.duke.Ui; +import seedu.duke.Person; +import seedu.duke.expense.Expense; + +import java.util.ArrayList; + +import static seedu.duke.Storage.LAST_INTERACTED; + +abstract class CommandExecutor extends PaymentOptimizer implements ExpenseSummarizer { + private static final int EDIT_ATTR_COUNT = 2; + private static final int ATTRIBUTE_DATA = 1; + private static final int EDIT_ATTRIBUTE = 0; + private static final int EDIT_INDEX = 0; + private static final String EDIT_LOCATION = "-location"; + private static final String EDIT_DATE = "-date"; + private static final String EDIT_EXRATE = "-exchangerate"; + private static final String EDIT_FORCUR = "-forcur"; + private static final String EDIT_HOMECUR = "-homecur"; + private static final int NEW_TRIP_ATTRIBUTES_COUNT = 6; + private static final int NUMBER_OF_PARAMETERS = 3; + private static final int INDEX_OF_SECOND_COMMAND = 0; + private static final int INDEX_OF_CATEGORY = 1; + private static final int INDEX_OF_EXPENSE_ATTRIBUTE = 2; + private static final String LAST = "last"; + private static final String FILTER = "filter"; + + //@@author yeezao + /** + * Creates a new instance of {@link Trip}. + * + * @param attributesInString attributes of the trip to be added (in a single {@link String}), before being parsed. + * + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + * @throws IndexOutOfBoundsException if the user has not entered sufficient attributes to create a new trip. + * @throws SameNameException if the user enters multiple persons with the same name. + */ + protected static void executeCreateTrip(String attributesInString) + throws ForceCancelException, IndexOutOfBoundsException, SameNameException { + String[] newTripInfo = attributesInString.split("/", NEW_TRIP_ATTRIBUTES_COUNT); + if (newTripInfo.length < NEW_TRIP_ATTRIBUTES_COUNT) { + throw new IndexOutOfBoundsException(); + } + Trip newTrip = new Trip(newTripInfo); + if (!isTripADuplicate(newTrip)) { + addNewTripToList(newTrip); + } else { + if (doesUserWantToAddDuplicateTrip()) { + addNewTripToList(newTrip); + } + } + + } + + /** + * Adds a newly-created {@link Trip} to the listOfTrips. + * + * @param newTrip instance of a newly-created {@link Trip}. + */ + private static void addNewTripToList(Trip newTrip) { + Storage.getListOfTrips().add(newTrip); + Ui.newTripSuccessfullyCreated(newTrip); + Storage.setLastTrip(newTrip); + } + + /** + * Asks if the user wants to proceed with adding a trip that has been detected as a duplicate. + * + * @return true if the user still wants to add the trip + * @throws ForceCancelException if the user does not want to add the trip + */ + private static boolean doesUserWantToAddDuplicateTrip() throws ForceCancelException { + Ui.duplicateTripWarning(); + while (true) { + String userOption = Ui.receiveUserInput(); + if (userOption.contains(Ui.USER_CONTINUE)) { + return true; + } else if (userOption.contains(Ui.USER_QUIT)) { + Ui.printForceCancelled(); + return false; + } + } + } + + /** + * Checks if the trip might be a duplicate of an already existing trip. + * The following attributes are checked: date, exchange rate, location, currency. + * + * @param newTrip instance of newly-created {@link Trip} object + * @return true if the trip is detected as a possible duplicate + */ + public static boolean isTripADuplicate(Trip newTrip) { + for (Trip tripToCompare : Storage.getListOfTrips()) { + if (tripToCompare.getDateOfTrip().equals(newTrip.getDateOfTrip()) + && tripToCompare.getExchangeRate() == newTrip.getExchangeRate() + && tripToCompare.getForeignCurrency().equalsIgnoreCase(newTrip.getForeignCurrency()) + && tripToCompare.getLocation().equalsIgnoreCase(newTrip.getLocation())) { + return true; + } + } + return false; + } + + + + /** + * Gets the trip to be edited and edits the specified attributes of the trip. + * + * @param inputDescription - user input of trip index and trip attributes to edit. + * + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + * + * @see Parser#editTripWithIndex(String) + * @see Parser#editTripPerAttribute(Trip, String) + * + */ + protected static void executeEditTrip(String inputDescription) throws ForceCancelException { + String[] tripToEditInfo = inputDescription.split(" ", EDIT_ATTR_COUNT); + String attributesToEdit = tripToEditInfo[ATTRIBUTE_DATA]; + Trip tripToEdit; + if (tripToEditInfo[EDIT_INDEX].equals(LAST_INTERACTED)) { + tripToEdit = Storage.getLastTrip(); + if (tripToEdit == null) { + Ui.printNoLastTripError(); + return; + } + } else { + tripToEdit = editTripWithIndex(tripToEditInfo[EDIT_INDEX].strip()); + } + editTripPerAttribute(tripToEdit, attributesToEdit); + } + + /** + * Sets the user-specified trip as opened. Requires that the {@code listOfTrips} has at least one open trip. + * + * @param indexAsString index of trip to open, as a {@link String} to be parsed. + * + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executeOpen(String indexAsString) throws ForceCancelException { + //assumes that listOfTrips have at least 1 trip + int indexToGet = Integer.parseInt(indexAsString) - 1; + if (Storage.checkOpenTrip()) { + Ui.printTripClosed(Storage.getOpenTrip()); + } + Storage.setOpenTrip(indexToGet); + Ui.printOpenTripMessage(Storage.getOpenTrip()); + } + //@@author + + //@@author itsleeqian + /** + * Prints out the summary of expenses of an individual or everyone. + * @param inputParams the individual to view. Can also be null to print everyone. + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executeSummary(String inputParams) throws ForceCancelException { + Trip currentTrip = Storage.getOpenTrip(); + if (inputParams == null) { + //list everybody's expense summary + for (Person p : currentTrip.getListOfPersons()) { + ExpenseSummarizer.getIndividualExpenseSummary(p); + System.out.println(); + } + } else { + //list only 1 person, if exists + try { + //returns null if no such person + Person personToView = getValidPersonInTripFromString(inputParams, currentTrip); + if (personToView != null) { + ExpenseSummarizer.getIndividualExpenseSummary(personToView); + } else { + Ui.printNoPersonFound(inputParams); + Ui.printSummaryFormatError(); + } + + } catch (IndexOutOfBoundsException e) { + Ui.printNoExpensesError(); + } + } + } + //@@author + + //@@author leeyikai + + /** + * Checks to see which expenses user wants to see and calls the appropriate method. + * @param inputParams contains the information that determines what expenses user wants to see + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executeView(String inputParams) throws ForceCancelException { + Trip openTrip = Storage.getOpenTrip(); + if (inputParams == null) { + openTrip.viewAllExpenses(); + } else if (inputParams.equalsIgnoreCase(LAST_INTERACTED)) { + if (openTrip.getLastExpense() == null) { + Ui.noRecentExpenseError(); + } else { + System.out.println(openTrip.getLastExpense()); + } + } else { + String[] paramString = inputParams.split(" ", NUMBER_OF_PARAMETERS); + String secondCommand = paramString[INDEX_OF_SECOND_COMMAND]; + String expenseCategory = null; + String expenseAttribute = null; + if (!isNumeric(secondCommand)) { + expenseCategory = paramString[INDEX_OF_CATEGORY]; + expenseAttribute = paramString[INDEX_OF_EXPENSE_ATTRIBUTE]; + } + if (secondCommand.equalsIgnoreCase(FILTER)) { + try { + openTrip.getFilteredExpenses(expenseCategory, expenseAttribute); + } catch (IndexOutOfBoundsException e) { + Ui.printNoExpensesError(); + } + } else if (isNumeric(secondCommand)) { + try { + int index = Integer.parseInt(secondCommand); + System.out.println(openTrip.getExpenseAtIndex(index)); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + Ui.printUnknownExpenseIndexError(); + } + + } + } + } + //@@author + + //@@author yeezao + /** + * Checks whether to delete trip or delete expense (by determining if a trip is open), + * and calls the appropriate method. + * + * @param inputParams attributes of trip to be deleted (if valid, this should be the trip/expense index) + * + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + * + * @see Parser#executeDeleteTrip(int) + * @see Parser#executeDeleteExpense(int) + */ + protected static void executeDelete(String inputParams) throws ForceCancelException { + int index; + if (Storage.checkOpenTrip()) { + Trip currTrip = Storage.getOpenTrip(); + if (inputParams.equalsIgnoreCase(LAST_INTERACTED)) { + index = currTrip.getListOfExpenses().indexOf(currTrip.getLastExpense()); + } else { + index = Integer.parseInt(inputParams) - 1; + } + executeDeleteExpense(index); + } else { + if (inputParams.equalsIgnoreCase(LAST_INTERACTED)) { + index = Storage.getListOfTrips().indexOf(Storage.getLastTrip()); + } else { + index = Integer.parseInt(inputParams) - 1; + } + executeDeleteTrip(index); + } + } + //@@author + + //@@author itsleeqian + /** + * Lists either trips or expenses depending on if a trip is open or not. + * @throws ForceCancelException allows the user to cancel an operation when an input is required + */ + protected static void executeList() throws ForceCancelException { + if (!Storage.checkOpenTrip()) { + Ui.printAllTrips(); + } else { + Ui.printExpensesInList(Storage.getOpenTrip().getListOfExpenses()); + } + } + //@@author + + //@@author joshualeeky + + /** + * Creates an Expense object in the current opened trip. + * @param inputDescription the input of the user. + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executeCreateExpense(String inputDescription) throws ForceCancelException { + Trip currTrip = Storage.getOpenTrip(); + assert Storage.checkOpenTrip(); + Expense newExpense = new Expense(inputDescription); + currTrip.addExpense(newExpense); + currTrip.setLastExpense(newExpense); + Ui.printExpenseAddedSuccess(); + } + + /** + * Prints how much a Person object owe other Person object and/or how much other Person objects owe the Person + * object. + * + * @param inputParams the input of the user. + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executeAmount(String inputParams) throws ForceCancelException { + Trip trip = Storage.getOpenTrip(); + Person toBeChecked = getValidPersonInTripFromString(inputParams, trip); + if (toBeChecked == null) { + Ui.invalidArgForAmount(); + } else { + Ui.printAmount(toBeChecked, trip); + } + } + + //@@author + + //@@author itsleeqian + /** + * Lists the people involved in a trip. + * @throws TripNotOpenException cannot list people if no trip is open. + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executePeople() throws TripNotOpenException, ForceCancelException { + Trip currTrip = Storage.getOpenTrip(); + if (Storage.checkOpenTrip()) { + System.out.println("These are the people involved in this trip:"); + Ui.printListOfPeople(currTrip.getListOfPersons()); + } else { + throw new TripNotOpenException(); + } + } + //@@author + + //@@author leeyikai + + /** + * Check that there are expenses in the current open trip. If there is, execute the optimization method. + * + * @throws NoExpensesException stops the optimize command when there is no expenses available to optimize. + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + protected static void executeOptimize() throws NoExpensesException, ForceCancelException { + if (Storage.getOpenTrip().getListOfExpenses().size() > 0) { + checkForOptimization(); + } else { + throw new NoExpensesException(); + } + } + //@@author + + //@@author joshualeeky + private static void executeDeleteExpense(int expenseIndex) throws ForceCancelException { + Trip currentTrip = Storage.getOpenTrip(); + Expense expenseToDelete = currentTrip.getListOfExpenses().get(expenseIndex); + double expenseAmount = expenseToDelete.getAmountSpent(); + correctBalances(expenseToDelete); + currentTrip.removeExpense(expenseIndex); + Ui.printDeleteExpenseSuccessful(expenseAmount); + currentTrip.setLastExpense(null); + } + + //@@author yeezao + /** + * Deletes a trip from the listOfTrips. + * + * @param tripIndex index of Trip to be applied to listOfTrips + */ + private static void executeDeleteTrip(int tripIndex) { + ArrayList listOfTrips = Storage.getListOfTrips(); + Trip tripToDelete = listOfTrips.get(tripIndex); + listOfTrips.remove(tripIndex); + Ui.printDeleteTripSuccessful(tripToDelete); + Storage.setLastTrip(null); + } + + + /** + * Parses the user input to determine which attributes to edit, + * and calls the relevant setters to edit those attributes. + * + * @param tripToEdit user-specified trip to be edited + * @param attributeToEdit String of all attributes to be added and their new values + * + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + private static void editTripPerAttribute(Trip tripToEdit, String attributeToEdit) throws ForceCancelException { + String[] splitCommandAndData = attributeToEdit.split(" ", EDIT_ATTR_COUNT); + String data = splitCommandAndData[ATTRIBUTE_DATA]; + switch (splitCommandAndData[EDIT_ATTRIBUTE]) { + case EDIT_LOCATION: + String originalLocation = tripToEdit.getLocation(); + tripToEdit.setLocation(data); + Ui.changeLocationSuccessful(tripToEdit, originalLocation); + break; + case EDIT_DATE: + String originalDate = tripToEdit.getDateOfTripString(); + tripToEdit.setDateOfTrip(data); + Ui.changeDateSuccessful(tripToEdit, originalDate); + break; + case EDIT_EXRATE: + double originalExRate = tripToEdit.getExchangeRate(); + tripToEdit.setExchangeRate(data); + Ui.changeExchangeRateSuccessful(tripToEdit, originalExRate); + break; + case EDIT_HOMECUR: + String originalHomeCurrency = tripToEdit.getRepaymentCurrency(); + tripToEdit.setRepaymentCurrency(data); + Ui.changeHomeCurrencySuccessful(tripToEdit, originalHomeCurrency); + break; + case EDIT_FORCUR: + String originalForeignCurrency = tripToEdit.getForeignCurrency(); + tripToEdit.setForeignCurrency(data); + Ui.changeForeignCurrencySuccessful(tripToEdit, originalForeignCurrency); + break; + default: + System.out.println(splitCommandAndData[EDIT_ATTRIBUTE] + " was not recognised. " + + "Please try again after this process is complete"); + } + } + + /** + * Gets the trip to be edited from the user-entered index. + * + * @param tripIndexInString index of trip to be edited, as a {@link String} to be parsed. + * @return the {@link Trip} object to be edited. + */ + private static Trip editTripWithIndex(String tripIndexInString) { + int indexToEdit = Integer.parseInt(tripIndexInString) - 1; + Trip tripToEdit = Storage.getListOfTrips().get(indexToEdit); + Storage.setLastTrip(tripToEdit); + return tripToEdit; + } + //@@author + + public static boolean isNumeric(String secondCommand) { + try { + Integer.parseInt(secondCommand); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + //@@author joshualeeky + private static void correctBalances(Expense expense) { + Person payer = expense.getPayer(); + for (Person person : expense.getPersonsList()) { + if (person == payer) { + payer.setMoneyOwed(payer, -expense.getAmountSplit().get(person.getName())); + continue; + } + payer.setMoneyOwed(person, -expense.getAmountSplit().get(person.getName())); + person.setMoneyOwed(payer, expense.getAmountSplit().get(person.getName())); + person.setMoneyOwed(person, -expense.getAmountSplit().get(person.getName())); + } + } + + private static Person getValidPersonInTripFromString(String name, Trip trip) { + for (Person person : trip.getListOfPersons()) { + if (name.equalsIgnoreCase(person.getName())) { + return person; + } + } + return null; + } + //@@author + + //@author leeyikai + + /** + * Gets the necessary information and carry out the optimized payment function. When finished optimizing, + * this method will call the appropriate method in {@link Ui} and print the optimized transactions out. + * + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + private static void checkForOptimization() throws ForceCancelException { + Trip trip = Storage.getOpenTrip(); + PaymentOptimizer.optimizePayments(trip); + Ui.printOptimizedAmounts(); + } + //@@author +} diff --git a/src/main/java/seedu/duke/parser/CommandHandler.java b/src/main/java/seedu/duke/parser/CommandHandler.java new file mode 100644 index 0000000000..af6bdeea20 --- /dev/null +++ b/src/main/java/seedu/duke/parser/CommandHandler.java @@ -0,0 +1,153 @@ +package seedu.duke.parser; + +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.InvalidAmountException; +import seedu.duke.exceptions.NoExpensesException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.exceptions.TripNotOpenException; +import seedu.duke.Storage; +import seedu.duke.Ui; + +import static seedu.duke.Storage.LAST_INTERACTED; + +abstract class CommandHandler extends CommandExecutor { + + //@@author yeezao + /** + * Confirms that the user entered paramaters, and calls {@link Parser#executeCreateTrip(String)}. + * + * @param inputParams attributes of the trip to be created. + */ + protected static void handleCreateTrip(String inputParams) throws ForceCancelException { + try { + assert inputParams != null; + executeCreateTrip(inputParams); + } catch (IndexOutOfBoundsException | NullPointerException e) { + Ui.printCreateFormatError(); + } catch (SameNameException e) { + Ui.sameNameInTripError(); + } + } + + protected static void handleEditTrip(String inputParams) throws ForceCancelException { + try { + assert inputParams != null; + executeEditTrip(inputParams); + } catch (NumberFormatException | IndexOutOfBoundsException | NullPointerException e) { + Ui.printEditFormatError(); + } + } + + protected static void handleOpenTrip(String inputParams) throws ForceCancelException { + try { + assert inputParams != null; + executeOpen(inputParams); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + Ui.printSingleUnknownTripIndexError(); + System.out.println(); + } catch (NullPointerException e) { + Ui.emptyArgForOpenCommand(); + } + } + //@@author + + protected static void handleTripSummary(String inputParams) throws ForceCancelException { + try { + executeSummary(inputParams); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printUnknownTripIndexError(); + } + } + + protected static void handleViewTrip(String inputParams) throws ForceCancelException { + try { + executeView(inputParams); + } catch (ArrayIndexOutOfBoundsException e) { + Ui.printFilterFormatError(); + } + } + + //@@author yeezao + protected static void handleDelete(String inputParams) throws ForceCancelException { + try { + assert inputParams != null; + executeDelete(inputParams); + } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { + if (Storage.checkOpenTrip()) { + Ui.printUnknownExpenseIndexError(); + } else { + Ui.printUnknownTripIndexError(); + } + } catch (IndexOutOfBoundsException e) { + if (Storage.checkOpenTrip()) { + if (inputParams.equalsIgnoreCase(LAST_INTERACTED)) { + Ui.noRecentExpenseError(); + } else { + Ui.printUnknownExpenseIndexError(); + } + } else { + if (inputParams.equalsIgnoreCase(LAST_INTERACTED)) { + Ui.printNoLastTripError(); + } else { + Ui.printUnknownTripIndexError(); + } + } + } catch (NullPointerException e) { + if (!Storage.checkOpenTrip()) { + Ui.emptyArgForDeleteTripCommand(); + } else { + Ui.emptyArgForDeleteExpenseCommand(); + } + } + } + //@@author + + protected static void handleList() throws ForceCancelException { + executeList(); + } + + /** + * Confirms that the user had entered parameters for creating a new expense, and redirects to + * {@link Parser#executeCreateExpense(String)} to create the expense. + * + * @param inputParams attributes of expense to be created. + */ + protected static void handleCreateExpense(String inputParams) throws ForceCancelException { + try { + assert inputParams != null; + executeCreateExpense(inputParams); + } catch (NullPointerException | IndexOutOfBoundsException | NumberFormatException e) { + Ui.printExpenseFormatError(); + } + } + + protected static void handleAmount(String inputParams) throws ForceCancelException { + try { + executeAmount(inputParams); + } catch (NullPointerException e) { + Ui.invalidAmountFormat(); + } + } + + + /** + * Prints out the list of people in the trip if trip is open. + * Otherwise, informs the user no trip open. + */ + protected static void handlePeople() throws ForceCancelException { + try { + executePeople(); + } catch (TripNotOpenException e) { + Ui.printNoOpenTripError(); + } + } + + protected static void handleOptimize() throws ForceCancelException { + try { + executeOptimize(); + } catch (NoExpensesException e) { + Ui.printNoExpensesError(); + } + } +} + diff --git a/src/main/java/seedu/duke/parser/ExpenseSummarizer.java b/src/main/java/seedu/duke/parser/ExpenseSummarizer.java new file mode 100644 index 0000000000..9b410a6883 --- /dev/null +++ b/src/main/java/seedu/duke/parser/ExpenseSummarizer.java @@ -0,0 +1,93 @@ +package seedu.duke.parser; + +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.trip.Trip; +import seedu.duke.Ui; +import seedu.duke.expense.Expense; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +//@@author itsleeqian +public interface ExpenseSummarizer { + + /** + * Prints the summary of expenses in a trip for a Person. + * @param person The person to print for. + * @throws ForceCancelException allows the user to cancel an operation when an input is required. + */ + static void getIndividualExpenseSummary(Person person) throws ForceCancelException { + double currentAmount; //amount paid for current expense + double totalAmountSpent = 0; + double totalAmountSpentInLocalCurrency = 0; + Trip currTrip = Storage.getOpenTrip(); + ArrayList listOfExpenses = currTrip.getListOfExpenses(); + int expensesInvolved = 0; //num of expenses involved + HashMap categoriesSplit = new HashMap<>(); //contains the amount spent in each category + for (Expense e : listOfExpenses) { + if (containsPerson(e.getPersonsList(), person.getName())) { + currentAmount = e.getAmountSplit().get(person.getName()); + String currentCategory = e.getCategory(); + totalAmountSpent += currentAmount; + expensesInvolved++; + //the following if else is to update the category/amtSpent hashmap + if (!categoriesSplit.containsKey(currentCategory)) { + categoriesSplit.put(currentCategory, currentAmount); + } else { + double updatedValue = categoriesSplit.get(currentCategory) + currentAmount; + categoriesSplit.put(currentCategory, updatedValue); + } + } + } + totalAmountSpentInLocalCurrency = roundToLocal(totalAmountSpentInLocalCurrency, currTrip, categoriesSplit); + System.out.println(person + " has spent " + + Ui.stringForeignMoney(totalAmountSpent) + + " (" + currTrip.getRepaymentCurrency() + " " + + currTrip.getRepaymentCurrencySymbol() + + String.format(currTrip.getRepaymentCurrencyFormat(), totalAmountSpentInLocalCurrency) + ") on " + + expensesInvolved + + " expenses on the following categories:"); + for (Map.Entry set : categoriesSplit.entrySet()) { + System.out.println(set.getKey() + ": " + Ui.stringForeignMoney(set.getValue()) + + " (" + Ui.stringRepaymentMoney(set.getValue()) + ")"); + } + } + + /** + * Helper method for getIndividualExpenseSummary() method. + * Returns the rounded and formatted total repayment amount spent. + * @param totalAmount the amount before rounding + * @param currTrip the Trip the user is in/computing + * @param categoriesSplit the HashMap containing the category and the amount spent on said category + * @return a rounded and formatted value for amount spent in local currency + */ + private static double roundToLocal(double totalAmount, Trip currTrip, HashMap categoriesSplit) + throws ForceCancelException { + for (Map.Entry set : categoriesSplit.entrySet()) { + totalAmount += Storage.formatRepaymentMoneyDouble( + set.getValue() / currTrip.getExchangeRate()); + } + return totalAmount; + } + + /** + * Returns true if personsList contains a person with a specific name. + * This is to replace the list.contains() method due to bugs with json deserialization. + * + * @param personsList list of persons to check + * @param name the name to check for + * @return true if personsList contains a person with a specific name + */ + private static boolean containsPerson(ArrayList personsList, String name) { + for (Person person : personsList) { + if (person.getName().equals(name)) { + return true; + } + } + return false; + } + //@@author +} diff --git a/src/main/java/seedu/duke/parser/Parser.java b/src/main/java/seedu/duke/parser/Parser.java new file mode 100644 index 0000000000..7f740d8da7 --- /dev/null +++ b/src/main/java/seedu/duke/parser/Parser.java @@ -0,0 +1,220 @@ +package seedu.duke.parser; + +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.Storage; +import seedu.duke.Ui; + +import java.time.LocalDate; +import java.util.logging.Level; + +public class Parser extends CommandHandler { + + private static final int SPLIT_COMMAND_FROM_INFO_LENGTH = 2; + private static final int INPUT_COMMAND = 0; + private static final int INPUT_INFO = 1; + + private static final String QUIT_COMMAND = "quit"; + private static final String CLOSE_COMMAND = "close"; + + /** + * Parses the user-entered command and additional information/flags. + * + * @param userInput the {@link String} containing the user input + * @return whether the program should continue running after processing the given user input + */ + public static boolean parseUserInput(String userInput) { + + String[] rawInput = userInput.split(" ", SPLIT_COMMAND_FROM_INFO_LENGTH); + String inputCommand = rawInput[INPUT_COMMAND].toLowerCase(); + String inputParams = null; + + if (rawInput.length == SPLIT_COMMAND_FROM_INFO_LENGTH) { + inputParams = rawInput[INPUT_INFO]; + } + + try { + if (inputCommand.equals(QUIT_COMMAND)) { + Ui.goodBye(); + return false; + } else if (inputCommand.equals(CLOSE_COMMAND)) { + Storage.closeTrip(); + return true; + } else if (inputCommand.equals(HELP_COMMAND)) { + Ui.displayHelp(); + return true; + } else if (!checkValidCommand(inputCommand)) { + Storage.getLogger().log(Level.WARNING, "invalid user input"); + Ui.printUnknownCommandError(); + return true; + } else if (Storage.getListOfTrips().isEmpty() && !inputCommand.equals(CREATE_COMMAND)) { + Storage.getLogger().log(Level.WARNING, "No trip created yet"); + Ui.printNoTripError(); + return true; + } else { + handleValidCommands(inputCommand, inputParams); + return true; + } + } catch (NullPointerException e) { + Ui.printNoOpenTripError(); + return true; + } catch (ForceCancelException e) { + Ui.printForceCancelled(); + return true; + } + } + + private static final String CREATE_COMMAND = "create"; + private static final String EDIT_COMMAND = "edit"; + private static final String OPEN_COMMAND = "open"; + private static final String SUMMARY_COMMAND = "summary"; + private static final String VIEW_COMMAND = "view"; + private static final String DELETE_COMMAND = "delete"; + private static final String LIST_COMMAND = "list"; + private static final String EXPENSE_COMMAND = "expense"; + private static final String AMOUNT_COMMAND = "amount"; + private static final String HELP_COMMAND = "help"; + private static final String PEOPLE_COMMAND = "people"; + private static final String OPTIMIZE_COMMAND = "optimize"; + + /** + * Handles commands entered by the user that are confirmed as valid, and redirects to the appropriate method + * for further updates. + * + * @param inputCommand Valid command executed by the user. + * @param inputParams Additional information appended to the command by the user + * (inputParams are not checked and may not be valid). + * + * @see Parser#parseUserInput(String) + */ + private static void handleValidCommands(String inputCommand, String inputParams) throws ForceCancelException { + + switch (inputCommand) { + case CREATE_COMMAND: + handleCreateTrip(inputParams); + break; + + case EDIT_COMMAND: + handleEditTrip(inputParams); + break; + + case OPEN_COMMAND: + handleOpenTrip(inputParams); + break; + + case SUMMARY_COMMAND: + handleTripSummary(inputParams); + break; + + case VIEW_COMMAND: + handleViewTrip(inputParams); + break; + + case DELETE_COMMAND: + handleDelete(inputParams); + break; + + case LIST_COMMAND: + handleList(); + break; + + case EXPENSE_COMMAND: + handleCreateExpense(inputParams); + break; + + case AMOUNT_COMMAND: + handleAmount(inputParams); + break; + + case PEOPLE_COMMAND: + handlePeople(); + break; + + case OPTIMIZE_COMMAND: + handleOptimize(); + break; + + default: + Ui.printUnknownCommandError(); + } + } + + //@@author lixiyuan416 + + /** + * Helper function to get user to confirm y/n. + * + * @return true if user agrees, false otherwise + */ + public static boolean getUserToConfirm() { + boolean isValidInput = false; + boolean doesUserAgree = false; + while (!isValidInput) { + String userReply = Storage.getScanner().nextLine(); + if (userReply.strip().equalsIgnoreCase(Ui.USER_CONTINUE)) { + isValidInput = true; + doesUserAgree = true; + } else if (userReply.strip().equalsIgnoreCase(Ui.USER_QUIT)) { + isValidInput = true; + } else { + System.out.println("Enter y/n"); + } + } + return doesUserAgree; + } + //@@author + + private static boolean checkValidCommand(String inputCommand) { + return Storage.getValidCommands().contains(inputCommand); + } + + //@@author yeezao + public static boolean doesUserWantToForceCancel(String userInput) { + return userInput.equals("-cancel"); + } + + private static final int DAYDD = 0; + private static final int MONTHMM = 1; + private static final int YEARYYYY = 2; + + /** + * Checks if the user-entered date is a date that actually exists. + * + * @param dateInString the entire dd-mm-yyyy date as a single string. + * @return true if the date actually exists. + */ + public static boolean doesDateReallyExist(String dateInString) { + String[] dateSplitUp = dateInString.split("-"); + int day; + int month; + int year; + try { + day = Integer.parseInt(dateSplitUp[DAYDD]); + month = Integer.parseInt(dateSplitUp[MONTHMM]); + year = Integer.parseInt(dateSplitUp[YEARYYYY]); + } catch (NumberFormatException e) { + return false; + } + + //definitely an invalid date + if (day < 1 || day > 31 || month < 1 || month > 12) { + return false; + } + //for months with 30 days + if ((month < 7 && month % 2 == 0) || (month >= 8 && month % 2 == 1)) { + if (day > 30) { + return false; + } + } + //leap year checks + if (month == 2) { + LocalDate leapYearCheck = LocalDate.of(year, 1, 1); + if (!leapYearCheck.isLeapYear() && day > 28) { + return false; + } else { + return (!leapYearCheck.isLeapYear() || day <= 29); + } + } + return true; + } + +} diff --git a/src/main/java/seedu/duke/parser/PaymentOptimizer.java b/src/main/java/seedu/duke/parser/PaymentOptimizer.java new file mode 100644 index 0000000000..6f9922bcc8 --- /dev/null +++ b/src/main/java/seedu/duke/parser/PaymentOptimizer.java @@ -0,0 +1,153 @@ +//@@author leeyikai + +package seedu.duke.parser; + +import seedu.duke.Person; +import seedu.duke.trip.Trip; + +import java.util.ArrayList; +import java.util.HashMap; + +class PaymentOptimizer { + static double EPSILON = 0.001; + + /** + * Calls different methods that helps to calculate the optimized payments. + * + * @param trip {@link Trip} that you want to optimize the payments for. + */ + static void optimizePayments(Trip trip) { + ArrayList listOfPersons = trip.getListOfPersons(); + ArrayList totalExpenses = new ArrayList<>(); + boolean isAllPaid = false; + getTotalAmountForPerson(totalExpenses, listOfPersons); + int currentIndex; + while (!isAllPaid) { + for (Person person : listOfPersons) { + currentIndex = listOfPersons.indexOf(person); + if (totalExpenses.get(currentIndex) < 0) { + findNextPersonToPay(totalExpenses, currentIndex, listOfPersons); + } + } + isAllPaid = checkIfAllPaid(totalExpenses); + } + } + + /** + * Checks if every {@link Person} in the currently opened {@link Trip} has been paid. + * + * @param totalExpenses {@link ArrayList} containing the net total expenses of each person. + * @return true if every {@link Person} in the current {@link Trip} has been paid. + */ + private static boolean checkIfAllPaid(ArrayList totalExpenses) { + for (Double i : totalExpenses) { + if (!isZero(i)) { + return false; + } + } + return true; + } + + /** + * Finds the next {@link Person} to pay that is being owed money. Returns only when every single {@link Person} + * in the {@link Trip} has been paid. + * + * @param totalExpenses {@link ArrayList} containing the net total expenses of each person. + * @param indexOfPersonPaying index of the person who will be paying. + * @param listOfPersons list of persons in the currently opened trip + */ + private static void findNextPersonToPay(ArrayList totalExpenses, int indexOfPersonPaying, + ArrayList listOfPersons) { + Double expensesOfCurrentPerson; + Double expensesOfPersonPaying; + Person personPaying; + Person personReceiving; + String nameOfPersonPaying; + String nameOfPersonReceiving; + for (Person person : listOfPersons) { + int indexOfPersonReceiving = listOfPersons.indexOf(person); + expensesOfCurrentPerson = totalExpenses.get(indexOfPersonReceiving); + expensesOfPersonPaying = -totalExpenses.get(indexOfPersonPaying); + personPaying = listOfPersons.get(indexOfPersonPaying); + personReceiving = listOfPersons.get(indexOfPersonReceiving); + nameOfPersonPaying = personPaying.getName(); + nameOfPersonReceiving = personReceiving.getName(); + if (totalExpenses.get(indexOfPersonReceiving) > 0) { + if (isMoreThanOrEqual(expensesOfPersonPaying, expensesOfCurrentPerson)) { + personPaying.getOptimizedMoneyOwed().put(nameOfPersonReceiving, -expensesOfCurrentPerson); + personReceiving.getOptimizedMoneyOwed().put(nameOfPersonPaying, expensesOfCurrentPerson); + totalExpenses.set(indexOfPersonReceiving, 0.0); + totalExpenses.set(indexOfPersonPaying, -(expensesOfPersonPaying - expensesOfCurrentPerson)); + } else { + personPaying.getOptimizedMoneyOwed().put(nameOfPersonReceiving, -expensesOfPersonPaying); + personReceiving.getOptimizedMoneyOwed().put(nameOfPersonPaying, expensesOfPersonPaying); + totalExpenses.set(indexOfPersonPaying, 0.0); + totalExpenses.set(indexOfPersonReceiving, expensesOfCurrentPerson - expensesOfPersonPaying); + } + } + if (isZero(totalExpenses.get(indexOfPersonPaying))) { + return; + } + } + } + + /** + * Gets the net total of expenses for every {@link Person} in {@link Trip}. + * + * @param totalExpenses {@link ArrayList} containing the net total of each {@link Person} in the open {@link Trip}. + * @param listOfPersons {@link ArrayList} of {@link Person} in the currently open {@link Trip}. + */ + private static void getTotalAmountForPerson(ArrayList totalExpenses, ArrayList listOfPersons) { + Double totalAmountPerPerson; + for (Person person : listOfPersons) { + totalAmountPerPerson = 0.0; + HashMap personExpenses = person.getMoneyOwed(); + String otherPersonName; + for (Person otherPerson : listOfPersons) { + if (!otherPerson.equals(person)) { + otherPersonName = otherPerson.getName(); + totalAmountPerPerson += personExpenses.get(otherPersonName); + person.setOptimizedMoneyOwed(otherPerson); + } + + } + totalExpenses.add(totalAmountPerPerson); + } + } + + + /** + * Checks if {@param firstValue} is larger or equal to {@param secondValue}. + * + * @param firstValue bigger or equal value that we want to compare + * @param secondValue smaller or equal value that we want to compare + * @return {@link Boolean} that is true if {@param firstValue} is greater or equal to {@param secondValue} + */ + private static boolean isMoreThanOrEqual(double firstValue, double secondValue) { + if (isEqual(firstValue, secondValue)) { + return true; + } + return firstValue > secondValue; + } + + /** + * Checks if {@param firstValue} is equal to {@param secondValue}. + * @param firstValue first value that we want to check. + * @param secondValue second value that we want to check against. + * @return true if {@param firstValue} is equals to {@param secondValue}. + */ + private static boolean isEqual(double firstValue, double secondValue) { + double difference = firstValue - secondValue; + return difference < EPSILON && difference > -EPSILON; + } + + /** + * Checks if {@param value} is zero. + * @param value value that we want to check. + * @return true if value is 0. + */ + private static boolean isZero(double value) { + return value < EPSILON && value > -EPSILON; + } +} +//@@author diff --git a/src/main/java/seedu/duke/trip/FilterFinder.java b/src/main/java/seedu/duke/trip/FilterFinder.java new file mode 100644 index 0000000000..c6924fc00b --- /dev/null +++ b/src/main/java/seedu/duke/trip/FilterFinder.java @@ -0,0 +1,167 @@ +package seedu.duke.trip; + +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.Ui; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.expense.Expense; +import seedu.duke.parser.Parser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.logging.Level; + +public interface FilterFinder { + + DateTimeFormatter inputPattern = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + //@@author lixiyuan416 + + /** + * Filters expenses inside a trip by date. + * See parent method {@link Trip#getFilteredExpenses(String, String)} + * + * @param listOfCurrentExpenses expense list of a trip + * @param expenseAttribute date to search for + * @throws ForceCancelException when user inputs command to cancel the view filter operation + */ + static void findMatchingDateExpenses(ArrayList listOfCurrentExpenses, String expenseAttribute) + throws ForceCancelException { + boolean areThereExpenses = false; + String inputDate = expenseAttribute; + while (!isDateValid(inputDate)) { + inputDate = Ui.receiveUserInput(); + } + LocalDate dateToFind = LocalDate.parse(inputDate, inputPattern); + for (Expense e : listOfCurrentExpenses) { + if (e.getDate().isEqual(dateToFind)) { + int index = listOfCurrentExpenses.indexOf(e); + Ui.printFilteredExpenses(e, index); + areThereExpenses = true; + } + } + if (!areThereExpenses) { + Ui.printNoMatchingExpenseError(); + } + } + //@@author + + //@@author leeyikai + + /** + * Finds the {@link Expense} that has the same {@link Person} name as {@param expenseAttribute} and prints + * the expense. If there are no matching {@link Expense}, an error message will be printed. + * + * @param listOfCurrentExpenses {@link ArrayList} containing the list of expenses currently in the trip. + * @param expenseAttribute @{@link String} containing the name of the {@link Person} that we want to find. + */ + static void findMatchingPayerExpenses(ArrayList listOfCurrentExpenses, String expenseAttribute) { + boolean areThereExpenses = false; + for (Expense e : listOfCurrentExpenses) { + if (e.getPayer().getName().equalsIgnoreCase(expenseAttribute)) { + int index = listOfCurrentExpenses.indexOf(e); + Ui.printFilteredExpenses(e, index); + areThereExpenses = true; + } + } + if (!areThereExpenses) { + Ui.printNoMatchingExpenseError(); + } + } + + /** + * Finds the {@link Expense} that contains the description as {@param expenseAttribute} and prints it out. + * If there are no matching {@link Expense}, an error message will be printed. + * + * @param listOfCurrentExpenses {@link ArrayList} containing the list of expenses currently in the trip. + * @param expenseAttribute @{@link String} containing the description that we want to find. + */ + static void findMatchingDescriptionExpenses(ArrayList listOfCurrentExpenses, + String expenseAttribute) { + boolean areThereExpenses = false; + String descriptionToLowerCase; + String attributeToLowerCase = expenseAttribute.toLowerCase(); + for (Expense e : listOfCurrentExpenses) { + descriptionToLowerCase = e.getDescription().toLowerCase(); + if (descriptionToLowerCase.contains(attributeToLowerCase)) { + int index = listOfCurrentExpenses.indexOf(e); + Ui.printFilteredExpenses(e, index); + areThereExpenses = true; + } + } + if (!areThereExpenses) { + Ui.printNoMatchingExpenseError(); + } + } + + /** + * Finds the {@link Expense} that contains the category as {@param expenseAttribute} and prints it out. + * If there are no matching {@link Expense}, an error message will be printed. + * + * @param listOfCurrentExpenses {@link ArrayList} containing the list of expenses currently in the trip. + * @param expenseAttribute @{@link String} containing the category that we want to find. + */ + static void findMatchingCategoryExpenses(ArrayList listOfCurrentExpenses, + String expenseAttribute) { + boolean areThereExpenses = false; + for (Expense e : listOfCurrentExpenses) { + if (e.getCategory().equalsIgnoreCase(expenseAttribute)) { + int index = listOfCurrentExpenses.indexOf(e); + Ui.printFilteredExpenses(e, index); + areThereExpenses = true; + } + } + if (!areThereExpenses) { + Ui.printNoMatchingExpenseError(); + } + } + //@@author + + //@@author lixiyuan416 + + /** + * Filters expenses inside a trip by person. + * See parent method {@link Trip#getFilteredExpenses(String, String)} + * + * @param listOfCurrentExpenses list of expenses of a trip + * @param personToSearchFor name of person + */ + static void findMatchingPersonExpenses(ArrayList listOfCurrentExpenses, + String personToSearchFor) { + boolean areThereExpenses = false; + for (Expense e : listOfCurrentExpenses) { + boolean isExpenseToBeAdded = false; + ArrayList personList = e.getPersonsList(); + for (Person p : personList) { + if (p.getName().equalsIgnoreCase(personToSearchFor)) { + isExpenseToBeAdded = true; + break; + } + } + if (isExpenseToBeAdded) { + int index = listOfCurrentExpenses.indexOf(e); + Ui.printFilteredExpenses(e, index); + areThereExpenses = true; + } + } + if (!areThereExpenses) { + Ui.printNoMatchingExpenseError(); + } + } + + private static boolean isDateValid(String inputDate) { + try { + if (Parser.doesDateReallyExist(inputDate)) { + LocalDate.parse(inputDate, inputPattern); + return true; + } + return false; + } catch (DateTimeParseException e) { + Storage.getLogger().log(Level.INFO, "Invalid date format entered"); + Ui.viewFilterDateFormatInvalid(); + return false; + } + } +} diff --git a/src/main/java/seedu/duke/trip/Trip.java b/src/main/java/seedu/duke/trip/Trip.java new file mode 100644 index 0000000000..af4a44bfaf --- /dev/null +++ b/src/main/java/seedu/duke/trip/Trip.java @@ -0,0 +1,370 @@ +package seedu.duke.trip; + +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.Ui; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.expense.Expense; +import seedu.duke.parser.Parser; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Comparator; +import java.util.ArrayList; + +import static seedu.duke.parser.Parser.isNumeric; + +public class Trip implements FilterFinder { + + private LocalDate dateOfTrip; + private ArrayList listOfExpenses = new ArrayList<>(); + private ArrayList listOfPersons = new ArrayList<>(); + private double exchangeRate; + private String foreignCurrency; + private String foreignCurrencyFormat; + private String foreignCurrencySymbol; + private String repaymentCurrency = "SGD"; + private String repaymentCurrencyFormat = "%.02f"; + private String repaymentCurrencySymbol = "$"; + private String location; + private Expense lastExpense = null; + private static final int ISO_LENGTH = 3; + private static final String CATEGORY = "category"; + private static final String DESCRIPTION = "description"; + private static final String PAYER = "payer"; + private static final String PERSON = "person"; + private static final String DATE = "date"; + + //@@author yeezao + /** + * Legacy constructor for Trip. Used as stub for testing. + */ + public Trip() { + //empty constructor + } + + private static final int LOCATION_STRING = 1; + private static final int DATE_STRING = 2; + private static final int FORCUR_STRING = 3; + private static final int EXRATE_STRING = 4; + private static final int PERSONS_STRING = 5; + + /** + * Non-empty {@link Trip} constructor. Reads in a String array and processes it to set attributes for a given Trip. + * + * @param newTripInfo array containing one attribute in each element + */ + public Trip(String[] newTripInfo) throws ForceCancelException, SameNameException { + assert newTripInfo.length == 6; + setLocation(newTripInfo[LOCATION_STRING].strip()); + setDateOfTrip(newTripInfo[DATE_STRING].strip()); + setForeignCurrency(newTripInfo[FORCUR_STRING].strip().toUpperCase()); + setExchangeRate(newTripInfo[EXRATE_STRING].strip()); + setListOfPersons(splitPeople(newTripInfo[PERSONS_STRING])); + } + + //@@author leeyikai + public void getFilteredExpenses(String expenseCategory, String expenseAttribute) { + + if (listOfExpenses.isEmpty()) { + Ui.printNoExpensesError(); + return; + } + try { + switch (expenseCategory) { + case CATEGORY: + FilterFinder.findMatchingCategoryExpenses(listOfExpenses, expenseAttribute); + break; + case DESCRIPTION: + FilterFinder.findMatchingDescriptionExpenses(listOfExpenses, expenseAttribute); + break; + case PAYER: + FilterFinder.findMatchingPayerExpenses(listOfExpenses, expenseAttribute); + break; + case PERSON: + FilterFinder.findMatchingPersonExpenses(listOfExpenses, expenseAttribute); + break; + case DATE: + FilterFinder.findMatchingDateExpenses(listOfExpenses, expenseAttribute); + break; + default: + Ui.printInvalidFilterError(); + break; + } + + } catch (IndexOutOfBoundsException e) { + Ui.printFilterFormatError(); + } catch (ForceCancelException e) { + Ui.printForceCancelled(); + } + + } + + //@@author yeezao + public LocalDate getDateOfTrip() { + return dateOfTrip; + } + + /** + * Returns the {@link LocalDate} object as a formatted string (with the format dd MMMM yy). + * + * @return the formatted date as a {@link String}. + */ + public String getDateOfTripString() { + DateTimeFormatter pattern = DateTimeFormatter.ofPattern("dd MMM yyyy"); + return getDateOfTrip().format(pattern); + } + + /** + * Parses a user input (in {@link String}) into a {@link LocalDate}. + * + * @param dateOfTrip user-entered date of trip as a String + */ + public void setDateOfTrip(String dateOfTrip) throws ForceCancelException { + DateTimeFormatter pattern = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + try { + LocalDate date = LocalDate.parse(dateOfTrip, pattern); + if (Parser.doesDateReallyExist(dateOfTrip)) { + if (date.isBefore(LocalDate.EPOCH)) { + dateInvalid(); + } else { + this.dateOfTrip = date; + } + } else { + dateInvalid(); + } + } catch (DateTimeParseException e) { + Ui.printDateTimeFormatError(); + userInputDateError(); + } + } + + private void dateInvalid() throws ForceCancelException { + Ui.dateInvalidError(); + userInputDateError(); + } + + private void userInputDateError() throws ForceCancelException { + String newInput = Ui.receiveUserInput(); + setDateOfTrip(newInput); + } + + + public double getExchangeRate() { + return exchangeRate; + } + + + /** + * Parses an exchange rate entered by the user (as a {@link String}) into a {@link Double}. + * + * @param exchangeRateString user-entered exchange rate (as a String) + */ + public void setExchangeRate(String exchangeRateString) throws ForceCancelException { + try { + this.exchangeRate = Double.parseDouble(exchangeRateString); + } catch (NumberFormatException e) { + Ui.printExchangeRateFormatError(); + String userInput = Ui.receiveUserInput(); + setExchangeRate(userInput); + } + } + + //@@author itsleeqian + public void setForeignCurrency(String foreignCurrency) throws ForceCancelException { + try { + if (isNumeric(foreignCurrency) || foreignCurrency.length() != ISO_LENGTH) { + throw new NumberFormatException(); + } + this.foreignCurrency = foreignCurrency; + setForeignCurrencyFormat(this.foreignCurrency); + setForeignCurrencySymbol(this.foreignCurrency); + } catch (NumberFormatException e) { + Ui.printIsoFormatError(); + String userInput = Ui.receiveUserInput(); + setForeignCurrency(userInput); + } + } + + //@@author joshualeeky + public String getForeignCurrency() { + return foreignCurrency; + } + + private void setForeignCurrencyFormat(String input) { + if (Storage.getAvailableCurrency().containsKey(input)) { + this.foreignCurrencyFormat = Storage.getAvailableCurrency().get(input)[1]; + } else { + this.foreignCurrencyFormat = "%.02f"; + } + } + + private void setForeignCurrencySymbol(String input) { + if (Storage.getAvailableCurrency().containsKey(input)) { + this.foreignCurrencySymbol = Storage.getAvailableCurrency().get(input)[0]; + } else { + this.foreignCurrencySymbol = ""; + } + } + + public String getForeignCurrencyFormat() { + return foreignCurrencyFormat; + } + + public String getForeignCurrencySymbol() { + return foreignCurrencySymbol; + } + + public String getRepaymentCurrency() { + return repaymentCurrency; + } + + //@@author itsleeqian + public void setRepaymentCurrency(String repaymentCurrency) throws ForceCancelException { + try { + if (isNumeric(repaymentCurrency) || repaymentCurrency.length() != ISO_LENGTH) { + throw new NumberFormatException(); + } + this.repaymentCurrency = repaymentCurrency.toUpperCase(); + setRepaymentCurrencyFormat(this.repaymentCurrency); + setRepaymentCurrencySymbol(this.repaymentCurrency); + } catch (NumberFormatException e) { + Ui.printIsoFormatError(); + String userInput = Ui.receiveUserInput(); + setRepaymentCurrency(userInput); + } + } + + private void setRepaymentCurrencyFormat(String input) { + if (Storage.getAvailableCurrency().containsKey(input)) { + this.repaymentCurrencyFormat = Storage.getAvailableCurrency().get(input)[1]; + } else { + this.repaymentCurrencyFormat = "%.02f"; + } + } + + public String getRepaymentCurrencyFormat() { + return repaymentCurrencyFormat; + } + + public String getRepaymentCurrencySymbol() { + return repaymentCurrencySymbol; + } + + private void setRepaymentCurrencySymbol(String input) { + if (Storage.getAvailableCurrency().containsKey(input)) { + this.repaymentCurrencySymbol = Storage.getAvailableCurrency().get(input)[0]; + } else { + this.repaymentCurrencySymbol = ""; + } + } + //@@author + + //@@author yeezao + public String getLocation() { + return this.location; + } + + + public void setListOfPersons(ArrayList listOfPersons) throws ForceCancelException, SameNameException { + if (listOfPersons.isEmpty()) { + Ui.noPersonsAdded(); + String userInput = Ui.receiveUserInput(); + setListOfPersons(splitPeople(userInput)); + return; + } + this.listOfPersons = listOfPersons; + } + + public ArrayList getListOfPersons() { + return listOfPersons; + } + + public void setLocation(String location) throws ForceCancelException { + if (location.isBlank()) { + Ui.locationIsBlank(); + setLocation(Ui.receiveUserInput()); + } else { + this.location = location; + } + } + + public Expense getLastExpense() { + return lastExpense; + } + + public void setLastExpense(Expense lastExpense) { + this.lastExpense = lastExpense; + } + + public void setPersonName(int indexOfPerson, String newName) { + Person personToEdit = listOfPersons.get(indexOfPerson); + personToEdit.setName(newName); + } + //@@author + + public void addExpense(Expense expense) { + listOfExpenses.add(expense); + listOfExpenses.sort(Comparator.comparing(Expense::getDate)); + } + + public ArrayList getListOfExpenses() { + return listOfExpenses; + } + + public void removeExpense(Integer i) { + listOfExpenses.remove(listOfExpenses.get(i)); + } + + public void viewAllExpenses() { + if (listOfExpenses.isEmpty()) { + Ui.printNoExpensesError(); + } else { + System.out.println("List of all Expenses in detail: "); + for (Expense expense : listOfExpenses) { + System.out.print(listOfExpenses.indexOf(expense) + 1 + ". "); + Ui.printExpenseDetails(expense); + } + } + } + + + public Expense getExpenseAtIndex(Integer index) { + return listOfExpenses.get(index - 1); + } + + + //@@author joshualeeky + + /** + * Splits the user-entered {@link String} of people involved in a trip into a String array. + * + * @param peopleChained String of all persons involved in the trip + * @return {@link String} array, each element of the array being a person involved in the trip + */ + private ArrayList splitPeople(String peopleChained) throws SameNameException { + ArrayList listOfPeopleNames = new ArrayList<>(); + ArrayList listOfPeopleNamesUpperCased = new ArrayList<>(); + ArrayList listOfPeople = new ArrayList<>(); + for (String personName : peopleChained.split(",")) { + if (listOfPeopleNamesUpperCased.contains(personName.strip().toUpperCase())) { + throw new SameNameException(); + } else if (!personName.isBlank()) { + listOfPeopleNames.add(personName.strip()); + listOfPeopleNamesUpperCased.add(personName.strip().toUpperCase()); + } + } + for (String name : listOfPeopleNames) { + Person person = new Person(name); + listOfPeople.add(person); + } + for (Person person : listOfPeople) { + for (Person personToAdd : listOfPeople) { + person.getMoneyOwed().put(personToAdd.getName(), 0.0); + } + } + return listOfPeople; + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} diff --git a/src/test/java/seedu/duke/ExpenseTest.java b/src/test/java/seedu/duke/ExpenseTest.java new file mode 100644 index 0000000000..41c8b97d1a --- /dev/null +++ b/src/test/java/seedu/duke/ExpenseTest.java @@ -0,0 +1,422 @@ +package seedu.duke; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.expense.Expense; +import seedu.duke.parser.Parser; +import seedu.duke.trip.FilterFinder; +import seedu.duke.trip.Trip; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.ByteArrayInputStream; +import java.time.LocalDate; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +//@@ joshualeeky +class ExpenseTest { + static Expense exp; + static Trip trip; + + @BeforeAll + static void setUp() throws ForceCancelException, SameNameException { + Logger logger = Logger.getLogger("ProgramLogger"); + logger.setLevel(Level.OFF); + Storage.setLogger(logger); + String[] stringArray = {"", "USA", "01-12-2020", "USD", "0.74", "Albert, Betty, Chris, Don, Evan"}; + trip = new Trip(stringArray); + Storage.getListOfTrips().add(trip); + Storage.setOpenTrip(Storage.getListOfTrips().indexOf(trip)); + String input = "02-12-2020" + System.lineSeparator() + "Chris" + System.lineSeparator() + "100" + + System.lineSeparator() + "200" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + exp = new Expense("600 food Albert, Betty, Chris /Dinner at fancy restaurant"); + trip.addExpense(exp); + System.setOut(System.out); + } + + @Test + void testSetDate() { + exp.setDate("21-01-2113"); + assertEquals("21 Jan 2113", exp.getStringDate()); + } + + @Test + void testGetAmountSpent() { + assertEquals(600.0, exp.getAmountSpent()); + } + + @Test + void testGetPersonExpense() { + assertEquals("\t\t1) Albert, USD $100.00" + System.lineSeparator() + + "\t\t2) Betty, USD $200.00" + System.lineSeparator() + + "\t\t3) Chris, USD $300.00" + System.lineSeparator(), exp.getPersonExpense()); + } + + @Test + void testGetDescription() { + assertEquals("Dinner at fancy restaurant", exp.getDescription()); + } + + @Test + void testSetCategory() { + exp.setCategory("f&b"); + assertEquals("f&b", exp.getCategory()); + } + + @Test + void testGetStringDate() { + assertEquals("02 Dec 2020", exp.getStringDate()); + } + + @Test + void testSetPayer() { + Person person2 = new Person("Betty"); + exp.setPayer(person2); + assertEquals("Betty", exp.getPayer().getName()); + } + + @Test + void testGetPersonsList() { + assertEquals("[Albert, Betty, Chris]", exp.getPersonsList().toString()); + } + + @Test + void testGetAmountSplit() { + assertEquals("{Chris=300.0, Betty=200.0, Albert=100.0}", exp.getAmountSplit().toString()); + } + + @Test + void testNormalAssign() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Evan" + System.lineSeparator() + "1010" + + System.lineSeparator() + "1010" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("2113 category Chris, Don, Evan /description"); + assertEquals("Evan", testExpense.getPayer().getName()); + assertEquals(1010, testExpense.getAmountSplit().get("Chris")); + assertEquals(1010.0, testExpense.getAmountSplit().get("Don")); + assertEquals(93.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testAddAllToExpense() throws ForceCancelException { + String input = "02-12-2020" + + System.lineSeparator() + "Albert" + System.lineSeparator() + "100" + System.lineSeparator() + + "200" + System.lineSeparator() + "300" + System.lineSeparator() + "400" + System.lineSeparator() + + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("1500 category -all /description"); + assertEquals("Albert", testExpense.getPayer().getName()); + assertEquals(100.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(200.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(300.0, testExpense.getAmountSplit().get("Chris")); + assertEquals(400.0, testExpense.getAmountSplit().get("Don")); + assertEquals(500.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testDuplicatePeopleInExpense() throws ForceCancelException { + String input = "Betty, Betty" + System.lineSeparator() + "Albert, Betty, Evan" + + System.lineSeparator() + "02-12-2020" + + System.lineSeparator() + "Evan" + System.lineSeparator() + "1010" + + System.lineSeparator() + "1010" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("3000 category Albert, Albert, Evan /description"); + assertEquals("Evan", testExpense.getPayer().getName()); + assertEquals(1010.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1010.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(980.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testCurrentDate() throws ForceCancelException { + String input = System.lineSeparator(); + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("3000 category Albert /description"); + assertEquals(LocalDate.now(), testExpense.getDate()); + } + + @Test + void testInvalidDates() throws ForceCancelException { + String input = "29-02-2021" + System.lineSeparator() + + "00-11-2021" + System.lineSeparator() + + "25-00-2021" + System.lineSeparator() + + "16-23-2021" + System.lineSeparator() + + System.lineSeparator() + "Albert" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("3000 category Albert, Betty, Evan /description"); + assertEquals("Albert", testExpense.getPayer().getName()); + assertEquals(1000.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1000.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(1000.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testInvalidAmountNotNumber() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String expectedOutput = "Please format your inputs as follows: " + + System.lineSeparator() + "expense [amount] [category] [people] /[description]." + + System.lineSeparator(); + Parser.parseUserInput("expense notNumber category Albert, Betty, Evan /description"); + assertEquals(actualOutput.toString(), expectedOutput); + } + + @Test + void testInvalidAmountNotPositiveNumber() throws ForceCancelException { + String input = "0" + System.lineSeparator() + "2113" + System.lineSeparator() + "02-12-2020"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("-2113 category Evan /description"); + assertEquals(2113.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testNormalAssignUserNo() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Evan" + System.lineSeparator() + "1010" + + System.lineSeparator() + "1010" + System.lineSeparator() + "n" + + System.lineSeparator() + "Evan" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("3000 category Albert, Betty, Evan /description"); + assertEquals("Evan", testExpense.getPayer().getName()); + assertEquals(1000.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1000.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(1000.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testAmountAssignedTooHigh() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Betty" + System.lineSeparator() + "2114" + + System.lineSeparator() + "Betty" + System.lineSeparator() + "1010" + + System.lineSeparator() + "1010" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("2113 category Albert, Betty, Evan /description"); + assertEquals("Betty", testExpense.getPayer().getName()); + assertEquals(1010.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1010.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(93.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testInvalidPersonInExpense() throws ForceCancelException { + String input = "Don" + System.lineSeparator() + "02-12-2020" + System.lineSeparator(); + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("2113 category Duke /description"); + assertEquals("Don", testExpense.getPayer().getName()); + assertEquals(2113.0, testExpense.getAmountSplit().get("Don")); + } + + @Test + void testInvalidAssignAmountNotNumber() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Betty" + + System.lineSeparator() + "NotANumber" + System.lineSeparator() + "1010" + + System.lineSeparator() + "1010" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("2113 category Albert, Betty, Evan /description"); + assertEquals("Betty", testExpense.getPayer().getName()); + assertEquals(1010.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1010.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(93.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testInvalidAssignAmountNegativeNumber() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Betty" + + System.lineSeparator() + "-2113" + System.lineSeparator() + "Betty" + System.lineSeparator() + "1010" + + System.lineSeparator() + "1010" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("2113 category Albert, Betty, Evan /description"); + assertEquals("Betty", testExpense.getPayer().getName()); + assertEquals(1010.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1010.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(93.0, testExpense.getAmountSplit().get("Evan")); + } + + @Test + void testInvalidPayer() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Chris" + System.lineSeparator() + "Albert" + + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("3000 category Albert, Don, Betty /description"); + assertEquals("Albert", testExpense.getPayer().getName()); + assertEquals(1000.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(1000.0, testExpense.getAmountSplit().get("Betty")); + assertEquals(1000.0, testExpense.getAmountSplit().get("Don")); + } + + @Test + void testUpdateOnePersonSpending() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("08-12-2010".getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("2113 category Albert /description"); + assertEquals("Albert", testExpense.getPayer().getName()); + assertEquals(2113.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(2213.0, trip.getListOfPersons().get(0).getMoneyOwed().get("Albert")); + } + + @Test + void testUpdateIndividualSpendingAssignZero() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Albert" + System.lineSeparator() + "1234" + + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("1234 category Albert, Evan, Don /description"); + assertEquals(1234.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(0.0, testExpense.getAmountSplit().get("Evan")); + assertEquals(0.0, testExpense.getAmountSplit().get("Don")); + } + + @Test + void testUpdateIndividualSpendingAssignZeroUserNo() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Albert" + System.lineSeparator() + "1200" + + System.lineSeparator() + "n" + System.lineSeparator() + "Albert" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("1200 category Albert, Evan, Don /description"); + assertEquals(400.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(400.0, testExpense.getAmountSplit().get("Evan")); + assertEquals(400.0, testExpense.getAmountSplit().get("Don")); + } + + @Test + void testUpdateIndividualSpendingEqual() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Evan" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("900 category Don, Evan, Betty /description"); + assertEquals(300.0, testExpense.getAmountSplit().get("Don")); + assertEquals(300.0, testExpense.getAmountSplit().get("Evan")); + assertEquals(300.0, testExpense.getAmountSplit().get("Betty")); + } + + @Test + void testUpdateIndividualSpendingInvalidEqual() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Evan" + System.lineSeparator() + "200" + + System.lineSeparator() + "equal" + System.lineSeparator() + "400" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("900 category Albert, Evan, Betty /description"); + assertEquals(200.0, testExpense.getAmountSplit().get("Albert")); + assertEquals(400.0, testExpense.getAmountSplit().get("Evan")); + assertEquals(300.0, testExpense.getAmountSplit().get("Betty")); + } + + @Test + void testUpdateIndividualSpendingMoreThanEqual() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Albert" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("901 category Albert, Don, Betty /description"); + assertEquals(300.34, testExpense.getAmountSplit().get("Albert")); + assertEquals(300.33, testExpense.getAmountSplit().get("Don")); + assertEquals(300.33, testExpense.getAmountSplit().get("Betty")); + } + + @Test + void testUpdateIndividualSpendingLessThanEqual() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Albert" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("899 category Albert, Don, Betty /description"); + assertEquals(299.66, testExpense.getAmountSplit().get("Albert")); + assertEquals(299.67, testExpense.getAmountSplit().get("Don")); + assertEquals(299.67, testExpense.getAmountSplit().get("Betty")); + } + + @Test + void testDeleteExpense() throws ForceCancelException { + String input = "02-12-2020" + System.lineSeparator() + "Albert" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Expense testExpense = new Expense("899 category Albert, Don, Betty /description"); + trip.addExpense(testExpense); + Parser.parseUserInput("delete 2"); + assertEquals(1, trip.getListOfExpenses().size()); + assertEquals("Dinner at fancy restaurant", trip.getListOfExpenses().get(0).getDescription()); + } + + @Test + void testDeleteLastExpense() { + String input = "02-12-2020" + System.lineSeparator() + "Albert" + System.lineSeparator() + "equal"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + Parser.parseUserInput("expense 899 category Albert, Don, Betty /description"); + Parser.parseUserInput("delete last"); + assertEquals(1, trip.getListOfExpenses().size()); + assertEquals("Dinner at fancy restaurant", trip.getListOfExpenses().get(0).getDescription()); + } + + @Test + void testInvalidDeleteLastExpense() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String expectedOutput = "You have not recently added an expense." + System.lineSeparator(); + Parser.parseUserInput("delete last"); + assertEquals(expectedOutput, actualOutput.toString()); + } + + //@@author lixiyuan416 + //Tests expense filtering methods + @Test + void findMatchingPersonExpenses_validName_printExpense() { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bo)); + String correctOutput = "1. \tDinner at fancy restaurant" + System.lineSeparator() + + "\tDate: 02 Dec 2020" + System.lineSeparator() + + "\tAmount Spent: USD $600.00" + System.lineSeparator() + + "\tPeople involved:" + System.lineSeparator() + + "\t\t1) Albert, USD $100.00" + System.lineSeparator() + + "\t\t2) Betty, USD $200.00" + System.lineSeparator() + + "\t\t3) Chris, USD $300.00" + System.lineSeparator() + + "\tPayer: Chris" + System.lineSeparator() + + "\tCategory: food"; + FilterFinder.findMatchingPersonExpenses(trip.getListOfExpenses(), "Chris"); + assertEquals(bo.toString().trim(), correctOutput); + } + + @Test + void findMatchingPersonExpenses_invalidName_printNotFound() { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bo)); + FilterFinder.findMatchingPersonExpenses(trip.getListOfExpenses(), "Mr Muscle"); + + assertEquals(bo.toString().trim(), "No matching expenses found."); + } + + @Test + void findMatchingDateExpensesReturnsEmpty() { + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + System.setOut(new PrintStream(bo)); + try { + FilterFinder.findMatchingDateExpenses(trip.getListOfExpenses(), "01-12-4000"); + } catch (ForceCancelException e) { + e.printStackTrace(); + } + assertEquals(bo.toString().trim(), "No matching expenses found."); + } + + @AfterAll + static void restoreSystemOut() { + System.setOut(System.out); + } + +} diff --git a/src/test/java/seedu/duke/FileTest.java b/src/test/java/seedu/duke/FileTest.java new file mode 100644 index 0000000000..418231b4bd --- /dev/null +++ b/src/test/java/seedu/duke/FileTest.java @@ -0,0 +1,93 @@ +//@@author yeezao + +package seedu.duke; + +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.trip.Trip; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.NoSuchElementException; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FileTest { + + @BeforeAll + static void setUp() throws IOException { + ArrayList listOfTrips = new ArrayList<>(); + listOfTrips.add(new Trip()); + listOfTrips.add(new Trip()); + Storage.setListOfTrips(listOfTrips); + FileStorage.initializeGson(); + Storage.setLogger(Logger.getLogger("logger")); + Storage.createNewFile(Storage.FILE_PATH); + Storage.writeToFile(Storage.FILE_PATH); + } + + @Test + void testRegularFileReadDirect() throws FileNotFoundException { + File file = new File("trips.json"); + assertTrue(file.canRead()); + String jsonString = FileStorage.readFromFile("trips.json"); + assertFalse(jsonString.isEmpty()); + Type tripType = new TypeToken>(){}.getType(); + FileStorage.initializeGson(); + assertNotNull(FileStorage.getGson()); + ArrayList listOfTrips = FileStorage.getGson().fromJson(jsonString, tripType); + assertNotNull(listOfTrips); + assertFalse(listOfTrips.isEmpty()); + } + + @Test + void testRegularFileRead() { + Storage.readFromFile("trips.json"); + assertNotNull(Storage.getListOfTrips()); + assertFalse(Storage.getListOfTrips().isEmpty()); + } + + @Test + void testNoFileDuringReadDirect() { + assertThrows(FileNotFoundException.class, () -> + FileStorage.readFromFile("randomfile.json")); + } + + @Test + void testReadEmptyFileDirect() { + try { + FileStorage.newBlankFile("tripsempty.json"); + } catch (IOException e) { + e.printStackTrace(); + } + assertThrows(NoSuchElementException.class, () -> + FileStorage.readFromFile("tripsempty.json")); + } + + @Test + void testReadCorruptedFile() { + assertThrows(JsonParseException.class, () -> { + String jsonString = FileStorage.readFromFile("tripscorrupted.json"); + Type tripType = new TypeToken>(){}.getType(); + FileStorage.getGson().fromJson(jsonString, tripType); + }); + } + + @Test + void testWriteFile() throws IOException { + Storage.readFromFile("tripsextra.json"); + Storage.writeToFile("trips.json"); + String jsonString = FileStorage.readFromFile("trips.json"); + assertFalse(jsonString.isBlank()); + } + +} diff --git a/src/test/java/seedu/duke/ParserTest.java b/src/test/java/seedu/duke/ParserTest.java new file mode 100644 index 0000000000..2f3485ea25 --- /dev/null +++ b/src/test/java/seedu/duke/ParserTest.java @@ -0,0 +1,37 @@ +package seedu.duke; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.parser.Parser; + +import java.io.ByteArrayInputStream; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ParserTest { + + @BeforeAll + static void setup() { + } + + @Test + void getUserToConfirm_yWithExtraSpaces_success() { + String input = " Y "; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + + assertTrue(Parser.getUserToConfirm()); + } + + void getUserToConfirm_garbageInputFollowedByn_success() { + String input = "abcd " + System.lineSeparator() + + "help" + System.lineSeparator() + + "n"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + + assertFalse(Parser.getUserToConfirm()); + } +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/PersonTest.java b/src/test/java/seedu/duke/PersonTest.java new file mode 100644 index 0000000000..31c824ddad --- /dev/null +++ b/src/test/java/seedu/duke/PersonTest.java @@ -0,0 +1,30 @@ +package seedu.duke; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PersonTest { + static Person testPerson; + + @BeforeAll + static void setUp() { + testPerson = new Person("Test Person"); + } + + @Test + void testSetName() { + testPerson.setName("Test"); + assertEquals("Test", testPerson.getName()); + } + + @Test + void testSetMoneyOwed() { + Person albert = new Person("Albert"); + testPerson.getMoneyOwed().put("Albert", 0.0); + testPerson.setMoneyOwed(albert, 200.00); + assertEquals(200.00, testPerson.getMoneyOwed().get("Albert")); + } +} diff --git a/src/test/java/seedu/duke/TripTest.java b/src/test/java/seedu/duke/TripTest.java new file mode 100644 index 0000000000..7dc34b4df4 --- /dev/null +++ b/src/test/java/seedu/duke/TripTest.java @@ -0,0 +1,519 @@ +package seedu.duke; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.parser.Parser; +import seedu.duke.trip.Trip; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Scanner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TripTest { + + private static Trip testTrip1; + private static InputStream origIn; + private static PrintStream origOut; + + @BeforeAll + static void setUp() throws SameNameException, ForceCancelException { + origIn = System.in; + origOut = System.out; + String[] stringArray = {"", "Canada", "02-03-2021", "cad", "0.123", "ben,jerry,tom"}; + testTrip1 = new Trip(stringArray); + } + + @Test + public void testNewTrip() throws ForceCancelException, SameNameException { + String[] stringArray = {"", "Canada", "02-03-2021", "cad", "0.123", "ben,jerry,tom"}; + Trip trip = new Trip(stringArray); + assertEquals("Canada", trip.getLocation()); + assertEquals("02 Mar 2021", trip.getDateOfTripString()); + assertEquals("2021-03-02", trip.getDateOfTrip().toString()); + assertEquals("CAD", trip.getForeignCurrency()); + assertEquals(0.123, trip.getExchangeRate()); + assertEquals(3, trip.getListOfPersons().size()); + assertEquals("ben", trip.getListOfPersons().get(0).getName()); + assertEquals("jerry", trip.getListOfPersons().get(1).getName()); + assertEquals("tom", trip.getListOfPersons().get(2).getName()); + } + + //@author yeezao + @Test + public void testNewTripUsingUserInput() { + ArrayList newListOfTrips = new ArrayList<>(); + Storage.setListOfTrips(newListOfTrips); + createNewTripForTest(); + Trip createdTrip = Storage.getListOfTrips().get(0); + assertNotNull(createdTrip); + assertEquals("United States of America", createdTrip.getLocation()); + assertEquals("02 Feb 2021", createdTrip.getDateOfTripString()); + assertEquals("USD", createdTrip.getForeignCurrency()); + assertEquals("$", createdTrip.getForeignCurrencySymbol()); + assertEquals("SGD", createdTrip.getRepaymentCurrency()); + assertEquals(0.74, createdTrip.getExchangeRate()); + ArrayList personArrayList = createdTrip.getListOfPersons(); + assertNotNull(personArrayList); + assertEquals(5, personArrayList.size()); + assertEquals("Ben", personArrayList.get(0).getName()); + assertEquals("Jerry", personArrayList.get(1).getName()); + assertEquals("Tom", personArrayList.get(2).getName()); + assertEquals("Harry", personArrayList.get(3).getName()); + assertEquals("Dick", personArrayList.get(4).getName()); + } + + @Test + public void testNewTripsWithDuplicates() { + String wholeUserInput = "a" + System.lineSeparator() + "n" + System.lineSeparator() + "y"; + System.setIn(new ByteArrayInputStream(wholeUserInput.getBytes())); + Storage.setScanner(new Scanner(System.in)); + createNewTripForTest(); + assertEquals(1, Storage.getListOfTrips().size()); + String userInput2 = "create /United States of America /02-02-2021 /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput2); + assertEquals(1, Storage.getListOfTrips().size()); + Parser.parseUserInput(userInput2); + assertEquals(2, Storage.getListOfTrips().size()); + } + + @Test + public void testNewTrip_CheckPerAttributeDuplicate() { + createNewTripForTest(); + assertEquals(1, Storage.getListOfTrips().size()); + String userInput = "create /United of America /02-02-2021 /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput); + assertEquals(2, Storage.getListOfTrips().size()); + userInput = "create /United of America /03-02-2021 /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput); + assertEquals(3, Storage.getListOfTrips().size()); + userInput = "create /United of America /03-02-2021 /UAD /0.74 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput); + assertEquals(4, Storage.getListOfTrips().size()); + userInput = "create /United of America /03-02-2021 /USD /0.75 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput); + assertEquals(5, Storage.getListOfTrips().size()); + } + + @Test + public void testNewTripInsufficientAttributes() { + Storage.setListOfTrips(new ArrayList<>()); + String userInput = "create /United States of America /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput); + assertEquals(0, Storage.getListOfTrips().size()); + } + + @Test + public void testNewTripBlankName() throws SameNameException, ForceCancelException { + String[] input = {" ", "somewhere", "02-02-2021", "USD", "0.22", "Ben, , Jerry"}; + Trip trip = new Trip(input); + assertEquals(2, trip.getListOfPersons().size()); + } + + @Test + public void testNewTripSameName() { + String[] input = {" ", "somewhere", "02-02-2021", "USD", "0.22", "Ben, Ben"}; + assertThrows(SameNameException.class, () -> new Trip(input)); + } + + @Test + public void testNewTripSameNameFull() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + Parser.parseUserInput("create /United States of America /02-02-2021 /USD /0.74 /Ben, Ben"); + String errorString = "You have entered people with the same name, please recreate the trip ensuring " + + "there are no repeated names for the trip." + System.lineSeparator(); + assertEquals(errorString, outputStream.toString()); + System.setOut(origOut); + } + + @Test + public void testEditLocation() throws ForceCancelException { + testTrip1.setLocation("under the rainbow"); + assertEquals("under the rainbow", testTrip1.getLocation()); + } + + @Test + public void testEditLocation_BlankInput() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("in our favourite rocket ship".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setLocation(""); + assertEquals("in our favourite rocket ship", testTrip1.getLocation()); + } + + @Test + public void testEditLocationFull() { + Storage.setListOfTrips(new ArrayList<>()); + createNewTripForTest(); + String userInput = "edit last -location going on a trip"; + Parser.parseUserInput(userInput); + Trip tripToCheck = Storage.getLastTrip(); + assertEquals("going on a trip", tripToCheck.getLocation()); + userInput = "edit 1 -location going on the trip"; + Parser.parseUserInput(userInput); + tripToCheck = Storage.getListOfTrips().get(0); + assertEquals("going on the trip", tripToCheck.getLocation()); + } + + @Test + public void testEditDate() throws ForceCancelException { + testTrip1.setDateOfTrip("08-08-2020"); + assertEquals("08 Aug 2020", testTrip1.getDateOfTripString()); + } + + @Test + public void testEditDateNotParsable() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("08-12-2010".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setDateOfTrip("something"); + assertEquals("08 Dec 2010", testTrip1.getDateOfTripString()); + } + + @Test + public void testEditDateBeforeEpoch() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("08-12-2010".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setDateOfTrip("01-01-1969"); + assertEquals("08 Dec 2010", testTrip1.getDateOfTripString()); + } + + @Test + public void testEditDateWhichDoesNotExist() throws ForceCancelException { + String scannerInputs = "35-02-2021" + System.lineSeparator() + + "00-11-2021" + System.lineSeparator() + + "25-00-2021" + System.lineSeparator() + + "16-23-2021" + System.lineSeparator() + + "08-12-2020"; + System.setIn(new ByteArrayInputStream(scannerInputs.getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setDateOfTrip("29-02-2021"); + assertEquals("08 Dec 2020", testTrip1.getDateOfTripString()); + } + + @Test + public void testEditDateFull() { + createNewTripForTest(); + String userInput = "edit last -date 09-01-1990"; + Parser.parseUserInput(userInput); + Trip tripToCheck = Storage.getLastTrip(); + assertEquals("09 Jan 1990", tripToCheck.getDateOfTripString()); + } + + @Test + public void testEditExrate() throws ForceCancelException { + testTrip1.setExchangeRate("12.0"); + assertEquals(12.0, testTrip1.getExchangeRate()); + } + + @Test + public void testEditExRateNotParsable() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("6.1".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setExchangeRate("something"); + assertEquals(6.1, testTrip1.getExchangeRate()); + } + + @Test + public void testEditExRateNotParsableWithForceCancel() { + assertThrows(ForceCancelException.class, () -> { + System.setIn(new ByteArrayInputStream("-cancel".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setExchangeRate("something"); + }); + } + + @Test + public void testEditExrateFull() { + String userInput = "edit last -exchangerate 0.01"; + Parser.parseUserInput(userInput); + Trip tripToCheck = Storage.getLastTrip(); + assertEquals(0.01, tripToCheck.getExchangeRate()); + } + + @Test + public void testEditTripExceptions() { + createNewTripForTest(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + Parser.parseUserInput("edit dfg -location somewhere"); + String errorString = "Please format your inputs as follows: " + + System.lineSeparator() + + "edit [trip num] [attribute] [new value]" + + System.lineSeparator() + + "attributes: -location, -date, -exchangerate, -forcur, -homecur" + + System.lineSeparator() + System.lineSeparator(); + assertEquals(errorString, outputStream.toString()); + outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + Parser.parseUserInput("edit 15 -location somewhere"); + assertEquals(errorString, outputStream.toString()); + System.setOut(origOut); + } + + @Test + public void testOpenTripByIndex() throws ForceCancelException { + createNewTripForTest(); + Storage.setOpenTrip(0); + assertTrue(Storage.checkOpenTrip()); + assertEquals(Storage.getOpenTrip(), Storage.getListOfTrips().get(0)); + assertEquals(Storage.getLastTrip(), Storage.getListOfTrips().get(0)); + } + + @Test + public void testOpenTripWithAlreadyOpenTrip() throws ForceCancelException { + createNewTripForTest(); + Parser.parseUserInput("create /United of America /02-02-2021 /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"); + Parser.parseUserInput("open 1"); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + Parser.parseUserInput("open 2"); + String outputMsgClose = "You have closed the following trip:" + + System.lineSeparator() + + Storage.getListOfTrips().get(0).getLocation() + " | " + + Storage.getListOfTrips().get(0).getDateOfTripString(); + String outputMsgOpen = "You have opened the following trip: " + + System.lineSeparator() + + Storage.getListOfTrips().get(1).getLocation() + " | " + + Storage.getListOfTrips().get(1).getDateOfTripString(); + String fullOutput = outputMsgClose + System.lineSeparator() + outputMsgOpen + + System.lineSeparator() + System.lineSeparator(); + assertEquals(fullOutput, outputStream.toString()); + assertEquals(Storage.getListOfTrips().get(1), Storage.getOpenTrip()); + System.setOut(origOut); + } + + @Test + public void testCloseTrip() throws ForceCancelException { + Storage.setOpenTrip(0); + Trip trip = Storage.getOpenTrip(); + Storage.closeTrip(); + assertFalse(Storage.checkOpenTrip()); + assertEquals(Storage.getLastTrip(), trip); + } + + @Test + public void testOpenTripFull() throws ForceCancelException { + createNewTripForTest(); + String userInput = "open 1"; + Parser.parseUserInput(userInput); + assertTrue(Storage.checkOpenTrip()); + assertEquals(Storage.getOpenTrip(), Storage.getListOfTrips().get(0)); + assertEquals(Storage.getLastTrip(), Storage.getListOfTrips().get(0)); + } + + @Test + public void testCloseTripFull() throws ForceCancelException { + Storage.setOpenTrip(0); + Trip trip = Storage.getOpenTrip(); + String userInput = "close"; + Parser.parseUserInput(userInput); + assertFalse(Storage.checkOpenTrip()); + assertEquals(Storage.getLastTrip(), trip); + } + + @Test + public void testOpenTripNull() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("1".getBytes())); + Storage.setScanner(new Scanner(System.in)); + createNewTripForTest(); + Storage.setOpenTrip(0); + Storage.closeTrip(); + assertFalse(Storage.checkOpenTrip()); + Storage.getOpenTrip(); + assertEquals(Storage.getOpenTrip(), Storage.getListOfTrips().get(0)); + assertEquals(Storage.getLastTrip(), Storage.getListOfTrips().get(0)); + } + + @Test + public void testDeleteTripFullByIndex() { + createNewTripForTest(); + Storage.closeTrip(); + String userInput = "delete 1"; + Parser.parseUserInput(userInput); + assertEquals(0, Storage.getListOfTrips().size()); + assertNull(Storage.getLastTrip()); + } + + @Test + public void testDeleteTripFullByLast() { + createNewTripForTest(); + String userInput = "delete last"; + Parser.parseUserInput(userInput); + assertEquals(0, Storage.getListOfTrips().size()); + assertNull(Storage.getLastTrip()); + } + + @Test + public void testLastTripNull() { + createNewTripForTest(); + Parser.parseUserInput("create /United of America /03-02-2021 /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"); + Storage.closeTrip(); + Parser.parseUserInput("delete 1"); + assertEquals(1, Storage.getListOfTrips().size()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + Parser.parseUserInput("edit last -location somewhere"); + String errorMsg = "You may have deleted the most recently modified trip. " + + "Please try again with the trip number of the trip you wish to edit."; + assertEquals(errorMsg + System.lineSeparator(), outputStream.toString()); + System.setOut(origOut); + } + + @Test + public void setListOfPersons_EmptyList() throws SameNameException, ForceCancelException { + System.setIn(new ByteArrayInputStream("me, someone".getBytes())); + Storage.setScanner(new Scanner(System.in)); + Trip trip = new Trip(); + trip.setListOfPersons(new ArrayList<>()); + ArrayList listOfPersons = trip.getListOfPersons(); + assertEquals("me", listOfPersons.get(0).getName()); + assertEquals("someone", listOfPersons.get(1).getName()); + } + + @Test + public void testSetLocation() throws ForceCancelException { + Trip trip = new Trip(); + trip.setLocation("America"); + assertEquals("America", trip.getLocation()); + } + + @Test + public void testSetName() { + Person person = new Person("CS2113T"); + person.setName("Duke"); + assertEquals("Duke", person.getName()); + } + + //@@author itsleeqian + @Test + public void testSetForeignCurrency() throws ForceCancelException { + Trip trip1 = new Trip(); + trip1.setForeignCurrency("USD"); + assertEquals("USD", trip1.getForeignCurrency()); + Trip trip2 = new Trip(); + trip2.setForeignCurrency("EUR"); + assertEquals("EUR", trip2.getForeignCurrency()); + } + + + @Test + public void testEditForeignCurrency() throws ForceCancelException { + testTrip1.setForeignCurrency("CNY"); + assertEquals("CNY", testTrip1.getForeignCurrency()); + } + + @Test + public void testEditForeignCurrency_InvalidCurrency() throws ForceCancelException { + String scannerInputs = "123" + System.lineSeparator() + + "test currency" + System.lineSeparator() + + "Galleon" + System.lineSeparator() + + "KRW"; + System.setIn(new ByteArrayInputStream(scannerInputs.getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setForeignCurrency("hello"); + assertEquals("KRW", testTrip1.getForeignCurrency()); + } + + @Test + public void testEditForeignCurrency_BlankInput() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("KRW".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setForeignCurrency(""); + assertEquals("KRW", testTrip1.getForeignCurrency()); + } + + @Test + public void testEditForeignCurrencyFull() { + createNewTripForTest(); + String userInput = "edit last -forcur TWD"; + Parser.parseUserInput(userInput); + Trip tripToCheck = Storage.getLastTrip(); + assertEquals("TWD", tripToCheck.getForeignCurrency()); + assertEquals("NT$", tripToCheck.getForeignCurrencySymbol()); + } + + + @Test + public void testSetHomeCurrency() throws ForceCancelException { + Trip trip1 = new Trip(); + trip1.setRepaymentCurrency("SGD"); + assertEquals("SGD", trip1.getRepaymentCurrency()); + Trip trip2 = new Trip(); + trip2.setRepaymentCurrency("MYR"); + assertEquals("MYR", trip2.getRepaymentCurrency()); + } + + @Test + public void testEditHomeCurrency() throws ForceCancelException { + testTrip1.setRepaymentCurrency("SAR"); + assertEquals("SAR", testTrip1.getRepaymentCurrency()); + } + + @Test + public void testEditHomeCurrency_InvalidCurrency() throws ForceCancelException { + String scannerInputs = "456" + System.lineSeparator() + + "kekW" + System.lineSeparator() + + "Galleon" + System.lineSeparator() + + "JPY"; + System.setIn(new ByteArrayInputStream(scannerInputs.getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setForeignCurrency("hello"); + assertEquals("JPY", testTrip1.getForeignCurrency()); + } + + @Test + public void testEditHomeCurrency_BlankInput() throws ForceCancelException { + System.setIn(new ByteArrayInputStream("KRW".getBytes())); + Storage.setScanner(new Scanner(System.in)); + testTrip1.setRepaymentCurrency(""); + assertEquals("KRW", testTrip1.getRepaymentCurrency()); + } + + @Test + public void testEditHomeCurrencyFull() { + createNewTripForTest(); + String userInput = "edit last -homecur IDR"; + Parser.parseUserInput(userInput); + Trip tripToCheck = Storage.getLastTrip(); + assertEquals("IDR", tripToCheck.getRepaymentCurrency()); + assertEquals("Rp", tripToCheck.getRepaymentCurrencySymbol()); + } + + //@@author + + @Test + public void testSetDate() throws ForceCancelException { + Trip trip = new Trip(); + trip.setDateOfTrip("23-09-2021"); + assertEquals("23 Sep 2021", trip.getDateOfTripString()); + } + + + @Test + public void sampleTest() { + assertTrue(true); + } + + @AfterAll + static void restore() { + System.setIn(origIn); + } + + private void createNewTripForTest() { + Storage.setListOfTrips(new ArrayList<>()); + String userInput = "create /United States of America /02-02-2021 /USD /0.74 /Ben, Jerry, Tom, Harry, Dick"; + Parser.parseUserInput(userInput); + } +} diff --git a/src/test/java/seedu/duke/ValidityCheckerTest.java b/src/test/java/seedu/duke/ValidityCheckerTest.java new file mode 100644 index 0000000000..8e35b34e79 --- /dev/null +++ b/src/test/java/seedu/duke/ValidityCheckerTest.java @@ -0,0 +1,46 @@ +package seedu.duke; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.parser.Parser; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ValidityCheckerTest { + + @BeforeAll + static void setUp() { + + } + + @Test + public void testDateCannotBeParsed() { + assertFalse(Parser.doesDateReallyExist("aa-bb-cccc")); + assertFalse(Parser.doesDateReallyExist("01-bb-cccc")); + assertFalse(Parser.doesDateReallyExist("01-01-cccc")); + assertTrue(Parser.doesDateReallyExist("01-01-2001")); + } + + @Test + public void testDateDoesNotExist_InputDatesReallyDontExist() { + + //nonsense inputs + assertFalse(Parser.doesDateReallyExist("35-02-2021")); + assertFalse(Parser.doesDateReallyExist("00-11-2021")); + assertFalse(Parser.doesDateReallyExist("25-00-2021")); + assertFalse(Parser.doesDateReallyExist("16-23-2021")); + + //checking for 31st + assertFalse(Parser.doesDateReallyExist("31-04-2021")); + assertFalse(Parser.doesDateReallyExist("31-11-2021")); + + //leap years + assertFalse(Parser.doesDateReallyExist("29-02-2021")); + assertTrue(Parser.doesDateReallyExist("29-02-2020")); + assertTrue(Parser.doesDateReallyExist("28-02-2020")); + + //this exists + assertTrue(Parser.doesDateReallyExist("08-12-2020")); + } +} diff --git a/src/test/java/seedu/duke/parser/CommandExecutorTest.java b/src/test/java/seedu/duke/parser/CommandExecutorTest.java new file mode 100644 index 0000000000..b720468feb --- /dev/null +++ b/src/test/java/seedu/duke/parser/CommandExecutorTest.java @@ -0,0 +1,128 @@ +package seedu.duke.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.Ui; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.expense.Expense; +import seedu.duke.trip.Trip; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Scanner; + + +class CommandExecutorTest { + private static final DateTimeFormatter inputPattern = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static Trip trip; + + @BeforeAll + static void setUp() throws SameNameException, ForceCancelException { + String[] stringArray = {"", "Canada", "02-03-2021", "cad", "0.123", "ben,jerry,tom"}; + trip = new Trip(stringArray); + ArrayList listOfTrips = new ArrayList<>(); + listOfTrips.add(trip); + Storage.setListOfTrips(listOfTrips); + Storage.setOpenTrip(0); + ArrayList listOfPersons = trip.getListOfPersons(); + Person person1 = listOfPersons.get(0); + Person person2 = listOfPersons.get(1); + ArrayList listOfPersons1 = new ArrayList<>(); + listOfPersons1.add(person1); + listOfPersons1.add(person2); + HashMap amountSplit1 = new HashMap<>(); + Expense exp1 = new Expense(8.00, "chicken nuggets", listOfPersons1, "food", + LocalDate.parse("11-02-2021", inputPattern), person1, amountSplit1); + exp1.setAmountSplit(person1, 0.0); + exp1.setAmountSplit(person2, 8.0); + person1.setMoneyOwed(person1, -8.0); + person1.setMoneyOwed(person2, 8.0); + person2.setMoneyOwed(person1, -8.0); + HashMap amountSplit2 = new HashMap<>(); + Expense exp2 = new Expense(16.00, "chicken", listOfPersons, "food", + LocalDate.parse("11-02-2021", inputPattern), person2, amountSplit2); + exp2.setAmountSplit(person1, 0.0); + exp2.setAmountSplit(person2, 10.0); + Person person3 = listOfPersons.get(2); + exp2.setAmountSplit(person3, 6.0); + person2.setMoneyOwed(person2, -16.0); + person2.setMoneyOwed(person1, 10.0); + person1.setMoneyOwed(person2, -10.0); + person2.setMoneyOwed(person3, 6.0); + person3.setMoneyOwed(person2, -6.0); + trip.addExpense(exp1); + trip.addExpense(exp2); + + } + + @Test + void testViewAllExpenses() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String expectedOutput = "List of all Expenses in detail: " + System.lineSeparator() + + "1. " + trip.getListOfExpenses().get(0).toString() + System.lineSeparator() + + "2. " + trip.getListOfExpenses().get(1).toString() + System.lineSeparator(); + trip.viewAllExpenses(); + assertEquals(expectedOutput, actualOutput.toString()); + } + + @Test + void testViewFilterByCategory() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "view filter category food"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "1. " + trip.getListOfExpenses().get(0).toString() + System.lineSeparator() + + "2. " + trip.getListOfExpenses().get(1).toString() + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + } + + @Test + void testViewFilterByPayer() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "view filter payer ben"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "1. " + trip.getListOfExpenses().get(0).toString() + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + } + + @Test + void testViewFilterByDescription() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "view filter description nuggets"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "1. " + trip.getListOfExpenses().get(0).toString() + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + } + + @Test + void testViewFilterByPersons() { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "view filter person tom"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "2. " + trip.getListOfExpenses().get(1).toString() + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + } + + +} \ No newline at end of file diff --git a/src/test/java/seedu/duke/parser/ExpenseSummaryTest.java b/src/test/java/seedu/duke/parser/ExpenseSummaryTest.java new file mode 100644 index 0000000000..241d2cf054 --- /dev/null +++ b/src/test/java/seedu/duke/parser/ExpenseSummaryTest.java @@ -0,0 +1,123 @@ +package seedu.duke.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.trip.Trip; +import seedu.duke.expense.Expense; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Scanner; + +public class ExpenseSummaryTest { + + private static final DateTimeFormatter inputPattern = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + private static Trip trip; + + @BeforeAll + static void setUp() throws SameNameException, ForceCancelException { + String[] stringArray = {"", "Canada", "02-03-2021", "cad", "1.08", "ben,jerry,tom"}; + trip = new Trip(stringArray); + ArrayList listOfTrips = new ArrayList<>(); + listOfTrips.add(trip); + Storage.setListOfTrips(listOfTrips); + Storage.setOpenTrip(Storage.getListOfTrips().indexOf(trip)); + ArrayList listOfPersons = trip.getListOfPersons(); + Person person1 = listOfPersons.get(0); + Person person2 = listOfPersons.get(1); + ArrayList listOfPersons1 = new ArrayList<>(); + listOfPersons1.add(person1); + listOfPersons1.add(person2); + HashMap amountSplit1 = new HashMap<>(); + Expense exp1 = new Expense(8.00, "chicken nuggets", listOfPersons1, "food", + LocalDate.parse("11-02-2021", inputPattern), person1, amountSplit1); + exp1.setAmountSplit(person1, 4.0); + exp1.setAmountSplit(person2, 4.0); + person1.setMoneyOwed(person1, -8.0); + person1.setMoneyOwed(person2, 4.0); + person2.setMoneyOwed(person1, -4.0); + HashMap amountSplit2 = new HashMap<>(); + Expense exp2 = new Expense(16.00, "travel to hotel", listOfPersons, "travel", + LocalDate.parse("11-02-2021", inputPattern), person2, amountSplit2); + exp2.setAmountSplit(person1, 0.0); + exp2.setAmountSplit(person2, 10.0); + Person person3 = listOfPersons.get(2); + exp2.setAmountSplit(person3, 6.0); + person2.setMoneyOwed(person2, -16.0); + person2.setMoneyOwed(person1, 10.0); + person1.setMoneyOwed(person2, -10.0); + person2.setMoneyOwed(person3, 6.0); + person3.setMoneyOwed(person2, -6.0); + trip.addExpense(exp1); + trip.addExpense(exp2); + } + + @Test + public void testExpenseSummary_individual() throws SameNameException, ForceCancelException { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "summary ben"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "ben has spent CAD $4.00 (SGD $3.70) on 2 expenses on the following categories:" + + System.lineSeparator() + "travel: CAD $0.00 (SGD $0.00)" + + System.lineSeparator() + "food: CAD $4.00 (SGD $3.70)" + + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + + } + + @Test + public void testExpenseSummary_invalidInput() throws SameNameException, ForceCancelException { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "summary 1"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "There are no persons with the name of [1] in this trip." + + System.lineSeparator() + "Please format your inputs as follows: " + + System.lineSeparator() + "\"summary\" or \"summary [person name]\"." + + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + } + + @Test + public void testExpenseSummary_full() throws SameNameException, ForceCancelException { + ByteArrayOutputStream actualOutput = new ByteArrayOutputStream(); + System.setOut(new PrintStream(actualOutput)); + String input = "summary"; + System.setIn(new ByteArrayInputStream(input.getBytes())); + Storage.setScanner(new Scanner(System.in)); + String expectedOutput = "ben has spent CAD $4.00 (SGD $3.70) on 2 expenses on the following categories:" + + System.lineSeparator() + "travel: CAD $0.00 (SGD $0.00)" + + System.lineSeparator() + "food: CAD $4.00 (SGD $3.70)" + + System.lineSeparator() + System.lineSeparator() + + "jerry has spent CAD $14.00 (SGD $12.96) on 2 expenses on the following categories:" + + System.lineSeparator() + "travel: CAD $10.00 (SGD $9.26)" + + System.lineSeparator() + "food: CAD $4.00 (SGD $3.70)" + + System.lineSeparator() + System.lineSeparator() + + "tom has spent CAD $6.00 (SGD $5.56) on 1 expenses on the following categories:" + + System.lineSeparator() + "travel: CAD $6.00 (SGD $5.56)" + + System.lineSeparator() + System.lineSeparator(); + Parser.parseUserInput(input); + assertEquals(expectedOutput, actualOutput.toString()); + } +} diff --git a/src/test/java/seedu/duke/parser/PaymentOptimizerTest.java b/src/test/java/seedu/duke/parser/PaymentOptimizerTest.java new file mode 100644 index 0000000000..35aa2b7be3 --- /dev/null +++ b/src/test/java/seedu/duke/parser/PaymentOptimizerTest.java @@ -0,0 +1,88 @@ +package seedu.duke.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import seedu.duke.Person; +import seedu.duke.Storage; +import seedu.duke.exceptions.ForceCancelException; +import seedu.duke.exceptions.SameNameException; +import seedu.duke.trip.Trip; +import seedu.duke.expense.Expense; +import java.time.LocalDate; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Scanner; + + +class PaymentOptimizerTest { + + static Expense exp1; + static Expense exp2; + static Expense exp3; + private static final DateTimeFormatter inputPattern = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + @BeforeAll + static void setUp() throws SameNameException, ForceCancelException { + String[] stringArray = {"", "Canada", "02-03-2021", "cad", "0.123", "ben,jerry,tom"}; + Trip trip = new Trip(stringArray); + ArrayList listOfTrips = new ArrayList<>(); + listOfTrips.add(trip); + Storage.setListOfTrips(listOfTrips); + Storage.setOpenTrip(0); + ArrayList listOfPersons = trip.getListOfPersons(); + Person person1 = listOfPersons.get(0); + Person person2 = listOfPersons.get(1); + HashMap amountSplit1 = new HashMap<>(); + Expense exp1 = new Expense(8.00, "chicken nuggers", listOfPersons, "food", + LocalDate.parse("11-02-2021", inputPattern), person1, amountSplit1); + exp1.setPayer(person1); + person1.setMoneyOwed(person1, -8.0); + person1.setMoneyOwed(person2, 5.0); + person2.setMoneyOwed(person1, -5.0); + Person person3 = listOfPersons.get(2); + person1.setMoneyOwed(person3, 3.0); + person3.setMoneyOwed(person1, -3.0); + exp1.setAmountSplit(person2, 4.0); + exp1.setAmountSplit(person3, 4.0); + HashMap amountSplit2 = new HashMap<>(); + Expense exp2 = new Expense(16.00, "chicken", listOfPersons, "food", + LocalDate.parse("11-02-2021", inputPattern), person2, amountSplit2); + exp2.setAmountSplit(person2, 10.0); + exp2.setAmountSplit(person3, 6.0); + person2.setMoneyOwed(person2, -16.0); + person2.setMoneyOwed(person1, 10.0); + person1.setMoneyOwed(person2, -10.0); + person2.setMoneyOwed(person3, 6.0); + person3.setMoneyOwed(person2, -6.0); + trip.addExpense(exp1); + trip.addExpense(exp2); + } + + @Test + public void testOptimizePayments() throws ForceCancelException { + Trip openTrip = Storage.getOpenTrip(); + Person person1 = openTrip.getListOfPersons().get(0); + PaymentOptimizer.optimizePayments(openTrip); + HashMap hashMapBen = person1.getOptimizedMoneyOwed(); + assertEquals(hashMapBen.get("jerry"), -2.0); + Person person2 = openTrip.getListOfPersons().get(1); + HashMap hashMapJerry = person2.getOptimizedMoneyOwed(); + assertEquals(hashMapJerry.get("ben"), 2.0); + assertEquals(hashMapJerry.get("tom"), 9.0); + Person person3 = openTrip.getListOfPersons().get(2); + HashMap hashMapTom = person3.getOptimizedMoneyOwed(); + assertEquals(hashMapTom.get("jerry"), -9.0); + + + } + + +} \ No newline at end of file diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..6628f50644 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,172 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling +Welcome to + ____ __ ___ ____ __ + / __ \____ ___ __/ |/ ___ / __ )____ ______/ /__ + / /_/ / __ `/ / / / /|_/ / _ \/ __ / __ `/ ___/ //_/ + / ____/ /_/ / /_/ / / / / __/ /_/ / /_/ / /__/ ,< +/_/ \__,_/\__, /_/ /_/\___/_____/\__,_/\___/_/|_| + /____/ + +No preloaded data found! We have created a file for you. +A new save file has been created! +Enter your command: Your trip to America on 02 Feb 2021 has been successfully added! +Enter your command: You have opened the following trip: +America | 02 Feb 2021 + +Enter your command: Enter date of expense: + Press enter to use today's date +Who paid for the expense?: Enter "equal" if expense is to be evenly split, enter individual spending otherwise +There is USD $8.00 left to be assigned. How much did yikai spend?: There is USD $8.00 left to be assigned. How much did yuzhao spend?: Assign the remaining USD $3.00 to qian? (y/n): Your expense has been added successfully! +Enter your command: 1. c a b + Date: 19 Oct 2021 + Amount Spent: USD $8.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $5.00 + 3) qian, USD $3.00 + Payer: yikai + Category: breakfast + +Enter your command: No matching expenses found. +Enter your command: List of Expenses: + 1. c a b | 19 Oct 2021 +Enter your command: The exchange rate has been changed from 0.74 to 1.0. +Enter your command: Enter date of expense: + Press enter to use today's date +Who paid for the expense?: Enter "equal" if expense is to be evenly split, enter individual spending otherwise +There is USD $24.00 left to be assigned. How much did yikai spend?: There is USD $24.00 left to be assigned. How much did yuzhao spend?: Assign the remaining USD $8.00 to qian? (y/n): Your expense has been added successfully! +Enter your command: Here are the optimized payment transactions: +yuzhao needs to pay USD $8.00 (SGD $8.00) to yikai +yuzhao needs to pay USD $13.00 (SGD $13.00) to qian +Enter your command: Enter date of expense: + Press enter to use today's date +Who paid for the expense?: Enter "equal" if expense is to be evenly split, enter individual spending otherwise +There is USD $48.00 left to be assigned. How much did yikai spend?: There is USD $16.00 left to be assigned. How much did yuzhao spend?: Assign the remaining USD $16.00 to qian? (y/n): Your expense has been added successfully! +Enter your command: yikai spent USD $32.00 (SGD $32.00) on the trip so far +yikai owes USD $27.00 (SGD $27.00) to yuzhao +qian owes USD $3.00 (SGD $3.00) to yikai +yikai does not owe anything to josh +yikai does not owe anything to xiyuan +Enter your command: qian spent USD $27.00 (SGD $27.00) on the trip so far +qian owes USD $3.00 (SGD $3.00) to yikai +qian does not owe anything to yuzhao +qian does not owe anything to josh +qian does not owe anything to xiyuan +Enter your command: yuzhao spent USD $21.00 (SGD $21.00) on the trip so far +yikai owes USD $27.00 (SGD $27.00) to yuzhao +yuzhao does not owe anything to qian +yuzhao does not owe anything to josh +yuzhao does not owe anything to xiyuan +Enter your command: 1. c a b + Date: 19 Oct 2021 + Amount Spent: USD $8.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $5.00 + 3) qian, USD $3.00 + Payer: yikai + Category: breakfast + +3. d a c + Date: 19 Oct 2021 + Amount Spent: USD $48.00 + People involved: + 1) yikai, USD $32.00 + 2) yuzhao, USD $0.00 + 3) qian, USD $16.00 + Payer: yuzhao + Category: breakfast + +Enter your command: 2. d a c + Date: 19 Oct 2021 + Amount Spent: USD $24.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $16.00 + 3) qian, USD $8.00 + Payer: qian + Category: lunch + +Enter your command: 2. d a c + Date: 19 Oct 2021 + Amount Spent: USD $24.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $16.00 + 3) qian, USD $8.00 + Payer: qian + Category: lunch + +3. d a c + Date: 19 Oct 2021 + Amount Spent: USD $48.00 + People involved: + 1) yikai, USD $32.00 + 2) yuzhao, USD $0.00 + 3) qian, USD $16.00 + Payer: yuzhao + Category: breakfast + +Enter your command: 1. c a b + Date: 19 Oct 2021 + Amount Spent: USD $8.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $5.00 + 3) qian, USD $3.00 + Payer: yikai + Category: breakfast + +Enter your command: No matching expenses found. +Enter your command: Please format your inputs as follows: +view filter [category, description, payer, person, date] [search keyword] +Enter your command: Sorry, we didn't recognize your entry. Please try again, or enter help to learn more. +Enter your command: Sorry, we didn't recognize your entry. Please try again, or enter help to learn more. +Enter your command: List of Expenses: + 1. c a b | 19 Oct 2021 + 2. d a c | 19 Oct 2021 + 3. d a c | 19 Oct 2021 +Enter your command: List of all Expenses in detail: +1. c a b + Date: 19 Oct 2021 + Amount Spent: USD $8.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $5.00 + 3) qian, USD $3.00 + Payer: yikai + Category: breakfast + +2. d a c + Date: 19 Oct 2021 + Amount Spent: USD $24.00 + People involved: + 1) yikai, USD $0.00 + 2) yuzhao, USD $16.00 + 3) qian, USD $8.00 + Payer: qian + Category: lunch + +3. d a c + Date: 19 Oct 2021 + Amount Spent: USD $48.00 + People involved: + 1) yikai, USD $32.00 + 2) yuzhao, USD $0.00 + 3) qian, USD $16.00 + Payer: yuzhao + Category: breakfast + +Enter your command: Your expense of USD $24.00 has been successfully removed. +Enter your command: Your expense of USD $8.00 has been successfully removed. +Enter your command: Your expense of USD $48.00 has been successfully removed. +Enter your command: yikai has spent USD $0.00 (SGD $0.00) on 0 expenses on the following categories: + +yuzhao has spent USD $0.00 (SGD $0.00) on 0 expenses on the following categories: + +qian has spent USD $0.00 (SGD $0.00) on 0 expenses on the following categories: + +josh has spent USD $0.00 (SGD $0.00) on 0 expenses on the following categories: + +xiyuan has spent USD $0.00 (SGD $0.00) on 0 expenses on the following categories: + +Enter your command: Exiting the program now. Goodbye! diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..dbf2c4e111 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +1,43 @@ -James Gosling \ No newline at end of file +create /America /02-02-2021 /USD /0.74 /yikai,yuzhao,qian,josh,xiyuan +open 1 +expense 8.00 breakfast yikai,yuzhao,qian /c a b +19-10-2021 +yikai +0 +5 +y +view filter category breakfast +view filter description d a c +list +edit 1 -exchangerate 1 +expense 24.00 lunch yikai,yuzhao,qian /d a c +19-10-2021 +qian +0 +16 +y +optimize yikai +expense 48.00 breakfast yikai,yuzhao,qian /d a c +19-10-2021 +yuzhao +32 +0 +y +amount yikai +amount qian +amount yuzhao +view filter category breakfast +view filter category lunch +view filter description d a c +view filter description c a b +view filter payer josh +view index 2 +yikai +equal +list +view +delete 2 +delete 1 +delete 1 +summary +quit \ No newline at end of file diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh index 1dcbd12021..d858938b8d 100755 --- a/text-ui-test/runtest.sh +++ b/text-ui-test/runtest.sh @@ -8,6 +8,8 @@ cd .. cd text-ui-test +rm trips.json + java -jar $(find ../build/libs/ -mindepth 1 -print -quit) < input.txt > ACTUAL.TXT cp EXPECTED.TXT EXPECTED-UNIX.TXT diff --git a/tripscorrupted.json b/tripscorrupted.json new file mode 100644 index 0000000000..2e62969f5f --- /dev/null +++ b/tripscorrupted.json @@ -0,0 +1 @@ +[{"dateOfTrip":"2021-12-25","listOfExpenses":{"amountSpent":58.2,"description":"heathrow express","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"transport","date":"2021-12-25","payer":{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},"amountSplit":{"dick":19.400000000000006,"tom":19.4,"harry":19.4},"exchangeRate":1.8529},{"amountSpent":20.0,"description":"Breakfast next to the hotel","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}}],"category":"food","date":"2021-12-26","payer":{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},"amountSplit":{"dick":14.0,"tom":6.0},"exchangeRate":1.8529},{"amountSpent":30.0,"description":"oyster card","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"transport","date":"2021-12-26","payer":{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},"amountSplit":{"dick":10.0,"tom":10.0,"harry":10.0},"exchangeRate":1.8529},{"amountSpent":66.5,"description":"london eye","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"attractions","date":"2021-12-28","payer":{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}},"amountSplit":{"dick":22.17,"tom":22.17,"harry":22.159999999999997},"exchangeRate":1.8529},{"amountSpent":42.0,"description":"afternoon tea","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"food","date":"2021-12-29","payer":{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}},"amountSplit":{"tom":20.0,"harry":22.0},"exchangeRate":1.8529},{"amountSpent":120.2,"description":"airport gift shop","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"souveniers","date":"2021-12-31","payer":{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},"amountSplit":{"dick":45.2,"tom":30.5,"harry":44.5},"exchangeRate":1.8529}],"listOfPersons":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"exchangeRate":1.8529,"foreignCurrency":"GBP","foreignCurrencyFormat":"%.02f","foreignCurrencySymbol":"£","repaymentCurrency":"SGD","repaymentCurrencyFormat":"%.02f","repaymentCurrencySymbol":"$","location":"London"}] \ No newline at end of file diff --git a/tripsempty.json b/tripsempty.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tripsextra.json b/tripsextra.json new file mode 100644 index 0000000000..ebeb567395 --- /dev/null +++ b/tripsextra.json @@ -0,0 +1 @@ +[{"dateOfTrip":"2021-12-25","listOfExpenses":[{"amountSpent":58.2,"description":"heathrow express","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"transport","date":"2021-12-25","payer":{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},"amountSplit":{"dick":19.400000000000006,"tom":19.4,"harry":19.4},"exchangeRate":1.8529},{"amountSpent":20.0,"description":"Breakfast next to the hotel","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}}],"category":"food","date":"2021-12-26","payer":{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},"amountSplit":{"dick":14.0,"tom":6.0},"exchangeRate":1.8529},{"amountSpent":30.0,"description":"oyster card","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"transport","date":"2021-12-26","payer":{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},"amountSplit":{"dick":10.0,"tom":10.0,"harry":10.0},"exchangeRate":1.8529},{"amountSpent":66.5,"description":"london eye","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"attractions","date":"2021-12-28","payer":{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}},"amountSplit":{"dick":22.17,"tom":22.17,"harry":22.159999999999997},"exchangeRate":1.8529},{"amountSpent":42.0,"description":"afternoon tea","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"food","date":"2021-12-29","payer":{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}},"amountSplit":{"tom":20.0,"harry":22.0},"exchangeRate":1.8529},{"amountSpent":120.2,"description":"airport gift shop","personsList":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"category":"souveniers","date":"2021-12-31","payer":{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},"amountSplit":{"dick":45.2,"tom":30.5,"harry":44.5},"exchangeRate":1.8529}],"listOfPersons":[{"name":"tom","moneyOwed":{"dick":29.800000000000004,"tom":108.07,"harry":2.3299999999999983}},{"name":"dick","moneyOwed":{"dick":110.77000000000001,"tom":-29.800000000000004,"harry":7.229999999999997}},{"name":"harry","moneyOwed":{"dick":-7.229999999999997,"tom":-2.3299999999999983,"harry":118.06}}],"exchangeRate":1.8529,"foreignCurrency":"GBP","foreignCurrencyFormat":"%.02f","foreignCurrencySymbol":"£","repaymentCurrency":"SGD","repaymentCurrencyFormat":"%.02f","repaymentCurrencySymbol":"$","location":"London"}] \ No newline at end of file